diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 513ed4b..977696f 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -5129,12 +5129,22 @@ impl Strategy for PlatformExprStrategy { .map(|position| position.symbol.clone()) .collect::>(); - let mut pending_symbols = self - .pending_highlimit_holdings - .iter() - .cloned() - .collect::>(); - let take_profit_multiplier = self.config.take_profit_expr.trim().parse::().ok(); + let take_profit_multiplier = self + .config + .take_profit_expr + .trim() + .parse::() + .ok() + .filter(|value| value.is_finite() && *value > 0.0); + let mut pending_symbols = if take_profit_multiplier.is_some() { + self.pending_highlimit_holdings + .iter() + .cloned() + .collect::>() + } else { + self.pending_highlimit_holdings.clear(); + BTreeSet::new() + }; if let Some(multiplier) = take_profit_multiplier { for position in ctx.portfolio.positions().values() { let avg_price = position @@ -5273,7 +5283,7 @@ impl Strategy for PlatformExprStrategy { } else if stop_hit { unresolved_stop_loss_symbols.insert(position.symbol.clone()); } - } else { + } else if take_profit_multiplier.is_some() { let stock = match self.stock_state(ctx, execution_date, &position.symbol) { Ok(stock) => stock, Err(BacktestError::Data(crate::data::DataSetError::MissingSnapshot { @@ -5758,6 +5768,123 @@ mod tests { assert!((marked - 1_293.0).abs() < 1e-6, "{marked}"); } + #[test] + fn platform_take_profit_false_does_not_track_upper_limit_pending_sell() { + let prev_date = d(2025, 2, 6); + let date = d(2025, 2, 7); + let symbol = "001368.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-02-07 10:18:00".to_string()), + day_open: 23.11, + open: 23.11, + high: 23.99, + low: 23.11, + close: 23.99, + last_price: 23.99, + bid1: 23.99, + ask1: 23.99, + prev_close: 21.81, + volume: 100_161, + tick_volume: 1_000, + bid1_volume: 1_000, + ask1_volume: 1_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 23.99, + lower_limit: 19.63, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + symbol: symbol.to_string(), + market_cap_bn: 24.90, + free_float_cap_bn: 6.10, + pe_ttm: 8.0, + turnover_ratio: Some(35.77), + effective_turnover_ratio: Some(35.77), + 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 mut portfolio = PortfolioState::new(1_000_000.0); + portfolio.position_mut(symbol).buy(prev_date, 6_200, 19.86); + let subscriptions = BTreeSet::new(); + let ctx = StrategyContext { + execution_date: date, + decision_date: date, + decision_index: 20, + 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.rotation_enabled = false; + cfg.signal_symbol = symbol.to_string(); + cfg.stop_loss_expr.clear(); + cfg.take_profit_expr = "false".to_string(); + let mut strategy = PlatformExprStrategy::new(cfg); + + let decision = strategy.on_day(&ctx).expect("platform decision"); + + assert!( + !decision.order_intents.iter().any(|intent| matches!( + intent, + OrderIntent::TargetValue { symbol: intent_symbol, target_value, .. } + if intent_symbol == symbol && *target_value == 0.0 + )), + "{:?}", + decision.order_intents + ); + assert!( + !decision.order_intents.iter().any(|intent| matches!( + intent, + OrderIntent::AlgoValue { reason, .. } if reason == "delayed_limit_open_sell" + )), + "{:?}", + decision.order_intents + ); + assert!(strategy.pending_highlimit_holdings.is_empty()); + } + #[test] fn platform_take_profit_sells_position_at_upper_limit() { let prev_date = d(2025, 2, 6);