Overall Statistics |
Total Trades 38 Average Win 11.14% Average Loss -2.91% Compounding Annual Return 10.142% Drawdown 54.900% Expectancy 1.271 Net Profit 262.924% Sharpe Ratio 0.521 Probabilistic Sharpe Ratio 2.858% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 3.83 Alpha 0.038 Beta 1.059 Annual Standard Deviation 0.276 Annual Variance 0.076 Information Ratio 0.257 Tracking Error 0.171 Treynor Ratio 0.136 Total Fees $2306.36 |
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': 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.25, 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.9, # 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.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]}}} # 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 = .0000001 # 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' # 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