Expose process event context to strategy runtime
This commit is contained in:
@@ -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,
|
||||
},
|
||||
decision_date,
|
||||
decision_index,
|
||||
&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(())
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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() },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user