From 92d5801f63952c14850e03837dd373e6561c5b7c Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 6 Jul 2026 05:45:52 +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=E8=A1=A5?= =?UTF-8?q?=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 213 +++++++++++++++--- 1 file changed, 184 insertions(+), 29 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index de1c964..5629d0a 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -6726,15 +6726,6 @@ impl PlatformExprStrategy { } continue; } - if !ctx.is_lagged_execution() - && let Some(reason) = - self.buy_rejection_reason(ctx, date, &candidate.symbol, &stock)? - { - if diagnostics.len() < 12 { - diagnostics.push(format!("{} rejected by {}", candidate.symbol, reason)); - } - continue; - } if !self.stock_passes_expr(ctx, day, &stock)? { if diagnostics.len() < 12 { diagnostics.push(format!("{} rejected by stock_expr", candidate.symbol)); @@ -7427,14 +7418,6 @@ impl PlatformExprStrategy { } continue; } - if !ctx.is_lagged_execution() - && let Some(reason) = self.buy_rejection_reason(ctx, date, symbol, &stock)? - { - if diagnostics.len() < 12 { - diagnostics.push(format!("{symbol} quote_plan rejected by {reason}")); - } - continue; - } if quote_usage == StockFilterQuoteUsage::IntradayQuote && !self.stock_passes_expr(ctx, day, &stock)? { @@ -12890,6 +12873,165 @@ mod tests { assert!(selected.is_empty()); } + #[test] + fn platform_selection_does_not_fill_candidates_with_buy_risk_rejections() { + let date = d(2025, 11, 10); + let signal = "000001.SH"; + let st_symbol = "300268.SZ"; + let normal_symbol = "301167.SZ"; + let market = |symbol: &str, close: f64| DailyMarketSnapshot { + date, + symbol: symbol.to_string(), + timestamp: Some(format!("{date} 15:00:00")), + day_open: close, + open: close, + high: close, + low: close, + close, + last_price: close, + bid1: close, + ask1: close, + prev_close: close, + 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: close * 1.1, + lower_limit: close * 0.9, + price_tick: 0.01, + }; + let data = DataSet::from_components( + [signal, st_symbol, normal_symbol] + .into_iter() + .map(|symbol| Instrument { + symbol: symbol.to_string(), + name: symbol.to_string(), + board: if symbol.ends_with(".SH") { "SH" } else { "SZ" }.to_string(), + round_lot: 100, + listed_at: Some(d(2020, 1, 1)), + delisted_at: None, + status: "active".to_string(), + }) + .collect(), + vec![ + market(signal, 10.0), + market(st_symbol, 10.0), + market(normal_symbol, 10.0), + ], + vec![ + DailyFactorSnapshot { + date, + symbol: st_symbol.to_string(), + market_cap_bn: 1.0, + free_float_cap_bn: 1.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }, + DailyFactorSnapshot { + date, + symbol: normal_symbol.to_string(), + market_cap_bn: 2.0, + free_float_cap_bn: 2.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }, + ], + vec![ + CandidateEligibility { + date, + symbol: st_symbol.to_string(), + is_st: true, + is_star_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: false, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + risk_level_code: None, + }, + CandidateEligibility { + date, + symbol: normal_symbol.to_string(), + is_st: false, + is_star_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + risk_level_code: None, + }, + ], + vec![BenchmarkSnapshot { + date, + benchmark: "000852.SH".to_string(), + open: 1000.0, + close: 1002.0, + prev_close: 998.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 mut cfg = PlatformExprStrategyConfig::microcap_rotation(); + cfg.aiquant_transaction_cost = true; + cfg.signal_symbol = signal.to_string(); + cfg.market_cap_lower_expr = "0".to_string(); + cfg.market_cap_upper_expr = "100".to_string(); + cfg.selection_limit_expr = "1".to_string(); + cfg.stock_filter_expr = "true".to_string(); + cfg.benchmark_short_ma_days = 1; + cfg.benchmark_long_ma_days = 1; + let strategy = PlatformExprStrategy::new(cfg); + let day = strategy.day_state(&ctx, date).expect("day state"); + + let (selected, diagnostics, _) = strategy + .select_symbols(&ctx, date, date, date, &day, 0.0, 100.0, 1) + .expect("select symbols"); + + assert_eq!(selected, vec![st_symbol.to_string()]); + assert!( + diagnostics + .iter() + .all(|line| !line.contains("rejected by st")), + "{diagnostics:?}" + ); + let stock = strategy + .selection_stock_state_with_factor_date(&ctx, date, date, st_symbol) + .expect("stock state"); + assert_eq!( + strategy + .buy_rejection_reason(&ctx, date, st_symbol, &stock) + .expect("buy risk"), + Some("st".to_string()) + ); + } + #[test] fn platform_next_open_select_symbols_defers_decision_day_limit_state() { let decision_date = d(2025, 1, 2); @@ -17403,7 +17545,7 @@ mod tests { } #[test] - fn platform_strategy_baseline_risk_excludes_new_listings_even_when_config_omits_it() { + fn platform_strategy_baseline_risk_rejects_new_listing_buy_without_selection_replacement() { let date = d(2025, 2, 3); let symbols = ["301001.SZ", "000001.SZ"]; let data = DataSet::from_components( @@ -17546,14 +17688,22 @@ mod tests { let decision = strategy.on_day(&ctx).expect("platform decision"); - assert!(!decision.order_intents.iter().any(|intent| matches!( - intent, - crate::strategy::OrderIntent::Value { symbol, .. } if symbol == "301001.SZ" - ))); - assert!(decision.order_intents.iter().any(|intent| matches!( - intent, - crate::strategy::OrderIntent::Value { symbol, .. } if symbol == "000001.SZ" - ))); + assert!( + decision + .diagnostics + .iter() + .any(|item| item == "selected_symbols=301001.SZ"), + "{:?}", + decision.diagnostics + ); + assert!( + decision.order_intents.iter().all(|intent| !matches!( + intent, + crate::strategy::OrderIntent::Value { symbol, .. } if symbol == "301001.SZ" || symbol == "000001.SZ" + )), + "{:?}", + decision.order_intents + ); } #[test] @@ -19906,8 +20056,8 @@ mod tests { ask1: 10.0, bid1_volume: 10_000, ask1_volume: 10_000, - volume_delta: 0, - amount_delta: 0.0, + volume_delta: 10_000, + amount_delta: 100_000.0, trading_phase: Some("continuous".to_string()), }) .collect(), @@ -24533,7 +24683,12 @@ mod tests { ); assert_eq!( plan.order_symbols, - vec!["000003.SZ".to_string(), "000004.SZ".to_string()] + vec![ + "000001.SZ".to_string(), + "000003.SZ".to_string(), + "000004.SZ".to_string(), + ], + "quote plan may prefetch a superset; final selection/order risk is checked in the decision path" ); }