Files
docker-configs/backtest/generate_pdf_report.py
2025-07-18 00:00:01 -05:00

313 lines
12 KiB
Python
Executable File

import sqlite3
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.backends.backend_pdf import PdfPages
from datetime import datetime
import warnings
warnings.filterwarnings('ignore')
# Import the strategy
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), 'strategy', 'emotional-damage'))
from backtest_emotional_damage import run_emotional_damage_backtest
def calculate_performance_metrics(values, dates):
"""Calculate comprehensive performance metrics"""
# Convert to pandas Series if needed
if isinstance(values, list):
values = pd.Series(values, index=dates)
# Total return
total_return = (values.iloc[-1] / values.iloc[0] - 1) * 100
# Annualized return
years = (dates[-1] - dates[0]).days / 365.25
annual_return = ((values.iloc[-1] / values.iloc[0]) ** (1/years) - 1) * 100
# Calculate daily returns
daily_returns = values.pct_change().dropna()
# Volatility (annualized)
volatility = daily_returns.std() * np.sqrt(252) * 100
# Sharpe ratio (assuming 0% risk-free rate)
sharpe_ratio = (daily_returns.mean() * 252) / (daily_returns.std() * np.sqrt(252))
# Maximum drawdown
peak = values.expanding().max()
drawdown = (values - peak) / peak
max_drawdown = drawdown.min() * 100
max_drawdown_date = drawdown.idxmin()
# Annual returns by year
annual_returns = {}
for year in range(dates[0].year, dates[-1].year + 1):
year_mask = [d.year == year for d in dates]
if any(year_mask):
year_values = values[year_mask]
if len(year_values) > 1:
year_return = (year_values.iloc[-1] / year_values.iloc[0] - 1) * 100
annual_returns[year] = year_return
return {
'total_return': total_return,
'annual_return': annual_return,
'volatility': volatility,
'sharpe_ratio': sharpe_ratio,
'max_drawdown': max_drawdown,
'max_drawdown_date': max_drawdown_date,
'annual_returns': annual_returns
}
def create_pdf_report():
"""Generate comprehensive PDF report"""
print("Generating PDF report...")
# Run the backtest
results = run_emotional_damage_backtest()
strategy = results['strategy']
portfolio_df = results['portfolio_df']
benchmark_data = results['benchmark_data']
strategy_metrics = results['strategy_metrics']
qqq_metrics = results['qqq_metrics']
spy_metrics = results['spy_metrics']
# Create PDF
pdf_filename = f"emotional_damage_strategy_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
with PdfPages(pdf_filename) as pdf:
# Page 1: Title and Executive Summary
fig, ax = plt.subplots(figsize=(11, 8.5))
ax.axis('off')
# Title
ax.text(0.5, 0.9, 'Emotional Damage Strategy',
fontsize=24, fontweight='bold', ha='center')
ax.text(0.5, 0.85, 'Backtest Performance Report',
fontsize=18, ha='center')
ax.text(0.5, 0.8, f'Generated on {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}',
fontsize=12, ha='center')
# Strategy description
strategy_desc = """
Strategy Description:
The Emotional Damage strategy is a tactical allocation approach that:
• Starts with 100% QQQ allocation
• Switches to 100% cash when CNN Fear & Greed Index < 25 (extreme fear)
• Buys top 10 most volatile QQQ stocks when F&G recovers > 25
• Returns to QQQ when F&G Index > 75 (extreme greed)
Backtest Period: {} to {}
Total Trades Executed: {}
""".format(
portfolio_df.index[0].strftime('%Y-%m-%d'),
portfolio_df.index[-1].strftime('%Y-%m-%d'),
len(strategy.trades)
)
ax.text(0.05, 0.65, strategy_desc, fontsize=11, va='top')
# Performance summary table
summary_data = [
['Metric', 'Emotional Damage', 'QQQ Buy & Hold', 'SPY Buy & Hold'],
['Total Return', f"{strategy_metrics['total_return']:.1f}%",
f"{qqq_metrics['total_return']:.1f}%", f"{spy_metrics['total_return']:.1f}%"],
['Annual Return', f"{strategy_metrics['annual_return']:.1f}%",
f"{qqq_metrics['annual_return']:.1f}%", f"{spy_metrics['annual_return']:.1f}%"],
['Max Drawdown', f"{strategy_metrics['max_drawdown']:.1f}%",
f"{qqq_metrics['max_drawdown']:.1f}%", f"{spy_metrics['max_drawdown']:.1f}%"],
['Sharpe Ratio', f"{strategy_metrics['sharpe_ratio']:.2f}",
f"{qqq_metrics['sharpe_ratio']:.2f}", f"{spy_metrics['sharpe_ratio']:.2f}"],
['Max DD Date', strategy_metrics['max_drawdown_date'].strftime('%Y-%m-%d'),
qqq_metrics['max_drawdown_date'].strftime('%Y-%m-%d'),
spy_metrics['max_drawdown_date'].strftime('%Y-%m-%d')]
]
# Create table
table = ax.table(cellText=summary_data[1:], colLabels=summary_data[0],
cellLoc='center', loc='center', bbox=[0.05, 0.15, 0.9, 0.35])
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1, 2)
# Style header row
for i in range(len(summary_data[0])):
table[(0, i)].set_facecolor('#4472C4')
table[(0, i)].set_text_props(weight='bold', color='white')
plt.tight_layout()
pdf.savefig(fig, bbox_inches='tight')
plt.close()
# Page 2: Portfolio Value Over Time
fig, ax = plt.subplots(figsize=(11, 8.5))
# Normalize all series to same starting value for comparison
start_value = 100000
strategy_values = portfolio_df['value']
qqq_values = benchmark_data['qqq_value']
spy_values = benchmark_data['spy_value']
# Plot all three strategies
ax.plot(strategy_values.index, strategy_values, label='Emotional Damage Strategy',
linewidth=2, color='red')
ax.plot(qqq_values.index, qqq_values, label='QQQ Buy & Hold',
linewidth=2, color='blue')
ax.plot(spy_values.index, spy_values, label='SPY Buy & Hold',
linewidth=2, color='green')
ax.set_title('Portfolio Value Comparison Over Time', fontsize=16, fontweight='bold')
ax.set_xlabel('Date', fontsize=12)
ax.set_ylabel('Portfolio Value ($)', fontsize=12)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
# Format y-axis as currency
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:,.0f}'))
plt.xticks(rotation=45)
plt.tight_layout()
pdf.savefig(fig, bbox_inches='tight')
plt.close()
# Page 3: Annual Returns Comparison
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 8.5))
# Annual returns bar chart
years = sorted(set(strategy_metrics['annual_returns'].keys()) |
set(qqq_metrics['annual_returns'].keys()) |
set(spy_metrics['annual_returns'].keys()))
strategy_annual = [strategy_metrics['annual_returns'].get(year, 0) for year in years]
qqq_annual = [qqq_metrics['annual_returns'].get(year, 0) for year in years]
spy_annual = [spy_metrics['annual_returns'].get(year, 0) for year in years]
x = np.arange(len(years))
width = 0.25
ax1.bar(x - width, strategy_annual, width, label='Emotional Damage', color='red', alpha=0.7)
ax1.bar(x, qqq_annual, width, label='QQQ Buy & Hold', color='blue', alpha=0.7)
ax1.bar(x + width, spy_annual, width, label='SPY Buy & Hold', color='green', alpha=0.7)
ax1.set_title('Annual Returns Comparison', fontsize=14, fontweight='bold')
ax1.set_xlabel('Year')
ax1.set_ylabel('Annual Return (%)')
ax1.set_xticks(x)
ax1.set_xticklabels(years, rotation=45)
ax1.legend()
ax1.grid(True, alpha=0.3)
ax1.axhline(y=0, color='black', linestyle='-', alpha=0.5)
# Drawdown chart
strategy_peak = strategy_values.expanding().max()
strategy_dd = (strategy_values - strategy_peak) / strategy_peak * 100
qqq_peak = qqq_values.expanding().max()
qqq_dd = (qqq_values - qqq_peak) / qqq_peak * 100
spy_peak = spy_values.expanding().max()
spy_dd = (spy_values - spy_peak) / spy_peak * 100
ax2.fill_between(strategy_dd.index, strategy_dd, 0, alpha=0.3, color='red', label='Emotional Damage')
ax2.fill_between(qqq_dd.index, qqq_dd, 0, alpha=0.3, color='blue', label='QQQ Buy & Hold')
ax2.fill_between(spy_dd.index, spy_dd, 0, alpha=0.3, color='green', label='SPY Buy & Hold')
ax2.set_title('Drawdown Comparison', fontsize=14, fontweight='bold')
ax2.set_xlabel('Date')
ax2.set_ylabel('Drawdown (%)')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
pdf.savefig(fig, bbox_inches='tight')
plt.close()
# Page 4: Strategy Trades and Fear & Greed Index
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 8.5))
# Fear & Greed Index over time
fg_data = portfolio_df['fg_index']
ax1.plot(fg_data.index, fg_data, color='purple', linewidth=1)
ax1.axhline(y=25, color='red', linestyle='--', alpha=0.7, label='Fear Threshold (25)')
ax1.axhline(y=75, color='green', linestyle='--', alpha=0.7, label='Greed Threshold (75)')
ax1.fill_between(fg_data.index, 0, 25, alpha=0.2, color='red', label='Extreme Fear')
ax1.fill_between(fg_data.index, 75, 100, alpha=0.2, color='green', label='Extreme Greed')
ax1.set_title('CNN Fear & Greed Index Over Time', fontsize=14, fontweight='bold')
ax1.set_ylabel('Fear & Greed Index')
ax1.legend()
ax1.grid(True, alpha=0.3)
ax1.set_ylim(0, 100)
# Strategy state over time
states = portfolio_df['state']
state_colors = {'QQQ_HOLD': 'blue', 'CASH_WAIT': 'gray', 'VOLATILE_STOCKS': 'orange'}
for i, state in enumerate(states.unique()):
mask = states == state
ax2.scatter(states[mask].index, [i] * sum(mask),
c=state_colors.get(state, 'black'), label=state, alpha=0.6, s=1)
ax2.set_title('Strategy State Over Time', fontsize=14, fontweight='bold')
ax2.set_xlabel('Date')
ax2.set_ylabel('Strategy State')
ax2.set_yticks(range(len(states.unique())))
ax2.set_yticklabels(states.unique())
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
pdf.savefig(fig, bbox_inches='tight')
plt.close()
# Page 5: Trade Log (Recent Trades)
fig, ax = plt.subplots(figsize=(11, 8.5))
ax.axis('off')
ax.text(0.5, 0.95, 'Recent Trade Log (Last 20 Trades)',
fontsize=16, fontweight='bold', ha='center')
# Get recent trades
recent_trades = strategy.trades[-20:] if len(strategy.trades) >= 20 else strategy.trades
trade_data = [['Date', 'Action', 'Ticker', 'Shares', 'Price', 'Value']]
for trade in recent_trades:
trade_data.append([
trade['date'].strftime('%Y-%m-%d'),
trade['action'],
trade['ticker'],
f"{trade['shares']:.2f}",
f"${trade['price']:.2f}",
f"${trade['value']:,.2f}"
])
# Create table
if len(trade_data) > 1:
table = ax.table(cellText=trade_data[1:], colLabels=trade_data[0],
cellLoc='center', loc='center', bbox=[0.05, 0.1, 0.9, 0.8])
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 1.5)
# Style header row
for i in range(len(trade_data[0])):
table[(0, i)].set_facecolor('#4472C4')
table[(0, i)].set_text_props(weight='bold', color='white')
plt.tight_layout()
pdf.savefig(fig, bbox_inches='tight')
plt.close()
print(f"PDF report saved as: {pdf_filename}")
return pdf_filename
if __name__ == "__main__":
create_pdf_report()