- Added HOURLY_ATR_STOP_MULTIPLIER (1.8x) vs daily (3.5x) - Added hourly-specific trail multipliers - Strategy now uses timeframe field to select appropriate stops - Tested multiple configurations on hourly: * 3.5x stops: -0.5% return, 45% max DD * 1.8x stops: -45% return, 53% max DD (worse) * Conservative regime (0.25x): -65% return, 67% max DD (terrible) - Conclusion: Hourly doesn't work with this strategy - Daily with relaxed regime remains best: +17.4% over 5yr, 24% max DD Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
120 lines
4.3 KiB
Python
120 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Analyze SPY regime detection during backtest periods."""
|
|
|
|
import pandas as pd
|
|
import yfinance as yf
|
|
from datetime import datetime, timedelta
|
|
|
|
def calculate_ema(series, period):
|
|
"""Calculate EMA using pandas."""
|
|
return series.ewm(span=period, adjust=False).mean()
|
|
|
|
def determine_regime(price, ema50, ema200):
|
|
"""Replicate the Rust regime detection logic."""
|
|
if pd.isna(price) or pd.isna(ema50) or pd.isna(ema200):
|
|
return "Caution"
|
|
|
|
# Bear: price below 200 EMA AND 50 EMA below 200 EMA
|
|
if price < ema200 and ema50 < ema200:
|
|
return "Bear"
|
|
|
|
# Caution: price below 50 EMA
|
|
if price < ema50:
|
|
return "Caution"
|
|
|
|
# Bull: price above both, 50 above 200
|
|
if ema50 > ema200:
|
|
return "Bull"
|
|
|
|
# Edge case: price above both but 50 still below 200
|
|
return "Caution"
|
|
|
|
def analyze_period(start_date, end_date, period_name):
|
|
"""Analyze SPY regime for a given period."""
|
|
print(f"\n{'='*70}")
|
|
print(f"{period_name}: {start_date} to {end_date}")
|
|
print('='*70)
|
|
|
|
# Fetch SPY data with extra warmup for EMA-200
|
|
warmup_start = (datetime.strptime(start_date, '%Y-%m-%d') - timedelta(days=400)).strftime('%Y-%m-%d')
|
|
spy = yf.download('SPY', start=warmup_start, end=end_date, progress=False)
|
|
|
|
if spy.empty:
|
|
print(f"ERROR: No SPY data available for {period_name}")
|
|
return
|
|
|
|
# Calculate EMAs
|
|
spy['EMA50'] = calculate_ema(spy['Close'], 50)
|
|
spy['EMA200'] = calculate_ema(spy['Close'], 200)
|
|
|
|
# Determine regime for each day
|
|
spy['Regime'] = spy.apply(
|
|
lambda row: determine_regime(row['Close'], row['EMA50'], row['EMA200']),
|
|
axis=1
|
|
)
|
|
|
|
# Filter to actual trading period
|
|
trading_period = spy[start_date:end_date].copy()
|
|
|
|
if trading_period.empty:
|
|
print(f"ERROR: No trading data for {period_name}")
|
|
return
|
|
|
|
# Calculate SPY return
|
|
spy_start = trading_period['Close'].iloc[0]
|
|
spy_end = trading_period['Close'].iloc[-1]
|
|
spy_return = (spy_end - spy_start) / spy_start * 100
|
|
|
|
print(f"\nSPY Performance:")
|
|
print(f" Start: ${spy_start:.2f}")
|
|
print(f" End: ${spy_end:.2f}")
|
|
print(f" Return: {spy_return:+.2f}%")
|
|
|
|
# Count regime days
|
|
regime_counts = trading_period['Regime'].value_counts()
|
|
total_days = len(trading_period)
|
|
|
|
print(f"\nRegime Distribution ({total_days} trading days):")
|
|
for regime in ['Bull', 'Caution', 'Bear']:
|
|
count = regime_counts.get(regime, 0)
|
|
pct = count / total_days * 100
|
|
print(f" {regime:8s}: {count:4d} days ({pct:5.1f}%)")
|
|
|
|
# Show regime transitions
|
|
regime_changes = trading_period[trading_period['Regime'] != trading_period['Regime'].shift(1)]
|
|
if len(regime_changes) > 0:
|
|
print(f"\nRegime Transitions ({len(regime_changes)} total):")
|
|
for date, row in regime_changes.head(20).iterrows():
|
|
print(f" {date.strftime('%Y-%m-%d')}: {row['Regime']:8s} (SPY: ${row['Close']:.2f}, "
|
|
f"EMA50: ${row['EMA50']:.2f}, EMA200: ${row['EMA200']:.2f})")
|
|
if len(regime_changes) > 20:
|
|
print(f" ... and {len(regime_changes) - 20} more transitions")
|
|
|
|
# Identify problematic Bear periods during bull markets
|
|
bear_days = trading_period[trading_period['Regime'] == 'Bear']
|
|
if len(bear_days) > 0:
|
|
print(f"\n⚠️ WARNING: {len(bear_days)} days classified as BEAR:")
|
|
for date, row in bear_days.head(10).iterrows():
|
|
print(f" {date.strftime('%Y-%m-%d')}: SPY=${row['Close']:.2f}, "
|
|
f"EMA50=${row['EMA50']:.2f}, EMA200=${row['EMA200']:.2f}")
|
|
if len(bear_days) > 10:
|
|
print(f" ... and {len(bear_days) - 10} more Bear days")
|
|
|
|
# Show first and last months in detail
|
|
print(f"\nFirst Month Detail:")
|
|
first_month = trading_period.head(22)[['Close', 'EMA50', 'EMA200', 'Regime']]
|
|
for date, row in first_month.iterrows():
|
|
print(f" {date.strftime('%Y-%m-%d')}: {row['Regime']:8s} | "
|
|
f"SPY: ${row['Close']:7.2f} | EMA50: ${row['EMA50']:7.2f} | EMA200: ${row['EMA200']:7.2f}")
|
|
|
|
if __name__ == '__main__':
|
|
# Analyze 2023
|
|
analyze_period('2023-01-01', '2023-12-31', '2023 Backtest')
|
|
|
|
# Analyze 2024
|
|
analyze_period('2024-01-01', '2024-12-31', '2024 Backtest')
|
|
|
|
print(f"\n{'='*70}")
|
|
print("Analysis complete.")
|
|
print('='*70)
|