fix(backtest): include unrealized PnL in equity curve for accurate MDD
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -335,6 +335,7 @@ class Backtester:
|
|||||||
logger.info(f"총 이벤트: {len(events):,}개")
|
logger.info(f"총 이벤트: {len(events):,}개")
|
||||||
|
|
||||||
# 메인 루프
|
# 메인 루프
|
||||||
|
latest_prices: dict[str, float] = {}
|
||||||
for ts, sym, candle_idx in events:
|
for ts, sym, candle_idx in events:
|
||||||
date_str = str(ts.date())
|
date_str = str(ts.date())
|
||||||
self.risk.new_day(date_str)
|
self.risk.new_day(date_str)
|
||||||
@@ -342,9 +343,10 @@ class Backtester:
|
|||||||
df_ind = all_indicators[sym]
|
df_ind = all_indicators[sym]
|
||||||
signal = all_signals[sym][candle_idx]
|
signal = all_signals[sym][candle_idx]
|
||||||
row = df_ind.iloc[candle_idx]
|
row = df_ind.iloc[candle_idx]
|
||||||
|
latest_prices[sym] = float(row["close"])
|
||||||
|
|
||||||
# 에퀴티 기록
|
# 에퀴티 기록
|
||||||
self._record_equity(ts)
|
self._record_equity(ts, current_prices=latest_prices)
|
||||||
|
|
||||||
# 1) 일일 손실 체크
|
# 1) 일일 손실 체크
|
||||||
if not self.risk.is_trading_allowed():
|
if not self.risk.is_trading_allowed():
|
||||||
@@ -568,12 +570,15 @@ class Backtester:
|
|||||||
}
|
}
|
||||||
self.trades.append(trade)
|
self.trades.append(trade)
|
||||||
|
|
||||||
def _record_equity(self, ts: pd.Timestamp):
|
def _record_equity(self, ts: pd.Timestamp, current_prices: dict[str, float] | None = None):
|
||||||
# 미실현 PnL 포함 에퀴티
|
|
||||||
unrealized = 0.0
|
unrealized = 0.0
|
||||||
for pos in self.positions.values():
|
for sym, pos in self.positions.items():
|
||||||
# 에퀴티 기록 시점에는 현재가를 알 수 없으므로 entry_price 기준으로 0 처리
|
price = (current_prices or {}).get(sym)
|
||||||
pass
|
if price is not None:
|
||||||
|
if pos.side == "LONG":
|
||||||
|
unrealized += (price - pos.entry_price) * pos.quantity
|
||||||
|
else:
|
||||||
|
unrealized += (pos.entry_price - price) * pos.quantity
|
||||||
equity = self.balance + unrealized
|
equity = self.balance + unrealized
|
||||||
self.equity_curve.append({"timestamp": str(ts), "equity": round(equity, 4)})
|
self.equity_curve.append({"timestamp": str(ts), "equity": round(equity, 4)})
|
||||||
if equity > self._peak_equity:
|
if equity > self._peak_equity:
|
||||||
|
|||||||
@@ -54,3 +54,26 @@ def test_default_sltp_backward_compatible(signal_df):
|
|||||||
if len(r_default) > 0:
|
if len(r_default) > 0:
|
||||||
assert len(r_default) == len(r_explicit)
|
assert len(r_default) == len(r_explicit)
|
||||||
assert (r_default["label"].values == r_explicit["label"].values).all()
|
assert (r_default["label"].values == r_explicit["label"].values).all()
|
||||||
|
|
||||||
|
|
||||||
|
def test_equity_curve_includes_unrealized_pnl():
|
||||||
|
"""에퀴티 커브에 미실현 PnL이 반영되어야 한다."""
|
||||||
|
from src.backtester import Backtester, BacktestConfig, Position
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
cfg = BacktestConfig(symbols=["TEST"], initial_balance=1000.0)
|
||||||
|
bt = Backtester.__new__(Backtester)
|
||||||
|
bt.cfg = cfg
|
||||||
|
bt.balance = 1000.0
|
||||||
|
bt._peak_equity = 1000.0
|
||||||
|
bt.equity_curve = []
|
||||||
|
bt.positions = {"TEST": Position(
|
||||||
|
symbol="TEST", side="LONG", entry_price=100.0,
|
||||||
|
quantity=10.0, sl=95.0, tp=110.0,
|
||||||
|
entry_time=pd.Timestamp("2026-01-01"), entry_fee=0.4,
|
||||||
|
)}
|
||||||
|
|
||||||
|
bt._record_equity(pd.Timestamp("2026-01-01 00:15:00"), current_prices={"TEST": 105.0})
|
||||||
|
|
||||||
|
last = bt.equity_curve[-1]
|
||||||
|
assert last["equity"] == 1050.0, f"Expected 1050.0 (1000+50), got {last['equity']}"
|
||||||
|
|||||||
Reference in New Issue
Block a user