608 lines
26 KiB
Python
608 lines
26 KiB
Python
import sqlite3
|
|
import pandas as pd
|
|
import numpy as np
|
|
from datetime import datetime, timedelta
|
|
import warnings
|
|
warnings.filterwarnings('ignore')
|
|
|
|
class EnhancedEmotionalDamageStrategy:
|
|
def __init__(self, initial_capital=100000):
|
|
self.initial_capital = initial_capital
|
|
self.cash = initial_capital
|
|
self.positions = {} # ticker: shares
|
|
self.portfolio_value = []
|
|
self.trades = []
|
|
self.fear_threshold = 25
|
|
self.greed_threshold = 75
|
|
self.top_stocks_count = 10
|
|
self.stop_loss_threshold = 0.15 # 15% stop loss
|
|
|
|
# New state management
|
|
self.state = 'QQQ_HOLD'
|
|
self.transition_steps = 4
|
|
self.current_step = 0
|
|
self.target_allocation = {}
|
|
self.last_fear_date = None
|
|
|
|
# For gradual transitions - store transition plan
|
|
self.transition_plan = {}
|
|
self.transition_cash_pool = 0
|
|
|
|
def get_data(self):
|
|
"""Load Fear & Greed Index and stock data"""
|
|
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"Strategy connecting to database at: {db_path}")
|
|
conn = sqlite3.connect(db_path)
|
|
|
|
# Get Fear & Greed Index
|
|
fg_data = pd.read_sql_query('''
|
|
SELECT date, fear_greed_index
|
|
FROM fear_greed_index
|
|
ORDER BY date
|
|
''', conn)
|
|
fg_data['date'] = pd.to_datetime(fg_data['date'])
|
|
fg_data.set_index('date', inplace=True)
|
|
|
|
# Get real QQQ price data
|
|
qqq_data = pd.read_sql_query('''
|
|
SELECT date, close as qqq_close
|
|
FROM qqq
|
|
ORDER BY date
|
|
''', conn)
|
|
qqq_data['date'] = pd.to_datetime(qqq_data['date'])
|
|
qqq_data.set_index('date', inplace=True)
|
|
|
|
# Get available tickers
|
|
cursor = conn.cursor()
|
|
cursor.execute('SELECT ticker FROM ticker_list WHERE records > 1000')
|
|
self.available_tickers = [row[0] for row in cursor.fetchall()]
|
|
|
|
conn.close()
|
|
|
|
# Merge data
|
|
self.data = pd.merge(fg_data, qqq_data, left_index=True, right_index=True, how='inner')
|
|
self.data.sort_index(inplace=True)
|
|
|
|
print(f"Loaded data from {self.data.index.min().strftime('%Y-%m-%d')} to {self.data.index.max().strftime('%Y-%m-%d')}")
|
|
print(f"Available tickers: {len(self.available_tickers)}")
|
|
|
|
def get_stock_price(self, ticker, date):
|
|
"""Get stock price for a specific ticker and date"""
|
|
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')
|
|
conn = sqlite3.connect(db_path)
|
|
|
|
query = f'''
|
|
SELECT close FROM {ticker.lower()}
|
|
WHERE date <= ?
|
|
ORDER BY date DESC
|
|
LIMIT 1
|
|
'''
|
|
|
|
cursor = conn.cursor()
|
|
cursor.execute(query, (date.strftime('%Y-%m-%d'),))
|
|
result = cursor.fetchone()
|
|
conn.close()
|
|
|
|
return result[0] if result else None
|
|
|
|
def calculate_volatility(self, ticker, current_date):
|
|
"""Calculate historical volatility over the past month"""
|
|
import os
|
|
from datetime import timedelta
|
|
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')
|
|
conn = sqlite3.connect(db_path)
|
|
|
|
try:
|
|
# Calculate volatility for the past 30 days
|
|
start_date = current_date - timedelta(days=30)
|
|
|
|
query = f'''
|
|
SELECT date, close FROM {ticker.lower()}
|
|
WHERE date >= ? AND date <= ?
|
|
ORDER BY date
|
|
'''
|
|
|
|
df = pd.read_sql_query(query, conn, params=(
|
|
start_date.strftime('%Y-%m-%d'),
|
|
current_date.strftime('%Y-%m-%d')
|
|
))
|
|
|
|
if len(df) > 10:
|
|
df['returns'] = df['close'].pct_change()
|
|
volatility = df['returns'].std() * np.sqrt(252)
|
|
conn.close()
|
|
return volatility
|
|
|
|
except Exception as e:
|
|
pass
|
|
|
|
conn.close()
|
|
return 0
|
|
|
|
def check_technical_indicators(self, ticker, date):
|
|
"""Check RSI, MACD, and SMA technical indicators"""
|
|
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')
|
|
conn = sqlite3.connect(db_path)
|
|
|
|
try:
|
|
# Get 50 days of data for technical analysis
|
|
query = f'''
|
|
SELECT date, close FROM {ticker.lower()}
|
|
WHERE date <= ?
|
|
ORDER BY date DESC
|
|
LIMIT 50
|
|
'''
|
|
|
|
df = pd.read_sql_query(query, conn, params=(date.strftime('%Y-%m-%d'),))
|
|
|
|
if len(df) < 20:
|
|
conn.close()
|
|
return False
|
|
|
|
df = df.sort_values('date')
|
|
df.reset_index(drop=True, inplace=True)
|
|
|
|
# Calculate RSI
|
|
delta = df['close'].diff()
|
|
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
|
|
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
|
|
rs = gain / loss
|
|
rsi = 100 - (100 / (1 + rs))
|
|
|
|
# Calculate MACD
|
|
ema12 = df['close'].ewm(span=12).mean()
|
|
ema26 = df['close'].ewm(span=26).mean()
|
|
macd = ema12 - ema26
|
|
signal = macd.ewm(span=9).mean()
|
|
|
|
# Calculate SMA
|
|
sma5 = df['close'].rolling(window=5).mean()
|
|
sma20 = df['close'].rolling(window=20).mean()
|
|
|
|
# Check conditions (use latest values)
|
|
latest_rsi = rsi.iloc[-1]
|
|
latest_macd = macd.iloc[-1]
|
|
latest_signal = signal.iloc[-1]
|
|
latest_sma5 = sma5.iloc[-1]
|
|
latest_sma20 = sma20.iloc[-1]
|
|
|
|
# RSI should be above 30 (not oversold)
|
|
rsi_ok = latest_rsi > 30
|
|
|
|
# MACD convergence: MACD and Signal lines are converging
|
|
# Check if MACD is getting closer to signal line (momentum improving)
|
|
if len(macd) >= 2 and len(signal) >= 2:
|
|
prev_macd = macd.iloc[-2]
|
|
prev_signal = signal.iloc[-2]
|
|
prev_diff = abs(prev_macd - prev_signal)
|
|
current_diff = abs(latest_macd - latest_signal)
|
|
macd_ok = current_diff < prev_diff # Lines are converging
|
|
else:
|
|
macd_ok = latest_macd > latest_signal # Fallback to original condition
|
|
|
|
# SMA5 should be above SMA20 (uptrend)
|
|
sma_ok = latest_sma5 > latest_sma20
|
|
|
|
# Need at least 2 out of 3 indicators to be positive
|
|
score = sum([rsi_ok, macd_ok, sma_ok])
|
|
|
|
conn.close()
|
|
return score >= 2
|
|
|
|
except Exception as e:
|
|
conn.close()
|
|
return False
|
|
|
|
def select_volatile_stocks(self, fear_start_date, fear_end_date):
|
|
"""Select stocks using technical indicators, then sort by volatility"""
|
|
qualified_stocks = []
|
|
|
|
# Filter stocks using technical indicators
|
|
for ticker in self.available_tickers:
|
|
if self.check_technical_indicators(ticker, fear_end_date):
|
|
vol = self.calculate_volatility(ticker, fear_end_date) # Calculate past month volatility
|
|
|
|
if vol > 0.1: # Normal volatility threshold
|
|
qualified_stocks.append((ticker, vol))
|
|
|
|
# Sort by volatility and select top 4
|
|
qualified_stocks.sort(key=lambda x: x[1], reverse=True)
|
|
top_stocks = [ticker for ticker, vol in qualified_stocks[:4]]
|
|
|
|
return top_stocks
|
|
|
|
def execute_trade(self, date, action, ticker=None, shares=None, price=None, value=None):
|
|
"""Execute and record a trade"""
|
|
# Note: In enhanced strategy, actual execution is handled by gradual_transition
|
|
# This method is only for recording trades
|
|
|
|
# Get F&G index for this date
|
|
fg_index = self.data.loc[date, 'fear_greed_index'] if date in self.data.index else None
|
|
|
|
self.trades.append({
|
|
'date': date,
|
|
'action': action,
|
|
'ticker': ticker,
|
|
'shares': shares,
|
|
'price': price,
|
|
'value': value,
|
|
'fg_index': fg_index,
|
|
'cash_after': self.cash,
|
|
'portfolio_state': self.state
|
|
})
|
|
|
|
def calculate_portfolio_value(self, date):
|
|
"""Calculate total portfolio value"""
|
|
total_value = self.cash
|
|
|
|
for ticker, shares in self.positions.items():
|
|
if ticker == 'QQQ':
|
|
price = self.data.loc[date, 'qqq_close']
|
|
else:
|
|
price = self.get_stock_price(ticker, date)
|
|
|
|
if price:
|
|
total_value += shares * price
|
|
|
|
return total_value
|
|
|
|
def check_stop_loss(self, date):
|
|
"""Check 15% stop loss"""
|
|
for ticker in list(self.positions.keys()):
|
|
if ticker == 'QQQ':
|
|
continue
|
|
|
|
current_price = self.get_stock_price(ticker, date)
|
|
if not current_price:
|
|
continue
|
|
|
|
# Find average buy price
|
|
buy_trades = [t for t in self.trades
|
|
if t['ticker'] == ticker and t['action'] in ['BUY_GRADUAL']]
|
|
if buy_trades:
|
|
total_cost = sum(t['price'] * t['shares'] for t in buy_trades)
|
|
total_shares = sum(t['shares'] for t in buy_trades)
|
|
avg_price = total_cost / total_shares
|
|
|
|
loss_pct = (current_price - avg_price) / avg_price
|
|
if loss_pct <= -self.stop_loss_threshold:
|
|
# Sell and buy QQQ
|
|
shares = self.positions[ticker]
|
|
value = shares * current_price
|
|
self.cash += value
|
|
del self.positions[ticker]
|
|
|
|
self.execute_trade(date, 'STOP_LOSS', ticker, shares, current_price, value)
|
|
|
|
# Buy QQQ with integer shares
|
|
qqq_price = self.data.loc[date, 'qqq_close']
|
|
qqq_shares = int(value / qqq_price)
|
|
|
|
if qqq_shares > 0:
|
|
actual_qqq_value = qqq_shares * qqq_price
|
|
self.positions['QQQ'] = self.positions.get('QQQ', 0) + qqq_shares
|
|
self.cash -= actual_qqq_value # Subtract the actual QQQ purchase value
|
|
self.execute_trade(date, 'BUY_QQQ_STOPLOSS', 'QQQ', qqq_shares, qqq_price, actual_qqq_value)
|
|
|
|
print(f"{date.strftime('%Y-%m-%d')}: Stop loss triggered for {ticker}, loss: {loss_pct*100:.1f}%")
|
|
|
|
def start_transition(self, date, target_type, stocks=None):
|
|
"""Initialize transition plan to avoid compounding errors"""
|
|
self.transition_plan = {'type': target_type, 'stocks': stocks}
|
|
|
|
if target_type == 'CASH':
|
|
# Plan to sell ALL positions (including QQQ) over 4 steps
|
|
self.transition_plan['positions_to_sell'] = {}
|
|
for ticker in self.positions:
|
|
self.transition_plan['positions_to_sell'][ticker] = self.positions[ticker]
|
|
|
|
elif target_type == 'QQQ':
|
|
# Plan to sell all non-QQQ positions and convert to cash pool
|
|
self.transition_cash_pool = 0
|
|
cash_from_positions = 0
|
|
|
|
for ticker in self.positions:
|
|
if ticker != 'QQQ':
|
|
price = self.get_stock_price(ticker, date)
|
|
if price:
|
|
cash_from_positions += self.positions[ticker] * price
|
|
|
|
self.transition_cash_pool = self.cash + cash_from_positions
|
|
self.transition_plan['total_cash_to_invest'] = self.transition_cash_pool
|
|
self.transition_plan['positions_to_sell'] = {}
|
|
for ticker in self.positions:
|
|
if ticker != 'QQQ':
|
|
self.transition_plan['positions_to_sell'][ticker] = self.positions[ticker]
|
|
|
|
elif target_type == 'VOLATILE' and stocks:
|
|
# Plan to invest available cash in volatile stocks
|
|
# Include cash from any remaining positions (should be mostly cash by now)
|
|
cash_from_positions = 0
|
|
for ticker in self.positions:
|
|
if ticker != 'QQQ':
|
|
price = self.get_stock_price(ticker, date)
|
|
if price:
|
|
cash_from_positions += self.positions[ticker] * price
|
|
|
|
total_available_cash = self.cash + cash_from_positions
|
|
self.transition_plan['total_cash_to_invest'] = total_available_cash
|
|
|
|
def gradual_transition(self, date, target_type, stocks=None):
|
|
"""Handle 4-step gradual transitions with fixed allocation"""
|
|
step_size = 1.0 / self.transition_steps
|
|
|
|
if target_type == 'CASH':
|
|
# Sell positions gradually based on initial plan
|
|
for ticker in list(self.transition_plan.get('positions_to_sell', {})):
|
|
if ticker in self.positions:
|
|
total_shares_to_sell = self.transition_plan['positions_to_sell'][ticker]
|
|
shares_to_sell = int(total_shares_to_sell * step_size)
|
|
|
|
if shares_to_sell > 0 and shares_to_sell <= self.positions[ticker]:
|
|
price = self.get_stock_price(ticker, date)
|
|
if price:
|
|
value = shares_to_sell * price
|
|
self.cash += value
|
|
self.positions[ticker] -= shares_to_sell
|
|
if self.positions[ticker] <= 0:
|
|
del self.positions[ticker]
|
|
self.execute_trade(date, 'SELL_GRADUAL', ticker, shares_to_sell, price, value)
|
|
|
|
elif target_type == 'VOLATILE' and stocks:
|
|
# Buy only the most volatile stock each step (up to 4 different stocks)
|
|
total_cash = self.transition_plan.get('total_cash_to_invest', 0)
|
|
cash_this_step = total_cash * step_size
|
|
|
|
if cash_this_step > 0 and self.cash >= cash_this_step:
|
|
# Buy only the most volatile stock this step
|
|
current_step_index = min(self.current_step, len(stocks) - 1)
|
|
ticker = stocks[current_step_index] # stocks are already sorted by volatility
|
|
|
|
price = self.get_stock_price(ticker, date)
|
|
if price and cash_this_step > 0:
|
|
shares = int(cash_this_step / price) # Integer shares only
|
|
if shares > 0:
|
|
actual_value = shares * price
|
|
self.positions[ticker] = self.positions.get(ticker, 0) + shares
|
|
self.cash -= actual_value
|
|
self.execute_trade(date, 'BUY_GRADUAL', ticker, shares, price, actual_value)
|
|
|
|
elif target_type == 'QQQ':
|
|
# Sell positions gradually and buy QQQ with fixed allocation
|
|
# First sell positions
|
|
for ticker in list(self.transition_plan.get('positions_to_sell', {})):
|
|
if ticker in self.positions:
|
|
total_shares_to_sell = self.transition_plan['positions_to_sell'][ticker]
|
|
shares_to_sell = int(total_shares_to_sell * step_size)
|
|
|
|
if shares_to_sell > 0 and shares_to_sell <= self.positions[ticker]:
|
|
price = self.get_stock_price(ticker, date)
|
|
if price:
|
|
value = shares_to_sell * price
|
|
self.cash += value
|
|
self.positions[ticker] -= shares_to_sell
|
|
if self.positions[ticker] <= 0:
|
|
del self.positions[ticker]
|
|
self.execute_trade(date, 'SELL_GRADUAL', ticker, shares_to_sell, price, value)
|
|
|
|
# Then buy QQQ with step portion of planned cash
|
|
total_cash = self.transition_plan.get('total_cash_to_invest', 0)
|
|
cash_this_step = total_cash * step_size
|
|
|
|
if cash_this_step > 0 and self.cash >= cash_this_step:
|
|
qqq_price = self.data.loc[date, 'qqq_close']
|
|
qqq_shares = int(cash_this_step / qqq_price) # Integer shares only
|
|
|
|
if qqq_shares > 0:
|
|
actual_value = qqq_shares * qqq_price
|
|
self.positions['QQQ'] = self.positions.get('QQQ', 0) + qqq_shares
|
|
self.cash -= actual_value
|
|
self.execute_trade(date, 'BUY_GRADUAL', 'QQQ', qqq_shares, qqq_price, actual_value)
|
|
|
|
def run_backtest(self):
|
|
"""Run the enhanced strategy backtest"""
|
|
print("Running Enhanced Emotional Damage Strategy...")
|
|
|
|
self.get_data()
|
|
|
|
# Start with 100% QQQ
|
|
first_date = self.data.index[0]
|
|
qqq_price = self.data.loc[first_date, 'qqq_close']
|
|
qqq_shares = int(self.initial_capital / qqq_price) # Integer shares only
|
|
self.positions['QQQ'] = qqq_shares
|
|
self.cash = self.initial_capital - (qqq_shares * qqq_price) # Remaining cash after buying integer shares
|
|
|
|
fear_start_date = None
|
|
|
|
for date, row in self.data.iterrows():
|
|
fg_index = row['fear_greed_index']
|
|
|
|
# Check stop loss
|
|
self.check_stop_loss(date)
|
|
|
|
if self.state == 'QQQ_HOLD':
|
|
# Check for fear threshold
|
|
if fg_index < self.fear_threshold:
|
|
fear_start_date = date
|
|
self.state = 'FEAR_TRANSITION'
|
|
self.current_step = 0
|
|
self.start_transition(date, 'CASH')
|
|
print(f"{date.strftime('%Y-%m-%d')}: Fear threshold hit ({fg_index:.1f}), starting transition to cash")
|
|
|
|
elif self.state == 'FEAR_TRANSITION':
|
|
# Gradual transition to cash
|
|
self.gradual_transition(date, 'CASH')
|
|
self.current_step += 1
|
|
|
|
if self.current_step >= self.transition_steps:
|
|
self.state = 'CASH_WAIT'
|
|
print(f"{date.strftime('%Y-%m-%d')}: Transition to cash complete")
|
|
|
|
elif self.state == 'CASH_WAIT':
|
|
# Wait for recovery, then select volatile stocks
|
|
if fg_index >= self.fear_threshold and fear_start_date:
|
|
# Select top volatile stocks
|
|
top_stocks = self.select_volatile_stocks(fear_start_date, date)
|
|
|
|
if top_stocks:
|
|
self.state = 'GREED_TRANSITION'
|
|
self.current_step = 0
|
|
self.transition_stocks = top_stocks
|
|
self.start_transition(date, 'VOLATILE', top_stocks)
|
|
print(f"{date.strftime('%Y-%m-%d')}: Fear recovered, starting transition to volatile stocks: {top_stocks}")
|
|
else:
|
|
# No suitable stocks, go back to QQQ
|
|
self.state = 'QQQ_TRANSITION'
|
|
self.current_step = 0
|
|
self.start_transition(date, 'QQQ')
|
|
print(f"{date.strftime('%Y-%m-%d')}: Fear recovered, no suitable stocks, returning to QQQ")
|
|
|
|
elif self.state == 'GREED_TRANSITION':
|
|
# Gradual transition to volatile stocks
|
|
self.gradual_transition(date, 'VOLATILE', self.transition_stocks)
|
|
self.current_step += 1
|
|
|
|
if self.current_step >= self.transition_steps:
|
|
self.state = 'VOLATILE_STOCKS'
|
|
print(f"{date.strftime('%Y-%m-%d')}: Transition to volatile stocks complete")
|
|
|
|
elif self.state == 'VOLATILE_STOCKS':
|
|
# Check for greed threshold
|
|
if fg_index > self.greed_threshold:
|
|
self.state = 'QQQ_TRANSITION'
|
|
self.current_step = 0
|
|
self.start_transition(date, 'QQQ')
|
|
print(f"{date.strftime('%Y-%m-%d')}: Greed threshold hit ({fg_index:.1f}), starting transition to QQQ")
|
|
|
|
elif self.state == 'QQQ_TRANSITION':
|
|
# Gradual transition back to QQQ
|
|
self.gradual_transition(date, 'QQQ')
|
|
self.current_step += 1
|
|
|
|
if self.current_step >= self.transition_steps:
|
|
self.state = 'QQQ_HOLD'
|
|
print(f"{date.strftime('%Y-%m-%d')}: Transition to QQQ complete")
|
|
|
|
# Record portfolio value
|
|
portfolio_value = self.calculate_portfolio_value(date)
|
|
self.portfolio_value.append({
|
|
'date': date,
|
|
'value': portfolio_value,
|
|
'state': self.state,
|
|
'fg_index': fg_index
|
|
})
|
|
|
|
print(f"Backtest completed! Total trades: {len(self.trades)}")
|
|
|
|
def calculate_performance_metrics(self, 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()
|
|
|
|
# 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,
|
|
'sharpe_ratio': sharpe_ratio,
|
|
'annual_returns': annual_rets
|
|
}
|
|
|
|
def run_enhanced_backtest():
|
|
"""Run the enhanced strategy"""
|
|
|
|
strategy = EnhancedEmotionalDamageStrategy(initial_capital=100000)
|
|
strategy.run_backtest()
|
|
|
|
# Convert results
|
|
portfolio_df = pd.DataFrame(strategy.portfolio_value)
|
|
portfolio_df.set_index('date', inplace=True)
|
|
|
|
# Get benchmark data (both 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')
|
|
conn = sqlite3.connect(db_path)
|
|
|
|
# Get QQQ data
|
|
qqq_data = pd.read_sql_query('''
|
|
SELECT date, close as qqq_close
|
|
FROM qqq
|
|
ORDER BY date
|
|
''', conn)
|
|
qqq_data['date'] = pd.to_datetime(qqq_data['date'])
|
|
qqq_data.set_index('date', inplace=True)
|
|
|
|
# Get SPY data
|
|
spy_data = pd.read_sql_query('''
|
|
SELECT date, spy_close
|
|
FROM fear_greed_data
|
|
ORDER BY date
|
|
''', conn)
|
|
spy_data['date'] = pd.to_datetime(spy_data['date'])
|
|
spy_data.set_index('date', inplace=True)
|
|
|
|
conn.close()
|
|
|
|
# Merge benchmark data
|
|
benchmark_data = pd.merge(qqq_data, spy_data, left_index=True, right_index=True, how='inner')
|
|
|
|
# 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 benchmarks
|
|
start_value = 100000
|
|
benchmark_data['qqq_value'] = start_value * (benchmark_data['qqq_close'] / benchmark_data['qqq_close'].iloc[0])
|
|
benchmark_data['spy_value'] = start_value * (benchmark_data['spy_close'] / benchmark_data['spy_close'].iloc[0])
|
|
|
|
# Calculate metrics
|
|
strategy_metrics = strategy.calculate_performance_metrics(portfolio_df['value'])
|
|
qqq_metrics = strategy.calculate_performance_metrics(benchmark_data['qqq_value'])
|
|
spy_metrics = strategy.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
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
results = run_enhanced_backtest()
|
|
print("Enhanced backtest completed!") |