feat: add dated candidate universe contracts
This commit is contained in:
@@ -439,6 +439,7 @@ pub struct PlatformExprStrategyConfig {
|
|||||||
pub matching_type: MatchingType,
|
pub matching_type: MatchingType,
|
||||||
pub quote_quantity_limit: bool,
|
pub quote_quantity_limit: bool,
|
||||||
pub current_day_precomputed_factors: bool,
|
pub current_day_precomputed_factors: bool,
|
||||||
|
pub candidate_symbols_by_date: BTreeMap<NaiveDate, BTreeSet<String>>,
|
||||||
pub intraday_execution_time: Option<NaiveTime>,
|
pub intraday_execution_time: Option<NaiveTime>,
|
||||||
pub explicit_action_times: Vec<NaiveTime>,
|
pub explicit_action_times: Vec<NaiveTime>,
|
||||||
pub delayed_limit_open_exit_enabled: bool,
|
pub delayed_limit_open_exit_enabled: bool,
|
||||||
@@ -513,6 +514,7 @@ impl PlatformExprStrategyConfig {
|
|||||||
matching_type: MatchingType::CurrentBarClose,
|
matching_type: MatchingType::CurrentBarClose,
|
||||||
quote_quantity_limit: true,
|
quote_quantity_limit: true,
|
||||||
current_day_precomputed_factors: false,
|
current_day_precomputed_factors: false,
|
||||||
|
candidate_symbols_by_date: BTreeMap::new(),
|
||||||
intraday_execution_time: None,
|
intraday_execution_time: None,
|
||||||
explicit_action_times: Vec::new(),
|
explicit_action_times: Vec::new(),
|
||||||
delayed_limit_open_exit_enabled: false,
|
delayed_limit_open_exit_enabled: false,
|
||||||
@@ -9590,6 +9592,15 @@ impl PlatformExprStrategy {
|
|||||||
let factor_symbol_ids = factor_day.factor_symbol_ids();
|
let factor_symbol_ids = factor_day.factor_symbol_ids();
|
||||||
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
|
debug_assert_eq!(factor_rows.len(), factor_symbol_ids.len());
|
||||||
for (factor, symbol_id) in factor_rows.iter().zip(factor_symbol_ids.iter().copied()) {
|
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) {
|
if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -31514,6 +31525,33 @@ mod tests {
|
|||||||
decision.order_intents,
|
decision.order_intents,
|
||||||
decision.diagnostics
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -892,6 +892,8 @@ pub struct StrategyExpressionSelectionConfig {
|
|||||||
pub stock_filter_expr: Option<String>,
|
pub stock_filter_expr: Option<String>,
|
||||||
#[serde(default, alias = "current_day_precomputed_factors")]
|
#[serde(default, alias = "current_day_precomputed_factors")]
|
||||||
pub current_day_precomputed_factors: Option<bool>,
|
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)]
|
#[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 {
|
if let Some(enabled) = selection.current_day_precomputed_factors {
|
||||||
cfg.current_day_precomputed_factors = enabled;
|
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()
|
if let Some(allocation) = runtime_expr.allocation.as_ref()
|
||||||
&& let Some(expr) = allocation
|
&& let Some(expr) = allocation
|
||||||
@@ -3057,7 +3084,11 @@ mod tests {
|
|||||||
"marketCapLowerExpr": "3",
|
"marketCapLowerExpr": "3",
|
||||||
"marketCapUpperExpr": "28",
|
"marketCapUpperExpr": "28",
|
||||||
"stockFilterExpr": "stock_ma5 > stock_ma10",
|
"stockFilterExpr": "stock_ma5 > stock_ma10",
|
||||||
"currentDayPrecomputedFactors": true
|
"currentDayPrecomputedFactors": true,
|
||||||
|
"candidateSymbolsByDate": {
|
||||||
|
"2025-01-02": ["600000.sh", "000001.SZ"],
|
||||||
|
"2025-01-03": []
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"trading": {
|
"trading": {
|
||||||
"refreshRateExpr": "year >= 2024 ? 5 : 20",
|
"refreshRateExpr": "year >= 2024 ? 5 : 20",
|
||||||
@@ -3089,6 +3120,13 @@ mod tests {
|
|||||||
assert!(cfg.daily_top_up_enabled);
|
assert!(cfg.daily_top_up_enabled);
|
||||||
assert!(cfg.retry_empty_rebalance);
|
assert!(cfg.retry_empty_rebalance);
|
||||||
assert!(cfg.current_day_precomputed_factors);
|
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_eq!(cfg.weak_market_shrink_overweight_threshold, Some(1.1));
|
||||||
assert!(!cfg.calendar_rebalance_interval);
|
assert!(!cfg.calendar_rebalance_interval);
|
||||||
assert_eq!(cfg.explicit_actions.len(), 1);
|
assert_eq!(cfg.explicit_actions.len(), 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user