From 85cfdca14ce6c86d829abaf8e53503e81a456c46 Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 25 Aug 2026 08:34:06 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=BC=95=E6=93=8E=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E4=BA=8B=E4=BB=B6=E9=87=8D=E5=A4=8D=E5=A4=8D=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/engine.rs | 247 ++++++++++++++++++++++----------- 1 file changed, 169 insertions(+), 78 deletions(-) diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 48d1255..56750f4 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -319,6 +319,7 @@ pub struct BacktestDayProgress { pub total_return: f64, pub benchmark_close: f64, pub daily_fill_count: usize, + pub daily_order_count: usize, pub cumulative_trade_count: usize, pub holding_count: usize, pub notes: String, @@ -1716,11 +1717,20 @@ where } pub fn run(&mut self) -> Result { - self.run_with_progress(|_| {}) + self.run_with_progress_options(false, false, |_| {}) } - pub fn run_with_progress( + pub fn run_with_progress(&mut self, on_progress: F) -> Result + where + F: FnMut(&BacktestDayProgress), + { + self.run_with_progress_options(true, true, on_progress) + } + + pub fn run_with_progress_options( &mut self, + include_progress_details: bool, + include_progress_diagnostics: bool, mut on_progress: F, ) -> Result where @@ -1866,6 +1876,8 @@ where execution_date, execution_date, ); + let day_order_start = result.order_events.len(); + let day_fill_start = result.fills.len(); let decision_slot = decision_slots.get(execution_idx).copied().flatten(); let Some((decision_index, decision_date)) = decision_slot else { @@ -1884,13 +1896,13 @@ where let futures_expiration_report = self.settle_futures_expirations(execution_date); merge_broker_report(&mut report, futures_expiration_report); - let daily_fill_count = report.fill_events.len(); - let day_orders = report.order_events.clone(); - let day_fills = report.fill_events.clone(); - let broker_diagnostics = report.diagnostics.clone(); - let execution_risk_decisions = risk_decisions_from_order_events(&day_orders); + let broker_diagnostics = std::mem::take(&mut report.diagnostics); + let execution_risk_decisions = + risk_decisions_from_order_events(&report.order_events); self.extend_result(&mut result, report, execution_date, execution_date); result.risk_decisions.extend(execution_risk_decisions); + let daily_fill_count = result.fills.len() - day_fill_start; + let daily_order_count = result.order_events.len() - day_order_start; let benchmark = self.data @@ -1907,7 +1919,11 @@ where .collect::>() .join(" | "); let holdings_for_day = portfolio.holdings_summary(execution_date); - let day_process_events = process_events.clone(); + let holding_start = result.daily_holdings.len(); + let holding_count = holdings_for_day.len(); + result.daily_holdings.extend(holdings_for_day); + let progress_process_start = result.process_events.len(); + result.process_events.append(&mut process_events); let aggregate_cash = self.aggregate_cash(&portfolio); let aggregate_market_value = self.aggregate_market_value(&portfolio); let aggregate_total_equity = self.aggregate_total_equity(&portfolio); @@ -1928,7 +1944,6 @@ where notes, diagnostics, }); - result.daily_holdings.extend(holdings_for_day.clone()); let latest = result .equity_curve .last() @@ -1943,16 +1958,28 @@ where total_return: latest.unit_nav - 1.0, benchmark_close: latest.benchmark_close, daily_fill_count, + daily_order_count, cumulative_trade_count: result.fills.len(), - holding_count: holdings_for_day.len(), - notes: latest.notes.clone(), - diagnostics: latest.diagnostics.clone(), - orders: day_orders, - fills: day_fills, - holdings: holdings_for_day, - process_events: day_process_events, + holding_count, + notes: include_progress_diagnostics + .then(|| latest.notes.clone()) + .unwrap_or_default(), + diagnostics: include_progress_diagnostics + .then(|| latest.diagnostics.clone()) + .unwrap_or_default(), + orders: include_progress_details + .then(|| result.order_events[day_order_start..].to_vec()) + .unwrap_or_default(), + fills: include_progress_details + .then(|| result.fills[day_fill_start..].to_vec()) + .unwrap_or_default(), + holdings: include_progress_details + .then(|| result.daily_holdings[holding_start..].to_vec()) + .unwrap_or_default(), + process_events: include_progress_details + .then(|| result.process_events[progress_process_start..].to_vec()) + .unwrap_or_default(), }); - result.process_events.append(&mut process_events); stock_equity_by_date.insert(execution_date, portfolio.total_equity()); continue; }; @@ -2683,18 +2710,13 @@ where )?; let post_trade_open_orders = self.open_order_views(); - let visible_order_events = result - .order_events - .iter() - .cloned() - .chain(report.order_events.iter().cloned()) - .collect::>(); - let visible_fills = result - .fills - .iter() - .cloned() - .chain(report.fill_events.iter().cloned()) - .collect::>(); + let mut broker_diagnostics = std::mem::take(&mut report.diagnostics); + self.extend_result( + &mut result, + std::mem::take(&mut report), + decision_date, + execution_date, + ); publish_phase_event( &mut self.strategy, &mut self.process_event_bus, @@ -2728,8 +2750,8 @@ where decision_date, default_stage_time(ScheduleStage::AfterTrading), ), - order_events: visible_order_events.as_slice(), - fills: visible_fills.as_slice(), + order_events: result.order_events.as_slice(), + fills: result.fills.as_slice(), })?; publish_phase_event( &mut self.strategy, @@ -2765,8 +2787,8 @@ where &mut process_events, &mut self.process_event_bus, default_stage_time(ScheduleStage::AfterTrading), - visible_order_events.as_slice(), - visible_fills.as_slice(), + result.order_events.as_slice(), + result.fills.as_slice(), )?; self.apply_strategy_directives( execution_date, @@ -2800,18 +2822,13 @@ where report.account_events.extend(close_report.account_events); report.diagnostics.extend(close_report.diagnostics); let post_close_open_orders = self.open_order_views(); - let visible_order_events_after_close = result - .order_events - .iter() - .cloned() - .chain(report.order_events.iter().cloned()) - .collect::>(); - let visible_fills_after_close = result - .fills - .iter() - .cloned() - .chain(report.fill_events.iter().cloned()) - .collect::>(); + broker_diagnostics.append(&mut report.diagnostics); + self.extend_result( + &mut result, + std::mem::take(&mut report), + decision_date, + execution_date, + ); publish_phase_event( &mut self.strategy, &mut self.process_event_bus, @@ -2862,8 +2879,8 @@ where decision_date, default_stage_time(ScheduleStage::Settlement), ), - order_events: visible_order_events_after_close.as_slice(), - fills: visible_fills_after_close.as_slice(), + order_events: result.order_events.as_slice(), + fills: result.fills.as_slice(), })?; publish_phase_event( &mut self.strategy, @@ -2899,8 +2916,8 @@ where &mut process_events, &mut self.process_event_bus, default_stage_time(ScheduleStage::Settlement), - visible_order_events_after_close.as_slice(), - visible_fills_after_close.as_slice(), + result.order_events.as_slice(), + result.fills.as_slice(), )?; self.apply_strategy_directives( execution_date, @@ -2927,8 +2944,8 @@ where dynamic_universe_snapshot.as_ref(), &subscriptions_snapshot, &mut process_events, - visible_order_events_after_close.as_slice(), - visible_fills_after_close.as_slice(), + result.order_events.as_slice(), + result.fills.as_slice(), )?; merge_broker_report(&mut directive_report, management_fee_report); publish_phase_event( @@ -2949,12 +2966,17 @@ where "settlement:post", )?; merge_broker_report(&mut report, directive_report); - let daily_fill_count = report.fill_events.len(); - let day_orders = report.order_events.clone(); - let day_fills = report.fill_events.clone(); - let broker_diagnostics = report.diagnostics.clone(); - let execution_risk_decisions = risk_decisions_from_order_events(&day_orders); - self.extend_result(&mut result, report, decision_date, execution_date); + broker_diagnostics.append(&mut report.diagnostics); + self.extend_result( + &mut result, + std::mem::take(&mut report), + decision_date, + execution_date, + ); + let daily_fill_count = result.fills.len() - day_fill_start; + let daily_order_count = result.order_events.len() - day_order_start; + let execution_risk_decisions = + risk_decisions_from_order_events(&result.order_events[day_order_start..]); result.risk_decisions.extend(decision.risk_decisions); result.risk_decisions.extend(execution_risk_decisions); @@ -2976,7 +2998,11 @@ where .collect::>() .join(" | "); let holdings_for_day = portfolio.holdings_summary(execution_date); - let day_process_events = process_events.clone(); + let holding_start = result.daily_holdings.len(); + let holding_count = holdings_for_day.len(); + result.daily_holdings.extend(holdings_for_day); + let progress_process_start = result.process_events.len(); + result.process_events.append(&mut process_events); let aggregate_cash = self.aggregate_cash(&portfolio); let aggregate_market_value = self.aggregate_market_value(&portfolio); let aggregate_total_equity = self.aggregate_total_equity(&portfolio); @@ -2997,7 +3023,6 @@ where notes, diagnostics, }); - result.daily_holdings.extend(holdings_for_day.clone()); let latest = result .equity_curve .last() @@ -3012,16 +3037,28 @@ where total_return: latest.unit_nav - 1.0, benchmark_close: latest.benchmark_close, daily_fill_count, + daily_order_count, cumulative_trade_count: result.fills.len(), - holding_count: holdings_for_day.len(), - notes: latest.notes.clone(), - diagnostics: latest.diagnostics.clone(), - orders: day_orders, - fills: day_fills, - holdings: holdings_for_day, - process_events: day_process_events, + holding_count, + notes: include_progress_diagnostics + .then(|| latest.notes.clone()) + .unwrap_or_default(), + diagnostics: include_progress_diagnostics + .then(|| latest.diagnostics.clone()) + .unwrap_or_default(), + orders: include_progress_details + .then(|| result.order_events[day_order_start..].to_vec()) + .unwrap_or_default(), + fills: include_progress_details + .then(|| result.fills[day_fill_start..].to_vec()) + .unwrap_or_default(), + holdings: include_progress_details + .then(|| result.daily_holdings[holding_start..].to_vec()) + .unwrap_or_default(), + process_events: include_progress_details + .then(|| result.process_events[progress_process_start..].to_vec()) + .unwrap_or_default(), }); - result.process_events.extend(process_events); stock_equity_by_date.insert(execution_date, portfolio.total_equity()); } @@ -3045,16 +3082,13 @@ where mut report: BrokerExecutionReport, decision_date: NaiveDate, execution_date: NaiveDate, - ) -> BrokerExecutionReport { + ) { annotate_broker_report_dates(&mut report, decision_date, decision_date, execution_date); - result.order_events.extend(report.order_events.clone()); - result.fills.extend(report.fill_events.clone()); - result - .position_events - .extend(report.position_events.clone()); - result.account_events.extend(report.account_events.clone()); - result.process_events.extend(report.process_events.clone()); - report + result.order_events.append(&mut report.order_events); + result.fills.append(&mut report.fill_events); + result.position_events.append(&mut report.position_events); + result.account_events.append(&mut report.account_events); + result.process_events.append(&mut report.process_events); } fn apply_corporate_actions( @@ -4872,11 +4906,12 @@ mod tests { .expect("dataset") } - fn run_with_matching( + fn engine_with_matching( matching_type: MatchingType, execution_price_field: PriceField, decision_lag_trading_days: usize, - ) -> super::BacktestResult { + ) -> BacktestEngine + { let first = d(2025, 1, 2); let broker = BrokerSimulator::new_with_execution_price( ChinaAShareCostModel::default(), @@ -4904,10 +4939,66 @@ mod tests { broker, config, ) + } + + fn run_with_matching( + matching_type: MatchingType, + execution_price_field: PriceField, + decision_lag_trading_days: usize, + ) -> super::BacktestResult { + engine_with_matching( + matching_type, + execution_price_field, + decision_lag_trading_days, + ) .run() .expect("backtest run") } + #[test] + fn compact_progress_keeps_counts_without_event_payload_clones() { + let mut engine = engine_with_matching(MatchingType::CurrentBarClose, PriceField::Close, 0); + let mut progress = Vec::new(); + let result = engine + .run_with_progress_options(false, false, |event| { + progress.push(( + event.daily_order_count, + event.daily_fill_count, + event.orders.len(), + event.fills.len(), + event.holdings.len(), + event.process_events.len(), + event.notes.len(), + event.diagnostics.len(), + )); + }) + .expect("compact progress run"); + assert!(!progress.is_empty()); + assert!( + progress + .iter() + .any(|(orders, fills, ..)| *orders > 0 && *fills > 0) + ); + assert!(progress.iter().all( + |(_, _, orders, fills, holdings, process_events, notes, diagnostics)| { + *orders == 0 + && *fills == 0 + && *holdings == 0 + && *process_events == 0 + && *notes == 0 + && *diagnostics == 0 + } + )); + assert_eq!( + progress.iter().map(|value| value.0).sum::(), + result.order_events.len() + ); + assert_eq!( + progress.iter().map(|value| value.1).sum::(), + result.fills.len() + ); + } + fn full_day_coverage_engine( data: DataSet, date: NaiveDate,