feat: add dated candidate universe contracts
This commit is contained in:
@@ -439,6 +439,7 @@ pub struct PlatformExprStrategyConfig {
|
||||
pub matching_type: MatchingType,
|
||||
pub quote_quantity_limit: bool,
|
||||
pub current_day_precomputed_factors: bool,
|
||||
pub candidate_symbols_by_date: BTreeMap<NaiveDate, BTreeSet<String>>,
|
||||
pub intraday_execution_time: Option<NaiveTime>,
|
||||
pub explicit_action_times: Vec<NaiveTime>,
|
||||
pub delayed_limit_open_exit_enabled: bool,
|
||||
@@ -513,6 +514,7 @@ impl PlatformExprStrategyConfig {
|
||||
matching_type: MatchingType::CurrentBarClose,
|
||||
quote_quantity_limit: true,
|
||||
current_day_precomputed_factors: false,
|
||||
candidate_symbols_by_date: BTreeMap::new(),
|
||||
intraday_execution_time: None,
|
||||
explicit_action_times: Vec::new(),
|
||||
delayed_limit_open_exit_enabled: false,
|
||||
@@ -9590,6 +9592,15 @@ impl PlatformExprStrategy {
|
||||
let factor_symbol_ids = factor_day.factor_symbol_ids();
|
||||
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
|
||||
for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) {
|
||||
if !self.config.candidate_symbols_by_date.is_empty()
|
||||
&& !self
|
||||
.config
|
||||
.candidate_symbols_by_date
|
||||
.get(&date)
|
||||
.is_some_and(|symbols| symbols.contains(&factor.symbol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
|
||||
continue;
|
||||
}
|
||||
@@ -31514,6 +31525,33 @@ mod tests {
|
||||
decision.order_intents,
|
||||
decision.diagnostics
|
||||
);
|
||||
|
||||
let mut filtered_cfg = PlatformExprStrategyConfig::microcap_rotation();
|
||||
filtered_cfg.signal_symbol = "000001.SZ".to_string();
|
||||
filtered_cfg.refresh_rate = 99;
|
||||
filtered_cfg.max_positions = 1;
|
||||
filtered_cfg.benchmark_short_ma_days = 1;
|
||||
filtered_cfg.benchmark_long_ma_days = 1;
|
||||
filtered_cfg.market_cap_lower_expr = "0".to_string();
|
||||
filtered_cfg.market_cap_upper_expr = "100".to_string();
|
||||
filtered_cfg.selection_limit_expr = "1".to_string();
|
||||
filtered_cfg.stock_filter_expr = "close > 0".to_string();
|
||||
filtered_cfg.current_day_precomputed_factors = true;
|
||||
filtered_cfg.retry_empty_rebalance = true;
|
||||
filtered_cfg
|
||||
.candidate_symbols_by_date
|
||||
.insert(curr, BTreeSet::from(["300002.SZ".to_string()]));
|
||||
let mut filtered_strategy = PlatformExprStrategy::new(filtered_cfg);
|
||||
let filtered = filtered_strategy.on_day(&ctx).expect("filtered decision");
|
||||
assert!(
|
||||
matches!(
|
||||
filtered.order_intents.first(),
|
||||
Some(crate::strategy::OrderIntent::TargetValue { symbol, .. }) if symbol == "300002.SZ"
|
||||
),
|
||||
"intents={:?} diagnostics={:?}",
|
||||
filtered.order_intents,
|
||||
filtered.diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -892,6 +892,8 @@ pub struct StrategyExpressionSelectionConfig {
|
||||
pub stock_filter_expr: Option<String>,
|
||||
#[serde(default, alias = "current_day_precomputed_factors")]
|
||||
pub current_day_precomputed_factors: Option<bool>,
|
||||
#[serde(default, alias = "candidate_symbols_by_date")]
|
||||
pub candidate_symbols_by_date: BTreeMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
@@ -2040,6 +2042,31 @@ pub fn platform_expr_config_from_spec(
|
||||
if let Some(enabled) = selection.current_day_precomputed_factors {
|
||||
cfg.current_day_precomputed_factors = enabled;
|
||||
}
|
||||
for (raw_date, raw_symbols) in &selection.candidate_symbols_by_date {
|
||||
let trade_date = NaiveDate::parse_from_str(raw_date, "%Y-%m-%d").map_err(|_| {
|
||||
format!("candidateSymbolsByDate contains invalid date: {raw_date}")
|
||||
})?;
|
||||
let mut symbols = BTreeSet::new();
|
||||
for raw_symbol in raw_symbols {
|
||||
let symbol = normalize_symbol(raw_symbol, None);
|
||||
let valid = symbol.rsplit_once('.').is_some_and(|(code, exchange)| {
|
||||
code.len() == 6
|
||||
&& code.bytes().all(|byte| byte.is_ascii_digit())
|
||||
&& matches!(exchange, "SH" | "SZ" | "BJ")
|
||||
});
|
||||
if !valid {
|
||||
return Err(format!(
|
||||
"candidateSymbolsByDate contains invalid stock symbol: {raw_symbol}"
|
||||
));
|
||||
}
|
||||
if !symbols.insert(symbol.clone()) {
|
||||
return Err(format!(
|
||||
"candidateSymbolsByDate contains duplicate date/symbol: {raw_date} {symbol}"
|
||||
));
|
||||
}
|
||||
}
|
||||
cfg.candidate_symbols_by_date.insert(trade_date, symbols);
|
||||
}
|
||||
}
|
||||
if let Some(allocation) = runtime_expr.allocation.as_ref()
|
||||
&& let Some(expr) = allocation
|
||||
@@ -3057,7 +3084,11 @@ mod tests {
|
||||
"marketCapLowerExpr": "3",
|
||||
"marketCapUpperExpr": "28",
|
||||
"stockFilterExpr": "stock_ma5 > stock_ma10",
|
||||
"currentDayPrecomputedFactors": true
|
||||
"currentDayPrecomputedFactors": true,
|
||||
"candidateSymbolsByDate": {
|
||||
"2025-01-02": ["600000.sh", "000001.SZ"],
|
||||
"2025-01-03": []
|
||||
}
|
||||
},
|
||||
"trading": {
|
||||
"refreshRateExpr": "year >= 2024 ? 5 : 20",
|
||||
@@ -3089,6 +3120,13 @@ mod tests {
|
||||
assert!(cfg.daily_top_up_enabled);
|
||||
assert!(cfg.retry_empty_rebalance);
|
||||
assert!(cfg.current_day_precomputed_factors);
|
||||
assert_eq!(
|
||||
cfg.candidate_symbols_by_date[&NaiveDate::from_ymd_opt(2025, 1, 2).unwrap()],
|
||||
BTreeSet::from(["000001.SZ".to_string(), "600000.SH".to_string()])
|
||||
);
|
||||
assert!(
|
||||
cfg.candidate_symbols_by_date[&NaiveDate::from_ymd_opt(2025, 1, 3).unwrap()].is_empty()
|
||||
);
|
||||
assert_eq!(cfg.weak_market_shrink_overweight_threshold, Some(1.1));
|
||||
assert!(!cfg.calendar_rebalance_interval);
|
||||
assert_eq!(cfg.explicit_actions.len(), 1);
|
||||
|
||||
Reference in New Issue
Block a user