Overall Statistics |
Total Trades 408 Average Win 3.22% Average Loss -1.10% Compounding Annual Return 63.509% Drawdown 86.000% Expectancy 1.664 Net Profit 9772.275% Sharpe Ratio 1.172 Probabilistic Sharpe Ratio 2.559% Loss Rate 32% Win Rate 68% Profit-Loss Ratio 2.93 Alpha 1.311 Beta 1.323 Annual Standard Deviation 1.354 Annual Variance 1.833 Information Ratio 1.035 Tracking Error 1.331 Treynor Ratio 1.199 Total Fees $88575.42 |
import matplotlib.pyplot as plt from matplotlib.ticker import PercentFormatter from datetime import timedelta from scipy.optimize import curve_fit import pandas as pd import numpy as np import seaborn as sns sns.set_style('darkgrid') pd.plotting.register_matplotlib_converters() 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() ### ---------------------------------------------------------------------------- def ConsolidatePrices(df, consolidationPeriod): if consolidationPeriod is None: return df # consolidate and take the last value df = df.groupby(pd.Grouper(level = 'time', freq = consolidationPeriod), group_keys = False).apply(lambda x: x.tail(1)) return df def GetDailyClose(qb, tickers, startDate, endDate, resolution, consolidationPeriod = None): # add symbols symbols = [qb.AddEquity(ticker, resolution).Symbol for ticker in tickers] # get historical data df = qb.History(symbols, startDate, endDate, resolution)['close'] # unstack df = df.unstack(0) df.columns.name = None # rename index to avoid issues with symbol object naming df.columns = [qb.Securities[x].Symbol.Value + '_' for x in df.columns] # when using daily resolution, QuantConnect uses the date at midnight after the trading day # hence skipping Mondays and showing Saturdays. We avoid this by subtracting one day from the index df.index = df.index- timedelta(1) # apply consolidation df = ConsolidatePrices(df, consolidationPeriod) return df def Sigmoid(x, L, b, x0, k): y = (L - b) / (1 + np.exp(-k * (x - x0))) + b return y def CalculateCumulativeReturns(x, pricesType): if pricesType == 'log': cumulativeReturns = (x - x.shift()).fillna(0).cumsum() elif pricesType == 'raw': cumulativeReturns = x.pct_change().fillna(0).add(1).cumprod().add(-1) return cumulativeReturns def PlotSigmoidOnSplitDf(df, benchmarkDf = None, splitDf = 2): if benchmarkDf is not None: # calculate cumulative returns from log prices df = df.transform(lambda x: CalculateCumulativeReturns(x, 'log')) yLabel = 'Cumulative Log-Returns' else: yLabel = 'Log-Prices' plt.figure(figsize = (12, 8)) # plot plt.plot(df.index, df.values, '.', label = 'data') if benchmarkDf is not None: # filter benchmark data to match index sameDates = benchmarkDf.index.intersection(df.index) filteredBenchmarkDf = benchmarkDf.loc[benchmarkDf.index.isin(sameDates)] # calculate cumulative returns from log prices filteredBenchmarkDf = filteredBenchmarkDf.transform(lambda x: CalculateCumulativeReturns(x, 'log')) benchmarkData = filteredBenchmarkDf.values # plot plt.plot(df.index, benchmarkData, color = 'black', label = 'benchmark', alpha = 0.2) listOfDf = np.array_split(df, splitDf) xdataLast = 0 for df in listOfDf: xdata = np.array([i + xdataLast for i in range(0, len(df.index))]) ydata = df.values # fit function to get optimal parameters p0 = [np.max(ydata), np.median(xdata), 1, np.min(ydata)] popt, pcov = curve_fit(Sigmoid, xdata, ydata, p0, bounds = ([-np.inf, -np.inf, np.min(xdata), -np.inf], [np.inf, np.inf, np.max(xdata), np.inf]), maxfev = 100000) # get y by applying sigmoid y = Sigmoid(xdata, *popt) # plot plt.plot(df.index, y, label = 'fit') # update xdataLast xdataLast = xdata[-1] plt.title(df.name) plt.ylabel(yLabel) plt.legend() plt.show() def FitCurve(df): xdata = np.array([i for i in range(0, len(df.index))]) ydata = df.values if ydata[-1] < ydata[0]: return [] # fit function to get optimal parameters p0 = [np.max(ydata), np.min(ydata), np.median(xdata), 1] try: popt, pcov = curve_fit(Sigmoid, xdata, ydata, p0, bounds = ([-np.inf, -np.inf, np.min(xdata), -np.inf], [np.inf, np.inf, np.max(xdata), np.inf]), maxfev = 100000) except: return [] if popt[0] < popt[3]: return [] # get y by applying sigmoid y = Sigmoid(xdata, *popt) # get optimal coefficients optL = popt[0] # L optB = popt[1] # b optX0 = popt[2] # x0 optK = popt[3] # k # calculate total growth in period startPrice = ydata[0] endPrice = ydata[-1] periodReturn = (endPrice / startPrice) - 1 # calculate slope at midpoint slopeMidpoint = (optL - optB) * optK / 4 # calculate error residuals = np.square(ydata - y) mse = np.mean(np.square(residuals)) rmse = np.sqrt(mse) return [len(df.index), rmse, periodReturn, optL, optB, optX0, optK, slopeMidpoint, df.index, ydata, y]
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 UpdateEmailDictionary(self, expiryGroup, infoDict): ''' Update the email dictionary ''' legLabel = infoDict['legLabel'] daysToRoll = infoDict['daysToRoll'] monetizingValue = infoDict['monetizingValue'] monetizingLiquidate = self.strategyParametersDict[expiryGroup]['monetizingLiquidate'] monetizingValueVsTarget = monetizingValue / monetizingLiquidate if daysToRoll == 7 or daysToRoll == 1: self.emailDict[expiryGroup]['rollingContracts'] = '* Rolling ' + legLabel + ' in ' + str(daysToRoll) + ' days.\n' currentThreshold = self.emailDict[expiryGroup]['monetizingContracts'][1] if monetizingValueVsTarget >= currentThreshold: self.emailDict[expiryGroup]['monetizingContracts'] = ['* ' + legLabel + ' is at ' + '{:.0%}'.format(monetizingValueVsTarget) + ' of monetizing target.\n', currentThreshold + 0.25] def UpdateExpiryGroupsToRestart(self, expiryGroup, dynamicRule): ''' Adds expiry groups to expiryGroupsToRestartDict when needed to restart legs ''' tag = '(' + expiryGroup + '; ' + dynamicRule + ' triggered)' rollMaxExpiryDays = self.strategyParametersDict[expiryGroup]['rollMaxExpiryDays'] for expiryGroup in self.sameRollMaxExpiryDaysExpiryGroups[str(rollMaxExpiryDays)]: self.expiryGroupsToRestartDict[expiryGroup] = [None, tag] def CalculateDaysToExpiration(self, infoDict): ''' Calculate number of days left to contracts expiration ''' nextExpiryDate = infoDict['nextExpiryDate'] daysToExpiration = (nextExpiryDate.date() - self.Time.date()).days if daysToExpiration < 0: daysToExpiration = 0 return daysToExpiration def CalculateDaysToRoll(self, expiryGroup, infoDict): ''' Calculate number of days left to roll contracts ''' daysToExpiration = CalculateDaysToExpiration(self, infoDict) daysToRollBeforeExpiration = self.strategyParametersDict[expiryGroup]['daysToRollBeforeExpiration'] daysToRoll = daysToExpiration - daysToRollBeforeExpiration return daysToRoll def CalculateMonetizingValue(self, infoDict, remainingContractsValue): ''' Calculate monetizing value ''' initialContractsValue = infoDict['initialContractsValue'] contractsValueChange = remainingContractsValue - initialContractsValue portfolioValueAtPurchase = infoDict['portfolioValueAtPurchase'] monetizingValue = contractsValueChange / portfolioValueAtPurchase return monetizingValue def UpdateOptionsCumulativeAttribution(self, expiryGroup, infoDict): ''' Update and plot options cumulative attribution ''' # get variables from the leg listContracts = infoDict['listContracts'] numberOfContracts = infoDict['numberOfContracts'] initialContractsValue = infoDict['initialContractsValue'] legLabel = infoDict['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 if self.Portfolio[contract].Quantity > 0: remainingContractsValue += numberOfContracts * 100 * bidPrice else: remainingContractsValue += 0 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 + ' Attr', 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, infoDict): ''' Calculate remaining contracts value ''' try: remainingContractsValue = sum([(self.Portfolio[contract].Quantity * 100 * self.Securities[contract].BidPrice) for contract in infoDict['listContracts']]) except: remainingContractsValue = 0 # adjust the remaining contracts value to account for possible overlapping legs totalContracts = sum([self.Portfolio[contract].Quantity for contract in infoDict['listContracts']]) numberOfContracts = infoDict['numberOfContracts'] if totalContracts > 0: legRatio = numberOfContracts / totalContracts remainingContractsValue = legRatio * remainingContractsValue return remainingContractsValue def CheckDynamicRebalancing(self, expiryGroup, infoDict, monetizingValue): ''' Check dynamic rebalancing rules ''' dynamicRebalancing = False dynamicRule = '' # get rules parameters ---------------------------------------------------- # underlying moves up/down underlyingPriceDownMoveLiquidate = self.strategyParametersDict[expiryGroup]['underlyingPriceDownMoveLiquidate'] underlyingPriceUpMoveLiquidate = self.strategyParametersDict[expiryGroup]['underlyingPriceUpMoveLiquidate'] # underlying sideways underlyingPriceLowerBoundSidewaysLiquidate = self.strategyParametersDict[expiryGroup]['underlyingPriceLowerBoundSidewaysLiquidate'] underlyingPriceUpperBoundSidewaysLiquidate = self.strategyParametersDict[expiryGroup]['underlyingPriceUpperBoundSidewaysLiquidate'] underlyingPriceDaysSidewaysLiquidate = self.strategyParametersDict[expiryGroup]['underlyingPriceDaysSidewaysLiquidate'] # monetizing threshold monetizingLiquidate = self.strategyParametersDict[expiryGroup]['monetizingLiquidate'] # calculate underlyingPriceMove -------------------------------------------- underlyingSymbol = self.expiryGroupSymbols[expiryGroup] underlyingCurrentPrice = self.Securities[underlyingSymbol].Price underlyingPriceAtEntry = infoDict['underlyingPriceAtEntry'] underlyingPriceMove = (underlyingCurrentPrice / underlyingPriceAtEntry) - 1 # check for underlying moves ----------------------------------------------- legs = infoDict['legs'] if legs == 'calls' and underlyingPriceDownMoveLiquidate is not None: if underlyingPriceMove < underlyingPriceDownMoveLiquidate: dynamicRebalancing = True dynamicRule = 'underlyingPriceDownMoveLiquidate' elif legs == 'puts' and underlyingPriceUpMoveLiquidate is not None: if underlyingPriceMove > underlyingPriceUpMoveLiquidate: dynamicRebalancing = True dynamicRule = 'underlyingPriceUpMoveLiquidate' elif underlyingPriceDownMoveLiquidate is not None and underlyingPriceUpMoveLiquidate is not None: if underlyingPriceMove < underlyingPriceDownMoveLiquidate or underlyingPriceMove > underlyingPriceUpMoveLiquidate: dynamicRebalancing = True dynamicRule = 'underlyingPriceDownMoveLiquidate/underlyingPriceUpMoveLiquidate' 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: dynamicRebalancing = True dynamicRule = 'underlyingPriceLowerBoundSidewaysLiquidate/underlyingPriceUpperBoundSidewaysLiquidate' # check for monetization --------------------------------------------------- if monetizingLiquidate is not None and monetizingValue > monetizingLiquidate: dynamicRebalancing = True dynamicRule = 'monetizingLiquidate' # check the validity of the contracts first to avoid monetizing due to wrong prices listContracts = infoDict['listContracts'] validContracts = CheckContractValidity(self, listContracts, expiryGroup) if not validContracts: dynamicRebalancing = False dynamicRule = '' if dynamicRebalancing and self.tradingLogs: self.Log(expiryGroup + ': liquidating all option contracts with the same rollMaxExpiryDays due to dynamic rule: ' + dynamicRule + '; underlyingPriceMove: ' + str(underlyingPriceMove) + '; monetizingValue: ' + str(monetizingValue)) return dynamicRebalancing, dynamicRule def RollExpiryGroup(self, infoDict, expiryGroup, rollingType, message): ''' Liquidate existing contracts and enter new ones ''' parameters = self.strategyParametersDict[expiryGroup] if rollingType == 'static': daysToExpiration = None expiryDays = parameters['rollMaxExpiryDays'] else: daysToExpiration = CalculateDaysToExpiration(self, infoDict) expiryDays = parameters['maxExpiryDays'] self.Log(parameters['legLabel'] + '; start of ' + rollingType + ' early rebalancing ----------') listContracts = infoDict['listContracts'] if self.tradingLogs: LoggingContractsInfo(self, listContracts) # we add the remaining contracts value to the budget remainingContractsValue = CalculateRemainingContractsValue(self, infoDict) 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, parameters['calendarType'], parameters['positionSizing'], expiryDays, parameters['daysToRollBeforeExpiration'], parameters['calls'], parameters['puts'], parameters['legLabel'], daysToExpiration, remainingContractsValue, rollingType) 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 CalculateImbalanceDollarAllocation(self, symbol, targetAllocationPercent, reallocationType, cashImbalanceAmount, totalCashSymbolsHoldings): ''' Calculate allocation needed per symbol ''' # calculate current allocation percent and pp deviation from target percent if totalCashSymbolsHoldings != 0: currentAllocationPercent = self.Portfolio[symbol].HoldingsValue / totalCashSymbolsHoldings ppAllocationDeviationFromTarget = currentAllocationPercent - targetAllocationPercent else: currentAllocation = 0 ppAllocationDeviationFromTarget = 0 # calculate the dollar amount needed to reallocate if reallocationType == 'trimming': if not self.Portfolio[symbol].Invested: imbalanceDollarAllocation = cashImbalanceAmount * targetAllocationPercent else: imbalanceDollarAllocation = cashImbalanceAmount * currentAllocationPercent elif reallocationType == 'rebalancing': dollarTargetAllocation = (totalCashSymbolsHoldings + cashImbalanceAmount) * targetAllocationPercent imbalanceDollarAllocation = dollarTargetAllocation - self.Portfolio[symbol].HoldingsValue return ppAllocationDeviationFromTarget, imbalanceDollarAllocation def ReallocateCashAssets(self, reallocationType = 'trimming', budgetOptions = 0): ''' Reallocate holdings for the cash assets for trimming or rebalancing reasons ''' # calculate imbalanceAmount cashImbalanceAmount = self.Portfolio.Cash - budgetOptions # calculate total holdings for cash symbols totalCashSymbolsHoldings = sum(self.Portfolio[symbol].HoldingsValue for symbol in self.dictCashSymbols) infoLogBefore = ('information before ' + reallocationType + ' cash assets' + '; TotalPortfolioValue: ' + str(self.Portfolio.TotalPortfolioValue) + '; cashSymbols allocation percent: ' + str({symbol.Value: round(self.Portfolio[symbol].HoldingsValue / totalCashSymbolsHoldings, 2) if totalCashSymbolsHoldings != 0 else 0 for symbol in self.dictCashSymbols}) + '; Cash: ' + str(self.Portfolio.Cash)) sharesBySymbol = {} triggerRebalancing = False for symbol, targetAllocationList in self.dictCashSymbols.items(): # calculate the imbalance amount to reallocate ppAllocationDeviationFromTarget, imbalanceDollarAllocation = CalculateImbalanceDollarAllocation(self, symbol, targetAllocationList[0], reallocationType, cashImbalanceAmount, totalCashSymbolsHoldings) # calculate number of shares to reallocate per symbol if imbalanceDollarAllocation == 0: continue elif imbalanceDollarAllocation < 0: sharesBySymbol[symbol] = round(imbalanceDollarAllocation / self.Securities[symbol].Price) - 1 else: sharesBySymbol[symbol] = int(imbalanceDollarAllocation / self.Securities[symbol].Price) # check for triggerRebalancing if (targetAllocationList[1] is not None and (ppAllocationDeviationFromTarget < targetAllocationList[1][0] or ppAllocationDeviationFromTarget > targetAllocationList[1][1])): triggerRebalancing = True # place orders if reallocationType == 'trimming' or triggerRebalancing: # sort dictionary in ascending order by number of shares in order to place potential sell orders first # and avoid margin issues sharesBySymbol = {k: v for k, v in sorted(sharesBySymbol.items(), key = lambda item: item[1])} for symbol, shares in sharesBySymbol.items(): self.MarketOrder(symbol, shares, False, str(reallocationType + ' Cash Asset ' + self.specialTag)) # calculate total holdings for cash symbols totalCashSymbolsHoldings = sum(self.Portfolio[symbol].HoldingsValue for symbol in self.dictCashSymbols) infoLogAfter = ('information after ' + reallocationType + ' cash assets' + '; TotalPortfolioValue: ' + str(self.Portfolio.TotalPortfolioValue) + '; cashSymbols allocation percent: ' + str({symbol.Value: round(self.Portfolio[symbol].HoldingsValue / totalCashSymbolsHoldings, 2) if totalCashSymbolsHoldings != 0 else 0 for symbol in self.dictCashSymbols}) + '; Cash: ' + str(self.Portfolio.Cash)) if self.tradingLogs: if reallocationType == 'trimming' or triggerRebalancing: self.Log(infoLogBefore) self.Log(infoLogAfter) self.specialTag = '' def EnterOptionContracts(self, expiryGroup, calendarType, positionSizing, maxExpiryDays, daysToRollBeforeExpiration, dictCalls, dictPuts, legLabel, daysToExpiration = None, remainingContractsValue = None, rollingType = ''): ''' Enter option contracts ''' # get the symbol for the group expiryGroupSymbol = self.expiryGroupSymbols[expiryGroup] # 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, expiryGroup, calendarType, maxExpiryDays, daysToRollBeforeExpiration, dictValidCalls, dictValidPuts, legLabel, rollingType) 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} # get relevant variables --------------------------------------------------- # check if we have calls/puts or both 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 total number of days to expiration totalExpiryDays = (nextExpiryDate.date() - entryDate.date()).days self.Plot('Chart Expiry Days', legLabel + ' Expiry', totalExpiryDays) # calculate how many days left to roll daysToRoll = totalExpiryDays - daysToRollBeforeExpiration # save the underlying price at entry underlyingPriceAtEntry = self.Securities[expiryGroupSymbol].Price # portfolio value at purchase portfolioValueAtPurchase = self.Portfolio.TotalPortfolioValue # entering legs ------------------------------------------------------------ # get adjusted budget adjustedAnnualBudget = CalculateAdjustedAnnualBudget(self, totalExpiryDays, daysToRollBeforeExpiration, daysToExpiration) # apply multiplier budget for position sizing ----------- if positionSizing == 'multiplier': # calculate the budget for options budgetOptions = CalculateBudgetOptions(self, expiryGroupSymbol, adjustedAnnualBudget, 1, remainingContractsValue, legLabel) # 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 # enter longs and shorts -------------------------------- initialContractsValue = 0 totalNumberOfContracts = 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, legLabel) 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 totalNumberOfContracts += shortNumberOfContracts # 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, legLabel) 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 totalNumberOfContracts += longNumberOfContracts # 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 + ' Notional', round(totalNotionalRatio * 100, 0)) # information for allContractsByExpiryGroup -------------------------------- self.allContractsByExpiryGroup[expiryGroup] = {'legLabel': legLabel, 'legs': legs, 'listContracts': listContracts, 'entryDate': entryDate, 'nextExpiryDate': nextExpiryDate, 'daysToRoll': daysToRoll, 'underlyingPriceAtEntry': underlyingPriceAtEntry, 'numberOfContracts': totalNumberOfContracts, 'initialContractsValue': initialContractsValue, 'remainingContractsValue': initialContractsValue, 'portfolioValueAtPurchase': portfolioValueAtPurchase, 'monetizingValue': 0} if self.LiveMode: self.emailDict[expiryGroup]['enterContracts'] = ('* Entered ' + str(totalNumberOfContracts) + ' 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.\n') if self.tradingLogs: self.Log(expiryGroup + ': entering new option contracts for next period; nextExpiryDate: ' + str(nextExpiryDate)) return True def CalculateAdjustedAnnualBudget(self, totalExpiryDays, daysToRollBeforeExpiration, daysToExpiration): ''' Get adjusted annual budget (for rolling days and early rebalancing) for entire expiry group ''' rollAdjustment = 365 / (totalExpiryDays - daysToRollBeforeExpiration) if daysToExpiration is not None: earlyRebalancingAdjustment = 1 - ((math.ceil(daysToExpiration) - daysToRollBeforeExpiration) / totalExpiryDays) 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, legLabel): ''' Calculate the budget for options ''' # calculate budget options budgetOptions = adjustedAnnualBudget * annualBudgetPercent * self.Portfolio.TotalPortfolioValue self.Log('TotalPortfolioValue: ' + str(self.Portfolio.TotalPortfolioValue)) 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 ----------') # trimming cash assets to make sure cash and cashSymbols holdings are well balanced ReallocateCashAssets(self, reallocationType = 'trimming', budgetOptions = budgetOptions) self.Plot('Chart Budget', legLabel + ' 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 = int((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 = sum(self.Portfolio[symbol].Quantity * self.Securities[symbol].Price for symbol in self.dictCashSymbols) 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.lastMinutePricesDict.pop(contract, None) if self.tradingLogs: self.Log(expiryGroup + '/' + str(contract) + ': liquidating due to ' + tag) if self.LiveMode: legLabel = self.allContractsByExpiryGroup[expiryGroup]['legLabel'] remainingContractsValue = self.allContractsByExpiryGroup[expiryGroup]['remainingContractsValue'] self.emailDict[expiryGroup]['liquidateContracts'] = ('* Liquidated contracts for ' + legLabel + ' due to ' + tag + ': ' + str([x.Value for x in openContracts]) + '. The contracts had remaining value of ' + '${:,.2f}'.format(remainingContractsValue) + '.\n') 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 if 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, expiryGroup, calendarType, maxExpiryDays, daysToRollBeforeExpiration, dictCalls, dictPuts, legLabel, rollingType): ''' Get the final option contracts to trade ''' # get a list with the option chain for the underlying symbol and the current date expiryGroupSymbol = self.expiryGroupSymbols[expiryGroup] contracts = self.OptionChainProvider.GetOptionContractList(expiryGroupSymbol, self.Time.date()) if len(contracts) == 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, 'calls', expiryGroup, contracts, strikePercentsForCalls, calendarType, maxExpiryDays, daysToRollBeforeExpiration, legLabel, rollingType) puts = FilterOptionContracts(self, 'puts', expiryGroup, contracts, strikePercentsForPuts, calendarType, maxExpiryDays, daysToRollBeforeExpiration, legLabel, rollingType) dictContracts = {'calls': calls, 'puts': puts} return dictContracts def GetFurthestContracts(self, optionSide, contracts, calendarType, maxExpiryDays, daysToRollBeforeExpiration): ''' Get the furthest expiration contracts given maxExpiryDays ''' 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 but greater than daysToRollBeforeExpiration 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') if len(contractList) == 0: return None, None # 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] return furthestExpiryDate, furthestContracts def FilterOptionContracts(self, optionSide, expiryGroup, contracts, strikePercents, calendarType, maxExpiryDays, daysToRollBeforeExpiration, legLabel, rollingType): ''' Filter a list of option contracts using the given expiry and strike arguments ''' if not strikePercents: return {} # get furthest expiration contracts furthestExpiryDate, furthestContracts = GetFurthestContracts(self, optionSide, contracts, calendarType, maxExpiryDays, daysToRollBeforeExpiration) if furthestContracts is None: return {} # calculate how many days to roll this new contract that we just found daysToRollNewContractThisLeg = (furthestExpiryDate.date() - self.Time.date()).days - daysToRollBeforeExpiration # check restarting when static rolling due to short distance between legs if rollingType == 'static': isRestartingNeeded = CheckRestartingLegs(self, expiryGroup, legLabel, optionSide, contracts, daysToRollNewContractThisLeg) if isRestartingNeeded: return {} # find the strike price for ATM options expiryGroupSymbol = self.expiryGroupSymbols[expiryGroup] atmStrike = sorted(furthestContracts, key = lambda x: abs(x.ID.StrikePrice - self.Securities[expiryGroupSymbol].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] CheckStrikeExpiryDeviations(self, strikeGroup, strikeContracts, strikePercents, atmStrike, strikePrice, maxExpiryDays, furthestExpiryDate) return strikeContracts def CheckRestartingLegs(self, expiryGroup, legLabel, optionSide, contracts, daysToRollNewContractThisLeg): ''' The objective is to keep certain legs (of same term) away from each other by N days. We will restart the legs in case that helps us maintain that distance. ''' if self.minDaysBetweenLegs is None: return False # calculate how many days to roll a restarted contract for this leg parameters = self.strategyParametersDict[expiryGroup] furthestExpiryDate, furthestContracts = GetFurthestContracts(self, optionSide, contracts, parameters['calendarType'], parameters['maxExpiryDays'], parameters['daysToRollBeforeExpiration']) if furthestContracts is None: return False daysToRollRestartedContractThisLeg = (furthestExpiryDate.date() - self.Time.date()).days - parameters['daysToRollBeforeExpiration'] # now we find other legs with the same intended horizon and check the distance between legs for expiryGroup, infoDict in self.allContractsByExpiryGroup.items(): isInvested = sum(self.Securities[contract].Invested for contract in infoDict['listContracts']) > 0 if infoDict['legLabel'] != legLabel and infoDict['legLabel'][:2] == legLabel[:2] and infoDict['legs'] == optionSide: if not isInvested: dynamicRule = 'restartingForDistance' UpdateExpiryGroupsToRestart(self, expiryGroup, dynamicRule) return True # calculate how many days to roll a restarted contract for the other leg ----------------- parameters = self.strategyParametersDict[expiryGroup] furthestExpiryDate, furthestContracts = GetFurthestContracts(self, optionSide, contracts, parameters['calendarType'], parameters['maxExpiryDays'], parameters['daysToRollBeforeExpiration']) if furthestContracts is None: return False daysToRollRestartedContractOtherLeg = (furthestExpiryDate.date() - self.Time.date()).days - parameters['daysToRollBeforeExpiration'] # get the distance between legs if restarting distanceRestarting = abs(daysToRollRestartedContractOtherLeg - daysToRollRestartedContractThisLeg) # calculate how many days to roll the currently open contract for the other leg ----------- daysToRollCurrentContractOtherLeg = CalculateDaysToRoll(self, expiryGroup, infoDict) # get the distance if just rolling distanceRolling = abs(daysToRollCurrentContractOtherLeg - daysToRollNewContractThisLeg) # compare distances and restart if needed -------------------------------------------------- if distanceRestarting > distanceRolling and distanceRestarting > self.minDaysBetweenLegs: dynamicRule = 'restartingForDistance' UpdateExpiryGroupsToRestart(self, expiryGroup, dynamicRule) return True return False def CheckStrikeExpiryDeviations(self, strikeGroup, strikeContracts, strikePercents, atmStrike, strikePrice, maxExpiryDays, furthestExpiryDate): ''' Check for deviations in strike price and expiry vs their targets and store in dataChecksDict ''' contractId = strikeContracts[strikeGroup].Value.replace(' ', '') # check if the final strike deviates too much from our strikePriceTarget if contractId not in self.dataChecksDict['strikePriceTargetDeviation']: strikePriceTarget = atmStrike * (1 + strikePercents[strikeGroup]) 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 = (furthestExpiryDate.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) 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(' ', '') if self.tradingLogs: 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_11_02 v46 ### ---------------------------------------------------------------------------- ### Changed budgetOptions calculations to use TotalPortfolioValue ### ---------------------------------------------------------------------------- import pandas as pd import json import copy import HelperFunctions as helpers from StrategiesParameters import GetStrategyParametersDict from System.Drawing import Color from datetime import timedelta class OptionsBasketStrategyTemplateAlgorithm(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(2011, 4, 1) #20090301 # just comment out end date to run through today self.SetEndDate(2020, 8, 1) #20100301 self.SetCash(1000000) # add emails if needed for Live trading self.emails = [] # select a ticker as benchmark (will plot Buy&Hold of this benchmark) self.benchmarkTicker = 'QQQ' # select strategy ticker # current options are SPD, SPUC, SPYC, QQD, QQU, QQC and testing (for testing parameters) # for more information check the StrategiesParameters.py script self.strategyTicker = 'testing' # select tickers for cash assets, their target allocations from cash and boundaries for rebalancing due to allocation deviation from target # (holdings of these assets will be 100% of remaining cash not used for options) # {ticker: [allocation, [lower bound percentage point deviation from target, upper bound percentage point deviation from target]]} dictCashTickers = {'QQQ': [0.75, None], 'TSLA': [0.25, [-0.05, 0.25]]} # 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.12 # this variable applies to the function CheckRestarting which tries to keep a certain number of days between legs with same horizon # select None to deactivate self.minDaysBetweenLegs = None # None or an integer # get the dictionary of dictionaries containing all the parameters needed for the strategy self.strategyParametersDict = GetStrategyParametersDict(self.strategyTicker) # 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 = True # 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, 'SPY101218C00135000': 50, 'SPY100619C00135000': 50, 'SPY150918C00240000': 50, 'SPY160115C00245000': 50, 'SPY170317C00300000': 50, 'SPY160916C00275000': 50, 'SPY151219C00245000': 50, 'SPY160916C00290000': 50, 'SPY160115C00255000': 50, 'SPY160617C00265000': 50, 'SPY170120C00290000': 50, 'SPY151219C00265000': 50, 'SPY160617C00275000': 50, 'SPY151219C00255000': 50, 'SPY101218C00130000': 50, 'SPY151219P00105000': 50, 'SPY150918P00110000': 50, 'SPY160916P00105000': 50, 'SPY150918P00110000': 50, 'SPY150918P00120000': 50, 'SPY100619P00055000': 50, 'SPY100619P00050000': 50, # QQQ --------------------------------------------------------------------------- 'QQQ150918P00060000': 50, 'QQQ160115P00054630': 50, 'QQQ150918P00070000': 50, 'QQQ151016P00070000': 50, 'QQQ151218P00059630': 50, 'QQQ160115P00049630': 50, 'QQQ150918P00055000': 50, 'QQQ160115P00044630': 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: helpers.CustomSecurityInitializer(self, x)) # add cash assets self.dictCashSymbols = {} for ticker, targetAllocationPercent in dictCashTickers.items(): equity = self.AddEquity(ticker, Resolution.Minute) equity.VolatilityModel = StandardDeviationOfReturnsVolatilityModel(30) self.dictCashSymbols[equity.Symbol] = targetAllocationPercent # add other underlying assets if needed self.expiryGroupSymbols = {} self.optionsValueWinDict = {} self.initialOptionsValueDict = {} self.cumSumOptionsAttrDict = {} self.emailDict = {} for expiryGroup, parameters in self.strategyParametersDict.items(): if parameters['activate']: ticker = parameters['ticker'] self.expiryGroupSymbols[expiryGroup] = self.AddEquity(ticker, Resolution.Minute).Symbol # add rolling windows to track contracts value self.optionsValueWinDict[expiryGroup] = RollingWindow[float](1) self.initialOptionsValueDict[expiryGroup] = RollingWindow[float](1) self.cumSumOptionsAttrDict[expiryGroup] = 0 # add email dictionary self.emailDict[expiryGroup] = {'enterContracts': '', 'liquidateContracts': '', 'rollingContracts': '', 'monetizingContracts': ['', 0.25]} # get numner of active expiry groups self.numberOfActiveExpiryGroups = sum(parameters['activate'] for expiryGroup, parameters in self.strategyParametersDict.items()) # create dictionary with expiry groups belonging to the same rollMaxExpiryDays rollMaxExpiryDays = [parameters['rollMaxExpiryDays'] for expiryGroup, parameters in self.strategyParametersDict.items() if parameters['activate']] self.sameRollMaxExpiryDaysExpiryGroups = {str(elem): [] for elem in rollMaxExpiryDays} for expiryGroup, parameters in self.strategyParametersDict.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') 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) # plot options expiry days left expiryDaysPlot = Chart('Chart Expiry Days') self.AddChart(expiryDaysPlot) self.SetWarmup(30, Resolution.Daily) self.portValueWin = RollingWindow[float](1) self.allContractsByExpiryGroup = {} self.dailyPortfolioGreeksDict = {} self.lastMinutePricesDict = {} self.expiryGroupsToRestartDict = {} # empty dict to store expiry groups to restart due to underlying price move self.dataChecksDict = {'extremePriceChange': {}, 'strikePriceTargetDeviation': {}, 'expiryDaysTargetDeviation': {}, 'contractAboveLimitPrice': {}, 'contractPriceZero': {}, 'emptyOptionContracts': {}} self.dataCheckPrinted = False self.assignedOption = False self.initBenchmarkPrice = 0 self.specialTag = '' self.day = 0 self.minute = 0 # schedule functions equitySymbol = list(self.dictCashSymbols.keys())[0] self.Schedule.On(self.DateRules.EveryDay(equitySymbol), self.TimeRules.AfterMarketOpen(equitySymbol, 0), self.ReadInfoFromObjectStore) self.Schedule.On(self.DateRules.EveryDay(equitySymbol), self.TimeRules.At(9, 55), self.RebalanceCashAssets) self.Schedule.On(self.DateRules.EveryDay(equitySymbol), self.TimeRules.At(10, 0), self.SaveInfoToObjectStore) self.Schedule.On(self.DateRules.EveryDay(equitySymbol), self.TimeRules.At(10, 0), self.SendEmailNotification) # dictionary manually inputted here containing a replica of the information contained in self.allContractsByExpiryGroup # we use this in LiveMode when the algo fails to save the object so we force it to save this instead self.manualAllContractsByExpiryGroup = {} def OnWarmupFinished(self): ''' Code to run after initialization ''' if self.LiveMode: if not self.Portfolio.Invested: jsonFile = json.dumps(self.allContractsByExpiryGroup) self.ObjectStore.Save('allContractsByExpiryGroup', jsonFile) if self.manualAllContractsByExpiryGroup: jsonFile = json.dumps(self.manualAllContractsByExpiryGroup) self.ObjectStore.Save('allContractsByExpiryGroup', jsonFile) self.ReadInfoFromObjectStore() for email in self.emails: self.Notify.Email(email, self.strategyName + ' is Live!', self.strategyName + ' is Live!') def OnData(self, data): ''' Event triggering every time there is new data ''' if self.Time.minute == self.minute: self.expiryGroupsToRestartDict = {} return self.minute = self.Time.minute if self.Time.day != self.day: # simulate buy and hold the benchmark and plot its daily value -------------------------------------- helpers.UpdateBenchmarkValue(self) self.Plot('Strategy Equity', self.benchmarkTicker, self.benchmarkValue) # update the Portfolio Greeks dictionary ------------------------------------------------------------ #todayPortfolioGreeks = helpers.UpdatePortfolioGreeks(self, data) #if todayPortfolioGreeks: # for greek, value in todayPortfolioGreeks.items(): # self.Plot('Chart Portfolio Greeks', 'Daily Portfolio ' + greek, value) # save today starting portfolio value self.todayStartingPortfolioValue = self.Portfolio.TotalPortfolioValue # plotting --------------------------------------------------------- for expiryGroup, infoDict in self.allContractsByExpiryGroup.items(): helpers.UpdateOptionsCumulativeAttribution(self, expiryGroup, infoDict) # get days left to expiration daysToExpiration = helpers.CalculateDaysToExpiration(self, infoDict) legLabel = infoDict['legLabel'] self.Plot('Chart Expiry Days', legLabel + ' Expiry', daysToExpiration) 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 = helpers.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: helpers.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.strategyParametersDict.items(): if expiryGroup not in self.allContractsByExpiryGroup.keys() and parameters['activate']: enterContractsWorked = helpers.EnterOptionContracts(self, 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(): # get days left to roll daysToRoll = helpers.CalculateDaysToRoll(self, expiryGroup, infoDict) infoDict['daysToRoll'] = daysToRoll # skip expiryGroup that is already in expiryGroupsToRestartDict # unless it is time to roll due to static rule if expiryGroup in self.expiryGroupsToRestartDict: if daysToRoll <= 0: self.expiryGroupsToRestartDict.pop(expiryGroup) else: continue # calculate change in contracts value since purchase remainingContractsValue = helpers.CalculateRemainingContractsValue(self, infoDict) infoDict['remainingContractsValue'] = remainingContractsValue # calculate monetizingValue as the change in options value since purchase divided by the portfolio value at purchase monetizingValue = helpers.CalculateMonetizingValue(self, infoDict, remainingContractsValue) infoDict['monetizingValue'] = monetizingValue # check dynamic rebalancing ------------------------------------------------------------- dynamicRebalancing, dynamicRule = helpers.CheckDynamicRebalancing(self, expiryGroup, infoDict, monetizingValue) if dynamicRebalancing: helpers.UpdateExpiryGroupsToRestart(self, expiryGroup, dynamicRule) continue # run static rebalancing (expiration) ------------------------------------------------ if daysToRoll <= 0: # liquidate and roll liquidationWorked, enterOptionContractsWorked = helpers.RollExpiryGroup(self, infoDict, expiryGroup, 'static', 'static rebalancing') # restart the entire expiry group due to dynamic rebalancing --------------------------------------------------- if self.expiryGroupsToRestartDict: for expiryGroup, infoDict in self.allContractsByExpiryGroup.items(): if (expiryGroup in self.expiryGroupsToRestartDict and (self.expiryGroupsToRestartDict[expiryGroup][0] is None or self.Time > self.expiryGroupsToRestartDict[expiryGroup][0])): # liquidate and roll message = self.expiryGroupsToRestartDict[expiryGroup][1] liquidationWorked, enterOptionContractsWorked = helpers.RollExpiryGroup(self, infoDict, expiryGroup, 'dynamic', message) if not liquidationWorked: continue if not enterOptionContractsWorked: self.expiryGroupsToRestartDict[expiryGroup][0] = self.Time + timedelta(days = 21) continue self.expiryGroupsToRestartDict.pop(expiryGroup) def RebalanceCashAssets(self): ''' Run scheduled ReallocateCashAssets for rebalancing ''' helpers.ReallocateCashAssets(self, reallocationType = 'rebalancing') 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) self.Log('allContractsByExpiryGroup (before casting Symbols): ' + str(self.allContractsByExpiryGroup)) 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'] listContractSymbols = [] for contract in listContracts: try: listContractSymbols.append(self.Symbol(contract)) except BaseException as e: self.Log('self.Symbol() failed due to ' + str(e)) if len(listContractSymbols) == 0: self.allContractsByExpiryGroup.pop(expiryGroup, None) continue self.allContractsByExpiryGroup[expiryGroup]['listContracts'] = listContractSymbols # remove expiry groups with contracts that are no longer invested for contractSymbol in listContractSymbols: if not self.Securities[contractSymbol].Invested: self.allContractsByExpiryGroup.pop(expiryGroup, None) self.Log('allContractsByExpiryGroup (after casting Symbols): ' + str(self.allContractsByExpiryGroup)) except BaseException as e: self.Log('ReadInfoFromObjectStore failed due to ' + str(e)) 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('saving allContractsByExpiryGroup: ' + str(self.allContractsByExpiryGroup)) def SendEmailNotification(self): ''' Send email notifications ''' if self.LiveMode: for expiryGroup, infoDict in self.allContractsByExpiryGroup.items(): helpers.UpdateEmailDictionary(self, expiryGroup, infoDict) emailBodyAll = '' for expiryGroup, subjectsDict in self.emailDict.items(): for subject in subjectsDict: if subject == 'monetizingContracts': emailBody = subjectsDict[subject][0] subjectsDict[subject][0] = '' else: emailBody = subjectsDict[subject] subjectsDict[subject] = '' emailBodyAll += emailBody if emailBodyAll: portfolioValueInfo = '* Today starting Portfolio Value: ' + '${:,.4f}'.format(self.todayStartingPortfolioValue) + '.\n' cashAssetsHoldingsInfo = '* Cash Symbols shares: ' + str({symbol.Value: self.Portfolio[symbol].Quantity for symbol in self.dictCashSymbols}) emailBodyAll = portfolioValueInfo + emailBodyAll + cashAssetsHoldingsInfo for email in self.emails: self.Notify.Email(email, self.strategyName + ' - Actions', emailBodyAll, emailBodyAll) 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
def GetStrategyParametersDict(strategyTicker): ''' Return a dictionary containing the strategy parameters given a strategy ticker ''' strategyParametersDict = {} ### SPY ----------------------------------------------------------------------------------------------------------------------------------------------------------- if strategyTicker == 'SPD': strategyParametersDict = {'ExpiryGroupA': {'activate': True, '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': True, '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.075, # 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': True, '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.075, # 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]}}} elif strategyTicker == 'SPUC': strategyParametersDict = {'ExpiryGroupA': {'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]}}, 'ExpiryGroupB': {'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]}}} elif strategyTicker == 'SPYC': strategyParametersDict = {'ExpiryGroupA': {'activate': True, '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.075, # 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.15], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupB': {'activate': True, '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.0375, # 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.175], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupC': {'activate': True, '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.0375, # 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.175], '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.075, # 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.25], '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.075, # 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.25], '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]}}} ### QQQ ----------------------------------------------------------------------------------------------------------------------------------------------------------- elif strategyTicker == 'QQD': strategyParametersDict = {'ExpiryGroupA': {'activate': True, 'ticker': 'QQQ', '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.175, # 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.6, 0.3], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupB': {'activate': True, 'ticker': 'QQQ', '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.1, # 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.7, 0.35], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupC': {'activate': True, 'ticker': 'QQQ', '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.1, # 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.7, 0.35], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}} elif strategyTicker == 'QQU': strategyParametersDict = {'ExpiryGroupA': {'activate': True, 'ticker': 'QQQ', '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.175, # 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.35, 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]}}, 'ExpiryGroupB': {'activate': True, 'ticker': 'QQQ', '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.175, # 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.35, 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]}}} elif strategyTicker == 'QQC': strategyParametersDict = {'ExpiryGroupA': {'activate': True, 'ticker': 'QQQ', '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.0875, # 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.6, 0.15], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupB': {'activate': True, 'ticker': 'QQQ', '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.05, # 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.7, 0.175], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupC': {'activate': True, 'ticker': 'QQQ', '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.05, # 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.7, 0.175], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupD': {'activate': True, 'ticker': 'QQQ', '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.0875, # 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.35, 0.25], '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': 'QQQ', '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.0875, # 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.35, 0.25], '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]}}} ### testing ---------------------------------------------------------------------------------------------------------------------------------------------------- elif strategyTicker == 'testing': strategyParametersDict = {'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': True, 'ticker': 'TSLA', 'legLabel': 'TSLA Call 1', 'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any' 'positionSizing': 'dollar', # options are 'multiplier' and 'dollar' 'maxExpiryDays': 130, 'rollMaxExpiryDays': 220, 'daysToRollBeforeExpiration': 30, 'monetizingLiquidate': None, # 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.6, 10/12*0.25], 'strikePercentB': [0.15, None], 'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}, 'puts': {'strikePercentA': [-0.5, None], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupB': {'activate': True, 'ticker': 'TSLA', 'legLabel': 'TSLA Call 2', 'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any' 'positionSizing': 'dollar', # options are 'multiplier' and 'dollar' 'maxExpiryDays': 220, 'rollMaxExpiryDays': 220, 'daysToRollBeforeExpiration': 30, 'monetizingLiquidate': None, # 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.6, 10/12*0.25], 'strikePercentB': [0.15, None], 'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}, 'puts': {'strikePercentA': [-0.5, None], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupC': {'activate': True, 'ticker': 'TSLA', 'legLabel': 'TSLA Call 3', 'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any' 'positionSizing': 'dollar', # options are 'multiplier' and 'dollar' 'maxExpiryDays': 400, 'rollMaxExpiryDays': 760, 'daysToRollBeforeExpiration': 30, 'monetizingLiquidate': None, # 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': [1, 10/12*0.25], 'strikePercentB': [0.15, None], 'strikePercentC': [0.05, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}, 'puts': {'strikePercentA': [-0.5, None], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupD': {'activate': True, 'ticker': 'TSLA', 'legLabel': 'TSLA Call 4', 'calendarType': 'monthlies', # options are 'monthlies' (only), 'weeklies' (only) and 'any' 'positionSizing': 'dollar', # options are 'multiplier' and 'dollar' 'maxExpiryDays': 760, 'rollMaxExpiryDays': 760, 'daysToRollBeforeExpiration': 30, 'monetizingLiquidate': None, # 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': [1, 10/12*0.25], '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': 'QQQ', 'legLabel': 'QQQ Put 1', '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.125, # 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.6, 2/12*0.3], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupF': {'activate': True, 'ticker': 'QQQ', 'legLabel': 'QQQ Put 2', '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.075, # 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.7, 2/12*0.35], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}, 'ExpiryGroupG': {'activate': True, 'ticker': 'QQQ', 'legLabel': 'QQQ Put 3', '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.075, # 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.7, 2/12*0.35], 'strikePercentB': [-0.45, None], 'strikePercentC': [-0.225, None], 'strikePercentD': [0.15, None], 'strikePercentE': [0.15, None]}}} return strategyParametersDict