Overall Statistics
Total Trades
35
Average Win
82.56%
Average Loss
-13.95%
Compounding Annual Return
4994726.341%
Drawdown
82.400%
Expectancy
5.511
Net Profit
336080.175%
Sharpe Ratio
6210465.379
Probabilistic Sharpe Ratio
99.990%
Loss Rate
6%
Win Rate
94%
Profit-Loss Ratio
5.92
Alpha
25959543.25
Beta
-1.629
Annual Standard Deviation
4.18
Annual Variance
17.472
Information Ratio
6108229.276
Tracking Error
4.25
Treynor Ratio
-15936514.701
Total Fees
$2146.50
##########################################################################
# The 'Mini-Leap' Roller
# Author: Ikezi Kamanu
# -------------------------------------------------
#
# Entry:
#   30 Mins before market close, if the portfolio is empty, 
#   open as many single OTM call options as possible, with
#   strikes that are 10% above the underlying, expiring in 120 days.
#
# Exit:
#   30 Mins after market open, for every call in the  portfolio, 
#   if the call is ITM or there are less than 30 days till expiry, 
#   liquidate the position. They will be re-opened (rolled) before close. 
#
##########################################################################

class MiniLeapRoller(QCAlgorithm):

    # ==============================================================
    # Initialize data, capital, scheduled routines, etc.
    # ==============================================================
    def Initialize(self):
        
        # set params: ticker, entry, exit thresholds
        # -----------------------------------------------------
        self.ticker = "ZM"
        self.distFromPrice = int(self.GetParameter("priceDist"))   # pick strike that is this % above from price   
        self.enterAtDTE    = int(self.GetParameter("enterDTE"))    # buy option with this many days till expiry 
        self.exitAtDTE     = int(self.GetParameter("exitDTE"))     # sell when this many days left tille expiry
        
        # set start/end date for backtest
        # --------------------------------------------
        self.SetStartDate(2020, 1, 1)  # Set Start Date 
        self.SetEndDate(2020, 9, 30)  # Set End Date
        
        # set starting balance for backtest
        # --------------------------------------------
        self.SetCash(2000)  

        # 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)
        
        # 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 30 minutes before every market close
        # --------------------------------------------------------------
        self.Schedule.On(self.DateRules.EveryDay(self.symbol), \
                        self.TimeRules.BeforeMarketClose(self.symbol, 30), \
                        self.OnThirtyMinsBeforeMarketClose)
        
        # set the warmup period
        # ----------------------
        self.SetWarmUp(200)
        

    # ==============================================================
    # 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
        

    # ==============================================================
    # Run this 30 minutes before market close
    # ==============================================================
    def OnThirtyMinsBeforeMarketClose(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/100) )
            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()

            # retrive closest call contracts
            # -------------------------------
            callContract = self.GetLongCall(self.symbol, callStrike, expiration)

            
            # subscribe to data for those contracts
            # -----------------------------------------
            self.AddOptionContract(callContract, Resolution.Minute)

            # buy call
            # --------------
            self.SetHoldings(callContract, 1)   
            self.Debug ("\r+-------------")
            self.Debug (f"| {self.Time}   [+]---  BUY  {str(callContract)} || Stock @ {str(self.underlyingPrice)}") 
            
            
    # ==============================================================
    # Run this 30 minutes after market open
    # ==============================================================
    def OnThirtyMinsAfterMarketOpen(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

        # check for open contracts and close them if warranted.
        # ------------------------------------------------------
        for symbol in self.Securities.Keys:
            if self.Securities[symbol].Invested:
                
                # if current contract is ITM, liquidate 
                # ---------------------------------------
                if (self.underlyingPrice > self.Securities[symbol].StrikePrice):  
                    self.Debug (f"| {self.Time}   [-]---  SELL {symbol} ||  ITM  | Stock @ {str(self.underlyingPrice)}")
                    self.Debug ("+-------------")
                    self.Liquidate()
                    return
               
                # if current contract expiry is less than 'X' liquidate 
                # -----------------------------------------------------------
                elif ((self.Securities[symbol].Expiry - self.Time).days < self.exitAtDTE):  
                    self.Debug (f"| {self.Time}   [-]---  SELL {symbol} ||  <30 DTE  | {(self.Securities[symbol].Expiry - self.Time).days} DTE")
                    self.Debug ("+-------------")
                    self.Liquidate()   
                    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