feat: add dated candidate universe contracts

This commit is contained in:
boris
2026-09-06 03:57:33 +08:00
parent 0f1d49bf63
commit 199f988b2e
2 changed files with 77 additions and 1 deletions
@@ -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]
+39 -1
View File
@@ -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);