Overall Statistics |
Total Trades 14 Average Win 1.27% Average Loss -5.44% Compounding Annual Return -97.818% Drawdown 28.300% Expectancy -0.824 Net Profit -27.738% Sharpe Ratio -2.607 Probabilistic Sharpe Ratio 0.000% Loss Rate 86% Win Rate 14% Profit-Loss Ratio 0.23 Alpha -0.395 Beta -0.183 Annual Standard Deviation 0.366 Annual Variance 0.134 Information Ratio -7.401 Tracking Error 0.542 Treynor Ratio 5.215 Total Fees $1713.50 |
########################################################################## # The Dynamic 'Mini-Leap' Trader # Author: Ikezi Kamanu # Contributors: Leandro Maia # ---------------------------------------------------- # # Trade 'mini-LEAP' option contracts (120 - 365 DTE), # based on daily universe selection. # # ---------------------------------------------------- # Entry: # Documentation pending. # # Position Management: # Documentation pending. # # Exit: # Documentation pending. # ########################################################################## from dateutil import parser class DynamicMiniLeapRoller(QCAlgorithm): # ============================================================== # Initialize data, capital, scheduled routines, etc. # ============================================================== def Initialize(self): # Initializers (params, universe, routines, etc) # ---------------------------------------------------------- self.InitializeAlgoParams() self.InitializeBacktestParams() self.SetupUnderlyingSecurity() self.InitializeUniverse() self.ScheduleRoutines() # ====================================================== # Set algo params: ticker, thresholds # ====================================================== def InitializeAlgoParams(self): self.ticker = "SPY" self.data = {} self.distFromPrice = int(self.GetParameter("priceDistPct"))/100 # pick OTM strike that is x% higher than price self.enterAtDTE = int(self.GetParameter("enterDTE")) # buy an option with x days till expiry self.exitAtDTE = int(self.GetParameter("exitDTE")) # sell the option when x days til expiry self.roiTarget = int(self.GetParameter("roiTargetPct"))/100 # sell when option has gained x% ROI self.pctOfAcct = int(self.GetParameter("pctOfAcct"))/100 # trade with X% of your acct balance self.stopLoss = int(self.GetParameter("stopLossPct"))/100 # sell when option value has lost -x% self.maxDaysInTrade = int(self.GetParameter("maxDaysInTrade")) # sell when contract has beeen open for X days self.MarketOpenMoreThanThirtyMins = False # ====================================================== # Set backtest params: dates, cash, etc # ====================================================== def InitializeBacktestParams(self): # set start/end date for backtest # -------------------------------------------- self.SetStartDate(2020, 4, 1) # Set Start Date self.SetEndDate(2020, 5, 1) # Set End Date # set starting balance for backtest # -------------------------------------------- self.SetCash(100000) # ====================================================== # Configure underlying, set the custom intializer # ====================================================== def SetupUnderlyingSecurity(self): # add the underlying asset # --------------------------------- self.equity = self.AddEquity(self.ticker, Resolution.Minute) self.equity.SetDataNormalizationMode(DataNormalizationMode.Raw) self.symbol = self.equity.Symbol self.forceInitialized = False # set custom security intializer # ------------------------------- self.SetSecurityInitializer(self.InitializeSecurities) # ============================================================== # Initialize the security # ============================================================== def InitializeSecurities(self, security): # intialize securities with last known price, # so that we can immediately trade the security # ------------------------------------------------ bar = self.GetLastKnownPrice(security) security.SetMarketPrice(bar) # ============================================================== # OnData Event handler # ============================================================== def OnData(self, data): # if we have data # ------------------------------ if (self.Securities[self.symbol] is not None) and \ (self.Securities[self.symbol].HasData) and \ (data.Bars.ContainsKey(self.symbol)): # keep track of the current close price # ------------------------------------------ self.underlyingPrice = data.Bars[self.symbol].Close ####################################################### ############### Routines ################## ########################################@############## # ============================================================== # Schedule Routines (similar to chron jobs) # ============================================================== def ScheduleRoutines(self): # schedule routine to run 30 minutes after every market open # -------------------------------------------------------------- self.Schedule.On(self.DateRules.EveryDay(self.symbol), \ self.TimeRules.AfterMarketOpen(self.symbol, 30), \ self.OnThirtyMinsAfterMarketOpen) # schedule routine to run every 10 minutes # ----------------------------------------- self.Schedule.On(self.DateRules.EveryDay(self.symbol), \ self.TimeRules.Every(timedelta(minutes=10)), \ self.OnEveryTenMinsDuringMarket) # schedule routine to run 30 minutes before every market close # -------------------------------------------------------------- # self.Schedule.On(self.DateRules.EveryDay(self.symbol), \ # self.TimeRules.BeforeMarketClose(self.symbol, 30), \ # self.OnThirtyMinsBeforeMarketClose) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen(self.SPY, 1), self.OnOneMinuteAfterMarketOpen) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.BeforeMarketClose(self.SPY, 1), self.OnOneMinuteBeforeMarketCloses) # ============================================================== # Run this Logic 1 minute after market open # ============================================================== def OnOneMinuteAfterMarketOpen(self): self.MarketOpenMoreThanThirtyMins = False self.FlushPrevSelectStocks() self.UpdateSymbolDataAtMarketOpen() # ============================================================== # Run this Logic 1 minute before market close # ============================================================== def OnOneMinuteBeforeMarketCloses(self): self.MarketOpenMoreThanThirtyMins = False self.UpdateSymbolDataAtMarketClose() # ============================================================== # Run this Logic 30 minutes after market open # ============================================================== def OnThirtyMinsAfterMarketOpen(self): self.MarketOpenMoreThanThirtyMins = True self.SelectStocksFromUniverse() # return if we are still warming up # or if we do not have any data # ---------------------------------- if(self.IsWarmingUp) or \ (not self.Securities[self.symbol].HasData): return self.ManageOpenPositions() #self.OpenLongCallIfNotInvested() # ============================================================== # Run this logic every 10 minutes while market is open # ============================================================== def OnEveryTenMinsDuringMarket(self): if self.MarketOpenMoreThanThirtyMins : self.ManageOpenPositions() self.OpenLongCallIfNotInvested() # ============================================================== # Run this 30 minutes before market close # ============================================================== # def OnThirtyMinsBeforeMarketClose(self): # self.OpenLongCallIfNotInvested ################################################################### ############## Order Logic (open/close/manage) ############### ################################################################### # ======================================================================= # Open a new option for the most recent stock in our universe selection # If the universe is empty, use our default stock # ======================================================================= def OpenLongCallIfNotInvested(self): # exit if we are still warming up # or if we do not have any data # ---------------------------------- if(self.IsWarmingUp) or \ (not self.Securities[self.symbol].HasData): return # otherwise, if we have no holdings, open a position. # -------------------------------------------------- if not self.Portfolio.Invested: # set strikes and expiration # ------------------------------ callStrike = self.underlyingPrice * (1 + (self.distFromPrice) ) expiration = self.Time + timedelta(days=self.enterAtDTE) ### todo: consider using ATR for strike selection ### todo: consider using delta for strike selection ### todo: encapsulate strike and expiry selection in GetLongCall() if(len(self.selectedStocks) > 0 ): theSymbol = self.selectedStocks.pop() self.equity = self.AddEquity(theSymbol.Value, Resolution.Minute) self.equity.SetDataNormalizationMode(DataNormalizationMode.Raw) self.symbol = self.equity.Symbol self.OpenLongCall(theSymbol, callStrike, expiration ) else: return # universe is empty. do nothing # theSymbol = self.symbol # use previously used security if nothing in universe selection # ============================================================== # Open Long Call # ============================================================== def OpenLongCall(self, symbolArg, callStrike, expiration ): # retrive closest call contracts # ------------------------------- callContract = self.GetLongCall(symbolArg, callStrike, expiration) # subscribe to data for those contracts # ----------------------------------------- theOption = self.AddOptionContract(callContract, Resolution.Minute) # buy call contract # ------------------- orderMsg = "." #"Strike: "+str(callContract.StrikePrice) + " Stock @ " + str(callContract.UnderlyingLastPrice) self.SetHoldings(callContract, self.pctOfAcct, False, orderMsg) self.Debug("[[ BUY ]] " + str(theOption) + " | "+ orderMsg + " | "+ self.Time.ctime() ) # store the opening time in the object store. # ------------------------------------------- # todo: Refactor this to use SymbolData. Currently using suggestion from here: # https://www.quantconnect.com/forum/discussion/9212/how-to-check-position-age/p1 self.ObjectStore.Save(str(theOption), str(self.Time)) # ============================================================== # Roll Long Call # ============================================================== # def RollLongCall(self, optionSymbolArg, callStrike, expiration ): # underlyingSymbol = optionSymbolArg.underlyingSymbol # self.Liquidate(optionSymbolArg, orderMsg) # self.OpenLongCall(underlyingSymbol, callStrike, expiration ) # ============================================================== # Manage positions: take profits, stop losses, rolling, etc. # ============================================================== def ManageOpenPositions(self): # check for open contracts and close them if warranted. # ------------------------------------------------------ for symbol in self.Securities.Keys: if self.Securities[symbol].Invested: # Set debug values # --------------------------------------------------- currTime = self.Time stockPrice = self.underlyingPrice profitPct = round(self.Securities[symbol].Holdings.UnrealizedProfitPercent,2) daysTillExp = (self.Securities[symbol].Expiry - self.Time).days # if current contract is ITM, liquidate # This should be the first check, because we want to keep them OTM # ----------------------------------------------------------------- if (self.underlyingPrice > self.Securities[symbol].StrikePrice): orderMsg = "[ITM] exit @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice) self.Liquidate(symbol, orderMsg) return # if current contract has hit x% return, liquidate # --------------------------------------------------- ### ### todo: experiment with diff ROI roll targets here. ### for now, set the param value too high to reach ### so this code never gets called. explore later ### elif( profitPct >= (self.roiTarget)): orderMsg = "[TP] exit @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice) self.Liquidate(symbol, orderMsg) return # if current contract has hit x% loss, liquidate # --------------------------------------------------- elif( profitPct <= (-self.stopLoss)): orderMsg = "[SL] exit @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice) self.Liquidate(symbol, orderMsg) return # if current position has been open for X days, liquidate # -------------------------------------------------------- elif self.ObjectStore.ContainsKey(str(symbol)): date = self.ObjectStore.Read(str(symbol)) date = parser.parse(date) if (self.Time - date).days >= self.maxDaysInTrade: self.ObjectStore.Delete(str(symbol)) orderMsg = "[Duration] exit @ "+str(self.maxDaysInTrade)+" days @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice) + " || " + str(daysTillExp) + " DTE" self.Liquidate(symbol, orderMsg) return # if current contract expiry is less than 'X' liquidate # ----------------------------------------------------------- elif ((self.Securities[symbol].Expiry - self.Time).days < self.exitAtDTE): orderMsg = "[DTE] exit @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice) + " || " + str(daysTillExp) + " DTE" self.Liquidate(symbol, orderMsg) return # ============================================================== # Get Long Call, given a symbol, desired strike and expiration # ============================================================== def GetLongCall(self, symbolArg, callStrikeArg, expirationArg): contracts = self.OptionChainProvider.GetOptionContractList(symbolArg, self.Time) # get all calls # ------------- calls = [symbol for symbol in contracts if symbol.ID.OptionRight == OptionRight.Call] # sort contracts by expiry dates and select expiration closest to desired expiration # -------------------------------------------- callsSortedByExpiration = sorted(calls, key=lambda p: abs(p.ID.Date - expirationArg), reverse=False) closestExpirationDate = callsSortedByExpiration[0].ID.Date # get all contracts for selected expiration # ------------------------------------------------ callsFilteredByExpiration = [contract for contract in callsSortedByExpiration if contract.ID.Date == closestExpirationDate] # sort contracts and select the one closest to desired strike # ----------------------------------------------------------- callsSortedByStrike = sorted(callsFilteredByExpiration, key=lambda p: abs(p.ID.StrikePrice - callStrikeArg), reverse=False) callOptionContract = callsSortedByStrike[0] return callOptionContract ###################################################################### ########### Universe / Stock Selection Logic ############### ###################################################################### # ============================================================== # Initialize the universe, and universe selection criteria # ============================================================== def InitializeUniverse(self): self.UniverseSettings.Resolution = Resolution.Minute self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) self.SPY = self.AddEquity('SPY', Resolution.Minute).Symbol self.day = -1 self.num_coarse = 20 self.min_stock_price = 10 # set params used for filter criteria # ------------------------------------ self.min_days_after_earnings = 10 self.max_days_after_earnings = 80 self.ema_period = 40 # 8 on 5 min timeframe self.sma_period = 275 # 55 on 5 min timeframe self.bb_period = 600 # 20 on 30 min timeframe self.bb_k = 2 self.gap_distance = 0.02 # 2% self.data = {} self.selectedStocks = [] # ============================================================== # Universe Coarse selection logic. # --------------------------------- # Find stocks with fundamentals with price > 5, # rank by dollar volume, and then pick the top X # ============================================================== def CoarseSelectionFunction(self, coarse): if self.day == self.Time.day: return Universe.Unchanged self.day = self.Time.day # drop stocks which have no fundamental data or have too low prices selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > self.min_stock_price)] # rank the stocks by dollar volume # ----------------------------------- filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True) return [ x.Symbol for x in filtered[:self.num_coarse]] # ======================================================================== # Universe Fine selection logic # --------------------------------- # Find common stocks where the share class is not a depository receipt, # and have neither had recent earnings reports nor any coming soon. # # todo: explore other ways to account for future earnings. # ======================================================================== def FineSelectionFunction(self, fine): filtered = [x for x in fine if x.SecurityReference.IsPrimaryShare and x.SecurityReference.SecurityType == "ST00000001" and x.SecurityReference.IsDepositaryReceipt == 0 and x.CompanyReference.IsLimitedPartnership == 0 and x.EarningReports.FileDate < self.Time - timedelta(days=self.min_days_after_earnings) and x.EarningReports.FileDate > self.Time - timedelta(days=self.max_days_after_earnings)] return [x.Symbol for x in filtered] # ==================================================================== # Flush stocks previously selected from universe # ==================================================================== def FlushPrevSelectStocks(self): self.selectedStocks.clear() # ===================================================================== # Select new stocks from universe that pass technical indicator filters # ===================================================================== def SelectStocksFromUniverse(self): for symbol in self.data.keys(): if (self.data[symbol].GapUp) and (self.data[symbol].EMA > self.data[symbol].SMA) and \ (self.data[symbol].BB.UpperBand.Current.Value < self.Securities[symbol].Close): # if stock passes criteria (ie: there is signal), add to our 'selected stocks' # ---------------------------------------------------------------------------- self.selectedStocks.append(symbol) self.Debug(" + " + str(symbol).split(" ")[0] + "\t Added on " + self.Time.ctime()) # ============================================================================ # When securities are added or removed, update our internal symbol dictionary # ============================================================================ def OnSecuritiesChanged(self, changes): for security in changes.RemovedSecurities: if security.Symbol in self.data: del self.data[security.Symbol] for security in changes.AddedSecurities: if (security.Symbol not in self.data) and \ (security.Symbol.SecurityType == SecurityType.Equity): self.data[security.Symbol] = SymbolData(security.Symbol, self.ema_period, self.sma_period, self.bb_period, self.bb_k, self) # ============================================================================ # Update state of SymbolData at market open # ============================================================================ def UpdateSymbolDataAtMarketOpen(self): for symbol in self.data.keys(): gap = (self.Securities[symbol].Close - self.data[symbol].LastClose) / self.data[symbol].LastClose if gap > self.gap_distance: self.data[symbol].GapUp = True else: self.data[symbol].GapUp = False # ============================================================================ # Update state of SymbolData at market close # ============================================================================ def UpdateSymbolDataAtMarketClose(self): for symbol in self.data.keys(): self.data[symbol].LastClose = self.Securities[symbol].Close class SymbolData(object): def __init__(self, symbol, ema, sma, bb, k, algorithm): self.Symbol = symbol self.LastClose = 0 self.GapUp = False self.EMA = ExponentialMovingAverage(ema) self.SMA = SimpleMovingAverage(sma) self.BB = BollingerBands(bb, k, MovingAverageType.Exponential) algorithm.RegisterIndicator(symbol, self.EMA, Resolution.Minute, Field.Close) algorithm.RegisterIndicator(symbol, self.SMA, Resolution.Minute, Field.Close) algorithm.RegisterIndicator(symbol, self.BB, Resolution.Minute, Field.Close) # Logic for 'manual' warmup . # -------------------------------------- # Check for daily data. we need at least one day # ------------------------------------------------ history = algorithm.History(symbol, 1, Resolution.Daily) if history.empty or 'close' not in history.columns: return for index, row in history.loc[symbol].iterrows(): self.LastClose = row['close'] # Check for minute data. we need at least the MAX of our indicator periods # -------------------------------------------------------------------------- history = algorithm.History(symbol, max(ema, sma, bb), Resolution.Minute) if history.empty or 'close' not in history.columns: return for index, row in history.loc[symbol].iterrows(): self.EMA.Update(index, row['close']) self.SMA.Update(index, row['close']) self.BB.Update(index, row['close'])