Expose weekly and monthly platform rebalance schedules
This commit is contained in:
@@ -32,7 +32,10 @@ pub use events::{
|
||||
};
|
||||
pub use instrument::Instrument;
|
||||
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 rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck};
|
||||
pub use scheduler::{ScheduleFrequency, ScheduleRule, ScheduleStage, Scheduler};
|
||||
|
||||
@@ -8,8 +8,71 @@ use crate::data::{DailyMarketSnapshot, EligibleUniverseSnapshot, PriceField};
|
||||
use crate::engine::BacktestError;
|
||||
use crate::events::OrderSide;
|
||||
use crate::portfolio::PortfolioState;
|
||||
use crate::scheduler::{ScheduleRule, ScheduleStage, Scheduler};
|
||||
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)]
|
||||
pub struct PlatformExprStrategyConfig {
|
||||
pub strategy_name: String,
|
||||
@@ -38,6 +101,7 @@ pub struct PlatformExprStrategyConfig {
|
||||
pub stock_mid_ma_days: usize,
|
||||
pub stock_long_ma_days: usize,
|
||||
pub skip_month_day_ranges: Vec<(u32, u32, u32)>,
|
||||
pub rebalance_schedule: Option<PlatformRebalanceSchedule>,
|
||||
}
|
||||
|
||||
impl PlatformExprStrategyConfig {
|
||||
@@ -82,6 +146,7 @@ fn band_low(index_close) {
|
||||
stock_mid_ma_days: 10,
|
||||
stock_long_ma_days: 20,
|
||||
skip_month_day_ranges: Vec::new(),
|
||||
rebalance_schedule: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2136,7 +2201,11 @@ impl Strategy for PlatformExprStrategy {
|
||||
.min(self.config.max_positions.max(1));
|
||||
let (stock_list, selection_notes) =
|
||||
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_execution_state = ProjectedExecutionState::default();
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
title: "rebalance.every_days(n).at([..])".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 {
|
||||
title: "selection.market_cap_band / selection.limit / ordering.rank_by / ordering.rank_expr".to_string(),
|
||||
detail: "控制候选范围、数量和排序。支持表达式驱动的动态市值带和排序表达式。".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user