支持任意交易阶段调度时间
This commit is contained in:
@@ -2043,7 +2043,7 @@ where
|
|||||||
ProcessEventKind::BeforeTrading,
|
ProcessEventKind::BeforeTrading,
|
||||||
"before_trading",
|
"before_trading",
|
||||||
)?;
|
)?;
|
||||||
let mut before_trading_decision = collect_scheduled_decisions(
|
let mut before_trading_decision = collect_scheduled_decisions_for_stage(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&scheduler,
|
&scheduler,
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -2059,7 +2059,6 @@ where
|
|||||||
&self.subscriptions,
|
&self.subscriptions,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::BeforeTrading),
|
|
||||||
result.order_events.as_slice(),
|
result.order_events.as_slice(),
|
||||||
result.fills.as_slice(),
|
result.fills.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
@@ -2107,7 +2106,7 @@ where
|
|||||||
ProcessEventKind::PreOpenAuction,
|
ProcessEventKind::PreOpenAuction,
|
||||||
"open_auction:pre",
|
"open_auction:pre",
|
||||||
)?;
|
)?;
|
||||||
let mut auction_decision = collect_scheduled_decisions(
|
let mut auction_decision = collect_scheduled_decisions_for_stage(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&scheduler,
|
&scheduler,
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -2123,7 +2122,6 @@ where
|
|||||||
&self.subscriptions,
|
&self.subscriptions,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::OpenAuction),
|
|
||||||
result.order_events.as_slice(),
|
result.order_events.as_slice(),
|
||||||
result.fills.as_slice(),
|
result.fills.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
@@ -2300,7 +2298,7 @@ where
|
|||||||
})
|
})
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
decision.merge_from(collect_scheduled_decisions(
|
decision.merge_from(collect_scheduled_decisions_for_stage(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&scheduler,
|
&scheduler,
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -2316,7 +2314,6 @@ where
|
|||||||
&self.subscriptions,
|
&self.subscriptions,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::OnDay),
|
|
||||||
result.order_events.as_slice(),
|
result.order_events.as_slice(),
|
||||||
result.fills.as_slice(),
|
result.fills.as_slice(),
|
||||||
)?);
|
)?);
|
||||||
@@ -2355,7 +2352,7 @@ where
|
|||||||
ProcessEventKind::PreBar,
|
ProcessEventKind::PreBar,
|
||||||
"bar:pre",
|
"bar:pre",
|
||||||
)?;
|
)?;
|
||||||
decision.merge_from(collect_scheduled_decisions(
|
decision.merge_from(collect_scheduled_decisions_for_stage(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&scheduler,
|
&scheduler,
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -2371,7 +2368,6 @@ where
|
|||||||
&self.subscriptions,
|
&self.subscriptions,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::Bar),
|
|
||||||
result.order_events.as_slice(),
|
result.order_events.as_slice(),
|
||||||
result.fills.as_slice(),
|
result.fills.as_slice(),
|
||||||
)?);
|
)?);
|
||||||
@@ -2770,7 +2766,7 @@ where
|
|||||||
ProcessEventKind::AfterTrading,
|
ProcessEventKind::AfterTrading,
|
||||||
"after_trading",
|
"after_trading",
|
||||||
)?;
|
)?;
|
||||||
let mut after_trading_decision = collect_scheduled_decisions(
|
let mut after_trading_decision = collect_scheduled_decisions_for_stage(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&scheduler,
|
&scheduler,
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -2786,7 +2782,6 @@ where
|
|||||||
&self.subscriptions,
|
&self.subscriptions,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::AfterTrading),
|
|
||||||
result.order_events.as_slice(),
|
result.order_events.as_slice(),
|
||||||
result.fills.as_slice(),
|
result.fills.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
@@ -2899,7 +2894,7 @@ where
|
|||||||
ProcessEventKind::Settlement,
|
ProcessEventKind::Settlement,
|
||||||
"settlement",
|
"settlement",
|
||||||
)?;
|
)?;
|
||||||
let mut settlement_decision = collect_scheduled_decisions(
|
let mut settlement_decision = collect_scheduled_decisions_for_stage(
|
||||||
&mut self.strategy,
|
&mut self.strategy,
|
||||||
&scheduler,
|
&scheduler,
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -2915,7 +2910,6 @@ where
|
|||||||
&self.subscriptions,
|
&self.subscriptions,
|
||||||
&mut process_events,
|
&mut process_events,
|
||||||
&mut self.process_event_bus,
|
&mut self.process_event_bus,
|
||||||
default_stage_time(ScheduleStage::Settlement),
|
|
||||||
result.order_events.as_slice(),
|
result.order_events.as_slice(),
|
||||||
result.fills.as_slice(),
|
result.fills.as_slice(),
|
||||||
)?;
|
)?;
|
||||||
@@ -3845,6 +3839,70 @@ fn collect_scheduled_decisions<S: Strategy>(
|
|||||||
Ok(combined)
|
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>(
|
fn publish_phase_event<S: Strategy>(
|
||||||
strategy: &mut S,
|
strategy: &mut S,
|
||||||
process_event_bus: &mut ProcessEventBus,
|
process_event_bus: &mut ProcessEventBus,
|
||||||
|
|||||||
@@ -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(
|
fn stock_passes_expr(
|
||||||
&self,
|
&self,
|
||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
@@ -10148,13 +10130,18 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn schedule_rules(&self) -> Vec<ScheduleRule> {
|
fn schedule_rules(&self) -> Vec<ScheduleRule> {
|
||||||
if self.config.explicit_action_stage != PlatformExplicitActionStage::Minute {
|
if self.config.explicit_actions.is_empty() {
|
||||||
return Vec::new();
|
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
|
self.config
|
||||||
.explicit_action_schedule
|
.explicit_action_schedule
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|schedule| schedule.as_schedule_rule(ScheduleStage::Minute))
|
.map(|schedule| schedule.as_schedule_rule(stage))
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -10164,9 +10151,7 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
_rule: &ScheduleRule,
|
_rule: &ScheduleRule,
|
||||||
) -> Result<StrategyDecision, BacktestError> {
|
) -> Result<StrategyDecision, BacktestError> {
|
||||||
if self.config.explicit_action_stage == PlatformExplicitActionStage::Minute
|
if !self.config.explicit_actions.is_empty() {
|
||||||
&& !self.config.explicit_actions.is_empty()
|
|
||||||
{
|
|
||||||
return self.explicit_action_decision(ctx);
|
return self.explicit_action_decision(ctx);
|
||||||
}
|
}
|
||||||
Ok(StrategyDecision::default())
|
Ok(StrategyDecision::default())
|
||||||
@@ -10210,7 +10195,7 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
) -> Result<StrategyDecision, BacktestError> {
|
) -> Result<StrategyDecision, BacktestError> {
|
||||||
if self.config.explicit_action_stage == PlatformExplicitActionStage::OpenAuction
|
if self.config.explicit_action_stage == PlatformExplicitActionStage::OpenAuction
|
||||||
&& !self.config.explicit_actions.is_empty()
|
&& !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);
|
return self.explicit_action_decision(ctx);
|
||||||
}
|
}
|
||||||
@@ -10264,7 +10249,7 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
self.selection_dates(ctx);
|
self.selection_dates(ctx);
|
||||||
let (explicit_action_intents, mut explicit_action_diagnostics) = if !in_skip_window
|
let (explicit_action_intents, mut explicit_action_diagnostics) = if !in_skip_window
|
||||||
&& self.config.explicit_action_stage == PlatformExplicitActionStage::OnDay
|
&& 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)?
|
self.explicit_action_intents(ctx, decision_date, &day)?
|
||||||
} else {
|
} else {
|
||||||
@@ -11841,8 +11826,8 @@ mod tests {
|
|||||||
FactorTextValue, FuturesCommissionType, FuturesTradingParameter, Instrument,
|
FactorTextValue, FuturesCommissionType, FuturesTradingParameter, Instrument,
|
||||||
IntradayExecutionQuote, MatchingType, OpenOrderView, OrderIntent, OrderSide,
|
IntradayExecutionQuote, MatchingType, OpenOrderView, OrderIntent, OrderSide,
|
||||||
PortfolioState, ProcessEvent, ProcessEventKind, RebalanceCashMode, ScheduleStage,
|
PortfolioState, ProcessEvent, ProcessEventKind, RebalanceCashMode, ScheduleStage,
|
||||||
ScheduleTimeRule, SlippageModel, Strategy, StrategyContext, TargetPortfolioOrderPricing,
|
ScheduleTimeRule, Scheduler, SlippageModel, Strategy, StrategyContext,
|
||||||
TradingCalendar, default_stage_time,
|
TargetPortfolioOrderPricing, TradingCalendar, default_stage_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||||
@@ -31286,7 +31271,25 @@ mod tests {
|
|||||||
}];
|
}];
|
||||||
let mut strategy = PlatformExprStrategy::new(cfg);
|
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_eq!(auction_decision.order_intents.len(), 1);
|
||||||
assert!(
|
assert!(
|
||||||
auction_decision
|
auction_decision
|
||||||
@@ -31411,8 +31414,25 @@ mod tests {
|
|||||||
}];
|
}];
|
||||||
let mut strategy = PlatformExprStrategy::new(cfg);
|
let mut strategy = PlatformExprStrategy::new(cfg);
|
||||||
|
|
||||||
let auction_decision = strategy.open_auction(&ctx).expect("auction decision");
|
assert!(
|
||||||
assert!(auction_decision.order_intents.is_empty());
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -678,6 +678,8 @@ impl Strategy for ScheduledProbeStrategy {
|
|||||||
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
|
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
|
||||||
ScheduleRule::monthly("first_trading_day_on_day", 1, ScheduleStage::OnDay)
|
ScheduleRule::monthly("first_trading_day_on_day", 1, ScheduleStage::OnDay)
|
||||||
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
|
.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_before_trading:2025-01-30",
|
||||||
"scheduled:daily_market_open:2025-01-30",
|
"scheduled:daily_market_open:2025-01-30",
|
||||||
"scheduled:first_trading_day_on_day: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_before_trading:2025-01-31",
|
||||||
"scheduled:daily_market_open:2025-01-31",
|
"scheduled:daily_market_open:2025-01-31",
|
||||||
"scheduled:friday_on_day: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_before_trading:2025-02-03",
|
||||||
"scheduled:daily_market_open:2025-02-03",
|
"scheduled:daily_market_open:2025-02-03",
|
||||||
"scheduled:first_trading_day_on_day: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();
|
let process_log = process_log.borrow();
|
||||||
|
|||||||
Reference in New Issue
Block a user