Files
TradingBot-NewBot/engine/metrics.py
T

110 lines
3.8 KiB
Python

"""
Performance metrics computed from a completed backtest.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Sequence
import numpy as np
from .backtest import Trade
@dataclass
class Metrics:
n_trades: int
n_wins: int
n_losses: int
win_rate: float # 0-1
avg_win_r: float # average win in R
avg_loss_r: float # average loss in R (negative)
profit_factor: float
expectancy_r: float # expected R per trade
total_pips: float
total_return: float # fractional (e.g. 0.12 = 12%)
max_drawdown: float # fractional (negative)
sharpe: float # annualised daily Sharpe
avg_duration: float # hours
n_longs: int
n_shorts: int
final_balance: float
def __str__(self) -> str:
lines = [
f" Trades : {self.n_trades} ({self.n_longs}L / {self.n_shorts}S)",
f" Win rate : {self.win_rate*100:.1f}%",
f" Avg win (R) : {self.avg_win_r:+.2f}R",
f" Avg loss (R) : {self.avg_loss_r:+.2f}R",
f" Expectancy : {self.expectancy_r:+.3f}R / trade",
f" Profit factor : {self.profit_factor:.2f}",
f" Total pips : {self.total_pips:+.1f}",
f" Total return : {self.total_return*100:+.2f}%",
f" Final balance : ${self.final_balance:,.2f}",
f" Max drawdown : {self.max_drawdown*100:.2f}%",
f" Sharpe : {self.sharpe:.2f}",
f" Avg duration : {self.avg_duration:.1f} h",
]
return "\n".join(lines)
def score(self) -> float:
"""Composite optimisation score (higher = better)."""
if self.n_trades < 20:
return -999.0
return self.sharpe * (1 + self.expectancy_r) * (1 + self.total_return)
def compute(
trades: list[Trade],
equity: np.ndarray,
initial_balance: float = 10_000.0,
) -> Metrics:
closed = [t for t in trades if t.closed]
if not closed:
return Metrics(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, initial_balance)
pips = np.array([t.pnl_pips for t in closed])
r_vals = np.array([t.pnl_r for t in closed])
wins = r_vals[r_vals > 0]
losses = r_vals[r_vals <= 0]
gross_profit = wins.sum() if len(wins) else 0.0
gross_loss = abs(losses.sum()) if len(losses) else 0.0
pf = gross_profit / gross_loss if gross_loss else float("inf")
# equity metrics
peak = np.maximum.accumulate(equity)
dd = (equity - peak) / peak
max_dd = dd.min()
# annualised Sharpe via daily returns
by_day: dict[str, float] = {}
for t in closed:
day = t.exit_time.date().isoformat() # type: ignore[union-attr]
by_day[day] = by_day.get(day, 0) + t.pnl_r * t.risk_pct
daily = np.array(list(by_day.values()))
sharpe = (daily.mean() / daily.std() * np.sqrt(252)) if (len(daily) > 1 and daily.std() > 0) else 0.0
durations = [t.duration_hours for t in closed]
return Metrics(
n_trades = len(closed),
n_wins = int((r_vals > 0).sum()),
n_losses = int((r_vals <= 0).sum()),
win_rate = float((r_vals > 0).mean()),
avg_win_r = float(wins.mean()) if len(wins) else 0.0,
avg_loss_r = float(losses.mean()) if len(losses) else 0.0,
profit_factor = pf,
expectancy_r = float(r_vals.mean()),
total_pips = float(pips.sum()),
total_return = float(equity[-1] / initial_balance - 1),
max_drawdown = float(max_dd),
sharpe = float(sharpe),
avg_duration = float(np.mean(durations)),
n_longs = sum(1 for t in closed if t.direction == "long"),
n_shorts = sum(1 for t in closed if t.direction == "short"),
final_balance = float(equity[-1]),
)