拆分调仓日期与执行时钟
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user