修复定时轮动提前在日线阶段执行
This commit is contained in:
@@ -1117,6 +1117,7 @@ pub struct PlatformExprStrategy {
|
|||||||
position_entry_dates: BTreeMap<String, NaiveDate>,
|
position_entry_dates: BTreeMap<String, NaiveDate>,
|
||||||
position_holding_days: BTreeMap<String, i64>,
|
position_holding_days: BTreeMap<String, i64>,
|
||||||
position_holding_days_last_counted: BTreeMap<String, NaiveDate>,
|
position_holding_days_last_counted: BTreeMap<String, NaiveDate>,
|
||||||
|
executing_scheduled_rotation: bool,
|
||||||
/// 已编译表达式 AST 缓存。
|
/// 已编译表达式 AST 缓存。
|
||||||
/// Key 是经过 normalize/helper template 展开之后的完整 script 文本,
|
/// Key 是经过 normalize/helper template 展开之后的完整 script 文本,
|
||||||
/// Value 是 Rhai 编译产物。命中后 eval 走 eval_ast_with_scope,避免重复
|
/// Value 是 Rhai 编译产物。命中后 eval 走 eval_ast_with_scope,避免重复
|
||||||
@@ -1450,6 +1451,7 @@ impl PlatformExprStrategy {
|
|||||||
position_entry_dates: BTreeMap::new(),
|
position_entry_dates: BTreeMap::new(),
|
||||||
position_holding_days: BTreeMap::new(),
|
position_holding_days: BTreeMap::new(),
|
||||||
position_holding_days_last_counted: BTreeMap::new(),
|
position_holding_days_last_counted: BTreeMap::new(),
|
||||||
|
executing_scheduled_rotation: false,
|
||||||
compiled_cache: RefCell::new(AHashMap::new()),
|
compiled_cache: RefCell::new(AHashMap::new()),
|
||||||
cache_hits: RefCell::new(0),
|
cache_hits: RefCell::new(0),
|
||||||
cache_misses: RefCell::new(0),
|
cache_misses: RefCell::new(0),
|
||||||
@@ -11613,8 +11615,25 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn schedule_rules(&self) -> Vec<ScheduleRule> {
|
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() {
|
if self.config.explicit_actions.is_empty() {
|
||||||
return Vec::new();
|
return rules;
|
||||||
}
|
}
|
||||||
let stage = match self.config.explicit_action_stage {
|
let stage = match self.config.explicit_action_stage {
|
||||||
PlatformExplicitActionStage::OpenAuction => ScheduleStage::OpenAuction,
|
PlatformExplicitActionStage::OpenAuction => ScheduleStage::OpenAuction,
|
||||||
@@ -11622,32 +11641,52 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
PlatformExplicitActionStage::Minute => ScheduleStage::Minute,
|
PlatformExplicitActionStage::Minute => ScheduleStage::Minute,
|
||||||
};
|
};
|
||||||
let Some(schedule) = self.config.explicit_action_schedule.as_ref() else {
|
let Some(schedule) = self.config.explicit_action_schedule.as_ref() else {
|
||||||
return Vec::new();
|
return rules;
|
||||||
};
|
};
|
||||||
if self.config.explicit_action_times.is_empty() {
|
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);
|
||||||
}
|
}
|
||||||
self.config
|
return rules;
|
||||||
.explicit_action_times
|
}
|
||||||
.iter()
|
for time in &self.config.explicit_action_times {
|
||||||
.map(|time| {
|
|
||||||
let mut timed_schedule = schedule.clone();
|
let mut timed_schedule = schedule.clone();
|
||||||
timed_schedule.time_rule =
|
timed_schedule.time_rule =
|
||||||
Some(ScheduleTimeRule::physical_time(time.hour(), time.minute()));
|
Some(ScheduleTimeRule::physical_time(time.hour(), time.minute()));
|
||||||
timed_schedule.as_schedule_rule(stage)
|
let rule = timed_schedule.as_schedule_rule(stage);
|
||||||
})
|
if !rules.contains(&rule) {
|
||||||
.collect()
|
rules.push(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rules
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_scheduled(
|
fn on_scheduled(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &StrategyContext<'_>,
|
ctx: &StrategyContext<'_>,
|
||||||
_rule: &ScheduleRule,
|
rule: &ScheduleRule,
|
||||||
) -> Result<StrategyDecision, BacktestError> {
|
) -> Result<StrategyDecision, BacktestError> {
|
||||||
if !self.config.explicit_actions.is_empty() {
|
let mut decision = if self.config.explicit_actions.is_empty() {
|
||||||
return self.explicit_action_decision(ctx);
|
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> {
|
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> {
|
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 execution_date = ctx.execution_date;
|
||||||
let decision_date = ctx.decision_date;
|
let decision_date = ctx.decision_date;
|
||||||
let defer_execution_risk = ctx.is_lagged_execution();
|
let defer_execution_risk = ctx.is_lagged_execution();
|
||||||
@@ -13678,6 +13728,58 @@ mod tests {
|
|||||||
assert!((scale - 1.2).abs() < 1e-12, "scale={scale}");
|
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]
|
#[test]
|
||||||
fn universe_exclude_matches_explicit_symbols_and_bjse_alias() {
|
fn universe_exclude_matches_explicit_symbols_and_bjse_alias() {
|
||||||
let excludes = vec![
|
let excludes = vec![
|
||||||
|
|||||||
Reference in New Issue
Block a user