213 lines
7.5 KiB
Python
213 lines
7.5 KiB
Python
"""
|
|
Performance Analysis and Reporting
|
|
|
|
This module provides tools for analyzing backtest results and generating reports.
|
|
"""
|
|
|
|
from typing import Dict, Any, List
|
|
import pandas as pd
|
|
import matplotlib.pyplot as plt
|
|
import numpy as np
|
|
from datetime import datetime
|
|
|
|
|
|
class PerformanceAnalyzer:
|
|
"""
|
|
Analyzes backtest performance and generates reports.
|
|
"""
|
|
|
|
def __init__(self, backtest_results: Dict[str, Any]):
|
|
"""
|
|
Initialize with backtest results.
|
|
|
|
Args:
|
|
backtest_results: Results dictionary from BacktestEngine.run()
|
|
"""
|
|
self.results = backtest_results
|
|
self.metrics = backtest_results['metrics']
|
|
self.trades = backtest_results['trades']
|
|
self.strategy_name = backtest_results['strategy_name']
|
|
|
|
def print_summary(self):
|
|
"""Print a summary of the backtest results."""
|
|
print("\n" + "="*60)
|
|
print(f"BACKTEST SUMMARY: {self.strategy_name}")
|
|
print("="*60)
|
|
print(f"\nInitial Balance: ${self.metrics['initial_balance']:,.2f}")
|
|
print(f"Final Balance: ${self.metrics['final_balance']:,.2f}")
|
|
print(f"Total Return: {self.metrics['total_return_pct']:.2f}%")
|
|
print(f"\nTotal Trades: {self.metrics['total_trades']}")
|
|
print(f"Winning Trades: {self.metrics['winning_trades']}")
|
|
print(f"Losing Trades: {self.metrics['losing_trades']}")
|
|
print(f"Win Rate: {self.metrics['win_rate_pct']:.2f}%")
|
|
print(f"\nTotal Profit: ${self.metrics['total_profit']:,.2f}")
|
|
print(f"Total Loss: ${self.metrics['total_loss']:,.2f}")
|
|
print(f"Profit Factor: {self.metrics['profit_factor']:.2f}")
|
|
print(f"\nAverage Win: ${self.metrics['avg_win']:,.2f}")
|
|
print(f"Average Loss: ${self.metrics['avg_loss']:,.2f}")
|
|
print(f"Max Drawdown: {self.metrics['max_drawdown_pct']:.2f}%")
|
|
|
|
if self.metrics.get('parameters'):
|
|
print(f"\nStrategy Parameters:")
|
|
for key, value in self.metrics['parameters'].items():
|
|
print(f" {key}: {value}")
|
|
|
|
print("="*60 + "\n")
|
|
|
|
def get_trades_dataframe(self) -> pd.DataFrame:
|
|
"""Convert trades list to pandas DataFrame."""
|
|
if not self.trades:
|
|
return pd.DataFrame()
|
|
|
|
df = pd.DataFrame(self.trades)
|
|
df['open_time'] = pd.to_datetime(df['open_time'])
|
|
df['close_time'] = pd.to_datetime(df['close_time'])
|
|
df['duration'] = df['close_time'] - df['open_time']
|
|
|
|
return df
|
|
|
|
def plot_equity_curve(self, save_path: str = None):
|
|
"""
|
|
Plot equity curve over time.
|
|
|
|
Args:
|
|
save_path: Optional path to save the plot
|
|
"""
|
|
if not self.trades:
|
|
print("No trades to plot")
|
|
return
|
|
|
|
df = self.get_trades_dataframe()
|
|
df = df.sort_values('close_time')
|
|
|
|
# Calculate cumulative equity
|
|
cumulative_profit = df['profit'].cumsum()
|
|
equity_curve = self.metrics['initial_balance'] + cumulative_profit
|
|
|
|
plt.figure(figsize=(12, 6))
|
|
plt.plot(df['close_time'], equity_curve, linewidth=2, label='Equity')
|
|
plt.axhline(y=self.metrics['initial_balance'], color='r', linestyle='--', label='Initial Balance')
|
|
plt.xlabel('Time')
|
|
plt.ylabel('Equity ($)')
|
|
plt.title(f'Equity Curve - {self.strategy_name}')
|
|
plt.legend()
|
|
plt.grid(True, alpha=0.3)
|
|
plt.tight_layout()
|
|
|
|
if save_path:
|
|
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
|
print(f"Equity curve saved to {save_path}")
|
|
else:
|
|
plt.show()
|
|
|
|
def plot_drawdown(self, save_path: str = None):
|
|
"""
|
|
Plot drawdown over time.
|
|
|
|
Args:
|
|
save_path: Optional path to save the plot
|
|
"""
|
|
if not self.trades:
|
|
print("No trades to plot")
|
|
return
|
|
|
|
df = self.get_trades_dataframe()
|
|
df = df.sort_values('close_time')
|
|
|
|
# Calculate cumulative equity
|
|
cumulative_profit = df['profit'].cumsum()
|
|
equity_curve = self.metrics['initial_balance'] + cumulative_profit
|
|
|
|
# Calculate running maximum
|
|
running_max = equity_curve.expanding().max()
|
|
drawdown = (equity_curve - running_max) / running_max * 100
|
|
|
|
plt.figure(figsize=(12, 6))
|
|
plt.fill_between(df['close_time'], drawdown, 0, alpha=0.3, color='red', label='Drawdown')
|
|
plt.plot(df['close_time'], drawdown, linewidth=1, color='darkred')
|
|
plt.xlabel('Time')
|
|
plt.ylabel('Drawdown (%)')
|
|
plt.title(f'Drawdown Chart - {self.strategy_name}')
|
|
plt.legend()
|
|
plt.grid(True, alpha=0.3)
|
|
plt.tight_layout()
|
|
|
|
if save_path:
|
|
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
|
print(f"Drawdown chart saved to {save_path}")
|
|
else:
|
|
plt.show()
|
|
|
|
def plot_monthly_returns(self, save_path: str = None):
|
|
"""
|
|
Plot monthly returns.
|
|
|
|
Args:
|
|
save_path: Optional path to save the plot
|
|
"""
|
|
if not self.trades:
|
|
print("No trades to plot")
|
|
return
|
|
|
|
df = self.get_trades_dataframe()
|
|
df = df.sort_values('close_time')
|
|
|
|
# Group by month
|
|
df['month'] = df['close_time'].dt.to_period('M')
|
|
monthly_returns = df.groupby('month')['profit'].sum()
|
|
monthly_returns_pct = (monthly_returns / self.metrics['initial_balance']) * 100
|
|
|
|
plt.figure(figsize=(12, 6))
|
|
colors = ['green' if x > 0 else 'red' for x in monthly_returns_pct]
|
|
plt.bar(range(len(monthly_returns_pct)), monthly_returns_pct, color=colors, alpha=0.7)
|
|
plt.xlabel('Month')
|
|
plt.ylabel('Return (%)')
|
|
plt.title(f'Monthly Returns - {self.strategy_name}')
|
|
plt.xticks(range(len(monthly_returns_pct)), [str(x) for x in monthly_returns_pct.index], rotation=45)
|
|
plt.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
|
|
plt.grid(True, alpha=0.3, axis='y')
|
|
plt.tight_layout()
|
|
|
|
if save_path:
|
|
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
|
print(f"Monthly returns chart saved to {save_path}")
|
|
else:
|
|
plt.show()
|
|
|
|
def export_trades_csv(self, filepath: str):
|
|
"""
|
|
Export trades to CSV file.
|
|
|
|
Args:
|
|
filepath: Path to save CSV file
|
|
"""
|
|
df = self.get_trades_dataframe()
|
|
df.to_csv(filepath, index=False)
|
|
print(f"Trades exported to {filepath}")
|
|
|
|
def generate_report(self, output_dir: str = "backtest_results"):
|
|
"""
|
|
Generate a comprehensive report with all charts and data.
|
|
|
|
Args:
|
|
output_dir: Directory to save report files
|
|
"""
|
|
import os
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
prefix = f"{self.strategy_name}_{timestamp}"
|
|
|
|
# Print summary
|
|
self.print_summary()
|
|
|
|
# Generate plots
|
|
self.plot_equity_curve(os.path.join(output_dir, f"{prefix}_equity_curve.png"))
|
|
self.plot_drawdown(os.path.join(output_dir, f"{prefix}_drawdown.png"))
|
|
self.plot_monthly_returns(os.path.join(output_dir, f"{prefix}_monthly_returns.png"))
|
|
|
|
# Export trades
|
|
self.export_trades_csv(os.path.join(output_dir, f"{prefix}_trades.csv"))
|
|
|
|
print(f"\nReport generated in {output_dir}/")
|