Overall Statistics
Total Orders
4
Average Win
0%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0%
Expectancy
0
Start Equity
100000
End Equity
121745
Net Profit
0%
Sharpe Ratio
0
Sortino Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
0
Tracking Error
0
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$1700000.00
Lowest Capacity Asset
SPXW 3281PG4IW5MR2|SPX 31
Portfolio Turnover
14.88%
# region imports
from AlgorithmImports import *
# endregion

class SPXExample(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2023, 6, 1)
        self.SetEndDate(2023, 6, 1)
        self.SetCash(100000)
        self.AmountToSpendOnOptions = 5000
        self.EnableZeroDayOptions = True
        self.LogOptions = True

        # SPX weekly options, including 0dte
        self.spx = self.add_index("SPX").Symbol
        self.SPXoption = self.AddIndexOption(self.spx, "SPXW")
        self.SPXoption.SetFilter(self.OptionFilter)

        self.NeedToBuy = True
        self.CallOptionType = 1
        self.PutOptionType = 2

    def OnData(self, data: Slice):
        # Buy once.  Buy a call and a put.
        if self.NeedToBuy:
            self.NeedToBuy = False
            # Buy call by Delta, closest to 0.70
            self.BuyOptionByDelta(self.CallOptionType, self.SPXoption, 0.70)
            # Buy put by Delta, closest to 0.50
            self.BuyOptionByDelta(self.PutOptionType, self.SPXoption, 0.50)
            # Buy call by Price, closest to 2.00
            self.BuyOptionByPrice(self.CallOptionType, self.SPXoption, 2)
            # Buy put by Price, closest to 2.00
            self.BuyOptionByPrice(self.PutOptionType, self.SPXoption, 2)


    def OptionFilter(self, universe):
        # Note that we're limiting expirations to within 0-7 days here.  If you need later this should be adjusted.
        return universe.IncludeWeeklys().Strikes(-10, 10).Expiration(0, 7)

    def BuyOptionByPrice(self, OptionType, underlyingOptions, price):
        self.BuyOption(OptionType, underlyingOptions, price, False)
    def BuyOptionByDelta(self, OptionType, underlyingOptions, delta):
        self.BuyOption(OptionType, underlyingOptions, delta, True)

    def BuyOption(self, OptionType, underlyingOptions, deltaprice, BuyOptionsByDelta = True):
        chain = self.CurrentSlice.OptionChains.get(underlyingOptions.Symbol)
        if chain is None:
            self.Log("Chain error!")
            return

        # First grab expiry dates
        expiry_dates = [contract.Expiry.date() for contract in chain]
        expiry_dates = sorted(set(expiry_dates))
        
        # If you explicitly don't want an option that expires today you can pick another date.
        # Here we just grab the next expiry if 0dte is allowed, otherwise we filter for dates after today.
        next_expiry = None
        if self.EnableZeroDayOptions:
            next_expiry = expiry_dates[0]
        else:
            expiries = [date for date in expiry_dates if date > self.Time.date()]
            if not expiries:
                return
            next_expiry = expiries[0]
        
        
        # Filter contracts for the next expiry date and are call options
        all_options = None
        desired_delta = deltaprice
        if OptionType == self.CallOptionType:
            # Call
            all_options = [contract for contract in chain if contract.Expiry.date() == next_expiry and contract.Right == OptionRight.Call]
        else:
            # Put
            all_options = [contract for contract in chain if contract.Expiry.date() == next_expiry and contract.Right == OptionRight.Put]
            # reverse delta for puts
            if desired_delta > 0:
                desired_delta = -desired_delta
        if not all_options:
            return
        
        # Select the option to BUY with delta closest to the desired delta
        # Result is put into self.selected_option
        if BuyOptionsByDelta:
            self.SelectOptionByDelta(all_options, desired_delta)
        else:
            self.SelectOptionByPrice(all_options, deltaprice)

        
        # Find the last prices for the options.
        option_price = None
        if self.selected_option.Symbol in self.CurrentSlice.Bars:
            option_contract_data = self.CurrentSlice.Bars[self.selected_option.Symbol]
            option_price = option_contract_data.Close

        # Now select a number of contracts based on how much we want to invest
        numcontracts = math.floor(self.AmountToSpendOnOptions/(option_price*100))
        numcontracts = max(1,numcontracts)
        numcontracts = min(300,numcontracts)
        self.MarketOrder(self.selected_option.Symbol, numcontracts)

        # Log option details.
        if self.LogOptions:
            self.LogOption(self.CallOptionType, option_price)


    def SelectOptionByDelta(self, all_options, desired_delta):
        closest_option = None
        closest_delta_diff = float('inf')
        for option in all_options:
            delta_diff = abs(option.Greeks.Delta - desired_delta)
            #self.Log(f"Delta: {option.Greeks.Delta}, diff: {delta_diff}")
            if delta_diff < closest_delta_diff:
                closest_delta_diff = delta_diff
                closest_option = option
                #self.Log("selected")
        self.selected_option = closest_option

    def SelectOptionByPrice(self, all_options, desired_price):
        closest_option = None
        closest_price_diff = float('inf')

        for option in all_options:
            mid_price = (option.BidPrice + option.AskPrice) / 2
            price_diff = abs(mid_price - desired_price)
            if price_diff < closest_price_diff:
                closest_price_diff = price_diff
                closest_option = option
            elif price_diff == closest_price_diff:
                # If price difference is the same, select the lowest strike call or highest strike put
                if (closest_option.Right == OptionRight.Call and option.Strike < closest_option.Strike):
                    closest_option = option
                elif (closest_option.Right == OptionRight.Put and option.Strike > closest_option.Strike):
                    closest_option = option
        self.selected_option = closest_option

    def LogOption(self, optiontype, lastoptionprice):
        # The last option selected should be in self.selected_option so we'll log details of that.
        # If we wanted to find the actual price we bought at, we probably would need to put that in an OnOrderEvent function.
        opt = "CALL"
        if optiontype == self.PutOptionType:
            opt = "PUT"
        strike_price = self.selected_option.Strike
        delta = self.selected_option.Greeks.Delta
        expiry_date = self.selected_option.Expiry
        self.Log(f"{opt} Strike: {strike_price}, Delta: {delta}, Expiry: {expiry_date}, Last Price: {lastoptionprice}")