支持任意交易阶段调度时间

This commit is contained in:
boris
2026-08-25 09:32:34 +08:00
parent 5ff8ddca92
commit c9ddff46dd
3 changed files with 125 additions and 42 deletions
+70 -12
View File
@@ -2043,7 +2043,7 @@ where
ProcessEventKind::BeforeTrading,
"before_trading",
)?;
let mut before_trading_decision = collect_scheduled_decisions(
let mut before_trading_decision = collect_scheduled_decisions_for_stage(
&mut self.strategy,
&scheduler,
execution_date,
@@ -2059,7 +2059,6 @@ where
&self.subscriptions,
&mut process_events,
&mut self.process_event_bus,
default_stage_time(ScheduleStage::BeforeTrading),
result.order_events.as_slice(),
result.fills.as_slice(),
)?;
@@ -2107,7 +2106,7 @@ where
ProcessEventKind::PreOpenAuction,
"open_auction:pre",
)?;
let mut auction_decision = collect_scheduled_decisions(
let mut auction_decision = collect_scheduled_decisions_for_stage(
&mut self.strategy,
&scheduler,
execution_date,
@@ -2123,7 +2122,6 @@ where
&self.subscriptions,
&mut process_events,
&mut self.process_event_bus,
default_stage_time(ScheduleStage::OpenAuction),
result.order_events.as_slice(),
result.fills.as_slice(),
)?;
@@ -2300,7 +2298,7 @@ where
})
.transpose()?
.unwrap_or_default();
decision.merge_from(collect_scheduled_decisions(
decision.merge_from(collect_scheduled_decisions_for_stage(
&mut self.strategy,
&scheduler,
execution_date,
@@ -2316,7 +2314,6 @@ where
&self.subscriptions,
&mut process_events,
&mut self.process_event_bus,
default_stage_time(ScheduleStage::OnDay),
result.order_events.as_slice(),
result.fills.as_slice(),
)?);
@@ -2355,7 +2352,7 @@ where
ProcessEventKind::PreBar,
"bar:pre",
)?;
decision.merge_from(collect_scheduled_decisions(
decision.merge_from(collect_scheduled_decisions_for_stage(
&mut self.strategy,
&scheduler,
execution_date,
@@ -2371,7 +2368,6 @@ where
&self.subscriptions,
&mut process_events,
&mut self.process_event_bus,
default_stage_time(ScheduleStage::Bar),
result.order_events.as_slice(),
result.fills.as_slice(),
)?);
@@ -2770,7 +2766,7 @@ where
ProcessEventKind::AfterTrading,
"after_trading",
)?;
let mut after_trading_decision = collect_scheduled_decisions(
let mut after_trading_decision = collect_scheduled_decisions_for_stage(
&mut self.strategy,
&scheduler,
execution_date,
@@ -2786,7 +2782,6 @@ where
&self.subscriptions,
&mut process_events,
&mut self.process_event_bus,
default_stage_time(ScheduleStage::AfterTrading),
result.order_events.as_slice(),
result.fills.as_slice(),
)?;
@@ -2899,7 +2894,7 @@ where
ProcessEventKind::Settlement,
"settlement",
)?;
let mut settlement_decision = collect_scheduled_decisions(
let mut settlement_decision = collect_scheduled_decisions_for_stage(
&mut self.strategy,
&scheduler,
execution_date,
@@ -2915,7 +2910,6 @@ where
&self.subscriptions,
&mut process_events,
&mut self.process_event_bus,
default_stage_time(ScheduleStage::Settlement),
result.order_events.as_slice(),
result.fills.as_slice(),
)?;
@@ -3845,6 +3839,70 @@ fn collect_scheduled_decisions<S: Strategy>(
Ok(combined)
}
fn collect_scheduled_decisions_for_stage<S: Strategy>(
strategy: &mut S,
scheduler: &Scheduler<'_>,
execution_date: NaiveDate,
stage: ScheduleStage,
rules: &[ScheduleRule],
decision_date: NaiveDate,
decision_index: usize,
data: &crate::data::DataSet,
portfolio: &PortfolioState,
futures_account: Option<&FuturesAccountState>,
open_orders: &[crate::strategy::OpenOrderView],
dynamic_universe: Option<&BTreeSet<String>>,
subscriptions: &BTreeSet<String>,
process_events: &mut Vec<ProcessEvent>,
process_event_bus: &mut ProcessEventBus,
order_events: &[OrderEvent],
fills: &[FillEvent],
) -> Result<crate::strategy::StrategyDecision, BacktestError> {
let mut times = BTreeSet::new();
for rule in rules.iter().filter(|rule| rule.stage == stage) {
let time = match rule.time_rule.as_ref() {
Some(crate::scheduler::ScheduleTimeRule::MinuteOfDay(value)) => {
let hour = value / 60;
let minute = value % 60;
Some(NaiveTime::from_hms_opt(hour, minute, 0).ok_or_else(|| {
BacktestError::Execution(format!(
"invalid schedule minute-of-day {} for rule {}",
value, rule.name
))
})?)
}
Some(crate::scheduler::ScheduleTimeRule::BeforeTrading) | None => {
default_stage_time(stage)
}
};
times.insert(time);
}
let mut combined = crate::strategy::StrategyDecision::default();
for time in times {
combined.merge_from(collect_scheduled_decisions(
strategy,
scheduler,
execution_date,
stage,
rules,
decision_date,
decision_index,
data,
portfolio,
futures_account,
open_orders,
dynamic_universe,
subscriptions,
process_events,
process_event_bus,
time,
order_events,
fills,
)?);
}
Ok(combined)
}
fn publish_phase_event<S: Strategy>(
strategy: &mut S,
process_event_bus: &mut ProcessEventBus,
+50 -30
View File
@@ -8532,24 +8532,6 @@ impl PlatformExprStrategy {
})
}
fn explicit_actions_active(
&self,
calendar: &crate::calendar::TradingCalendar,
date: NaiveDate,
) -> bool {
let stage = match self.config.explicit_action_stage {
PlatformExplicitActionStage::OpenAuction => ScheduleStage::OpenAuction,
PlatformExplicitActionStage::OnDay => ScheduleStage::OnDay,
PlatformExplicitActionStage::Minute => ScheduleStage::Minute,
};
self.config
.explicit_action_schedule
.as_ref()
.is_none_or(|schedule| {
schedule.matches(calendar, date, stage, default_stage_time(stage))
})
}
fn stock_passes_expr(
&self,
ctx: &StrategyContext<'_>,
@@ -10148,13 +10130,18 @@ impl Strategy for PlatformExprStrategy {
}
fn schedule_rules(&self) -> Vec<ScheduleRule> {
if self.config.explicit_action_stage != PlatformExplicitActionStage::Minute {
if self.config.explicit_actions.is_empty() {
return Vec::new();
}
let stage = match self.config.explicit_action_stage {
PlatformExplicitActionStage::OpenAuction => ScheduleStage::OpenAuction,
PlatformExplicitActionStage::OnDay => ScheduleStage::OnDay,
PlatformExplicitActionStage::Minute => ScheduleStage::Minute,
};
self.config
.explicit_action_schedule
.as_ref()
.map(|schedule| schedule.as_schedule_rule(ScheduleStage::Minute))
.map(|schedule| schedule.as_schedule_rule(stage))
.into_iter()
.collect()
}
@@ -10164,9 +10151,7 @@ impl Strategy for PlatformExprStrategy {
ctx: &StrategyContext<'_>,
_rule: &ScheduleRule,
) -> Result<StrategyDecision, BacktestError> {
if self.config.explicit_action_stage == PlatformExplicitActionStage::Minute
&& !self.config.explicit_actions.is_empty()
{
if !self.config.explicit_actions.is_empty() {
return self.explicit_action_decision(ctx);
}
Ok(StrategyDecision::default())
@@ -10210,7 +10195,7 @@ impl Strategy for PlatformExprStrategy {
) -> Result<StrategyDecision, BacktestError> {
if self.config.explicit_action_stage == PlatformExplicitActionStage::OpenAuction
&& !self.config.explicit_actions.is_empty()
&& self.explicit_actions_active(ctx.data.calendar(), ctx.decision_date)
&& self.config.explicit_action_schedule.is_none()
{
return self.explicit_action_decision(ctx);
}
@@ -10264,7 +10249,7 @@ impl Strategy for PlatformExprStrategy {
self.selection_dates(ctx);
let (explicit_action_intents, mut explicit_action_diagnostics) = if !in_skip_window
&& self.config.explicit_action_stage == PlatformExplicitActionStage::OnDay
&& self.explicit_actions_active(ctx.data.calendar(), signal_date)
&& self.config.explicit_action_schedule.is_none()
{
self.explicit_action_intents(ctx, decision_date, &day)?
} else {
@@ -11841,8 +11826,8 @@ mod tests {
FactorTextValue, FuturesCommissionType, FuturesTradingParameter, Instrument,
IntradayExecutionQuote, MatchingType, OpenOrderView, OrderIntent, OrderSide,
PortfolioState, ProcessEvent, ProcessEventKind, RebalanceCashMode, ScheduleStage,
ScheduleTimeRule, SlippageModel, Strategy, StrategyContext, TargetPortfolioOrderPricing,
TradingCalendar, default_stage_time,
ScheduleTimeRule, Scheduler, SlippageModel, Strategy, StrategyContext,
TargetPortfolioOrderPricing, TradingCalendar, default_stage_time,
};
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -31286,7 +31271,25 @@ mod tests {
}];
let mut strategy = PlatformExprStrategy::new(cfg);
let auction_decision = strategy.open_auction(&ctx).expect("auction decision");
assert!(
strategy
.open_auction(&ctx)
.expect("direct auction decision")
.order_intents
.is_empty()
);
let rules = strategy.schedule_rules();
let scheduler = Scheduler::new(ctx.data.calendar());
let triggered = scheduler.triggered_rules_at(
date,
ScheduleStage::OpenAuction,
default_stage_time(ScheduleStage::OpenAuction),
&rules,
);
assert_eq!(triggered.len(), 1);
let auction_decision = strategy
.on_scheduled(&ctx, triggered[0])
.expect("scheduled auction decision");
assert_eq!(auction_decision.order_intents.len(), 1);
assert!(
auction_decision
@@ -31411,8 +31414,25 @@ mod tests {
}];
let mut strategy = PlatformExprStrategy::new(cfg);
let auction_decision = strategy.open_auction(&ctx).expect("auction decision");
assert!(auction_decision.order_intents.is_empty());
assert!(
strategy
.open_auction(&ctx)
.expect("direct auction decision")
.order_intents
.is_empty()
);
let rules = strategy.schedule_rules();
let scheduler = Scheduler::new(ctx.data.calendar());
assert!(
scheduler
.triggered_rules_at(
date,
ScheduleStage::OpenAuction,
default_stage_time(ScheduleStage::OpenAuction),
&rules,
)
.is_empty()
);
}
#[test]
+5
View File
@@ -678,6 +678,8 @@ impl Strategy for ScheduledProbeStrategy {
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
ScheduleRule::monthly("first_trading_day_on_day", 1, ScheduleStage::OnDay)
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
ScheduleRule::daily("daily_on_day_close", ScheduleStage::OnDay)
.with_time_rule(ScheduleTimeRule::physical_time(15, 0)),
]
}
@@ -3217,12 +3219,15 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
"scheduled:daily_before_trading:2025-01-30",
"scheduled:daily_market_open:2025-01-30",
"scheduled:first_trading_day_on_day:2025-01-30",
"scheduled:daily_on_day_close:2025-01-30",
"scheduled:daily_before_trading:2025-01-31",
"scheduled:daily_market_open:2025-01-31",
"scheduled:friday_on_day:2025-01-31",
"scheduled:daily_on_day_close:2025-01-31",
"scheduled:daily_before_trading:2025-02-03",
"scheduled:daily_market_open:2025-02-03",
"scheduled:first_trading_day_on_day:2025-02-03",
"scheduled:daily_on_day_close:2025-02-03",
]
);
let process_log = process_log.borrow();