Overall Statistics
#region imports
from AlgorithmImports import *
#endregion

class IronCondorAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetEndDate(2010, 12, 31)
        self.SetCash(10000)
        
        


        #option = self.AddOption("SPY")
        
        self.equity = self.AddEquity('SPY', Resolution.Minute)
        self.symbol = self.equity.Symbol

        self.expiry = datetime(2300, 5, 17)


        self.InitOptionsAndGreeks(self.equity)

        #option.SetOptionAssignmentModel(NullOptionAssignmentModel())
        
        # use the underlying equity GOOG as the benchmark
        self.SetBenchmark(self.symbol)

        self.long_call_delta = 0.1
        self.short_call_delta = 0.25
        self.short_put_delta = 0.25
        self.long_put_delta = 0.1

        self.DTE = 45

        self.strikeCount  = 500 # no of strikes around underyling price => for universe selection
        self.minExpiryDTE = 40  # min num of days to expiration => for uni selection
        self.maxExpiryDTE = 50  # max num of days to expiration => for uni selection


    def SelectContractByDelta(self, symbolArg, strikeDeltaArg, expiryDTE, optionRightArg):

        canonicalSymbol = self.AddOption(symbolArg)
        self.canonicalSymbol = canonicalSymbol
        if (self.CurrentSlice.OptionChains.values().__len__()>0):
            theOptionChain  = self.CurrentSlice.OptionChains[canonicalSymbol.Symbol]
            theExpiryDate   = self.Time + timedelta(days=expiryDTE)
            
            ## Filter the Call/Put options contracts
            filteredContracts = [x for x in theOptionChain if x.Right == optionRightArg] 

            ## Sort the contracts according to their closeness to our desired expiry
            contractsSortedByExpiration = sorted(filteredContracts, key=lambda p: abs(p.Expiry - theExpiryDate), reverse=False)
            closestExpirationDate = contractsSortedByExpiration[0].Expiry                                        
                                                
            ## Get all contracts for selected expiration
            
            contractsMatchingExpiryDTE = [contract for contract in contractsSortedByExpiration if contract.Expiry == closestExpirationDate]

            if not contractsMatchingExpiryDTE:
                self.Debug("No contracts found matching the closest expiration date.")
                return None

            ## Get the contract with the contract with the closest delta
            closestContract = min(contractsMatchingExpiryDTE, key=lambda x: abs(abs(x.Greeks.Delta)-strikeDeltaArg))

            return closestContract    
    
    def OptionsFilterFunction(self, optionsContractsChain):

        strikeCount  = self.strikeCount # no of strikes around underyling price => for universe selection
        minExpiryDTE = self.minExpiryDTE  # min num of days to expiration => for uni selection
        maxExpiryDTE = self.maxExpiryDTE  # max num of days to expiration => for uni selection
        
        return optionsContractsChain.IncludeWeeklys()\
                                    .Strikes(-strikeCount, strikeCount)\
                                    .Expiration(timedelta(minExpiryDTE), timedelta(maxExpiryDTE))


    ## Initialize Options settings, chain filters, pricing models, etc
    ## ====================================================================
    def InitOptionsAndGreeks(self, theEquity ):

        ## 1. Specify the data normalization mode (must be 'Raw' for options)
        theEquity.SetDataNormalizationMode(DataNormalizationMode.Raw)
        
        ## 2. Set Warmup period of at leasr 30 days
        self.SetWarmup(30, Resolution.Daily)

        ## 3. Set the security initializer to call SetMarketPrice
        self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x)))

        ## 4. Subscribe to the option feed for the symbol
        theOptionSubscription = self.AddOption(theEquity.Symbol)

        ## 5. set the pricing model, to calculate Greeks and volatility
        theOptionSubscription.PriceModel = OptionPriceModels.CrankNicolsonFD()  # both European & American, automatically
                
        ## 6. Set the function to filter out strikes and expiry dates from the option chain
        theOptionSubscription.SetFilter(self.OptionsFilterFunction)

    def OnData(self,slice):

        #for security in self.Securities.Values:
        #    if security.Invested and security.Symbol.SecurityType == SecurityType.Option:
        #        days_till_expiry = (security.Expiry - self.Time).days
        #        #self.Log(f'Days till expiry: {days_till_expiry}')
        #        
        #        trading_days_till_expiry = list(self.TradingCalendar.GetDaysByType(TradingDayType.BusinessDay, self.Time, self.Time + (security.Expiry - self.Time)))
        #        #self.Log(f'Trading Days till expiry: {len(trading_days_till_expiry)}')
        #        
        #        # do buying/selling logic here
        #        if days_till_expiry < 0: # 0 = disable
        #            self.Liquidate()

        days_till_expiry = (self.expiry - self.Time).days
        if days_till_expiry < 10: # 0 = disable
            self.Liquidate()
        # If there is underlying assets in portfolio at expiration, liquidate the stocks in order to roll into new contracts
        if self.Portfolio[self.symbol].Invested:
            self.Liquidate()
        
        if self.Portfolio.Invested or not self.IsMarketOpen(self.symbol):
            return
        

        # Select the strikes in the strategy legs
        
        far_put = self.SelectContractByDelta(self.equity.Symbol,self.long_put_delta,self.DTE,OptionRight.Put)
        near_put = self.SelectContractByDelta(self.equity.Symbol,self.short_put_delta,self.DTE,OptionRight.Put)
        near_call = self.SelectContractByDelta(self.equity.Symbol,self.short_call_delta,self.DTE,OptionRight.Call)
        far_call = self.SelectContractByDelta(self.equity.Symbol,self.long_call_delta,self.DTE,OptionRight.Call)
       # expiry = self.SelectContractByDelta(self.equity.Symbol,self.long_call_delta,self.DTE,OptionRight.Call).Expiry

        self.expiry = datetime(2300, 5, 17)
        if not far_call is None:
            self.expiry = far_call.Expiry

        try:
            self.iron_condor = OptionStrategies.IronCondor(
                self.canonicalSymbol.Symbol, 
                far_put.Strike,
                near_put.Strike,
                near_call.Strike,
                far_call.Strike,
                far_call.Expiry)

            self.Sell(self.iron_condor, 1)
            self.Debug(f"{self.Time}: Selling IC {far_put.Strike} - {near_put.Strike} | {near_put.UnderlyingLastPrice} | {near_call.Strike} - {far_call.Strike}, for {near_put.BidPrice + near_call.BidPrice - far_call.AskPrice - far_put.AskPrice}")
        except:
            #self.Debug(f"{self.Time}: Error with the following strikes:{far_put}, {near_put}, {near_call}, {far_call}. Skipping. ")
            return

    #def OnOrderEvent(self, orderEvent):
    #    if orderEvent.Status == 3:
    #        self.Debug(orderEvent)