保留平台策略全部交易时点
This commit is contained in:
@@ -3,7 +3,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
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 crate::broker::{MatchingType, RebalanceCashMode, SlippageModel};
|
||||
@@ -429,6 +429,7 @@ pub struct PlatformExprStrategyConfig {
|
||||
pub quote_quantity_limit: bool,
|
||||
pub current_day_precomputed_factors: bool,
|
||||
pub intraday_execution_time: Option<NaiveTime>,
|
||||
pub explicit_action_times: Vec<NaiveTime>,
|
||||
pub delayed_limit_open_exit_enabled: bool,
|
||||
pub delayed_limit_open_exit_time: Option<NaiveTime>,
|
||||
pub release_slot_on_exit_signal: bool,
|
||||
@@ -500,6 +501,7 @@ impl PlatformExprStrategyConfig {
|
||||
quote_quantity_limit: true,
|
||||
current_day_precomputed_factors: false,
|
||||
intraday_execution_time: None,
|
||||
explicit_action_times: Vec::new(),
|
||||
delayed_limit_open_exit_enabled: false,
|
||||
delayed_limit_open_exit_time: None,
|
||||
release_slot_on_exit_signal: false,
|
||||
@@ -10430,11 +10432,21 @@ impl Strategy for PlatformExprStrategy {
|
||||
PlatformExplicitActionStage::OnDay => ScheduleStage::OnDay,
|
||||
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
|
||||
.explicit_action_schedule
|
||||
.as_ref()
|
||||
.map(|schedule| schedule.as_schedule_rule(stage))
|
||||
.into_iter()
|
||||
.explicit_action_times
|
||||
.iter()
|
||||
.map(|time| {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -10452,7 +10464,11 @@ impl Strategy for PlatformExprStrategy {
|
||||
fn decision_quote_times(&self) -> Vec<NaiveTime> {
|
||||
let mut times = BTreeSet::new();
|
||||
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 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]
|
||||
fn platform_rebalance_date_due_does_not_use_the_on_day_default_clock() {
|
||||
let calendar = sample_calendar();
|
||||
|
||||
@@ -2214,7 +2214,27 @@ pub fn platform_expr_config_from_spec(
|
||||
cfg.benchmark_symbol = normalize_symbol(&cfg.benchmark_symbol, None);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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> {
|
||||
raw.iter()
|
||||
.filter_map(|item| parse_schedule_clock_time(Some(item.as_str())))
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -3858,9 +3880,17 @@ mod tests {
|
||||
#[test]
|
||||
fn multiple_trade_times_do_not_imply_delayed_limit_exit() {
|
||||
let spec = serde_json::json!({
|
||||
"rebalance": { "tradeTimes": ["10:31", "10:40"] },
|
||||
"rebalance": { "tradeTimes": ["10:40", "10:31", "10:40"] },
|
||||
"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,
|
||||
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_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]
|
||||
fn rejects_removed_compatibility_profile() {
|
||||
let spec = serde_json::json!({
|
||||
|
||||
Reference in New Issue
Block a user