Overall Statistics
Total Trades
35
Average Win
12.97%
Average Loss
-4.34%
Compounding Annual Return
10.231%
Drawdown
73.400%
Expectancy
0.680
Net Profit
266.888%
Sharpe Ratio
0.561
Probabilistic Sharpe Ratio
0.316%
Loss Rate
58%
Win Rate
42%
Profit-Loss Ratio
2.99
Alpha
0.497
Beta
1.076
Annual Standard Deviation
1.077
Annual Variance
1.16
Information Ratio
0.479
Tracking Error
1.054
Treynor Ratio
0.562
Total Fees
$11380.53
from QuantConnect.Securities.Option import *
from datetime import timedelta

def RebalanceUnderlying(self, shares = None):
    
    ''' Rebalance holdings for the underlying asset '''

    if self.tradingLogs:
        self.Log('information before rebalancing underlying'
        + '; MarginRemaining: ' + str(self.Portfolio.MarginRemaining)
        + '; TotalPortfolioValue: ' + str(self.Portfolio.TotalPortfolioValue)
        + '; Underlying HoldingsValue: ' + str(self.Portfolio[self.underlyingSymbol].HoldingsValue)
        + '; Cash: ' + str(self.Portfolio.Cash))
    
    # calculate the new target percent for the underlying
    if shares is None:
        shares = int(self.Portfolio.Cash / self.Securities[self.underlyingSymbol].Price)
        self.MarketOrder(self.underlyingSymbol, shares, False, str('Rebalancing Underlying ' + self.specialTag))
    else:
        self.MarketOrder(self.underlyingSymbol, shares, False, str('Rebalancing Underlying ' + self.specialTag))
    
    self.specialTag = ''
    
    if self.tradingLogs:
        self.Log('information after rebalancing underlying'
        + '; MarginRemaining: ' + str(self.Portfolio.MarginRemaining)
        + '; TotalPortfolioValue: ' + str(self.Portfolio.TotalPortfolioValue)
        + '; Underlying HoldingsValue: ' + str(self.Portfolio[self.underlyingSymbol].HoldingsValue)
        + '; Cash: ' + str(self.Portfolio.Cash))
    
def EnterOptionContracts(self, expiryGroup, ticker, expiryGroupSymbol, maxExpiryDays, dictCalls, dictPuts):
    
    ''' Enter option contracts '''
    
    if self.checkNextDay:
        return False
    
    # get only the valid calls/puts for which we actually want to trade
    dictValidCalls = {key: value for key, value in dictCalls.items() if value[1] is not None and value[1] != 0}
    dictValidPuts = {key: value for key, value in dictPuts.items() if value[1] is not None and value[1] != 0}
    
    # get dictionaries with relevant contracts for calls and puts
    try:
        dictContracts = GetTradingContracts(self, expiryGroupSymbol, maxExpiryDays, dictValidCalls, dictValidPuts)
    except BaseException as e:
        if self.tradingLogs:
            self.Log('GetTradingContracts function failed due to: ' + str(e))
        dictContracts = {'calls': {}, 'puts': {}}

    # create a list with all the contracts for calls and puts to added and traded
    listContracts = list(dictContracts['calls'].values()) + list(dictContracts['puts'].values())
    
    if len(listContracts) == 0:
        return False
    
    # loop through filtered contracts and add them to get data
    for contract in listContracts:
        option = self.AddOptionContract(contract, Resolution.Minute)
        option.PriceModel = OptionPriceModels.CrankNicolsonFD() # apply options pricing model
        CustomSecurityInitializer(self, self.Securities[contract])

    # check the validity of the contracts
    validContracts = CheckContractValidity(self, listContracts, expiryGroup, ticker)
    if not validContracts:
        return False

    # separate long/short calls/puts
    dictLongs, dictShorts = {}, {}
    dictLongs['calls'] = {key: value for key, value in dictValidCalls.items() if value[1] > 0}
    dictLongs['puts'] = {key: value for key, value in dictValidPuts.items() if value[1] > 0}
    dictShorts['calls'] = {key: value for key, value in dictValidCalls.items() if value[1] < 0}
    dictShorts['puts'] = {key: value for key, value in dictValidPuts.items() if value[1] < 0}
    
    # calculate the number of option contracts to trade
    numberOfContracts = CalculateNumberOfOptionContracts(self, expiryGroupSymbol, expiryGroup, ticker,
                                                        dictContracts, dictLongs, dictShorts)
    
    # check notional ratio
    self.notionalRatio = 0
    notionalCoverage = numberOfContracts * 100
    if expiryGroupSymbol in self.Portfolio.Keys:
        underlyingShares = self.Portfolio[expiryGroupSymbol].Quantity
    else:
        underlyingShares = 0
        
    if underlyingShares > 0:
        self.notionalRatio = notionalCoverage / underlyingShares
        
        if self.notionalRatio < self.minNotionalRatio:
            self.Log('notionalRatio is below minNotionalRatio; ' + str(self.notionalRatio) + ' vs ' + str(self.minNotionalRatio))
            self.checkNextDay = True
            return False
                    
    if numberOfContracts < 1:
        self.specialTag = '(' + expiryGroup + ' trade missing since < 1 contract on ' + str(self.Time.date()) + ')'
        if self.tradingLogs:
            self.Log(expiryGroup + '/' + ticker + ': numberOfContracts is less than 1')

    # entering legs ------------------------------------------------------------
    
    # start with short positions to get the premium
    for optionSide, strikeGroups in dictShorts.items():
        for strikeGroup, value in strikeGroups.items():
            self.MarketOrder(dictContracts[optionSide][strikeGroup], numberOfContracts * value[1], False,
                            expiryGroup + '; short ' + optionSide + '; strike ' + '{:.0%}'.format(value[0])
                            + ' vs atm; notional ratio ' + '{:.0%}'.format(self.notionalRatio))
                            
    # long positions
    for optionSide, strikeGroups in dictLongs.items():
        for strikeGroup, value in strikeGroups.items():
            self.MarketOrder(dictContracts[optionSide][strikeGroup], numberOfContracts * value[1], False,
                            expiryGroup + '; long ' + optionSide + '; strike ' + '{:.0%}'.format(value[0])
                            + ' vs atm; notional ratio ' + '{:.0%}'.format(self.notionalRatio))
    
    # information for allContractsByExpiryGroup --------------------------------
    
    # save the date when we enter the positions
    entryDate = self.Time
    
    # get the next expiry date
    nextExpiryDate = listContracts[0].ID.Date

    # check if we have calls/puts or bo
    if dictValidCalls and dictValidPuts:
        legs = 'both'
    elif dictValidCalls and not dictValidPuts:
        legs = 'calls'
    elif not dictValidCalls and dictValidPuts:
        legs = 'puts'
    else:
        legs = 'calls'
    
    # save the underlying price at entry
    underlyingPriceAtEntry = self.Securities[expiryGroupSymbol].Price
    
    # save relevant information in the dictionary allContractsByExpiryGroup
    self.allContractsByExpiryGroup[expiryGroup] = [entryDate, nextExpiryDate, legs, underlyingPriceAtEntry, listContracts]
    
    if self.tradingLogs:
        self.Log(expiryGroup + '/' + ticker + ': entering new option contracts for next period; nextExpiryDate: ' + str(nextExpiryDate))
    
    return True
    
def CalculateNumberOfOptionContracts(self, expiryGroupSymbol, expiryGroup, ticker,
                                    dictContracts, dictLongs, dictShorts):
                                    
    ''' Calculate the number of option contracts '''
    
    # get numerator
    base = 30
    days = self.dictParameters[expiryGroup]['rollMaxExpiryDays']
    daysNearestMultiple = base * round(days / base)
    budget = self.annualBudget / (365 / daysNearestMultiple)
    numerator = (budget * (self.Portfolio[expiryGroupSymbol].HoldingsValue + self.Portfolio.Cash)) / (100 * self.numberOfActiveExpiryGroups)
    
    # rebalancing underlying to make sure cash and underlying holdings are well balanced
    if self.Portfolio[expiryGroupSymbol].HoldingsValue > 0:
        budgetOptions = numerator * 100
        cashImbalance = self.Portfolio.Cash - budgetOptions
        if cashImbalance < 0:
            shares = round(cashImbalance / self.Securities[expiryGroupSymbol].Price) - 1
        else:
            shares = int(cashImbalance / self.Securities[expiryGroupSymbol].Price)
        
        if self.tradingLogs:
            self.Log('rebalancing underlying due to cash imbalance'
            + '; budgetOptions: ' + str(budgetOptions)
            + '; Cash: ' + str(self.Portfolio.Cash)
            + '; cashImbalance: ' + str(cashImbalance)
            + '; shares: ' + str(shares))
        
        RebalanceUnderlying(self, shares)
    
    # get denominator
    # sum product of multipliers and prices (we split into longs/shorts to correctly apply AskPrice/BidPrice)
    sumProdLongCalls = sum([value[1] * self.Securities[dictContracts['calls'][key]].AskPrice for key, value in dictLongs['calls'].items()])
    sumProdLongPuts = sum([value[1] * self.Securities[dictContracts['puts'][key]].AskPrice for key, value in dictLongs['puts'].items()])
    sumProdShortCalls = sum([value[1] * self.Securities[dictContracts['calls'][key]].BidPrice for key, value in dictShorts['calls'].items()])
    sumProdShortPuts = sum([value[1] * self.Securities[dictContracts['puts'][key]].BidPrice for key, value in dictShorts['puts'].items()])
    
    sumProdCallsPuts = sumProdLongCalls + sumProdLongPuts + sumProdShortCalls + sumProdShortPuts
    
    # get number of contracts
    numberOfContracts = numerator / sumProdCallsPuts
        
    return numberOfContracts
    
def LiquidateOptionContracts(self, expiryGroup, ticker, openContracts, tag = 'no message'):
    
    ''' Liquidate any open option contracts '''

    # check the validity of the contracts
    validContracts = CheckContractValidity(self, openContracts, expiryGroup, ticker)
    if not validContracts:
        return False
        
    if self.tradingLogs:
        openOptionContracts = GetOpenOptionContracts(self)
        self.Log('open option contracts and HoldingsValue before liquidating: '
                + str({self.Securities[contract].Symbol.Value: self.Portfolio[contract].HoldingsValue for contract in openOptionContracts}))
    
    for contract in openContracts:
        self.Liquidate(contract, 'Liquidated - ' + expiryGroup + ' ' + tag)
        self.RemoveSecurity(contract)
        self.lastMinutePricesDict.pop(contract, None)
    
    if self.tradingLogs:
        self.Log(expiryGroup + '/' + ticker + ': liquidating due to ' + tag)
        
    return True

def CheckContractValidity(self, contracts, expiryGroup, ticker):
    
    ''' Check the validity of the contracts '''
    
    for contract in contracts:
        contractId = str(self.Securities[contract].Symbol).replace(' ', '')

        # this is to remove specific option contracts above a certain price
        if (contractId in self.avoidContractsWithPrice
        and (self.Securities[contract].AskPrice > self.avoidContractsWithPrice[contractId]
        or self.Securities[contract].BidPrice > self.avoidContractsWithPrice[contractId])):
            if contractId not in self.dataChecksDict['contractAboveLimitPrice']:
                self.dataChecksDict['contractAboveLimitPrice'].update({contractId: [self.Time]})
            else:
                self.dataChecksDict['contractAboveLimitPrice'][contractId].append(self.Time)

            return False
        
        elif self.Securities[contract].AskPrice == 0 or self.Securities[contract].BidPrice == 0:
            if contractId not in self.dataChecksDict['contractPriceZero']:
                self.dataChecksDict['contractPriceZero'].update({contractId: [self.Time]})
            else:
                self.dataChecksDict['contractPriceZero'][contractId].append(self.Time)
            
            return False
            
    return True
        
def GetOpenOptionContracts(self):
    
    ''' Get any open option contracts '''

    return [x.Symbol for x in self.ActiveSecurities.Values if x.Invested and x.Type == SecurityType.Option]

def GetTradingContracts(self, expiryGroupSymbol, maxExpiryDays, dictCalls, dictPuts):
    
    ''' Get the final option contracts to trade '''
    
    # get a list with the option chain for the underlying symbol and the current date
    optionContracts = self.OptionChainProvider.GetOptionContractList(expiryGroupSymbol, self.Time.date())
    if len(optionContracts) == 0:
        if self.Time.date() not in self.dataChecksDict['emptyOptionContracts']:
            self.dataChecksDict['emptyoptionContracts'].update({self.Time.date(): 'emptyOptionContracts'})

        return {'calls': {}, 'puts': {}}
    
    strikePercentsForCalls = {key: value[0] for key, value in dictCalls.items()}
    strikePercentsForPuts = {key: value[0] for key, value in dictPuts.items()}
    
    # get calls and puts contracts after filtering for expiry date and strike prices
    calls = FilterOptionContracts(self, optionSide = 'calls', symbol = expiryGroupSymbol, contracts = optionContracts,
                                strikePercents = strikePercentsForCalls, maxExpiryDays = maxExpiryDays)
    puts = FilterOptionContracts(self, optionSide = 'puts', symbol = expiryGroupSymbol, contracts = optionContracts,
                                strikePercents = strikePercentsForPuts, maxExpiryDays = maxExpiryDays)

    dictContracts = {'calls': calls, 'puts': puts}
        
    return dictContracts

def FilterOptionContracts(self, optionSide, symbol, contracts, strikePercents, maxExpiryDays):
    
    '''
    Description:
        Filter a list of option contracts using the below arguments
    Args:
        optionSide: Puts/Calls
        symbol: Relevant symbol
        contracts: List of option contracts
        strikePercents: Dictionary with strike percents
        maxExpiryDays: Number of days to find the expiration date of the contracts
    Return:
        A dictionary with the option contract for each strike percent
    '''
    
    # avoid specific contracts before filtering
    contracts = [x for x in contracts if x.Value.replace(' ', '') not in self.avoidContracts 
                                        and x.Value.replace(' ', '')[:7] not in self.avoidContracts
                                        and x.Value.replace(' ', '')[:9] not in self.avoidContracts
                                        and x.Value.replace(' ', '')[:10] not in self.avoidContracts]
    
    # fitler the contracts with expiry date below maxExpiryDays
    if self.onlyMonthlies:
        contractList = [i for i in contracts if OptionSymbol.IsStandardContract(i) and (i.ID.Date.date() - self.Time.date()).days <= maxExpiryDays]
    else:
        contractList = [i for i in contracts if (i.ID.Date.date() - self.Time.date()).days <= maxExpiryDays]
    
    # get the furthest expiration contracts
    furthestExpiryDate = max([i.ID.Date for i in contractList])
    furthestContracts = [i for i in contractList if i.ID.Date == furthestExpiryDate]
    
    # find the strike price for ATM options
    atmStrike = sorted(furthestContracts, key = lambda x: abs(x.ID.StrikePrice - self.Securities[symbol].Price))[0].ID.StrikePrice
    
    # create a list of all possible strike prices
    strikesList = sorted(set([i.ID.StrikePrice for i in furthestContracts]))
    
    # find strikes
    strikePrices = {}
    # loop through strikePercents and create a new dictionary strikePrices with the strikeGroup and the strikePrice
    for strikeGroup, strikePercent in strikePercents.items():
        objectiveStrike = atmStrike * (1 + strikePercent)
        if strikePercent <= 0:
            strikePrices[strikeGroup] = min([x for x in strikesList if x >= objectiveStrike and x <= atmStrike])
        else:
            strikePrices[strikeGroup] = max([x for x in strikesList if x >= atmStrike and x <= objectiveStrike])

    if optionSide == 'calls':
        side = 0
    elif optionSide == 'puts':
        side = 1
    else:
        raise ValueError('optionSide parameter has to be either calls or puts!')
    
    # find the contracts
    strikeContracts = {}
    # loop through strikePrices and create a new dictionary strikeContracts with the strikeGroup and the strikeContract
    for strikeGroup, strikePrice in strikePrices.items():
        strikeContracts[strikeGroup] = [i for i in furthestContracts if i.ID.OptionRight == side and i.ID.StrikePrice == strikePrice][0]
    
    return strikeContracts
    
def UpdateBenchmarkValue(self):
        
    ''' Simulate buy and hold the Benchmark '''
    
    if self.initBenchmarkPrice == 0:
        self.initBenchmarkCash = self.Portfolio.Cash
        self.initBenchmarkPrice = self.Benchmark.Evaluate(self.Time)
        self.benchmarkValue = self.initBenchmarkCash
    else:
        currentBenchmarkPrice = self.Benchmark.Evaluate(self.Time)
        self.benchmarkValue = (currentBenchmarkPrice / self.initBenchmarkPrice) * self.initBenchmarkCash
        
def UpdatePortfolioGreeks(self, slice):
    
    ''' Calculate the Greeks per contract and return the current Portfolio Greeks  '''
    
    portfolioGreeks = {}
    
    # loop through the option chains
    for i in slice.OptionChains:
        chain = i.Value
        contracts = [x for x in chain]
        
        if len(contracts) == 0:
            continue

        # get the portfolio greeks
        portfolioDelta = sum(x.Greeks.Delta * self.Portfolio[x.Symbol].Quantity for x in contracts) * 100
        portfoliGamma = sum(x.Greeks.Gamma * self.Portfolio[x.Symbol].Quantity for x in contracts) * 100
        portfolioVega = sum(x.Greeks.Vega * self.Portfolio[x.Symbol].Quantity for x in contracts) * 100
        portfolioRho = sum(x.Greeks.Rho * self.Portfolio[x.Symbol].Quantity for x in contracts) * 100
        portfolioTheta = sum(x.Greeks.Theta * self.Portfolio[x.Symbol].Quantity for x in contracts) * 100
        
        portfolioGreeks = {'Delta': portfolioDelta, 'Gamma': portfoliGamma,
                            'Vega': portfolioVega, 'Rho': portfolioRho, 'Theta': portfolioTheta}
                            
    return portfolioGreeks
    
def CheckData(self, contracts):
    
    ''' Check for erroneous data '''
    
    for contract in contracts:
        # get current bid and ask prices
        currentBidPrice = self.Securities[contract].BidPrice
        currentAskPrice = self.Securities[contract].AskPrice
        
        # add bid and ask prices or retrieve the last ones if we already have them
        if contract not in self.lastMinutePricesDict:
            self.lastMinutePricesDict[contract] = [currentBidPrice, currentAskPrice]
            continue
        
        else:
            lastBidPrice = self.lastMinutePricesDict[contract][0]
            lastAskPrice = self.lastMinutePricesDict[contract][1]
            
            # update prices
            self.lastMinutePricesDict[contract] = [currentBidPrice, currentAskPrice]
        
        # get the percent change for both bid and ask prices
        pctChangeBid = ((currentBidPrice / lastBidPrice) - 1) * 100
        pctChangeAsk = ((currentAskPrice / lastAskPrice) - 1) * 100
        
        # store extreme price changes
        if abs(pctChangeBid) > self.extremePriceChangeCheck or abs(pctChangeAsk) > self.extremePriceChangeCheck:
            contractId = str(self.Securities[contract].Symbol).replace(' ', '')
            
            self.Log('contractId: ' + str(contractId) + '; currentBidPrice: ' + str(currentBidPrice) + '; lastBidPrice: ' + str(lastBidPrice)
            + '; currentAskPrice: ' + str(currentAskPrice) + '; lastAskPrice: ' + str(lastAskPrice))

            if contractId not in self.dataChecksDict['extremePriceChange']:
                self.dataChecksDict['extremePriceChange'].update({contractId: [self.Time]})
            else:
                self.dataChecksDict['extremePriceChange'][contractId].append(self.Time)

def CustomSecurityInitializer(self, security):
    
    '''
    Description:
        Initialize the security with different models
    Args:
        security: Security which characteristics we want to change'''
    
    security.SetMarketPrice(self.GetLastKnownPrice(security))
    security.SetDataNormalizationMode(DataNormalizationMode.Raw)
    security.SetLeverage(self.leverage)

    if security.Type == SecurityType.Equity:
        if self.constantFeeEquities is not None:
            # constant fee model that takes a dollar amount parameter to apply to each order
            security.SetFeeModel(CustomFeeModel(self.constantFeeEquities))
        if self.constantSlippagePercentEquities is not None:
            # constant slippage model that takes a percentage parameter to apply to each order value
            security.SetSlippageModel(CustomSlippageModel(self.constantSlippagePercentEquities))
            
    elif security.Type == SecurityType.Option:
        if self.constantFeeOptions is not None:
            # constant fee model that takes a dollar amount parameter to apply to each order
            security.SetFeeModel(CustomFeeModel(self.constantFeeOptions))
        if self.constantSlippagePercentOptions is not None:
            # constant slippage model that takes a percentage parameter to apply to each order value
            security.SetSlippageModel(CustomSlippageModel(self.constantSlippagePercentOptions))

class CustomFeeModel:
    
    ''' Custom implementation of the Fee Model '''
    
    def __init__(self, multiple):
        self.multiple = multiple
    
    def GetOrderFee(self, parameters):
        
        ''' Get the fee for the order '''
        
        absQuantity = parameters.Order.AbsoluteQuantity
        fee = max(1, absQuantity * self.multiple)
        
        return OrderFee(CashAmount(fee, 'USD'))
        
class CustomSlippageModel:
    
    ''' Custom implementation of the Slippage Model '''
        
    def __init__(self, multiple):
        self.multiple = multiple

    def GetSlippageApproximation(self, asset, order):
        
        ''' Apply slippage calculation to order price '''
        
        quantity = order.Quantity
        price = [asset.AskPrice if quantity > 0 else asset.BidPrice][0]
        
        slippage = price * self.multiple
        
        return slippage
### 2020_06_07 v28
### ----------------------------------------------------------------------------
# Added the option to NOT ENTER new contracts if notional ratio (notional coverage / underlying shares) is below a user-defined value
# Note for now this option ONLY WORKS for a single group of given rollMaxExpiryDays and single strike specified in each group
### ----------------------------------------------------------------------------

from datetime import timedelta
from HelperFunctions import *
import pandas as pd

class OptionsStrategyTemplateAlgorithm(QCAlgorithm):

    def Initialize(self):
        
        ''' Initialization at beginning of backtest '''
        
        ### user-defined inputs ---------------------------------------------------------------------------------------------------
        
        # didn't have full list of options available for spy before 7/1/10...verified EEM options available from 7/1/10 so ok there
        # weeklies seem to start for SPY in 01/16
        self.SetStartDate(2007, 1, 1) #20090301
        # just comment out end date to run through today
        self.SetEndDate(2020, 4, 30) #20100301
        self.SetCash(1000000)
        
        # select a ticker as benchmark (will plot Buy&Hold of this benchmark)
        self.benchmarkTicker = 'SPY'
    
        # select ticker for underlying asset (holdings of this asset will be 100% of portfolio)
        underlyingTicker = 'SPY'

        # variable to force the use of monthly contracts only or to allow for others
        self.onlyMonthlies = True
        
        # dictionary of dictionaries containing the different groups of option legs by expiry date
        # the format of the strikes is [strike percent, direction of trade (1 to Buy, -1 to Sell (more than 1 to trade more contracts), None to not trade)]
        self.dictParameters = {'UnderlyingSynthetic': {'activate': False,
                                                        'ticker': 'SPY',
                                                        'maxExpiryDays': 10,
                                                        'rollMaxExpiryDays': 10,
                                                        'daysToRollBeforeExpiration': 1,
                                                        'underlyingPriceDownMoveLiquidate': -0.9, # applied to calls
                                                        'underlyingPriceUpMoveLiquidate': 0.9, # applied to puts
                                                        'underlyingPriceLowerBoundSidewaysLiquidate': -0.01, # applied to calls/puts
                                                        'underlyingPriceUpperBoundSidewaysLiquidate': 0.01, # applied to calls/puts
                                                        'underlyingPriceDaysSidewaysLiquidate': 5, # number of days underlying price within lower/upper bound
                                                        'calls': {'strikePercentA': [0.01, -1], 'strikePercentB': [0.15, None],
                                                                'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                        'puts': {'strikePercentA': [-0.15, None], 'strikePercentB': [-0.01, 1]},
                                                                'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                
                               'ExpiryGroupA': {'activate': False,
                                                'ticker': 'SPY',
                                                'maxExpiryDays': 100,
                                                'rollMaxExpiryDays': 100,
                                                'daysToRollBeforeExpiration': 30,
                                                'underlyingPriceDownMoveLiquidate': -0.9, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': 0.9, # applied to puts  0.075
                                                'underlyingPriceLowerBoundSidewaysLiquidate': -0.9, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': 0.9, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': 600, # number of days underlying price within lower/upper bound
                                                'calls': {'strikePercentA': [0.15, None], 'strikePercentB': [0.15, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                'puts': {'strikePercentA': [-0.30, 1], 'strikePercentB': [-0.20, None],
                                                        'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},
            
                                'ExpiryGroupB': {'activate': False,
                                                 'ticker': 'SPY',
                                                'maxExpiryDays': 190,
                                                'rollMaxExpiryDays': 190,
                                                'daysToRollBeforeExpiration': 30,
                                                'underlyingPriceDownMoveLiquidate': -0.9, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': 0.9, # applied to puts
                                                'underlyingPriceLowerBoundSidewaysLiquidate': -0.05, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': 0.9, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': 600, # number of days underlying price within lower/upper bound
                                                'calls': {'strikePercentA': [0.15, None], 'strikePercentB': [0.15, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                'puts': {'strikePercentA': [-0.40, 1], 'strikePercentB': [-0.45, None],
                                                        'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},
                  
                                'ExpiryGroupC': {'activate': False,
                                                 'ticker': 'SPY',
                                                'maxExpiryDays': 370,
                                                'rollMaxExpiryDays': 370,
                                                'daysToRollBeforeExpiration': 1,
                                                'underlyingPriceDownMoveLiquidate': -0.1, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': 0.9, # applied to puts
                                                'underlyingPriceLowerBoundSidewaysLiquidate': -0.9, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': 0.9, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': 600, # number of days underlying price within lower/upper bound
                                                'calls': {'strikePercentA': [0.05, 1], 'strikePercentB': [0.15, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                'puts': {'strikePercentA': [-0.3, None], 'strikePercentB': [-0.20, None],
                                                        'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},
            
                                'ExpiryGroupD': {'activate': False,
                                                'ticker': 'SPY',
                                                'maxExpiryDays': 190,
                                                'rollMaxExpiryDays': 190,
                                                'daysToRollBeforeExpiration': 1,
                                                'underlyingPriceDownMoveLiquidate': -0.1, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': 0.9, # applied to puts
                                                'underlyingPriceLowerBoundSidewaysLiquidate': -0.9, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': 0.9, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': 600, # number of days underlying price within lower/upper bound
                                                'calls': {'strikePercentA': [0.1, 1], 'strikePercentB': [0.15, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                'puts': {'strikePercentA': [-0.1, None], 'strikePercentB': [0.15, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},
                                
                                'ExpiryGroupE': {'activate': True,
                                                'ticker': 'SPY',
                                                'maxExpiryDays': 720,
                                                'rollMaxExpiryDays': 720,
                                                'daysToRollBeforeExpiration': 90,
                                                'underlyingPriceDownMoveLiquidate': -0.25, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': 0.9, # applied to puts
                                                'underlyingPriceLowerBoundSidewaysLiquidate': -0.03, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': 0.03, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': 600, # number of days underlying price within lower/upper bound
                                                'calls': {'strikePercentA': [0.4, 1], 'strikePercentB': [0.15, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                'puts': {'strikePercentA': [-0.15, None], 'strikePercentB': [0.15, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},
            
                                'ExpiryGroupF': {'activate': False,
                                                'ticker': 'SPY',
                                                'maxExpiryDays': 460,
                                                'rollMaxExpiryDays': 460,
                                                'daysToRollBeforeExpiration': 1,
                                                'underlyingPriceDownMoveLiquidate': -0.25, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': 0.9, # applied to puts
                                                'underlyingPriceLowerBoundSidewaysLiquidate': -0.03, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': 0.03, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': 600, # number of days underlying price within lower/upper bound
                                                'calls': {'strikePercentA': [0.20, 1], 'strikePercentB': [0.30, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                'puts': {'strikePercentA': [-0.15, None], 'strikePercentB': [0.15, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}}
                                                           
        # take annual budget and split it evenly between all expiry groups and spreads budget evenly across all contracts
        # in a one year horizon (accounts for rollMaxExpiryDays in each group)...trades account for multipliers at end too
        self.annualBudget = 0.02
        
        # minimum notional ratio allowed to enter new contracts
        self.minNotionalRatio = 0
        
        # overwrite the default model for fees
        # - Default: Set to None to use the default IB Tiered Model for both stocks and options from here https://www.interactivebrokers.com/en/index.php?f=1590&p=options1
        #    --- FYI for low number of stocks the default fee comes out to .005 (presumably dominated by the .0035 IB commission at low number of shares)
        #    --- FYI for low number of options contracts at hefty premiums the default fee comes out to .25...don't understand that yet since commission alone looks to be 0.65
        # - Custom Constant Fee: Provide a dollar amount to apply to each order quantity ($ per share for stock and $ per contract for options)
        self.constantFeeEquities = None
        self.constantFeeOptions = None
        
        # overwrite the default model for slippage
        # - Default: Set to None to use the default slippage model which uses 0% slippage
        # - Custom Constant Slippage: Provide a % (in the form of a decimal ranged 0 to 1) to apply to each order value
        self.constantSlippagePercentEquities = None
        self.constantSlippagePercentOptions = None
        
        # data checks and logs:
        # variable to turn on/off trading logs
        self.tradingLogs = False
        # variable to avoid specific option contracts whose price is above a certain level
        self.avoidContractsWithPrice = {} # format: 'SPY150821P00181000': 55, 'SPY150918C00250000': 55, 'SPY150918P00130000': 55
        # current put avoidance list: ['SPY1509', 'SPY1508', 'SPY1009']
        self.avoidContracts = ['SPY1508', 'SPY1509', 'SPY101218C', 'SPY100918C','SPY110319C'] # formats: 'SPY150918C00240000', 'SPY1012', 'SPY100918', 'SPY100918C'
        #'SPY080919' call blowing me up in 08
        # check for extreme changes in minute price to report
        self.extremePriceChangeCheck = 1000
        
        # variable to enable/disable assignments before expiration
        # when set to True, the order from assignments will be cancelled until intended liquidation date
        # when set to False, the assignment is avoided and then all option contracts are immediately liquidated
        self.avoidAssignment = True
        
        # set leverage
        self.leverage = 1000000
        
        ### -------------------------------------------------------------------------------------------------------------------------

        self.SetSecurityInitializer(lambda x: CustomSecurityInitializer(self, x))
        
        self.SetBenchmark(self.benchmarkTicker)
        
        equity = self.AddEquity(underlyingTicker, Resolution.Minute)
        equity.VolatilityModel = StandardDeviationOfReturnsVolatilityModel(30)
        self.underlyingSymbol = equity.Symbol
        
        self.expiryGroupSymbols = {}
        for expiryGroup, parameters in self.dictParameters.items():
            if parameters['activate']:
                ticker = parameters['ticker']
                if ticker != underlyingTicker:
                    self.expiryGroupSymbols[expiryGroup] = self.AddEquity(ticker, Resolution.Minute).Symbol
                else:
                    self.expiryGroupSymbols[expiryGroup] = self.underlyingSymbol
        
        self.numberOfActiveExpiryGroups = sum(parameters['activate'] for expiryGroup, parameters in self.dictParameters.items())
        
        rollMaxExpiryDays = [parameters['rollMaxExpiryDays'] for expiryGroup, parameters in self.dictParameters.items() if parameters['activate']]
        self.sameRollMaxExpiryDaysExpiryGroups = {str(elem): [] for elem in rollMaxExpiryDays}
        for expiryGroup, parameters in self.dictParameters.items():
            if parameters['activate']:
                rollMaxExpiryDays = parameters['rollMaxExpiryDays']
                self.sameRollMaxExpiryDaysExpiryGroups[str(rollMaxExpiryDays)].append(expiryGroup)
                
        # plot the Portfolio Greeks
        portfolioGreeksPlot = Chart('Chart Portfolio Greeks')
        portfolioGreeksPlot.AddSeries(Series('Daily Portfolio Delta', SeriesType.Line, ''))
        portfolioGreeksPlot.AddSeries(Series('Daily Portfolio Gamma', SeriesType.Line, ''))
        portfolioGreeksPlot.AddSeries(Series('Daily Portfolio Vega', SeriesType.Line, ''))
        portfolioGreeksPlot.AddSeries(Series('Daily Portfolio Rho', SeriesType.Line, ''))
        portfolioGreeksPlot.AddSeries(Series('Daily Portfolio Theta', SeriesType.Line, ''))
        self.AddChart(portfolioGreeksPlot)
        
        #self.Portfolio.MarginCallModel = MarginCallModel.Null
        self.SetWarmup(30, Resolution.Daily)
        self.allContractsByExpiryGroup = {}
        self.dailyPortfolioGreeksDict = {}
        self.lastMinutePricesDict = {}
        self.dataChecksDict = {'extremePriceChange': {}, 'contractAboveLimitPrice': {},
                                'contractPriceZero': {}, 'emptyOptionContracts': {}}
        self.dataCheckPrinted = False
        self.assignedOption = False
        self.initBenchmarkPrice = 0
        self.rebalanceUnderlying = False
        self.specialTag = ''
        self.day = 0
        
    def OnData(self, slice):
        
        ''' Event triggering every time there is new data '''
        
        # print data checks at the end of the backtest
        if self.Time.date() >= (self.EndDate.date() - timedelta(2)) and not self.dataCheckPrinted:
            self.Log(self.dataChecksDict)
            self.dataCheckPrinted = True
        
        tradingDay = date(self.Time.year, self.Time.month, self.Time.day)
        if tradingDay != self.day:
            self.checkNextDay = False
            
            # simulate buy and hold the benchmark and plot its daily value --------------------------------------
            UpdateBenchmarkValue(self)
            self.Plot('Strategy Equity', self.benchmarkTicker, self.benchmarkValue)
            
            # update the Portfolio Greeks dictionary ------------------------------------------------------------
            todayPortfolioGreeks = UpdatePortfolioGreeks(self, slice)
            
            if todayPortfolioGreeks:
                #self.dailyPortfolioGreeksDict[tradingDay] = todayPortfolioGreeks
                #portfolioGreeksDf = pd.DataFrame.from_dict(self.dailyPortfolioGreeksDict, orient = 'index')
                #self.Log(portfolioGreeksDf)
                
                for greek, value in todayPortfolioGreeks.items():
                    self.Plot('Chart Portfolio Greeks', 'Daily Portfolio ' + greek, value)
                
            self.day = tradingDay
        
        # check if we got assigned and liquidate all remaining legs --------------------------------------------
        if self.assignedOption:
            # close all option contracts at once
            openOptionContracts = GetOpenOptionContracts(self)
            for contract in openOptionContracts:
                self.Liquidate(contract, 'Liquidated - option assignment')
                self.RemoveSecurity(contract)
                
            self.assignedOption = False
        
        # get a list with open option contracts ------------------------------------------------------------------
        openOptionContracts = GetOpenOptionContracts(self)
        
        # check on strange data ----------------------------------------------------------------------------------
        CheckData(self, openOptionContracts)
        
        # run below code only during this hour (halved bt time from 16 mins to 8 mins) ---------------------------
        if not self.Time.hour == 9:
            return

        # empty list to store expiry groups to restart due to underlying price move
        expiryGroupsToRestartList = []
        
        # enter first contracts -----------------------------------------------------------------------------------
        for expiryGroup, parameters in self.dictParameters.items():
            if expiryGroup not in self.allContractsByExpiryGroup.keys() and parameters['activate']:
                ticker = parameters['ticker']
                enterContractsWorked = EnterOptionContracts(self, expiryGroup, ticker, self.expiryGroupSymbols[expiryGroup], parameters['maxExpiryDays'],
                                                            parameters['calls'], parameters['puts'])
                
                if not enterContractsWorked:
                    continue
                
                self.rebalanceUnderlying = True
                                        
        # rebalance holdings of underlying asset ------------------------------------------------------------------
        if self.rebalanceUnderlying and not self.dictParameters['UnderlyingSynthetic']['activate']:
            RebalanceUnderlying(self)
            self.rebalanceUnderlying = False
        
        # liquidate contracts about to expire/due to underlying price move ------------------------------------
        for expiryGroup, parameters in self.allContractsByExpiryGroup.items():
             
            # skip expiryGroup that is already in expiryGroupsToRestartList
            if expiryGroup in expiryGroupsToRestartList:
                continue
            
            # get inputs
            entryDate = parameters[0]
            nextExpiryDate = parameters[1]
            legs = parameters[2]
            underlyingPriceAtEntry = parameters[3]
            contracts = parameters[4]
            daysToRollBeforeExpiration = self.dictParameters[expiryGroup]['daysToRollBeforeExpiration']
            ticker = self.dictParameters[expiryGroup]['ticker']
            underlyingSymbol = self.expiryGroupSymbols[expiryGroup]
            underlyingPriceLowerBoundSidewaysLiquidate = self.dictParameters[expiryGroup]['underlyingPriceLowerBoundSidewaysLiquidate']
            underlyingPriceUpperBoundSidewaysLiquidate = self.dictParameters[expiryGroup]['underlyingPriceUpperBoundSidewaysLiquidate']
            underlyingPriceDaysSidewaysLiquidate = self.dictParameters[expiryGroup]['underlyingPriceDaysSidewaysLiquidate']
            
            # check where underlying price vs underlyingPriceAtEntry
            # and liquidate if beyond/within threshold ----------------------------
            underlyingPriceMoveLiquidate = False
            underlyingCurrentPrice = self.Securities[underlyingSymbol].Price
            underlyingPriceMove = (underlyingCurrentPrice / underlyingPriceAtEntry) - 1
            if legs == 'calls':
                if underlyingPriceMove < self.dictParameters[expiryGroup]['underlyingPriceDownMoveLiquidate']:
                    underlyingPriceMoveLiquidate = True
            elif legs == 'puts':
                if underlyingPriceMove > self.dictParameters[expiryGroup]['underlyingPriceUpMoveLiquidate']:
                    underlyingPriceMoveLiquidate = True
            else:
                if (underlyingPriceMove < self.dictParameters[expiryGroup]['underlyingPriceDownMoveLiquidate']
                or underlyingPriceMove > self.dictParameters[expiryGroup]['underlyingPriceUpMoveLiquidate']):
                    underlyingPriceMoveLiquidate = True
                    
            if (self.Time - entryDate) >= timedelta(underlyingPriceDaysSidewaysLiquidate):
                if underlyingPriceLowerBoundSidewaysLiquidate < underlyingPriceMove < underlyingPriceUpperBoundSidewaysLiquidate:
                    underlyingPriceMoveLiquidate = True
            
            if underlyingPriceMoveLiquidate:
                rollMaxExpiryDays = self.dictParameters[expiryGroup]['rollMaxExpiryDays']
                expiryGroupsToRestartList.extend( self.sameRollMaxExpiryDaysExpiryGroups[str(rollMaxExpiryDays)] )
                self.tag = ('(' + expiryGroup + ' underlyingPriceMoveLiquidate rule triggered; underlying price moved '
                            + '{:.4%}'.format(underlyingPriceMove))
                
                if self.tradingLogs:
                    self.Log(expiryGroup + '/' + ticker
                    + ': liquidating all option contracts with the same rollMaxExpiryDays due to underlying price move rule'
                    + '; underlyingPriceAtEntry was ' + str(underlyingPriceAtEntry) + '; underlyingCurrentPrice is ' + str(underlyingCurrentPrice))
            
            # check for expiration -----------------------------------------
            elif (nextExpiryDate - self.Time) < timedelta(daysToRollBeforeExpiration):
                
                # liquidating expired contracts
                liquidationWorked = LiquidateOptionContracts(self, expiryGroup, ticker, contracts, 'contract expiration')
                
                if not liquidationWorked:
                    continue

                # roll over expired contracts -------------------------------------------------------------------
                parameters = self.dictParameters[expiryGroup]
                enterContractsWorked = EnterOptionContracts(self, expiryGroup, ticker, underlyingSymbol, parameters['rollMaxExpiryDays'],
                                                            parameters['calls'], parameters['puts'])
                                                            
                if not enterContractsWorked:
                    continue
        
        # restart the entire expiry group due to underlying price deviation ------------------------------------------
        if len(expiryGroupsToRestartList) > 0:
            for expiryGroup, parameters in self.allContractsByExpiryGroup.items():
                if expiryGroup in expiryGroupsToRestartList:
                    ticker = self.dictParameters[expiryGroup]['ticker']
                    contracts = parameters[4]
                    
                    # liquidating expired contracts
                    liquidationWorked = LiquidateOptionContracts(self, expiryGroup, ticker, contracts, self.tag)
                    
                    if not liquidationWorked:
                        continue
                    
                     # roll over expired contracts -------------------------------------------------------------------
                    parameters = self.dictParameters[expiryGroup]
                    enterContractsWorked = EnterOptionContracts(self, expiryGroup, ticker, underlyingSymbol, parameters['maxExpiryDays'],
                                                                parameters['calls'], parameters['puts'])
                    
                    if not enterContractsWorked:
                        continue

                    expiryGroupsToRestartList.remove(expiryGroup)
                
    def OnOrderEvent(self, orderEvent):
        
        ''' Check if the order is a Simulated Option Assignment Before Expiration and act accordingly '''
        
        ticket = self.Transactions.GetOrderTicket(orderEvent.OrderId)
        if ticket.OrderType == OrderType.OptionExercise:
            if ticket.Tag == 'Simulated option assignment before expiration':
                if self.avoidAssignment:
                    ticket.Cancel()
                else:
                    # set assignedOption to True in order to trigger the OnData event to LiquidateOptionContracts
                    self.assignedOption = True
        
            if ticket.Tag ==  'Automatic option exercise on expiration - Adjusting(or removing) the exercised/assigned option':
                self.assignedOption = True