diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 092a429..7384045 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -653,6 +653,7 @@ pub struct PlatformExprStrategyConfig { pub delayed_limit_open_exit_time: Option, pub release_slot_on_exit_signal: bool, pub redistribute_target_weights_after_exit: bool, + pub reenter_exited_targets: bool, pub explicit_action_stage: PlatformExplicitActionStage, pub explicit_action_schedule: Option, pub subscription_guard_required: bool, @@ -729,6 +730,7 @@ impl PlatformExprStrategyConfig { delayed_limit_open_exit_time: None, release_slot_on_exit_signal: false, redistribute_target_weights_after_exit: false, + reenter_exited_targets: false, explicit_action_stage: PlatformExplicitActionStage::OnDay, explicit_action_schedule: None, subscription_guard_required: false, @@ -2436,11 +2438,13 @@ 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); + if !self.config.reenter_exited_targets { + 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); + } } } @@ -13863,6 +13867,35 @@ mod tests { ); } + #[test] + fn stock_pool_target_lifecycle_keeps_exits_for_next_day_reentry() { + let date = d(2025, 1, 3); + let mut config = PlatformExprStrategyConfig::generic(); + config.reenter_exited_targets = true; + let mut strategy = PlatformExprStrategy::new(config); + strategy.last_target_order = Some(vec!["KEEP".to_string(), "EXITED".to_string()]); + strategy.last_target_selection = + Some(BTreeSet::from(["KEEP".to_string(), "EXITED".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(), "EXITED".to_string()].as_slice()) + ); + assert!( + strategy + .last_target_selection + .as_ref() + .expect("target selection") + .contains("EXITED") + ); + assert!(!strategy.position_entry_dates.contains_key("EXITED")); + } + fn single_symbol_platform_data(dates: &[NaiveDate], symbol: &str) -> DataSet { DataSet::from_components( vec![Instrument { diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index efae343..ce8674c 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -1022,6 +1022,8 @@ pub struct StrategyExpressionTradingConfig { #[serde(default)] pub redistribute_target_weights_after_exit: Option, #[serde(default)] + pub reenter_exited_targets: Option, + #[serde(default)] pub subscription_guard_required: Option, #[serde(default)] pub subscriptions: Vec, @@ -2299,6 +2301,9 @@ pub fn platform_expr_config_from_spec( if let Some(enabled) = trading.redistribute_target_weights_after_exit { cfg.redistribute_target_weights_after_exit = enabled; } + if let Some(enabled) = trading.reenter_exited_targets { + cfg.reenter_exited_targets = enabled; + } if let Some(enabled) = trading.delayed_limit_open_exit { cfg.delayed_limit_open_exit_enabled = enabled; if enabled { @@ -3490,7 +3495,8 @@ mod tests { "rebalanceExistingPositions": true, "holdUntilExit": true, "releaseSlotOnExitSignal": true, - "redistributeTargetWeightsAfterExit": true + "redistributeTargetWeightsAfterExit": true, + "reenterExitedTargets": true } } }); @@ -3505,6 +3511,7 @@ mod tests { assert!(cfg.hold_until_exit_enabled); assert!(cfg.release_slot_on_exit_signal); assert!(cfg.redistribute_target_weights_after_exit); + assert!(cfg.reenter_exited_targets); } #[test]