diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index a434023..ad98033 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -8285,6 +8285,47 @@ impl Strategy for PlatformExprStrategy { } } + let mut actionable_stop_take_exit_symbols = BTreeSet::::new(); + if self.config.aiquant_transaction_cost + && self.config.rotation_enabled + && trading_ratio > 0.0 + && trading_ratio < 1.0 + && selection_limit > 0 + { + for position in ctx.portfolio.positions().values() { + if position.quantity == 0 + || delayed_sold_symbols.contains(&position.symbol) + || pending_full_close_symbols.contains(&position.symbol) + || self.pending_highlimit_holdings.contains(&position.symbol) + { + continue; + } + if self.regular_sell_should_wait_due_to_highlimit( + ctx, + projection_date, + &position.symbol, + self.intraday_execution_start_time(), + )? { + continue; + } + let (stop_hit, profit_hit) = + self.stop_take_action_for_position(ctx, signal_date, &day, position)?; + if !stop_hit && !profit_hit { + continue; + } + if defer_execution_risk + || self.can_sell_position_at_time( + ctx, + execution_date, + &position.symbol, + Some(self.intraday_execution_start_time()), + ) + { + actionable_stop_take_exit_symbols.insert(position.symbol.clone()); + } + } + } + if self.config.aiquant_transaction_cost && self.config.rotation_enabled && trading_ratio > 0.0 @@ -8311,6 +8352,9 @@ impl Strategy for PlatformExprStrategy { if pending_full_close_symbols.contains(&position.symbol) { continue; } + if actionable_stop_take_exit_symbols.contains(&position.symbol) { + continue; + } let current_value = self.projected_position_value_at_execution_price( ctx, &projected, @@ -19840,7 +19884,7 @@ mod tests { } #[test] - fn platform_weak_market_adjusts_before_stop_loss_position() { + fn platform_weak_market_skips_target_adjust_before_actionable_stop_loss() { let prev_date = d(2025, 2, 2); let date = d(2025, 2, 3); let symbols = ["000001.SZ", "000002.SZ"]; @@ -19965,36 +20009,28 @@ mod tests { let decision = strategy.on_day(&ctx).expect("platform decision"); - let stop_loss_index = decision - .order_intents - .iter() - .position(|intent| { - matches!( - intent, - OrderIntent::TargetValue { - symbol, - target_value, - reason, - } if symbol == "000001.SZ" && *target_value == 0.0 && reason == "stop_loss_exit" - ) - }) - .expect("stop-loss exit"); assert!( - decision.order_intents[..stop_loss_index] - .iter() - .any(|intent| matches!( - intent, - OrderIntent::Shares { - symbol, - quantity, - reason, - } if symbol == "000001.SZ" - && reason == "daily_position_target_adjust" - && *quantity > 0 - )), + !decision.order_intents.iter().any(|intent| matches!( + intent, + OrderIntent::Shares { + symbol, + quantity, + reason, + } if symbol == "000001.SZ" + && reason == "daily_position_target_adjust" + && *quantity > 0 + )), "{:?}", decision.order_intents ); + assert!(decision.order_intents.iter().any(|intent| matches!( + intent, + OrderIntent::TargetValue { + symbol, + target_value, + reason, + } if symbol == "000001.SZ" && *target_value == 0.0 && reason == "stop_loss_exit" + ))); assert!(decision.order_intents.iter().any(|intent| matches!( intent, OrderIntent::Value { symbol, reason, .. }