diff --git a/apps/backtester/run.py b/apps/backtester/run.py index 9ea98b8..762bf27 100644 --- a/apps/backtester/run.py +++ b/apps/backtester/run.py @@ -234,8 +234,11 @@ class BacktestRunner: self._open_positions = still_open # --- Compute current equity for kill-switch check --- - unrealized = self._compute_unrealized_pnl(date) - self._equity = self._cash + unrealized + market_value = self._compute_positions_market_value(date) + unrealized = market_value - sum( + p.entry_price * p.shares_open for p in self._open_positions + ) + self._equity = self._cash + market_value self._peak_equity = max(self._peak_equity, self._equity) drawdown_pct = ( (self._peak_equity - self._equity) / self._peak_equity * 100.0 @@ -281,14 +284,23 @@ class BacktestRunner: self._open_positions.append(pos) self._cash -= pos.entry_price * pos.shares_total self._daily_new_risk_used += plan.risk_dollars - # Update portfolio state for next candidate in same day + # Update equity and portfolio state for next candidate + mv = self._compute_positions_market_value(date) + self._equity = self._cash + mv + ur = mv - sum( + p.entry_price * p.shares_open + for p in self._open_positions + ) portfolio_state = self._build_portfolio_state( - date, drawdown_pct, self._compute_unrealized_pnl(date) + date, drawdown_pct, ur ) # --- Record daily equity curve snapshot --- - unrealized_final = self._compute_unrealized_pnl(date) - self._equity = self._cash + unrealized_final + market_value_final = self._compute_positions_market_value(date) + unrealized_final = market_value_final - sum( + p.entry_price * p.shares_open for p in self._open_positions + ) + self._equity = self._cash + market_value_final self._peak_equity = max(self._peak_equity, self._equity) final_drawdown = ( (self._peak_equity - self._equity) / self._peak_equity * 100.0 @@ -316,14 +328,27 @@ class BacktestRunner: ) ) - def _compute_unrealized_pnl(self, date: dt.date) -> float: + def _compute_positions_market_value(self, date: dt.date) -> float: + """Market value of all open positions using today's close. + + Falls back to entry_price when bar is missing (assumes no change + rather than treating the position as worthless). + """ total = 0.0 for pos in self._open_positions: bar = self.store.get_bar(pos.plan.candidate.symbol, date) if bar and bar.get("close"): - total += (float(bar["close"]) - pos.entry_price) * pos.shares_open + total += float(bar["close"]) * pos.shares_open + else: + total += pos.entry_price * pos.shares_open return total + def _compute_unrealized_pnl(self, date: dt.date) -> float: + """Unrealized PnL = market_value − cost_basis.""" + market_value = self._compute_positions_market_value(date) + cost_basis = sum(p.entry_price * p.shares_open for p in self._open_positions) + return market_value - cost_basis + def _build_portfolio_state( self, date: dt.date,