diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index bbf82c6..fe5f4f1 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -203,7 +203,13 @@ where portfolio: &portfolio, }; self.strategy.before_trading(&daily_context)?; - self.strategy.open_auction(&daily_context)?; + let auction_decision = self.strategy.open_auction(&daily_context)?; + let mut report = self.broker.execute( + execution_date, + &mut portfolio, + &self.data, + &auction_decision, + )?; let decision = decision_slot .map(|(decision_idx, decision_date)| { @@ -218,9 +224,16 @@ where .transpose()? .unwrap_or_default(); - let report = + let intraday_report = self.broker .execute(execution_date, &mut portfolio, &self.data, &decision)?; + report.order_events.extend(intraday_report.order_events); + report.fill_events.extend(intraday_report.fill_events); + report + .position_events + .extend(intraday_report.position_events); + report.account_events.extend(intraday_report.account_events); + report.diagnostics.extend(intraday_report.diagnostics); let daily_fill_count = report.fill_events.len(); let day_orders = report.order_events.clone(); let day_fills = report.fill_events.clone(); diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index 859e111..fb926ca 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -18,8 +18,11 @@ pub trait Strategy { fn before_trading(&mut self, _ctx: &StrategyContext<'_>) -> Result<(), BacktestError> { Ok(()) } - fn open_auction(&mut self, _ctx: &StrategyContext<'_>) -> Result<(), BacktestError> { - Ok(()) + fn open_auction( + &mut self, + _ctx: &StrategyContext<'_>, + ) -> Result { + Ok(StrategyDecision::default()) } fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result; fn after_trading(&mut self, _ctx: &StrategyContext<'_>) -> Result<(), BacktestError> { diff --git a/crates/fidc-core/tests/engine_hooks.rs b/crates/fidc-core/tests/engine_hooks.rs index e4d525a..562048b 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -32,11 +32,14 @@ impl Strategy for HookProbeStrategy { Ok(()) } - fn open_auction(&mut self, ctx: &StrategyContext<'_>) -> Result<(), fidc_core::BacktestError> { + fn open_auction( + &mut self, + ctx: &StrategyContext<'_>, + ) -> Result { self.log .borrow_mut() .push(format!("auction:{}", ctx.execution_date)); - Ok(()) + Ok(StrategyDecision::default()) } fn on_day( @@ -71,6 +74,47 @@ impl Strategy for HookProbeStrategy { } } +struct AuctionOrderStrategy { + saw_quantity_in_on_day: Rc>>, +} + +impl Strategy for AuctionOrderStrategy { + fn name(&self) -> &str { + "auction-order" + } + + fn open_auction( + &mut self, + _ctx: &StrategyContext<'_>, + ) -> Result { + Ok(StrategyDecision { + rebalance: false, + target_weights: BTreeMap::new(), + exit_symbols: BTreeSet::new(), + order_intents: vec![fidc_core::OrderIntent::Value { + symbol: "000001.SZ".to_string(), + value: 1_000.0, + reason: "auction_buy".to_string(), + }], + notes: Vec::new(), + diagnostics: Vec::new(), + }) + } + + fn on_day( + &mut self, + ctx: &StrategyContext<'_>, + ) -> Result { + let quantity = ctx + .portfolio + .position("000001.SZ") + .map(|position| position.quantity) + .unwrap_or(0); + *self.saw_quantity_in_on_day.borrow_mut() = Some(quantity); + Ok(StrategyDecision::default()) + } +} + #[test] fn engine_runs_strategy_hooks_in_daily_order() { let date1 = d(2025, 1, 2); @@ -239,3 +283,101 @@ fn engine_runs_strategy_hooks_in_daily_order() { ] ); } + +#[test] +fn engine_executes_open_auction_decisions_before_on_day() { + let date = d(2025, 1, 2); + let data = DataSet::from_components( + vec![Instrument { + symbol: "000001.SZ".to_string(), + name: "Anchor".to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: Some(d(2020, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }], + vec![DailyMarketSnapshot { + date, + symbol: "000001.SZ".to_string(), + timestamp: Some("2025-01-02 09:25:00".to_string()), + day_open: 10.0, + open: 10.0, + high: 10.0, + low: 10.0, + close: 10.0, + last_price: 10.0, + bid1: 10.0, + ask1: 10.0, + prev_close: 10.0, + volume: 100_000, + tick_volume: 100_000, + bid1_volume: 100_000, + ask1_volume: 100_000, + trading_phase: Some("open_auction".to_string()), + paused: false, + upper_limit: 11.0, + lower_limit: 9.0, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: "000001.SZ".to_string(), + market_cap_bn: 20.0, + free_float_cap_bn: 18.0, + pe_ttm: 10.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }], + vec![CandidateEligibility { + date, + symbol: "000001.SZ".to_string(), + is_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }], + vec![BenchmarkSnapshot { + date, + benchmark: "000300.SH".to_string(), + open: 100.0, + close: 100.0, + prev_close: 99.0, + volume: 1_000_000, + }], + ) + .expect("dataset"); + + let observed_quantity = Rc::new(RefCell::new(None)); + let strategy = AuctionOrderStrategy { + saw_quantity_in_on_day: observed_quantity.clone(), + }; + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks::default(), + PriceField::DayOpen, + ); + let mut engine = BacktestEngine::new( + data, + strategy, + broker, + BacktestConfig { + initial_cash: 10_000.0, + benchmark_code: "000300.SH".to_string(), + start_date: Some(date), + end_date: Some(date), + decision_lag_trading_days: 0, + execution_price_field: PriceField::DayOpen, + }, + ); + + let result = engine.run().expect("backtest run"); + assert_eq!(*observed_quantity.borrow(), Some(100)); + assert_eq!(result.fills.len(), 1); + assert_eq!(result.fills[0].reason, "auction_buy"); + assert_eq!(result.fills[0].quantity, 100); +}