Overall Statistics
Total Orders
21
Average Win
127.67%
Average Loss
0%
Compounding Annual Return
849797.540%
Drawdown
79.300%
Expectancy
0
Start Equity
2000
End Equity
1764855.9
Net Profit
88142.795%
Sharpe Ratio
26069.728
Sortino Ratio
58149.038
Probabilistic Sharpe Ratio
99.784%
Loss Rate
0%
Win Rate
100%
Profit-Loss Ratio
0
Alpha
97534.569
Beta
-1.153
Annual Standard Deviation
3.741
Annual Variance
13.997
Information Ratio
25775.345
Tracking Error
3.784
Treynor Ratio
-84624.502
Total Fees
$1374.10
Estimated Strategy Capacity
$32000.00
Lowest Capacity Asset
ZM XL7X5HIXH43Q|ZM X3RPXTZRW09X
Portfolio Turnover
8.60%
################################################################################
# "If Only I Had..." - The COVID $ZM Call Roller
# -----------------------------------------------
# A thought experiment on riding Zoom's explosive COVID rally through systematic 
# options trading. This algorithm simulates buying slightly OTM call options on 
# Zoom during the 2020 pandemic surge and rolling them forward for maximum gains.
# 
# Entry: Trading days between 1/1/20-9/30/20, buy ZM calls 120 DTE, strike 15% OTM
# Exit: Liquidate when ITM or at 30 DTE
#
# Not investment advice - just a FOMO-inspired backtesting experiment!
################################################################################

from AlgorithmImports import *
class MiniLeapRoller(QCAlgorithm):

    # ================================================
    # Initialize data, capital, schedulers, etc.
    # ================================================
    def Initialize(self):
        
        # set params: ticker, entry, exit thresholds
        # -----------------------------------------------------
        self.ticker = "ZM"
        self.distFromPrice = int(self.GetParameter("priceDist"))   # pick strike that is this % dist. 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.OnThirtyMinsIntoMarketOpen)
                  
        # 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):
        
        # exit if we dont have data
        # ------------------------------
        if self.Securities[self.symbol] is None:
            return
        
        if not self.Securities[self.symbol].HasData:
             return

        # exit if there are no bars or indicators aren't ready
        # ----------------------------------------------------------
        if not data.Bars.ContainsKey(self.symbol): 
            return

        else:    
            
            # get/store  current price of underlying
            # -----------------------------
            self.underlyingPrice = data.Bars[self.symbol].Close


    # ================================================
    # Run 30 minutes after market open
    # ================================================
    def OnThirtyMinsIntoMarketOpen(self):
        
        # exit if we are still warming up
        # ----------------------------------
        if(self.IsWarmingUp):
            return

        # exit if we dont have data
        # --------------------------
        if 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: use deltas to pick strikes 

            # 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 (f"{self.Time}   [+]---  BUY  {str(callContract)} || Stock @ {str(self.underlyingPrice)}") 
            
    # ==============================================
    # Run 30 minutes before market close
    # ==============================================
    def OnThirtyMinsBeforeMarketClose(self):
        # exit if we are still warming up
        # ----------------------------------
        if(self.IsWarmingUp):
            return

        # exit if we dont have data
        # --------------------------
        if not self.Securities[self.symbol].HasData:
            return

        # otherwise, check for holdings and close them position.
        # --------------------------------------------------
        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 is < 30 DTE, 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, strike and expiration
    # ========================================================
    def GetLongCall(self, symbolArg, callStrikeArg, expirationArg):

        contracts = self.OptionChainProvider.GetOptionContractList(symbolArg, self.Time)
        
        # get calls
        # -------------
        calls = [symbolArg for symbolArg in contracts if symbolArg.ID.OptionRight == OptionRight.Call]

        # get 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 with closest expiration
        # ------------------------------------------------
        callsFilteredByExpiration = [contract for contract in callsSortedByExpiration if contract.ID.Date == closestExpirationDate]
        
        # sort contracts to find ones near our desired strikes
        # ------------------------------------------------------
        callsSortedByStrike = sorted(callsFilteredByExpiration, key=lambda p: abs(p.ID.StrikePrice - callStrikeArg), reverse=False)
        
        # pick contract closest to desired strike and expiration
        # ------------------------------------------------
        callOptionContract = callsSortedByStrike[0]
        
        return callOptionContract