| 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 0.939 Beta 0.867 Annual Standard Deviation 1.354 Annual Variance 1.833 Information Ratio 0.659 Tracking Error 1.274 Treynor Ratio 1.829 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 = 'TSLA'
# 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 = Truedef 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