diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index fcaae50..fef4e15 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -1225,6 +1225,7 @@ pub struct PlatformExprStrategy { rebalance_day_counter: usize, last_rebalance_date: Option, last_target_selection: Option>, + last_target_order: Option>, last_trading_ratio: Option, portfolio_drawdown_controller: Option, pending_highlimit_holdings: BTreeSet, @@ -1559,6 +1560,7 @@ impl PlatformExprStrategy { rebalance_day_counter: 0, last_rebalance_date: None, last_target_selection: None, + last_target_order: None, last_trading_ratio: None, portfolio_drawdown_controller, pending_highlimit_holdings: BTreeSet::new(), @@ -12531,10 +12533,22 @@ impl Strategy for PlatformExprStrategy { } let stop_take_exit_signal_symbols = current_stop_take_exit_symbols.clone(); - let target_portfolio_weight_bps = + let target_portfolio_weights = if self.config.target_portfolio_daily_enabled && selection_limit > 0 { let mut scales = Vec::new(); - for symbol in stock_list.iter().take(selection_limit) { + let original_target_symbols = self + .last_target_order + .as_ref() + .filter(|symbols| !symbols.is_empty()) + .cloned() + .or_else(|| { + self.last_target_selection + .as_ref() + .filter(|symbols| !symbols.is_empty()) + .map(|symbols| symbols.iter().cloned().collect()) + }) + .unwrap_or_else(|| stock_list.iter().take(selection_limit).cloned().collect()); + for symbol in original_target_symbols.iter().take(selection_limit) { let decision_stock = self.stock_state_with_factor_date( ctx, decision_date, @@ -12552,20 +12566,20 @@ impl Strategy for PlatformExprStrategy { &excluded_target_symbols, selection_limit, ) - .into_iter() - .collect::>() } else { - BTreeMap::new() + Vec::new() }; + let target_portfolio_weight_bps = target_portfolio_weights + .iter() + .cloned() + .collect::>(); if self.config.rotation_enabled && self.config.daily_position_target_adjust_enabled && trading_ratio > 0.0 && (self.config.target_portfolio_daily_enabled || trading_ratio < 1.0) && selection_limit > 0 - && !(persistent_model_lifecycle - && self.config.target_portfolio_daily_enabled - && daily_top_up_active) + && !(self.config.target_portfolio_daily_enabled && daily_top_up_active) && (!ctx.portfolio.positions().is_empty() || (persistent_model_lifecycle && !self.position_entry_dates.is_empty())) { @@ -13133,51 +13147,11 @@ impl Strategy for PlatformExprStrategy { } } - if daily_top_up_active - && self.config.target_portfolio_daily_enabled - && persistent_model_lifecycle - { - let mut target_symbols = self - .position_entry_dates - .keys() - .filter(|symbol| !exit_symbols.contains(*symbol)) - .filter(|symbol| !factor_position_action_symbols.contains(*symbol)) - .cloned() - .collect::>(); - for symbol in &stock_list { - if target_symbols.len() >= selection_limit { - break; - } - if target_symbols.contains(symbol) || exit_symbols.contains(symbol) { - continue; - } - if ctx - .data - .market_latest_back_adjusted_close(signal_date, symbol) - .is_none() - { - continue; - } - self.remember_position_entry_date(symbol, signal_date); - target_symbols.insert(symbol.clone()); - } - - for symbol in target_symbols { - let decision_stock = self.stock_state_with_factor_date( - ctx, - decision_date, - selection_factor_date, - &symbol, - )?; - let stock_scale = self.buy_scale(ctx, &day, &decision_stock)?; - let target_value = if let Some(weight_bps) = - target_portfolio_weight_bps.get(&symbol) - { - strategy_visible_total_value * trading_ratio * f64::from(*weight_bps) / 10_000.0 - } else { - strategy_visible_total_value * trading_ratio / selection_limit as f64 - * stock_scale - }; + if daily_top_up_active && self.config.target_portfolio_daily_enabled { + for (symbol, weight_bps) in &target_portfolio_weights { + let target_value = + strategy_visible_total_value * trading_ratio * f64::from(*weight_bps) + / 10_000.0; if !target_value.is_finite() || target_value <= 0.0 { continue; } @@ -13205,6 +13179,7 @@ impl Strategy for PlatformExprStrategy { &mut projected_execution_state, ); intraday_attempted_buys.insert(symbol.clone()); + self.remember_position_entry_date(symbol, signal_date); } let after_qty = projected .position(&symbol) @@ -13219,40 +13194,9 @@ impl Strategy for PlatformExprStrategy { deferred_daily_target_values.insert(symbol.clone(), target_value); } if after_qty > before_qty { - same_bar_buy_symbols.insert(symbol); + same_bar_buy_symbols.insert(symbol.clone()); } } - } else if daily_top_up_active && self.config.target_portfolio_daily_enabled { - self.try_daily_top_up_at_position( - ctx, - &day, - &stock_list, - decision_date, - execution_date, - projection_date, - selection_factor_date, - signal_date, - daily_top_up_target_budget, - selection_limit, - defer_execution_risk, - None, - &mut projected, - &mut projected_execution_state, - &mut order_intents, - &mut available_cash, - &mut slot_working_symbols, - &mut same_bar_buy_symbols, - &pending_full_close_symbols, - &slot_blocking_symbols, - &same_day_sold_symbols, - &exit_symbols, - &delayed_sold_symbols, - &mut intraday_attempted_buys, - &mut daily_top_up_pending_buy_value, - &deferred_daily_target_values, - debug_daily_top_up, - &mut daily_top_up_debug_notes, - )?; } if periodic_rebalance { @@ -13453,8 +13397,13 @@ impl Strategy for PlatformExprStrategy { } } if self.config.rotation_enabled && periodic_rebalance { - self.last_target_selection = - Some(stock_list.iter().take(selection_limit).cloned().collect()); + let target_order = stock_list + .iter() + .take(selection_limit) + .cloned() + .collect::>(); + self.last_target_selection = Some(target_order.iter().cloned().collect()); + self.last_target_order = Some(target_order); } if self.config.rotation_enabled && trading_ratio.is_finite() { self.last_trading_ratio = Some(trading_ratio); @@ -27773,6 +27722,14 @@ mod tests { let mut strategy = PlatformExprStrategy::new(cfg); strategy.rebalance_day_counter = 2; strategy.last_rebalance_date = Some(prev_date); + let target_order = vec![ + buy_first.to_string(), + buy_second.to_string(), + keep_first.to_string(), + keep_second.to_string(), + ]; + strategy.last_target_selection = Some(target_order.iter().cloned().collect()); + strategy.last_target_order = Some(target_order); strategy .position_entry_dates .insert(take_profit.to_string(), prev_date);