import matplotlib.pyplot as plt import seaborn as sns from matplotlib.backends.backend_pdf import PdfPages import pandas as pd import numpy as np from datetime import datetime import sqlite3 import sys import os # Add the strategy path sys.path.insert(0, os.path.join(os.path.dirname(__file__))) from backtest_emotional_damage_enhanced_v2 import EnhancedEmotionalDamageStrategy def run_enhanced_backtest_local(): """Run enhanced strategy backtest locally""" strategy = EnhancedEmotionalDamageStrategy(initial_capital=100000) strategy.run_backtest() # Convert results to DataFrame portfolio_df = pd.DataFrame(strategy.portfolio_value) portfolio_df.set_index('date', inplace=True) # Get benchmark data (QQQ and SPY) import os script_dir = os.path.dirname(os.path.abspath(__file__)) backtest_dir = os.path.dirname(os.path.dirname(script_dir)) db_path = os.path.join(backtest_dir, 'data', 'stock_data.db') print(f"Connecting to database at: {db_path}") conn = sqlite3.connect(db_path) benchmark_data = pd.read_sql_query(''' SELECT date, spy_close FROM fear_greed_data ORDER BY date ''', conn) benchmark_data['date'] = pd.to_datetime(benchmark_data['date']) benchmark_data.set_index('date', inplace=True) conn.close() # Align dates common_dates = portfolio_df.index.intersection(benchmark_data.index) portfolio_df = portfolio_df.loc[common_dates] benchmark_data = benchmark_data.loc[common_dates] # Normalize to starting value for comparison start_value = 100000 portfolio_df['normalized'] = portfolio_df['value'] # Create QQQ and SPY buy-and-hold benchmarks benchmark_data['qqq_value'] = start_value * (benchmark_data['spy_close'] / benchmark_data['spy_close'].iloc[0]) benchmark_data['spy_value'] = start_value * (benchmark_data['spy_close'] / benchmark_data['spy_close'].iloc[0]) # Calculate performance metrics def calculate_performance_metrics(returns): """Calculate performance metrics""" total_return = (returns.iloc[-1] / returns.iloc[0] - 1) * 100 annual_return = ((returns.iloc[-1] / returns.iloc[0]) ** (252 / len(returns)) - 1) * 100 # Calculate max drawdown peak = returns.expanding().max() drawdown = (returns - peak) / peak max_drawdown = drawdown.min() * 100 # Find max drawdown period max_dd_date = drawdown.idxmin() max_dd_year = max_dd_date.year # Calculate Sharpe ratio daily_returns = returns.pct_change().dropna() sharpe_ratio = np.sqrt(252) * daily_returns.mean() / daily_returns.std() # Annual returns by year annual_rets = {} for year in returns.index.year.unique(): year_data = returns[returns.index.year == year] if len(year_data) > 1: year_return = (year_data.iloc[-1] / year_data.iloc[0] - 1) * 100 annual_rets[year] = year_return return { 'total_return': total_return, 'annual_return': annual_return, 'max_drawdown': max_drawdown, 'max_drawdown_date': max_dd_date, 'max_drawdown_year': max_dd_year, 'sharpe_ratio': sharpe_ratio, 'annual_returns': annual_rets } strategy_metrics = calculate_performance_metrics(portfolio_df['value']) qqq_metrics = calculate_performance_metrics(benchmark_data['qqq_value']) spy_metrics = calculate_performance_metrics(benchmark_data['spy_value']) return { 'strategy': strategy, 'portfolio_df': portfolio_df, 'benchmark_data': benchmark_data, 'strategy_metrics': strategy_metrics, 'qqq_metrics': qqq_metrics, 'spy_metrics': spy_metrics } def generate_enhanced_pdf_report(): """Generate comprehensive PDF report for enhanced strategy""" print("Running enhanced strategy backtest...") results = run_enhanced_backtest_local() 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 report in the strategy directory report_filename = f"enhanced_emotional_damage_strategy_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf" report_path = os.path.join(os.path.dirname(__file__), report_filename) with PdfPages(report_path) as pdf: # Page 1: Executive Summary fig = plt.figure(figsize=(16, 12)) fig.suptitle('Enhanced Emotional Damage Strategy - Comprehensive Analysis', fontsize=20, fontweight='bold') # Create grid layout gs = fig.add_gridspec(3, 3, hspace=0.3, wspace=0.3) # Performance comparison table ax1 = fig.add_subplot(gs[0, :]) ax1.axis('tight') ax1.axis('off') table_data = [ ['Metric', 'Enhanced Strategy', 'QQQ Buy & Hold', 'SPY Buy & Hold'], [f'Total Return', f'{strategy_metrics["total_return"]:.1f}%', f'{qqq_metrics["total_return"]:.1f}%', f'{spy_metrics["total_return"]:.1f}%'], [f'Annual Return', f'{strategy_metrics["annual_return"]:.1f}%', f'{qqq_metrics["annual_return"]:.1f}%', f'{spy_metrics["annual_return"]:.1f}%'], [f'Max Drawdown', f'{strategy_metrics["max_drawdown"]:.1f}%', f'{qqq_metrics["max_drawdown"]:.1f}%', f'{spy_metrics["max_drawdown"]:.1f}%'], [f'Sharpe Ratio', f'{strategy_metrics["sharpe_ratio"]:.2f}', f'{qqq_metrics["sharpe_ratio"]:.2f}', f'{spy_metrics["sharpe_ratio"]:.2f}'], [f'Max DD Year', f'{strategy_metrics["max_drawdown_year"]}', f'{qqq_metrics.get("max_drawdown_year", "N/A")}', f'{spy_metrics.get("max_drawdown_year", "N/A")}'] ] table = ax1.table(cellText=table_data[1:], colLabels=table_data[0], cellLoc='center', loc='upper center', colWidths=[0.25, 0.25, 0.25, 0.25]) table.auto_set_font_size(False) table.set_fontsize(11) table.scale(1.2, 2.5) table.auto_set_column_width(col=list(range(len(table_data[0])))) # Color the header row for i in range(len(table_data[0])): table[(0, i)].set_facecolor('#4CAF50') table[(0, i)].set_text_props(weight='bold', color='white') ax1.set_title('Performance Summary (18+ Years Backtest)', fontsize=16, fontweight='bold', pad=30) # Portfolio value over time ax2 = fig.add_subplot(gs[1, :2]) ax2.plot(portfolio_df.index, portfolio_df['value'], label='Enhanced Strategy', linewidth=2.5, color='#2E86AB') ax2.plot(benchmark_data.index, benchmark_data['qqq_value'], label='QQQ', alpha=0.7, color='#A23B72') ax2.plot(benchmark_data.index, benchmark_data['spy_value'], label='SPY', alpha=0.7, color='#F18F01') ax2.set_title('Portfolio Value Over Time (Starting from $100,000)', fontsize=14, fontweight='bold') ax2.set_ylabel('Portfolio Value ($)', fontsize=12) ax2.legend(fontsize=11) ax2.grid(True, alpha=0.3) ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x/1000:.0f}K')) # Fear & Greed Index with strategy states ax3 = fig.add_subplot(gs[1, 2]) ax3_twin = ax3.twinx() # Fear & Greed Index ax3.plot(portfolio_df.index, portfolio_df['fg_index'], color='red', alpha=0.7, linewidth=1) ax3.axhline(y=25, color='red', linestyle='--', alpha=0.5, label='Fear (25)') ax3.axhline(y=75, color='green', linestyle='--', alpha=0.5, label='Greed (75)') ax3.set_ylabel('Fear & Greed Index', color='red', fontsize=10) ax3.tick_params(axis='y', labelcolor='red') ax3.set_ylim(0, 100) ax3.set_title('Fear & Greed Index', fontsize=12) # Strategy states as background state_colors = {'QQQ_HOLD': '#E8F5E8', 'FEAR_TRANSITION': '#FFE6E6', 'CASH_WAIT': '#FFF2E6', 'GREED_TRANSITION': '#E6F3FF', 'VOLATILE_STOCKS': '#FFE6CC', 'QQQ_TRANSITION': '#FFE6F0'} current_state = None start_idx = 0 for i, (idx, row) in enumerate(portfolio_df.iterrows()): if row['state'] != current_state: if current_state is not None: ax3.axvspan(portfolio_df.index[start_idx], idx, alpha=0.2, color=state_colors.get(current_state, 'gray')) current_state = row['state'] start_idx = i if current_state is not None: ax3.axvspan(portfolio_df.index[start_idx], portfolio_df.index[-1], alpha=0.2, color=state_colors.get(current_state, 'gray')) # Annual returns comparison ax4 = fig.add_subplot(gs[2, :]) years = sorted(strategy_metrics['annual_returns'].keys()) strategy_rets = [strategy_metrics['annual_returns'][y] for y in years] qqq_rets = [qqq_metrics['annual_returns'][y] for y in years] spy_rets = [spy_metrics['annual_returns'][y] for y in years] x = np.arange(len(years)) width = 0.25 bars1 = ax4.bar(x - width, strategy_rets, width, label='Enhanced Strategy', color='#2E86AB', alpha=0.8) bars2 = ax4.bar(x, qqq_rets, width, label='QQQ', color='#A23B72', alpha=0.8) bars3 = ax4.bar(x + width, spy_rets, width, label='SPY', color='#F18F01', alpha=0.8) ax4.set_xlabel('Year', fontsize=12) ax4.set_ylabel('Annual Return (%)', fontsize=12) ax4.set_title('Annual Returns Comparison', fontsize=14, fontweight='bold') ax4.set_xticks(x) ax4.set_xticklabels(years, rotation=45) ax4.legend(fontsize=11) ax4.grid(True, alpha=0.3) ax4.axhline(y=0, color='black', linestyle='-', alpha=0.3) # Add value labels on bars for bars in [bars1, bars2, bars3]: for bar in bars: height = bar.get_height() if abs(height) > 5: # Only label significant returns ax4.annotate(f'{height:.0f}%', xy=(bar.get_x() + bar.get_width() / 2, height), xytext=(0, 3 if height > 0 else -15), textcoords="offset points", ha='center', va='bottom' if height > 0 else 'top', fontsize=9) plt.suptitle('Enhanced Emotional Damage Strategy - Comprehensive Analysis', fontsize=20, fontweight='bold') pdf.savefig(fig, bbox_inches='tight', dpi=300) plt.close() # Page 2: Drawdown Analysis fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12)) fig.suptitle('Risk Analysis', fontsize=16, fontweight='bold') # Calculate drawdowns def calculate_drawdown(returns): peak = returns.expanding().max() drawdown = (returns - peak) / peak return drawdown * 100 strategy_dd = calculate_drawdown(portfolio_df['value']) qqq_dd = calculate_drawdown(benchmark_data['qqq_value']) spy_dd = calculate_drawdown(benchmark_data['spy_value']) # Drawdown comparison ax1.plot(portfolio_df.index, strategy_dd, label='Enhanced Strategy', linewidth=2, color='#2E86AB') ax1.plot(benchmark_data.index, qqq_dd, label='QQQ', alpha=0.7, color='#A23B72') ax1.plot(benchmark_data.index, spy_dd, label='SPY', alpha=0.7, color='#F18F01') ax1.axhline(y=0, color='black', linestyle='-', alpha=0.3) ax1.fill_between(portfolio_df.index, strategy_dd, 0, alpha=0.2, color='#2E86AB') ax1.set_title('Drawdown Comparison Over Time') ax1.set_ylabel('Drawdown (%)') ax1.legend() ax1.grid(True, alpha=0.3) # Drawdown histogram significant_dd = strategy_dd[strategy_dd < -10] # > 10% drawdowns if len(significant_dd) > 0: ax2.hist(significant_dd, bins=20, alpha=0.7, color='#A23B72', edgecolor='black') ax2.axvline(x=significant_dd.min(), color='red', linestyle='--', label=f'Max DD: {significant_dd.min():.1f}%') ax2.set_title('Distribution of Significant Drawdowns') ax2.set_xlabel('Drawdown (%)') ax2.set_ylabel('Frequency') ax2.legend() ax2.grid(True, alpha=0.3) else: ax2.text(0.5, 0.5, 'No significant drawdowns > 10%', ha='center', va='center', transform=ax2.transAxes, fontsize=12) # Rolling 252-day returns window = 252 strategy_rolling = portfolio_df['value'].pct_change(window).rolling(window).mean() * 100 qqq_rolling = benchmark_data['qqq_value'].pct_change(window).rolling(window).mean() * 100 ax3.plot(portfolio_df.index, strategy_rolling, label='Enhanced Strategy', linewidth=2, color='#2E86AB') ax3.plot(benchmark_data.index, qqq_rolling, label='QQQ', alpha=0.7, color='#A23B72') ax3.set_title(f'Rolling {window}-Day Annualized Returns') ax3.set_ylabel('Return (%)') ax3.legend() ax3.grid(True, alpha=0.3) # Risk-adjusted returns scatter strategies = ['Enhanced Strategy', 'QQQ', 'SPY'] returns = [strategy_metrics['annual_return'], qqq_metrics['annual_return'], spy_metrics['annual_return']] risks = [abs(strategy_metrics['max_drawdown']), abs(qqq_metrics['max_drawdown']), abs(spy_metrics['max_drawdown'])] sharpes = [strategy_metrics['sharpe_ratio'], qqq_metrics['sharpe_ratio'], spy_metrics['sharpe_ratio']] colors = ['#2E86AB', '#A23B72', '#F18F01'] for i, (strat, ret, risk, sharpe, color) in enumerate(zip(strategies, returns, risks, sharpes, colors)): ax4.scatter(risk, ret, s=sharpe*100, alpha=0.7, color=color, label=f'{strat} (Sharpe: {sharpe:.2f})') ax4.annotate(strat, (risk, ret), xytext=(5, 5), textcoords='offset points', fontsize=10, fontweight='bold') ax4.set_xlabel('Maximum Drawdown (%)') ax4.set_ylabel('Annual Return (%)') ax4.set_title('Risk vs Return (bubble size = Sharpe Ratio)') ax4.grid(True, alpha=0.3) ax4.legend() plt.tight_layout() pdf.savefig(fig, bbox_inches='tight', dpi=300) plt.close() # Page 3: Trading Activity and Strategy Features fig = plt.figure(figsize=(16, 12)) gs = fig.add_gridspec(3, 2, hspace=0.3, wspace=0.3) fig.suptitle('Trading Activity and Enhanced Features', fontsize=16, fontweight='bold') # Trading frequency trades_df = pd.DataFrame(strategy.trades) if len(trades_df) > 0: trades_df['date'] = pd.to_datetime(trades_df['date']) trades_df['year'] = trades_df['date'].dt.year ax1 = fig.add_subplot(gs[0, 0]) trades_by_year = trades_df.groupby('year').size() ax1.bar(trades_by_year.index, trades_by_year.values, color='#2E86AB', alpha=0.7, edgecolor='black') ax1.set_title('Trading Frequency by Year') ax1.set_xlabel('Year') ax1.set_ylabel('Number of Trades') ax1.grid(True, alpha=0.3) # Trade types ax2 = fig.add_subplot(gs[0, 1]) trade_types = trades_df['action'].value_counts() colors = plt.cm.Set3(np.linspace(0, 1, len(trade_types))) wedges, texts, autotexts = ax2.pie(trade_types.values, labels=trade_types.index, autopct='%1.1f%%', colors=colors) ax2.set_title('Trade Types Distribution') # Recent trades table ax3 = fig.add_subplot(gs[1, :]) ax3.axis('tight') ax3.axis('off') recent_trades = trades_df.tail(15) if len(trades_df) > 15 else trades_df if len(recent_trades) > 0: trade_table_data = [] for _, trade in recent_trades.iterrows(): trade_table_data.append([ trade['date'].strftime('%Y-%m-%d'), trade['action'][:15], trade['ticker'], f"{trade['shares']:.0f}", f"${trade['price']:.2f}", f"${trade['value']:,.0f}" ]) trade_table = ax3.table(cellText=trade_table_data, colLabels=['Date', 'Action', 'Ticker', 'Shares', 'Price', 'Value'], cellLoc='center', loc='center') trade_table.auto_set_font_size(False) trade_table.set_fontsize(9) trade_table.scale(1.2, 1.5) ax3.set_title('Recent 15 Trades', fontsize=14, fontweight='bold', pad=20) # Strategy features ax4 = fig.add_subplot(gs[2, :]) features_text = """ ENHANCED STRATEGY FEATURES: 1. 4-Step Gradual Position Transitions • Reduces market impact and slippage • Provides better entry/exit timing • Smooth transitions between QQQ, cash, and volatile stocks 2. 15% Stop-Loss Protection • Individual stock risk management • Automatic QQQ replacement on stop-loss triggers • Protects against significant losses 3. Technical Indicator Filtering • MACD: Identifies trend reversals and momentum shifts • RSI: Avoids oversold conditions (RSI > 30 filter) • EMA: Uses EMA5/EMA20 crossover for trend confirmation 4. Enhanced Volatility Selection • Combines technical signals with historical volatility • More selective stock picking process • Dynamic selection based on recent market conditions 5. Fear & Greed Based Market Timing • Systematic entry/exit based on CNN Fear & Greed Index • Counter-emotional trading biases • Proven market sentiment indicator PERFORMANCE SUMMARY: """ # Add performance summary to features perf_summary = f""" Backtest Period: {portfolio_df.index.min().strftime('%Y-%m-%d')} to {portfolio_df.index.max().strftime('%Y-%m-%d')} Total Trades: {len(strategy.trades)} Total Return: {strategy_metrics['total_return']:.1f}% Annual Return: {strategy_metrics['annual_return']:.1f}% Max Drawdown: {strategy_metrics['max_drawdown']:.1f}% Sharpe Ratio: {strategy_metrics['sharpe_ratio']:.2f} """ full_text = features_text + perf_summary ax4.text(0.05, 0.95, full_text, transform=ax4.transAxes, fontsize=10, verticalalignment='top', fontfamily='monospace', bbox=dict(boxstyle="round,pad=0.3", facecolor='lightgray', alpha=0.3)) ax4.axis('off') plt.tight_layout() pdf.savefig(fig, bbox_inches='tight', dpi=300) plt.close() print(f"Enhanced PDF report generated: {report_filename}") return report_path if __name__ == "__main__": filename = generate_enhanced_pdf_report() print(f"Report saved as: {filename}")