440 lines
19 KiB
Python
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}") |