diff --git a/services/trading/backtest_replay.py b/services/trading/backtest_replay.py index 5fbb240..c8315de 100644 --- a/services/trading/backtest_replay.py +++ b/services/trading/backtest_replay.py @@ -212,7 +212,7 @@ class BacktestReplay: ) # Simulate simple exit logic: close positions held > 5 days - # (simplified — real engine uses stop-loss/take-profit) + # or use actual market price if available tickers_to_close = [] for ticker, pos_info in simulated_positions.items(): hold_days = (current_date - pos_info["entry_date"]).days @@ -221,8 +221,8 @@ class BacktestReplay: for ticker in tickers_to_close: pos_info = simulated_positions.pop(ticker) - # Simulate a small random-ish exit based on entry price - exit_price = pos_info["entry_price"] * 1.01 # simplified + # Use actual market price if available, otherwise estimate + exit_price = company_prices.get(ticker, pos_info["entry_price"] * 1.01) qty = pos_info["quantity"] pnl = (exit_price - pos_info["entry_price"]) * qty pnl_pct = ( @@ -253,10 +253,10 @@ class BacktestReplay: 0, portfolio_state.open_position_count - 1 ) - # Compute daily portfolio value + # Compute daily portfolio value using latest market prices positions_value = sum( - p["entry_price"] * p["quantity"] - for p in simulated_positions.values() + company_prices.get(t, p["entry_price"]) * p["quantity"] + for t, p in simulated_positions.items() ) current_value = portfolio_state.active_pool + positions_value portfolio_state.total_value = current_value @@ -277,6 +277,42 @@ class BacktestReplay: current_date += timedelta(days=1) + # Force-close any remaining open positions at end of backtest + # using the latest available market prices + for ticker in list(simulated_positions.keys()): + pos_info = simulated_positions.pop(ticker) + exit_price = company_prices.get(ticker, pos_info["entry_price"]) + qty = pos_info["quantity"] + pnl = (exit_price - pos_info["entry_price"]) * qty + pnl_pct = ( + (exit_price - pos_info["entry_price"]) / pos_info["entry_price"] + if pos_info["entry_price"] > 0 + else 0.0 + ) + hold_duration = timedelta( + days=(config.end_date - pos_info["entry_date"]).days + ) + + trade = ClosedTrade( + ticker=ticker, + entry_price=pos_info["entry_price"], + exit_price=exit_price, + quantity=qty, + pnl=pnl, + pnl_pct=pnl_pct, + hold_duration=hold_duration, + recommendation_id=pos_info.get("recommendation_id"), + ) + closed_trades.append(trade) + trade_log.append(self._perf.compute_trade_metrics(trade)) + portfolio_state.active_pool += exit_price * qty + + if closed_trades: + logger.info( + "Backtest %s completed: %d trades, final value=$%.2f", + backtest_id, len(closed_trades), portfolio_state.active_pool, + ) + # Compute final metrics metrics = self._perf.compute_metrics( closed_trades=closed_trades,