From f507c63069278e82b6d2f8bd9922636a0376bc78 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 4 Jul 2026 05:19:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E6=95=9B=E5=B9=B3=E5=8F=B0=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E9=80=89=E8=82=A1=E9=A3=8E=E6=8E=A7=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 195 ++++++++++++++++-- 1 file changed, 181 insertions(+), 14 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 80c5fa9..4bb3e53 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -13,9 +13,7 @@ use crate::data::{ use crate::engine::BacktestError; use crate::events::OrderSide; use crate::portfolio::PortfolioState; -use crate::risk_control::{ - ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit, RiskCheckScope, -}; +use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, FidcRiskDecisionAudit}; use crate::scheduler::{ ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler, default_stage_time, }; @@ -6036,22 +6034,13 @@ impl PlatformExprStrategy { candidate: &crate::data::CandidateEligibility, market: &DailyMarketSnapshot, ) -> Option<&'static str> { - if let Some(reason) = ChinaAShareRiskControl::baseline_rejection_reason_with_config( + ChinaAShareRiskControl::selection_rejection_reason_with_config( date, candidate, market, ctx.data.instrument(symbol), &self.config.risk_config, - RiskCheckScope::Selection, - ) { - return Some(reason); - } - if self.config.risk_config.static_rules.respect_allow_buy_sell - && (!candidate.allow_buy || !candidate.allow_sell) - { - return Some("trade_disabled"); - } - None + ) } fn stock_selection_limit_rejection_reason( @@ -8635,6 +8624,184 @@ mod tests { assert_eq!(universe[0].symbol, symbol); } + #[test] + fn platform_selection_uses_complete_static_risk_policy_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: 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!( + default_strategy + .selectable_universe_on(&ctx, date, date) + .is_empty() + ); + let rejected_rules = default_strategy + .selection_risk_decisions(&ctx, date, date) + .into_iter() + .map(|decision| (decision.symbol, decision.rule_code)) + .collect::>(); + assert_eq!( + rejected_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()), + ]) + ); + + let mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.risk_config.static_rules.reject_upper_limit_selection = false; + cfg.risk_config.static_rules.reject_lower_limit_selection = false; + cfg.risk_config.static_rules.reject_new_listing_selection = false; + cfg.risk_config.static_rules.reject_inactive_selection = false; + let relaxed_strategy = PlatformExprStrategy::new(cfg); + let relaxed_universe = relaxed_strategy.selectable_universe_on(&ctx, date, date); + assert_eq!( + relaxed_universe + .iter() + .map(|row| row.symbol.clone()) + .collect::>(), + symbols + .iter() + .map(|symbol| (*symbol).to_string()) + .collect::>() + ); + assert!( + relaxed_strategy + .selection_risk_decisions(&ctx, date, date) + .is_empty() + ); + } + #[test] fn platform_selection_and_buy_respect_allow_flags_are_configurable() { let date = d(2025, 1, 2);