Overall Statistics
Total Trades
88
Average Win
6.94%
Average Loss
-0.99%
Compounding Annual Return
12.136%
Drawdown
74.100%
Expectancy
2.086
Net Profit
374.346%
Sharpe Ratio
0.438
Probabilistic Sharpe Ratio
0.005%
Loss Rate
62%
Win Rate
38%
Profit-Loss Ratio
7.02
Alpha
0.201
Beta
1.019
Annual Standard Deviation
0.712
Annual Variance
0.508
Information Ratio
0.299
Tracking Error
0.681
Treynor Ratio
0.306
Total Fees
$5422.44
import matplotlib.pyplot as plt
from matplotlib.ticker import PercentFormatter
import pandas as pd

def AddDrawdownInformation(df):

    # convert to cumulative return series
    cumRetSeries = df.add(1).cumprod()
    
    # initialize variables
    lastPeak = cumRetSeries.iloc[0][0]
    cumRetSeries['drawdown'] = 1
    cumRetSeries['ddGroup'] = 0

    # loop through the series and calculate drawdown
    count = 0
    for i in range(len(cumRetSeries)):
        if cumRetSeries.iloc[i, 0] < lastPeak:
            cumRetSeries.iloc[i, 1] = cumRetSeries.iloc[i, 0] / lastPeak
            cumRetSeries.iloc[i, 2] = count
        else:
            lastPeak = cumRetSeries.iloc[i, 0]
            cumRetSeries.iloc[i, 2] = count
            count += 1
        
    cumRetSeries['drawdown'] = cumRetSeries['drawdown'] - 1
    
    # get the max drawdown per group
    maxDrawdown = cumRetSeries.groupby('ddGroup', as_index = False)['drawdown'].min()
    maxDrawdown = maxDrawdown['drawdown']
    maxDrawdown = maxDrawdown.to_frame()
    maxDrawdown.columns = ['maxDrawdown']
    # get the start of the drawdown for each group
    startDrawdown = {key: value[0].date() for key, value in cumRetSeries.groupby('ddGroup').groups.items()}
    startDrawdown = pd.DataFrame.from_dict(startDrawdown, orient = 'index')
    startDrawdown.columns = ['startDrawdown']
    # get the end of the drawdown for each group
    endDrawdown = {key: value[-1].date() for key, value in cumRetSeries.groupby('ddGroup').groups.items()}
    endDrawdown = pd.DataFrame.from_dict(endDrawdown, orient = 'index')
    endDrawdown.columns = ['endDrawdown']
    # get the bottom of the drawdown for each group
    bottomDrawdown = cumRetSeries.groupby('ddGroup', as_index = False)['drawdown'].idxmin()
    bottomDrawdown = bottomDrawdown.to_frame()
    bottomDrawdown.columns = ['bottomDrawdown']
    
    infoDrawdown = pd.concat([startDrawdown, bottomDrawdown, endDrawdown, maxDrawdown], axis = 1)
    finalDf = pd.merge(cumRetSeries, infoDrawdown, how = 'inner', left_on = 'ddGroup', right_index = True)
        
    return finalDf
        
def AddDrawupInformation(drawdownDf, minimumDrawdownBetweenDrawups = 0.02):
    
    # initialize variables
    df = drawdownDf.copy()
    df['drawup'] = 1
    df['duGroup'] = 0
    
    # loop through the dataframe and calculate drawup
    count = 0
    newDrawdown = False
    for i in range(len(df)):
        # check if we started a new drawdown
        if df.iloc[i].name == df.iloc[i, 3] and df.iloc[i, 6] < minimumDrawdownBetweenDrawups * -1:
            newDrawdown = True
        
        if df.iloc[i].name == df.iloc[i, 4] and df.iloc[i, 6] < minimumDrawdownBetweenDrawups * -1:
            lastBottom = df.iloc[i, 0]
            newDrawdown = False
            count += 1
            
        if not newDrawdown and count > 0:
            df.iloc[i, 7] = df.iloc[i, 0] / lastBottom
            df.iloc[i, 8] = count
            
    df['drawup'] = df['drawup'] - 1
    
    # get the max drawup per group
    maxDrawup = df.groupby('duGroup')['drawup'].max()
    maxDrawup = maxDrawup.to_frame()
    maxDrawup.columns = ['maxDrawup']
    
    finalDf = pd.merge(df, maxDrawup, how = 'left', left_on = 'duGroup', right_index = True)

    return finalDf

def PlotDrawdownSeries(df, maxDays = 100, minimumMaxDrawdown = 0.1):
    
    filteredDf = df[(df['maxDrawdown'] <= minimumMaxDrawdown * -1) & (df.index >= df['startDrawdown']) & (df.index <= df['bottomDrawdown'])]
    grouped = filteredDf.groupby('ddGroup')
    
    plt.figure(figsize = (10, 10))
    for name, group in grouped:
        y = [0] + group['drawdown'].values[:maxDays].tolist()
        y = [i * 100 for i in y]
        x = [i for i in range(len(y))]
        
        fromDate = group['startDrawdown'][0].strftime('%Y-%m-%d')
        toDate = group['bottomDrawdown'][0].strftime('%Y-%m-%d')
        duration = (group.index[-1] - group.index[0]).days
        maxDD = group['maxDrawdown'][0]
        plt.plot(x, y, label = fromDate + '/' + toDate + '; ' + str(duration) + ' days ; ' + '{:.0%}'.format(maxDD), linewidth = 1)
        
        plt.title('Historical Drawdown Series With Max DD Above ' + '{:.0%}'.format(abs(minimumMaxDrawdown))
                  + '\n First ' + str(maxDays) + ' Trading Days')
        plt.gca().yaxis.set_major_formatter(PercentFormatter(decimals = 0))
        plt.gca().spines['right'].set_visible(False)
        plt.gca().spines['top'].set_visible(False)
        plt.gca().xaxis.set_ticks_position('none')
        plt.gca().yaxis.set_ticks_position('none')
        plt.axhline(y = 0, color = 'black', linestyle = '-', linewidth = 1)
    
    plt.legend(loc = 'right', bbox_to_anchor = (1.5, 0.5), ncol = 1, frameon = False)
    plt.show()

def PlotDrawupSeries(df, maxDays = 100, minimumMaxDrawup = 0.1):
    
    filteredDf = df[(df['duGroup'] != 0) & (df['maxDrawup'] > minimumMaxDrawup)]
    grouped = filteredDf.groupby('duGroup')

    plt.figure(figsize = (10, 10))
    for name, group in grouped:
        y = [0] + group['drawup'].values[:maxDays].tolist()
        y = [i * 100 for i in y]
        x = [i for i in range(len(y))]
        
        fromDate = group.index[0].strftime('%Y-%m-%d')
        toDate = group.index[-1].strftime('%Y-%m-%d')
        duration = (group.index[-1] - group.index[0]).days
        maxDU = group['maxDrawup'][0]
        plt.plot(x, y, label = fromDate + '/' + toDate + '; ' + str(duration) + ' days ; ' + '{:.0%}'.format(maxDU), linewidth = 1)
        
        plt.title('Historical Drawdup Series With Max DU Above ' + '{:.0%}'.format(abs(minimumMaxDrawup))
                  + '\n First ' + str(maxDays) + ' Trading Days')
        plt.gca().yaxis.set_major_formatter(PercentFormatter(decimals = 0))
        plt.gca().spines['right'].set_visible(False)
        plt.gca().spines['top'].set_visible(False)
        plt.gca().xaxis.set_ticks_position('none')
        plt.gca().yaxis.set_ticks_position('none')
        plt.axhline(y = 0, color = 'black', linestyle = '-', linewidth = 1)
    
    plt.legend(loc = 'right', bbox_to_anchor = (1.5, 0.5), ncol = 1, frameon = False)
    plt.show()
from QuantConnect.Securities.Option import *
from datetime import timedelta
import json
import math

def FormatHandler(x):
    
    ''' Serialize datetime and QC Symbol object for json storage '''
    
    if isinstance(x, datetime):
        return x.isoformat()
    elif isinstance(x, Symbol):
        return str(x.Value)
        
def UpdateOptionsCumulativeAttribution(self, expiryGroup, infoDict):
    
    ''' Update and plot options cumulative attribution '''
    
    listContracts = infoDict['listContracts']
    initialContractsValue = infoDict['initialContractsValue']
    legLabel = self.dictParameters[expiryGroup]['legLabel']
    
    # calculate current options value
    try:
        remainingContractsValue = 0
        for contract in listContracts:
            contractId = str(self.Securities[contract].Symbol).replace(' ', '')
            if contractId in self.avoidContractsWithPrice and self.Securities[contract].BidPrice > self.avoidContractsWithPrice[contractId]:
                bidPrice = 0.01
            else:
                bidPrice = self.Securities[contract].BidPrice
        
            remainingContractsValue += self.Portfolio[contract].Quantity * 100 * bidPrice
    except:
        remainingContractsValue = 0
    
    if self.portValueWin.IsReady and self.optionsValueWinDict[expiryGroup].IsReady and self.initialOptionsValueDict[expiryGroup].IsReady:
        if initialContractsValue != self.initialOptionsValueDict[expiryGroup][0]:
            prevOptionsValue = initialContractsValue
        else:
            prevOptionsValue = self.optionsValueWinDict[expiryGroup][0]
            
        if remainingContractsValue == 0:
            remainingContractsValue = prevOptionsValue
        
        # calculate the change in options value between yesterday and today, and retrieve previous portfolio value
        optionsValueChange = remainingContractsValue - prevOptionsValue
        prevPortValue = self.portValueWin[0]
        # calculate options attribution and add to cumulative calculation
        optionsAttr = (optionsValueChange / prevPortValue)
        cumOptionsAttr = optionsAttr + self.cumSumOptionsAttrDict[expiryGroup]
        
        if cumOptionsAttr != 0:
            self.Plot('Chart Options Cumulative Attribution', legLabel, cumOptionsAttr * 100)
        
        self.cumSumOptionsAttrDict[expiryGroup] = cumOptionsAttr
    
    # update rolling windows
    self.portValueWin.Add(self.Portfolio.TotalPortfolioValue)
    self.optionsValueWinDict[expiryGroup].Add(remainingContractsValue)
    self.initialOptionsValueDict[expiryGroup].Add(initialContractsValue)
        
def CalculateRemainingContractsValue(self, listContracts):
                        
    ''' Calculate remaining contracts value '''
    
    try:
        remainingContractsValue = sum([(self.Portfolio[contract].Quantity * 100 * self.Securities[contract].BidPrice) for contract in listContracts])
    except:
        remainingContractsValue = 0

    return remainingContractsValue
    
def CheckDynamicRebalancing(self, expiryGroup, infoDict, monetizingValue):
    
    ''' Check dynamic rebalancing rules '''
    
    dynamicRebalancing = False
    dynamicRule = ''
    
    # get rules parameters
    underlyingPriceDownMoveLiquidate = self.dictParameters[expiryGroup]['underlyingPriceDownMoveLiquidate']
    underlyingPriceUpMoveLiquidate = self.dictParameters[expiryGroup]['underlyingPriceUpMoveLiquidate']
    
    underlyingPriceLowerBoundSidewaysLiquidate = self.dictParameters[expiryGroup]['underlyingPriceLowerBoundSidewaysLiquidate']
    underlyingPriceUpperBoundSidewaysLiquidate = self.dictParameters[expiryGroup]['underlyingPriceUpperBoundSidewaysLiquidate']
    underlyingPriceDaysSidewaysLiquidate = self.dictParameters[expiryGroup]['underlyingPriceDaysSidewaysLiquidate']
    
    monetizingLiquidate = self.dictParameters[expiryGroup]['monetizingLiquidate']
    
    # calculate underlyingPriceMove
    underlyingSymbol = self.expiryGroupSymbols[expiryGroup]
    underlyingCurrentPrice = self.Securities[underlyingSymbol].Price
    underlyingPriceAtEntry = infoDict['underlyingPriceAtEntry']
    underlyingPriceMove = (underlyingCurrentPrice / underlyingPriceAtEntry) - 1
    
    legs = infoDict['legs']
    if legs == 'calls' and underlyingPriceDownMoveLiquidate is not None:
        if underlyingPriceMove < underlyingPriceDownMoveLiquidate:
            dynamicRule = 'underlyingPriceDownMoveLiquidate'
            dynamicRebalancing = True
    elif legs == 'puts' and underlyingPriceUpMoveLiquidate is not None:
        if underlyingPriceMove > underlyingPriceUpMoveLiquidate:
            dynamicRule = 'underlyingPriceUpMoveLiquidate'
            dynamicRebalancing = True
    elif underlyingPriceDownMoveLiquidate is not None and underlyingPriceUpMoveLiquidate is not None:
        if underlyingPriceMove < underlyingPriceDownMoveLiquidate or underlyingPriceMove > underlyingPriceUpMoveLiquidate:
            dynamicRule = 'underlyingPriceDownMoveLiquidate/underlyingPriceUpMoveLiquidate'
            dynamicRebalancing = True
    
    entryDate = infoDict['entryDate']
    if (underlyingPriceDaysSidewaysLiquidate is not None and underlyingPriceLowerBoundSidewaysLiquidate is not None
    and underlyingPriceUpperBoundSidewaysLiquidate is not None and (self.Time - entryDate) >= timedelta(underlyingPriceDaysSidewaysLiquidate)):
        if underlyingPriceLowerBoundSidewaysLiquidate < underlyingPriceMove < underlyingPriceUpperBoundSidewaysLiquidate:
            dynamicRule = 'underlyingPriceLowerBoundSidewaysLiquidate/underlyingPriceUpperBoundSidewaysLiquidate'
            dynamicRebalancing = True
            
    if monetizingLiquidate is not None and monetizingValue > monetizingLiquidate:
        dynamicRule = 'monetizingLiquidate'
        dynamicRebalancing = True
        
    return dynamicRebalancing, dynamicRule
    
def RollExpiryGroup(self, infoDict, expiryGroup, rollingType):
    
    ''' Liquidate existing contracts and enter new ones '''
    
    parameters = self.dictParameters[expiryGroup]
    
    if rollingType == 'static':
        message = 'static rebalancing'
        daysToExpiration = None
        expiryDays = parameters['rollMaxExpiryDays']
    else:
        message = self.tag
        nextExpiryDate = infoDict['nextExpiryDate']
        daysToExpiration = (nextExpiryDate - self.Time).days
        expiryDays = parameters['maxExpiryDays']

    self.Log('start of ' + rollingType + ' early rebalancing ----------')
    listContracts = infoDict['listContracts']
    LoggingContractsInfo(self, listContracts)
    
    # we add the remaining contracts value to the budget
    remainingContractsValue = CalculateRemainingContractsValue(self, listContracts)
    initialContractsValue = infoDict['initialContractsValue']
    if remainingContractsValue > (initialContractsValue * 0.75):
        remainingContractsValue = 0

    # liquidating contracts ------------------------
    liquidationWorked = LiquidateOptionContracts(self, expiryGroup, listContracts, message)
    if not liquidationWorked:
        return False, False
    
    # roll over contracts --------------------------
    enterOptionContractsWorked = EnterOptionContracts(self, expiryGroup, self.expiryGroupSymbols[expiryGroup],
                                                    parameters['calendarType'], parameters['positionSizing'],
                                                    expiryDays, parameters['daysToRollBeforeExpiration'],
                                                    parameters['calls'], parameters['puts'], parameters['legLabel'],
                                                    daysToExpiration, remainingContractsValue)
    if not enterOptionContractsWorked:
        return True, False
        
    return True, True
    
def LoggingContractsInfo(self, listContracts):
    
    ''' Print some contracts info before rolling '''
    
    for contract in listContracts:
        contractId = str(self.Securities[contract].Symbol).replace(' ', '')
        lastPrice = self.Securities[contract].Price
        bidPrice = self.Securities[contract].BidPrice
        askPrice = self.Securities[contract].AskPrice
        self.Log(str(contractId) + '; lastPrice: ' + str(lastPrice) + '; bidPrice: ' + str(bidPrice) + '; askPrice: ' + str(askPrice))
        
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, expiryGroupSymbol, calendarType, positionSizing, maxExpiryDays, daysToRollBeforeExpiration,
                        dictCalls, dictPuts, legLabel, daysToExpiration = None, remainingContractsValue = None):
    
    ''' Enter option contracts '''
    
    # 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, calendarType, maxExpiryDays, daysToRollBeforeExpiration, 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)
    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}

    # entering legs ------------------------------------------------------------
    
    # get adjusted budget
    adjustedAnnualBudget = CalculateAdjustedAnnualBudget(self, daysToRollBeforeExpiration, daysToExpiration)
    
    # apply multiplier budget for position sizing -----------
    if positionSizing == 'multiplier':
        # calculate the budget for options
        budgetOptions = CalculateBudgetOptions(self, expiryGroupSymbol, adjustedAnnualBudget, 1, remainingContractsValue)
        # calculate sum product of option prices for the entire expiry group
        sumProdOptionPrices = CalculateSumProdOptionPrices(self, dictContracts, dictLongs, dictShorts)
        # calculate the number of contracts to trade
        numberOfContracts = CalculateNumberOfContracts(self, budgetOptions, sumProdOptionPrices, expiryGroup, 'ALL')
        if numberOfContracts is None:
            return False

    # ------------------------------------------------------

    initialContractsValue = 0
    totalNotionalRatio = 0
    
    # start with short positions to get the premium
    for optionSide, strikeGroups in dictShorts.items():
        for strikeGroup, value in strikeGroups.items():
            # apply dollar budget when required ------------
            if positionSizing == 'dollar':
                # calculate the number of option contracts to trade
                annualBudgetPercent = value[1]
                budgetOptions = CalculateBudgetOptions(self, expiryGroupSymbol, adjustedAnnualBudget, annualBudgetPercent, remainingContractsValue)
                initialContractsValue += budgetOptions
                optionPrice = self.Securities[dictContracts[optionSide][strikeGroup]].BidPrice
                # calculate the number of contracts to trade
                shortNumberOfContracts = CalculateNumberOfContracts(self, budgetOptions, optionPrice, expiryGroup, strikeGroup)
                if shortNumberOfContracts is None:
                    return False
                
            else:
                multiplier = value[1]
                initialContractsValue += budgetOptions * multiplier
                shortNumberOfContracts = numberOfContracts * multiplier
            
            # get notional ratio
            notionalRatio = CalculateNotionalRatio(self, shortNumberOfContracts, expiryGroupSymbol)
            totalNotionalRatio += notionalRatio
            
            # place market order
            self.MarketOrder(dictContracts[optionSide][strikeGroup], shortNumberOfContracts, False,
                            expiryGroup + '; short ' + optionSide + '; strike ' + '{:.0%}'.format(value[0])
                            + ' vs atm; notional ratio ' + '{:.0%}'.format(notionalRatio))
                            
    # long positions
    for optionSide, strikeGroups in dictLongs.items():
        for strikeGroup, value in strikeGroups.items():
            # apply dollar budget when required ------------
            if positionSizing == 'dollar':
                # calculate the number of option contracts to trade
                annualBudgetPercent = value[1]
                budgetOptions = CalculateBudgetOptions(self, expiryGroupSymbol, adjustedAnnualBudget, annualBudgetPercent, remainingContractsValue)
                initialContractsValue += budgetOptions
                optionPrice = self.Securities[dictContracts[optionSide][strikeGroup]].AskPrice
                # calculate the number of contracts to trade
                longNumberOfContracts = CalculateNumberOfContracts(self, budgetOptions, optionPrice, expiryGroup, strikeGroup)
                if longNumberOfContracts is None:
                    return False
                
            else:
                multiplier = value[1]
                initialContractsValue += budgetOptions * multiplier
                longNumberOfContracts = numberOfContracts * multiplier
            
            # get notional ratio
            notionalRatio = CalculateNotionalRatio(self, longNumberOfContracts, expiryGroupSymbol)
            totalNotionalRatio += notionalRatio
            
            # place market order
            self.MarketOrder(dictContracts[optionSide][strikeGroup], longNumberOfContracts, False,
                            expiryGroup + '; long ' + optionSide + '; strike ' + '{:.0%}'.format(value[0])
                            + ' vs atm; notional ratio ' + '{:.0%}'.format(notionalRatio))
                            
    self.Plot('Chart Notional', legLabel, round(totalNotionalRatio * 100, 0))
    
    # information for allContractsByExpiryGroup --------------------------------
    
    # 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 date when we enter the positions
    entryDate = self.Time
    # get the next expiry date
    nextExpiryDate = listContracts[0].ID.Date
    # calculate how many days left to roll
    daysToExpiration = (nextExpiryDate - entryDate).days
    daysToRoll = daysToExpiration - daysToRollBeforeExpiration
    
    # save the underlying price at entry
    underlyingPriceAtEntry = self.Securities[expiryGroupSymbol].Price
    # portfolio value at purchase
    portfolioValueAtPurchase = self.Portfolio.TotalPortfolioValue
                                                    
    self.allContractsByExpiryGroup[expiryGroup] = {'legLabel': legLabel, 'legs': legs, 'listContracts': listContracts,
                                                    'entryDate': entryDate, 'nextExpiryDate': nextExpiryDate, 'daysToRoll': daysToRoll,
                                                    'underlyingPriceAtEntry': underlyingPriceAtEntry,
                                                    'initialContractsValue': initialContractsValue,
                                                    'remainingContractsValue': initialContractsValue,
                                                    'portfolioValueAtPurchase': portfolioValueAtPurchase,
                                                    'monetizingValue': 0}
                                                    
    self.Log('Entered new contracts for ' + legLabel + ': ' + str([x.Value for x in listContracts])
    + '. The contracts had a cost of ' + '{:.2%}'.format(initialContractsValue / portfolioValueAtPurchase)
    + ' of Portfolio Value and will be rolled in ' + str(daysToRoll) + ' days')
    
    if self.tradingLogs:
        self.Log(expiryGroup + ': entering new option contracts for next period; nextExpiryDate: ' + str(nextExpiryDate))
    
    return True
    
def CalculateAdjustedAnnualBudget(self, daysToRollBeforeExpiration, daysToExpiration):
    
    ''' Get adjusted annual budget (for rolling days and early rebalancing) for entire expiry group '''
    
    rollAdjustment = 365 / (self.expiryDays - daysToRollBeforeExpiration)
    
    if daysToExpiration is not None:
        earlyRebalancingAdjustment = 1 - ((math.ceil(daysToExpiration) - daysToRollBeforeExpiration) / self.expiryDays)
        if earlyRebalancingAdjustment < 0:
            earlyRebalancingAdjustment = 1
    else:
        earlyRebalancingAdjustment = 1
    
    adjustedAnnualBudget = (self.annualBudget / rollAdjustment) * earlyRebalancingAdjustment
    
    self.Log('adjustedAnnualBudget: ' + str(adjustedAnnualBudget))
    
    return adjustedAnnualBudget
    
def CalculateBudgetOptions(self, expiryGroupSymbol, adjustedAnnualBudget, annualBudgetPercent, remainingContractsValue):
                                    
    ''' Calculate the budget for options '''
    
    budgetOptions = adjustedAnnualBudget * annualBudgetPercent * (self.Portfolio[self.underlyingSymbol].HoldingsValue + self.Portfolio.Cash)
    
    self.Log('underlyingHoldingsValue + Cash: ' + str(self.Portfolio[self.underlyingSymbol].HoldingsValue + self.Portfolio.Cash))
    self.Log('budgetOptions: ' + str(budgetOptions))
    
    if remainingContractsValue is not None:
        budgetOptions = budgetOptions + remainingContractsValue
        self.Log('remainingContractsValue: ' + str(remainingContractsValue))
        self.Log('final budgetOptions: ' + str(budgetOptions))
        self.Log('end of early rebalancing ----------')
    
    # rebalancing underlying to make sure cash and underlying holdings are well balanced
    cashImbalance = self.Portfolio.Cash - budgetOptions
    if cashImbalance < 0:
        shares = round(cashImbalance / self.Securities[self.underlyingSymbol].Price) - 1
    else:
        shares = int(cashImbalance / self.Securities[self.underlyingSymbol].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)
        
    #self.Plot('Chart Budget', 'budgetOptions (%)', round(budgetOptions / self.Portfolio.TotalPortfolioValue, 4) * 100)
    
    return budgetOptions
    
def CalculateSumProdOptionPrices(self, dictContracts, dictLongs, dictShorts):
    
    ''' calculate the sum product of option prices needed for position sizing system based on number of contracts '''
    
    # 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()])
    
    sumProdOptionPrices = sumProdLongCalls + sumProdLongPuts + sumProdShortCalls + sumProdShortPuts
    
    return sumProdOptionPrices
    
def CalculateNumberOfContracts(self, budgetOptions, optionPrice, expiryGroup, strikeGroup):
        
    ''' Calculate the number of contracts to trade '''
    
    numberOfContracts = (budgetOptions / 100) / optionPrice
    
    if abs(numberOfContracts) < 1:
        self.specialTag = '(' + expiryGroup + '/' + strikeGroup + ' trade missing since < 1 contract on ' + str(self.Time.date()) + ')'
        if self.tradingLogs:
            self.Log(expiryGroup + '/' + strikeGroup + ': numberOfContracts to trade < 1')
        return None
        
    return numberOfContracts
    
def CalculateNotionalRatio(self, numberOfContracts, expiryGroupSymbol):
        
    ''' Calculate notional ratio coverage '''
    
    notionalRatio = 0
    
    numerator = numberOfContracts * 100 * self.Securities[expiryGroupSymbol].Price
    denominator = self.Portfolio[self.underlyingSymbol].Quantity * self.Securities[self.underlyingSymbol].Price

    if denominator != 0:
        notionalRatio = numerator / denominator
        
    return notionalRatio
    
def LiquidateOptionContracts(self, expiryGroup, openContracts, tag = 'no message'):
    
    ''' Liquidate any open option contracts '''

    # check the validity of the contracts
    validContracts = CheckContractValidity(self, openContracts, expiryGroup)
    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:
        if self.Securities[contract].Invested:
            self.Liquidate(contract, 'Liquidated - ' + expiryGroup + ' ' + tag)
            self.RemoveSecurity(contract)
            self.lastMinutePricesDict.pop(contract, None)
    
            if self.tradingLogs:
                self.Log(expiryGroup + '/' + str(contract) + ': liquidating due to ' + tag)
    
    legLabel = self.allContractsByExpiryGroup[expiryGroup]['legLabel']
    remainingContractsValue = self.allContractsByExpiryGroup[expiryGroup]['remainingContractsValue']
    self.Log('Liquidated contracts for ' + legLabel + ' due to ' + tag + ': ' + str([x.Value for x in openContracts])
    + '. The contracts had remaining value of ' + '${:,.2f}'.format(remainingContractsValue))
        
    return True

def CheckContractValidity(self, contracts, expiryGroup):
    
    ''' 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)
                
            #self.Plot('Chart Data Checks', 'contractPriceZero', 0)
            
            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, calendarType, maxExpiryDays, daysToRollBeforeExpiration, 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'})
            #self.Plot('Chart Data Checks', 'emptyOptionContracts', 0)

        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, calendarType = calendarType, maxExpiryDays = maxExpiryDays,
                                daysToRollBeforeExpiration = daysToRollBeforeExpiration)
    puts = FilterOptionContracts(self, optionSide = 'puts', symbol = expiryGroupSymbol, contracts = optionContracts,
                                strikePercents = strikePercentsForPuts, calendarType = calendarType, maxExpiryDays = maxExpiryDays,
                                daysToRollBeforeExpiration = daysToRollBeforeExpiration)

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

def FilterOptionContracts(self, optionSide, symbol, contracts, strikePercents, calendarType, maxExpiryDays, daysToRollBeforeExpiration):
    
    '''
    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
        calendarType: monthlies, weeklies or any
        maxExpiryDays: Number of days to find the expiration date of the contracts
    Return:
        A dictionary with the option contract for each strike percent
    '''
    
    if optionSide == 'calls':
        side = 0
    elif optionSide == 'puts':
        side = 1
    else:
        raise ValueError('optionSide parameter has to be either calls or puts!')
    
    # 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
                                        and x.ID.OptionRight == side]
    
    # fitler the contracts with expiry date below maxExpiryDays
    if calendarType == 'monthlies':
        contractList = [i for i in contracts if (OptionSymbol.IsStandardContract(i) and (i.ID.Date.date() - self.Time.date()).days <= maxExpiryDays
                                                and (i.ID.Date.date() - self.Time.date()).days > daysToRollBeforeExpiration)]
    elif calendarType == 'weeklies':
        contractList = [i for i in contracts if (OptionSymbol.IsWeekly(i) and (i.ID.Date.date() - self.Time.date()).days <= maxExpiryDays
                                                and (i.ID.Date.date() - self.Time.date()).days > daysToRollBeforeExpiration)]
    elif calendarType == 'any':
        contractList = [i for i in contracts if (i.ID.Date.date() - self.Time.date()).days <= maxExpiryDays]
    else:
        raise ValueError('calendarType must be either monthlies, weeklies or any')
        
    # 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])
            
    # 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.StrikePrice == strikePrice][0]
        
        # check if the final strike deviates too much from our strikePriceTarget
        contractId = strikeContracts[strikeGroup].Value.replace(' ', '')
        strikePriceTarget = atmStrike * (1 + strikePercents[strikeGroup])
        if contractId not in self.dataChecksDict['strikePriceTargetDeviation']:
            strikePriceTargetDeviation = abs((strikePrice / strikePriceTarget) - 1) * 100
            if strikePriceTargetDeviation > self.strikePriceTargetDeviationCheck:
                self.dataChecksDict['strikePriceTargetDeviation'].update({contractId: [strikePriceTarget, strikePrice]})
            #self.Plot('Chart Data Checks', str(maxExpiryDays) + 'x' + str(daysToRollBeforeExpiration) + ' strikePriceTargetDeviation (%)', strikePriceTargetDeviation)
        
        # check if the final expiry days deviates too much from our expiryDaysTarget
        if contractId not in self.dataChecksDict['expiryDaysTargetDeviation']:
            base = 30
            expiryDaysTarget = base * round(maxExpiryDays / base)
            expiryDays = (strikeContracts[strikeGroup].ID.Date.date() - self.Time.date()).days
            expiryDaysTargetDeviation = abs(expiryDaysTarget - expiryDays)
            if expiryDaysTargetDeviation > self.expiryDaysTargetDeviationCheck:
                self.dataChecksDict['expiryDaysTargetDeviation'].update({contractId: [expiryDaysTarget, expiryDays]})
            #self.Plot('Chart Data Checks', str(maxExpiryDays) + 'x' + str(daysToRollBeforeExpiration) + ' expiryDaysTargetDeviation (Days)', expiryDaysTargetDeviation)
    
    if strikeContracts:
        self.expiryDays = (furthestExpiryDate.date() - self.Time.date()).days
    
    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
        currentPrice = self.Securities[contract].Price
        currentVolume = self.Securities[contract].Volume
        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] = [currentPrice, currentBidPrice, currentAskPrice]
            continue
        
        else:
            lastPrice = self.lastMinutePricesDict[contract][0]
            lastBidPrice = self.lastMinutePricesDict[contract][1]
            lastAskPrice = self.lastMinutePricesDict[contract][2]
            
            # update prices
            self.lastMinutePricesDict[contract] = [currentPrice, 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)
            + '; currentPrice: ' + str(currentPrice) + '; lastPrice: ' + str(lastPrice) + '; currentVolume: ' + str(currentVolume)
            + '; 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)
            
            maxPctChange = pctChangeBid if abs(pctChangeBid) > abs(pctChangeAsk) else pctChangeAsk
            #self.Plot('Chart Data Checks', 'extremePriceChange (%)', maxPctChange)

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_08_29 v41
### ----------------------------------------------------------------------------
### 
### ----------------------------------------------------------------------------

from HelperFunctions import *
from System.Drawing import Color
from datetime import timedelta
import pandas as pd
import json
import copy

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, 7, 31) #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 remaining cash not used for options)
        underlyingTicker = 'IVV'
        
        # dictionary of dictionaries containing the different groups of option legs by expiry date
        # the format of the strikes is [strike percent, annualBudgetPercent]
        self.dictParameters = {'UnderlyingSynthetic': {'activate': False,
                                                        'ticker': 'SPY',
                                                        'legLabel': 'UnderlyingSynthetic',
                                                        'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any'
                                                        'positionSizing': 'multiplier', # options are 'multiplier' and 'dollar'
                                                        'maxExpiryDays': 10,
                                                        'rollMaxExpiryDays': 10,
                                                        'daysToRollBeforeExpiration': 1,
                                                        'monetizingLiquidate': 0.3, # contracts value change vs initial portfolio value
                                                        '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',
                                                'legLabel': 'ST Put',
                                                'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any'
                                                'positionSizing': 'dollar', # options are 'multiplier' and 'dollar'
                                                'maxExpiryDays': 130,
                                                'rollMaxExpiryDays': 130,
                                                'daysToRollBeforeExpiration': 30,
                                                'monetizingLiquidate': 0.15, # contracts value change vs initial portfolio value
                                                'underlyingPriceDownMoveLiquidate': None, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': None, # applied to puts
                                                'underlyingPriceLowerBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': None, # 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.5, 0.3], 'strikePercentB': [-0.45, None],
                                                        'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},
            
                                'ExpiryGroupB': {'activate': False,
                                                'ticker': 'SPY',
                                                'legLabel': 'MT Put 1',
                                                'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any'
                                                'positionSizing': 'dollar', # options are 'multiplier' and 'dollar'
                                                'maxExpiryDays': 220,
                                                'rollMaxExpiryDays': 401,    # at 401 because we don't want our rebal rules re: calls at 400 to effect our puts
                                                'daysToRollBeforeExpiration': 30,
                                                'monetizingLiquidate': 0.15, # contracts value change vs initial portfolio value
                                                'underlyingPriceDownMoveLiquidate': None, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': None, # applied to puts
                                                'underlyingPriceLowerBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': None, # 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.5, 0.35], 'strikePercentB': [-0.45, None],
                                                        'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},

                                'ExpiryGroupC': {'activate': False,
                                                'ticker': 'SPY',
                                                'legLabel': 'MT Put 2',
                                                'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any'
                                                'positionSizing': 'dollar', # options are 'multiplier' and 'dollar'
                                                'maxExpiryDays': 400,
                                                'rollMaxExpiryDays': 401,    # at 401 because we don't want our rebal rules re: calls at 400 to effect our puts
                                                'daysToRollBeforeExpiration': 30,
                                                'monetizingLiquidate': 0.15, # contracts value change vs initial portfolio value
                                                'underlyingPriceDownMoveLiquidate': None, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': None, # applied to puts
                                                'underlyingPriceLowerBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': None, # 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.5, 0.35], 'strikePercentB': [-0.45, None],
                                                        'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},

                               'ExpiryGroupD': {'activate': True,
                                                'ticker': 'SPY',
                                                'legLabel': 'MT Call 1',
                                                'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any'
                                                'positionSizing': 'dollar', # options are 'multiplier' and 'dollar'
                                                'maxExpiryDays': 400,
                                                'rollMaxExpiryDays': 400,
                                                'daysToRollBeforeExpiration': 30,
                                                'monetizingLiquidate': 0.15, # contracts value change vs initial portfolio value
                                                'underlyingPriceDownMoveLiquidate': None, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': None, # applied to puts (0.075)
                                                'underlyingPriceLowerBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': None, # number of days underlying price within lower/upper bound
                                                'calls': {'strikePercentA': [0.25, 0.5], 'strikePercentB': [-0.1, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                'puts': {'strikePercentA': [-0.3, None], 'strikePercentB': [-0.2, None],
                                                        'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},
            
                                'ExpiryGroupE': {'activate': True,
                                                'ticker': 'SPY',
                                                'legLabel': 'MT Call 2',
                                                'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any'
                                                'positionSizing': 'dollar', # options are 'multiplier' and 'dollar'
                                                'maxExpiryDays': 580,
                                                'rollMaxExpiryDays': 400,
                                                'daysToRollBeforeExpiration': 30,
                                                'monetizingLiquidate': 0.15, # contracts value change vs initial portfolio value
                                                'underlyingPriceDownMoveLiquidate': None, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': None, # applied to puts (0.075)
                                                'underlyingPriceLowerBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': None, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': None, # number of days underlying price within lower/upper bound
                                                'calls': {'strikePercentA': [0.25, 0.5], 'strikePercentB': [-0.05, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                'puts': {'strikePercentA': [-0.3, None], 'strikePercentB': [-0.3, None],
                                                        'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}},
                                                 
                                'ExpiryGroupF': {'activate': False,
                                                'ticker': 'SPY',
                                                'legLabel': 'Test',
                                                'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any'
                                                'positionSizing': 'multiplier', # options are 'multiplier' and 'dollar'
                                                'maxExpiryDays': 40,
                                                'rollMaxExpiryDays': 40,
                                                'daysToRollBeforeExpiration': 1,
                                                'monetizingLiquidate': None, # contracts value change vs initial portfolio value
                                                'underlyingPriceDownMoveLiquidate': None, # applied to calls
                                                'underlyingPriceUpMoveLiquidate': None, # applied to puts
                                                'underlyingPriceLowerBoundSidewaysLiquidate': -0.1, # applied to calls/puts
                                                'underlyingPriceUpperBoundSidewaysLiquidate': 0.1, # applied to calls/puts
                                                'underlyingPriceDaysSidewaysLiquidate': None, # number of days underlying price within lower/upper bound
                                                'calls': {'strikePercentA': [0, -1], 'strikePercentB': [0.15, None],
                                                        'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]},
                                                'puts': {'strikePercentA': [0, 1], '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 = {# SPY ---------------------------------------------------------------------------
                                        'SPY081219C00180000': 10, 'SPY081219C00195000': 10, 'SPY081219C00170000': 10,
                                        'SPY090320C00170000': 10, 'SPY081219C00175000': 10, 'SPY081219C00160000': 10,
                                        'SPY081219C00190000': 5, 'SPY080919C00190000': 5,
                                        'SPY160115C00245000': 50, 'SPY170317C00300000': 50, 'SPY160916C00275000': 50, 
                                        'SPY151219C00245000': 50, 'SPY160916C00290000': 50, 'SPY160115C00255000': 50,
                                        'SPY160617C00265000': 50, 'SPY170120C00290000': 50, 'SPY151219C00265000': 50,
                                        'SPY160617C00275000': 50, 'SPY151219C00255000': 50, 'SPY160916P00105000': 50,
                                        'SPY151219P00105000': 50, 'SPY150918P00110000': 50,
                                        
                                        # QQQ ---------------------------------------------------------------------------
                                        'QQQ150918P00060000': 50, 'QQQ160115P00054630': 50}
                                        
        self.avoidContracts = [] # ['SPY1508', 'SPY1509', 'SPY101218C', 'SPY100918C','SPY110319C'] # formats: 'SPY150918C00240000', 'SPY1012', 'SPY100918', 'SPY100918C'
        # check for extreme changes in minute price to report
        self.extremePriceChangeCheck = 50000 # percentage (e.g. 10 for 10%)
        # check for large deviations between our target strike price and final strike price selected
        self.strikePriceTargetDeviationCheck = 10 # percentage (e.g. 10 for 10%)
        # check for large deviations between our target expiry days and final expiry days selected
        self.expiryDaysTargetDeviationCheck = 10 # difference in number of days between expiry days target and selected
        
        # 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
        
        ### -------------------------------------------------------------------------------------------------------------------------
        
        # apply CustomSecurityInitializer
        self.SetSecurityInitializer(lambda x: CustomSecurityInitializer(self, x))
        
        # add underlying asset
        equity = self.AddEquity(underlyingTicker, Resolution.Minute)
        #equity.VolatilityModel = StandardDeviationOfReturnsVolatilityModel(30)
        self.underlyingSymbol = equity.Symbol
        
        # add more underlying assets if needed
        self.expiryGroupSymbols = {}
        self.optionsValueWinDict = {}
        self.initialOptionsValueDict = {}
        self.cumSumOptionsAttrDict = {}
        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
                    
                # add rolling windows to track contracts value
                self.optionsValueWinDict[expiryGroup] = RollingWindow[float](1)
                self.initialOptionsValueDict[expiryGroup] = RollingWindow[float](1)
                self.cumSumOptionsAttrDict[expiryGroup] = 0
        
        # get numner of active expiry groups
        self.numberOfActiveExpiryGroups = sum(parameters['activate'] for expiryGroup, parameters in self.dictParameters.items())
        
        # create dictionary with expiry groups belonging to the same rollMaxExpiryDays
        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)
                
        # add benchmark
        self.SetBenchmark(self.benchmarkTicker)
                
        # 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)
        
        # plot data checks
        #dataChecksPlot = Chart('Chart Data Checks')
        #dataChecksPlot.AddSeries(Series('extremePriceChange (%)', SeriesType.Line, '%'))
        #dataChecksPlot.AddSeries(Series('contractPriceZero', SeriesType.Scatter))
        #dataChecksPlot.AddSeries(Series('emptyOptionContracts', SeriesType.Scatter))
        #self.AddChart(dataChecksPlot)
        
        # plot budget
        #budgetPlot = Chart('Chart Budget')
        #budgetPlot.AddSeries(Series('budgetOptions (%)', SeriesType.Line, '%'))
        #self.AddChart(budgetPlot)
        
        # plot notional
        notionalPlot = Chart('Chart Notional')
        self.AddChart(notionalPlot)
        
        # plot options cumulative return
        optionsCumRetPlot = Chart('Chart Options Cumulative Attribution')
        self.AddChart(optionsCumRetPlot)
        
        #self.SetWarmup(30, Resolution.Daily)
        self.portValueWin = RollingWindow[float](1)
        
        self.allContractsByExpiryGroup = {}
        self.dailyPortfolioGreeksDict = {}
        self.lastMinutePricesDict = {}
        self.dataChecksDict = {'extremePriceChange': {}, 'strikePriceTargetDeviation': {},
                                'expiryDaysTargetDeviation': {}, 'contractAboveLimitPrice': {},
                                'contractPriceZero': {}, 'emptyOptionContracts': {}}
        self.expiryGroupsToRestartDict = {} # empty dict to store expiry groups to restart due to underlying price move
        self.dataCheckPrinted = False
        self.assignedOption = False
        self.initBenchmarkPrice = 0
        self.specialTag = ''
        self.day = 0
        
        # schedule functions
        self.Schedule.On(self.DateRules.EveryDay(self.underlyingSymbol), self.TimeRules.AfterMarketOpen(self.underlyingSymbol, 0), self.ReadInfoFromObjectStore)
        self.Schedule.On(self.DateRules.EveryDay(self.underlyingSymbol), self.TimeRules.At(10, 0), self.SaveInfoToObjectStore)
        
    def OnWarmupFinished(self):
        
        ''' Code to run after initialization '''
        
        if self.LiveMode and not self.Portfolio.Invested:
            jsonFile = json.dumps(self.allContractsByExpiryGroup)
            self.ObjectStore.Save('allContractsByExpiryGroup', jsonFile)
        
    def OnData(self, data):
        
        ''' Event triggering every time there is new data '''
        
        if self.Time.day != self.day:
            # simulate buy and hold the benchmark and plot its daily value --------------------------------------
            #UpdateBenchmarkValue(self)
            #self.Plot('Strategy Equity', self.benchmarkTicker, self.benchmarkValue)
            
            # update and plot options cumulative attribution ----------------------------------------------------
            for expiryGroup, infoDict in self.allContractsByExpiryGroup.items():
                UpdateOptionsCumulativeAttribution(self, expiryGroup, infoDict)
                
            # update the Portfolio Greeks dictionary ------------------------------------------------------------
            #todayPortfolioGreeks = UpdatePortfolioGreeks(self, data)
            
            #if todayPortfolioGreeks:
            #    for greek, value in todayPortfolioGreeks.items():
            #        self.Plot('Chart Portfolio Greeks', 'Daily Portfolio ' + greek, value)
                
            self.day = self.Time.day

        if not self.LiveMode:
            # 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
            
            # get a list with open option contracts ----------------------------
            openOptionContracts = GetOpenOptionContracts(self)
            
            # check if we got assigned and liquidate all remaining legs --------
            if self.assignedOption:
                # close all option contracts at once
                for contract in openOptionContracts:
                    self.Liquidate(contract, 'Liquidated - option assignment')
                    self.RemoveSecurity(contract)
                self.assignedOption = False
            
            # check on strange data --------------------------------------------
            try:
                CheckData(self, openOptionContracts)
            except BaseException as e:
                if self.tradingLogs:
                    self.Log('CheckData function failed due to: ' + str(e))
        
        # run below code only during this hour (halved bt time from 16 mins to 8 mins) ---------------------------
        if not self.Time.hour == 9:
            return

        # enter first contracts -----------------------------------------------------------------------------------
        for expiryGroup, parameters in self.dictParameters.items():
            if expiryGroup not in self.allContractsByExpiryGroup.keys() and parameters['activate']:
                enterContractsWorked = EnterOptionContracts(self, expiryGroup, self.expiryGroupSymbols[expiryGroup],
                                                            parameters['calendarType'], parameters['positionSizing'],
                                                            parameters['maxExpiryDays'], parameters['daysToRollBeforeExpiration'],
                                                            parameters['calls'], parameters['puts'], parameters['legLabel'])
        
        # roll contracts due to static or dynamic rebalancing rules ----------------------------------------------
        for expiryGroup, infoDict in self.allContractsByExpiryGroup.items():
             
            # skip expiryGroup that is already in expiryGroupsToRestartDict
            if expiryGroup in self.expiryGroupsToRestartDict:
                continue
            
            # get days left to roll
            entryDate = infoDict['entryDate']
            nextExpiryDate = infoDict['nextExpiryDate']
            daysToExpiration = (nextExpiryDate - self.Time).days
            daysToRollBeforeExpiration = self.dictParameters[expiryGroup]['daysToRollBeforeExpiration']
            daysToRoll = daysToExpiration - daysToRollBeforeExpiration
            infoDict['daysToRoll'] = daysToRoll
            
            # get list of contracts
            listContracts = infoDict['listContracts']
            
            # calculate change in contracts value since purchase
            remainingContractsValue = CalculateRemainingContractsValue(self, listContracts)
            infoDict['remainingContractsValue'] = remainingContractsValue
            
            # calculate monetizingValue as the change in options value since purchase divided by the portfolio value at purchase
            initialContractsValue = infoDict['initialContractsValue']
            contractsValueChange = remainingContractsValue - initialContractsValue
            portfolioValueAtPurchase = infoDict['portfolioValueAtPurchase']
            monetizingValue = contractsValueChange / portfolioValueAtPurchase
            infoDict['monetizingValue'] = monetizingValue

            # run dynamic rebalancing --------------------------------------------------------------
            dynamicRebalancing, dynamicRule = CheckDynamicRebalancing(self, expiryGroup, infoDict, monetizingValue)
            if dynamicRebalancing:
                rollMaxExpiryDays = self.dictParameters[expiryGroup]['rollMaxExpiryDays']
                for expiryGroup in self.sameRollMaxExpiryDaysExpiryGroups[str(rollMaxExpiryDays)]:
                    self.expiryGroupsToRestartDict[expiryGroup] = None
                    
                self.tag = '(' + expiryGroup + ' dynamic rebalancing rule triggered; ' + dynamicRule + ')'
                
                if self.tradingLogs:
                    self.Log(expiryGroup
                    + ': liquidating all option contracts with the same rollMaxExpiryDays due to dynamic rule: ' + dynamicRule
                    + '; underlyingPriceMove: ' + str(underlyingPriceMove) + '; monetizingValue: ' + str(monetizingValue))
            
            # run static rebalancing (expiration) ------------------------------------------------
            if daysToRoll < 0:
                # liquidate and roll
                liquidationWorked, enterOptionContractsWorked = RollExpiryGroup(self, infoDict, expiryGroup, 'static')
                
                if not liquidationWorked or not enterOptionContractsWorked:
                    continue
        
        # restart the entire expiry group due to dynamic rebalancing ---------------------------------------------------
        if len(list(self.expiryGroupsToRestartDict.keys())) > 0:
            for expiryGroup, infoDict in self.allContractsByExpiryGroup.items():
                if (expiryGroup in self.expiryGroupsToRestartDict
                and (self.expiryGroupsToRestartDict[expiryGroup] is None or self.Time > self.expiryGroupsToRestartDict[expiryGroup])):

                    # skip if it is time to roll by static rule
                    nextExpiryDate = infoDict['nextExpiryDate']
                    daysToExpiration = (nextExpiryDate - self.Time).days
                    daysToRollBeforeExpiration = self.dictParameters[expiryGroup]['daysToRollBeforeExpiration']
                    daysToRoll = daysToExpiration - daysToRollBeforeExpiration
                    infoDict['daysToRoll'] = daysToRoll
                    if daysToRoll < 0:
                        self.expiryGroupsToRestartDict.pop(expiryGroup)
                        continue
                    
                    # liquidate and roll
                    liquidationWorked, enterOptionContractsWorked = RollExpiryGroup(self, infoDict, expiryGroup, 'dynamic')
                
                    if not liquidationWorked:
                        continue
                    
                    if not enterOptionContractsWorked:
                        self.expiryGroupsToRestartDict[expiryGroup] = self.Time + timedelta(21)
                        continue
                    
                    self.expiryGroupsToRestartDict.pop(expiryGroup)
        
    def ReadInfoFromObjectStore(self):
    
        ''' Retrieve allContractsByExpiryGroup at the market open '''
        
        if self.LiveMode:
            try:
                # read dictionary
                jsonObj = self.ObjectStore.Read('allContractsByExpiryGroup')
                allContractsByExpiryGroupJson = json.loads(jsonObj)
                self.allContractsByExpiryGroup = copy.deepcopy(allContractsByExpiryGroupJson)
                
                for expiryGroup, infoDict in allContractsByExpiryGroupJson.items():
                    # convert entryDate and nextExpiryDate back to datetime objects
                    self.allContractsByExpiryGroup[expiryGroup]['entryDate'] = pd.to_datetime(infoDict['entryDate']).to_pydatetime()
                    self.allContractsByExpiryGroup[expiryGroup]['nextExpiryDate'] = pd.to_datetime(infoDict['nextExpiryDate']).to_pydatetime()
                    
                    # convert listContracts back to QuantConnect Symbol objects
                    listContracts = infoDict['listContracts']
                    listContractsSymbols = [self.Symbol(contract) for contract in listContracts]
                    self.allContractsByExpiryGroup[expiryGroup]['listContracts'] = listContractsSymbols
                    
                    # remove expiry groups with contracts that are no longer invested
                    for contractSymbol in listContractsSymbols:
                        if not self.Securities[contractSymbol].Invested:
                            self.allContractsByExpiryGroup.pop(expiryGroup, None)
            except:
                self.Log('allContractsByExpiryGroup not in the object store')
                
            self.Log('current allContractsByExpiryGroup: ' + str(self.allContractsByExpiryGroup))

    def SaveInfoToObjectStore(self):  
        
        ''' Save information to the object store at 10am '''
        
        if self.LiveMode:
            jsonFile = json.dumps(self.allContractsByExpiryGroup, default = FormatHandler)
            self.ObjectStore.Save('allContractsByExpiryGroup', jsonFile)
            
            self.Log('current allContractsByExpiryGroup: ' + str(self.allContractsByExpiryGroup))
            
        # send email notifications -----------------------------------------
        for expiryGroup, infoDict in self.allContractsByExpiryGroup.items():
            legLabel = infoDict['legLabel']
            daysToRoll = infoDict['daysToRoll']
            if daysToRoll == 6 or daysToRoll == 0:
                self.Log('rolling ' + legLabel + ' in ' + str(daysToRoll) + ' days')
                
    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