diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 1c9544c..e270330 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -7745,31 +7745,11 @@ impl PlatformExprStrategy { band_high: f64, limit: usize, ) -> Result<(Vec, Vec, Vec), BacktestError> { - let selection_risk_deferral = if ctx.is_lagged_execution() { - SelectionRiskDeferral::All - } else { - SelectionRiskDeferral::None - }; - let universe = if selection_risk_deferral != SelectionRiskDeferral::None { - self.selectable_universe_on_with_options( - ctx, - date, - universe_factor_date, - selection_risk_deferral, - ) - } else { - self.selectable_universe_on(ctx, date, universe_factor_date) - }; - let risk_decisions = if selection_risk_deferral != SelectionRiskDeferral::None { - self.selection_risk_decisions_with_options( - ctx, - date, - universe_factor_date, - selection_risk_deferral, - ) - } else { - self.selection_risk_decisions(ctx, date, universe_factor_date) - }; + // Selection policy is evaluated on the signal day. Lagged execution only + // defers buy/sell risk to the actual execution bar; it must not disable an + // explicitly configured signal-day universe filter. + let universe = self.selectable_universe_on(ctx, date, universe_factor_date); + let risk_decisions = self.selection_risk_decisions(ctx, date, universe_factor_date); let mut diagnostics = Self::selection_risk_decision_diagnostics( &risk_decisions, date, @@ -9456,7 +9436,16 @@ impl Strategy for PlatformExprStrategy { } projected = trial_projected; projected_execution_state = trial_execution_state; - if quantity_delta != 0 { + if self.config.target_portfolio_daily_enabled { + order_intents.push(OrderIntent::TargetValue { + symbol: position.symbol.clone(), + target_value, + reason: "daily_position_target_adjust".to_string(), + }); + if quantity_delta > 0 { + same_bar_buy_symbols.insert(position.symbol.clone()); + } + } else if quantity_delta != 0 { order_intents.push(OrderIntent::Shares { symbol: position.symbol.clone(), quantity: quantity_delta, @@ -16344,6 +16333,8 @@ mod tests { cfg.stock_filter_expr = "false".to_string(); cfg.stop_loss_expr.clear(); cfg.take_profit_expr.clear(); + cfg.target_portfolio_daily_enabled = true; + cfg.matching_type = MatchingType::NextBarOpen; cfg.risk_config.trading_constraints.volume_limit_enabled = true; cfg.risk_config.trading_constraints.liquidity_limit_enabled = true; cfg.risk_config.trading_constraints.volume_percent = 0.25; @@ -16353,13 +16344,17 @@ mod tests { assert!(decision.order_intents.iter().any(|intent| matches!( intent, - OrderIntent::Shares { + OrderIntent::TargetValue { symbol: intent_symbol, - quantity, + target_value, reason } if intent_symbol == symbol && reason == "daily_position_target_adjust" - && *quantity == 100 + && *target_value > 0.0 + ))); + assert!(!decision.order_intents.iter().any(|intent| matches!( + intent, + OrderIntent::Shares { reason, .. } if reason == "daily_position_target_adjust" ))); } @@ -19088,7 +19083,7 @@ mod tests { } #[test] - fn platform_next_open_selection_records_risk_diagnostics_without_filtering() { + fn platform_next_open_selection_applies_explicit_signal_day_risk_filter() { let factor_date = d(2023, 11, 10); let decision_date = d(2023, 11, 13); let execution_date = d(2023, 11, 14); @@ -19259,7 +19254,7 @@ mod tests { decision .diagnostics .iter() - .any(|item| item == "selected_symbols=600462.SH"), + .all(|item| item != "selected_symbols=600462.SH"), "{:?}", decision.diagnostics ); @@ -19272,15 +19267,15 @@ mod tests { decision.diagnostics ); assert!( - decision.order_intents.iter().any(|intent| matches!( + decision.order_intents.iter().all(|intent| !matches!( intent, OrderIntent::Shares { symbol, quantity, .. } if symbol == candidate_symbol && *quantity > 0 - ) || matches!( + ) && !matches!( intent, OrderIntent::Value { symbol, value, .. } if symbol == candidate_symbol && *value > 0.0 - ) || matches!( + ) && !matches!( intent, OrderIntent::TargetValue { symbol,