Overall Statistics
Total Trades
155
Average Win
40.55%
Average Loss
-0.16%
Compounding Annual Return
4.831%
Drawdown
9.700%
Expectancy
5.438
Net Profit
60.360%
Sharpe Ratio
0.229
Probabilistic Sharpe Ratio
0.000%
Loss Rate
97%
Win Rate
3%
Profit-Loss Ratio
246.85
Alpha
0.068
Beta
-0.151
Annual Standard Deviation
0.215
Annual Variance
0.046
Information Ratio
-0.276
Tracking Error
0.266
Treynor Ratio
-0.325
Total Fees
$2375.50
Estimated Strategy Capacity
$3400000.00
Lowest Capacity Asset
QQQ 31R15TVANMZ8M|QQQ RIWIV7K5Z9LX
# Watch my Tutorial: https://youtu.be/Lq-Ri7YU5fU
#    Options:    Options perform best during periods of short sharp drops when volatility spikes. 
#                Options held till expiry do a great job of reducing drawdown severity but won't boost returns unless the drawdown is 
#                protracted and still exists at time of Expiry.

from datetime import timedelta

class OptionChainProviderPutProtection(QCAlgorithm):

    def Initialize(self):
        # set start/end date for backtest
        self.SetStartDate(2011, 10, 1)
        self.SetEndDate(2021, 10, 1)
        # set starting balance for backtest
        self.SetCash(400000)
        # add the underlying asset
        self.equity = self.AddEquity("QQQ", Resolution.Minute)
        self.equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
        self.underlyingSymbol = self.equity.Symbol
        
        # initialize the option contract with empty string
        self.contract = str()
        self.contractsAdded = set()

        # parameters ------------------------------------------------------------
        self.OTM = 0.20 # target percentage OTM of put
        self.StaggerStrike = 0.05 + self.OTM  # Stagger strikes at X% levels OTM
        self.DTE = 60 # target 'Calendar' days till expiration
        self.DTEBuffer = 15
        self.DaysBeforeExp = 2 # number of days before expiry to exit
        self.ProfitTarget = 0.5 # OptionValue as a Percentage of Portfolio
        self.percentage = 0.97 # percentage of portfolio for underlying asset
        self.options_weight = 0.02 * 30 / 365 # This allows roughly X% per year for StrikeStaggeredPuts
        # ------------------------------------------------------------------------
    
        # schedule Plotting function 30 minutes after every market open
        self.Schedule.On(self.DateRules.EveryDay(self.underlyingSymbol), self.TimeRules.AfterMarketOpen(self.underlyingSymbol, 30), self.Plotting)

 
    def OnData(self, data):
        
        if not data.ContainsKey(self.underlyingSymbol) or data[self.underlyingSymbol] is None or data[self.underlyingSymbol].IsFillForward: 
            return

        # OPTIONS  -------------------------------------------    
        # Close Options Positions.
        for contract in self.contractsAdded:
            if self.Portfolio[contract].Invested:
                if not data.ContainsKey(contract):
                    continue
                # Do nothing if data is NoneType.
                if data[contract] is None: 
                    continue
                # 2) Close put if Target achieved (Rebalance proceeds?).
                optionValue = self.Portfolio[contract].Quantity * 100 * self.Securities[contract].BidPrice
                optionExposure = optionValue / self.Portfolio.TotalPortfolioValue
                if optionExposure > self.ProfitTarget: 
                    self.SellPut(contract, data)
                    self.Log(f'Put Target achieved @ {self.Time}')
                    self.Log(f'BidPrice: {self.Securities[contract].BidPrice}, AskPrice: {self.Securities[contract].AskPrice}')
                    
                # 3) Close put before it expires
                if self.Time.minute % 5 == 1 and (contract.ID.Date - self.Time) <= timedelta(self.DaysBeforeExp):
                    self.SellPut(contract, data)

                
        # Initiate our Options position
        option_invested = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
        option_invested = sorted(option_invested, key=lambda x: x.ID.StrikePrice, reverse=True)
        buyOption = False
        # 1) If no options, BUY.
        if not option_invested:
            buyOption = True
        # 2) If price has exceeded our current strike by X%, BUY
        elif option_invested[0]:
            buyOption = bool( (1 - option_invested[0].ID.StrikePrice / self.Securities[self.underlyingSymbol].Price) > self.StaggerStrike)
        # Don't try to buy on each data point. Wait a few minutes to search for our contract and initiate a position.    
        if buyOption:
            if self.Time.minute % 5 == 0:
                self.contract = self.OptionsFilter(data)
        if self.Time.minute % 5 == 3 and self.contract and not self.Portfolio[self.contract].Invested:
            self.BuyPut(data)
            
    
    def BuyPut(self, data):
        # Do nothing if data is NoneType or data is stale.
        if data.ContainsKey(self.contract) and not self.Portfolio[self.contract].Invested:
            if data[self.contract] is None or data[self.contract].IsFillForward: 
                return
            quantity = self.CalculateOrderQuantity(self.contract, self.options_weight)
            if quantity < 1:
                self.Log('Cannot buy Options... too expensive!')
            self.SetHoldings(self.contract, self.options_weight)
            
    
    def SellPut(self, contract, data):
        self.SetHoldings(contract, 0) #, ([PortfolioTarget(self.underlyingSymbol, self.percentage)]) 
        self.contract = str()
        
    
    def OptionsFilter(self, data):
        ''' OptionChainProvider gets a list of option contracts for an underlying symbol at requested date.
            Then you can manually filter the contract list returned by GetOptionContractList.
            The manual filtering will be limited to the information included in the Symbol
            (strike, expiration, type, style) and/or prices from a History call '''

        contracts = self.OptionChainProvider.GetOptionContractList(self.underlyingSymbol, data.Time)
        underlyingPrice = self.Securities[self.underlyingSymbol].Price
        # filter the out-of-money put options from the contract list which expire close to self.DTE num of days from now
        otm_puts = [i for i in contracts if i.ID.OptionRight == OptionRight.Put and
                                            underlyingPrice - i.ID.StrikePrice > self.OTM * underlyingPrice and
                                            (self.DTE - self.DTEBuffer) < (i.ID.Date - data.Time).days < (self.DTE + self.DTEBuffer)]
        if len(otm_puts) > 0:
            # sort options by closest to self.DTE days from now and desired strike, and pick first
            contract = sorted( sorted(otm_puts, key = lambda x: abs((x.ID.Date - self.Time).days - self.DTE)),
                                                key = lambda x: underlyingPrice - x.ID.StrikePrice)[0]
            if contract not in self.contractsAdded:
                self.contractsAdded.add(contract)
                self.AddOptionContract(contract, Resolution.Minute)
            return contract
        else:
            #self.Log('No Options at this time...')
            return str()

    def Plotting(self):

        self.Plot("Data Chart", self.underlyingSymbol, self.Securities[self.underlyingSymbol].Close)
        # plot strike of put option
        option_invested = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
        option_invested = sorted(option_invested, key=lambda x: x.ID.StrikePrice)
        if option_invested:
            self.Plot("Opton Invested", "Options", len(option_invested))
            self.Plot("Data Chart", "Strike1", option_invested[0].ID.StrikePrice)

    def OnOrderEvent(self, orderEvent):
        # log order events
        pass
        #self.Log(str(orderEvent))