From a9511f9a4abfdbf0f35f0bc710d8af8b9fc05839 Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 28 Aug 2026 00:00:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E8=B0=83=E4=BB=93=E6=97=A5?= =?UTF-8?q?=E6=9C=9F=E4=B8=8E=E6=89=A7=E8=A1=8C=E6=97=B6=E9=92=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 48 ++++++++++++------- crates/fidc-core/src/scheduler.rs | 33 +++++++++++++ 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 0e4272d..0febfa3 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -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); diff --git a/crates/fidc-core/src/scheduler.rs b/crates/fidc-core/src/scheduler.rs index a260517..9671d7d 100644 --- a/crates/fidc-core/src/scheduler.rs +++ b/crates/fidc-core/src/scheduler.rs @@ -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();