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}")