diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index c4dc457..e9a9869 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -220,31 +220,39 @@ where }; let schedule_rules = self.strategy.schedule_rules(); let mut process_events = Vec::new(); - push_phase_event( + publish_phase_event( + &mut self.strategy, + &daily_context, &mut process_events, execution_date, ProcessEventKind::PreBeforeTrading, "before_trading:pre", - ); + )?; self.strategy.before_trading(&daily_context)?; - push_phase_event( + publish_phase_event( + &mut self.strategy, + &daily_context, &mut process_events, execution_date, ProcessEventKind::BeforeTrading, "before_trading", - ); - push_phase_event( + )?; + publish_phase_event( + &mut self.strategy, + &daily_context, &mut process_events, execution_date, ProcessEventKind::PostBeforeTrading, "before_trading:post", - ); - push_phase_event( + )?; + publish_phase_event( + &mut self.strategy, + &daily_context, &mut process_events, execution_date, ProcessEventKind::PreOpenAuction, "open_auction:pre", - ); + )?; let mut auction_decision = collect_scheduled_decisions( &mut self.strategy, &scheduler, @@ -252,34 +260,53 @@ where ScheduleStage::OpenAuction, &schedule_rules, &daily_context, + &mut process_events, )?; auction_decision.merge_from(self.strategy.open_auction(&daily_context)?); - push_phase_event( + publish_phase_event( + &mut self.strategy, + &daily_context, &mut process_events, execution_date, ProcessEventKind::OpenAuction, "open_auction", - ); + )?; let mut report = self.broker.execute( execution_date, &mut portfolio, &self.data, &auction_decision, )?; - process_events.append(&mut report.process_events); - push_phase_event( + let post_auction_context = StrategyContext { + execution_date, + decision_date, + decision_index, + data: &self.data, + portfolio: &portfolio, + }; + publish_process_events( + &mut self.strategy, + &post_auction_context, + &mut process_events, + &mut report.process_events, + )?; + publish_phase_event( + &mut self.strategy, + &post_auction_context, &mut process_events, execution_date, ProcessEventKind::PostOpenAuction, "open_auction:post", - ); + )?; - push_phase_event( + publish_phase_event( + &mut self.strategy, + &post_auction_context, &mut process_events, execution_date, ProcessEventKind::PreOnDay, "on_day:pre", - ); + )?; let mut decision = decision_slot .map(|(decision_idx, decision_date)| { self.strategy.on_day(&StrategyContext { @@ -305,18 +332,40 @@ where data: &self.data, portfolio: &portfolio, }, + &mut process_events, )?); - push_phase_event( + let on_day_context = StrategyContext { + execution_date, + decision_date, + decision_index, + data: &self.data, + portfolio: &portfolio, + }; + publish_phase_event( + &mut self.strategy, + &on_day_context, &mut process_events, execution_date, ProcessEventKind::OnDay, "on_day", - ); + )?; let mut intraday_report = self.broker .execute(execution_date, &mut portfolio, &self.data, &decision)?; - process_events.append(&mut intraday_report.process_events); + let post_intraday_context = StrategyContext { + execution_date, + decision_date, + decision_index, + data: &self.data, + portfolio: &portfolio, + }; + publish_process_events( + &mut self.strategy, + &post_intraday_context, + &mut process_events, + &mut intraday_report.process_events, + )?; report.order_events.extend(intraday_report.order_events); report.fill_events.extend(intraday_report.fill_events); report @@ -324,12 +373,14 @@ where .extend(intraday_report.position_events); report.account_events.extend(intraday_report.account_events); report.diagnostics.extend(intraday_report.diagnostics); - push_phase_event( + publish_phase_event( + &mut self.strategy, + &post_intraday_context, &mut process_events, execution_date, ProcessEventKind::PostOnDay, "on_day:post", - ); + )?; portfolio.update_prices(execution_date, &self.data, PriceField::Close)?; @@ -340,51 +391,68 @@ where data: &self.data, portfolio: &portfolio, }; - push_phase_event( + publish_phase_event( + &mut self.strategy, + &post_trade_context, &mut process_events, execution_date, ProcessEventKind::PreAfterTrading, "after_trading:pre", - ); + )?; self.strategy.after_trading(&post_trade_context)?; - push_phase_event( + publish_phase_event( + &mut self.strategy, + &post_trade_context, &mut process_events, execution_date, ProcessEventKind::AfterTrading, "after_trading", - ); + )?; let mut close_report = self.broker.after_trading(execution_date); - process_events.append(&mut close_report.process_events); + publish_process_events( + &mut self.strategy, + &post_trade_context, + &mut process_events, + &mut close_report.process_events, + )?; report.order_events.extend(close_report.order_events); report.fill_events.extend(close_report.fill_events); report.position_events.extend(close_report.position_events); report.account_events.extend(close_report.account_events); report.diagnostics.extend(close_report.diagnostics); - push_phase_event( + publish_phase_event( + &mut self.strategy, + &post_trade_context, &mut process_events, execution_date, ProcessEventKind::PostAfterTrading, "after_trading:post", - ); - push_phase_event( + )?; + publish_phase_event( + &mut self.strategy, + &post_trade_context, &mut process_events, execution_date, ProcessEventKind::PreSettlement, "settlement:pre", - ); + )?; self.strategy.on_settlement(&post_trade_context)?; - push_phase_event( + publish_phase_event( + &mut self.strategy, + &post_trade_context, &mut process_events, execution_date, ProcessEventKind::Settlement, "settlement", - ); - push_phase_event( + )?; + publish_phase_event( + &mut self.strategy, + &post_trade_context, &mut process_events, execution_date, ProcessEventKind::PostSettlement, "settlement:post", - ); + )?; let daily_fill_count = report.fill_events.len(); let day_orders = report.order_events.clone(); let day_fills = report.fill_events.clone(); @@ -876,28 +944,70 @@ fn collect_scheduled_decisions( stage: ScheduleStage, rules: &[ScheduleRule], ctx: &StrategyContext<'_>, + process_events: &mut Vec, ) -> Result { let mut combined = crate::strategy::StrategyDecision::default(); for rule in scheduler.triggered_rules(execution_date, stage, rules) { + publish_phase_event( + strategy, + ctx, + process_events, + execution_date, + ProcessEventKind::PreScheduled, + format!("scheduled:{}:{}:pre", rule.name, stage_label(stage)), + )?; combined.merge_from(strategy.on_scheduled(ctx, rule)?); + publish_phase_event( + strategy, + ctx, + process_events, + execution_date, + ProcessEventKind::PostScheduled, + format!("scheduled:{}:{}:post", rule.name, stage_label(stage)), + )?; } Ok(combined) } -fn push_phase_event( +fn publish_phase_event( + strategy: &mut S, + ctx: &StrategyContext<'_>, events: &mut Vec, date: NaiveDate, kind: ProcessEventKind, detail: impl Into, -) { - events.push(ProcessEvent { +) -> Result<(), BacktestError> { + let event = ProcessEvent { date, kind, order_id: None, symbol: None, side: None, detail: detail.into(), - }); + }; + strategy.on_process_event(ctx, &event)?; + events.push(event); + Ok(()) +} + +fn publish_process_events( + strategy: &mut S, + ctx: &StrategyContext<'_>, + target: &mut Vec, + incoming: &mut Vec, +) -> Result<(), BacktestError> { + for event in incoming.drain(..) { + strategy.on_process_event(ctx, &event)?; + target.push(event); + } + Ok(()) +} + +fn stage_label(stage: ScheduleStage) -> &'static str { + match stage { + ScheduleStage::OpenAuction => "open_auction", + ScheduleStage::OnDay => "on_day", + } } mod date_format { diff --git a/crates/fidc-core/src/events.rs b/crates/fidc-core/src/events.rs index 3d9c3ef..f2dc98f 100644 --- a/crates/fidc-core/src/events.rs +++ b/crates/fidc-core/src/events.rs @@ -99,6 +99,8 @@ pub enum ProcessEventKind { PreOpenAuction, OpenAuction, PostOpenAuction, + PreScheduled, + PostScheduled, PreOnDay, OnDay, PostOnDay, diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index c6d05e6..61dda9e 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -9,13 +9,20 @@ use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime}; use crate::cost::ChinaAShareCostModel; use crate::data::{DataSet, PriceField}; use crate::engine::BacktestError; -use crate::events::OrderSide; +use crate::events::{OrderSide, ProcessEvent}; use crate::portfolio::PortfolioState; use crate::scheduler::ScheduleRule; use crate::universe::{DynamicMarketCapBandSelector, SelectionContext, UniverseSelector}; pub trait Strategy { fn name(&self) -> &str; + fn on_process_event( + &mut self, + _ctx: &StrategyContext<'_>, + _event: &ProcessEvent, + ) -> Result<(), BacktestError> { + Ok(()) + } fn schedule_rules(&self) -> Vec { Vec::new() } diff --git a/crates/fidc-core/tests/engine_hooks.rs b/crates/fidc-core/tests/engine_hooks.rs index d011f2c..1dfe6fd 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -118,6 +118,7 @@ impl Strategy for AuctionOrderStrategy { struct ScheduledProbeStrategy { log: Rc>>, + process_log: Rc>>, } struct LimitCarryStrategy { @@ -129,6 +130,17 @@ impl Strategy for ScheduledProbeStrategy { "scheduled-probe" } + fn on_process_event( + &mut self, + _ctx: &StrategyContext<'_>, + event: &fidc_core::ProcessEvent, + ) -> Result<(), fidc_core::BacktestError> { + self.process_log + .borrow_mut() + .push(format!("{:?}:{}", event.kind, event.detail)); + Ok(()) + } + fn schedule_rules(&self) -> Vec { vec![ ScheduleRule::daily("daily_auction", ScheduleStage::OpenAuction), @@ -826,7 +838,11 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() { .expect("dataset"); let log = Rc::new(RefCell::new(Vec::new())); - let strategy = ScheduledProbeStrategy { log: log.clone() }; + let process_log = Rc::new(RefCell::new(Vec::new())); + let strategy = ScheduledProbeStrategy { + log: log.clone(), + process_log: process_log.clone(), + }; let broker = BrokerSimulator::new_with_execution_price( ChinaAShareCostModel::default(), ChinaEquityRuleHooks::default(), @@ -859,4 +875,25 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() { "scheduled:first_trading_day_on_day:2025-02-03", ] ); + let process_log = process_log.borrow(); + assert!( + process_log + .iter() + .any(|item| { item == "PreScheduled:scheduled:daily_auction:open_auction:pre" }) + ); + assert!( + process_log + .iter() + .any(|item| { item == "PostScheduled:scheduled:daily_auction:open_auction:post" }) + ); + assert!( + process_log + .iter() + .any(|item| { item == "PreScheduled:scheduled:friday_on_day:on_day:pre" }) + ); + assert!( + process_log + .iter() + .any(|item| { item == "PostScheduled:scheduled:first_trading_day_on_day:on_day:post" }) + ); }