拆分调仓日期与执行时钟

This commit is contained in:
boris
2026-08-28 00:00:30 +08:00
parent 85c38b0756
commit a9511f9a4a
2 changed files with 65 additions and 16 deletions
+32 -16
View File
@@ -24,9 +24,7 @@ use crate::numeric_expr_vm::{
};
use crate::portfolio::PortfolioState;
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit};
use crate::scheduler::{
ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler, default_stage_time,
};
use crate::scheduler::{ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler};
use crate::strategy::{
AlgoOrderStyle, OrderIntent, OrderTimeInForce, Strategy, StrategyContext, StrategyDecision,
TargetPortfolioOrderPricing,
@@ -231,7 +229,8 @@ impl PlatformRebalanceSchedule {
}
}
fn matches(
#[cfg(test)]
fn matches_at(
&self,
calendar: &crate::calendar::TradingCalendar,
date: NaiveDate,
@@ -246,6 +245,11 @@ impl PlatformRebalanceSchedule {
.next()
.is_some()
}
fn is_due_on(&self, calendar: &crate::calendar::TradingCalendar, date: NaiveDate) -> bool {
let scheduler = Scheduler::new(calendar);
scheduler.is_due_on(date, &self.as_schedule_rule(ScheduleStage::OnDay))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -10640,12 +10644,7 @@ impl Strategy for PlatformExprStrategy {
} else if !self.config.signal_rebalance_dates.is_empty() {
self.config.signal_rebalance_dates.contains(&decision_date) || empty_rebalance_retry
} else if let Some(schedule) = &self.config.rebalance_schedule {
schedule.matches(
ctx.data.calendar(),
signal_date,
ScheduleStage::OnDay,
default_stage_time(ScheduleStage::OnDay),
) || empty_rebalance_retry
schedule.is_due_on(ctx.data.calendar(), signal_date) || empty_rebalance_retry
} else if self.config.calendar_rebalance_interval {
self.last_rebalance_date
.map(|last| {
@@ -21872,13 +21871,13 @@ mod tests {
},
time_rule: None,
};
assert!(schedule.matches(
assert!(schedule.matches_at(
&calendar,
d(2025, 1, 31),
ScheduleStage::OnDay,
default_stage_time(ScheduleStage::OnDay),
));
assert!(!schedule.matches(
assert!(!schedule.matches_at(
&calendar,
d(2025, 2, 3),
ScheduleStage::OnDay,
@@ -21893,13 +21892,13 @@ mod tests {
frequency: PlatformScheduleFrequency::Monthly { tradingday: 1 },
time_rule: None,
};
assert!(schedule.matches(
assert!(schedule.matches_at(
&calendar,
d(2025, 2, 3),
ScheduleStage::OnDay,
default_stage_time(ScheduleStage::OnDay),
));
assert!(!schedule.matches(
assert!(!schedule.matches_at(
&calendar,
d(2025, 2, 4),
ScheduleStage::OnDay,
@@ -21917,13 +21916,13 @@ mod tests {
},
time_rule: Some(ScheduleTimeRule::physical_time(10, 18)),
};
assert!(schedule.matches(
assert!(schedule.matches_at(
&calendar,
d(2025, 1, 31),
ScheduleStage::OnDay,
Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()),
));
assert!(!schedule.matches(
assert!(!schedule.matches_at(
&calendar,
d(2025, 1, 31),
ScheduleStage::OnDay,
@@ -21931,6 +21930,23 @@ mod tests {
));
}
#[test]
fn platform_rebalance_date_due_does_not_use_the_on_day_default_clock() {
let calendar = sample_calendar();
let schedule = PlatformRebalanceSchedule {
frequency: PlatformScheduleFrequency::Daily,
time_rule: Some(ScheduleTimeRule::physical_time(15, 0)),
};
assert!(schedule.is_due_on(&calendar, d(2025, 1, 30)));
assert!(!schedule.matches_at(
&calendar,
d(2025, 1, 30),
ScheduleStage::OnDay,
default_stage_time(ScheduleStage::OnDay),
));
}
#[test]
fn platform_strategy_emits_explicit_actions_when_rotation_is_disabled() {
let date = d(2025, 2, 3);
+33
View File
@@ -164,6 +164,16 @@ impl<'a> Scheduler<'a> {
.collect()
}
/// Evaluate only the trading-calendar frequency of a rule.
///
/// Strategy callbacks and order execution clocks are separate contracts:
/// a 15:00 schedule is still due on the same daily/weekly/monthly trading
/// date even when the engine's coarse `on_day` callback runs at another
/// default time. Exact clock matching remains in `triggered_rules_at`.
pub fn is_due_on(&self, date: NaiveDate, rule: &ScheduleRule) -> bool {
self.matches(date, rule)
}
fn matches(&self, date: NaiveDate, rule: &ScheduleRule) -> bool {
match &rule.frequency {
ScheduleFrequency::Daily => true,
@@ -265,6 +275,29 @@ mod tests {
])
}
#[test]
fn date_due_is_independent_from_the_order_execution_clock() {
let calendar = sample_calendar();
let scheduler = Scheduler::new(&calendar);
let daily = ScheduleRule::daily("close_signal", ScheduleStage::OnDay)
.with_time_rule(ScheduleTimeRule::physical_time(15, 0));
assert!(scheduler.is_due_on(d(2025, 1, 30), &daily));
assert!(scheduler.is_due_on(d(2025, 1, 31), &daily));
assert!(scheduler.triggered_rules_at(
d(2025, 1, 30),
ScheduleStage::OnDay,
Some(NaiveTime::from_hms_opt(15, 0, 0).unwrap()),
std::slice::from_ref(&daily),
).len() == 1);
assert!(scheduler.triggered_rules_at(
d(2025, 1, 30),
ScheduleStage::OnDay,
Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()),
std::slice::from_ref(&daily),
).is_empty());
}
#[test]
fn scheduler_matches_daily_weekly_and_monthly_rules() {
let calendar = sample_calendar();