diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 69ea40a..dbf0fb1 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -2419,8 +2419,16 @@ impl PlatformExprStrategy { .entry(symbol.clone()) .or_insert(0); self.position_holding_days_last_counted - .entry(symbol) + .entry(symbol.clone()) .or_insert(signal_date); + if let Some(target_order) = &mut self.last_target_order + && !target_order.contains(&symbol) + { + target_order.push(symbol.clone()); + } + if let Some(target_selection) = &mut self.last_target_selection { + target_selection.insert(symbol); + } } } @@ -2428,6 +2436,12 @@ impl PlatformExprStrategy { self.position_entry_dates.remove(symbol); self.position_holding_days.remove(symbol); self.position_holding_days_last_counted.remove(symbol); + if let Some(target_order) = &mut self.last_target_order { + target_order.retain(|target| target != symbol); + } + if let Some(target_selection) = &mut self.last_target_selection { + target_selection.remove(symbol); + } } fn max_holding_days_exceeded(&self, symbol: &str) -> Option { @@ -13252,11 +13266,16 @@ impl Strategy for PlatformExprStrategy { } } + let blocked_exit_slots = exit_symbols + .iter() + .filter(|symbol| !Self::projected_position_is_flat(&projected, symbol)) + .count(); + let daily_target_count = selection_limit.saturating_sub(blocked_exit_slots); let daily_target_portfolio_scales = resolved_target_scales( &original_target_scales, &candidate_target_scales, - &same_day_sold_symbols, - selection_limit, + &exit_symbols, + daily_target_count, self.config.redistribute_target_weights_after_exit, )?; if daily_top_up_active && self.config.target_portfolio_daily_enabled { @@ -13785,6 +13804,58 @@ mod tests { assert!((redistributed.iter().map(|(_, scale)| *scale).sum::() - 3.0).abs() < 1e-12); } + #[test] + fn model_target_lifecycle_removes_exits_and_appends_replacements() { + let date = d(2025, 1, 3); + let mut strategy = PlatformExprStrategy::new(PlatformExprStrategyConfig::generic()); + strategy.last_target_order = Some(vec![ + "KEEP".to_string(), + "EXITED".to_string(), + "RETRY".to_string(), + ]); + strategy.last_target_selection = Some(BTreeSet::from([ + "KEEP".to_string(), + "EXITED".to_string(), + "RETRY".to_string(), + ])); + strategy + .position_entry_dates + .insert("EXITED".to_string(), date); + + strategy.forget_position_entry_date("EXITED"); + assert_eq!( + strategy.last_target_order.as_deref(), + Some(["KEEP".to_string(), "RETRY".to_string()].as_slice()) + ); + assert!( + !strategy + .last_target_selection + .as_ref() + .expect("target selection") + .contains("EXITED") + ); + + strategy.remember_position_entry_date("REPLACEMENT", date); + assert_eq!( + strategy.last_target_order.as_deref(), + Some( + [ + "KEEP".to_string(), + "RETRY".to_string(), + "REPLACEMENT".to_string(), + ] + .as_slice() + ) + ); + assert!( + strategy + .last_target_selection + .as_ref() + .expect("target selection") + .contains("REPLACEMENT") + ); + } + fn single_symbol_platform_data(dates: &[NaiveDate], symbol: &str) -> DataSet { DataSet::from_components( vec![Instrument {