From 203a20592aef2d7df46b2c24b0d3cbdab0a6e5f9 Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 5 Jul 2026 18:19:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E5=B9=B3=E5=8F=B0=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E5=BC=8F=E9=80=89=E8=82=A1=E9=A3=8E=E6=8E=A7=E7=BC=BA?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/data.rs | 2 +- .../fidc-core/src/platform_expr_strategy.rs | 135 +++++++++++++++++- 2 files changed, 132 insertions(+), 5 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 3c38f0c..42dacf4 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -3156,7 +3156,7 @@ fn build_eligible_universe_for_date_from_factors( rows } -fn missing_candidate_risk_state(date: NaiveDate, symbol: &str) -> CandidateEligibility { +pub(crate) fn missing_candidate_risk_state(date: NaiveDate, symbol: &str) -> CandidateEligibility { CandidateEligibility { date, symbol: symbol.to_string(), diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 13bce69..a0702a6 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -6175,8 +6175,13 @@ impl PlatformExprStrategy { if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) { continue; } - let Some(candidate) = ctx.data.candidate(date, &factor.symbol) else { - continue; + let synthetic_candidate; + let candidate = if let Some(candidate) = ctx.data.candidate(date, &factor.symbol) { + candidate + } else { + synthetic_candidate = + crate::data::missing_candidate_risk_state(date, &factor.symbol); + &synthetic_candidate }; let Some(market) = ctx.data.market(date, &factor.symbol) else { continue; @@ -6237,8 +6242,13 @@ impl PlatformExprStrategy { if ctx.has_dynamic_universe() && !ctx.dynamic_universe_contains(&factor.symbol) { continue; } - let Some(candidate) = ctx.data.candidate(date, &factor.symbol) else { - continue; + let synthetic_candidate; + let candidate = if let Some(candidate) = ctx.data.candidate(date, &factor.symbol) { + candidate + } else { + synthetic_candidate = + crate::data::missing_candidate_risk_state(date, &factor.symbol); + &synthetic_candidate }; let Some(market) = ctx.data.market(date, &factor.symbol) else { continue; @@ -9170,6 +9180,123 @@ mod tests { assert_eq!(universe[0].symbol, symbol); } + #[test] + fn platform_selection_missing_candidate_risk_state_does_not_change_default_universe() { + let date = d(2025, 1, 2); + let symbol = "000001.SZ"; + let data = DataSet::from_components( + vec![Instrument { + symbol: symbol.to_string(), + name: symbol.to_string(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: Some(d(2020, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }], + vec![DailyMarketSnapshot { + date, + symbol: symbol.to_string(), + timestamp: None, + day_open: 10.0, + open: 10.0, + high: 10.2, + low: 9.8, + close: 10.1, + last_price: 10.1, + bid1: 10.0, + ask1: 10.1, + prev_close: 10.0, + volume: 1_000_000, + minute_volume: 10_000, + bid1_volume: 10_000, + ask1_volume: 10_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 11.0, + lower_limit: 9.0, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn: 10.0, + free_float_cap_bn: 9.0, + pe_ttm: 12.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }], + Vec::::new(), + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1001.0, + prev_close: 999.0, + volume: 1_000_000, + }], + ) + .expect("dataset"); + let portfolio = PortfolioState::new(1_000_000.0); + let subscriptions = BTreeSet::new(); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 0, + data: &data, + portfolio: &portfolio, + futures_account: None, + open_orders: &[], + dynamic_universe: None, + subscriptions: &subscriptions, + process_events: &[], + active_process_event: None, + active_datetime: None, + order_events: &[], + fills: &[], + }; + + let default_strategy = + PlatformExprStrategy::new(PlatformExprStrategyConfig::microcap_rotation()); + assert_eq!( + default_strategy + .selectable_universe_on(&ctx, date, date) + .iter() + .map(|row| row.symbol.as_str()) + .collect::>(), + vec![symbol], + "default selection must not drop source rows only because risk facts are missing" + ); + assert!( + default_strategy + .selection_risk_decisions(&ctx, date, date) + .is_empty() + ); + + let mut strict_cfg = PlatformExprStrategyConfig::microcap_rotation(); + strict_cfg.risk_config.static_rules.reject_st_selection = true; + let strict_strategy = PlatformExprStrategy::new(strict_cfg); + assert!( + strict_strategy + .selectable_universe_on(&ctx, date, date) + .is_empty(), + "explicit selection risk can still reject missing risk facts" + ); + let diagnostics = PlatformExprStrategy::selection_risk_decision_diagnostics( + &strict_strategy.selection_risk_decisions(&ctx, date, date), + date, + date, + 5, + ); + assert!( + diagnostics + .iter() + .any(|item| item.contains("\"rule_code\":\"missing_risk_state\"")), + "{diagnostics:?}" + ); + } + #[test] fn platform_selection_uses_complete_static_risk_policy_before_universe_output() { let date = d(2025, 1, 2);