| Overall Statistics |
|
Total Trades 34 Average Win 0.20% Average Loss -0.25% Compounding Annual Return 2.074% Drawdown 1.500% Expectancy 0.063 Net Profit 0.259% Sharpe Ratio 0.575 Probabilistic Sharpe Ratio 42.802% Loss Rate 41% Win Rate 59% Profit-Loss Ratio 0.81 Alpha 0 Beta 0 Annual Standard Deviation 0.026 Annual Variance 0.001 Information Ratio 0.575 Tracking Error 0.026 Treynor Ratio 0 Total Fees $374.60 Estimated Strategy Capacity $170000.00 Lowest Capacity Asset KFT S5HU4FPL6G6D |
from AlgorithmImports import *
from settings import *
from collections import namedtuple
import operator
import functools
class ExpansionBreakoutStrategy(QCAlgorithm):
def Initialize(self):
settings = self.GetSettings()
self.SetStartDate(settings.startDate)
self.SetEndDate(settings.endDate)
self.SetCash(settings.startCash)
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
self.DefaultOrderProperties = InteractiveBrokersOrderProperties()
self.DefaultOrderProperties.TimeInForce = TimeInForce.GoodTilCanceled
self.selectionDataDict = {}
self.UpSymbols = self.DownSymbols = []
self.day = self.month = None
self.coarseSymbols = []
self.symbolDataDict = {}
self.HasOpenLongPositions = self.HasOpenShortPositions = False
self.NumberOfLongPositions = self.NumberOfShortPositions = 0
self.maxLongPositions = settings.numberOfLongPositions
self.maxShortPositions = settings.numberOfShortPositions
self.numberOfSymbolsCoarse = settings.numberOfStocksToScan
self.maximumPricePerShare = settings.maximumPricePerShare
self.minimumPricePerShare = settings.minimumPricePerShare
self.trailingStopDistPct = 0
if self.GetParameters().ContainsKey("trailingStopDistPct"):
param = self.GetParameter("trailingStopDistPct")
if param is not None and (isinstance(param, float) or isinstance(param, int) or isinstance(param, str)):
self.trailingStopDistPct = float(param)
if self.trailingStopDistPct == 0:
self.trailingStopDistPct = settings.trailingStopDistPct
self.initialStopDistancePoints = settings.initialStopDistPoints
self.weight = min(settings.portfolioWeightPerStock, 1/(self.maxLongPositions + self.maxShortPositions))
self.UniverseSettings.FillForward = True
self.AddUniverse(self.SelectCoarse)
self.tickerToPlot = settings.tickerToPlot
def GetSettings(self):
paramNames = ['startDate', 'endDate', 'startCash', 'numberOfStocksToScan', 'minimumPricePerShare', 'maximumPricePerShare', 'numberOfLongPositions', 'numberOfShortPositions',
'initialStopDistPoints', 'trailingStopDistPct', 'portfolioWeightPerStock', 'tickerToPlot']
paramValues = [datetime(*startDate), datetime(*endDate), startCash, numberOfStocksToScan, minimumPricePerShare, maximumPricePerShare, numberOfLongPositions, numberOfShortPositions,
initialStopDistPoints, trailingStopDistPct, portfolioWeightPerStock, tickerToPlot]
paramSettings = namedtuple('Settings', paramNames)
settings = paramSettings(*[*map(operator.itemgetter(1), zip(paramNames, paramValues))])
return settings
def OnData(self, data):
for symbol, symbolData in self.symbolDataDict.items():
if symbol not in data.QuoteBars:
continue
if symbolData.Holdings.Invested:
if symbolData.ExitConditionMet:
self.Liquidate(symbol)
else:
if not symbolData.TrailingStopFlag:
if symbolData.Holdings.UnrealizedProfitPercent < 0.02:
symbolData.trailingStop.Reset()
else:
symbolData.TrailingStopFlag = True
continue
else:
if symbolData.Flag and symbolData.Signal != 0:
direction = sign(symbolData.Signal)
if (direction > 0 and self.NumberOfLongPositions >= self.maxLongPositions) or (direction < 0 and self.NumberOfShortPositions >= self.maxShortPositions):
continue
# we have a signal for this symbol, let's place a stop entry order
orderQuantity = self.CalculateOrderQuantity(symbol, direction * self.weight)
if orderQuantity == 0:
continue
stopEntryPrice = symbolData.CurrentHigh.Current.Value if direction > 0 else symbolData.CurrentLow.Current.Value
stopEntryPrice += direction*1/8
orderProperties = OrderProperties()
orderProperties.TimeInForce = TimeInForce.GoodTilDate(self.Time + timedelta(days=1))
self.StopMarketOrder(symbol, orderQuantity, stopEntryPrice, orderProperties = orderProperties)
symbolData.Flag = False
else:
continue
def OnOrderEvent(self, orderEvent):
if orderEvent.Status == OrderStatus.Filled:
symbol = orderEvent.Symbol
holdings = self.symbolDataDict[symbol].Holdings
if orderEvent.FillQuantity * holdings.Quantity > 0:
# we have entered a position, let's place the stop market order
openOrders = self.Transactions.GetOpenOrders(symbol)
if len(openOrders) == 0:
direction = sign(orderEvent.FillQuantity)
orderProperties = OrderProperties()
orderProperties.TimeInForce = TimeInForce.GoodTilCanceled
cmp = operator.gt if direction > 0 else operator.lt
if cmp(orderEvent.FillPrice, self.symbolDataDict[symbol].PreviousClose):
stopPrice = self.symbolDataDict[symbol].StopPrice
self.StopMarketOrder(symbol, -orderEvent.FillQuantity, stopPrice, orderProperties = orderProperties)
self.symbolDataDict[symbol].trailingStop.Reset()
if holdings.Quantity == 0:
# liquidated, let's cancel all open orders for this symbol
self.Transactions.CancelOpenOrders(symbol)
self.UpdatePortfolioState()
def UpdatePortfolioState(self):
numberOfLongPositions = numberOfShortPositions = 0
for symbol, holding in self.Portfolio.items():
if holding.IsLong:
numberOfLongPositions += 1
elif holding.IsShort:
numberOfShortPositions += 1
else:
continue
self.NumberOfLongPositions = numberOfLongPositions
self.NumberOfShortPositions = numberOfShortPositions
self.HasOpenLongPositions = numberOfLongPositions > 0
self.HasOpenShortPositions = numberOfShortPositions > 0
@property
def CurrentHoldings(self):
return [(symbol.Value, holding.Quantity) for symbol,holding in self.Portfolio.items() if holding.Invested]
def OnSecuritiesChanged(self, changes):
for security in changes.RemovedSecurities:
symbol = security.Symbol
symbolData = self.symbolDataDict.pop(symbol, None)
if symbolData is not None:
symbolData.Dispose(self)
for security in changes.AddedSecurities:
symbol = security.Symbol
self.symbolDataDict[symbol] = SymbolData(self, security, self.selectionDataDict[symbol], self.trailingStopDistPct, self.initialStopDistancePoints, self.tickerToPlot)
def SelectCoarse(self, coarse):
selection = []
upSignals = []
downSignals = []
symbols = self.coarseSymbols
if self.month != self.Time.month:
filteredCoarse = [c for c in coarse if ( self.minimumPricePerShare < c.Price < self.maximumPricePerShare
and c.Volume > 1e5 and c.HasFundamentalData
and (self.Time - c.Symbol.ID.Date).days > 42)]
symbols = [c.Symbol for c in sorted(filteredCoarse, key=lambda c: c.DollarVolume, reverse=True)[:self.numberOfSymbolsCoarse]]
self.coarseSymbols = symbols
self.month = self.Time.month
lastBar = self.History(symbols, 1, Resolution.Daily)
for symbol in symbols:
if symbol.ID.ToString() not in lastBar.index.levels[0]:
continue
if symbol not in self.selectionDataDict:
self.selectionDataDict[symbol] = SelectionData(self, symbol)
bar = lastBar.loc[symbol]
if bar.shape != (1,5) or bar.isnull().any().any():
continue
tradeBar = TradeBar(bar.index[0], symbol, bar.open[0], bar.high[0], bar.low[0], bar.close[0], bar.volume[0])
selectionData = self.selectionDataDict[symbol]
selectionData.Update(tradeBar)
if selectionData.Signal == 1:
upSignals.append(symbol)
elif selectionData.Signal == -1:
downSignals.append(symbol)
if len(upSignals) > 0 and self.NumberOfLongPositions < self.maxLongPositions:
self.UpSymbols = sorted(upSignals, key = lambda x: self.selectionDataDict[x].relativeVolume.Current.Value)[:self.maxLongPositions - self.NumberOfLongPositions]
selection.extend(self.UpSymbols)
if len(downSignals) > 0 and self.NumberOfShortPositions < self.maxShortPositions:
self.DownSymbols = sorted(downSignals, key = lambda x: self.selectionDataDict[x].relativeVolume.Current.Value)[:self.maxShortPositions - self.NumberOfShortPositions]
selection.extend(self.DownSymbols)
symbolsWithOpenOrders = [symbol for symbol, symbolData in self.symbolDataDict.items() if len(self.Transactions.GetOpenOrders(symbol)) > 0]
currentHoldings = [symbol for symbol,holding in self.Portfolio.items() if holding.Invested]
selection.extend(symbolsWithOpenOrders)
selection.extend(currentHoldings)
return np.unique(selection).tolist()
class SymbolData:
def __init__(self, algorithm, security, selectionData, trailingStopDistPct, initialStopDistPoints, tickerToPlot):
self.algorithm = algorithm
self.Security = security
self.Symbol = security.Symbol
self.Holdings = security.Holdings
self.selectionData = selectionData
self.Signal = selectionData.Signal
previousClose = selectionData.expansionBreakoutIndicator.Close
self.StopPrice = previousClose - self.Signal*initialStopDistPoints
self.PreviousClose = previousClose
previousHigh = selectionData.expansionBreakoutIndicator.High
previousLow = selectionData.expansionBreakoutIndicator.Low
self.CurrentHigh = algorithm.Identity(self.Symbol, Resolution.Daily, Field.High)
self.CurrentLow = algorithm.Identity(self.Symbol, Resolution.Daily, Field.Low)
self.PreviousHigh = IndicatorExtensions.Of(Delay(1), self.CurrentHigh)
self.PreviousLow = IndicatorExtensions.Of(Delay(1), self.CurrentLow)
self.Consolidators = [algorithm.ResolveConsolidator(self.Symbol, resolution) for resolution in [Resolution.Minute, Resolution.Daily]]
self.Flag = True
self.day = None
self.CurrentHigh.Update(algorithm.Time, previousHigh)
self.CurrentLow.Update(algorithm.Time, previousLow)
self.TrailingStopFlag = False
self.trailingStop = TrailingStop(trailingStopDistPct, self.Signal)
algorithm.RegisterIndicator(self.Symbol, self.trailingStop, Resolution.Minute)
algorithm.WarmUpIndicator(self.Symbol, self.trailingStop, Resolution.Minute)
self.scheduledEvents = []
if tickerToPlot == self.Symbol.Value:
self.InitCharts()
self.scheduledEvents.append(algorithm.Schedule.On(algorithm.DateRules.EveryDay(self.Symbol), algorithm.TimeRules.Every(timedelta(minutes=5)), self.UpdateCharts))
@property
def IsReady(self):
return self.trailingStop.IsReady and self.CurrentHigh.IsReady and self.CurrentLow.IsReady
@property
def ExitConditionMet(self):
if self.trailingStop.Triggered:
return True
return False
def Dispose(self, algorithm):
for consolidator in self.Consolidators:
algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, consolidator)
if len(self.scheduledEvents) > 0:
for scheduledEvent in self.scheduledEvents:
scheduledEvent = None
def InitCharts(self):
chart = Chart(self.Symbol.Value, ChartType.Stacked)
chart.AddSeries(Series('Price', SeriesType.Scatter, 0, "$"))
# chart.AddSeries(Series('Price (Low)', SeriesType.Line, 0, "$"))
chart.AddSeries(Series('Trailing Stop', SeriesType.Scatter, 0, "$"))
chart.AddSeries(Series('Portfolio Exposure', SeriesType.Scatter, 1, "%"))
self.algorithm.AddChart(chart)
def UpdateCharts(self):
if self.Symbol not in self.algorithm.CurrentSlice.Bars:
return
if not self.algorithm.IsMarketOpen(self.Symbol):
return
tradeBar = self.algorithm.CurrentSlice.Bars[self.Symbol]
self.algorithm.Plot(self.Symbol.Value, 'Price', tradeBar.Close)
self.algorithm.Plot(self.Symbol.Value, 'Portfolio Exposure', self.algorithm.Portfolio[self.Symbol].HoldingsValue/self.algorithm.Portfolio.TotalPortfolioValue*100)
if not self.IsReady or self.trailingStop.Value == 0:
return
self.algorithm.Plot(self.Symbol.Value, 'Trailing Stop', self.trailingStop.Value)
class SelectionData:
def __init__(self, algorithm, symbol):
self.algorithm = algorithm
self.Symbol = symbol
self.expansionBreakoutIndicator = ExpansionBreakoutIndicator()
algorithm.WarmUpIndicator(self.Symbol, self.expansionBreakoutIndicator, Resolution.Daily)
self.relativeVolume = RelativeDailyVolume()
algorithm.WarmUpIndicator(self.Symbol, self.relativeVolume, Resolution.Daily)
def Update(self, tradeBar):
self.expansionBreakoutIndicator.Update(tradeBar)
self.relativeVolume.Update(tradeBar)
@property
def Signal(self):
if not self.IsReady:
return 0
return self.expansionBreakoutIndicator.Signal
@property
def Volume(self):
return self.expansionBreakoutIndicator.Volume
@property
def IsReady(self):
return self.expansionBreakoutIndicator.IsReady
class ExpansionBreakoutIndicator(PythonIndicator):
def __init__(self):
self.Time = datetime.min
self.Value = 0
self.Close = 0
self.High = 0
self.Low = 0
self.Volume = 0
self.rollingHigh = Maximum(42)
self.rollingLow = Minimum(42)
self.dailyRange = 0
self.largestDailyRange = Maximum(9)
self.WarmUpPeriod = 42
self.Signal = 0
self.previousHigh = 0
self.previousLow = 0
self.previousClose = 0
def Update(self, data):
self.Signal = 0
self.Time = data.Time
self.previousClose = self.Close
self.Close = data.Close
self.High = data.High
self.Low = data.Low
self.Volume = data.Volume
self.dailyRange = data.High - data.Low
if self.dailyRange >= self.largestDailyRange.Current.Value:
if self.Close > self.rollingHigh.Current.Value:
self.Signal = 1
elif self.Close < self.rollingLow.Current.Value:
self.Signal = -1
self.previousHigh = self.rollingHigh.Current.Value
self.previousLow = self.rollingLow.Current.Value
self.rollingHigh.Update(data.Time, data.High)
self.rollingLow.Update(data.Time, data.Low)
self.largestDailyRange.Update(data.Time, self.dailyRange)
@property
def IsReady(self):
return self.rollingHigh.IsReady and self.Time > datetime.min
def sign(x):
if x == 0:
return x
if x > 0:
return 1
if x < 0:
return -1
class TrailingStop(PythonIndicator):
def __init__(self, pct_dist = 0.1, direction = 1):
self.pctDist = pct_dist
self.direction = direction
self.prevClose = 0
self.Flag = False
self.Value = 0
self.Time = datetime.min
self.WarmUpPeriod = 1
def Update(self, data):
self.Time = data.Time
if self.Value*self.prevClose == 0:
self.prevClose = data.Close
self.Value = self.prevClose*(1 - self.direction*self.pctDist)
return
if self.direction == 1:
if data.Close > self.prevClose:
self.Value = data.Close*(1 - self.pctDist)
self.prevClose = data.Close
if data.Close < self.Value:
self.Flag = True
elif self.direction == -1:
if data.Close < self.prevClose:
self.Value = data.Close*(1 + self.pctDist)
self.prevClose = data.Close
if data.Close > self.Value:
self.Flag = True
@property
def IsReady(self):
return self.prevClose * self.Value != 0
def Reset(self):
self.Value = 0
self.Time = datetime.min
self.prevClose = 0
self.Flag = False
@property
def Triggered(self):
return self.Flag''' ---------- startDate ---------- The start date of the backtest in the format (YYYY, MM, DD). -------- endDate -------- The end date of the backtest in the format (YYYY, MM, DD). You can also set the endDate as a date in the future. By doing so the backtest will run to the most recent day which is usually yesterday. --------------------- numberOfStocksToScan --------------------- The number of stocks you want to scan for signals. Let you control the speed of the backtest. A number significantly larger than 1,000 will slow down the backtest dramatically without significant impact. I recommend to choose a number between 100 and 1,000. --------------------- minimumPricePerShare --------------------- The minimum price per share for the universe selection method. Example: minimumPricePerShare = 10 will ignore all stocks with a share price below 10 on that day. --------------------- maximumPricePerShare --------------------- The maximum price per share for the universe selection method. See also minimumPricePerShare above. --------------------- numberOfLongPositions --------------------- The maximum number of long positions held at the same time. ---------------------- numberOfShortPositions ---------------------- Analogous to above, the maximum number of short positions held at the same time. --------------------- initialStopDistPoints --------------------- The distance for the initial stop in points/dollars. Does not affect the trailing stop. ------------------- trailingStopDistPct ------------------- The percentage distance of the trailing stop. ------------------------ portfolioWeightPerStock ------------------------ Controls the position size as a percentage of the current total portfolio value. Example: portfolioWeightPerStock = 0.5 will allocate 50% of cash per security per signal. ------------ tickerToPlot ------------ The Ticker you'd like to choose for plotting. This will plot the price, trailing stop and portfolio weight/exposure every minute. '''
################################################################################ # ------------------------------------------------------------------------------ # The settings for your trading algorithm # ------------------------------------------------------------------------------ ################################################################################ # ------------------------------------------------------------------------------ # General settings # ------------------------------------------------------------------------------ startDate = (2022, 1, 1) endDate = (2022, 2, 15) startCash = 1000000 # ------------------------------------------------------------------------------ # Selection # ------------------------------------------------------------------------------ numberOfStocksToScan = 500 minimumPricePerShare = 15 maximumPricePerShare = 130 numberOfLongPositions = 1 numberOfShortPositions = 1 # ------------------------------------------------------------------------------ # Exit conditions, Risk Management & Position sizing # ------------------------------------------------------------------------------ initialStopDistPoints = 1 trailingStopDistPct = 1 portfolioWeightPerStock = 0.1 # --------- # Charting # --------- tickerToPlot = 'NARI'