| Overall Statistics |
|
Total Orders 20 Average Win 86.13% Average Loss -18.80% Compounding Annual Return 31.972% Drawdown 58.200% Expectancy 3.342 Start Equity 100000 End Equity 4232086.27 Net Profit 4132.086% Sharpe Ratio 0.782 Sortino Ratio 0.779 Probabilistic Sharpe Ratio 12.020% Loss Rate 22% Win Rate 78% Profit-Loss Ratio 4.58 Alpha 0.116 Beta 1.78 Annual Standard Deviation 0.344 Annual Variance 0.118 Information Ratio 0.708 Tracking Error 0.258 Treynor Ratio 0.151 Total Fees $3466.77 Estimated Strategy Capacity $320000000.00 Lowest Capacity Asset TQQQ UK280CGTCB51 Portfolio Turnover 0.23% |
from AlgorithmImports import *
class CombinedSPYandTQQQAlgorithm(QCAlgorithm):
def Initialize(self):
# Set start and end date for the algorithm
self.SetStartDate(2011, 1, 1)
self.SetEndDate(2024, 6, 30)
self.SetCash(100000)
# Add equity data for SPY (Dynamic strategy) and TQQQ (Buy and hold with stop-loss)
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.tqqq = self.AddEquity("TQQQ", Resolution.Daily).Symbol
# Initialize parameters using QuantConnect's parameter feature
self.allocation_spy = float(self.GetParameter("allocation_spy", 0.3)) # Default to 50% allocation to SPY
self.allocation_tqqq = 1.0 - self.allocation_spy # The rest goes to TQQQ
self.tqqq_drawdown_threshold = float(self.GetParameter("tqqq_drawdown_threshold", 0.45)) # Default to 45% drawdown threshold for TQQQ
self.spy_stop_loss_pct = 0.15 # Default to 15% stop-loss for SPY
self.spy_trailing_stop_loss_pct = 0.15 # Default to 15% trailing stop-loss for SPY
self.spy_long_ma_period = 6 # Default to 6 months for SPY long MA period
self.spy_capital_preservation_mult = 3.0 # Multiplier for buy threshold in capital preservation mode
self.spy_cap_pres_sell_mult = 2.0 # Multiplier for sell threshold in capital preservation mode
# Initialize indicators for SPY (Dynamic strategy)
self.atr = self.ATR(self.spy, 14, MovingAverageType.Simple, Resolution.Daily)
self.rsi = self.RSI(self.spy, 14, MovingAverageType.Simple, Resolution.Daily)
# Initialize drawdown-based stop-loss variables for TQQQ
self.tqqq_entry_price = None
self.tqqq_peak_price = None
self.tqqq_invested = False
self.tqqq_entries = 0 # To track the total number of TQQQ entries
# Moving averages for a slower long-term trend signal for TQQQ
self.tqqq_fast_ma = self.SMA(self.tqqq, 100, Resolution.Daily)
self.tqqq_slow_ma = self.SMA(self.tqqq, 300, Resolution.Daily)
self.reentry_buffer = None # Buffer period before reentering TQQQ
# Monthly data containers for SPY
self.monthly_prices_spy = []
self.monthly_ohlc_spy = []
self.current_month = None
# Number of months to use for moving average (SPY)
self.short_ma_period = 1 # equivalent to 1 month
self.long_ma_period = self.spy_long_ma_period # equivalent to long_ma_period months
# To store the calculated monthly moving averages for SPY
self.short_ma_values = []
self.long_ma_values = []
# Set warm-up period for SPY (at least 3 months of data to calculate the first long MA)
self.SetWarmUp(300) # Ensure enough data for moving averages
# Risk management variables for SPY
self.entry_price_spy = None # To store the entry price for SPY
self.trailing_stop_price_spy = None # To store the trailing stop price for SPY
self.highest_portfolio_value = self.Portfolio.TotalPortfolioValue # Track highest portfolio value for capital preservation mode
self.capital_preservation_mode = False
# Track the number of market entries for SPY
self.market_entries_spy = 0
def OnWarmupFinished(self):
# Buy and hold TQQQ immediately after warm-up based on allocation
if not self.tqqq_invested and self.Securities[self.tqqq].HasData:
self.SetHoldings(self.tqqq, self.allocation_tqqq)
self.tqqq_entry_price = self.Securities[self.tqqq].Price
self.tqqq_peak_price = self.tqqq_entry_price # Initialize peak price
self.tqqq_invested = True
self.tqqq_entries += 1
self.Debug(f"TQQQ Initial Buy - Holding {self.allocation_tqqq*100}% of Portfolio in TQQQ on {self.Time}")
def OnData(self, data):
if self.IsWarmingUp or not data.ContainsKey(self.spy) or not data.ContainsKey(self.tqqq):
return
current_tqqq_price = data[self.tqqq].Price
# Exit Condition: Check if TQQQ should be sold based on drawdown
exit_condition = self.tqqq_invested and current_tqqq_price < self.tqqq_peak_price * (1 - self.tqqq_drawdown_threshold)
# Reentry Condition: Only check if not invested and after buffer period
reentry_condition = not self.tqqq_invested and self.Time > (self.reentry_buffer or self.Time) and self.tqqq_fast_ma.Current.Value > self.tqqq_slow_ma.Current.Value
if exit_condition and not reentry_condition:
self.SetHoldings(self.tqqq, 0)
self.tqqq_invested = False
self.reentry_buffer = self.Time + timedelta(days=30) # Example: Add a 30-day buffer before reentry
self.Debug(f"TQQQ Drawdown Stop-Loss Triggered - Exiting Market on {self.Time} | Peak Price: {self.tqqq_peak_price}, Current Price: {current_tqqq_price}")
elif reentry_condition and not exit_condition:
self.SetHoldings(self.tqqq, self.allocation_tqqq)
self.tqqq_entry_price = current_tqqq_price
self.tqqq_peak_price = current_tqqq_price
self.tqqq_invested = True
self.tqqq_entries += 1
self.Debug(f"TQQQ Reentry - Reentering Market on {self.Time} with Slower Long-Term Uptrend | Fast MA: {self.tqqq_fast_ma.Current.Value}, Slow MA: {self.tqqq_slow_ma.Current.Value}")
# Update peak price during investment
if self.tqqq_invested:
self.tqqq_peak_price = max(self.tqqq_peak_price, current_tqqq_price)
# SPY-related logic (unchanged)
month = pd.Timestamp(self.Time).month
year = self.Time.year
if self.current_month is None:
self.current_month = (year, month)
if (year, month) != self.current_month:
if len(self.monthly_prices_spy) > 0:
open_price = self.monthly_prices_spy[0]
high_price = max(self.monthly_prices_spy)
low_price = min(self.monthly_prices_spy)
close_price = self.monthly_prices_spy[-1]
self.monthly_ohlc_spy.append((open_price, high_price, low_price, close_price))
self.CalculateMonthlyMovingAveragesSPY(close_price)
self.current_month = (year, month)
self.monthly_prices_spy = []
if data[self.spy] is not None and data[self.spy].Close is not None:
self.monthly_prices_spy.append(data[self.spy].Close)
if self.Portfolio[self.spy].Invested and self.entry_price_spy is not None:
current_price_spy = data[self.spy].Close
if current_price_spy < self.entry_price_spy * (1 - self.spy_stop_loss_pct):
self.SetHoldings(self.spy, 0)
self.Debug(f"SPY Stop-Loss Triggered - Exiting Market on {self.Time} | Entry Price: {self.entry_price_spy}, Current Price: {current_price_spy}")
self.entry_price_spy = None
self.trailing_stop_price_spy = None
return
if self.trailing_stop_price_spy is not None and current_price_spy < self.trailing_stop_price_spy:
self.SetHoldings(self.spy, 0)
self.Debug(f"SPY Trailing Stop-Loss Triggered - Exiting Market on {self.Time} | Trailing Stop Price: {self.trailing_stop_price_spy}, Current Price: {current_price_spy}")
self.entry_price_spy = None
self.trailing_stop_price_spy = None
return
self.trailing_stop_price_spy = max(self.trailing_stop_price_spy, current_price_spy * (1 - self.spy_trailing_stop_loss_pct))
def CalculateMonthlyMovingAveragesSPY(self, close_price):
self.short_ma_values.append(close_price)
self.long_ma_values.append(close_price)
if len(self.short_ma_values) >= self.short_ma_period:
short_ma = sum(self.short_ma_values[-self.short_ma_period:]) / self.short_ma_period
else:
short_ma = None
if len(self.long_ma_values) >= self.long_ma_period:
long_ma = sum(self.long_ma_values[-self.long_ma_period:]) / self.long_ma_period
else:
long_ma = None
if short_ma and long_ma:
volatility_factor = self.atr.Current.Value / close_price
momentum_factor = (50 - abs(self.rsi.Current.Value - 50)) / 50
buy_threshold = 0.02 * (1 + volatility_factor + momentum_factor)
sell_threshold = 0.05 * (1 + volatility_factor + momentum_factor)
if self.capital_preservation_mode:
buy_threshold *= self.spy_capital_preservation_mult
sell_threshold *= self.spy_cap_pres_sell_mult
ma_difference = (short_ma - long_ma) / long_ma
if ma_difference > buy_threshold and not self.Portfolio[self.spy].Invested:
self.SetHoldings(self.spy, self.allocation_spy)
self.entry_price_spy = close_price
self.trailing_stop_price_spy = close_price * (1 - self.spy_trailing_stop_loss_pct)
self.market_entries_spy += 1
self.Debug(f"SPY Buy Signal - Entering Market on {self.Time} | Short MA: {short_ma}, Long MA: {long_ma}, Difference: {ma_difference:.2%}, Buy Threshold: {buy_threshold:.2%}")
elif ma_difference < -sell_threshold and self.Portfolio[self.spy].Invested:
self.SetHoldings(self.spy, 0)
self.entry_price_spy = None
self.trailing_stop_price_spy = None
self.Debug(f"SPY Sell Signal - Exiting Market on {self.Time} | Short MA: {short_ma}, Long MA: {long_ma}, Difference: {ma_difference:.2%}, Sell Threshold: {sell_threshold:.2%}")
else:
self.Debug(f"SPY: Not enough data to calculate moving averages on {self.Time}")
if len(self.short_ma_values) > self.long_ma_period:
self.short_ma_values = self.short_ma_values[-self.long_ma_period:]
if len(self.long_ma_values) > self.long_ma_period:
self.long_ma_values = self.long_ma_values[-self.long_ma_period:]
if self.Portfolio.TotalPortfolioValue < self.highest_portfolio_value * (1 - 0.25): # Using the hardcoded drawdown threshold
self.capital_preservation_mode = True
self.Debug(f"Capital Preservation Mode Activated on {self.Time}")
else:
self.capital_preservation_mode = False
def OnEndOfAlgorithm(self):
self.Debug(f"End of algorithm - Total Market Entries in SPY: {self.market_entries_spy}")
self.Debug(f"End of algorithm - Total Market Entries in TQQQ: {self.tqqq_entries}")
# Report total profits for SPY and TQQQ
spy_profit = self.Portfolio[self.spy].UnrealizedProfit
tqqq_profit = self.Portfolio[self.tqqq].UnrealizedProfit
self.Debug(f"Total Profit from SPY: ${spy_profit:.2f}")
self.Debug(f"Total Profit from TQQQ: ${tqqq_profit:.2f}")