修复信号日期调仓执行语义
This commit is contained in:
@@ -204,6 +204,7 @@ pub struct PlatformExprStrategyConfig {
|
|||||||
pub stock_long_ma_days: usize,
|
pub stock_long_ma_days: usize,
|
||||||
pub skip_month_day_ranges: Vec<(Option<u32>, u32, u32, u32)>,
|
pub skip_month_day_ranges: Vec<(Option<u32>, u32, u32, u32)>,
|
||||||
pub rebalance_schedule: Option<PlatformRebalanceSchedule>,
|
pub rebalance_schedule: Option<PlatformRebalanceSchedule>,
|
||||||
|
pub signal_rebalance_dates: BTreeSet<NaiveDate>,
|
||||||
pub rotation_enabled: bool,
|
pub rotation_enabled: bool,
|
||||||
pub daily_top_up_enabled: bool,
|
pub daily_top_up_enabled: bool,
|
||||||
pub retry_empty_rebalance: bool,
|
pub retry_empty_rebalance: bool,
|
||||||
@@ -274,6 +275,7 @@ fn band_low(index_close) {
|
|||||||
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,
|
rebalance_schedule: None,
|
||||||
|
signal_rebalance_dates: BTreeSet::new(),
|
||||||
rotation_enabled: true,
|
rotation_enabled: true,
|
||||||
daily_top_up_enabled: false,
|
daily_top_up_enabled: false,
|
||||||
retry_empty_rebalance: false,
|
retry_empty_rebalance: false,
|
||||||
@@ -7324,7 +7326,9 @@ impl Strategy for PlatformExprStrategy {
|
|||||||
self.config.retry_empty_rebalance && ctx.portfolio.positions().is_empty();
|
self.config.retry_empty_rebalance && ctx.portfolio.positions().is_empty();
|
||||||
let effective_refresh_rate = self.effective_refresh_rate(ctx, &day)?;
|
let effective_refresh_rate = self.effective_refresh_rate(ctx, &day)?;
|
||||||
let periodic_rebalance = if self.config.rotation_enabled && !in_skip_window {
|
let periodic_rebalance = if self.config.rotation_enabled && !in_skip_window {
|
||||||
if let Some(schedule) = &self.config.rebalance_schedule {
|
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(
|
schedule.matches(
|
||||||
ctx.data.calendar(),
|
ctx.data.calendar(),
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -16852,6 +16856,41 @@ mod tests {
|
|||||||
dynamic_decision.diagnostics
|
dynamic_decision.diagnostics
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let mut signal_dates_cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
|
signal_dates_cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
|
signal_dates_cfg.refresh_rate = 20;
|
||||||
|
signal_dates_cfg.max_positions = 2;
|
||||||
|
signal_dates_cfg.benchmark_short_ma_days = 1;
|
||||||
|
signal_dates_cfg.benchmark_long_ma_days = 1;
|
||||||
|
signal_dates_cfg.market_cap_lower_expr = "0".to_string();
|
||||||
|
signal_dates_cfg.market_cap_upper_expr = "100".to_string();
|
||||||
|
signal_dates_cfg.selection_limit_expr = "2".to_string();
|
||||||
|
signal_dates_cfg.stock_filter_expr = "close > 0".to_string();
|
||||||
|
signal_dates_cfg
|
||||||
|
.signal_rebalance_dates
|
||||||
|
.insert(third_ctx.decision_date);
|
||||||
|
let mut signal_dates_strategy = PlatformExprStrategy::new(signal_dates_cfg);
|
||||||
|
signal_dates_strategy.rebalance_day_counter = 2;
|
||||||
|
let signal_dates_decision = signal_dates_strategy
|
||||||
|
.on_day(&third_ctx)
|
||||||
|
.expect("signal-date refresh decision");
|
||||||
|
assert!(
|
||||||
|
signal_dates_decision
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.contains("periodic_rebalance=true")),
|
||||||
|
"{:?}",
|
||||||
|
signal_dates_decision.diagnostics
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
signal_dates_decision
|
||||||
|
.order_intents
|
||||||
|
.iter()
|
||||||
|
.any(|intent| matches!(intent, OrderIntent::Value { reason, .. } if reason == "periodic_rebalance_buy")),
|
||||||
|
"{:?}",
|
||||||
|
signal_dates_decision.order_intents
|
||||||
|
);
|
||||||
|
|
||||||
let mut no_retry_cfg = PlatformExprStrategyConfig::microcap_rotation();
|
let mut no_retry_cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||||
no_retry_cfg.signal_symbol = "000001.SZ".to_string();
|
no_retry_cfg.signal_symbol = "000001.SZ".to_string();
|
||||||
no_retry_cfg.refresh_rate = 15;
|
no_retry_cfg.refresh_rate = 15;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use chrono::NaiveTime;
|
use chrono::{NaiveDate, NaiveTime};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@@ -55,6 +55,10 @@ pub struct StrategyUniverseSpec {
|
|||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StrategyRebalanceSpec {
|
pub struct StrategyRebalanceSpec {
|
||||||
|
#[serde(default)]
|
||||||
|
pub frequency: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub dates: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub trade_times: Vec<String>,
|
pub trade_times: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -663,6 +667,9 @@ pub fn platform_expr_config_from_spec(
|
|||||||
{
|
{
|
||||||
cfg.market = market.clone();
|
cfg.market = market.clone();
|
||||||
}
|
}
|
||||||
|
if let Some(dates) = spec.rebalance.as_ref().and_then(signal_rebalance_dates) {
|
||||||
|
cfg.signal_rebalance_dates = dates;
|
||||||
|
}
|
||||||
if let Some(engine) = spec.engine_config.as_ref() {
|
if let Some(engine) = spec.engine_config.as_ref() {
|
||||||
if let Some(rank_limit) = engine.rank_limit.filter(|value| *value > 0) {
|
if let Some(rank_limit) = engine.rank_limit.filter(|value| *value > 0) {
|
||||||
cfg.max_positions = rank_limit;
|
cfg.max_positions = rank_limit;
|
||||||
@@ -1194,6 +1201,24 @@ fn parse_platform_rebalance_schedule(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn signal_rebalance_dates(rebalance: &StrategyRebalanceSpec) -> Option<BTreeSet<NaiveDate>> {
|
||||||
|
let frequency = rebalance
|
||||||
|
.frequency
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
if frequency != "signal_dates" && frequency != "signal-dates" && frequency != "signal dates" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dates = rebalance
|
||||||
|
.dates
|
||||||
|
.iter()
|
||||||
|
.filter_map(|value| NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d").ok())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
if dates.is_empty() { None } else { Some(dates) }
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_schedule_time_rule(
|
fn parse_schedule_time_rule(
|
||||||
schedule: &StrategyExpressionScheduleConfig,
|
schedule: &StrategyExpressionScheduleConfig,
|
||||||
) -> Option<ScheduleTimeRule> {
|
) -> Option<ScheduleTimeRule> {
|
||||||
@@ -1628,6 +1653,28 @@ mod tests {
|
|||||||
assert_eq!(cfg.weak_market_shrink_overweight_threshold, Some(1.2));
|
assert_eq!(cfg.weak_market_shrink_overweight_threshold, Some(1.2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_signal_dates_rebalance_into_platform_config() {
|
||||||
|
let spec = serde_json::json!({
|
||||||
|
"rebalance": {
|
||||||
|
"frequency": "signal_dates",
|
||||||
|
"dates": ["2025-11-10", "2025-11-11", "invalid"]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
cfg.signal_rebalance_dates
|
||||||
|
.contains(&NaiveDate::from_ymd_opt(2025, 11, 10).unwrap())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
cfg.signal_rebalance_dates
|
||||||
|
.contains(&NaiveDate::from_ymd_opt(2025, 11, 11).unwrap())
|
||||||
|
);
|
||||||
|
assert_eq!(cfg.signal_rebalance_dates.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_execution_cost_overrides_into_platform_config() {
|
fn parses_execution_cost_overrides_into_platform_config() {
|
||||||
let spec = serde_json::json!({
|
let spec = serde_json::json!({
|
||||||
|
|||||||
Reference in New Issue
Block a user