Overall Statistics
Total Trades
44
Average Win
0.20%
Average Loss
-0.09%
Compounding Annual Return
-1.514%
Drawdown
1.900%
Expectancy
0.546
Net Profit
-0.122%
Sharpe Ratio
-0.152
Probabilistic Sharpe Ratio
36.064%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
2.09
Alpha
0.419
Beta
-0.62
Annual Standard Deviation
0.067
Annual Variance
0.004
Information Ratio
-6.453
Tracking Error
0.109
Treynor Ratio
0.016
Total Fees
$44.00
Estimated Strategy Capacity
$24000000.00
Lowest Capacity Asset
FB 30I1FG1ACLNRA|FB V6OIPNZEM8V9
from scipy.stats import norm
from datetime import timedelta

class IronCondorAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2017, 2, 1)
        self.SetEndDate(2017, 3, 1)
        self.SetCash(100000)
        
        self.SetSecurityInitializer(self.CustomSecurityInitializer)
        
        # Add equities
        self.AddUniverse(self.Universe.DollarVolume.Top(5))
        [self.AddEquity(symbol, Resolution.Minute) for symbol in ["GOOG", "SPY", "VCR", "SLY", "EEM", "XLP", "ARKK", "XLY", "AAPL"]]
        self.activeSymbols = []
        
        # use the underlying equity GOOG as the benchmark
        self.SetBenchmark("SPY")
        self.SetWarmUp(1)
        # Set trading intervals
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen("SPY", 1), self.TradeOptions)
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(12, 0), self.TradeOptions)
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.BeforeMarketClose("SPY", 10), self.TradeOptions)
    
    def CustomSecurityInitializer(self, security):
        security.SetDataNormalizationMode(DataNormalizationMode.Raw)
        security.SetMarketPrice(self.GetLastKnownPrice(security))
        
    def OnSecuritiesChanged(self, changes):
        for change in changes.AddedSecurities:
            if change.Type == SecurityType.Equity:
                change.SetLeverage(1)
                self.activeSymbols.append(change.Symbol)
                
        for change in changes.RemovedSecurities:
            if change.Symbol in self.activeSymbols:
                self.activeSymbols.remove(change.Symbol)
                
    def OnOrderEvent(self, orderEvent):
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        if order.Type == OrderType.OptionExercise:
            self.Liquidate(orderEvent.Symbol.Underlying)

    def TradeOptions(self):
        if self.Portfolio.Invested:
            self.AdjustOptions()
        if not self.Portfolio.Invested and self.Time.hour != 0 and self.Time.minute != 0: 
            self.AddToPortfolio()
    
    def CloseCondor(self, x):
        #CCC = Close Condor Called
        self.Debug("CCC")
        for symbol in x:
            self.Liquidate(symbol.Symbol)
    
    def AdjustOptions(self):
        if self.Portfolio.Invested:
            for symbol, x in self.condor_list.items():
                days_to_expiry = abs(x[0].Expiry - self.Time).days
                # if this condor expires in 25+ days, leave it condor
                if days_to_expiry > 25:
                    continue
                elif days_to_expiry < 3.75:
                    self.CloseCondor(x)
                    
                # OTM Check
                otm = True
                for c in x:
                    if c.Right == 1 and self.Securities[symbol].Price - c.StrikePrice < 0:
                        otm = False
                    elif c.Right == 0 and self.Securities[symbol].Price - c.StrikePrice > 0:
                        otm = False
                # If Condor is in the head region, close
                if days_to_expiry <= 25 and otm:
                    self.CloseCondor(x)
        
    def AddToPortfolio(self):
        self.condor_list = {}
        
        for symbol in self.activeSymbols:
            contracts = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
            # option universe filter
            contracts = self.InitialFilter(symbol, contracts, -15, 15, 35, 50)
            if not contracts: continue
            
            # sorted the optionchain by expiration date and choose the furthest date
            expiry = sorted(contracts, key = lambda x: x.ID.Date, reverse=True)[0].ID.Date
            
            # get puts
            puts = [contract for contract in contracts if contract.ID.Date == expiry \
                                                        and contract.ID.OptionRight == OptionRight.Put \
                                                        and contract.ID.StrikePrice < self.Securities[symbol].Price]
            if len(puts) <= 2: continue
            put_contracts = sorted(puts, key = lambda x: x.ID.StrikePrice)
            
            # get calls
            calls = [contract for contract in contracts if contract.ID.Date == expiry \
                                                        and contract.ID.OptionRight == OptionRight.Call \
                                                        and contract.ID.StrikePrice > self.Securities[symbol].Price]
            if len(calls) <= 2: continue
            call_contracts = sorted(calls, key = lambda x: x.ID.StrikePrice) 
            
            # get and subscribe to the option contracts
            otm_put_lower = self.AddOptionContract(put_contracts[0], Resolution.Minute)
            otm_put = self.AddOptionContract(put_contracts[-1], Resolution.Minute)
            otm_call = self.AddOptionContract(call_contracts[0], Resolution.Minute)
            otm_call_higher = self.AddOptionContract(call_contracts[-1], Resolution.Minute)
            self.condor_list[symbol] = [otm_call, otm_call_higher, otm_put, otm_put_lower]
            
            # get the margin requirement
            totalPrice = (otm_call_higher.AskPrice + otm_put_lower.AskPrice + otm_put.BidPrice + otm_call.BidPrice) * 100
            margin = self.Portfolio.GetMarginRemaining(symbol, OrderDirection.Buy)
            if margin > totalPrice * 4:
                self.Buy(otm_put_lower.Symbol ,1)
                self.Sell(otm_put.Symbol ,1)
                self.Sell(otm_call.Symbol ,1)
                self.Buy(otm_call_higher.Symbol ,1)
                
    def InitialFilter(self, underlyingsymbol, symbol_list, min_strike_rank, max_strike_rank, min_expiry, max_expiry):
        ''' This method is an initial filter of option contracts
            based on the range of strike price and the expiration date 
            https://www.quantconnect.com/tutorials/applied-options/iron-condor'''
        if len(symbol_list) == 0 : return
        # fitler the contracts based on the expiry range
        contract_list = [i for i in symbol_list if min_expiry <= (i.ID.Date.date() - self.Time.date()).days <= max_expiry]
        # find the strike price of ATM option
        atm_strike = sorted(contract_list,
                            key = lambda x: abs(x.ID.StrikePrice - self.Securities[underlyingsymbol].Price))[0].ID.StrikePrice
        strike_list = sorted(set([i.ID.StrikePrice for i in contract_list]))
        # find the index of ATM strike in the sorted strike list
        atm_strike_rank = strike_list.index(atm_strike)
        try: 
            min_strike = strike_list[atm_strike_rank + min_strike_rank + 1]
            max_strike = strike_list[atm_strike_rank + max_strike_rank - 1]

        except:
            min_strike = strike_list[0]
            max_strike = strike_list[-1]
            
        filtered_contracts = [i for i in contract_list if i.ID.StrikePrice >= min_strike \
                                                        and i.ID.StrikePrice <= max_strike]

        return filtered_contracts