Overall Statistics
Total Trades
14
Average Win
0%
Average Loss
-0.38%
Compounding Annual Return
-5.247%
Drawdown
2.800%
Expectancy
-1
Net Profit
-2.632%
Sharpe Ratio
-2.395
Probabilistic Sharpe Ratio
0.115%
Loss Rate
100%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0.015
Annual Variance
0
Information Ratio
-2.395
Tracking Error
0.015
Treynor Ratio
0
Total Fees
$31.50
Estimated Strategy Capacity
$16000000.00
Lowest Capacity Asset
SPY Y05J8JAEVQ4M|SPY R735QTJ8XC9X
# region imports
from AlgorithmImports import *
# endregion

symbol_dict = {  # will be used to store the configs of multiple symbols
                'SPY':  # equity symbol
                    {
                        'initialAmount': 10000,  # adjust this for trade size
                        'contractDelta': 0.5,  # sets the delta for contract to select
                        'SMA': 25,  # SMA for the indicator
                        'waitPeriod': 0,  # additional days to wait after exit
                        'trailingStop': 0.03  # trailing stop percentage
                    }
                }

class SMAOptions(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2022, 1, 1)  # Set Start Date 
        self.SetEndDate(2022, 6, 30)  # sets end date
        self.SetCash(1000000)  # Set starting account size

        # settings for option contracts expirations
        self.expirationThreshold = timedelta(10)  # this is the days threshold to perform a time roll
        self.minContractExpiration = timedelta(30)  # this is the minimum number of days out to look for a contract
        self.maxContractExpiration = timedelta(60)  # this is the maximum number of days out to look for a contract

        # sets brokerage model
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)

        # setting empty dictionaries for multiple symbols
        self.tradeAmount = {}
        self.isRollOut = {}
        self.rollValue = {}
        self.equity = {}
        self.equityDailyPrice = {}
        self.equitySingleDayPrice = {}
        self.entryAmount = {}
        self.entryStrike = {}
        self.manualSMA1 = {}
        self.manualSMA2 = {}
        self.autoSMA = {}
        self.waitPeriod = {}
        self.nextEntryTime = {}
        self.nextExitTime = {}
        self.trailingStop = {}
        self.exitStop = {}
        self.call = {}

        # initialize a warm up period of 0
        warmUpPeriod = 0
        
        self.exitPeriod = timedelta(0.5) 

        # loop to initialize each symbol
        for symbol in symbol_dict.keys():
            
            # adds config parameters to log
            symbolConfigMessage = "[Symbol] {0}, [initialAmount] {1}, [contractDelta] {2}, [SMA] {3}, [trailingStop] {4}".format(
                                        symbol,
                                        symbol_dict[symbol]['initialAmount'],
                                        symbol_dict[symbol]['contractDelta'],
                                        symbol_dict[symbol]['SMA'],
                                        symbol_dict[symbol]['waitPeriod'],
                                        symbol_dict[symbol]['trailingStop']
                                    )
            self.Log(symbolConfigMessage)

            self.tradeAmount[symbol] = 0
            
            self.isRollOut[symbol] = False
            self.rollValue[symbol] = 0

            # sets underlying asset
            self.equity[symbol] = self.AddEquity(symbol, Resolution.Minute)  
            self.equity[symbol].SetDataNormalizationMode(DataNormalizationMode.Raw)
            
            self.equityDailyPrice[symbol] = None
            self.entryAmount[symbol] = 0  # used to track initial entry over rolls for pnl
            self.entryStrike[symbol] = 0. # used to track initial strike price

            option = self.AddOption(self.equity[symbol].Symbol, Resolution.Minute) 
            option.SetFilter(-2, 2, self.minContractExpiration, self.maxContractExpiration)
            # set pricing model to calculate Greeks
            option.PriceModel = OptionPriceModels.BjerksundStensland()

            # initialize indicators
            self.manualSMA1[symbol] = SimpleMovingAverage(self.equity[symbol].Symbol, symbol_dict[symbol]['SMA'])
            self.manualSMA2[symbol] = SimpleMovingAverage(self.equity[symbol].Symbol, symbol_dict[symbol]['SMA'])
            self.autoSMA[symbol] = self.SMA(self.equity[symbol].Symbol, symbol_dict[symbol]['SMA'], Resolution.Daily)

            self.waitPeriod[symbol] = timedelta(0.5) + timedelta(symbol_dict[symbol]['waitPeriod'])
            self.nextEntryTime[symbol] = self.Time
            self.nextExitTime[symbol] = self.Time

            # trailing stop initialization
            self.trailingStop[symbol] = 0
            self.exitStop[symbol] = 0

            # calculates warm up period to be maximum SMA out of the equities
            warmUpPeriod = max(warmUpPeriod, symbol_dict[symbol]['SMA'])

            # needed for plotting each symbol
            stockPlot = Chart(f'{symbol} Plot')
            stockPlot.AddSeries(Series('Price', SeriesType.Candle, '$'))
            self.AddChart(stockPlot)

        # Generating consolidators
        consDaily = TradeBarConsolidator(Resolution.Daily)
        consDaily.DataConsolidated += self.OnDailyData
        self.SubscriptionManager.AddConsolidator(self.equity['SPY'].Symbol, consDaily)

        consSingleDay = TradeBarConsolidator(timedelta(1))
        consSingleDay.DataConsolidated += self.OnSingleDayData 
        self.SubscriptionManager.AddConsolidator(self.equity['SPY'].Symbol, consSingleDay)

        self.exchange = self.Securities[self.equity[symbol].Symbol].Exchange  # using to check when exchange is open

        # sets the warm up period
        self.SetWarmUp(timedelta(warmUpPeriod))

        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.At(9, 32), self.PlotLogDaily)


    def OnData(self, data: Slice):
        # checks if smas are ready or if no daily price
        if self.IsWarmingUp:
            return

        for symbol in symbol_dict.keys():
            #if not self.daily_bb[symbol].IsReady:
            #    continue
            if self.equityDailyPrice[symbol] is None:
                continue

            # runs only when exchange hours are open
            if self.exchange.ExchangeOpen:

                # checks for open option positions
                option_invested = [x for x, holding in self.Portfolio.items() if holding.Invested and holding.Type == SecurityType.Option and x.HasUnderlying and x.Underlying.Equals(self.equity[symbol].Symbol)]
                
                if option_invested:
                    # calculates trailing and exit stop:
                    self.trailingStop[symbol] = self.equityDailyPrice[symbol] * (1-symbol_dict[symbol]['trailingStop'])

                    self.exitStop[symbol] = max(self.exitStop[symbol], self.trailingStop[symbol])

                    # Exits if price is below the stop
                    if self.exitStop[symbol] > self.equity[symbol].Price:
                        if self.nextExitTime[symbol] <= self.Time:  # exit needs to wait a day after entry
                            pnl = self.Securities[option_invested[0]].Holdings.HoldingsValue - self.entryAmount[symbol]

                            orderMessage = "[Time] {0}; [Underlying] {1}; [Strike Price] {2}; [Contract Delta] {3}; [Contract Expiration] {4}; [Enter Trigger] {5:.2f}; [Enter Amount] {6:.2f}; [Portfolio Value] {7:.2f}; [Trailing Stop] {8:.2f}; [Exit Stop] {9:.2f}; [PNL] {10:.2f}".format(
                                    self.Time,
                                    self.equity[symbol].Price,
                                    self.call[symbol].Strike,
                                    self.call[symbol].Greeks.Delta,
                                    self.call[symbol].Expiry,
                                    self.autoSMA[symbol].Current.Value,
                                    self.entryAmount[symbol],
                                    self.Portfolio.TotalPortfolioValue,
                                    self.trailingStop[symbol],
                                    self.exitStop[symbol],
                                    pnl
                                )
                            self.Liquidate(option_invested[0], orderMessage)
                            self.Log(orderMessage)
                            self.nextEntryTime[symbol] = self.Time + self.waitPeriod[symbol]  # Waits at least until next day to re-enter

                            # resets all flags/counters
                            self.trailingStop[symbol] = 0
                            self.exitStop[symbol] = 0
                            self.entryAmount[symbol] = 0
                            self.entryStrike[symbol] = 0
                    
                    # If too close to expiration, liquidate and perform a time roll
                    elif self.Time + self.expirationThreshold > option_invested[0].ID.Date:
                        self.rollValue[symbol] = self.Securities[option_invested[0]].Holdings.HoldingsValue 
                        timeRollMessage = "[Time] {0}; [Contract Expiration] {1}; [Time Roll Amount] {2}".format(
                                            self.Time,
                                            self.call[symbol].Expiry,
                                            self.rollValue[symbol]
                                            )
                        self.Liquidate(option_invested[0], timeRollMessage)
                        self.Log(timeRollMessage)
                        self.isRollOut[symbol] = True

                        # determines the option chain and buys the option
                        for i in data.OptionChains:
                            chains = i.Value
                            self.BuyCall(chains, symbol)
                    
                # Attempts entry if no open options positions
                else:
                    # entry if above upper band
                    if self.autoSMA[symbol].Current.Value <= self.equity[symbol].Price:
                        if self.nextEntryTime[symbol] <= self.Time:
                            self.nextExitTime[symbol] = self.Time + self.exitPeriod
                            self.entryStrike[symbol] = self.autoSMA[symbol].Current.Value
                            for i in data.OptionChains:
                                chains = i.Value
                                self.BuyCall(chains, symbol)
                        
    
    def BuyCall(self, chains, symbol):
        # sorts the options chains and picks the furthest away between 30-60 days
        expiry = sorted(chains, key = lambda x: x.Expiry, reverse=True)[0].Expiry
        # filter out calls
        calls = [i for i in chains if i.Expiry == expiry and i.Right == OptionRight.Call]

        # finds contract closest to the requested delta with and without roll
        if self.isRollOut[symbol] is False:
            call_contracts = sorted(calls, key = lambda x: abs(x.Greeks.Delta - symbol_dict[symbol]['contractDelta']))
        elif self.isRollOut[symbol]:
            call_contracts = sorted(calls, key = lambda x: abs(x.Strike - self.entryStrike[symbol]))
        else:
            self.Debug("Roll flag error")
            return

        # saves first element to the self.call variable
        if len(call_contracts) == 0:
            return
        self.call[symbol] = call_contracts[0]
        
        try:
            # calculates trade amount
            if self.isRollOut[symbol] is False:
                self.tradeAmount[symbol] = symbol_dict[symbol]['initialAmount']
            else:
                self.tradeAmount[symbol] = self.rollValue[symbol]

            # determines quantity based on trade amount
            quantity = int((self.tradeAmount[symbol]) / self.call[symbol].AskPrice / 100)

            # updates trade amount based on quantity calculated
            self.tradeAmount[symbol] = quantity * self.call[symbol].AskPrice * 100

            if self.isRollOut[symbol] is False:
                self.entryAmount[symbol] = self.tradeAmount[symbol]

            buyLogMessage= "[Time] {0}; [Underlying] {1}; [Strike Price] {2}; [Contract Delta] {3}; [Contract Expiration] {4}; [Enter Trigger] {5:.2f}; [Enter Amount] {6:.2f}; [Portfolio Value] {7:.2f}".format(
                            self.Time,
                            self.equity[symbol].Price,
                            self.call[symbol].Strike,
                            self.call[symbol].Greeks.Delta,
                            self.call[symbol].Expiry,
                            self.autoSMA[symbol].Current.Value,
                            self.tradeAmount[symbol],
                            self.Portfolio.TotalPortfolioValue
                            )
            #self.Buy(self.call.Symbol, quantity)
            self.MarketOrder(self.call[symbol].Symbol, quantity, False, buyLogMessage)
            self.Log(buyLogMessage)
        except:
            self.Debug("error computing quantity") # this is done since I had run into some errors computing the quantity



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


    def OnDailyData(self, sender, bar):
        # updates indicator calculations and daily price on consolidated daily bar
        self.manualSMA1['SPY'].Update(bar.EndTime, bar.Close)
        self.equityDailyPrice['SPY'] = bar.Close
    

    def OnSingleDayData(self, sender, bar):
        self.manualSMA2['SPY'].Update(bar.EndTime, bar.Close)
        self.equitySingleDayPrice['SPY'] = bar.Close


    def PlotLogDaily(self):
        for symbol in symbol_dict.keys():
            try:
                dailyMessage = "[Time] {0}, [Symbol] {1}, [Manual SMA Daily] ${2:.2f}, [Daily Close] ${3:.2f}, [Manual SMA Single Day] ${4:.2f}, [Single Day Close] ${5:.2f}, [Auto SMA] ${6:.2f}".format(
                                self.Time,
                                symbol,
                                self.manualSMA1[symbol].Current.Value,
                                self.equityDailyPrice[symbol],
                                self.manualSMA2[symbol].Current.Value,
                                self.equitySingleDayPrice[symbol],
                                self.autoSMA[symbol].Current.Value
                                )
                self.Log(dailyMessage)    
                # plots the daily price and the entry line
                self.Plot(f'{symbol} Plot', 'Price', self.equityDailyPrice[symbol])
                self.Plot(f'{symbol} Plot', 'SMA', self.autoSMA[symbol].Current.Value)
            except:
                self.Debug("symbol not found")