diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index d484ad4..11c9633 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -966,6 +966,31 @@ where } } + fn complete_daily_target_position(intent: &OrderIntent) -> Option<(&str, bool)> { + match intent.unwrapped() { + OrderIntent::TargetValue { + symbol, + target_value, + reason, + } + | OrderIntent::TimedTargetValue { + symbol, + target_value, + reason, + .. + } + | OrderIntent::LimitTargetValue { + symbol, + target_value, + reason, + .. + } if reason == "model_target_portfolio_daily" => { + Some((symbol, target_value.is_finite() && *target_value > 0.0)) + } + _ => None, + } + } + fn infer_target_position_limit( &self, portfolio: &PortfolioState, @@ -979,6 +1004,21 @@ where return None; } + let complete_daily_targets = intents + .iter() + .filter_map(|intent| Self::complete_daily_target_position(intent)) + .collect::>(); + if !complete_daily_targets.is_empty() { + return Some( + complete_daily_targets + .into_iter() + .filter(|(_, positive)| *positive) + .map(|(symbol, _)| symbol) + .collect::>() + .len(), + ); + } + let held_symbols = portfolio .positions() .iter() @@ -10206,6 +10246,43 @@ mod tests { } } + #[test] + fn complete_daily_target_batch_uses_positive_target_count_for_slot_limit() { + let prev_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"); + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks, + PriceField::Open, + ); + let mut portfolio = PortfolioState::new(20_000.0); + for symbol in ["000001.SZ", "000002.SZ", "000003.SZ"] { + portfolio.position_mut(symbol).buy(prev_date, 1_000, 10.0); + } + let intents = vec![ + OrderIntent::TargetValue { + symbol: "000001.SZ".to_string(), + target_value: 0.0, + reason: "stop_loss_exit".to_string(), + }, + OrderIntent::TargetValue { + symbol: "000002.SZ".to_string(), + target_value: 10_000.0, + reason: "model_target_portfolio_daily".to_string(), + }, + OrderIntent::TargetValue { + symbol: "000004.SZ".to_string(), + target_value: 10_000.0, + reason: "model_target_portfolio_daily".to_string(), + }, + ]; + let refs = intents.iter().collect::>(); + + assert_eq!( + broker.infer_target_position_limit(&portfolio, &refs), + Some(2) + ); + } + #[test] fn failed_target_exit_blocks_replacement_entry_when_no_position_slot_is_released() { let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date"); diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index d372f0e..fe83013 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -13295,7 +13295,7 @@ impl Strategy for PlatformExprStrategy { { continue; } - let execution_stock = if self.uses_intraday_execution_quotes() { + if self.uses_intraday_execution_quotes() { let execution_time = self.intraday_execution_start_time(); if self .scheduled_quote_at_time(ctx, projection_date, symbol, Some(execution_time)) @@ -13306,10 +13306,7 @@ impl Strategy for PlatformExprStrategy { )); continue; } - self.stock_state_at_time(ctx, projection_date, symbol, Some(execution_time))? - } else { - self.stock_state(ctx, projection_date, symbol)? - }; + } let target_value = target_value_for_scale( strategy_visible_total_value, trading_ratio, @@ -13324,20 +13321,6 @@ impl Strategy for PlatformExprStrategy { .position(&symbol) .map(|position| position.quantity) .unwrap_or(0); - if before_qty == 0 - && !defer_execution_risk - && let Some(reason) = self.buy_rejection_reason( - ctx, - execution_date, - symbol, - execution_stock.as_ref(), - )? - { - selection_notes.push(format!( - "daily_target_skipped symbol={symbol} reason={reason} no_order=true" - )); - continue; - } if before_qty > 0 { self.project_target_value( ctx,