Overall Statistics |
Total Trades 230 Average Win 1.04% Average Loss -0.47% Compounding Annual Return 15.942% Drawdown 16.300% Expectancy 0.395 Net Profit 168.328% Sharpe Ratio 0.993 Probabilistic Sharpe Ratio 42.461% Loss Rate 56% Win Rate 44% Profit-Loss Ratio 2.19 Alpha 0.033 Beta 0.734 Annual Standard Deviation 0.116 Annual Variance 0.013 Information Ratio 0.068 Tracking Error 0.056 Treynor Ratio 0.157 Total Fees $242.94 Estimated Strategy Capacity $120000.00 Lowest Capacity Asset SPY 31R15TVX5CHYE|SPY R735QTJ8XC9X |
#region imports from AlgorithmImports import * #endregion # ###################################################### # ## Code Seperated for readiability # ###################################################### class OptionsUtil(): def __init__(self, algo, theEquity): self.algo = algo self.InitOptionsAndGreeks(theEquity) ## Initialize Options settings, chain filters, pricing models, etc ## --------------------------------------------------------------------------- def InitOptionsAndGreeks(self, theEquity): ## 1. Specify the data normalization mode (must be 'Raw' for options) theEquity.SetDataNormalizationMode(DataNormalizationMode.Raw) ## 2. Set Warmup period of at least 30 days self.algo.SetWarmup(30, Resolution.Daily) ## 3. Set the security initializer to call SetMarketPrice self.algo.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.algo.GetLastKnownPrice(x))) ## 4. Subscribe to the option feed for the symbol theOptionSubscription = self.algo.AddOption(theEquity.Symbol) ## 5. set the pricing model, to calculate Greeks and volatility theOptionSubscription.PriceModel = OptionPriceModels.CrankNicolsonFD() # both European & American, automatically ## 6. Set the function to filter out strikes and expiry dates from the option chain theOptionSubscription.SetFilter(self.OptionsFilterFunction) ## Buy an OTM Call Option. ## Use Delta to select a call contract to buy ## --------------------------------------------------------------------------- def BuyAnOTMCall(self, theSymbol): ## Buy a Call expiring callDelta = float(self.algo.GetParameter("callDelta"))/100 callDTE = int(self.algo.GetParameter("callDTE")) callContract = self.SelectContractByDelta(theSymbol, callDelta, callDTE, OptionRight.Call) # construct an order message -- good for debugging and order rrecords # ------------------------------------------------------------------------------ # if( callContract is not None ): # Might need this.... orderMessage = f"Stock @ ${self.algo.CurrentSlice[theSymbol].Close} |" + \ f"Buy {callContract.Symbol} "+ \ f"({round(callContract.Greeks.Delta,2)} Delta)" self.algo.Debug(f"{self.algo.Time} {orderMessage}") self.algo.Order(callContract.Symbol, 1, False, orderMessage ) ## Sell an OTM Put Option. ## Use Delta to select a put contract to sell ## --------------------------------------------------------------------------- def SellAnOTMPut(self, theSymbol): ## Sell a Put expiring in 2 weeks (14 days) putDelta = float(self.algo.GetParameter("putDelta"))/100 putDTE = int(self.algo.GetParameter("putDTE")) putContract = self.SelectContractByDelta(theSymbol, putDelta, putDTE, OptionRight.Put) ## construct an order message -- good for debugging and order rrecords orderMessage = f"Stock @ ${self.algo.CurrentSlice[theSymbol].Close} |" + \ f"Sell {putContract.Symbol} "+ \ f"({round(putContract.Greeks.Delta,2)} Delta)" self.algo.Debug(f"{self.algo.Time} {orderMessage}") self.algo.Order(putContract.Symbol, -1, False, orderMessage ) ## Get an options contract that matches the specified criteria: ## Underlying symbol, delta, days till expiration, Option right (put or call) ## --------------------------------------------------------------------------- def SelectContractByDelta(self, symbolArg, strikeDeltaArg, expiryDTE, optionRightArg= OptionRight.Call): canonicalSymbol = self.algo.AddOption(symbolArg) if(canonicalSymbol.Symbol not in self.algo.CurrentSlice.OptionChains): self.algo.Log(f"{self.algo.Time} [Error] Option Chain not found for {canonicalSymbol.Symbol}") return theOptionChain = self.algo.CurrentSlice.OptionChains[canonicalSymbol.Symbol] theExpiryDate = self.algo.Time + timedelta(days=expiryDTE) ## Filter the Call/Put options contracts filteredContracts = [x for x in theOptionChain if x.Right == optionRightArg] ## Sort the contracts according to their closeness to our desired expiry contractsSortedByExpiration = sorted(filteredContracts, key=lambda p: abs(p.Expiry - theExpiryDate), reverse=False) closestExpirationDate = contractsSortedByExpiration[0].Expiry ## Get all contracts for selected expiration contractsMatchingExpiryDTE = [contract for contract in contractsSortedByExpiration if contract.Expiry == closestExpirationDate] ## Get the contract with the contract with the closest delta closestContract = min(contractsMatchingExpiryDTE, key=lambda x: abs(abs(x.Greeks.Delta)-strikeDeltaArg)) return closestContract ## The options filter function. ## Filter the options chain so we only have relevant strikes & expiration dates. ## --------------------------------------------------------------------------- def OptionsFilterFunction(self, optionsContractsChain): strikeCount = 30 # no of strikes around underyling price => for universe selection minExpiryDTE = 55 # min num of days to expiration => for uni selection maxExpiryDTE = 65 # max num of days to expiration => for uni selection return optionsContractsChain.IncludeWeeklys()\ .Strikes(-strikeCount, strikeCount)\ .Expiration(timedelta(minExpiryDTE), timedelta(maxExpiryDTE))
#region imports from AlgorithmImports import * #endregion ############################################################ # Long SPY shares with .5% allocated to 30 delta protective puts at 60 DTE, rolled at 30 days. # Reference (reddit discussion): https://bit.ly/3zJHUIj ############################################################ from datetime import timedelta from OptionsUtil import * class LongSPYOTMPut(QCAlgorithm): def Initialize(self): self.SetStartDate(2015, 1, 1) self.SetEndDate(2021, 9, 1) self.SetCash(100000) self.equity = self.AddEquity("SPY", Resolution.Minute) self.SPYSymbol = self.equity.Symbol self.InitParameters() self.OptionsUtil = OptionsUtil(self, self.equity) ## Initialize parameters (periods, days-till-expiration, etc) ## ------------------------------------------------------------ def InitParameters(self): ## Params for our SPY Put contract self.putExpiryDate = None # var to track the expiry date self.putInitialDTE = 60 #int(self.ParamManager.GetParameter("putInitialDTE")) # Enter at 60 days to exp self.putExitDTECoeff = .5 #float(self.ParamManager.GetParameter("putExitDTECoeff")) # Exit at .5 (half)-way to exp self.putStrikeDelta = .30 #float(self.ParamManager.GetParameter("putStrikeDelta"))/100 # -30% delta self.spendPctOnShares = .995 #float(self.ParamManager.GetParameter("spendPctOnShares"))/100 # 95% on shares ## schedule routine to run 30 minutes after every market open self.Schedule.On(self.DateRules.EveryDay(self.SPYSymbol), \ self.TimeRules.AfterMarketOpen(self.SPYSymbol, 30), \ self.DailyAtMarketOpen) ## Every morning at market open, Check for entries / exits. ## ------------------------------------------------------------ def DailyAtMarketOpen(self): ## If algo is done warming up if ((not self.IsWarmingUp) and self.CurrentSlice.ContainsKey("SPY")): # check the number of puts in the portfolio putsInPortfolio = len([x for x in self.Portfolio if (x.Value.Symbol.HasUnderlying and x.Value.Invested)]) ## If we have no investments or no puts in our portfolio, (re)allocate shares & puts if (not self.Portfolio.Invested or (putsInPortfolio == 0)): self.SetSharesHoldings() ## Set shares holdings to X% (may involve buying or selling) self.BuyOTMPuts() ## Buy OTM Puts with whatever is left ## If we have holdings, check to see if we are past our exit DTE for the puts elif( self.Portfolio.Invested ): currentDTE = (self.putExpiryDate - self.Time).days if ( currentDTE <= (self.putInitialDTE * self.putExitDTECoeff) ): for x in self.Portfolio: if x.Value.Invested: assetLabel = "Puts " if (x.Value.Symbol.HasUnderlying) else "Shares" assetChange = round(self.Portfolio[x.Value.Symbol].UnrealizedProfitPercent,2) profitLabel = "Profit" if (assetChange > 0) else "loss" if(x.Value.Symbol.HasUnderlying): self.Liquidate(x.Value.Symbol, tag=f" {currentDTE} DTE. Sold {assetLabel} [ {assetChange}% {profitLabel} ]") ## Allocate X% of available capital to SPY shares ## ------------------------------------------------------------ def SetSharesHoldings(self): if(self.CurrentSlice.ContainsKey(self.SPYSymbol) and (self.CurrentSlice[self.SPYSymbol] is not None)): currentSpyPrice = self.CurrentSlice[self.SPYSymbol].Price approxSpendAmt = math.floor((self.Portfolio.Cash*self.spendPctOnShares) / currentSpyPrice) * currentSpyPrice self.SetHoldings("SPY",self.spendPctOnShares, tag=f"Set SPY to ~{self.spendPctOnShares*100}% of Equity. ") else: self.Log(f"${self.Time} [Warning] Could not adjust SPY shares held. Slice doesnt contian key") ## Buy OTM Put Options with all available cash ## ------------------------------------------------------------ def BuyOTMPuts(self): ## Buy a Put at the specified delta, with the specified expiration date putContract = self.OptionsUtil.SelectContractByDelta(self.SPYSymbol, self.putStrikeDelta, \ self.putInitialDTE, OptionRight.Put) if( putContract is None ): # self.Log(f"{self.Time} [Error] Could not retrieve Put Contract from chain") return ## Calculate our affordable quantity affordableQty = math.floor(self.Portfolio.Cash / ( putContract.AskPrice * 100 )) ## construct an order message -- good for debugging and order rrecords orderMessage = f"Buy Puts with ~{round((1-self.spendPctOnShares),2)*100}% of Equity. (${round(self.Portfolio.Cash,2)}) "+\ f"- {putContract.Strike} Strike | {(putContract.Expiry-self.Time).days} DTE"+ \ f"({round(putContract.Greeks.Delta,2)} Delta)" if( affordableQty > 0 ): self.Order(putContract.Symbol, affordableQty, False, orderMessage ) self.putExpiryDate = putContract.Expiry else: self.Log(f"${self.Time} [Warning] {self.Portfolio.Cash} is not enough cash to buy Puts ") self.Liquidate()