保留平台策略全部交易时点

This commit is contained in:
boris
2026-08-28 12:44:20 +08:00
parent 59be3b5dd5
commit 4f647ef529
2 changed files with 147 additions and 9 deletions
+77 -6
View File
@@ -3,7 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc; use std::sync::Arc;
use ahash::{AHashMap, AHashSet}; use ahash::{AHashMap, AHashSet};
use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime}; use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
use rhai::{AST, Dynamic, Engine, Map, Scope}; use rhai::{AST, Dynamic, Engine, Map, Scope};
use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel}; use crate::broker::{MatchingType, RebalanceCashMode, SlippageModel};
@@ -429,6 +429,7 @@ pub struct PlatformExprStrategyConfig {
pub quote_quantity_limit: bool, pub quote_quantity_limit: bool,
pub current_day_precomputed_factors: bool, pub current_day_precomputed_factors: bool,
pub intraday_execution_time: Option<NaiveTime>, pub intraday_execution_time: Option<NaiveTime>,
pub explicit_action_times: Vec<NaiveTime>,
pub delayed_limit_open_exit_enabled: bool, pub delayed_limit_open_exit_enabled: bool,
pub delayed_limit_open_exit_time: Option<NaiveTime>, pub delayed_limit_open_exit_time: Option<NaiveTime>,
pub release_slot_on_exit_signal: bool, pub release_slot_on_exit_signal: bool,
@@ -500,6 +501,7 @@ impl PlatformExprStrategyConfig {
quote_quantity_limit: true, quote_quantity_limit: true,
current_day_precomputed_factors: false, current_day_precomputed_factors: false,
intraday_execution_time: None, intraday_execution_time: None,
explicit_action_times: Vec::new(),
delayed_limit_open_exit_enabled: false, delayed_limit_open_exit_enabled: false,
delayed_limit_open_exit_time: None, delayed_limit_open_exit_time: None,
release_slot_on_exit_signal: false, release_slot_on_exit_signal: false,
@@ -10430,11 +10432,21 @@ impl Strategy for PlatformExprStrategy {
PlatformExplicitActionStage::OnDay => ScheduleStage::OnDay, PlatformExplicitActionStage::OnDay => ScheduleStage::OnDay,
PlatformExplicitActionStage::Minute => ScheduleStage::Minute, PlatformExplicitActionStage::Minute => ScheduleStage::Minute,
}; };
let Some(schedule) = self.config.explicit_action_schedule.as_ref() else {
return Vec::new();
};
if self.config.explicit_action_times.is_empty() {
return vec![schedule.as_schedule_rule(stage)];
}
self.config self.config
.explicit_action_schedule .explicit_action_times
.as_ref() .iter()
.map(|schedule| schedule.as_schedule_rule(stage)) .map(|time| {
.into_iter() let mut timed_schedule = schedule.clone();
timed_schedule.time_rule =
Some(ScheduleTimeRule::physical_time(time.hour(), time.minute()));
timed_schedule.as_schedule_rule(stage)
})
.collect() .collect()
} }
@@ -10452,7 +10464,11 @@ impl Strategy for PlatformExprStrategy {
fn decision_quote_times(&self) -> Vec<NaiveTime> { fn decision_quote_times(&self) -> Vec<NaiveTime> {
let mut times = BTreeSet::new(); let mut times = BTreeSet::new();
if self.uses_intraday_execution_quotes() { if self.uses_intraday_execution_quotes() {
times.insert(self.intraday_execution_start_time()); if self.config.explicit_action_times.is_empty() {
times.insert(self.intraday_execution_start_time());
} else {
times.extend(self.config.explicit_action_times.iter().copied());
}
} }
if self.config.delayed_limit_open_exit_enabled { if self.config.delayed_limit_open_exit_enabled {
if let Some(time) = self.config.delayed_limit_open_exit_time { if let Some(time) = self.config.delayed_limit_open_exit_time {
@@ -21945,6 +21961,61 @@ mod tests {
)); ));
} }
#[test]
fn platform_explicit_actions_keep_every_declared_trade_time() {
let calendar = sample_calendar();
let date = d(2025, 2, 3);
let first = NaiveTime::from_hms_opt(10, 18, 0).unwrap();
let second = NaiveTime::from_hms_opt(14, 59, 0).unwrap();
let mut config = PlatformExprStrategyConfig::generic();
config.matching_type = MatchingType::MinuteLast;
config.intraday_execution_time = Some(second);
config.explicit_action_times = vec![first, second];
config.explicit_action_schedule = Some(PlatformRebalanceSchedule {
frequency: PlatformScheduleFrequency::Daily,
time_rule: Some(ScheduleTimeRule::physical_time(14, 59)),
});
config.explicit_actions = vec![PlatformTradeAction::Order {
kind: PlatformExplicitOrderKind::TargetPercent,
symbol: "000001.SZ".to_string(),
amount_expr: "0.5".to_string(),
limit_price_expr: None,
time_in_force: None,
start_time_expr: None,
end_time_expr: None,
when_expr: None,
reason: "multi_time_target".to_string(),
}];
let strategy = PlatformExprStrategy::new(config);
let rules = strategy.schedule_rules();
let scheduler = Scheduler::new(&calendar);
assert_eq!(rules.len(), 2);
assert_eq!(strategy.decision_quote_times(), vec![first, second]);
assert_eq!(
scheduler
.triggered_rules_at(date, ScheduleStage::OnDay, Some(first), &rules)
.len(),
1
);
assert_eq!(
scheduler
.triggered_rules_at(date, ScheduleStage::OnDay, Some(second), &rules)
.len(),
1
);
assert!(
scheduler
.triggered_rules_at(
date,
ScheduleStage::OnDay,
NaiveTime::from_hms_opt(12, 0, 0),
&rules,
)
.is_empty()
);
}
#[test] #[test]
fn platform_rebalance_date_due_does_not_use_the_on_day_default_clock() { fn platform_rebalance_date_due_does_not_use_the_on_day_default_clock() {
let calendar = sample_calendar(); let calendar = sample_calendar();
+70 -3
View File
@@ -2214,7 +2214,27 @@ pub fn platform_expr_config_from_spec(
cfg.benchmark_symbol = normalize_symbol(&cfg.benchmark_symbol, None); cfg.benchmark_symbol = normalize_symbol(&cfg.benchmark_symbol, None);
} }
let trade_times = spec_trade_times(spec); let trade_times = spec_trade_times(spec);
if let Some(main_trade_time) = trade_times.last().copied() { let explicit_trading_schedule = spec
.runtime_expressions
.as_ref()
.and_then(|runtime| runtime.trading.as_ref())
.and_then(|trading| trading.schedule.as_ref());
if !cfg.explicit_actions.is_empty() {
cfg.explicit_action_times = if explicit_trading_schedule.is_some() {
explicit_trading_schedule
.and_then(parse_schedule_execution_time)
.into_iter()
.collect()
} else {
trade_times.clone()
};
}
if let Some(main_trade_time) = cfg
.explicit_action_times
.last()
.or_else(|| trade_times.last())
.copied()
{
cfg.intraday_execution_time = Some(main_trade_time); cfg.intraday_execution_time = Some(main_trade_time);
} }
if let Some(execution) = spec.execution.as_ref() { if let Some(execution) = spec.execution.as_ref() {
@@ -2359,6 +2379,8 @@ fn parse_schedule_clock_time(raw: Option<&str>) -> Option<NaiveTime> {
fn parse_trade_times(raw: &[String]) -> Vec<NaiveTime> { fn parse_trade_times(raw: &[String]) -> Vec<NaiveTime> {
raw.iter() raw.iter()
.filter_map(|item| parse_schedule_clock_time(Some(item.as_str()))) .filter_map(|item| parse_schedule_clock_time(Some(item.as_str())))
.collect::<BTreeSet<_>>()
.into_iter()
.collect() .collect()
} }
@@ -3858,9 +3880,17 @@ mod tests {
#[test] #[test]
fn multiple_trade_times_do_not_imply_delayed_limit_exit() { fn multiple_trade_times_do_not_imply_delayed_limit_exit() {
let spec = serde_json::json!({ let spec = serde_json::json!({
"rebalance": { "tradeTimes": ["10:31", "10:40"] }, "rebalance": { "tradeTimes": ["10:40", "10:31", "10:40"] },
"runtimeExpressions": { "runtimeExpressions": {
"schedule": { "frequency": "daily", "time": "10:40" } "schedule": { "frequency": "daily", "time": "10:40" },
"trading": {
"rotationEnabled": false,
"actions": [{
"kind": "target_percent",
"symbol": "000001.SZ",
"amountExpr": "0.5"
}]
}
} }
}); });
@@ -3870,10 +3900,47 @@ mod tests {
cfg.intraday_execution_time, cfg.intraday_execution_time,
Some(NaiveTime::from_hms_opt(10, 40, 0).unwrap()) Some(NaiveTime::from_hms_opt(10, 40, 0).unwrap())
); );
assert_eq!(
cfg.explicit_action_times,
vec![
NaiveTime::from_hms_opt(10, 31, 0).unwrap(),
NaiveTime::from_hms_opt(10, 40, 0).unwrap(),
]
);
assert!(!cfg.delayed_limit_open_exit_enabled); assert!(!cfg.delayed_limit_open_exit_enabled);
assert_eq!(cfg.delayed_limit_open_exit_time, None); assert_eq!(cfg.delayed_limit_open_exit_time, None);
} }
#[test]
fn explicit_trading_schedule_overrides_rebalance_trade_times() {
let spec = serde_json::json!({
"rebalance": { "tradeTimes": ["10:18", "14:59"] },
"runtimeExpressions": {
"schedule": { "frequency": "daily", "time": "14:59" },
"trading": {
"rotationEnabled": false,
"schedule": { "frequency": "daily", "time": "11:07" },
"actions": [{
"kind": "target_percent",
"symbol": "000001.SZ",
"amountExpr": "0.5"
}]
}
}
});
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert_eq!(
cfg.explicit_action_times,
vec![NaiveTime::from_hms_opt(11, 7, 0).unwrap()]
);
assert_eq!(
cfg.intraday_execution_time,
Some(NaiveTime::from_hms_opt(11, 7, 0).unwrap())
);
}
#[test] #[test]
fn rejects_removed_compatibility_profile() { fn rejects_removed_compatibility_profile() {
let spec = serde_json::json!({ let spec = serde_json::json!({