保留平台策略全部交易时点
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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!({
|
||||||
|
|||||||
Reference in New Issue
Block a user