Add scheduled process hooks to strategy engine

This commit is contained in:
boris
2026-04-23 03:57:10 -07:00
parent 2bbfa35187
commit 6b5112a363
4 changed files with 195 additions and 39 deletions

View File

@@ -220,31 +220,39 @@ where
}; };
let schedule_rules = self.strategy.schedule_rules(); let schedule_rules = self.strategy.schedule_rules();
let mut process_events = Vec::new(); let mut process_events = Vec::new();
push_phase_event( publish_phase_event(
&mut self.strategy,
&daily_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PreBeforeTrading, ProcessEventKind::PreBeforeTrading,
"before_trading:pre", "before_trading:pre",
); )?;
self.strategy.before_trading(&daily_context)?; self.strategy.before_trading(&daily_context)?;
push_phase_event( publish_phase_event(
&mut self.strategy,
&daily_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::BeforeTrading, ProcessEventKind::BeforeTrading,
"before_trading", "before_trading",
); )?;
push_phase_event( publish_phase_event(
&mut self.strategy,
&daily_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PostBeforeTrading, ProcessEventKind::PostBeforeTrading,
"before_trading:post", "before_trading:post",
); )?;
push_phase_event( publish_phase_event(
&mut self.strategy,
&daily_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PreOpenAuction, ProcessEventKind::PreOpenAuction,
"open_auction:pre", "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,
@@ -252,34 +260,53 @@ where
ScheduleStage::OpenAuction, ScheduleStage::OpenAuction,
&schedule_rules, &schedule_rules,
&daily_context, &daily_context,
&mut process_events,
)?; )?;
auction_decision.merge_from(self.strategy.open_auction(&daily_context)?); auction_decision.merge_from(self.strategy.open_auction(&daily_context)?);
push_phase_event( publish_phase_event(
&mut self.strategy,
&daily_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::OpenAuction, ProcessEventKind::OpenAuction,
"open_auction", "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); let post_auction_context = StrategyContext {
push_phase_event( 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, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PostOpenAuction, ProcessEventKind::PostOpenAuction,
"open_auction:post", "open_auction:post",
); )?;
push_phase_event( publish_phase_event(
&mut self.strategy,
&post_auction_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PreOnDay, ProcessEventKind::PreOnDay,
"on_day:pre", "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 {
@@ -305,18 +332,40 @@ where
data: &self.data, data: &self.data,
portfolio: &portfolio, 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, &mut process_events,
execution_date, execution_date,
ProcessEventKind::OnDay, ProcessEventKind::OnDay,
"on_day", "on_day",
); )?;
let mut 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); 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.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
@@ -324,12 +373,14 @@ 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( publish_phase_event(
&mut self.strategy,
&post_intraday_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PostOnDay, ProcessEventKind::PostOnDay,
"on_day:post", "on_day:post",
); )?;
portfolio.update_prices(execution_date, &self.data, PriceField::Close)?; portfolio.update_prices(execution_date, &self.data, PriceField::Close)?;
@@ -340,51 +391,68 @@ where
data: &self.data, data: &self.data,
portfolio: &portfolio, portfolio: &portfolio,
}; };
push_phase_event( publish_phase_event(
&mut self.strategy,
&post_trade_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PreAfterTrading, ProcessEventKind::PreAfterTrading,
"after_trading:pre", "after_trading:pre",
); )?;
self.strategy.after_trading(&post_trade_context)?; self.strategy.after_trading(&post_trade_context)?;
push_phase_event( publish_phase_event(
&mut self.strategy,
&post_trade_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::AfterTrading, ProcessEventKind::AfterTrading,
"after_trading", "after_trading",
); )?;
let mut close_report = self.broker.after_trading(execution_date); 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.order_events.extend(close_report.order_events);
report.fill_events.extend(close_report.fill_events); report.fill_events.extend(close_report.fill_events);
report.position_events.extend(close_report.position_events); report.position_events.extend(close_report.position_events);
report.account_events.extend(close_report.account_events); report.account_events.extend(close_report.account_events);
report.diagnostics.extend(close_report.diagnostics); report.diagnostics.extend(close_report.diagnostics);
push_phase_event( publish_phase_event(
&mut self.strategy,
&post_trade_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PostAfterTrading, ProcessEventKind::PostAfterTrading,
"after_trading:post", "after_trading:post",
); )?;
push_phase_event( publish_phase_event(
&mut self.strategy,
&post_trade_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PreSettlement, ProcessEventKind::PreSettlement,
"settlement:pre", "settlement:pre",
); )?;
self.strategy.on_settlement(&post_trade_context)?; self.strategy.on_settlement(&post_trade_context)?;
push_phase_event( publish_phase_event(
&mut self.strategy,
&post_trade_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::Settlement, ProcessEventKind::Settlement,
"settlement", "settlement",
); )?;
push_phase_event( publish_phase_event(
&mut self.strategy,
&post_trade_context,
&mut process_events, &mut process_events,
execution_date, execution_date,
ProcessEventKind::PostSettlement, ProcessEventKind::PostSettlement,
"settlement:post", "settlement: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();
@@ -876,28 +944,70 @@ fn collect_scheduled_decisions<S: Strategy>(
stage: ScheduleStage, stage: ScheduleStage,
rules: &[ScheduleRule], rules: &[ScheduleRule],
ctx: &StrategyContext<'_>, ctx: &StrategyContext<'_>,
process_events: &mut Vec<ProcessEvent>,
) -> Result<crate::strategy::StrategyDecision, BacktestError> { ) -> Result<crate::strategy::StrategyDecision, BacktestError> {
let mut combined = crate::strategy::StrategyDecision::default(); let mut combined = crate::strategy::StrategyDecision::default();
for rule in scheduler.triggered_rules(execution_date, stage, rules) { 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)?); 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) Ok(combined)
} }
fn push_phase_event( fn publish_phase_event<S: Strategy>(
strategy: &mut S,
ctx: &StrategyContext<'_>,
events: &mut Vec<ProcessEvent>, events: &mut Vec<ProcessEvent>,
date: NaiveDate, date: NaiveDate,
kind: ProcessEventKind, kind: ProcessEventKind,
detail: impl Into<String>, detail: impl Into<String>,
) { ) -> Result<(), BacktestError> {
events.push(ProcessEvent { let event = ProcessEvent {
date, date,
kind, kind,
order_id: None, order_id: None,
symbol: None, symbol: None,
side: None, side: None,
detail: detail.into(), detail: detail.into(),
}); };
strategy.on_process_event(ctx, &event)?;
events.push(event);
Ok(())
}
fn publish_process_events<S: Strategy>(
strategy: &mut S,
ctx: &StrategyContext<'_>,
target: &mut Vec<ProcessEvent>,
incoming: &mut Vec<ProcessEvent>,
) -> 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 { mod date_format {

View File

@@ -99,6 +99,8 @@ pub enum ProcessEventKind {
PreOpenAuction, PreOpenAuction,
OpenAuction, OpenAuction,
PostOpenAuction, PostOpenAuction,
PreScheduled,
PostScheduled,
PreOnDay, PreOnDay,
OnDay, OnDay,
PostOnDay, PostOnDay,

View File

@@ -9,13 +9,20 @@ use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime};
use crate::cost::ChinaAShareCostModel; use crate::cost::ChinaAShareCostModel;
use crate::data::{DataSet, PriceField}; use crate::data::{DataSet, PriceField};
use crate::engine::BacktestError; use crate::engine::BacktestError;
use crate::events::OrderSide; use crate::events::{OrderSide, ProcessEvent};
use crate::portfolio::PortfolioState; use crate::portfolio::PortfolioState;
use crate::scheduler::ScheduleRule; use crate::scheduler::ScheduleRule;
use crate::universe::{DynamicMarketCapBandSelector, SelectionContext, UniverseSelector}; use crate::universe::{DynamicMarketCapBandSelector, SelectionContext, UniverseSelector};
pub trait Strategy { pub trait Strategy {
fn name(&self) -> &str; fn name(&self) -> &str;
fn on_process_event(
&mut self,
_ctx: &StrategyContext<'_>,
_event: &ProcessEvent,
) -> Result<(), BacktestError> {
Ok(())
}
fn schedule_rules(&self) -> Vec<ScheduleRule> { fn schedule_rules(&self) -> Vec<ScheduleRule> {
Vec::new() Vec::new()
} }

View File

@@ -118,6 +118,7 @@ impl Strategy for AuctionOrderStrategy {
struct ScheduledProbeStrategy { struct ScheduledProbeStrategy {
log: Rc<RefCell<Vec<String>>>, log: Rc<RefCell<Vec<String>>>,
process_log: Rc<RefCell<Vec<String>>>,
} }
struct LimitCarryStrategy { struct LimitCarryStrategy {
@@ -129,6 +130,17 @@ impl Strategy for ScheduledProbeStrategy {
"scheduled-probe" "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<ScheduleRule> { fn schedule_rules(&self) -> Vec<ScheduleRule> {
vec![ vec![
ScheduleRule::daily("daily_auction", ScheduleStage::OpenAuction), ScheduleRule::daily("daily_auction", ScheduleStage::OpenAuction),
@@ -826,7 +838,11 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
.expect("dataset"); .expect("dataset");
let log = Rc::new(RefCell::new(Vec::new())); 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( let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(), ChinaAShareCostModel::default(),
ChinaEquityRuleHooks::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", "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" })
);
} }