| Overall Statistics |
|
Total Trades 14 Average Win 1.27% Average Loss -5.44% Compounding Annual Return -97.818% Drawdown 28.300% Expectancy -0.824 Net Profit -27.738% Sharpe Ratio -2.607 Probabilistic Sharpe Ratio 0.000% Loss Rate 86% Win Rate 14% Profit-Loss Ratio 0.23 Alpha -0.395 Beta -0.183 Annual Standard Deviation 0.366 Annual Variance 0.134 Information Ratio -7.401 Tracking Error 0.542 Treynor Ratio 5.215 Total Fees $1713.50 |
##########################################################################
# The Dynamic 'Mini-Leap' Trader
# Author: Ikezi Kamanu
# Contributors: Leandro Maia
# ----------------------------------------------------
#
# Trade 'mini-LEAP' option contracts (120 - 365 DTE),
# based on daily universe selection.
#
# ----------------------------------------------------
# Entry:
# Documentation pending.
#
# Position Management:
# Documentation pending.
#
# Exit:
# Documentation pending.
#
##########################################################################
from dateutil import parser
class DynamicMiniLeapRoller(QCAlgorithm):
# ==============================================================
# Initialize data, capital, scheduled routines, etc.
# ==============================================================
def Initialize(self):
# Initializers (params, universe, routines, etc)
# ----------------------------------------------------------
self.InitializeAlgoParams()
self.InitializeBacktestParams()
self.SetupUnderlyingSecurity()
self.InitializeUniverse()
self.ScheduleRoutines()
# ======================================================
# Set algo params: ticker, thresholds
# ======================================================
def InitializeAlgoParams(self):
self.ticker = "SPY"
self.data = {}
self.distFromPrice = int(self.GetParameter("priceDistPct"))/100 # pick OTM strike that is x% higher than price
self.enterAtDTE = int(self.GetParameter("enterDTE")) # buy an option with x days till expiry
self.exitAtDTE = int(self.GetParameter("exitDTE")) # sell the option when x days til expiry
self.roiTarget = int(self.GetParameter("roiTargetPct"))/100 # sell when option has gained x% ROI
self.pctOfAcct = int(self.GetParameter("pctOfAcct"))/100 # trade with X% of your acct balance
self.stopLoss = int(self.GetParameter("stopLossPct"))/100 # sell when option value has lost -x%
self.maxDaysInTrade = int(self.GetParameter("maxDaysInTrade")) # sell when contract has beeen open for X days
self.MarketOpenMoreThanThirtyMins = False
# ======================================================
# Set backtest params: dates, cash, etc
# ======================================================
def InitializeBacktestParams(self):
# set start/end date for backtest
# --------------------------------------------
self.SetStartDate(2020, 4, 1) # Set Start Date
self.SetEndDate(2020, 5, 1) # Set End Date
# set starting balance for backtest
# --------------------------------------------
self.SetCash(100000)
# ======================================================
# Configure underlying, set the custom intializer
# ======================================================
def SetupUnderlyingSecurity(self):
# add the underlying asset
# ---------------------------------
self.equity = self.AddEquity(self.ticker, Resolution.Minute)
self.equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
self.symbol = self.equity.Symbol
self.forceInitialized = False
# set custom security intializer
# -------------------------------
self.SetSecurityInitializer(self.InitializeSecurities)
# ==============================================================
# Initialize the security
# ==============================================================
def InitializeSecurities(self, security):
# intialize securities with last known price,
# so that we can immediately trade the security
# ------------------------------------------------
bar = self.GetLastKnownPrice(security)
security.SetMarketPrice(bar)
# ==============================================================
# OnData Event handler
# ==============================================================
def OnData(self, data):
# if we have data
# ------------------------------
if (self.Securities[self.symbol] is not None) and \
(self.Securities[self.symbol].HasData) and \
(data.Bars.ContainsKey(self.symbol)):
# keep track of the current close price
# ------------------------------------------
self.underlyingPrice = data.Bars[self.symbol].Close
#######################################################
############### Routines ##################
########################################@##############
# ==============================================================
# Schedule Routines (similar to chron jobs)
# ==============================================================
def ScheduleRoutines(self):
# schedule routine to run 30 minutes after every market open
# --------------------------------------------------------------
self.Schedule.On(self.DateRules.EveryDay(self.symbol), \
self.TimeRules.AfterMarketOpen(self.symbol, 30), \
self.OnThirtyMinsAfterMarketOpen)
# schedule routine to run every 10 minutes
# -----------------------------------------
self.Schedule.On(self.DateRules.EveryDay(self.symbol), \
self.TimeRules.Every(timedelta(minutes=10)), \
self.OnEveryTenMinsDuringMarket)
# schedule routine to run 30 minutes before every market close
# --------------------------------------------------------------
# self.Schedule.On(self.DateRules.EveryDay(self.symbol), \
# self.TimeRules.BeforeMarketClose(self.symbol, 30), \
# self.OnThirtyMinsBeforeMarketClose)
self.Schedule.On(self.DateRules.EveryDay(),
self.TimeRules.AfterMarketOpen(self.SPY, 1),
self.OnOneMinuteAfterMarketOpen)
self.Schedule.On(self.DateRules.EveryDay(),
self.TimeRules.BeforeMarketClose(self.SPY, 1),
self.OnOneMinuteBeforeMarketCloses)
# ==============================================================
# Run this Logic 1 minute after market open
# ==============================================================
def OnOneMinuteAfterMarketOpen(self):
self.MarketOpenMoreThanThirtyMins = False
self.FlushPrevSelectStocks()
self.UpdateSymbolDataAtMarketOpen()
# ==============================================================
# Run this Logic 1 minute before market close
# ==============================================================
def OnOneMinuteBeforeMarketCloses(self):
self.MarketOpenMoreThanThirtyMins = False
self.UpdateSymbolDataAtMarketClose()
# ==============================================================
# Run this Logic 30 minutes after market open
# ==============================================================
def OnThirtyMinsAfterMarketOpen(self):
self.MarketOpenMoreThanThirtyMins = True
self.SelectStocksFromUniverse()
# return if we are still warming up
# or if we do not have any data
# ----------------------------------
if(self.IsWarmingUp) or \
(not self.Securities[self.symbol].HasData):
return
self.ManageOpenPositions()
#self.OpenLongCallIfNotInvested()
# ==============================================================
# Run this logic every 10 minutes while market is open
# ==============================================================
def OnEveryTenMinsDuringMarket(self):
if self.MarketOpenMoreThanThirtyMins :
self.ManageOpenPositions()
self.OpenLongCallIfNotInvested()
# ==============================================================
# Run this 30 minutes before market close
# ==============================================================
# def OnThirtyMinsBeforeMarketClose(self):
# self.OpenLongCallIfNotInvested
###################################################################
############## Order Logic (open/close/manage) ###############
###################################################################
# =======================================================================
# Open a new option for the most recent stock in our universe selection
# If the universe is empty, use our default stock
# =======================================================================
def OpenLongCallIfNotInvested(self):
# exit if we are still warming up
# or if we do not have any data
# ----------------------------------
if(self.IsWarmingUp) or \
(not self.Securities[self.symbol].HasData):
return
# otherwise, if we have no holdings, open a position.
# --------------------------------------------------
if not self.Portfolio.Invested:
# set strikes and expiration
# ------------------------------
callStrike = self.underlyingPrice * (1 + (self.distFromPrice) )
expiration = self.Time + timedelta(days=self.enterAtDTE)
### todo: consider using ATR for strike selection
### todo: consider using delta for strike selection
### todo: encapsulate strike and expiry selection in GetLongCall()
if(len(self.selectedStocks) > 0 ):
theSymbol = self.selectedStocks.pop()
self.equity = self.AddEquity(theSymbol.Value, Resolution.Minute)
self.equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
self.symbol = self.equity.Symbol
self.OpenLongCall(theSymbol, callStrike, expiration )
else:
return # universe is empty. do nothing
# theSymbol = self.symbol # use previously used security if nothing in universe selection
# ==============================================================
# Open Long Call
# ==============================================================
def OpenLongCall(self, symbolArg, callStrike, expiration ):
# retrive closest call contracts
# -------------------------------
callContract = self.GetLongCall(symbolArg, callStrike, expiration)
# subscribe to data for those contracts
# -----------------------------------------
theOption = self.AddOptionContract(callContract, Resolution.Minute)
# buy call contract
# -------------------
orderMsg = "." #"Strike: "+str(callContract.StrikePrice) + " Stock @ " + str(callContract.UnderlyingLastPrice)
self.SetHoldings(callContract, self.pctOfAcct, False, orderMsg)
self.Debug("[[ BUY ]] " + str(theOption) + " | "+ orderMsg + " | "+ self.Time.ctime() )
# store the opening time in the object store.
# -------------------------------------------
# todo: Refactor this to use SymbolData. Currently using suggestion from here:
# https://www.quantconnect.com/forum/discussion/9212/how-to-check-position-age/p1
self.ObjectStore.Save(str(theOption), str(self.Time))
# ==============================================================
# Roll Long Call
# ==============================================================
# def RollLongCall(self, optionSymbolArg, callStrike, expiration ):
# underlyingSymbol = optionSymbolArg.underlyingSymbol
# self.Liquidate(optionSymbolArg, orderMsg)
# self.OpenLongCall(underlyingSymbol, callStrike, expiration )
# ==============================================================
# Manage positions: take profits, stop losses, rolling, etc.
# ==============================================================
def ManageOpenPositions(self):
# check for open contracts and close them if warranted.
# ------------------------------------------------------
for symbol in self.Securities.Keys:
if self.Securities[symbol].Invested:
# Set debug values
# ---------------------------------------------------
currTime = self.Time
stockPrice = self.underlyingPrice
profitPct = round(self.Securities[symbol].Holdings.UnrealizedProfitPercent,2)
daysTillExp = (self.Securities[symbol].Expiry - self.Time).days
# if current contract is ITM, liquidate
# This should be the first check, because we want to keep them OTM
# -----------------------------------------------------------------
if (self.underlyingPrice > self.Securities[symbol].StrikePrice):
orderMsg = "[ITM] exit @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice)
self.Liquidate(symbol, orderMsg)
return
# if current contract has hit x% return, liquidate
# ---------------------------------------------------
###
### todo: experiment with diff ROI roll targets here.
### for now, set the param value too high to reach
### so this code never gets called. explore later
###
elif( profitPct >= (self.roiTarget)):
orderMsg = "[TP] exit @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice)
self.Liquidate(symbol, orderMsg)
return
# if current contract has hit x% loss, liquidate
# ---------------------------------------------------
elif( profitPct <= (-self.stopLoss)):
orderMsg = "[SL] exit @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice)
self.Liquidate(symbol, orderMsg)
return
# if current position has been open for X days, liquidate
# --------------------------------------------------------
elif self.ObjectStore.ContainsKey(str(symbol)):
date = self.ObjectStore.Read(str(symbol))
date = parser.parse(date)
if (self.Time - date).days >= self.maxDaysInTrade:
self.ObjectStore.Delete(str(symbol))
orderMsg = "[Duration] exit @ "+str(self.maxDaysInTrade)+" days @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice) + " || " + str(daysTillExp) + " DTE"
self.Liquidate(symbol, orderMsg)
return
# if current contract expiry is less than 'X' liquidate
# -----------------------------------------------------------
elif ((self.Securities[symbol].Expiry - self.Time).days < self.exitAtDTE):
orderMsg = "[DTE] exit @ " + str(profitPct*100) + "% Profit || Stock @ $" + str(stockPrice) + " || " + str(daysTillExp) + " DTE"
self.Liquidate(symbol, orderMsg)
return
# ==============================================================
# Get Long Call, given a symbol, desired strike and expiration
# ==============================================================
def GetLongCall(self, symbolArg, callStrikeArg, expirationArg):
contracts = self.OptionChainProvider.GetOptionContractList(symbolArg, self.Time)
# get all calls
# -------------
calls = [symbol for symbol in contracts if symbol.ID.OptionRight == OptionRight.Call]
# sort contracts by expiry dates and select expiration closest to desired expiration
# --------------------------------------------
callsSortedByExpiration = sorted(calls, key=lambda p: abs(p.ID.Date - expirationArg), reverse=False)
closestExpirationDate = callsSortedByExpiration[0].ID.Date
# get all contracts for selected expiration
# ------------------------------------------------
callsFilteredByExpiration = [contract for contract in callsSortedByExpiration if contract.ID.Date == closestExpirationDate]
# sort contracts and select the one closest to desired strike
# -----------------------------------------------------------
callsSortedByStrike = sorted(callsFilteredByExpiration, key=lambda p: abs(p.ID.StrikePrice - callStrikeArg), reverse=False)
callOptionContract = callsSortedByStrike[0]
return callOptionContract
######################################################################
########### Universe / Stock Selection Logic ###############
######################################################################
# ==============================================================
# Initialize the universe, and universe selection criteria
# ==============================================================
def InitializeUniverse(self):
self.UniverseSettings.Resolution = Resolution.Minute
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.SPY = self.AddEquity('SPY', Resolution.Minute).Symbol
self.day = -1
self.num_coarse = 20
self.min_stock_price = 10
# set params used for filter criteria
# ------------------------------------
self.min_days_after_earnings = 10
self.max_days_after_earnings = 80
self.ema_period = 40 # 8 on 5 min timeframe
self.sma_period = 275 # 55 on 5 min timeframe
self.bb_period = 600 # 20 on 30 min timeframe
self.bb_k = 2
self.gap_distance = 0.02 # 2%
self.data = {}
self.selectedStocks = []
# ==============================================================
# Universe Coarse selection logic.
# ---------------------------------
# Find stocks with fundamentals with price > 5,
# rank by dollar volume, and then pick the top X
# ==============================================================
def CoarseSelectionFunction(self, coarse):
if self.day == self.Time.day:
return Universe.Unchanged
self.day = self.Time.day
# drop stocks which have no fundamental data or have too low prices
selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > self.min_stock_price)]
# rank the stocks by dollar volume
# -----------------------------------
filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True)
return [ x.Symbol for x in filtered[:self.num_coarse]]
# ========================================================================
# Universe Fine selection logic
# ---------------------------------
# Find common stocks where the share class is not a depository receipt,
# and have neither had recent earnings reports nor any coming soon.
#
# todo: explore other ways to account for future earnings.
# ========================================================================
def FineSelectionFunction(self, fine):
filtered = [x for x in fine if x.SecurityReference.IsPrimaryShare
and x.SecurityReference.SecurityType == "ST00000001"
and x.SecurityReference.IsDepositaryReceipt == 0
and x.CompanyReference.IsLimitedPartnership == 0
and x.EarningReports.FileDate < self.Time - timedelta(days=self.min_days_after_earnings)
and x.EarningReports.FileDate > self.Time - timedelta(days=self.max_days_after_earnings)]
return [x.Symbol for x in filtered]
# ====================================================================
# Flush stocks previously selected from universe
# ====================================================================
def FlushPrevSelectStocks(self):
self.selectedStocks.clear()
# =====================================================================
# Select new stocks from universe that pass technical indicator filters
# =====================================================================
def SelectStocksFromUniverse(self):
for symbol in self.data.keys():
if (self.data[symbol].GapUp) and (self.data[symbol].EMA > self.data[symbol].SMA) and \
(self.data[symbol].BB.UpperBand.Current.Value < self.Securities[symbol].Close):
# if stock passes criteria (ie: there is signal), add to our 'selected stocks'
# ----------------------------------------------------------------------------
self.selectedStocks.append(symbol)
self.Debug(" + " + str(symbol).split(" ")[0] + "\t Added on " + self.Time.ctime())
# ============================================================================
# When securities are added or removed, update our internal symbol dictionary
# ============================================================================
def OnSecuritiesChanged(self, changes):
for security in changes.RemovedSecurities:
if security.Symbol in self.data:
del self.data[security.Symbol]
for security in changes.AddedSecurities:
if (security.Symbol not in self.data) and \
(security.Symbol.SecurityType == SecurityType.Equity):
self.data[security.Symbol] = SymbolData(security.Symbol, self.ema_period, self.sma_period, self.bb_period, self.bb_k, self)
# ============================================================================
# Update state of SymbolData at market open
# ============================================================================
def UpdateSymbolDataAtMarketOpen(self):
for symbol in self.data.keys():
gap = (self.Securities[symbol].Close - self.data[symbol].LastClose) / self.data[symbol].LastClose
if gap > self.gap_distance:
self.data[symbol].GapUp = True
else:
self.data[symbol].GapUp = False
# ============================================================================
# Update state of SymbolData at market close
# ============================================================================
def UpdateSymbolDataAtMarketClose(self):
for symbol in self.data.keys():
self.data[symbol].LastClose = self.Securities[symbol].Close
class SymbolData(object):
def __init__(self, symbol, ema, sma, bb, k, algorithm):
self.Symbol = symbol
self.LastClose = 0
self.GapUp = False
self.EMA = ExponentialMovingAverage(ema)
self.SMA = SimpleMovingAverage(sma)
self.BB = BollingerBands(bb, k, MovingAverageType.Exponential)
algorithm.RegisterIndicator(symbol, self.EMA, Resolution.Minute, Field.Close)
algorithm.RegisterIndicator(symbol, self.SMA, Resolution.Minute, Field.Close)
algorithm.RegisterIndicator(symbol, self.BB, Resolution.Minute, Field.Close)
# Logic for 'manual' warmup .
# --------------------------------------
# Check for daily data. we need at least one day
# ------------------------------------------------
history = algorithm.History(symbol, 1, Resolution.Daily)
if history.empty or 'close' not in history.columns:
return
for index, row in history.loc[symbol].iterrows():
self.LastClose = row['close']
# Check for minute data. we need at least the MAX of our indicator periods
# --------------------------------------------------------------------------
history = algorithm.History(symbol, max(ema, sma, bb), Resolution.Minute)
if history.empty or 'close' not in history.columns:
return
for index, row in history.loc[symbol].iterrows():
self.EMA.Update(index, row['close'])
self.SMA.Update(index, row['close'])
self.BB.Update(index, row['close'])