修复定时轮动提前在日线阶段执行
This commit is contained in:
@@ -1117,6 +1117,7 @@ pub struct PlatformExprStrategy {
|
||||
position_entry_dates: BTreeMap<String, NaiveDate>,
|
||||
position_holding_days: BTreeMap<String, i64>,
|
||||
position_holding_days_last_counted: BTreeMap<String, NaiveDate>,
|
||||
executing_scheduled_rotation: bool,
|
||||
/// 已编译表达式 AST 缓存。
|
||||
/// Key 是经过 normalize/helper template 展开之后的完整 script 文本,
|
||||
/// Value 是 Rhai 编译产物。命中后 eval 走 eval_ast_with_scope,避免重复
|
||||
@@ -1450,6 +1451,7 @@ impl PlatformExprStrategy {
|
||||
position_entry_dates: BTreeMap::new(),
|
||||
position_holding_days: BTreeMap::new(),
|
||||
position_holding_days_last_counted: BTreeMap::new(),
|
||||
executing_scheduled_rotation: false,
|
||||
compiled_cache: RefCell::new(AHashMap::new()),
|
||||
cache_hits: RefCell::new(0),
|
||||
cache_misses: RefCell::new(0),
|
||||
@@ -11613,8 +11615,25 @@ impl Strategy for PlatformExprStrategy {
|
||||
}
|
||||
|
||||
fn schedule_rules(&self) -> Vec<ScheduleRule> {
|
||||
let mut rules = Vec::new();
|
||||
if self
|
||||
.config
|
||||
.rebalance_schedule
|
||||
.as_ref()
|
||||
.and_then(|schedule| schedule.time_rule.as_ref())
|
||||
.is_some()
|
||||
&& self.config.rotation_enabled
|
||||
{
|
||||
rules.push(
|
||||
self.config
|
||||
.rebalance_schedule
|
||||
.as_ref()
|
||||
.expect("checked timed rebalance schedule")
|
||||
.as_schedule_rule(ScheduleStage::OnDay),
|
||||
);
|
||||
}
|
||||
if self.config.explicit_actions.is_empty() {
|
||||
return Vec::new();
|
||||
return rules;
|
||||
}
|
||||
let stage = match self.config.explicit_action_stage {
|
||||
PlatformExplicitActionStage::OpenAuction => ScheduleStage::OpenAuction,
|
||||
@@ -11622,32 +11641,52 @@ impl Strategy for PlatformExprStrategy {
|
||||
PlatformExplicitActionStage::Minute => ScheduleStage::Minute,
|
||||
};
|
||||
let Some(schedule) = self.config.explicit_action_schedule.as_ref() else {
|
||||
return Vec::new();
|
||||
return rules;
|
||||
};
|
||||
if self.config.explicit_action_times.is_empty() {
|
||||
return vec![schedule.as_schedule_rule(stage)];
|
||||
let rule = schedule.as_schedule_rule(stage);
|
||||
if !rules.contains(&rule) {
|
||||
rules.push(rule);
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
self.config
|
||||
.explicit_action_times
|
||||
.iter()
|
||||
.map(|time| {
|
||||
let mut timed_schedule = schedule.clone();
|
||||
timed_schedule.time_rule =
|
||||
Some(ScheduleTimeRule::physical_time(time.hour(), time.minute()));
|
||||
timed_schedule.as_schedule_rule(stage)
|
||||
})
|
||||
.collect()
|
||||
for time in &self.config.explicit_action_times {
|
||||
let mut timed_schedule = schedule.clone();
|
||||
timed_schedule.time_rule =
|
||||
Some(ScheduleTimeRule::physical_time(time.hour(), time.minute()));
|
||||
let rule = timed_schedule.as_schedule_rule(stage);
|
||||
if !rules.contains(&rule) {
|
||||
rules.push(rule);
|
||||
}
|
||||
}
|
||||
rules
|
||||
}
|
||||
|
||||
fn on_scheduled(
|
||||
&mut self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
_rule: &ScheduleRule,
|
||||
rule: &ScheduleRule,
|
||||
) -> Result<StrategyDecision, BacktestError> {
|
||||
if !self.config.explicit_actions.is_empty() {
|
||||
return self.explicit_action_decision(ctx);
|
||||
let mut decision = if self.config.explicit_actions.is_empty() {
|
||||
StrategyDecision::default()
|
||||
} else {
|
||||
self.explicit_action_decision(ctx)?
|
||||
};
|
||||
let scheduled_rotation = self.config.rotation_enabled
|
||||
&& rule.name == "platform_periodic_rebalance"
|
||||
&& self
|
||||
.config
|
||||
.rebalance_schedule
|
||||
.as_ref()
|
||||
.and_then(|schedule| schedule.time_rule.as_ref())
|
||||
.is_some();
|
||||
if scheduled_rotation {
|
||||
self.executing_scheduled_rotation = true;
|
||||
let rotation = self.on_day(ctx);
|
||||
self.executing_scheduled_rotation = false;
|
||||
decision.merge_from(rotation?);
|
||||
}
|
||||
Ok(StrategyDecision::default())
|
||||
Ok(decision)
|
||||
}
|
||||
|
||||
fn decision_quote_times(&self) -> Vec<NaiveTime> {
|
||||
@@ -11701,6 +11740,17 @@ impl Strategy for PlatformExprStrategy {
|
||||
}
|
||||
|
||||
fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result<StrategyDecision, BacktestError> {
|
||||
if self.config.rotation_enabled
|
||||
&& self
|
||||
.config
|
||||
.rebalance_schedule
|
||||
.as_ref()
|
||||
.and_then(|schedule| schedule.time_rule.as_ref())
|
||||
.is_some()
|
||||
&& !self.executing_scheduled_rotation
|
||||
{
|
||||
return Ok(StrategyDecision::default());
|
||||
}
|
||||
let execution_date = ctx.execution_date;
|
||||
let decision_date = ctx.decision_date;
|
||||
let defer_execution_risk = ctx.is_lagged_execution();
|
||||
@@ -13678,6 +13728,58 @@ mod tests {
|
||||
assert!((scale - 1.2).abs() < 1e-12, "scale={scale}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timed_rotation_runs_on_its_schedule_instead_of_the_untimed_day_hook() {
|
||||
let date = d(2025, 1, 2);
|
||||
let symbol = "000001.SZ";
|
||||
let data = single_symbol_platform_data(&[date], symbol);
|
||||
let portfolio = PortfolioState::new(1_000_000.0);
|
||||
let subscriptions = BTreeSet::new();
|
||||
let ctx = StrategyContext {
|
||||
execution_date: date,
|
||||
decision_date: date,
|
||||
decision_index: 0,
|
||||
data: &data,
|
||||
portfolio: &portfolio,
|
||||
futures_account: None,
|
||||
open_orders: &[],
|
||||
dynamic_universe: None,
|
||||
subscriptions: &subscriptions,
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
let mut config = PlatformExprStrategyConfig::microcap_rotation();
|
||||
config.signal_symbol = symbol.to_string();
|
||||
config.refresh_rate = 1;
|
||||
config.max_positions = 1;
|
||||
config.selection_limit_expr = "1".to_string();
|
||||
config.market_cap_lower_expr = "0".to_string();
|
||||
config.market_cap_upper_expr = "100".to_string();
|
||||
config.stock_filter_expr = "close > 0".to_string();
|
||||
config.benchmark_short_ma_days = 1;
|
||||
config.benchmark_long_ma_days = 1;
|
||||
config.current_day_precomputed_factors = true;
|
||||
config.rebalance_schedule = Some(PlatformRebalanceSchedule {
|
||||
frequency: PlatformScheduleFrequency::Daily,
|
||||
time_rule: Some(ScheduleTimeRule::physical_time(9, 30)),
|
||||
});
|
||||
let mut strategy = PlatformExprStrategy::new(config);
|
||||
let rules = strategy.schedule_rules();
|
||||
|
||||
assert_eq!(rules.len(), 1);
|
||||
assert!(strategy.on_day(&ctx).expect("day decision").is_empty());
|
||||
|
||||
let decision = strategy
|
||||
.on_scheduled(&ctx, &rules[0])
|
||||
.expect("scheduled rotation decision");
|
||||
|
||||
assert!(!decision.is_empty(), "{decision:?}");
|
||||
assert!(!decision.order_intents.is_empty(), "{decision:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn universe_exclude_matches_explicit_symbols_and_bjse_alias() {
|
||||
let excludes = vec![
|
||||
|
||||
Reference in New Issue
Block a user