313 lines
12 KiB
Python
Executable File
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() |