Files
crypto_news/backtest/performance.py

86 lines
2.7 KiB
Python
Raw Permalink Normal View History

2026-03-20 07:49:42 +09:00
"""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),
}