deploy: 2026-03-20 07:49
This commit is contained in:
85
backtest/performance.py
Normal file
85
backtest/performance.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Performance analysis and reporting utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from backtest.backtester import BacktestResult, BacktestTrade
|
||||
|
||||
|
||||
def compute_sharpe_ratio(
|
||||
equity_curve: List[float], risk_free_rate: float = 0.0, periods: int = 252
|
||||
) -> float:
|
||||
"""Compute annualised Sharpe ratio from an equity curve."""
|
||||
if len(equity_curve) < 2:
|
||||
return 0.0
|
||||
returns = pd.Series(equity_curve).pct_change().dropna()
|
||||
excess = returns - risk_free_rate / periods
|
||||
if excess.std() == 0:
|
||||
return 0.0
|
||||
return float((excess.mean() / excess.std()) * np.sqrt(periods))
|
||||
|
||||
|
||||
def compute_sortino_ratio(
|
||||
equity_curve: List[float], risk_free_rate: float = 0.0, periods: int = 252
|
||||
) -> float:
|
||||
"""Compute annualised Sortino ratio (downside deviation only)."""
|
||||
if len(equity_curve) < 2:
|
||||
return 0.0
|
||||
returns = pd.Series(equity_curve).pct_change().dropna()
|
||||
excess = returns - risk_free_rate / periods
|
||||
downside = excess[excess < 0]
|
||||
if len(downside) == 0 or downside.std() == 0:
|
||||
return float("inf") if excess.mean() > 0 else 0.0
|
||||
return float((excess.mean() / downside.std()) * np.sqrt(periods))
|
||||
|
||||
|
||||
def compute_max_consecutive_losses(trades: List[BacktestTrade]) -> int:
|
||||
"""Return the longest streak of consecutive losing trades."""
|
||||
max_streak = 0
|
||||
current = 0
|
||||
for t in trades:
|
||||
if t.pnl <= 0:
|
||||
current += 1
|
||||
max_streak = max(max_streak, current)
|
||||
else:
|
||||
current = 0
|
||||
return max_streak
|
||||
|
||||
|
||||
def compute_calmar_ratio(
|
||||
total_return: float, max_drawdown: float, years: float = 1.0
|
||||
) -> float:
|
||||
"""Calmar ratio = annualised return / max drawdown."""
|
||||
if max_drawdown == 0:
|
||||
return float("inf") if total_return > 0 else 0.0
|
||||
annual_return = total_return / years
|
||||
return annual_return / max_drawdown
|
||||
|
||||
|
||||
def trade_analysis(trades: List[BacktestTrade]) -> dict:
|
||||
"""Detailed trade-level statistics."""
|
||||
if not trades:
|
||||
return {}
|
||||
|
||||
pnls = [t.pnl for t in trades]
|
||||
wins = [p for p in pnls if p > 0]
|
||||
losses = [p for p in pnls if p <= 0]
|
||||
durations = [t.exit_index - t.entry_index for t in trades]
|
||||
|
||||
return {
|
||||
"total_trades": len(trades),
|
||||
"avg_pnl": np.mean(pnls),
|
||||
"median_pnl": np.median(pnls),
|
||||
"std_pnl": np.std(pnls),
|
||||
"best_trade": max(pnls),
|
||||
"worst_trade": min(pnls),
|
||||
"avg_win": np.mean(wins) if wins else 0,
|
||||
"avg_loss": np.mean(losses) if losses else 0,
|
||||
"max_consecutive_losses": compute_max_consecutive_losses(trades),
|
||||
"avg_duration_candles": np.mean(durations),
|
||||
"total_fees": sum(t.fee for t in trades),
|
||||
}
|
||||
Reference in New Issue
Block a user