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 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};
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user