Expose process event context to strategy runtime
This commit is contained in:
@@ -124,6 +124,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
data: &data,
|
data: &data,
|
||||||
portfolio: &PortfolioState::new(10_000_000.0),
|
portfolio: &PortfolioState::new(10_000_000.0),
|
||||||
open_orders: &[],
|
open_orders: &[],
|
||||||
|
process_events: &[],
|
||||||
|
active_process_event: None,
|
||||||
})?;
|
})?;
|
||||||
eprintln!("DEBUG notes={:?}", decision.notes);
|
eprintln!("DEBUG notes={:?}", decision.notes);
|
||||||
eprintln!("DEBUG diagnostics={:?}", decision.diagnostics);
|
eprintln!("DEBUG diagnostics={:?}", decision.diagnostics);
|
||||||
|
|||||||
@@ -232,31 +232,42 @@ where
|
|||||||
.map(|decision_idx| (decision_idx, execution_dates[decision_idx]));
|
.map(|decision_idx| (decision_idx, execution_dates[decision_idx]));
|
||||||
let (decision_index, decision_date) =
|
let (decision_index, decision_date) =
|
||||||
decision_slot.unwrap_or((execution_idx, execution_date));
|
decision_slot.unwrap_or((execution_idx, execution_date));
|
||||||
|
let mut process_events = Vec::new();
|
||||||
let pre_open_orders = self.broker.open_order_views();
|
let pre_open_orders = self.broker.open_order_views();
|
||||||
let daily_context = StrategyContext {
|
let schedule_rules = self.strategy.schedule_rules();
|
||||||
|
publish_phase_event(
|
||||||
|
&mut self.strategy,
|
||||||
|
&mut self.process_event_bus,
|
||||||
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&pre_open_orders,
|
||||||
|
&mut process_events,
|
||||||
|
execution_date,
|
||||||
|
ProcessEventKind::PreBeforeTrading,
|
||||||
|
"before_trading:pre",
|
||||||
|
)?;
|
||||||
|
self.strategy.before_trading(&StrategyContext {
|
||||||
execution_date,
|
execution_date,
|
||||||
decision_date,
|
decision_date,
|
||||||
decision_index,
|
decision_index,
|
||||||
data: &self.data,
|
data: &self.data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &pre_open_orders,
|
open_orders: &pre_open_orders,
|
||||||
};
|
process_events: &process_events,
|
||||||
let schedule_rules = self.strategy.schedule_rules();
|
active_process_event: None,
|
||||||
let mut process_events = Vec::new();
|
})?;
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&daily_context,
|
|
||||||
&mut process_events,
|
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PreBeforeTrading,
|
decision_date,
|
||||||
"before_trading:pre",
|
decision_index,
|
||||||
)?;
|
&self.data,
|
||||||
self.strategy.before_trading(&daily_context)?;
|
&portfolio,
|
||||||
publish_phase_event(
|
&pre_open_orders,
|
||||||
&mut self.strategy,
|
|
||||||
&mut self.process_event_bus,
|
|
||||||
&daily_context,
|
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::BeforeTrading,
|
ProcessEventKind::BeforeTrading,
|
||||||
@@ -265,7 +276,12 @@ where
|
|||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&daily_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&pre_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PostBeforeTrading,
|
ProcessEventKind::PostBeforeTrading,
|
||||||
@@ -274,7 +290,12 @@ where
|
|||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&daily_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&pre_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PreOpenAuction,
|
ProcessEventKind::PreOpenAuction,
|
||||||
@@ -286,15 +307,33 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
ScheduleStage::OpenAuction,
|
ScheduleStage::OpenAuction,
|
||||||
&schedule_rules,
|
&schedule_rules,
|
||||||
&daily_context,
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&pre_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
)?;
|
)?;
|
||||||
auction_decision.merge_from(self.strategy.open_auction(&daily_context)?);
|
auction_decision.merge_from(self.strategy.open_auction(&StrategyContext {
|
||||||
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
data: &self.data,
|
||||||
|
portfolio: &portfolio,
|
||||||
|
open_orders: &pre_open_orders,
|
||||||
|
process_events: &process_events,
|
||||||
|
active_process_event: None,
|
||||||
|
})?);
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&daily_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&pre_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::OpenAuction,
|
ProcessEventKind::OpenAuction,
|
||||||
@@ -307,25 +346,27 @@ where
|
|||||||
&auction_decision,
|
&auction_decision,
|
||||||
)?;
|
)?;
|
||||||
let post_auction_open_orders = self.broker.open_order_views();
|
let post_auction_open_orders = self.broker.open_order_views();
|
||||||
let post_auction_context = StrategyContext {
|
|
||||||
execution_date,
|
|
||||||
decision_date,
|
|
||||||
decision_index,
|
|
||||||
data: &self.data,
|
|
||||||
portfolio: &portfolio,
|
|
||||||
open_orders: &post_auction_open_orders,
|
|
||||||
};
|
|
||||||
publish_process_events(
|
publish_process_events(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_auction_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_auction_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut report.process_events,
|
&mut report.process_events,
|
||||||
)?;
|
)?;
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_auction_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_auction_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PostOpenAuction,
|
ProcessEventKind::PostOpenAuction,
|
||||||
@@ -335,7 +376,12 @@ where
|
|||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_auction_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_auction_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PreOnDay,
|
ProcessEventKind::PreOnDay,
|
||||||
@@ -351,6 +397,8 @@ where
|
|||||||
data: &self.data,
|
data: &self.data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &on_day_open_orders,
|
open_orders: &on_day_open_orders,
|
||||||
|
process_events: &process_events,
|
||||||
|
active_process_event: None,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.transpose()?
|
.transpose()?
|
||||||
@@ -361,29 +409,23 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
ScheduleStage::OnDay,
|
ScheduleStage::OnDay,
|
||||||
&schedule_rules,
|
&schedule_rules,
|
||||||
&StrategyContext {
|
|
||||||
execution_date,
|
|
||||||
decision_date,
|
decision_date,
|
||||||
decision_index,
|
decision_index,
|
||||||
data: &self.data,
|
&self.data,
|
||||||
portfolio: &portfolio,
|
&portfolio,
|
||||||
open_orders: &on_day_open_orders,
|
&on_day_open_orders,
|
||||||
},
|
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
)?);
|
)?);
|
||||||
let on_day_context = StrategyContext {
|
|
||||||
execution_date,
|
|
||||||
decision_date,
|
|
||||||
decision_index,
|
|
||||||
data: &self.data,
|
|
||||||
portfolio: &portfolio,
|
|
||||||
open_orders: &on_day_open_orders,
|
|
||||||
};
|
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&on_day_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&on_day_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::OnDay,
|
ProcessEventKind::OnDay,
|
||||||
@@ -394,18 +436,15 @@ where
|
|||||||
self.broker
|
self.broker
|
||||||
.execute(execution_date, &mut portfolio, &self.data, &decision)?;
|
.execute(execution_date, &mut portfolio, &self.data, &decision)?;
|
||||||
let post_intraday_open_orders = self.broker.open_order_views();
|
let post_intraday_open_orders = self.broker.open_order_views();
|
||||||
let post_intraday_context = StrategyContext {
|
|
||||||
execution_date,
|
|
||||||
decision_date,
|
|
||||||
decision_index,
|
|
||||||
data: &self.data,
|
|
||||||
portfolio: &portfolio,
|
|
||||||
open_orders: &post_intraday_open_orders,
|
|
||||||
};
|
|
||||||
publish_process_events(
|
publish_process_events(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_intraday_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_intraday_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut intraday_report.process_events,
|
&mut intraday_report.process_events,
|
||||||
)?;
|
)?;
|
||||||
@@ -419,7 +458,12 @@ where
|
|||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_intraday_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_intraday_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PostOnDay,
|
ProcessEventKind::PostOnDay,
|
||||||
@@ -429,28 +473,39 @@ where
|
|||||||
portfolio.update_prices(execution_date, &self.data, PriceField::Close)?;
|
portfolio.update_prices(execution_date, &self.data, PriceField::Close)?;
|
||||||
|
|
||||||
let post_trade_open_orders = self.broker.open_order_views();
|
let post_trade_open_orders = self.broker.open_order_views();
|
||||||
let post_trade_context = StrategyContext {
|
publish_phase_event(
|
||||||
|
&mut self.strategy,
|
||||||
|
&mut self.process_event_bus,
|
||||||
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_trade_open_orders,
|
||||||
|
&mut process_events,
|
||||||
|
execution_date,
|
||||||
|
ProcessEventKind::PreAfterTrading,
|
||||||
|
"after_trading:pre",
|
||||||
|
)?;
|
||||||
|
self.strategy.after_trading(&StrategyContext {
|
||||||
execution_date,
|
execution_date,
|
||||||
decision_date,
|
decision_date,
|
||||||
decision_index,
|
decision_index,
|
||||||
data: &self.data,
|
data: &self.data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &post_trade_open_orders,
|
open_orders: &post_trade_open_orders,
|
||||||
};
|
process_events: &process_events,
|
||||||
|
active_process_event: None,
|
||||||
|
})?;
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_trade_context,
|
|
||||||
&mut process_events,
|
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PreAfterTrading,
|
decision_date,
|
||||||
"after_trading:pre",
|
decision_index,
|
||||||
)?;
|
&self.data,
|
||||||
self.strategy.after_trading(&post_trade_context)?;
|
&portfolio,
|
||||||
publish_phase_event(
|
&post_trade_open_orders,
|
||||||
&mut self.strategy,
|
|
||||||
&mut self.process_event_bus,
|
|
||||||
&post_trade_context,
|
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::AfterTrading,
|
ProcessEventKind::AfterTrading,
|
||||||
@@ -460,7 +515,12 @@ where
|
|||||||
publish_process_events(
|
publish_process_events(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_trade_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_trade_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut close_report.process_events,
|
&mut close_report.process_events,
|
||||||
)?;
|
)?;
|
||||||
@@ -470,18 +530,15 @@ where
|
|||||||
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);
|
||||||
let post_close_open_orders = self.broker.open_order_views();
|
let post_close_open_orders = self.broker.open_order_views();
|
||||||
let post_close_context = StrategyContext {
|
|
||||||
execution_date,
|
|
||||||
decision_date,
|
|
||||||
decision_index,
|
|
||||||
data: &self.data,
|
|
||||||
portfolio: &portfolio,
|
|
||||||
open_orders: &post_close_open_orders,
|
|
||||||
};
|
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_close_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_close_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PostAfterTrading,
|
ProcessEventKind::PostAfterTrading,
|
||||||
@@ -490,17 +547,36 @@ where
|
|||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_close_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_close_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PreSettlement,
|
ProcessEventKind::PreSettlement,
|
||||||
"settlement:pre",
|
"settlement:pre",
|
||||||
)?;
|
)?;
|
||||||
self.strategy.on_settlement(&post_close_context)?;
|
self.strategy.on_settlement(&StrategyContext {
|
||||||
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
data: &self.data,
|
||||||
|
portfolio: &portfolio,
|
||||||
|
open_orders: &post_close_open_orders,
|
||||||
|
process_events: &process_events,
|
||||||
|
active_process_event: None,
|
||||||
|
})?;
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_close_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_close_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::Settlement,
|
ProcessEventKind::Settlement,
|
||||||
@@ -509,7 +585,12 @@ where
|
|||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
&post_close_context,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
&self.data,
|
||||||
|
&portfolio,
|
||||||
|
&post_close_open_orders,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PostSettlement,
|
ProcessEventKind::PostSettlement,
|
||||||
@@ -1005,7 +1086,11 @@ fn collect_scheduled_decisions<S: Strategy>(
|
|||||||
execution_date: NaiveDate,
|
execution_date: NaiveDate,
|
||||||
stage: ScheduleStage,
|
stage: ScheduleStage,
|
||||||
rules: &[ScheduleRule],
|
rules: &[ScheduleRule],
|
||||||
ctx: &StrategyContext<'_>,
|
decision_date: NaiveDate,
|
||||||
|
decision_index: usize,
|
||||||
|
data: &crate::data::DataSet,
|
||||||
|
portfolio: &PortfolioState,
|
||||||
|
open_orders: &[crate::strategy::OpenOrderView],
|
||||||
process_events: &mut Vec<ProcessEvent>,
|
process_events: &mut Vec<ProcessEvent>,
|
||||||
process_event_bus: &mut ProcessEventBus,
|
process_event_bus: &mut ProcessEventBus,
|
||||||
) -> Result<crate::strategy::StrategyDecision, BacktestError> {
|
) -> Result<crate::strategy::StrategyDecision, BacktestError> {
|
||||||
@@ -1014,17 +1099,39 @@ fn collect_scheduled_decisions<S: Strategy>(
|
|||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
strategy,
|
strategy,
|
||||||
process_event_bus,
|
process_event_bus,
|
||||||
ctx,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
data,
|
||||||
|
portfolio,
|
||||||
|
open_orders,
|
||||||
process_events,
|
process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PreScheduled,
|
ProcessEventKind::PreScheduled,
|
||||||
format!("scheduled:{}:{}:pre", rule.name, stage_label(stage)),
|
format!("scheduled:{}:{}:pre", rule.name, stage_label(stage)),
|
||||||
)?;
|
)?;
|
||||||
combined.merge_from(strategy.on_scheduled(ctx, rule)?);
|
combined.merge_from(strategy.on_scheduled(
|
||||||
|
&StrategyContext {
|
||||||
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
data,
|
||||||
|
portfolio,
|
||||||
|
open_orders,
|
||||||
|
process_events: process_events.as_slice(),
|
||||||
|
active_process_event: None,
|
||||||
|
},
|
||||||
|
rule,
|
||||||
|
)?);
|
||||||
publish_phase_event(
|
publish_phase_event(
|
||||||
strategy,
|
strategy,
|
||||||
process_event_bus,
|
process_event_bus,
|
||||||
ctx,
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
data,
|
||||||
|
portfolio,
|
||||||
|
open_orders,
|
||||||
process_events,
|
process_events,
|
||||||
execution_date,
|
execution_date,
|
||||||
ProcessEventKind::PostScheduled,
|
ProcessEventKind::PostScheduled,
|
||||||
@@ -1037,7 +1144,12 @@ fn collect_scheduled_decisions<S: Strategy>(
|
|||||||
fn publish_phase_event<S: Strategy>(
|
fn publish_phase_event<S: Strategy>(
|
||||||
strategy: &mut S,
|
strategy: &mut S,
|
||||||
process_event_bus: &mut ProcessEventBus,
|
process_event_bus: &mut ProcessEventBus,
|
||||||
ctx: &StrategyContext<'_>,
|
execution_date: NaiveDate,
|
||||||
|
decision_date: NaiveDate,
|
||||||
|
decision_index: usize,
|
||||||
|
data: &crate::data::DataSet,
|
||||||
|
portfolio: &PortfolioState,
|
||||||
|
open_orders: &[crate::strategy::OpenOrderView],
|
||||||
events: &mut Vec<ProcessEvent>,
|
events: &mut Vec<ProcessEvent>,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
kind: ProcessEventKind,
|
kind: ProcessEventKind,
|
||||||
@@ -1052,7 +1164,18 @@ fn publish_phase_event<S: Strategy>(
|
|||||||
detail: detail.into(),
|
detail: detail.into(),
|
||||||
};
|
};
|
||||||
process_event_bus.publish(&event);
|
process_event_bus.publish(&event);
|
||||||
strategy.on_process_event(ctx, &event)?;
|
let process_events = events.as_slice();
|
||||||
|
let event_ctx = StrategyContext {
|
||||||
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
data,
|
||||||
|
portfolio,
|
||||||
|
open_orders,
|
||||||
|
process_events,
|
||||||
|
active_process_event: Some(&event),
|
||||||
|
};
|
||||||
|
strategy.on_process_event(&event_ctx, &event)?;
|
||||||
events.push(event);
|
events.push(event);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1060,13 +1183,29 @@ fn publish_phase_event<S: Strategy>(
|
|||||||
fn publish_process_events<S: Strategy>(
|
fn publish_process_events<S: Strategy>(
|
||||||
strategy: &mut S,
|
strategy: &mut S,
|
||||||
process_event_bus: &mut ProcessEventBus,
|
process_event_bus: &mut ProcessEventBus,
|
||||||
ctx: &StrategyContext<'_>,
|
execution_date: NaiveDate,
|
||||||
|
decision_date: NaiveDate,
|
||||||
|
decision_index: usize,
|
||||||
|
data: &crate::data::DataSet,
|
||||||
|
portfolio: &PortfolioState,
|
||||||
|
open_orders: &[crate::strategy::OpenOrderView],
|
||||||
target: &mut Vec<ProcessEvent>,
|
target: &mut Vec<ProcessEvent>,
|
||||||
incoming: &mut Vec<ProcessEvent>,
|
incoming: &mut Vec<ProcessEvent>,
|
||||||
) -> Result<(), BacktestError> {
|
) -> Result<(), BacktestError> {
|
||||||
for event in incoming.drain(..) {
|
for event in incoming.drain(..) {
|
||||||
process_event_bus.publish(&event);
|
process_event_bus.publish(&event);
|
||||||
strategy.on_process_event(ctx, &event)?;
|
let process_events = target.as_slice();
|
||||||
|
let event_ctx = StrategyContext {
|
||||||
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
data,
|
||||||
|
portfolio,
|
||||||
|
open_orders,
|
||||||
|
process_events,
|
||||||
|
active_process_event: Some(&event),
|
||||||
|
};
|
||||||
|
strategy.on_process_event(&event_ctx, &event)?;
|
||||||
target.push(event);
|
target.push(event);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -29,6 +29,15 @@ pub enum OrderSide {
|
|||||||
Sell,
|
Sell,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OrderSide {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Buy => "buy",
|
||||||
|
Self::Sell => "sell",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum OrderStatus {
|
pub enum OrderStatus {
|
||||||
Pending,
|
Pending,
|
||||||
@@ -120,6 +129,38 @@ pub enum ProcessEventKind {
|
|||||||
Trade,
|
Trade,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ProcessEventKind {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::PreBeforeTrading => "pre_before_trading",
|
||||||
|
Self::BeforeTrading => "before_trading",
|
||||||
|
Self::PostBeforeTrading => "post_before_trading",
|
||||||
|
Self::PreOpenAuction => "pre_open_auction",
|
||||||
|
Self::OpenAuction => "open_auction",
|
||||||
|
Self::PostOpenAuction => "post_open_auction",
|
||||||
|
Self::PreScheduled => "pre_scheduled",
|
||||||
|
Self::PostScheduled => "post_scheduled",
|
||||||
|
Self::PreOnDay => "pre_on_day",
|
||||||
|
Self::OnDay => "on_day",
|
||||||
|
Self::PostOnDay => "post_on_day",
|
||||||
|
Self::PreAfterTrading => "pre_after_trading",
|
||||||
|
Self::AfterTrading => "after_trading",
|
||||||
|
Self::PostAfterTrading => "post_after_trading",
|
||||||
|
Self::PreSettlement => "pre_settlement",
|
||||||
|
Self::Settlement => "settlement",
|
||||||
|
Self::PostSettlement => "post_settlement",
|
||||||
|
Self::OrderPendingNew => "order_pending_new",
|
||||||
|
Self::OrderCreationPass => "order_creation_pass",
|
||||||
|
Self::OrderCreationReject => "order_creation_reject",
|
||||||
|
Self::OrderPendingCancel => "order_pending_cancel",
|
||||||
|
Self::OrderCancellationPass => "order_cancellation_pass",
|
||||||
|
Self::OrderCancellationReject => "order_cancellation_reject",
|
||||||
|
Self::OrderUnsolicitedUpdate => "order_unsolicited_update",
|
||||||
|
Self::Trade => "trade",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ProcessEvent {
|
pub struct ProcessEvent {
|
||||||
#[serde(with = "date_format")]
|
#[serde(with = "date_format")]
|
||||||
|
|||||||
@@ -418,6 +418,19 @@ impl PlatformExprStrategy {
|
|||||||
"weekday",
|
"weekday",
|
||||||
"is_month_start",
|
"is_month_start",
|
||||||
"is_month_end",
|
"is_month_end",
|
||||||
|
"has_process_events",
|
||||||
|
"process_event_count",
|
||||||
|
"current_process_kind",
|
||||||
|
"current_process_order_id",
|
||||||
|
"current_process_symbol",
|
||||||
|
"current_process_side",
|
||||||
|
"current_process_detail",
|
||||||
|
"latest_process_kind",
|
||||||
|
"latest_process_order_id",
|
||||||
|
"latest_process_symbol",
|
||||||
|
"latest_process_side",
|
||||||
|
"latest_process_detail",
|
||||||
|
"process_event_counts",
|
||||||
"day_factors",
|
"day_factors",
|
||||||
"symbol",
|
"symbol",
|
||||||
"market_cap",
|
"market_cap",
|
||||||
@@ -1156,6 +1169,53 @@ impl PlatformExprStrategy {
|
|||||||
scope.push("open_buy_qty", ctx.open_buy_quantity() as i64);
|
scope.push("open_buy_qty", ctx.open_buy_quantity() as i64);
|
||||||
scope.push("open_sell_qty", ctx.open_sell_quantity() as i64);
|
scope.push("open_sell_qty", ctx.open_sell_quantity() as i64);
|
||||||
scope.push("latest_open_order_id", ctx.latest_open_order_id() as i64);
|
scope.push("latest_open_order_id", ctx.latest_open_order_id() as i64);
|
||||||
|
scope.push("has_process_events", ctx.has_process_events());
|
||||||
|
scope.push("process_event_count", ctx.process_event_count() as i64);
|
||||||
|
scope.push(
|
||||||
|
"current_process_kind",
|
||||||
|
ctx.current_process_event_kind().to_string(),
|
||||||
|
);
|
||||||
|
scope.push(
|
||||||
|
"current_process_order_id",
|
||||||
|
ctx.current_process_event_order_id() as i64,
|
||||||
|
);
|
||||||
|
scope.push(
|
||||||
|
"current_process_symbol",
|
||||||
|
ctx.current_process_event_symbol().to_string(),
|
||||||
|
);
|
||||||
|
scope.push(
|
||||||
|
"current_process_side",
|
||||||
|
ctx.current_process_event_side().to_string(),
|
||||||
|
);
|
||||||
|
scope.push(
|
||||||
|
"current_process_detail",
|
||||||
|
ctx.current_process_event_detail().to_string(),
|
||||||
|
);
|
||||||
|
scope.push(
|
||||||
|
"latest_process_kind",
|
||||||
|
ctx.latest_process_event_kind().to_string(),
|
||||||
|
);
|
||||||
|
scope.push(
|
||||||
|
"latest_process_order_id",
|
||||||
|
ctx.latest_process_event_order_id() as i64,
|
||||||
|
);
|
||||||
|
scope.push(
|
||||||
|
"latest_process_symbol",
|
||||||
|
ctx.latest_process_event_symbol().to_string(),
|
||||||
|
);
|
||||||
|
scope.push(
|
||||||
|
"latest_process_side",
|
||||||
|
ctx.latest_process_event_side().to_string(),
|
||||||
|
);
|
||||||
|
scope.push(
|
||||||
|
"latest_process_detail",
|
||||||
|
ctx.latest_process_event_detail().to_string(),
|
||||||
|
);
|
||||||
|
let mut process_event_counts = Map::new();
|
||||||
|
for (key, value) in ctx.process_event_counts() {
|
||||||
|
process_event_counts.insert(key.into(), Dynamic::from(value));
|
||||||
|
}
|
||||||
|
scope.push("process_event_counts", process_event_counts.clone());
|
||||||
let mut day_factors = Map::new();
|
let mut day_factors = Map::new();
|
||||||
day_factors.insert("signal_open".into(), Dynamic::from(day.signal_open));
|
day_factors.insert("signal_open".into(), Dynamic::from(day.signal_open));
|
||||||
day_factors.insert("signal_close".into(), Dynamic::from(day.signal_close));
|
day_factors.insert("signal_close".into(), Dynamic::from(day.signal_close));
|
||||||
@@ -1224,6 +1284,58 @@ impl PlatformExprStrategy {
|
|||||||
"latest_open_order_id".into(),
|
"latest_open_order_id".into(),
|
||||||
Dynamic::from(ctx.latest_open_order_id() as i64),
|
Dynamic::from(ctx.latest_open_order_id() as i64),
|
||||||
);
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"has_process_events".into(),
|
||||||
|
Dynamic::from(ctx.has_process_events()),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"process_event_count".into(),
|
||||||
|
Dynamic::from(ctx.process_event_count() as i64),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"current_process_kind".into(),
|
||||||
|
Dynamic::from(ctx.current_process_event_kind().to_string()),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"current_process_order_id".into(),
|
||||||
|
Dynamic::from(ctx.current_process_event_order_id() as i64),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"current_process_symbol".into(),
|
||||||
|
Dynamic::from(ctx.current_process_event_symbol().to_string()),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"current_process_side".into(),
|
||||||
|
Dynamic::from(ctx.current_process_event_side().to_string()),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"current_process_detail".into(),
|
||||||
|
Dynamic::from(ctx.current_process_event_detail().to_string()),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"latest_process_kind".into(),
|
||||||
|
Dynamic::from(ctx.latest_process_event_kind().to_string()),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"latest_process_order_id".into(),
|
||||||
|
Dynamic::from(ctx.latest_process_event_order_id() as i64),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"latest_process_symbol".into(),
|
||||||
|
Dynamic::from(ctx.latest_process_event_symbol().to_string()),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"latest_process_side".into(),
|
||||||
|
Dynamic::from(ctx.latest_process_event_side().to_string()),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"latest_process_detail".into(),
|
||||||
|
Dynamic::from(ctx.latest_process_event_detail().to_string()),
|
||||||
|
);
|
||||||
|
day_factors.insert(
|
||||||
|
"process_event_counts".into(),
|
||||||
|
Dynamic::from(process_event_counts),
|
||||||
|
);
|
||||||
scope.push("day_factors", day_factors);
|
scope.push("day_factors", day_factors);
|
||||||
if let Some(stock) = stock {
|
if let Some(stock) = stock {
|
||||||
let at_upper_limit =
|
let at_upper_limit =
|
||||||
@@ -2965,7 +3077,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||||
Instrument, OpenOrderView, PortfolioState, Strategy, StrategyContext, TradingCalendar,
|
Instrument, OpenOrderView, PortfolioState, ProcessEvent, ProcessEventKind, Strategy,
|
||||||
|
StrategyContext, TradingCalendar,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||||
@@ -3080,6 +3193,8 @@ mod tests {
|
|||||||
data: &data,
|
data: &data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &[],
|
open_orders: &[],
|
||||||
|
process_events: &[],
|
||||||
|
active_process_event: None,
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -3210,6 +3325,8 @@ mod tests {
|
|||||||
data: &data,
|
data: &data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &[],
|
open_orders: &[],
|
||||||
|
process_events: &[],
|
||||||
|
active_process_event: None,
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -3321,6 +3438,8 @@ mod tests {
|
|||||||
data: &data,
|
data: &data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &[],
|
open_orders: &[],
|
||||||
|
process_events: &[],
|
||||||
|
active_process_event: None,
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -3433,6 +3552,8 @@ mod tests {
|
|||||||
data: &data,
|
data: &data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &open_orders,
|
open_orders: &open_orders,
|
||||||
|
process_events: &[],
|
||||||
|
active_process_event: None,
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -3552,6 +3673,8 @@ mod tests {
|
|||||||
data: &data,
|
data: &data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &open_orders,
|
open_orders: &open_orders,
|
||||||
|
process_events: &[],
|
||||||
|
active_process_event: None,
|
||||||
};
|
};
|
||||||
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
cfg.signal_symbol = "000001.SZ".to_string();
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
@@ -3577,4 +3700,111 @@ mod tests {
|
|||||||
other => panic!("unexpected cancel intent: {other:?}"),
|
other => panic!("unexpected cancel intent: {other:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn platform_strategy_exposes_process_event_runtime_fields() {
|
||||||
|
let date = d(2025, 2, 3);
|
||||||
|
let data = DataSet::from_components(
|
||||||
|
vec![Instrument {
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
name: "Ping An Bank".to_string(),
|
||||||
|
board: "SZSE".to_string(),
|
||||||
|
round_lot: 100,
|
||||||
|
listed_at: Some(d(2010, 1, 1)),
|
||||||
|
delisted_at: None,
|
||||||
|
status: "active".to_string(),
|
||||||
|
}],
|
||||||
|
vec![DailyMarketSnapshot {
|
||||||
|
date,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
timestamp: Some("10:18:00".to_string()),
|
||||||
|
day_open: 10.0,
|
||||||
|
open: 10.0,
|
||||||
|
high: 10.2,
|
||||||
|
low: 9.9,
|
||||||
|
close: 10.1,
|
||||||
|
last_price: 10.05,
|
||||||
|
bid1: 10.04,
|
||||||
|
ask1: 10.05,
|
||||||
|
prev_close: 9.95,
|
||||||
|
volume: 1_000_000,
|
||||||
|
tick_volume: 5_000,
|
||||||
|
bid1_volume: 1_000,
|
||||||
|
ask1_volume: 1_000,
|
||||||
|
trading_phase: Some("continuous".to_string()),
|
||||||
|
paused: false,
|
||||||
|
upper_limit: 10.94,
|
||||||
|
lower_limit: 8.96,
|
||||||
|
price_tick: 0.01,
|
||||||
|
}],
|
||||||
|
vec![DailyFactorSnapshot {
|
||||||
|
date,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
market_cap_bn: 12.0,
|
||||||
|
free_float_cap_bn: 10.0,
|
||||||
|
pe_ttm: 8.0,
|
||||||
|
turnover_ratio: Some(22.0),
|
||||||
|
effective_turnover_ratio: Some(18.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: "000852.SH".to_string(),
|
||||||
|
open: 1000.0,
|
||||||
|
close: 1002.0,
|
||||||
|
prev_close: 998.0,
|
||||||
|
volume: 1_000_000,
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.expect("dataset");
|
||||||
|
let portfolio = PortfolioState::new(1_000_000.0);
|
||||||
|
let process_events = vec![ProcessEvent {
|
||||||
|
date,
|
||||||
|
kind: ProcessEventKind::OrderCreationReject,
|
||||||
|
order_id: Some(55),
|
||||||
|
symbol: Some("000001.SZ".to_string()),
|
||||||
|
side: Some(crate::OrderSide::Buy),
|
||||||
|
detail: "open at or above upper limit".to_string(),
|
||||||
|
}];
|
||||||
|
let ctx = StrategyContext {
|
||||||
|
execution_date: date,
|
||||||
|
decision_date: date,
|
||||||
|
decision_index: 0,
|
||||||
|
data: &data,
|
||||||
|
portfolio: &portfolio,
|
||||||
|
open_orders: &[],
|
||||||
|
process_events: &process_events,
|
||||||
|
active_process_event: None,
|
||||||
|
};
|
||||||
|
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
|
cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
|
cfg.rotation_enabled = false;
|
||||||
|
cfg.benchmark_short_ma_days = 1;
|
||||||
|
cfg.benchmark_long_ma_days = 1;
|
||||||
|
cfg.explicit_actions = vec![PlatformTradeAction::Order {
|
||||||
|
kind: PlatformExplicitOrderKind::Value,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
amount_expr: "cash * 0.1".to_string(),
|
||||||
|
limit_price_expr: None,
|
||||||
|
when_expr: Some(
|
||||||
|
"has_process_events && process_event_count == 1 && latest_process_kind == \"order_creation_reject\" && latest_process_order_id == 55 && latest_process_symbol == \"000001.SZ\" && latest_process_side == \"buy\" && process_event_counts[\"order_creation_reject\"] == 1".to_string(),
|
||||||
|
),
|
||||||
|
reason: "process_event_aware_entry".to_string(),
|
||||||
|
}];
|
||||||
|
let mut strategy = PlatformExprStrategy::new(cfg);
|
||||||
|
|
||||||
|
let decision = strategy.on_day(&ctx).expect("platform decision");
|
||||||
|
assert_eq!(decision.order_intents.len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ pub struct StrategyContext<'a> {
|
|||||||
pub data: &'a DataSet,
|
pub data: &'a DataSet,
|
||||||
pub portfolio: &'a PortfolioState,
|
pub portfolio: &'a PortfolioState,
|
||||||
pub open_orders: &'a [OpenOrderView],
|
pub open_orders: &'a [OpenOrderView],
|
||||||
|
pub process_events: &'a [ProcessEvent],
|
||||||
|
pub active_process_event: Option<&'a ProcessEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StrategyContext<'_> {
|
impl StrategyContext<'_> {
|
||||||
@@ -154,6 +156,103 @@ impl StrategyContext<'_> {
|
|||||||
pub fn available_sellable_qty(&self, symbol: &str, raw_sellable_qty: u32) -> u32 {
|
pub fn available_sellable_qty(&self, symbol: &str, raw_sellable_qty: u32) -> u32 {
|
||||||
raw_sellable_qty.saturating_sub(self.symbol_open_sell_quantity(symbol))
|
raw_sellable_qty.saturating_sub(self.symbol_open_sell_quantity(symbol))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_process_events(&self) -> bool {
|
||||||
|
!self.process_events.is_empty() || self.active_process_event.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_event_count(&self) -> usize {
|
||||||
|
self.process_events.len() + usize::from(self.active_process_event.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_event_count_by_kind(&self, kind: crate::events::ProcessEventKind) -> usize {
|
||||||
|
self.process_events
|
||||||
|
.iter()
|
||||||
|
.filter(|event| event.kind == kind)
|
||||||
|
.count()
|
||||||
|
+ usize::from(
|
||||||
|
self.active_process_event
|
||||||
|
.is_some_and(|event| event.kind == kind),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_process_event(&self) -> Option<&ProcessEvent> {
|
||||||
|
self.active_process_event
|
||||||
|
.or_else(|| self.process_events.last())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_process_event_kind(&self) -> &'static str {
|
||||||
|
self.latest_process_event()
|
||||||
|
.map(|event| event.kind.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_process_event_order_id(&self) -> u64 {
|
||||||
|
self.latest_process_event()
|
||||||
|
.and_then(|event| event.order_id)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_process_event_symbol(&self) -> &str {
|
||||||
|
self.latest_process_event()
|
||||||
|
.and_then(|event| event.symbol.as_deref())
|
||||||
|
.unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_process_event_side(&self) -> &'static str {
|
||||||
|
self.latest_process_event()
|
||||||
|
.and_then(|event| event.side.as_ref())
|
||||||
|
.map(OrderSide::as_str)
|
||||||
|
.unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn latest_process_event_detail(&self) -> &str {
|
||||||
|
self.latest_process_event()
|
||||||
|
.map(|event| event.detail.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_process_event_kind(&self) -> &'static str {
|
||||||
|
self.active_process_event
|
||||||
|
.map(|event| event.kind.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_process_event_order_id(&self) -> u64 {
|
||||||
|
self.active_process_event
|
||||||
|
.and_then(|event| event.order_id)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_process_event_symbol(&self) -> &str {
|
||||||
|
self.active_process_event
|
||||||
|
.and_then(|event| event.symbol.as_deref())
|
||||||
|
.unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_process_event_side(&self) -> &'static str {
|
||||||
|
self.active_process_event
|
||||||
|
.and_then(|event| event.side.as_ref())
|
||||||
|
.map(OrderSide::as_str)
|
||||||
|
.unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_process_event_detail(&self) -> &str {
|
||||||
|
self.active_process_event
|
||||||
|
.map(|event| event.detail.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_event_counts(&self) -> BTreeMap<String, i64> {
|
||||||
|
let mut counts = BTreeMap::<String, i64>::new();
|
||||||
|
for event in self.process_events {
|
||||||
|
*counts.entry(event.kind.as_str().to_string()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
if let Some(event) = self.active_process_event {
|
||||||
|
*counts.entry(event.kind.as_str().to_string()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
counts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
|
|||||||
@@ -140,6 +140,9 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
|||||||
ManualField { name: "position_count/max_positions/refresh_rate".to_string(), field_type: "int".to_string(), detail: "仓位计数与调仓周期。".to_string() },
|
ManualField { name: "position_count/max_positions/refresh_rate".to_string(), field_type: "int".to_string(), detail: "仓位计数与调仓周期。".to_string() },
|
||||||
ManualField { name: "has_open_orders/open_order_count/open_buy_order_count/open_sell_order_count".to_string(), field_type: "bool/int".to_string(), detail: "当前阶段挂单簿摘要。".to_string() },
|
ManualField { name: "has_open_orders/open_order_count/open_buy_order_count/open_sell_order_count".to_string(), field_type: "bool/int".to_string(), detail: "当前阶段挂单簿摘要。".to_string() },
|
||||||
ManualField { name: "open_buy_qty/open_sell_qty/latest_open_order_id".to_string(), field_type: "int".to_string(), detail: "当前阶段未成交买卖挂单的剩余数量汇总,以及最近一笔挂单 id。".to_string() },
|
ManualField { name: "open_buy_qty/open_sell_qty/latest_open_order_id".to_string(), field_type: "int".to_string(), detail: "当前阶段未成交买卖挂单的剩余数量汇总,以及最近一笔挂单 id。".to_string() },
|
||||||
|
ManualField { name: "has_process_events/process_event_count/process_event_counts".to_string(), field_type: "bool/int/map".to_string(), detail: "当前阶段可见的过程事件摘要;process_event_counts[\"trade\"] 这类写法可直接读取当天事件计数。".to_string() },
|
||||||
|
ManualField { name: "current_process_kind/current_process_order_id/current_process_symbol/current_process_side/current_process_detail".to_string(), field_type: "string/int".to_string(), detail: "当前正在回调的过程事件上下文;没有活动事件时为空字符串或 0。".to_string() },
|
||||||
|
ManualField { name: "latest_process_kind/latest_process_order_id/latest_process_symbol/latest_process_side/latest_process_detail".to_string(), field_type: "string/int".to_string(), detail: "当前阶段最近一条过程事件的摘要,可用于让 on_day/open_auction 逻辑响应 earlier lifecycle 或订单事件。".to_string() },
|
||||||
ManualField { name: "year/month/quarter/day_of_month/day_of_year/week_of_year/weekday".to_string(), field_type: "int".to_string(), detail: "日期维度字段。".to_string() },
|
ManualField { name: "year/month/quarter/day_of_month/day_of_year/week_of_year/weekday".to_string(), field_type: "int".to_string(), detail: "日期维度字段。".to_string() },
|
||||||
ManualField { name: "is_month_start/is_month_end".to_string(), field_type: "bool".to_string(), detail: "月初/月末标记。".to_string() },
|
ManualField { name: "is_month_start/is_month_end".to_string(), field_type: "bool".to_string(), detail: "月初/月末标记。".to_string() },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ struct ScheduledProbeStrategy {
|
|||||||
process_log: Rc<RefCell<Vec<String>>>,
|
process_log: Rc<RefCell<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ProcessContextProbeStrategy {
|
||||||
|
snapshots: Rc<RefCell<Vec<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
struct LimitCarryStrategy {
|
struct LimitCarryStrategy {
|
||||||
issued: bool,
|
issued: bool,
|
||||||
}
|
}
|
||||||
@@ -197,6 +201,33 @@ impl Strategy for LimitCarryStrategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Strategy for ProcessContextProbeStrategy {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"process-context-probe"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_process_event(
|
||||||
|
&mut self,
|
||||||
|
ctx: &StrategyContext<'_>,
|
||||||
|
_event: &fidc_core::ProcessEvent,
|
||||||
|
) -> Result<(), fidc_core::BacktestError> {
|
||||||
|
self.snapshots.borrow_mut().push(format!(
|
||||||
|
"{}:{}:{}",
|
||||||
|
ctx.current_process_event_kind(),
|
||||||
|
ctx.latest_process_event_kind(),
|
||||||
|
ctx.process_event_count()
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_day(
|
||||||
|
&mut self,
|
||||||
|
_ctx: &StrategyContext<'_>,
|
||||||
|
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||||
|
Ok(StrategyDecision::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn engine_runs_strategy_hooks_in_daily_order() {
|
fn engine_runs_strategy_hooks_in_daily_order() {
|
||||||
let date1 = d(2025, 1, 2);
|
let date1 = d(2025, 1, 2);
|
||||||
@@ -1133,3 +1164,104 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
.any(|item| { item == "PostScheduled:scheduled:first_trading_day_on_day:on_day:post" })
|
.any(|item| { item == "PostScheduled:scheduled:first_trading_day_on_day:on_day:post" })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn engine_exposes_current_process_context_to_strategies() {
|
||||||
|
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 10:18:00".to_string()),
|
||||||
|
day_open: 10.0,
|
||||||
|
open: 10.0,
|
||||||
|
high: 10.1,
|
||||||
|
low: 9.9,
|
||||||
|
close: 10.0,
|
||||||
|
last_price: 10.0,
|
||||||
|
bid1: 10.0,
|
||||||
|
ask1: 10.0,
|
||||||
|
prev_close: 9.9,
|
||||||
|
volume: 100_000,
|
||||||
|
tick_volume: 100_000,
|
||||||
|
bid1_volume: 100_000,
|
||||||
|
ask1_volume: 100_000,
|
||||||
|
trading_phase: Some("continuous".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 snapshots = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let strategy = ProcessContextProbeStrategy {
|
||||||
|
snapshots: snapshots.clone(),
|
||||||
|
};
|
||||||
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks::default(),
|
||||||
|
PriceField::Last,
|
||||||
|
);
|
||||||
|
let mut engine = BacktestEngine::new(
|
||||||
|
data,
|
||||||
|
strategy,
|
||||||
|
broker,
|
||||||
|
BacktestConfig {
|
||||||
|
initial_cash: 100_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::Last,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.run().expect("backtest run");
|
||||||
|
|
||||||
|
let snapshots = snapshots.borrow();
|
||||||
|
assert_eq!(
|
||||||
|
snapshots.first().map(String::as_str),
|
||||||
|
Some("pre_before_trading:pre_before_trading:1")
|
||||||
|
);
|
||||||
|
assert!(snapshots.iter().any(|item| item == "on_day:on_day:8"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ fn strategy_emits_target_weights_and_diagnostics() {
|
|||||||
data: &data,
|
data: &data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &[],
|
open_orders: &[],
|
||||||
|
process_events: &[],
|
||||||
|
active_process_event: None,
|
||||||
})
|
})
|
||||||
.expect("decision");
|
.expect("decision");
|
||||||
|
|
||||||
@@ -64,6 +66,8 @@ fn jq_strategy_emits_same_day_decision() {
|
|||||||
data: &data,
|
data: &data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
open_orders: &[],
|
open_orders: &[],
|
||||||
|
process_events: &[],
|
||||||
|
active_process_event: None,
|
||||||
})
|
})
|
||||||
.expect("jq decision");
|
.expect("jq decision");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user