Add process event stream for backtests

This commit is contained in:
boris
2026-04-23 01:58:40 -07:00
parent e5fe1f0432
commit 23ba74909d
6 changed files with 384 additions and 11 deletions

View File

@@ -5,7 +5,10 @@ use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
use crate::cost::CostModel; use crate::cost::CostModel;
use crate::data::{DataSet, IntradayExecutionQuote, PriceField}; use crate::data::{DataSet, IntradayExecutionQuote, PriceField};
use crate::engine::BacktestError; use crate::engine::BacktestError;
use crate::events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent}; use crate::events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
};
use crate::portfolio::PortfolioState; use crate::portfolio::PortfolioState;
use crate::rules::EquityRuleHooks; use crate::rules::EquityRuleHooks;
use crate::strategy::{OrderIntent, StrategyDecision}; use crate::strategy::{OrderIntent, StrategyDecision};
@@ -16,6 +19,7 @@ pub struct BrokerExecutionReport {
pub fill_events: Vec<FillEvent>, pub fill_events: Vec<FillEvent>,
pub position_events: Vec<PositionEvent>, pub position_events: Vec<PositionEvent>,
pub account_events: Vec<AccountEvent>, pub account_events: Vec<AccountEvent>,
pub process_events: Vec<ProcessEvent>,
pub diagnostics: Vec<String>, pub diagnostics: Vec<String>,
} }
@@ -440,6 +444,25 @@ where
order_id order_id
} }
fn emit_order_process_event(
report: &mut BrokerExecutionReport,
date: NaiveDate,
kind: ProcessEventKind,
order_id: u64,
symbol: &str,
side: OrderSide,
detail: impl Into<String>,
) {
report.process_events.push(ProcessEvent {
date,
kind,
order_id: Some(order_id),
symbol: Some(symbol.to_string()),
side: Some(side),
detail: detail.into(),
});
}
fn target_quantities( fn target_quantities(
&self, &self,
date: NaiveDate, date: NaiveDate,
@@ -881,6 +904,16 @@ where
return Ok(()); return Ok(());
}; };
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderPendingNew,
order_id,
symbol,
OrderSide::Sell,
format!("requested_quantity={requested_qty} reason={reason}"),
);
let rule = self.rules.can_sell( let rule = self.rules.can_sell(
date, date,
snapshot, snapshot,
@@ -889,6 +922,7 @@ where
self.execution_price_field, self.execution_price_field,
); );
if !rule.allowed { if !rule.allowed {
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
let status = match rule.reason.as_deref() { let status = match rule.reason.as_deref() {
Some("paused") Some("paused")
| Some("sell disabled by eligibility flags") | Some("sell disabled by eligibility flags")
@@ -903,11 +937,30 @@ where
requested_quantity: requested_qty, requested_quantity: requested_qty,
filled_quantity: 0, filled_quantity: 0,
status, status,
reason: format!("{reason}: {}", rule.reason.unwrap_or_default()), reason: format!("{reason}: {rule_reason}"),
}); });
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
OrderSide::Sell,
format!("status={status:?} reason={rule_reason}"),
);
return Ok(()); return Ok(());
} }
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderCreationPass,
order_id,
symbol,
OrderSide::Sell,
"sell order passed rule checks",
);
let sellable = position.sellable_qty(date); let sellable = position.sellable_qty(date);
let mut partial_fill_reason = if sellable < requested_qty { let mut partial_fill_reason = if sellable < requested_qty {
Some("sellable quantity limit".to_string()) Some("sellable quantity limit".to_string())
@@ -945,6 +998,18 @@ where
status: zero_fill_status_for_reason(&limit_reason), status: zero_fill_status_for_reason(&limit_reason),
reason: format!("{reason}: {limit_reason}"), reason: format!("{reason}: {limit_reason}"),
}); });
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
OrderSide::Sell,
format!(
"status={:?} reason={limit_reason}",
zero_fill_status_for_reason(&limit_reason)
),
);
return Ok(()); return Ok(());
} }
}; };
@@ -959,6 +1024,15 @@ where
status: OrderStatus::Rejected, status: OrderStatus::Rejected,
reason: format!("{reason}: no sellable quantity"), reason: format!("{reason}: no sellable quantity"),
}); });
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
OrderSide::Sell,
"status=Rejected reason=no sellable quantity",
);
return Ok(()); return Ok(());
} }
@@ -1031,6 +1105,15 @@ where
net_cash_flow: net_cash, net_cash_flow: net_cash,
reason: reason.to_string(), reason: reason.to_string(),
}); });
Self::emit_order_process_event(
report,
date,
ProcessEventKind::Trade,
order_id,
symbol,
OrderSide::Sell,
format!("filled_quantity={} price={}", leg.quantity, leg.price),
);
report.position_events.push(PositionEvent { report.position_events.push(PositionEvent {
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
@@ -1097,6 +1180,17 @@ where
status, status,
reason: order_reason, reason: order_reason,
}); });
if matches!(status, OrderStatus::Canceled | OrderStatus::Rejected) {
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
OrderSide::Sell,
format!("status={status:?} filled_quantity={filled_qty}"),
);
}
Ok(()) Ok(())
} }
@@ -1306,10 +1400,21 @@ where
let snapshot = data.require_market(date, symbol)?; let snapshot = data.require_market(date, symbol)?;
let candidate = data.require_candidate(date, symbol)?; let candidate = data.require_candidate(date, symbol)?;
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderPendingNew,
order_id,
symbol,
OrderSide::Buy,
format!("requested_quantity={requested_qty} reason={reason}"),
);
let rule = self let rule = self
.rules .rules
.can_buy(date, snapshot, candidate, self.execution_price_field); .can_buy(date, snapshot, candidate, self.execution_price_field);
if !rule.allowed { if !rule.allowed {
let rule_reason = rule.reason.as_deref().unwrap_or_default().to_string();
let status = match rule.reason.as_deref() { let status = match rule.reason.as_deref() {
Some("paused") Some("paused")
| Some("buy disabled by eligibility flags") | Some("buy disabled by eligibility flags")
@@ -1324,11 +1429,30 @@ where
requested_quantity: requested_qty, requested_quantity: requested_qty,
filled_quantity: 0, filled_quantity: 0,
status, status,
reason: format!("{reason}: {}", rule.reason.unwrap_or_default()), reason: format!("{reason}: {rule_reason}"),
}); });
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
OrderSide::Buy,
format!("status={status:?} reason={rule_reason}"),
);
return Ok(()); return Ok(());
} }
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderCreationPass,
order_id,
symbol,
OrderSide::Buy,
"buy order passed rule checks",
);
let mut partial_fill_reason = None; let mut partial_fill_reason = None;
let market_limited_qty = self.market_fillable_quantity( let market_limited_qty = self.market_fillable_quantity(
snapshot, snapshot,
@@ -1357,6 +1481,18 @@ where
status: zero_fill_status_for_reason(&limit_reason), status: zero_fill_status_for_reason(&limit_reason),
reason: format!("{reason}: {limit_reason}"), reason: format!("{reason}: {limit_reason}"),
}); });
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
OrderSide::Buy,
format!(
"status={:?} reason={limit_reason}",
zero_fill_status_for_reason(&limit_reason)
),
);
return Ok(()); return Ok(());
} }
}; };
@@ -1432,6 +1568,20 @@ where
.unwrap_or("insufficient cash after fees") .unwrap_or("insufficient cash after fees")
), ),
}); });
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
OrderSide::Buy,
format!(
"status=Rejected reason={}",
partial_fill_reason
.as_deref()
.unwrap_or("insufficient cash after fees")
),
);
return Ok(()); return Ok(());
} }
@@ -1471,6 +1621,15 @@ where
net_cash_flow: -cash_out, net_cash_flow: -cash_out,
reason: reason.to_string(), reason: reason.to_string(),
}); });
Self::emit_order_process_event(
report,
date,
ProcessEventKind::Trade,
order_id,
symbol,
OrderSide::Buy,
format!("filled_quantity={} price={}", leg.quantity, leg.price),
);
report.position_events.push(PositionEvent { report.position_events.push(PositionEvent {
date, date,
symbol: symbol.to_string(), symbol: symbol.to_string(),
@@ -1536,6 +1695,17 @@ where
status, status,
reason: order_reason, reason: order_reason,
}); });
if matches!(status, OrderStatus::Canceled | OrderStatus::Rejected) {
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
OrderSide::Buy,
format!("status={status:?} filled_quantity={filled_qty}"),
);
}
Ok(()) Ok(())
} }

View File

@@ -5,7 +5,10 @@ use thiserror::Error;
use crate::broker::{BrokerExecutionReport, BrokerSimulator}; use crate::broker::{BrokerExecutionReport, BrokerSimulator};
use crate::cost::CostModel; use crate::cost::CostModel;
use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField}; use crate::data::{BenchmarkSnapshot, DataSet, DataSetError, PriceField};
use crate::events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent}; use crate::events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
};
use crate::metrics::{BacktestMetrics, compute_backtest_metrics}; use crate::metrics::{BacktestMetrics, compute_backtest_metrics};
use crate::portfolio::{CashReceivable, HoldingSummary, PortfolioState}; use crate::portfolio::{CashReceivable, HoldingSummary, PortfolioState};
use crate::rules::EquityRuleHooks; use crate::rules::EquityRuleHooks;
@@ -59,6 +62,7 @@ pub struct BacktestResult {
pub fills: Vec<FillEvent>, pub fills: Vec<FillEvent>,
pub position_events: Vec<PositionEvent>, pub position_events: Vec<PositionEvent>,
pub account_events: Vec<AccountEvent>, pub account_events: Vec<AccountEvent>,
pub process_events: Vec<ProcessEvent>,
pub holdings_summary: Vec<HoldingSummary>, pub holdings_summary: Vec<HoldingSummary>,
pub daily_holdings: Vec<HoldingSummary>, pub daily_holdings: Vec<HoldingSummary>,
pub metrics: BacktestMetrics, pub metrics: BacktestMetrics,
@@ -82,6 +86,7 @@ pub struct BacktestDayProgress {
pub orders: Vec<OrderEvent>, pub orders: Vec<OrderEvent>,
pub fills: Vec<FillEvent>, pub fills: Vec<FillEvent>,
pub holdings: Vec<HoldingSummary>, pub holdings: Vec<HoldingSummary>,
pub process_events: Vec<ProcessEvent>,
} }
pub struct BacktestEngine<S, C, R> { pub struct BacktestEngine<S, C, R> {
@@ -166,6 +171,7 @@ where
fills: Vec::new(), fills: Vec::new(),
position_events: Vec::new(), position_events: Vec::new(),
account_events: Vec::new(), account_events: Vec::new(),
process_events: Vec::new(),
equity_curve: Vec::new(), equity_curve: Vec::new(),
holdings_summary: Vec::new(), holdings_summary: Vec::new(),
daily_holdings: Vec::new(), daily_holdings: Vec::new(),
@@ -206,7 +212,32 @@ where
portfolio: &portfolio, portfolio: &portfolio,
}; };
let schedule_rules = self.strategy.schedule_rules(); let schedule_rules = self.strategy.schedule_rules();
let mut process_events = Vec::new();
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PreBeforeTrading,
"before_trading:pre",
);
self.strategy.before_trading(&daily_context)?; self.strategy.before_trading(&daily_context)?;
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::BeforeTrading,
"before_trading",
);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PostBeforeTrading,
"before_trading:post",
);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PreOpenAuction,
"open_auction:pre",
);
let mut auction_decision = collect_scheduled_decisions( let mut auction_decision = collect_scheduled_decisions(
&mut self.strategy, &mut self.strategy,
&scheduler, &scheduler,
@@ -216,13 +247,32 @@ where
&daily_context, &daily_context,
)?; )?;
auction_decision.merge_from(self.strategy.open_auction(&daily_context)?); auction_decision.merge_from(self.strategy.open_auction(&daily_context)?);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::OpenAuction,
"open_auction",
);
let mut report = self.broker.execute( let mut report = self.broker.execute(
execution_date, execution_date,
&mut portfolio, &mut portfolio,
&self.data, &self.data,
&auction_decision, &auction_decision,
)?; )?;
process_events.append(&mut report.process_events);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PostOpenAuction,
"open_auction:post",
);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PreOnDay,
"on_day:pre",
);
let mut decision = decision_slot let mut decision = decision_slot
.map(|(decision_idx, decision_date)| { .map(|(decision_idx, decision_date)| {
self.strategy.on_day(&StrategyContext { self.strategy.on_day(&StrategyContext {
@@ -249,10 +299,17 @@ where
portfolio: &portfolio, portfolio: &portfolio,
}, },
)?); )?);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::OnDay,
"on_day",
);
let intraday_report = let mut intraday_report =
self.broker self.broker
.execute(execution_date, &mut portfolio, &self.data, &decision)?; .execute(execution_date, &mut portfolio, &self.data, &decision)?;
process_events.append(&mut intraday_report.process_events);
report.order_events.extend(intraday_report.order_events); report.order_events.extend(intraday_report.order_events);
report.fill_events.extend(intraday_report.fill_events); report.fill_events.extend(intraday_report.fill_events);
report report
@@ -260,6 +317,12 @@ where
.extend(intraday_report.position_events); .extend(intraday_report.position_events);
report.account_events.extend(intraday_report.account_events); report.account_events.extend(intraday_report.account_events);
report.diagnostics.extend(intraday_report.diagnostics); report.diagnostics.extend(intraday_report.diagnostics);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PostOnDay,
"on_day:post",
);
let daily_fill_count = report.fill_events.len(); let daily_fill_count = report.fill_events.len();
let day_orders = report.order_events.clone(); let day_orders = report.order_events.clone();
let day_fills = report.fill_events.clone(); let day_fills = report.fill_events.clone();
@@ -275,8 +338,44 @@ where
data: &self.data, data: &self.data,
portfolio: &portfolio, portfolio: &portfolio,
}; };
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PreAfterTrading,
"after_trading:pre",
);
self.strategy.after_trading(&post_trade_context)?; self.strategy.after_trading(&post_trade_context)?;
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::AfterTrading,
"after_trading",
);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PostAfterTrading,
"after_trading:post",
);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PreSettlement,
"settlement:pre",
);
self.strategy.on_settlement(&post_trade_context)?; self.strategy.on_settlement(&post_trade_context)?;
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::Settlement,
"settlement",
);
push_phase_event(
&mut process_events,
execution_date,
ProcessEventKind::PostSettlement,
"settlement:post",
);
let benchmark = let benchmark =
self.data self.data
@@ -296,6 +395,7 @@ where
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" | "); .join(" | ");
let holdings_for_day = portfolio.holdings_summary(execution_date); let holdings_for_day = portfolio.holdings_summary(execution_date);
let day_process_events = process_events.clone();
result.equity_curve.push(DailyEquityPoint { result.equity_curve.push(DailyEquityPoint {
date: execution_date, date: execution_date,
@@ -335,7 +435,9 @@ where
orders: day_orders, orders: day_orders,
fills: day_fills, fills: day_fills,
holdings: holdings_for_day, holdings: holdings_for_day,
process_events: day_process_events,
}); });
result.process_events.extend(process_events);
} }
if let Some(last_date) = execution_dates.last().copied() { if let Some(last_date) = execution_dates.last().copied() {
@@ -676,6 +778,22 @@ fn collect_scheduled_decisions<S: Strategy>(
Ok(combined) Ok(combined)
} }
fn push_phase_event(
events: &mut Vec<ProcessEvent>,
date: NaiveDate,
kind: ProcessEventKind,
detail: impl Into<String>,
) {
events.push(ProcessEvent {
date,
kind,
order_id: None,
symbol: None,
side: None,
detail: detail.into(),
});
}
mod date_format { mod date_format {
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::Serializer; use serde::Serializer;

View File

@@ -89,3 +89,40 @@ pub struct AccountEvent {
pub total_equity: f64, pub total_equity: f64,
pub note: String, pub note: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ProcessEventKind {
PreBeforeTrading,
BeforeTrading,
PostBeforeTrading,
PreOpenAuction,
OpenAuction,
PostOpenAuction,
PreOnDay,
OnDay,
PostOnDay,
PreAfterTrading,
AfterTrading,
PostAfterTrading,
PreSettlement,
Settlement,
PostSettlement,
OrderPendingNew,
OrderCreationPass,
OrderUnsolicitedUpdate,
Trade,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessEvent {
#[serde(with = "date_format")]
pub date: NaiveDate,
pub kind: ProcessEventKind,
#[serde(default)]
pub order_id: Option<u64>,
#[serde(default)]
pub symbol: Option<String>,
#[serde(default)]
pub side: Option<OrderSide>,
pub detail: String,
}

View File

@@ -26,7 +26,10 @@ pub use engine::{
BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError, BacktestResult, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError, BacktestResult,
DailyEquityPoint, DailyEquityPoint,
}; };
pub use events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent}; pub use events::{
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
ProcessEventKind,
};
pub use instrument::Instrument; pub use instrument::Instrument;
pub use metrics::{BacktestMetrics, compute_backtest_metrics}; pub use metrics::{BacktestMetrics, compute_backtest_metrics};
pub use platform_expr_strategy::{PlatformExprStrategy, PlatformExprStrategyConfig}; pub use platform_expr_strategy::{PlatformExprStrategy, PlatformExprStrategyConfig};

View File

@@ -6,8 +6,8 @@ use chrono::NaiveDate;
use fidc_core::{ use fidc_core::{
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
Instrument, PriceField, ScheduleRule, ScheduleStage, Strategy, StrategyContext, Instrument, PriceField, ProcessEventKind, ScheduleRule, ScheduleStage, Strategy,
StrategyDecision, StrategyContext, StrategyDecision,
}; };
fn d(year: i32, month: u32, day: u32) -> NaiveDate { fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -302,7 +302,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
}, },
); );
engine.run().expect("backtest succeeds"); let result = engine.run().expect("backtest succeeds");
assert_eq!( assert_eq!(
log.borrow().as_slice(), log.borrow().as_slice(),
@@ -319,6 +319,30 @@ fn engine_runs_strategy_hooks_in_daily_order() {
"settlement:2025-01-03", "settlement:2025-01-03",
] ]
); );
assert_eq!(result.process_events.len(), 30);
assert_eq!(
result.process_events[..15]
.iter()
.map(|event| &event.kind)
.collect::<Vec<_>>(),
vec![
&ProcessEventKind::PreBeforeTrading,
&ProcessEventKind::BeforeTrading,
&ProcessEventKind::PostBeforeTrading,
&ProcessEventKind::PreOpenAuction,
&ProcessEventKind::OpenAuction,
&ProcessEventKind::PostOpenAuction,
&ProcessEventKind::PreOnDay,
&ProcessEventKind::OnDay,
&ProcessEventKind::PostOnDay,
&ProcessEventKind::PreAfterTrading,
&ProcessEventKind::AfterTrading,
&ProcessEventKind::PostAfterTrading,
&ProcessEventKind::PreSettlement,
&ProcessEventKind::Settlement,
&ProcessEventKind::PostSettlement,
]
);
} }
#[test] #[test]

View File

@@ -2,8 +2,8 @@ use chrono::NaiveDate;
use fidc_core::{ use fidc_core::{
BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel,
ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, Instrument, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, Instrument,
IntradayExecutionQuote, OrderIntent, PortfolioState, PriceField, SlippageModel, IntradayExecutionQuote, OrderIntent, PortfolioState, PriceField, ProcessEventKind,
StrategyDecision, SlippageModel, StrategyDecision,
}; };
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
@@ -416,6 +416,11 @@ fn broker_cancels_buy_when_open_hits_upper_limit() {
.reason .reason
.contains("open at or above upper limit") .contains("open at or above upper limit")
); );
assert!(report.process_events.iter().any(|event| {
event.kind == ProcessEventKind::OrderUnsolicitedUpdate
&& event.symbol.as_deref() == Some("000002.SZ")
&& event.side == Some(fidc_core::OrderSide::Buy)
}));
} }
#[test] #[test]
@@ -989,6 +994,22 @@ fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() {
.iter() .iter()
.any(|item| item.contains("order_split_fill symbol=000002.SZ side=buy")) .any(|item| item.contains("order_split_fill symbol=000002.SZ side=buy"))
); );
assert_eq!(
report
.process_events
.iter()
.filter(|event| event.kind == ProcessEventKind::Trade)
.count(),
2
);
assert!(report.process_events.iter().any(|event| {
event.kind == ProcessEventKind::OrderPendingNew
&& event.symbol.as_deref() == Some("000002.SZ")
}));
assert!(report.process_events.iter().any(|event| {
event.kind == ProcessEventKind::OrderCreationPass
&& event.symbol.as_deref() == Some("000002.SZ")
}));
} }
#[test] #[test]