diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index fe730b5..a434023 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -7899,8 +7899,6 @@ impl Strategy for PlatformExprStrategy { let mut intraday_attempted_buys = BTreeSet::::new(); let mut delayed_sold_symbols = BTreeSet::::new(); let mut unresolved_stop_loss_symbols = BTreeSet::::new(); - let mut unsellable_stop_take_attempted_symbols = BTreeSet::::new(); - let mut pre_detected_stop_take_exit_symbols = BTreeSet::::new(); let mut pending_risk_level_forced_exit_symbols = BTreeSet::::new(); let delayed_limit_exit_time = self .config @@ -8229,31 +8227,8 @@ impl Strategy for PlatformExprStrategy { pending_risk_level_forced_exit_symbols.insert(position.symbol.clone()); continue; } - let (stop_hit, profit_hit) = - self.stop_take_action_for_position(ctx, signal_date, &day, position)?; let can_sell = defer_execution_risk || self.can_sell_position(ctx, execution_date, &position.symbol); - if stop_hit || profit_hit { - if can_sell { - pre_detected_stop_take_exit_symbols.insert(position.symbol.clone()); - } else { - let reason = if stop_hit { - "stop_loss_exit" - } else { - "take_profit_exit" - }; - order_intents.push(OrderIntent::TargetValue { - symbol: position.symbol.clone(), - target_value: 0.0, - reason: reason.to_string(), - }); - unsellable_stop_take_attempted_symbols.insert(position.symbol.clone()); - if stop_hit { - unresolved_stop_loss_symbols.insert(position.symbol.clone()); - } - } - continue; - } if !can_sell { continue; } @@ -8336,9 +8311,6 @@ impl Strategy for PlatformExprStrategy { if pending_full_close_symbols.contains(&position.symbol) { continue; } - if pre_detected_stop_take_exit_symbols.contains(&position.symbol) { - continue; - } let current_value = self.projected_position_value_at_execution_price( ctx, &projected, @@ -8373,13 +8345,6 @@ impl Strategy for PlatformExprStrategy { .map(|projected_position| projected_position.quantity) .unwrap_or(0); let quantity_delta = after_qty as i32 - before_qty as i32; - if quantity_delta > 0 - && self.config.daily_top_up_enabled - && (unsellable_stop_take_attempted_symbols.contains(&position.symbol) - || unresolved_stop_loss_symbols.contains(&position.symbol)) - { - continue; - } if self .config .weak_market_shrink_overweight_threshold @@ -8429,11 +8394,6 @@ impl Strategy for PlatformExprStrategy { let can_sell = defer_execution_risk || self.can_sell_position(ctx, execution_date, &position.symbol); if stop_hit { - if unsellable_stop_take_attempted_symbols.contains(&position.symbol) { - continue; - } - self.pending_full_close_symbols - .insert(position.symbol.clone()); exit_symbols.insert(position.symbol.clone()); order_intents.push(OrderIntent::TargetValue { symbol: position.symbol.clone(), @@ -8441,6 +8401,7 @@ impl Strategy for PlatformExprStrategy { reason: "stop_loss_exit".to_string(), }); self.forget_position_entry_date(&position.symbol); + slot_working_symbols.remove(&position.symbol); if can_sell { if self .project_target_zero( @@ -8454,7 +8415,6 @@ impl Strategy for PlatformExprStrategy { && Self::projected_position_is_flat(&projected, &position.symbol) { same_day_sold_symbols.insert(position.symbol.clone()); - slot_working_symbols.remove(&position.symbol); self.pending_full_close_symbols.remove(&position.symbol); } } else { @@ -8464,11 +8424,6 @@ impl Strategy for PlatformExprStrategy { } if profit_hit { - if unsellable_stop_take_attempted_symbols.contains(&position.symbol) { - continue; - } - self.pending_full_close_symbols - .insert(position.symbol.clone()); exit_symbols.insert(position.symbol.clone()); order_intents.push(OrderIntent::TargetValue { symbol: position.symbol.clone(), @@ -8476,6 +8431,7 @@ impl Strategy for PlatformExprStrategy { reason: "take_profit_exit".to_string(), }); self.forget_position_entry_date(&position.symbol); + slot_working_symbols.remove(&position.symbol); if can_sell && self .project_target_zero( @@ -8489,7 +8445,6 @@ impl Strategy for PlatformExprStrategy { && Self::projected_position_is_flat(&projected, &position.symbol) { same_day_sold_symbols.insert(position.symbol.clone()); - slot_working_symbols.remove(&position.symbol); self.pending_full_close_symbols.remove(&position.symbol); } } @@ -12933,7 +12888,7 @@ mod tests { } #[test] - fn platform_pending_full_close_carries_after_partial_stop_loss() { + fn platform_stop_loss_does_not_carry_synthetic_pending_full_close() { let prev_date = d(2025, 1, 6); let first_date = d(2025, 1, 7); let second_date = d(2025, 1, 8); @@ -13067,7 +13022,7 @@ mod tests { OrderIntent::TargetValue { symbol: intent_symbol, target_value, reason } if intent_symbol == symbol && *target_value == 0.0 && reason == "stop_loss_exit" ))); - assert!(strategy.pending_full_close_symbols.contains(symbol)); + assert!(!strategy.pending_full_close_symbols.contains(symbol)); let mut second_portfolio = PortfolioState::new(1_000_000.0); second_portfolio @@ -13090,16 +13045,11 @@ mod tests { fills: &[], }; let second_decision = strategy.on_day(&second_ctx).expect("second decision"); - assert!(second_decision.order_intents.iter().any(|intent| matches!( + assert!(!second_decision.order_intents.iter().any(|intent| matches!( intent, OrderIntent::TargetValue { symbol: intent_symbol, target_value, reason } if intent_symbol == symbol && *target_value == 0.0 && reason == "pending_full_close_exit" ))); - assert!(!second_decision.order_intents.iter().any(|intent| matches!( - intent, - OrderIntent::Shares { symbol: intent_symbol, quantity, reason } - if intent_symbol == symbol && *quantity > 0 && reason == "daily_position_target_adjust" - ))); assert!(!strategy.pending_full_close_symbols.contains(symbol)); } @@ -19890,7 +19840,7 @@ mod tests { } #[test] - fn platform_weak_market_skips_positive_adjust_for_stop_loss_position() { + fn platform_weak_market_adjusts_before_stop_loss_position() { let prev_date = d(2025, 2, 2); let date = d(2025, 2, 3); let symbols = ["000001.SZ", "000002.SZ"]; @@ -20030,7 +19980,7 @@ mod tests { }) .expect("stop-loss exit"); assert!( - !decision.order_intents[..stop_loss_index] + decision.order_intents[..stop_loss_index] .iter() .any(|intent| matches!( intent, @@ -20041,17 +19991,19 @@ mod tests { } if symbol == "000001.SZ" && reason == "daily_position_target_adjust" && *quantity > 0 - )) + )), + "{:?}", + decision.order_intents ); - assert!(!decision.order_intents.iter().any(|intent| matches!( + assert!(decision.order_intents.iter().any(|intent| matches!( intent, - OrderIntent::Value { symbol, reason, .. } | OrderIntent::Shares { symbol, reason, .. } - if symbol == "000001.SZ" && reason == "daily_top_up_buy" + OrderIntent::Value { symbol, reason, .. } + if symbol == "000002.SZ" && reason == "daily_top_up_buy" ))); } #[test] - fn platform_weak_market_skips_positive_adjust_when_stop_loss_sell_blocked() { + fn platform_weak_market_adjusts_before_lower_limit_blocked_stop_loss() { let prev_date = d(2025, 2, 2); let date = d(2025, 2, 3); let symbols = ["000001.SZ", "000002.SZ"]; @@ -20176,32 +20128,45 @@ 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.iter().any(|intent| matches!( - intent, - OrderIntent::Shares { - symbol, - quantity, - reason, - } if symbol == "000001.SZ" - && reason == "daily_position_target_adjust" - && *quantity > 0 - )), + 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 ); 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" + OrderIntent::Value { symbol, reason, .. } + if symbol == "000002.SZ" && reason == "daily_top_up_buy" ))); } #[test] - fn platform_weak_market_skips_positive_adjust_when_intraday_stop_loss_sell_blocked() { + fn platform_weak_market_adjusts_before_intraday_stop_loss_sell_blocked() { let prev_date = d(2025, 2, 2); let date = d(2025, 2, 3); let symbols = ["000001.SZ", "000002.SZ"]; @@ -20345,27 +20310,40 @@ 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.iter().any(|intent| matches!( - intent, - OrderIntent::Shares { - symbol, - quantity, - reason, - } if symbol == "000001.SZ" - && reason == "daily_position_target_adjust" - && *quantity > 0 - )), + 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 ); 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" + OrderIntent::Value { symbol, reason, .. } + if symbol == "000002.SZ" && reason == "daily_top_up_buy" ))); } @@ -21702,7 +21680,7 @@ mod tests { } #[test] - fn platform_daily_top_up_keeps_unsellable_stop_loss_in_target_count() { + fn platform_daily_top_up_releases_unsellable_stop_loss_slot() { let prev_date = d(2026, 3, 31); let date = d(2026, 4, 1); let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"]; @@ -21843,17 +21821,18 @@ mod tests { decision.order_intents ); assert!( - !decision - .order_intents - .iter() - .any(|intent| matches!(intent, OrderIntent::Value { .. })), + decision.order_intents.iter().any(|intent| matches!( + intent, + OrderIntent::Value { symbol, reason, .. } + if symbol == "000003.SZ" && reason == "daily_top_up_buy" + )), "{:?}", decision.order_intents ); } #[test] - fn platform_daily_top_up_does_not_release_unsellable_exit_signal_slot() { + fn platform_daily_top_up_release_slot_flag_matches_aiquant_exit_signal_state() { let prev_date = d(2026, 3, 31); let date = d(2026, 4, 1); let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"]; @@ -21995,10 +21974,11 @@ mod tests { decision.order_intents ); assert!( - !decision - .order_intents - .iter() - .any(|intent| matches!(intent, OrderIntent::Value { .. })), + decision.order_intents.iter().any(|intent| matches!( + intent, + OrderIntent::Value { symbol, reason, .. } + if symbol == "000003.SZ" && reason == "daily_top_up_buy" + )), "{:?}", decision.order_intents );