Files
docker-configs/backtest/strategy/emotional-damage/generate_enhanced_pdf_report.py
2025-07-18 00:00:01 -05:00

440 lines
19 KiB
Python

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}")