diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index d081e03..e346b3f 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -5986,7 +5986,7 @@ impl PlatformExprStrategy { if excludes.iter().any(|item| item == "one_yuan") && stock.is_one_yuan { return Ok(Some("one_yuan".to_string())); } - if !self.config.aiquant_transaction_cost && !candidate.allow_buy { + if !candidate.allow_buy { return Ok(Some("buy_disabled".to_string())); } let upper_limit_check_price = if self.config.aiquant_transaction_cost { @@ -12573,6 +12573,107 @@ mod tests { assert_eq!(rejection, None); } + #[test] + fn platform_aiquant_buy_rejection_keeps_candidate_buy_disabled() { + let date = d(2025, 4, 8); + let symbol = "300473.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: Some("2025-04-08 10:18:00".to_string()), + day_open: 20.0, + open: 20.0, + high: 20.3, + low: 19.8, + close: 20.1, + last_price: 20.0, + bid1: 19.99, + ask1: 20.01, + prev_close: 19.8, + volume: 10_000_000, + tick_volume: 10_000, + bid1_volume: 1_000, + ask1_volume: 1_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 21.78, + lower_limit: 17.82, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn: 15.0, + free_float_cap_bn: 10.0, + pe_ttm: 8.0, + turnover_ratio: Some(2.0), + effective_turnover_ratio: Some(2.0), + extra_factors: BTreeMap::new(), + }], + vec![CandidateEligibility { + date, + symbol: symbol.to_string(), + is_st: false, + is_new_listing: false, + is_paused: false, + allow_buy: false, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }], + 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: 40, + 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.universe_exclude.clear(); + cfg.aiquant_transaction_cost = true; + let strategy = PlatformExprStrategy::new(cfg); + let stock = strategy + .stock_state(&ctx, date, symbol) + .expect("stock state"); + + let rejection = strategy + .buy_rejection_reason(&ctx, date, symbol, &stock) + .expect("aiquant rejection"); + + assert_eq!(rejection.as_deref(), Some("buy_disabled")); + } + #[test] fn platform_strategy_baseline_risk_filter_cannot_be_disabled_by_empty_exclude() { let date = d(2024, 12, 31);