| Overall Statistics |
|
Total Trades 14 Average Win 0% Average Loss -0.38% Compounding Annual Return -5.247% Drawdown 2.800% Expectancy -1 Net Profit -2.632% Sharpe Ratio -2.395 Probabilistic Sharpe Ratio 0.115% Loss Rate 100% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0.015 Annual Variance 0 Information Ratio -2.395 Tracking Error 0.015 Treynor Ratio 0 Total Fees $31.50 Estimated Strategy Capacity $16000000.00 Lowest Capacity Asset SPY Y05J8JAEVQ4M|SPY R735QTJ8XC9X |
# region imports
from AlgorithmImports import *
# endregion
symbol_dict = { # will be used to store the configs of multiple symbols
'SPY': # equity symbol
{
'initialAmount': 10000, # adjust this for trade size
'contractDelta': 0.5, # sets the delta for contract to select
'SMA': 25, # SMA for the indicator
'waitPeriod': 0, # additional days to wait after exit
'trailingStop': 0.03 # trailing stop percentage
}
}
class SMAOptions(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2022, 1, 1) # Set Start Date
self.SetEndDate(2022, 6, 30) # sets end date
self.SetCash(1000000) # Set starting account size
# settings for option contracts expirations
self.expirationThreshold = timedelta(10) # this is the days threshold to perform a time roll
self.minContractExpiration = timedelta(30) # this is the minimum number of days out to look for a contract
self.maxContractExpiration = timedelta(60) # this is the maximum number of days out to look for a contract
# sets brokerage model
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
# setting empty dictionaries for multiple symbols
self.tradeAmount = {}
self.isRollOut = {}
self.rollValue = {}
self.equity = {}
self.equityDailyPrice = {}
self.equitySingleDayPrice = {}
self.entryAmount = {}
self.entryStrike = {}
self.manualSMA1 = {}
self.manualSMA2 = {}
self.autoSMA = {}
self.waitPeriod = {}
self.nextEntryTime = {}
self.nextExitTime = {}
self.trailingStop = {}
self.exitStop = {}
self.call = {}
# initialize a warm up period of 0
warmUpPeriod = 0
self.exitPeriod = timedelta(0.5)
# loop to initialize each symbol
for symbol in symbol_dict.keys():
# adds config parameters to log
symbolConfigMessage = "[Symbol] {0}, [initialAmount] {1}, [contractDelta] {2}, [SMA] {3}, [trailingStop] {4}".format(
symbol,
symbol_dict[symbol]['initialAmount'],
symbol_dict[symbol]['contractDelta'],
symbol_dict[symbol]['SMA'],
symbol_dict[symbol]['waitPeriod'],
symbol_dict[symbol]['trailingStop']
)
self.Log(symbolConfigMessage)
self.tradeAmount[symbol] = 0
self.isRollOut[symbol] = False
self.rollValue[symbol] = 0
# sets underlying asset
self.equity[symbol] = self.AddEquity(symbol, Resolution.Minute)
self.equity[symbol].SetDataNormalizationMode(DataNormalizationMode.Raw)
self.equityDailyPrice[symbol] = None
self.entryAmount[symbol] = 0 # used to track initial entry over rolls for pnl
self.entryStrike[symbol] = 0. # used to track initial strike price
option = self.AddOption(self.equity[symbol].Symbol, Resolution.Minute)
option.SetFilter(-2, 2, self.minContractExpiration, self.maxContractExpiration)
# set pricing model to calculate Greeks
option.PriceModel = OptionPriceModels.BjerksundStensland()
# initialize indicators
self.manualSMA1[symbol] = SimpleMovingAverage(self.equity[symbol].Symbol, symbol_dict[symbol]['SMA'])
self.manualSMA2[symbol] = SimpleMovingAverage(self.equity[symbol].Symbol, symbol_dict[symbol]['SMA'])
self.autoSMA[symbol] = self.SMA(self.equity[symbol].Symbol, symbol_dict[symbol]['SMA'], Resolution.Daily)
self.waitPeriod[symbol] = timedelta(0.5) + timedelta(symbol_dict[symbol]['waitPeriod'])
self.nextEntryTime[symbol] = self.Time
self.nextExitTime[symbol] = self.Time
# trailing stop initialization
self.trailingStop[symbol] = 0
self.exitStop[symbol] = 0
# calculates warm up period to be maximum SMA out of the equities
warmUpPeriod = max(warmUpPeriod, symbol_dict[symbol]['SMA'])
# needed for plotting each symbol
stockPlot = Chart(f'{symbol} Plot')
stockPlot.AddSeries(Series('Price', SeriesType.Candle, '$'))
self.AddChart(stockPlot)
# Generating consolidators
consDaily = TradeBarConsolidator(Resolution.Daily)
consDaily.DataConsolidated += self.OnDailyData
self.SubscriptionManager.AddConsolidator(self.equity['SPY'].Symbol, consDaily)
consSingleDay = TradeBarConsolidator(timedelta(1))
consSingleDay.DataConsolidated += self.OnSingleDayData
self.SubscriptionManager.AddConsolidator(self.equity['SPY'].Symbol, consSingleDay)
self.exchange = self.Securities[self.equity[symbol].Symbol].Exchange # using to check when exchange is open
# sets the warm up period
self.SetWarmUp(timedelta(warmUpPeriod))
self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.At(9, 32), self.PlotLogDaily)
def OnData(self, data: Slice):
# checks if smas are ready or if no daily price
if self.IsWarmingUp:
return
for symbol in symbol_dict.keys():
#if not self.daily_bb[symbol].IsReady:
# continue
if self.equityDailyPrice[symbol] is None:
continue
# runs only when exchange hours are open
if self.exchange.ExchangeOpen:
# checks for open option positions
option_invested = [x for x, holding in self.Portfolio.items() if holding.Invested and holding.Type == SecurityType.Option and x.HasUnderlying and x.Underlying.Equals(self.equity[symbol].Symbol)]
if option_invested:
# calculates trailing and exit stop:
self.trailingStop[symbol] = self.equityDailyPrice[symbol] * (1-symbol_dict[symbol]['trailingStop'])
self.exitStop[symbol] = max(self.exitStop[symbol], self.trailingStop[symbol])
# Exits if price is below the stop
if self.exitStop[symbol] > self.equity[symbol].Price:
if self.nextExitTime[symbol] <= self.Time: # exit needs to wait a day after entry
pnl = self.Securities[option_invested[0]].Holdings.HoldingsValue - self.entryAmount[symbol]
orderMessage = "[Time] {0}; [Underlying] {1}; [Strike Price] {2}; [Contract Delta] {3}; [Contract Expiration] {4}; [Enter Trigger] {5:.2f}; [Enter Amount] {6:.2f}; [Portfolio Value] {7:.2f}; [Trailing Stop] {8:.2f}; [Exit Stop] {9:.2f}; [PNL] {10:.2f}".format(
self.Time,
self.equity[symbol].Price,
self.call[symbol].Strike,
self.call[symbol].Greeks.Delta,
self.call[symbol].Expiry,
self.autoSMA[symbol].Current.Value,
self.entryAmount[symbol],
self.Portfolio.TotalPortfolioValue,
self.trailingStop[symbol],
self.exitStop[symbol],
pnl
)
self.Liquidate(option_invested[0], orderMessage)
self.Log(orderMessage)
self.nextEntryTime[symbol] = self.Time + self.waitPeriod[symbol] # Waits at least until next day to re-enter
# resets all flags/counters
self.trailingStop[symbol] = 0
self.exitStop[symbol] = 0
self.entryAmount[symbol] = 0
self.entryStrike[symbol] = 0
# If too close to expiration, liquidate and perform a time roll
elif self.Time + self.expirationThreshold > option_invested[0].ID.Date:
self.rollValue[symbol] = self.Securities[option_invested[0]].Holdings.HoldingsValue
timeRollMessage = "[Time] {0}; [Contract Expiration] {1}; [Time Roll Amount] {2}".format(
self.Time,
self.call[symbol].Expiry,
self.rollValue[symbol]
)
self.Liquidate(option_invested[0], timeRollMessage)
self.Log(timeRollMessage)
self.isRollOut[symbol] = True
# determines the option chain and buys the option
for i in data.OptionChains:
chains = i.Value
self.BuyCall(chains, symbol)
# Attempts entry if no open options positions
else:
# entry if above upper band
if self.autoSMA[symbol].Current.Value <= self.equity[symbol].Price:
if self.nextEntryTime[symbol] <= self.Time:
self.nextExitTime[symbol] = self.Time + self.exitPeriod
self.entryStrike[symbol] = self.autoSMA[symbol].Current.Value
for i in data.OptionChains:
chains = i.Value
self.BuyCall(chains, symbol)
def BuyCall(self, chains, symbol):
# sorts the options chains and picks the furthest away between 30-60 days
expiry = sorted(chains, key = lambda x: x.Expiry, reverse=True)[0].Expiry
# filter out calls
calls = [i for i in chains if i.Expiry == expiry and i.Right == OptionRight.Call]
# finds contract closest to the requested delta with and without roll
if self.isRollOut[symbol] is False:
call_contracts = sorted(calls, key = lambda x: abs(x.Greeks.Delta - symbol_dict[symbol]['contractDelta']))
elif self.isRollOut[symbol]:
call_contracts = sorted(calls, key = lambda x: abs(x.Strike - self.entryStrike[symbol]))
else:
self.Debug("Roll flag error")
return
# saves first element to the self.call variable
if len(call_contracts) == 0:
return
self.call[symbol] = call_contracts[0]
try:
# calculates trade amount
if self.isRollOut[symbol] is False:
self.tradeAmount[symbol] = symbol_dict[symbol]['initialAmount']
else:
self.tradeAmount[symbol] = self.rollValue[symbol]
# determines quantity based on trade amount
quantity = int((self.tradeAmount[symbol]) / self.call[symbol].AskPrice / 100)
# updates trade amount based on quantity calculated
self.tradeAmount[symbol] = quantity * self.call[symbol].AskPrice * 100
if self.isRollOut[symbol] is False:
self.entryAmount[symbol] = self.tradeAmount[symbol]
buyLogMessage= "[Time] {0}; [Underlying] {1}; [Strike Price] {2}; [Contract Delta] {3}; [Contract Expiration] {4}; [Enter Trigger] {5:.2f}; [Enter Amount] {6:.2f}; [Portfolio Value] {7:.2f}".format(
self.Time,
self.equity[symbol].Price,
self.call[symbol].Strike,
self.call[symbol].Greeks.Delta,
self.call[symbol].Expiry,
self.autoSMA[symbol].Current.Value,
self.tradeAmount[symbol],
self.Portfolio.TotalPortfolioValue
)
#self.Buy(self.call.Symbol, quantity)
self.MarketOrder(self.call[symbol].Symbol, quantity, False, buyLogMessage)
self.Log(buyLogMessage)
except:
self.Debug("error computing quantity") # this is done since I had run into some errors computing the quantity
def OnOrderEvent(self, orderEvent):
# log order events
self.Log("[OnOrderEvent] " + str(orderEvent))
pass
def OnDailyData(self, sender, bar):
# updates indicator calculations and daily price on consolidated daily bar
self.manualSMA1['SPY'].Update(bar.EndTime, bar.Close)
self.equityDailyPrice['SPY'] = bar.Close
def OnSingleDayData(self, sender, bar):
self.manualSMA2['SPY'].Update(bar.EndTime, bar.Close)
self.equitySingleDayPrice['SPY'] = bar.Close
def PlotLogDaily(self):
for symbol in symbol_dict.keys():
try:
dailyMessage = "[Time] {0}, [Symbol] {1}, [Manual SMA Daily] ${2:.2f}, [Daily Close] ${3:.2f}, [Manual SMA Single Day] ${4:.2f}, [Single Day Close] ${5:.2f}, [Auto SMA] ${6:.2f}".format(
self.Time,
symbol,
self.manualSMA1[symbol].Current.Value,
self.equityDailyPrice[symbol],
self.manualSMA2[symbol].Current.Value,
self.equitySingleDayPrice[symbol],
self.autoSMA[symbol].Current.Value
)
self.Log(dailyMessage)
# plots the daily price and the entry line
self.Plot(f'{symbol} Plot', 'Price', self.equityDailyPrice[symbol])
self.Plot(f'{symbol} Plot', 'SMA', self.autoSMA[symbol].Current.Value)
except:
self.Debug("symbol not found")