Overall Statistics |
Total Trades 20 Average Win 1.37% Average Loss -1.90% Compounding Annual Return 2.819% Drawdown 2.800% Expectancy 0.031 Net Profit 0.450% Sharpe Ratio 0.237 Probabilistic Sharpe Ratio 35.839% Loss Rate 40% Win Rate 60% Profit-Loss Ratio 0.72 Alpha 0.032 Beta 0.186 Annual Standard Deviation 0.108 Annual Variance 0.012 Information Ratio 0.396 Tracking Error 0.158 Treynor Ratio 0.138 Total Fees $20.00 Estimated Strategy Capacity $26000.00 Lowest Capacity Asset QQQ Y5MGN9M0F5K6|QQQ RIWIV7K5Z9LX |
#region imports from AlgorithmImports import * #endregion BACKTEST_START_YEAR = 2022 # Set start Year of the Backtest BACKTEST_START_MONTH = 12 # Set start Month of the Backtest BACKTEST_START_DAY = 3 # Set start Day of the Backtest BACKTEST_END_YEAR = 2023 # Set end Year of the Backtest BACKTEST_END_MONTH = 1 # Set end Month of the Backtest BACKTEST_END_DAY = 30 # Set end Day of the Backtest BACKTEST_ACCOUNT_CASH = 2000 # Set Backtest Strategy Cash STOCKS = ["QQQ"] # , "IWM", "QQQ", "AMZN", "AAPL", "NVDA", "TSLA", "META", "NFLX", "MSFT"] # Percentage around price at start of day for which we request options data # 6 here would get strike prices that are within +6% and -6% of price at start of day OPTION_SCAN_PERCENTAGE = 6 #PUT ATR PROFIT AND STOP LOSS, CHANGE CALCULATION FOR HISTOGRAM TO TRADINGVIEW ONE START_MINUTES_AFTER_OPEN = 10 END_MINUTES_BEFORE_CLOSE = 10 ATR_PERIOD = 14 ATR_MULTIPLIER = 1 ATR_MULTIPLIER_STOP_LOSS = 1 BOLLINGER_PERIOD = 20 BOLLINGER_FACTOR = 2 KELTNER_PERIOD = 20 KELTNER_FACTOR = 1.5 TTM_SQUEEZE_PERIOD = 20
# Importing QCAlgorithm and other needed classes, functions etc. from AlgorithmImports import * from symboldata import SymbolData import config # Class definition for the main algorithm that inherits from the QCAlgorithm class # This class contains the Initialize, OnData, and other methods that are required to run the algorithm # It is intended to be the main container for the logic of the algorithm # QCAlgorithm is a base class provided by the QuantConnect Lean Algorithm Framework for building and backtesting algorithmic trading strategies. # It provides a set of predefined methods and properties that can be overridden or used as is to implement a trading strategy. # The class can be used to handle data feeds, manage the portfolio and execution, and perform custom computations. # The QCAlgorithm class is the foundation of a trading strategy in QuantConnect, # and it is designed to be extended by the user to create custom trading logic. # It's a class that provides an easy and efficient way to create, test, and execute trading strategies. # The user can use this class as a base class to create a new algorithm, and it will have access to all the features of the Lean engine. class Core(QCAlgorithm): def Initialize(self): self.SetTimeZone(TimeZones.Chicago) # Setting start and end date for the backtest self.SetStartDate(config.BACKTEST_START_YEAR, config.BACKTEST_START_MONTH, config.BACKTEST_START_DAY) self.SetEndDate(config.BACKTEST_END_YEAR, config.BACKTEST_END_MONTH, config.BACKTEST_END_DAY) # Setting backtest account cash self.SetCash(config.BACKTEST_ACCOUNT_CASH) # Initialize stock and symbol data objects self.stocks = config.STOCKS self.symbols = [] self.symbol_instances = {} # Add stocks to the algorithm and store their symbols for stock in self.stocks: try: self.symbols.append(self.AddEquity(stock, Resolution.Minute, extendedMarketHours = True).Symbol) except: raise Exception(f"Unable to add stock of symbol {stock} to the algorithm") # Create symbol data objects for each stock symbol for symbol in self.symbols: self.symbol_instances[symbol] = SymbolData(self, symbol) # This function gets called with each new data point that arrives def OnData(self, data: Slice): # Loop through the symbol data objects for symbol, symbolData in self.symbol_instances.items(): # get list of invested options for the symbol option_invested = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option and x.Key.Underlying == symbol] # Check if we are in the allowed Time window for trades if symbolData.allow_trading: # Check that we are not invested into an option for the symbol if len(option_invested) == 0: # Loop through options data for i in data.OptionChains: if i.Key != symbolData.option_symbol: continue optionchain = i.Value if (data.OptionChains.Count == 0): return # Check if we have a long signal if symbolData.check_long(): day = 0 # Filter options by expiration and call/put for i in range(5): call = [x for x in optionchain if x.Right == OptionRight.Call and ((x.Expiry - self.Time).days) == day] day += 1 if len(call) > 0: break if len(call) == 0: continue # Find call closest to ATM or first strike ITM call_to_buy = min(filter(lambda x: x.Strike <= self.Securities[symbol].Price, call), key = lambda x: abs(x.Strike - self.Securities[symbol].Price)) # Send market order for the option contract self.MarketOrder(call_to_buy.Symbol, 1) self.Debug(symbol) self.Debug(self.Time) self.Debug(f"1 hour {symbolData.ema_5} {symbolData.ema_6} {symbolData.ema_7} {symbolData.ema_8} // 4 hour {symbolData.ema_9} {symbolData.ema_10} {symbolData.ema_11} {symbolData.ema_12}") self.Debug(f"keltner lower {symbolData.k_lower_queue}") self.Debug(f"keltner upper {symbolData.k_upper_queue}") self.Debug(f"bollinger lower {symbolData.b_lower_queue}") self.Debug(f"bollinger upper {symbolData.b_upper_queue}") self.Debug(f"ttm squeeze {symbolData.ttm_squeeze_queue}") self.Debug(f"ema 5 minute {symbolData.ema_4}") self.Debug(f"Price {symbolData.bar_5min}") self.Debug(f"High {max(symbolData.high_queue)} Low {min(symbolData.low_queue)}") symbolData.current_contract = call_to_buy symbolData.entry_price = self.Securities[symbol].Close # Check if we have a short signal elif symbolData.check_short(): day = 0 # Filter options by expiration and call/put for i in range(5): put = [x for x in optionchain if x.Right == OptionRight.Put and ((x.Expiry - self.Time).days) == day] day += 1 if len(put) > 0: break if len(put) == 0: continue # Find put closest to ATM or first strike ITM put_to_buy = min(filter(lambda x: x.Strike >= self.Securities[symbol].Price, put), key = lambda x: abs(x.Strike - self.Securities[symbol].Price)) # Send market order for the option contract self.Debug(symbol) self.Debug(self.Time) self.Debug(f"1 hour {symbolData.ema_5} {symbolData.ema_6} {symbolData.ema_7} {symbolData.ema_8} // 4 hour {symbolData.ema_9} {symbolData.ema_10} {symbolData.ema_11} {symbolData.ema_12}") self.Debug(f"keltner lower {symbolData.k_lower_queue}") self.Debug(f"keltner upper {symbolData.k_upper_queue}") self.Debug(f"bollinger lower {symbolData.b_lower_queue}") self.Debug(f"bollinger upper {symbolData.b_upper_queue}") self.Debug(f"5 minute EMA {symbolData.ema_4}") self.Debug(f"ttm squeeze {symbolData.ttm_squeeze_queue}") self.Debug(f"ema 5 minute {symbolData.ema_4}") self.Debug(f"Price {symbolData.bar_5min}") self.MarketOrder(put_to_buy.Symbol, 1) symbolData.current_contract = put_to_buy symbolData.entry_price = self.Securities[symbol].Close else: option = option_invested[0] check_call = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option and x.Key.Underlying == symbol and x.Key.ID.OptionRight == OptionRight.Call] check_put = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option and x.Key.Underlying == symbol and x.Key.ID.OptionRight == OptionRight.Put] if len(check_call) > 0: symbolData.check_atr_profit("LONG", option) symbolData.check_atr_loss("LONG", option) if len(check_put) > 0: symbolData.check_atr_profit("SHORT", option) symbolData.check_atr_loss("SHORT", option) #self.Debug(self.Portfolio[option].UnrealizedProfitPercent) # Check for profit and loss on the position and liquidate accordingly # if self.Portfolio[option].UnrealizedProfitPercent >= 0.05: # self.Liquidate(option) # symbolData.current_contract = None # elif self.Portfolio[option].UnrealizedProfitPercent <= -0.05: # self.Liquidate(option) # symbolData.current_contract = None # Liquidate at end of trading window elif len(option_invested) > 0: for option in option_invested: self.Liquidate(option)
from AlgorithmImports import * from QuantConnect.Securities.Option import OptionPriceModels from collections import deque from scipy import stats import config class SymbolData(): def __init__(self, algorithm, symbol): # Store algorithm and symbol self.algorithm = algorithm self.symbol = symbol self.current_contract = None # Add options data for symbol self.option = self.algorithm.AddOption(self.symbol, Resolution.Minute) self.option.SetLeverage(1.0) self.option_symbol = self.option.Symbol #self.option.PriceModel = OptionPriceModels.CrankNicolsonFD() self.option.SetFilter(self.UniverseFunc) # Create EMAs for 5min bars self.ema_1 = ExponentialMovingAverage(8) self.ema_2 = ExponentialMovingAverage(21) self.ema_3 = ExponentialMovingAverage(34) self.ema_4 = ExponentialMovingAverage(55) # Create EMAs for 1 hour bars self.ema_5 = ExponentialMovingAverage(8) self.ema_6 = ExponentialMovingAverage(21) self.ema_7 = ExponentialMovingAverage(34) self.ema_8 = ExponentialMovingAverage(55) # Create EMAs for 4 hour bars self.ema_9 = ExponentialMovingAverage(8) self.ema_10 = ExponentialMovingAverage(21) self.ema_11 = ExponentialMovingAverage(34) self.ema_12 = ExponentialMovingAverage(55) # Create deques to hold highs and lows self.high_queue = deque(maxlen=config.TTM_SQUEEZE_PERIOD) self.low_queue = deque(maxlen=config.TTM_SQUEEZE_PERIOD) # Create bollinger band, keltner channel and SMA self.bollinger_1 = BollingerBands(config.BOLLINGER_PERIOD, config.BOLLINGER_FACTOR) self.keltner_1 = KeltnerChannels(config.KELTNER_PERIOD, config.KELTNER_FACTOR) self.sma_1 = SimpleMovingAverage(config.TTM_SQUEEZE_PERIOD) self.linear_reg = LeastSquaresMovingAverage(20) # Create deques to hold x and y values for linear regression needed in TTM squeeze self.delta_x_queue = deque(maxlen=config.TTM_SQUEEZE_PERIOD) self.delta_y_queue = deque(maxlen=config.TTM_SQUEEZE_PERIOD) self.ling_reg_value = None self.bar_counter = 0 # Create deque to store ttm squeeze histogram values self.ttm_squeeze_queue = deque(maxlen=2) # Create deques to store bollinger and keltner values self.b_upper_queue = deque(maxlen=6) self.b_lower_queue = deque(maxlen=6) self.k_upper_queue = deque(maxlen=6) self.k_lower_queue = deque(maxlen=6) self.atr = AverageTrueRange(config.ATR_PERIOD) self.bar_5min = None # Schedule function calls at start and end of trading window, to open and close trading window self.algorithm.Schedule.On(self.algorithm.DateRules.EveryDay(), self.algorithm.TimeRules.AfterMarketOpen(self.symbol, config.START_MINUTES_AFTER_OPEN), self.open_window) self.algorithm.Schedule.On(self.algorithm.DateRules.EveryDay(), self.algorithm.TimeRules.BeforeMarketClose(self.symbol, config.END_MINUTES_BEFORE_CLOSE), self.close_window) self.allow_trading = False # Create consolidator for 1 hour bars self.hourly_consolidator = TradeBarConsolidator(timedelta(hours=1)) self.algorithm.SubscriptionManager.AddConsolidator(self.symbol, self.hourly_consolidator) self.hourly_consolidator.DataConsolidated += self.receive_hourly_bars # Create consolidator for 4 hour bars self.hourly_consolidator_2 = TradeBarConsolidator(timedelta(hours=4)) self.algorithm.SubscriptionManager.AddConsolidator(self.symbol, self.hourly_consolidator_2) self.hourly_consolidator_2.DataConsolidated += self.receive_hourly_bars_2 # Get bars from history to warm up hourly consolidators and indicators history = self.algorithm.History(tickers=[self.symbol], start=self.algorithm.Time - timedelta(days=90), end=self.algorithm.Time, resolution=Resolution.Hour, fillForward=False, extendedMarket=True) for row in history.itertuples(): bar = TradeBar(row.Index[1], self.symbol, row.open, row.high, row.low, row.close, row.volume) self.hourly_consolidator.Update(bar) self.hourly_consolidator_2.Update(bar) # Create consolidator for 5 minute bars self.minute_consolidator = TradeBarConsolidator(timedelta(minutes=5)) self.algorithm.SubscriptionManager.AddConsolidator(self.symbol, self.minute_consolidator) self.minute_consolidator.DataConsolidated += self.receive_minute_bars # Get bars from history to warm up minute consolidators and indicators history = self.algorithm.History(tickers=[self.symbol], start=self.algorithm.Time - timedelta(days=15), end=self.algorithm.Time, resolution=Resolution.Minute, fillForward=False, extendedMarket=True) for row in history.itertuples(): bar = TradeBar(row.Index[1], self.symbol, row.open, row.high, row.low, row.close, row.volume) self.minute_consolidator.Update(bar) # Open trading window def open_window(self): self.allow_trading = True # Close trading window def close_window(self): self.allow_trading = False # Receive 5 min bars and update indicators def receive_minute_bars(self, sender, bar): self.atr.Update(bar) self.bar_5min = bar.Close self.ema_1.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.ema_2.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.ema_3.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.ema_4.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.bollinger_1.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.keltner_1.Update(bar) self.sma_1.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.high_queue.appendleft(bar.High) self.low_queue.appendleft(bar.Low) self.bar_counter += 1 self.delta_x_queue.appendleft(self.bar_counter) if self.bollinger_1.IsReady and self.keltner_1.IsReady and self.sma_1.IsReady: self.b_upper_queue.appendleft(self.bollinger_1.UpperBand.Current.Value) self.b_lower_queue.appendleft(self.bollinger_1.LowerBand.Current.Value) self.k_upper_queue.appendleft(self.keltner_1.UpperBand.Current.Value) self.k_lower_queue.appendleft(self.keltner_1.LowerBand.Current.Value) self.calculate_ttm_squeeze(bar) # Calculate TTM Squeeze histogram def calculate_ttm_squeeze(self, bar): if len(self.high_queue) == config.TTM_SQUEEZE_PERIOD and len(self.low_queue) == config.TTM_SQUEEZE_PERIOD: highest = max(self.high_queue) lowest = min(self.low_queue) e_1 = ((highest + lowest) / 2) + self.sma_1.Current.Value osc_value = bar.Close - e_1 / 2 # if self.algorithm.Time.day == 23 and self.algorithm.Time.hour == 13 and self.algorithm.Time.minute == 45: # self.algorithm.Debug(f"High {max(self.high_queue)} Low {min(self.low_queue)} Close {bar.Close} Midline {donchian_midline} SMA {self.sma_1.Current.Value}") self.delta_y_queue.appendleft(osc_value) self.linear_reg.Update(IndicatorDataPoint(bar.EndTime, osc_value)) if len(self.delta_x_queue) == config.TTM_SQUEEZE_PERIOD and len(self.delta_y_queue) == config.TTM_SQUEEZE_PERIOD: #slope, intercept, r, p, std_err = stats.linregress(self.delta_x_queue, self.delta_y_queue) #self.ling_reg_value = intercept + slope * (config.TTM_SQUEEZE_PERIOD - 1 - 0) # if self.algorithm.Time.hour == 12 and self.algorithm.Time.minute == 0 and self.algorithm.Time.day == 24: # self.algorithm.Debug(self.ling_reg_value) # self.algorithm.Debug(self.algorithm.Securities[self.symbol].Close) self.ttm_squeeze_queue.appendleft(self.linear_reg.Current.Value) # Check long conditions def check_long(self): if self.ema_5.Current.Value > self.ema_6.Current.Value > self.ema_7.Current.Value > self.ema_8.Current.Value: if self.ema_9.Current.Value > self.ema_10.Current.Value > self.ema_11.Current.Value > self.ema_12.Current.Value: if self.bar_5min > self.ema_4.Current.Value: if self.ttm_squeeze_queue[0] > self.ttm_squeeze_queue[1]: if self.check_squeeze(): return True else: return False else: return False else: return False else: return False else: return False # Check short conditions def check_short(self): if self.ema_5.Current.Value < self.ema_6.Current.Value < self.ema_7.Current.Value < self.ema_8.Current.Value: if self.ema_9.Current.Value < self.ema_10.Current.Value < self.ema_11.Current.Value < self.ema_12.Current.Value: if self.bar_5min < self.ema_4.Current.Value: if self.ttm_squeeze_queue[0] < self.ttm_squeeze_queue[1]: if self.check_squeeze(): return True else: return False else: return False else: return False else: return False else: return False # Check for volatility squeeze def check_squeeze(self): if self.b_upper_queue[0] > self.k_upper_queue[0] or self.b_lower_queue[0] < self.k_lower_queue[0]: if (self.b_upper_queue[1] < self.k_upper_queue[1] and self.b_lower_queue[1] > self.k_lower_queue[1] and self.b_upper_queue[2] < self.k_upper_queue[2] and self.b_lower_queue[2] > self.k_lower_queue[2] and self.b_upper_queue[3] < self.k_upper_queue[3] and self.b_lower_queue[3] > self.k_lower_queue[3] and self.b_upper_queue[4] < self.k_upper_queue[4] and self.b_lower_queue[4] > self.k_lower_queue[4] and self.b_upper_queue[5] < self.k_upper_queue[5] and self.b_lower_queue[5] > self.k_lower_queue[5]): return True else: return False else: return False # Receive 1 hour bars and update indicators def receive_hourly_bars(self, sender, bar): self.ema_5.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.ema_6.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.ema_7.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.ema_8.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) # Receive 4 hour bars and update indicators def receive_hourly_bars_2(self, sender, bar): self.ema_9.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.ema_10.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.ema_11.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) self.ema_12.Update(IndicatorDataPoint(bar.EndTime, bar.Close)) def check_atr_profit(self, direction, option): if direction == "LONG": if self.algorithm.Securities[self.symbol].Price >= (self.entry_price + (self.atr.Current.Value * config.ATR_MULTIPLIER)): self.algorithm.Liquidate(option, tag=f"PROFIT at {(self.entry_price + (self.atr.Current.Value * config.ATR_MULTIPLIER))}") elif direction == "SHORT": if self.algorithm.Securities[self.symbol].Price <= (self.entry_price - (self.atr.Current.Value * config.ATR_MULTIPLIER)): self.algorithm.Liquidate(option, tag=f"PROFIT at {(self.entry_price - (self.atr.Current.Value * config.ATR_MULTIPLIER))}") def check_atr_loss(self, direction, option): if direction == "LONG": if self.algorithm.Securities[self.symbol].Price <= (self.entry_price - (self.atr.Current.Value * config.ATR_MULTIPLIER_STOP_LOSS)): self.algorithm.Liquidate(option, tag=f"STOP at {(self.entry_price - (self.atr.Current.Value * config.ATR_MULTIPLIER_STOP_LOSS))}") elif direction == "SHORT": if self.algorithm.Securities[self.symbol].Price >= (self.entry_price + (self.atr.Current.Value * config.ATR_MULTIPLIER_STOP_LOSS)): self.algorithm.Liquidate(option, tag=f"STOP at {(self.entry_price + (self.atr.Current.Value * config.ATR_MULTIPLIER_STOP_LOSS))}") # Request optiosn data for symbol def UniverseFunc(self, universe): ATM = self.algorithm.Securities[self.symbol].Price * config.OPTION_SCAN_PERCENTAGE/100 # Return all options that are within 6% and -6% of price and expiration between 1 and 5 days return universe.IncludeWeeklys().Strikes(-ATM, ATM).Expiration(TimeSpan.FromDays(1),TimeSpan.FromDays(5))