Expose weekly and monthly platform rebalance schedules

This commit is contained in:
boris
2026-04-23 02:42:28 -07:00
parent 948aca21e8
commit e400dec464
3 changed files with 123 additions and 2 deletions

View File

@@ -32,7 +32,10 @@ pub use events::{
}; };
pub use instrument::Instrument; pub use instrument::Instrument;
pub use metrics::{BacktestMetrics, compute_backtest_metrics}; pub use metrics::{BacktestMetrics, compute_backtest_metrics};
pub use platform_expr_strategy::{PlatformExprStrategy, PlatformExprStrategyConfig}; pub use platform_expr_strategy::{
PlatformExprStrategy, PlatformExprStrategyConfig, PlatformRebalanceSchedule,
PlatformScheduleFrequency,
};
pub use portfolio::{CashReceivable, HoldingSummary, PortfolioState, Position}; pub use portfolio::{CashReceivable, HoldingSummary, PortfolioState, Position};
pub use rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck}; pub use rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck};
pub use scheduler::{ScheduleFrequency, ScheduleRule, ScheduleStage, Scheduler}; pub use scheduler::{ScheduleFrequency, ScheduleRule, ScheduleStage, Scheduler};

View File

@@ -8,8 +8,71 @@ use crate::data::{DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField};
use crate::engine::BacktestError; use crate::engine::BacktestError;
use crate::events::OrderSide; use crate::events::OrderSide;
use crate::portfolio::PortfolioState; use crate::portfolio::PortfolioState;
use crate::scheduler::{ScheduleRule, ScheduleStage, Scheduler};
use crate::strategy::{OrderIntent, Strategy, StrategyContext, StrategyDecision}; use crate::strategy::{OrderIntent, Strategy, StrategyContext, StrategyDecision};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlatformScheduleFrequency {
Weekly {
weekday: Option<u32>,
tradingday: Option<i32>,
},
Monthly {
tradingday: i32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlatformRebalanceSchedule {
pub frequency: PlatformScheduleFrequency,
}
impl PlatformRebalanceSchedule {
fn as_schedule_rule(&self) -> ScheduleRule {
match self.frequency {
PlatformScheduleFrequency::Weekly {
weekday: Some(weekday),
..
} => ScheduleRule::weekly_by_weekday(
"platform_periodic_rebalance",
weekday,
ScheduleStage::OnDay,
),
PlatformScheduleFrequency::Weekly {
tradingday: Some(tradingday),
..
} => ScheduleRule::weekly_by_tradingday(
"platform_periodic_rebalance",
tradingday,
ScheduleStage::OnDay,
),
PlatformScheduleFrequency::Monthly { tradingday } => ScheduleRule::monthly(
"platform_periodic_rebalance",
tradingday,
ScheduleStage::OnDay,
),
PlatformScheduleFrequency::Weekly {
weekday: None,
tradingday: None,
} => ScheduleRule::weekly_by_weekday(
"platform_periodic_rebalance",
1,
ScheduleStage::OnDay,
),
}
}
fn matches(&self, calendar: &crate::calendar::TradingCalendar, date: NaiveDate) -> bool {
let scheduler = Scheduler::new(calendar);
let rule = self.as_schedule_rule();
scheduler
.triggered_rules(date, ScheduleStage::OnDay, std::slice::from_ref(&rule))
.into_iter()
.next()
.is_some()
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PlatformExprStrategyConfig { pub struct PlatformExprStrategyConfig {
pub strategy_name: String, pub strategy_name: String,
@@ -38,6 +101,7 @@ pub struct PlatformExprStrategyConfig {
pub stock_mid_ma_days: usize, pub stock_mid_ma_days: usize,
pub stock_long_ma_days: usize, pub stock_long_ma_days: usize,
pub skip_month_day_ranges: Vec<(u32, u32, u32)>, pub skip_month_day_ranges: Vec<(u32, u32, u32)>,
pub rebalance_schedule: Option<PlatformRebalanceSchedule>,
} }
impl PlatformExprStrategyConfig { impl PlatformExprStrategyConfig {
@@ -82,6 +146,7 @@ fn band_low(index_close) {
stock_mid_ma_days: 10, stock_mid_ma_days: 10,
stock_long_ma_days: 20, stock_long_ma_days: 20,
skip_month_day_ranges: Vec::new(), skip_month_day_ranges: Vec::new(),
rebalance_schedule: None,
} }
} }
@@ -2136,7 +2201,11 @@ impl Strategy for PlatformExprStrategy {
.min(self.config.max_positions.max(1)); .min(self.config.max_positions.max(1));
let (stock_list, selection_notes) = let (stock_list, selection_notes) =
self.select_symbols(ctx, date, &day, band_low, band_high, selection_limit)?; self.select_symbols(ctx, date, &day, band_low, band_high, selection_limit)?;
let periodic_rebalance = ctx.decision_index % self.config.refresh_rate == 0; let periodic_rebalance = if let Some(schedule) = &self.config.rebalance_schedule {
schedule.matches(ctx.data.calendar(), date)
} else {
ctx.decision_index % self.config.refresh_rate == 0
};
let mut projected = ctx.portfolio.clone(); let mut projected = ctx.portfolio.clone();
let mut projected_execution_state = ProjectedExecutionState::default(); let mut projected_execution_state = ProjectedExecutionState::default();
let mut order_intents = Vec::new(); let mut order_intents = Vec::new();
@@ -2323,3 +2392,48 @@ impl Strategy for PlatformExprStrategy {
}) })
} }
} }
#[cfg(test)]
mod tests {
use chrono::NaiveDate;
use super::{PlatformRebalanceSchedule, PlatformScheduleFrequency};
use crate::TradingCalendar;
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
}
fn sample_calendar() -> TradingCalendar {
TradingCalendar::new(vec![
d(2025, 1, 30),
d(2025, 1, 31),
d(2025, 2, 3),
d(2025, 2, 4),
d(2025, 2, 7),
])
}
#[test]
fn platform_rebalance_schedule_matches_weekly_weekday() {
let calendar = sample_calendar();
let schedule = PlatformRebalanceSchedule {
frequency: PlatformScheduleFrequency::Weekly {
weekday: Some(5),
tradingday: None,
},
};
assert!(schedule.matches(&calendar, d(2025, 1, 31)));
assert!(!schedule.matches(&calendar, d(2025, 2, 3)));
}
#[test]
fn platform_rebalance_schedule_matches_monthly_tradingday() {
let calendar = sample_calendar();
let schedule = PlatformRebalanceSchedule {
frequency: PlatformScheduleFrequency::Monthly { tradingday: 1 },
};
assert!(schedule.matches(&calendar, d(2025, 2, 3)));
assert!(!schedule.matches(&calendar, d(2025, 2, 4)));
}
}

View File

@@ -98,6 +98,10 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
title: "rebalance.every_days(n).at([..])".to_string(), title: "rebalance.every_days(n).at([..])".to_string(),
detail: "设置调仓周期和盘中决策/执行时刻。".to_string(), detail: "设置调仓周期和盘中决策/执行时刻。".to_string(),
}, },
ManualSection {
title: "rebalance.weekly / rebalance.monthly".to_string(),
detail: "支持按交易周或交易月调仓,例如 rebalance.weekly(weekday=5).at([\"10:18\"])、rebalance.weekly(tradingday=-1).at([\"10:18\"])、rebalance.monthly(tradingday=1).at([\"10:18\"])。当前这些调度规则会在 on_day 阶段触发。".to_string(),
},
ManualSection { ManualSection {
title: "selection.market_cap_band / selection.limit / ordering.rank_by / ordering.rank_expr".to_string(), title: "selection.market_cap_band / selection.limit / ordering.rank_by / ordering.rank_expr".to_string(),
detail: "控制候选范围、数量和排序。支持表达式驱动的动态市值带和排序表达式。".to_string(), detail: "控制候选范围、数量和排序。支持表达式驱动的动态市值带和排序表达式。".to_string(),