86 lines
3.2 KiB
Python
86 lines
3.2 KiB
Python
|
|
"""Performance metrics for backtesting."""
|
||
|
|
|
||
|
|
from decimal import Decimal
|
||
|
|
from typing import Dict, List, Any, Optional
|
||
|
|
from src.trading.paper_trading import PaperTradingSimulator
|
||
|
|
|
||
|
|
class BacktestMetrics:
|
||
|
|
"""Calculates backtest performance metrics."""
|
||
|
|
|
||
|
|
def calculate_metrics(
|
||
|
|
self,
|
||
|
|
simulator: PaperTradingSimulator,
|
||
|
|
trades: List[Dict[str, Any]],
|
||
|
|
initial_capital: Decimal,
|
||
|
|
total_fees: Optional[Decimal] = None
|
||
|
|
) -> Dict[str, Any]:
|
||
|
|
"""Calculate backtest metrics, including fee-adjusted metrics.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
simulator: Paper trading simulator
|
||
|
|
trades: List of executed trades (may include 'fee' field)
|
||
|
|
initial_capital: Initial capital
|
||
|
|
total_fees: Total fees paid (if None, calculated from trades)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary of metrics
|
||
|
|
"""
|
||
|
|
final_value = simulator.get_portfolio_value()
|
||
|
|
|
||
|
|
# Calculate total fees if not provided
|
||
|
|
if total_fees is None:
|
||
|
|
total_fees = sum(
|
||
|
|
Decimal(str(trade.get('fee', 0))) for trade in trades
|
||
|
|
)
|
||
|
|
|
||
|
|
# Gross return (before fees)
|
||
|
|
gross_return = ((final_value - initial_capital) / initial_capital) * 100 if initial_capital > 0 else 0
|
||
|
|
|
||
|
|
# Net return (after fees) - fees are already deducted in simulator
|
||
|
|
# But we calculate what return would be without fees for comparison
|
||
|
|
value_without_fees = final_value + total_fees
|
||
|
|
net_return = ((final_value - initial_capital) / initial_capital) * 100 if initial_capital > 0 else 0
|
||
|
|
gross_return_without_fees = ((value_without_fees - initial_capital) / initial_capital) * 100 if initial_capital > 0 else 0
|
||
|
|
|
||
|
|
# Fee impact
|
||
|
|
fee_impact = gross_return_without_fees - net_return
|
||
|
|
|
||
|
|
# Calculate win rate from trades
|
||
|
|
win_rate = self._calculate_win_rate(trades)
|
||
|
|
|
||
|
|
return {
|
||
|
|
"initial_capital": float(initial_capital),
|
||
|
|
"final_capital": float(final_value),
|
||
|
|
"total_return": float(net_return), # Net return (after fees)
|
||
|
|
"gross_return": float(gross_return_without_fees), # Gross return (before fees)
|
||
|
|
"total_fees": float(total_fees),
|
||
|
|
"fee_impact_percent": float(fee_impact),
|
||
|
|
"fee_percentage": float((total_fees / initial_capital) * 100) if initial_capital > 0 else 0.0,
|
||
|
|
"total_trades": len(trades),
|
||
|
|
"win_rate": win_rate,
|
||
|
|
"sharpe_ratio": 0.0, # Placeholder - would need returns series
|
||
|
|
"max_drawdown": 0.0, # Placeholder - would need equity curve
|
||
|
|
}
|
||
|
|
|
||
|
|
def _calculate_win_rate(self, trades: List[Dict[str, Any]]) -> float:
|
||
|
|
"""Calculate win rate from trades.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
trades: List of trades
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Win rate (0.0 to 1.0)
|
||
|
|
"""
|
||
|
|
if len(trades) < 2:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
# Simple approach: count buy-sell pairs
|
||
|
|
# This is a simplified calculation - full implementation would track positions
|
||
|
|
wins = 0
|
||
|
|
total_pairs = 0
|
||
|
|
|
||
|
|
# Group trades by symbol and calculate pairs
|
||
|
|
# For now, return placeholder
|
||
|
|
return 0.5
|
||
|
|
|