54 lines
1.7 KiB
Python
54 lines
1.7 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
from dataclasses import dataclass
|
||
|
|
import numpy as np
|
||
|
|
import pandas as pd
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Perf:
|
||
|
|
total_return: float
|
||
|
|
max_drawdown: float
|
||
|
|
sharpe_ratio: float
|
||
|
|
win_rate: float
|
||
|
|
num_trades: int
|
||
|
|
final_equity: float
|
||
|
|
initial_equity: float
|
||
|
|
num_stop_losses: int
|
||
|
|
total_fees: float
|
||
|
|
total_slippage_usd: float
|
||
|
|
avg_slippage_bps: float
|
||
|
|
|
||
|
|
|
||
|
|
def compute_metrics(equity_curve: pd.Series, trades: list[dict]) -> Perf:
|
||
|
|
ret = equity_curve.pct_change().fillna(0.0)
|
||
|
|
total_return = equity_curve.iat[-1] / equity_curve.iat[0] - 1.0
|
||
|
|
cummax = equity_curve.cummax()
|
||
|
|
dd = (equity_curve / cummax - 1.0).min()
|
||
|
|
max_drawdown = dd
|
||
|
|
|
||
|
|
if ret.std(ddof=0) > 0:
|
||
|
|
sharpe = (ret.mean() / ret.std(ddof=0)) * np.sqrt(252 * 24 * 60) # minute bars -> annualized
|
||
|
|
else:
|
||
|
|
sharpe = 0.0
|
||
|
|
|
||
|
|
closes = [t for t in trades if t.get("side") == "SELL"]
|
||
|
|
wins = [t for t in closes if t.get("pnl", 0.0) > 0]
|
||
|
|
win_rate = (len(wins) / len(closes)) if closes else 0.0
|
||
|
|
|
||
|
|
fees = sum(t.get("fee", 0.0) for t in trades)
|
||
|
|
slip = sum(t.get("slippage", 0.0) for t in trades)
|
||
|
|
slippage_bps = [t.get("slippage_bps", 0.0) for t in trades if "slippage_bps" in t]
|
||
|
|
|
||
|
|
return Perf(
|
||
|
|
total_return=total_return,
|
||
|
|
max_drawdown=max_drawdown,
|
||
|
|
sharpe_ratio=sharpe,
|
||
|
|
win_rate=win_rate,
|
||
|
|
num_trades=len(closes),
|
||
|
|
final_equity=float(equity_curve.iat[-1]),
|
||
|
|
initial_equity=float(equity_curve.iat[0]),
|
||
|
|
num_stop_losses=sum(1 for t in closes if t.get("reason") == "stop"),
|
||
|
|
total_fees=fees,
|
||
|
|
total_slippage_usd=slip,
|
||
|
|
avg_slippage_bps=float(np.mean(slippage_bps)) if slippage_bps else 0.0,
|
||
|
|
)
|