diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 098048f..0ae7f8b 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -958,6 +958,16 @@ impl PlatformExprStrategy { } } + fn refresh_available_cash_after_projected_sell( + &self, + available_cash: &mut f64, + projected: &PortfolioState, + ) { + if self.effective_rebalance_cash_mode() != RebalanceCashMode::PreOpenCash { + *available_cash = projected.cash().max(0.0); + } + } + pub fn new(config: PlatformExprStrategyConfig) -> Self { let mut engine = Engine::new(); engine.register_fn("round", |value: f64| value.round()); @@ -9515,8 +9525,8 @@ impl Strategy for PlatformExprStrategy { self.forget_position_entry_date(&position.symbol); let can_sell = defer_execution_risk || self.can_sell_position(ctx, execution_date, &position.symbol); - if can_sell - && self + if can_sell { + let close_submitted = self .project_target_zero( ctx, &mut projected, @@ -9524,13 +9534,19 @@ impl Strategy for PlatformExprStrategy { &position.symbol, &mut projected_execution_state, ) - .is_some() - && Self::projected_position_is_flat(&projected, &position.symbol) - { - same_day_sold_symbols.insert(position.symbol.clone()); - slot_working_symbols.remove(&position.symbol); - if !persistent_model_lifecycle { - self.pending_full_close_symbols.remove(&position.symbol); + .is_some(); + if close_submitted { + self.refresh_available_cash_after_projected_sell( + &mut aiquant_available_cash, + &projected, + ); + if Self::projected_position_is_flat(&projected, &position.symbol) { + same_day_sold_symbols.insert(position.symbol.clone()); + slot_working_symbols.remove(&position.symbol); + if !persistent_model_lifecycle { + self.pending_full_close_symbols.remove(&position.symbol); + } + } } } if debug_projection { @@ -9610,7 +9626,7 @@ impl Strategy for PlatformExprStrategy { reason: "risk_forced_exit".to_string(), }); self.forget_position_entry_date(&symbol); - if self + let close_submitted = self .project_target_zero_at_time( ctx, &mut projected, @@ -9619,13 +9635,18 @@ impl Strategy for PlatformExprStrategy { &mut projected_execution_state, Some(risk_level_forced_exit_time), ) - .is_some() - && Self::projected_position_is_flat(&projected, &symbol) - { - same_day_sold_symbols.insert(symbol.clone()); - slot_working_symbols.remove(&symbol); - if !persistent_model_lifecycle { - self.pending_full_close_symbols.remove(&symbol); + .is_some(); + if close_submitted { + self.refresh_available_cash_after_projected_sell( + &mut aiquant_available_cash, + &projected, + ); + if Self::projected_position_is_flat(&projected, &symbol) { + same_day_sold_symbols.insert(symbol.clone()); + slot_working_symbols.remove(&symbol); + if !persistent_model_lifecycle { + self.pending_full_close_symbols.remove(&symbol); + } } } } @@ -9834,6 +9855,10 @@ impl Strategy for PlatformExprStrategy { ) .is_some(); if close_submitted { + self.refresh_available_cash_after_projected_sell( + &mut aiquant_available_cash, + &projected, + ); slot_working_symbols.remove(&position.symbol); if Self::projected_position_is_flat(&projected, &position.symbol) { same_day_sold_symbols.insert(position.symbol.clone()); @@ -9938,6 +9963,10 @@ impl Strategy for PlatformExprStrategy { ) .is_some(); if close_submitted { + self.refresh_available_cash_after_projected_sell( + &mut aiquant_available_cash, + &projected, + ); slot_working_symbols.remove(&position.symbol); if Self::projected_position_is_flat(&projected, &position.symbol) { same_day_sold_symbols.insert(position.symbol.clone()); @@ -10022,6 +10051,10 @@ impl Strategy for PlatformExprStrategy { ) .is_some(); if close_submitted { + self.refresh_available_cash_after_projected_sell( + &mut aiquant_available_cash, + &projected, + ); if Self::projected_position_is_flat(&projected, &position.symbol) { same_day_sold_symbols.insert(position.symbol.clone()); if !persistent_model_lifecycle { @@ -10100,6 +10133,10 @@ impl Strategy for PlatformExprStrategy { ) .is_some(); if close_submitted { + self.refresh_available_cash_after_projected_sell( + &mut aiquant_available_cash, + &projected, + ); if Self::projected_position_is_flat(&projected, &position.symbol) { same_day_sold_symbols.insert(position.symbol.clone()); if !persistent_model_lifecycle { @@ -23911,12 +23948,16 @@ mod tests { ) .expect("dataset"); - let mut portfolio = PortfolioState::new(5_000.0); - portfolio.position_mut(keep_first).buy(prev_date, 100, 10.0); - portfolio.position_mut(take_profit).buy(prev_date, 100, 8.0); + let mut portfolio = PortfolioState::new(0.0); + portfolio + .position_mut(keep_first) + .buy(prev_date, 1_000, 10.0); + portfolio + .position_mut(take_profit) + .buy(prev_date, 1_000, 8.0); portfolio .position_mut(keep_second) - .buy(prev_date, 100, 10.0); + .buy(prev_date, 1_000, 10.0); let subscriptions = BTreeSet::new(); let ctx = StrategyContext { execution_date: date,