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()