From 7059d3a8d03731cfaff6a22dc6a31eb03c011558 Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 5 Jul 2026 17:44:34 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85next-open=E9=80=89=E8=82=A1?= =?UTF-8?q?=E9=A3=8E=E6=8E=A7=E5=BB=B6=E5=90=8E=E5=9B=9E=E5=BD=92=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 187 +++++++++++++++++- 1 file changed, 186 insertions(+), 1 deletion(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 79243b5..13bce69 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -8686,7 +8686,8 @@ mod tests { PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig, PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformTradeAction, - PlatformUniverseActionKind, StockFilterQuoteUsage, precomputed_stock_rolling_mean, + PlatformUniverseActionKind, SelectionRiskDeferral, StockFilterQuoteUsage, + precomputed_stock_rolling_mean, }; use crate::{ AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction, @@ -9376,6 +9377,190 @@ mod tests { ); } + #[test] + fn platform_next_open_defers_complete_static_selection_risk_before_universe_output() { + let date = d(2025, 1, 2); + let symbols = [ + "000001.SZ", + "000002.SZ", + "000003.SZ", + "000004.SZ", + "000005.SZ", + ]; + let instruments = symbols + .iter() + .map(|symbol| 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: if *symbol == "000004.SZ" { + Some(date) + } else { + None + }, + status: if *symbol == "000004.SZ" { + "delisted".to_string() + } else { + "active".to_string() + }, + }) + .collect::>(); + let market_rows = symbols + .iter() + .map(|symbol| { + let (last_price, upper_limit, lower_limit) = match *symbol { + "000001.SZ" => (11.0, 11.0, 9.0), + "000002.SZ" => (9.0, 11.0, 9.0), + _ => (10.0, 11.0, 9.0), + }; + DailyMarketSnapshot { + date, + symbol: (*symbol).to_string(), + timestamp: None, + day_open: last_price, + open: last_price, + high: last_price, + low: last_price, + close: last_price, + last_price, + bid1: last_price, + ask1: last_price, + 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, + lower_limit, + price_tick: 0.01, + } + }) + .collect::>(); + let factor_rows = symbols + .iter() + .enumerate() + .map(|(index, symbol)| DailyFactorSnapshot { + date, + symbol: (*symbol).to_string(), + market_cap_bn: 10.0 + index as f64, + free_float_cap_bn: 9.0 + index as f64, + pe_ttm: 12.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }) + .collect::>(); + let candidate_rows = symbols + .iter() + .map(|symbol| CandidateEligibility { + date, + symbol: (*symbol).to_string(), + is_st: false, + is_star_st: false, + is_new_listing: *symbol == "000003.SZ", + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + risk_level_code: if *symbol == "000005.SZ" { + Some("missing_risk_state:active_status".to_string()) + } else { + None + }, + }) + .collect::>(); + let data = DataSet::from_components( + instruments, + market_rows, + factor_rows, + candidate_rows, + 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: d(2025, 1, 3), + 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: &[], + }; + assert!(ctx.is_lagged_execution()); + + let mut strict_cfg = PlatformExprStrategyConfig::microcap_rotation(); + strict_cfg + .risk_config + .static_rules + .reject_upper_limit_selection = true; + strict_cfg + .risk_config + .static_rules + .reject_lower_limit_selection = true; + strict_cfg + .risk_config + .static_rules + .reject_new_listing_selection = true; + strict_cfg + .risk_config + .static_rules + .reject_inactive_selection = true; + let strict_strategy = PlatformExprStrategy::new(strict_cfg); + + let deferred_universe = strict_strategy.selectable_universe_on_with_options( + &ctx, + date, + date, + SelectionRiskDeferral::All, + ); + assert_eq!( + deferred_universe + .iter() + .map(|row| row.symbol.clone()) + .collect::>(), + symbols + .iter() + .map(|symbol| (*symbol).to_string()) + .collect::>() + ); + let deferred_rules = strict_strategy + .selection_risk_decisions_with_options(&ctx, date, date, SelectionRiskDeferral::All) + .into_iter() + .map(|decision| (decision.symbol, decision.rule_code)) + .collect::>(); + assert_eq!( + deferred_rules, + BTreeSet::from([ + ("000001.SZ".to_string(), "upper_limit".to_string()), + ("000002.SZ".to_string(), "lower_limit".to_string()), + ("000003.SZ".to_string(), "new_listing".to_string()), + ("000004.SZ".to_string(), "inactive_or_delisted".to_string()), + ("000005.SZ".to_string(), "missing_risk_state".to_string()), + ]) + ); + } + #[test] fn platform_selection_and_buy_respect_allow_flags_are_configurable() { let date = d(2025, 1, 2);