Initial commit: multi-symbol bot with backtest engine and RSI trend strategy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
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]),
|
||||
)
|
||||
Reference in New Issue
Block a user