Expose process event context to strategy runtime

This commit is contained in:
boris
2026-04-23 05:12:07 -07:00
parent 4d43d1b176
commit 2857f72d84
8 changed files with 742 additions and 92 deletions

View File

@@ -124,6 +124,8 @@ fn main() -> Result<(), Box<dyn Error>> {
data: &data,
portfolio: &PortfolioState::new(10_000_000.0),
open_orders: &[],
process_events: &[],
active_process_event: None,
})?;
eprintln!("DEBUG notes={:?}", decision.notes);
eprintln!("DEBUG diagnostics={:?}", decision.diagnostics);

View File

@@ -232,31 +232,42 @@ where
.map(|decision_idx| (decision_idx, execution_dates[decision_idx]));
let (decision_index, decision_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 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,
decision_date,
decision_index,
data: &self.data,
portfolio: &portfolio,
open_orders: &pre_open_orders,
};
let schedule_rules = self.strategy.schedule_rules();
let mut process_events = Vec::new();
process_events: &process_events,
active_process_event: None,
})?;
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&daily_context,
&mut process_events,
execution_date,
ProcessEventKind::PreBeforeTrading,
"before_trading:pre",
)?;
self.strategy.before_trading(&daily_context)?;
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&daily_context,
decision_date,
decision_index,
&self.data,
&portfolio,
&pre_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::BeforeTrading,
@@ -265,7 +276,12 @@ where
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&daily_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&pre_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::PostBeforeTrading,
@@ -274,7 +290,12 @@ where
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&daily_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&pre_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::PreOpenAuction,
@@ -286,15 +307,33 @@ where
execution_date,
ScheduleStage::OpenAuction,
&schedule_rules,
&daily_context,
decision_date,
decision_index,
&self.data,
&portfolio,
&pre_open_orders,
&mut process_events,
&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(
&mut self.strategy,
&mut self.process_event_bus,
&daily_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&pre_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::OpenAuction,
@@ -307,25 +346,27 @@ where
&auction_decision,
)?;
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(
&mut self.strategy,
&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 report.process_events,
)?;
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&post_auction_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&post_auction_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::PostOpenAuction,
@@ -335,7 +376,12 @@ where
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&post_auction_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&post_auction_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::PreOnDay,
@@ -351,6 +397,8 @@ where
data: &self.data,
portfolio: &portfolio,
open_orders: &on_day_open_orders,
process_events: &process_events,
active_process_event: None,
})
})
.transpose()?
@@ -361,29 +409,23 @@ where
execution_date,
ScheduleStage::OnDay,
&schedule_rules,
&StrategyContext {
execution_date,
decision_date,
decision_index,
data: &self.data,
portfolio: &portfolio,
open_orders: &on_day_open_orders,
},
&self.data,
&portfolio,
&on_day_open_orders,
&mut process_events,
&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(
&mut self.strategy,
&mut self.process_event_bus,
&on_day_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&on_day_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::OnDay,
@@ -394,18 +436,15 @@ where
self.broker
.execute(execution_date, &mut portfolio, &self.data, &decision)?;
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(
&mut self.strategy,
&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 intraday_report.process_events,
)?;
@@ -419,7 +458,12 @@ where
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&post_intraday_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&post_intraday_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::PostOnDay,
@@ -429,28 +473,39 @@ where
portfolio.update_prices(execution_date, &self.data, PriceField::Close)?;
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,
decision_date,
decision_index,
data: &self.data,
portfolio: &portfolio,
open_orders: &post_trade_open_orders,
};
process_events: &process_events,
active_process_event: None,
})?;
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&post_trade_context,
&mut process_events,
execution_date,
ProcessEventKind::PreAfterTrading,
"after_trading:pre",
)?;
self.strategy.after_trading(&post_trade_context)?;
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&post_trade_context,
decision_date,
decision_index,
&self.data,
&portfolio,
&post_trade_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::AfterTrading,
@@ -460,7 +515,12 @@ where
publish_process_events(
&mut self.strategy,
&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 close_report.process_events,
)?;
@@ -470,18 +530,15 @@ where
report.account_events.extend(close_report.account_events);
report.diagnostics.extend(close_report.diagnostics);
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(
&mut self.strategy,
&mut self.process_event_bus,
&post_close_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&post_close_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::PostAfterTrading,
@@ -490,17 +547,36 @@ where
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&post_close_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&post_close_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::PreSettlement,
"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(
&mut self.strategy,
&mut self.process_event_bus,
&post_close_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&post_close_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::Settlement,
@@ -509,7 +585,12 @@ where
publish_phase_event(
&mut self.strategy,
&mut self.process_event_bus,
&post_close_context,
execution_date,
decision_date,
decision_index,
&self.data,
&portfolio,
&post_close_open_orders,
&mut process_events,
execution_date,
ProcessEventKind::PostSettlement,
@@ -1005,7 +1086,11 @@ fn collect_scheduled_decisions<S: Strategy>(
execution_date: NaiveDate,
stage: ScheduleStage,
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_event_bus: &mut ProcessEventBus,
) -> Result<crate::strategy::StrategyDecision, BacktestError> {
@@ -1014,17 +1099,39 @@ fn collect_scheduled_decisions<S: Strategy>(
publish_phase_event(
strategy,
process_event_bus,
ctx,
execution_date,
decision_date,
decision_index,
data,
portfolio,
open_orders,
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(
&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(
strategy,
process_event_bus,
ctx,
execution_date,
decision_date,
decision_index,
data,
portfolio,
open_orders,
process_events,
execution_date,
ProcessEventKind::PostScheduled,
@@ -1037,7 +1144,12 @@ fn collect_scheduled_decisions<S: Strategy>(
fn publish_phase_event<S: Strategy>(
strategy: &mut S,
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>,
date: NaiveDate,
kind: ProcessEventKind,
@@ -1052,7 +1164,18 @@ fn publish_phase_event<S: Strategy>(
detail: detail.into(),
};
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);
Ok(())
}
@@ -1060,13 +1183,29 @@ fn publish_phase_event<S: Strategy>(
fn publish_process_events<S: Strategy>(
strategy: &mut S,
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>,
incoming: &mut Vec<ProcessEvent>,
) -> Result<(), BacktestError> {
for event in incoming.drain(..) {
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);
}
Ok(())

View File

@@ -29,6 +29,15 @@ pub enum OrderSide {
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)]
pub enum OrderStatus {
Pending,
@@ -120,6 +129,38 @@ pub enum ProcessEventKind {
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)]
pub struct ProcessEvent {
#[serde(with = "date_format")]

View File

@@ -418,6 +418,19 @@ impl PlatformExprStrategy {
"weekday",
"is_month_start",
"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",
"symbol",
"market_cap",
@@ -1156,6 +1169,53 @@ impl PlatformExprStrategy {
scope.push("open_buy_qty", ctx.open_buy_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("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();
day_factors.insert("signal_open".into(), Dynamic::from(day.signal_open));
day_factors.insert("signal_close".into(), Dynamic::from(day.signal_close));
@@ -1224,6 +1284,58 @@ impl PlatformExprStrategy {
"latest_open_order_id".into(),
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);
if let Some(stock) = stock {
let at_upper_limit =
@@ -2965,7 +3077,8 @@ mod tests {
};
use crate::{
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 {
@@ -3080,6 +3193,8 @@ mod tests {
data: &data,
portfolio: &portfolio,
open_orders: &[],
process_events: &[],
active_process_event: None,
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.signal_symbol = "000001.SZ".to_string();
@@ -3210,6 +3325,8 @@ mod tests {
data: &data,
portfolio: &portfolio,
open_orders: &[],
process_events: &[],
active_process_event: None,
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.signal_symbol = "000001.SZ".to_string();
@@ -3321,6 +3438,8 @@ mod tests {
data: &data,
portfolio: &portfolio,
open_orders: &[],
process_events: &[],
active_process_event: None,
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.signal_symbol = "000001.SZ".to_string();
@@ -3433,6 +3552,8 @@ mod tests {
data: &data,
portfolio: &portfolio,
open_orders: &open_orders,
process_events: &[],
active_process_event: None,
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.signal_symbol = "000001.SZ".to_string();
@@ -3552,6 +3673,8 @@ mod tests {
data: &data,
portfolio: &portfolio,
open_orders: &open_orders,
process_events: &[],
active_process_event: None,
};
let mut cfg = PlatformExprStrategyConfig::microcap_rotation();
cfg.signal_symbol = "000001.SZ".to_string();
@@ -3577,4 +3700,111 @@ mod tests {
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);
}
}

View File

@@ -70,6 +70,8 @@ pub struct StrategyContext<'a> {
pub data: &'a DataSet,
pub portfolio: &'a PortfolioState,
pub open_orders: &'a [OpenOrderView],
pub process_events: &'a [ProcessEvent],
pub active_process_event: Option<&'a ProcessEvent>,
}
impl StrategyContext<'_> {
@@ -154,6 +156,103 @@ impl StrategyContext<'_> {
pub fn available_sellable_qty(&self, symbol: &str, raw_sellable_qty: u32) -> u32 {
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)]

View File

@@ -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: "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: "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: "is_month_start/is_month_end".to_string(), field_type: "bool".to_string(), detail: "月初/月末标记。".to_string() },
],

View File

@@ -121,6 +121,10 @@ struct ScheduledProbeStrategy {
process_log: Rc<RefCell<Vec<String>>>,
}
struct ProcessContextProbeStrategy {
snapshots: Rc<RefCell<Vec<String>>>,
}
struct LimitCarryStrategy {
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]
fn engine_runs_strategy_hooks_in_daily_order() {
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" })
);
}
#[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"));
}

View File

@@ -26,6 +26,8 @@ fn strategy_emits_target_weights_and_diagnostics() {
data: &data,
portfolio: &portfolio,
open_orders: &[],
process_events: &[],
active_process_event: None,
})
.expect("decision");
@@ -64,6 +66,8 @@ fn jq_strategy_emits_same_day_decision() {
data: &data,
portfolio: &portfolio,
open_orders: &[],
process_events: &[],
active_process_event: None,
})
.expect("jq decision");