拆分调仓日期与执行时钟
This commit is contained in:
@@ -24,9 +24,7 @@ use crate::numeric_expr_vm::{
|
|||||||
};
|
};
|
||||||
use crate::portfolio::PortfolioState;
|
use crate::portfolio::PortfolioState;
|
||||||
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit};
|
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit};
|
||||||
use crate::scheduler::{
|
use crate::scheduler::{ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler};
|
||||||
ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler, default_stage_time,
|
|
||||||
};
|
|
||||||
use crate::strategy::{
|
use crate::strategy::{
|
||||||
AlgoOrderStyle, OrderIntent, OrderTimeInForce, Strategy, StrategyContext, StrategyDecision,
|
AlgoOrderStyle, OrderIntent, OrderTimeInForce, Strategy, StrategyContext, StrategyDecision,
|
||||||
TargetPortfolioOrderPricing,
|
TargetPortfolioOrderPricing,
|
||||||
@@ -231,7 +229,8 @@ impl PlatformRebalanceSchedule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn matches(
|
#[cfg(test)]
|
||||||
|
fn matches_at(
|
||||||
&self,
|
&self,
|
||||||
calendar: &crate::calendar::TradingCalendar,
|
calendar: &crate::calendar::TradingCalendar,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
@@ -246,6 +245,11 @@ impl PlatformRebalanceSchedule {
|
|||||||
.next()
|
.next()
|
||||||
.is_some()
|
.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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -10640,12 +10644,7 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
} else if !self.config.signal_rebalance_dates.is_empty() {
|
} else if !self.config.signal_rebalance_dates.is_empty() {
|
||||||
self.config.signal_rebalance_dates.contains(&decision_date) || empty_rebalance_retry
|
self.config.signal_rebalance_dates.contains(&decision_date) || empty_rebalance_retry
|
||||||
} else if let Some(schedule) = &self.config.rebalance_schedule {
|
} else if let Some(schedule) = &self.config.rebalance_schedule {
|
||||||
schedule.matches(
|
schedule.is_due_on(ctx.data.calendar(), signal_date) || empty_rebalance_retry
|
||||||
ctx.data.calendar(),
|
|
||||||
signal_date,
|
|
||||||
ScheduleStage::OnDay,
|
|
||||||
default_stage_time(ScheduleStage::OnDay),
|
|
||||||
) || empty_rebalance_retry
|
|
||||||
} else if self.config.calendar_rebalance_interval {
|
} else if self.config.calendar_rebalance_interval {
|
||||||
self.last_rebalance_date
|
self.last_rebalance_date
|
||||||
.map(|last| {
|
.map(|last| {
|
||||||
@@ -21872,13 +21871,13 @@ mod tests {
|
|||||||
},
|
},
|
||||||
time_rule: None,
|
time_rule: None,
|
||||||
};
|
};
|
||||||
assert!(schedule.matches(
|
assert!(schedule.matches_at(
|
||||||
&calendar,
|
&calendar,
|
||||||
d(2025, 1, 31),
|
d(2025, 1, 31),
|
||||||
ScheduleStage::OnDay,
|
ScheduleStage::OnDay,
|
||||||
default_stage_time(ScheduleStage::OnDay),
|
default_stage_time(ScheduleStage::OnDay),
|
||||||
));
|
));
|
||||||
assert!(!schedule.matches(
|
assert!(!schedule.matches_at(
|
||||||
&calendar,
|
&calendar,
|
||||||
d(2025, 2, 3),
|
d(2025, 2, 3),
|
||||||
ScheduleStage::OnDay,
|
ScheduleStage::OnDay,
|
||||||
@@ -21893,13 +21892,13 @@ mod tests {
|
|||||||
frequency: PlatformScheduleFrequency::Monthly { tradingday: 1 },
|
frequency: PlatformScheduleFrequency::Monthly { tradingday: 1 },
|
||||||
time_rule: None,
|
time_rule: None,
|
||||||
};
|
};
|
||||||
assert!(schedule.matches(
|
assert!(schedule.matches_at(
|
||||||
&calendar,
|
&calendar,
|
||||||
d(2025, 2, 3),
|
d(2025, 2, 3),
|
||||||
ScheduleStage::OnDay,
|
ScheduleStage::OnDay,
|
||||||
default_stage_time(ScheduleStage::OnDay),
|
default_stage_time(ScheduleStage::OnDay),
|
||||||
));
|
));
|
||||||
assert!(!schedule.matches(
|
assert!(!schedule.matches_at(
|
||||||
&calendar,
|
&calendar,
|
||||||
d(2025, 2, 4),
|
d(2025, 2, 4),
|
||||||
ScheduleStage::OnDay,
|
ScheduleStage::OnDay,
|
||||||
@@ -21917,13 +21916,13 @@ mod tests {
|
|||||||
},
|
},
|
||||||
time_rule: Some(ScheduleTimeRule::physical_time(10, 18)),
|
time_rule: Some(ScheduleTimeRule::physical_time(10, 18)),
|
||||||
};
|
};
|
||||||
assert!(schedule.matches(
|
assert!(schedule.matches_at(
|
||||||
&calendar,
|
&calendar,
|
||||||
d(2025, 1, 31),
|
d(2025, 1, 31),
|
||||||
ScheduleStage::OnDay,
|
ScheduleStage::OnDay,
|
||||||
Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()),
|
Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()),
|
||||||
));
|
));
|
||||||
assert!(!schedule.matches(
|
assert!(!schedule.matches_at(
|
||||||
&calendar,
|
&calendar,
|
||||||
d(2025, 1, 31),
|
d(2025, 1, 31),
|
||||||
ScheduleStage::OnDay,
|
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]
|
#[test]
|
||||||
fn platform_strategy_emits_explicit_actions_when_rotation_is_disabled() {
|
fn platform_strategy_emits_explicit_actions_when_rotation_is_disabled() {
|
||||||
let date = d(2025, 2, 3);
|
let date = d(2025, 2, 3);
|
||||||
|
|||||||
@@ -164,6 +164,16 @@ impl<'a> Scheduler<'a> {
|
|||||||
.collect()
|
.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 {
|
fn matches(&self, date: NaiveDate, rule: &ScheduleRule) -> bool {
|
||||||
match &rule.frequency {
|
match &rule.frequency {
|
||||||
ScheduleFrequency::Daily => true,
|
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]
|
#[test]
|
||||||
fn scheduler_matches_daily_weekly_and_monthly_rules() {
|
fn scheduler_matches_daily_weekly_and_monthly_rules() {
|
||||||
let calendar = sample_calendar();
|
let calendar = sample_calendar();
|
||||||
|
|||||||
Reference in New Issue
Block a user