Overall Statistics
Total Trades
184
Average Win
0.01%
Average Loss
-0.01%
Compounding Annual Return
-1.280%
Drawdown
0.200%
Expectancy
-0.054
Net Profit
-0.108%
Sharpe Ratio
-2.617
Probabilistic Sharpe Ratio
7.151%
Loss Rate
54%
Win Rate
46%
Profit-Loss Ratio
1.07
Alpha
-0.009
Beta
-0.001
Annual Standard Deviation
0.004
Annual Variance
0
Information Ratio
-2.714
Tracking Error
0.158
Treynor Ratio
6.976
Total Fees
$112.00
Estimated Strategy Capacity
$17000.00
Lowest Capacity Asset
SPY 325XSGU1JG9UU|SPY R735QTJ8XC9X
Portfolio Turnover
0.03%
#region imports
from AlgorithmImports import *
#endregion
from datetime import timedelta

class IronCondorAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2023, 3, 1)
        self.SetEndDate(2023, 3, 31)
        self.SetCash(100000) 
        self.equity = self.AddEquity("SPY", Resolution.Minute)
        
        self.InitOptionsAndGreeks(self.equity)

        # use the underlying equity SPY as the benchmark
        self.SetBenchmark(self.equity.Symbol)

        self.InitOptionsAndGreeks(self.equity)

        self.creditReceived = 0
        self.Leg1Cr = 0
        self.Leg2Db = 0
        self.Leg3Cr = 0
        self.Leg4Db = 0
        
    ## 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 least 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, data):
        self.OpenIronCondor()

        if self.Portfolio.Invested:
            self.Log("Open P&L: " + str(self.Portfolio.TotalUnrealizedProfit))

        # Check unrealized P&L against opening credit
        if self.Portfolio.TotalUnrealizedProfit < -(3* self.creditReceived * 100):
            self.Liquidate(tag="Stop Loss - 3x credit received")


    def OpenIronCondor(self):
        
        if self.Time.hour == 9 and self.Time.minute == 33 and not self.Portfolio.Invested:
            self.SellAnOTMPut5Delta()
            self.BuyAnOTMPut1Delta()
            self.SellAnOTMCall5Delta()
            self.BuyAnOTMCallDelta1Delta()
            self.creditReceived = (self.Leg1Cr - self.Leg2Db + self.Leg3Cr - self.Leg4Db)
            self.Log("Leg 1 Credit: " + str(self.Leg1Cr))
            self.Log("Leg 2 Debit: " + str(self.Leg2Db))
            self.Log("Leg 3 Credit: " + str(self.Leg3Cr))
            self.Log("Leg 4 Debit: " + str(self.Leg4Db))
            self.Log("Credit collected: " + str(self.creditReceived * 100))
            self.Log("Stop loss: " + str(-(3 * self.creditReceived * 100)))

    
    def SellAnOTMPut5Delta(self):
        
        ## Sell a 5 delta put expiring in today
        putContract = self.SelectContractByDelta(self.equity.Symbol, .05, 0, OptionRight.Put)
        
        ## construct an order message -- good for debugging and order rrecords
        orderMessage = f"Stock @ ${self.CurrentSlice[self.equity.Symbol].Close} | " + \
                       f"Sell {putContract.Symbol} "+ \
                       f"({round(putContract.Greeks.Delta, 2)} Delta)"
                       
        self.Debug(f"{self.Time} {orderMessage}")
        self.Order(putContract.Symbol, -1, False, orderMessage)
        self.Leg1Cr = putContract.AskPrice

    def BuyAnOTMPut1Delta(self):
        
        ## Buy a 1 delta put expiring in today
        putContract = self.SelectContractByDelta(self.equity.Symbol, .01, 0, OptionRight.Put)
        
        ## construct an order message -- good for debugging and order rrecords
        orderMessage = f"Stock @ ${self.CurrentSlice[self.equity.Symbol].Close} | " + \
                       f"Buy {putContract.Symbol} "+ \
                       f"({round(putContract.Greeks.Delta, 2)} Delta)"
                       
        self.Debug(f"{self.Time} {orderMessage}")
        self.Order(putContract.Symbol, 1, False, orderMessage)
        self.Leg2Db = putContract.BidPrice

    def SellAnOTMCall5Delta(self):
        
        ## Sell a 5 delta call expiring in today
        callContract = self.SelectContractByDelta(self.equity.Symbol, .05, 0, OptionRight.Call)
        
        ## construct an order message -- good for debugging and order rrecords
        orderMessage = f"Stock @ ${self.CurrentSlice[self.equity.Symbol].Close} | " + \
                       f"Sell {callContract.Symbol} "+ \
                       f"({round(callContract.Greeks.Delta, 2)} Delta)"
                       
        self.Debug(f"{self.Time} {orderMessage}")
        self.Order(callContract.Symbol, -1, False, orderMessage)  
        self.Leg3Cr = callContract.AskPrice  

    def BuyAnOTMCallDelta1Delta(self):
        
        ## Buy a 1 delta call expiring in today
        callContract = self.SelectContractByDelta(self.equity.Symbol, .01, 0, OptionRight.Call)
        
        ## construct an order message -- good for debugging and order rrecords
        orderMessage = f"Stock @ ${self.CurrentSlice[self.equity.Symbol].Close} | " + \
                       f"Buy {callContract.Symbol} "+ \
                       f"({round(callContract.Greeks.Delta, 2)} Delta)"
                       
        self.Debug(f"{self.Time} {orderMessage}")
        self.Order(callContract.Symbol, 1, False, orderMessage)
        self.Leg4Db = callContract.BidPrice


    ## Get an options contract that matches the specified criteria:
    ## Underlying symbol, delta, days till expiration, Option right (put or call)
    ## ============================================================================
    def SelectContractByDelta(self, symbolArg, strikeDeltaArg, expiryDTE, optionRightArg= OptionRight.Call):

        canonicalSymbol = self.AddOption(symbolArg)
        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]
    
        ## Get the contract with the contract with the closest delta
        closestContract = min(contractsMatchingExpiryDTE, key=lambda x: abs(abs(x.Greeks.Delta)-strikeDeltaArg))

        return closestContract


    ## The options filter function.
    ## Filter the options chain so we only have relevant strikes & expiration dates. 
    ## =============================================================================
    def OptionsFilterFunction(self, optionsContractsChain):

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

    
    # Liquidate before options are exercised
    def OnOrderEvent(self, orderEvent):
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        if order.Type == OrderType.OptionExercise:
            self.Liquidate()