Overall Statistics |
Total Trades 2 Average Win 0% Average Loss 0% Compounding Annual Return 7.325% Drawdown 0.100% Expectancy 0 Net Profit 0.129% Sharpe Ratio 12.175 Probabilistic Sharpe Ratio 100.000% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0.116 Beta -0.063 Annual Standard Deviation 0.005 Annual Variance 0 Information Ratio -12.499 Tracking Error 0.06 Treynor Ratio -1.035 Total Fees $2.00 |
from clr import AddReference AddReference("System") AddReference("QuantConnect.Algorithm") AddReference("QuantConnect.Common") AddReference("QuantConnect.Indicators") from System import * from QuantConnect import * from QuantConnect.Algorithm import * from QuantConnect.Data.Market import TradeBar from QuantConnect.Algorithm.Framework import * from QuantConnect.Algorithm.Framework.Risk import * from QuantConnect.Orders.Fees import ConstantFeeModel from QuantConnect.Algorithm.Framework.Alphas import * from QuantConnect.Algorithm.Framework.Execution import * from QuantConnect.Algorithm.Framework.Portfolio import * from QuantConnect.Algorithm.Framework.Selection import * from QuantConnect.Indicators import RollingWindow, SimpleMovingAverage from datetime import timedelta, datetime import numpy as np import sys import decimal as d class SMAPairsTrading(QCAlgorithm): def Initialize(self): self.SetStartDate(2020,2,8) self.SetEndDate(2020,2,14) self.SetCash(100000) symbols = [Symbol.Create("Z", SecurityType.Equity, Market.USA), Symbol.Create("ZG", SecurityType.Equity, Market.USA)] self.AddUniverseSelection(ManualUniverseSelectionModel(symbols)) self.res=self.UniverseSettings.Resolution = Resolution.Minute self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw #variables established in subroutine that need to be available universally self.upperthreshold=None self.lowerthreshold=None self.midprice = None self.longSymbol = None self.shortSymbol = None self.pair = [ ] self.spread = None self.deviation = 0.2 self.minProfit = 10.00 self.maxMarginPain = 200.00 self.maxDelta = 0.03 #difference in pennies from midpoint later to be calculated based on pair algo=self self.period=500 #these are minutes of lookback #self.alphaModel= self.AddAlpha(qc_PairsTradingAlphaModel(algo,self.deviation,self.period,self.minProfit,self.maxMarginPain)) self.alphaModel= self.AddAlpha(PairsTradingAlphaModel(algo)) #self.SetPortfolioConstruction( NullPortfolioConstructionModel() ) self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel()) #self.SetExecution(ImmediateExecutionModel()) self.SetExecution(qc_PairsTradingExecutionModel(algo,self.period,self.deviation,self.res)) self.SetBrokerageModel(AlphaStreamsBrokerageModel()) def setLimits(self,upper,lower,spread): self.upperthreshold = upper self.lowerthreshold = lower self.spread = spread def OnEndOfDay(self, symbol): self.Log("Taking a position of " + str(self.Portfolio[symbol].Quantity) + " units of symbol " + str(symbol)) class PairsTradingAlphaModel(AlphaModel): def __init__(self,algo): self.pair = [ ] self.spreadMean = SimpleMovingAverage(500) self.spreadStd = StandardDeviation(500) self.period = timedelta(minutes=5) self.algo = algo def Update(self, algo, data): ## Check to see if either ticker will return a NoneBar, and skip the data slice if so for security in algo.Securities: if self.DataEventOccured(data, security.Key): insights = [] #added by Serge return insights if self.algo is None: self.algo = algo spread = self.pair[1].Price - self.pair[0].Price self.spreadMean.Update(algo.Time, spread) self.spreadStd.Update(algo.Time, spread) deviation = 2 upperthreshold = self.spreadMean.Current.Value + self.spreadStd.Current.Value * deviation lowerthreshold = self.spreadMean.Current.Value - self.spreadStd.Current.Value * deviation self.algo.setLimits(upperthreshold,lowerthreshold,spread) if spread > upperthreshold: algo.Log("up signal at {}". format(algo.Time)) return Insight.Group( [ Insight.Price(self.pair[0].Symbol, self.period, InsightDirection.Up), Insight.Price(self.pair[1].Symbol, self.period, InsightDirection.Down) ]) if spread < lowerthreshold: algo.Log("down signal at {}". format(algo.Time)) return Insight.Group( [ Insight.Price(self.pair[0].Symbol, self.period, InsightDirection.Down), Insight.Price(self.pair[1].Symbol, self.period, InsightDirection.Up) ]) return [] def DataEventOccured(self, data, symbol): ## Helper function to check to see if data slice will contain a symbol if data.Splits.ContainsKey(symbol) or \ data.Dividends.ContainsKey(symbol) or \ data.Delistings.ContainsKey(symbol) or \ data.SymbolChangedEvents.ContainsKey(symbol): return True def OnSecuritiesChanged(self, algo, changes): self.pair = [x for x in changes.AddedSecurities] algo.pair = self.pair #1. Call for 500 days of history data for each symbol in the pair and save to the variable history history = algo.History([x.Symbol for x in self.pair],500) #2. Unstack the Pandas data frame to reduce it to the history close price and place stocks side by side history = history.close.unstack(level=0) #3. Iterate through the history tuple and update the mean and standard deviation with historical data for tuple in history.itertuples(): self.spreadMean.Update(tuple[0], tuple[2]-tuple[1]) self.spreadStd.Update(tuple[0], tuple[2]-tuple[1]) ''' if self.spreadMean: algo.Log("mean {}" . format(self.spreadMean)) ''' class qc_PairsTradingAlphaModel(AlphaModel): def __init__(self,algo,deviation=2,period=500,minProfit=10.00,maxLoss=100.00): self.period = period # this is lookback for STD and Mean calcs timedelta(minutes=5)# (seconds=300) 5 minutes self.spreadMean = SimpleMovingAverage(period)# 30000 seconds = 500 minutes = 8.3 hours. self.spreadStd = StandardDeviation(period) if deviation is None: deviation = 2 self.dev = deviation self.minProfit = minProfit self.maxMarginPain = maxLoss self.algo = algo self.pair = [] def Update(self, algo, data): ## Check to see if either ticker will return a NoneBar, and skip the data slice if so for security in algo.Securities: if self.DataEventOccured(data, security.Key): insights = [] #added by Serge return insights spread = self.pair[1].Price - self.pair[0].Price self.spreadMean.Update(algo.Time, spread) self.spreadStd.Update(algo.Time, spread) upperthreshold = self.spreadMean.Current.Value + self.spreadStd.Current.Value*4 lowerthreshold = self.spreadMean.Current.Value - self.spreadStd.Current.Value*4 if spread > upperthreshold: return Insight.Group( [ Insight.Price(self.pair[0].Symbol, self.period, InsightDirection.Up), Insight.Price(self.pair[1].Symbol, self.period, InsightDirection.Down) ]) if spread < lowerthreshold: return Insight.Group( [ Insight.Price(self.pair[0].Symbol, self.period, InsightDirection.Down), Insight.Price(self.pair[1].Symbol, self.period, InsightDirection.Up) ]) return [] def DataEventOccured(self, data, symbol): ## Helper function to check to see if data slice will contain a symbol if data.Splits.ContainsKey(symbol) or \ data.Dividends.ContainsKey(symbol) or \ data.Delistings.ContainsKey(symbol) or \ data.SymbolChangedEvents.ContainsKey(symbol): return True return False def OnSecuritiesChanged(self, algo, changes): self.pair = [x for x in changes.AddedSecurities] algo.pair = self.pair #1. Call for 500 days of history data for each symbol in the pair and save to the variable history history = self.algo.History([x.Symbol for x in self.pair], 500) #2. Unstack the Pandas data frame to reduce it to the history close price history = history.close.unstack(level=0) #3. Iterate through the history tuple and update the mean and standard deviation with historical data for tuple in history.itertuples(): self.spreadMean.Update(tuple[0], tuple[2]-tuple[1]) self.spreadStd.Update(tuple[0], tuple[2]-tuple[1]) class qc_PairsTradingExecutionModel(ExecutionModel): def __init__(self, algo, period = 60, deviation = 2, resolution = Resolution.Minute): '''Initializes a new instance of the StandardDeviationExecutionModel class Args: period: Period of the standard deviation indicator deviation: The number of deviation away from the mean before submitting an order resolution: The resolution of the STD and SMA indicators''' self.targetsCollection = PortfolioTargetCollection() self.pair = [] self.algo = algo self.period = period self.targetsCollection = PortfolioTargetCollection() self.period = period self.deviation = deviation self.resolution = resolution self.symbolData = {} # Gets or sets the maximum order value in units of the account currency. # This defaults to $10000. For example, if purchasing a stock with a price # of $100, then the maximum order size would be 20 shares. self.maximumOrderValue = 40000 self.longQuantity = d.Decimal(0.00) self.shortQuantity = d.Decimal(0.00) self.lastLongLimit= d.Decimal(0.00) self.lastShortLimit = d.Decimal(0.00) self.longChunkLimitId = None self.shortChunkLimitId = None self.chunksSet = False self.shortChunk = 40 self.longChunk = 40 if not hasattr(self,"reversal" ): self.reversal = False # to denote flip of symbols to long and short self.chunksSet = False self.targetLongQuantity = 0.00 self.targetShortQuantity = 0.00 self.someVal = "test" self.caller = self self.longSymbol = None self.shortSymbol = None def Execute(self,algo, targets): try: self.targetsCollection.AddRange(targets) if self.targetsCollection.Count > 0: for target in self.targetsCollection: symbol = target.Symbol # fetch our symbol data containing our STD/SMA indicators symbolData = self.symbolData.get(symbol, None) if symbolData is None: #prepare for next time self.symbolData[symbol] = SymbolData(algo, symbol, self.algo.period, self.algo.res) return # check order entry conditions if symbolData.STD.IsReady : isLongTrade = np.sign(target.Quantity) == 1 isShortTrade = np.sign(target.Quantity) == -1 isFlatTrade = np.sign(target.Quantity) == 0 unorderedQuantity = OrderSizing.GetUnorderedQuantity(self.algo, target) #gathere needed variables # get the maximum order size based on total order value chunkOrderSize = self.shortChunk if isShortTrade else self.longChunk orderSize = np.min([chunkOrderSize, np.floor(unorderedQuantity)]) if isLongTrade else np.max([chunkOrderSize,np.ceil(unorderedQuantity)]) #round down to even integer orderSize = np.floor(orderSize) if isLongTrade else np.ceil(orderSize) price = d.Decimal(self.algo.Securities[target.Symbol].Price) longs_open= sum([x.Quantity for x in algo.Transactions.GetOpenOrders(self.longSymbol)]) shorts_open= sum([x.Quantity for x in algo.Transactions.GetOpenOrders(self.shortSymbol)]) if orderSize == 0 : continue if isLongTrade: limit_price = price + self.algo.maxDelta #ok to place new chunk order on long side if self.longChunkLimitId is None: self.longChunkLimitId = algo.LimitOrder(target.Symbol,orderSize, limit_price) algo.Log("Time {} symbol {} price {} limit {} quantity {} " . format(algo.Time,target.Symbol,price,limit_price,orderSize)) self.lastLongLimit = limit_price elif isShortTrade: limit_price = price - self.algo.maxDelta if self.shortChunkLimitId is None: #ok to place new chunk order on long side self.shortChunkLimitId = algo.LimitOrder(target.Symbol,orderSize, limit_price, tag="limit short order") algo.Log("Time {} symbol {} price {} limit {} quantity {} " . format(algo.Time,target.Symbol,price,limit_price,orderSize)) self.lastShortLimit = limit_price #after for loop clear out targets Collection. may not clear out long orders we'll see self.targetsCollection.ClearFulfilled(algo) except Exception as e: self.Log("an error occurred at time {} " . format(self.Time)) self.Log('An unknown error occurred trying OnData {} line {} ' + str(sys.exc_info()[0]) ) self.Log('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) def OnOrderEvent(self, OrderEvent): orderId = self.Transactions.GetOrderById(OrderEvent.OrderId) self.algo.Log("Event detected: {0} {1}".format(self.orderTypeDict[OrderEvent.Type], self.orderDirectionDict[OrderEvent.Direction])) self.algo.Log("{0}".format(OrderEvent)) if OrderEvent.Status == OrderStatus.Invalid: if orderId == self.longChunkLimitId: self.longChunkLimitId = None elif orderId == self.shortChunkLimitId: self.longChunkLimitId = None elif orderId == self.longChunkMarketId: self.longChunkMarketId = None elif orderId == self.shortChunkMarketId: self.shortChunkMarketId = None self.algo.Log("Time {} ERROR : Invalid order " . format(self.algo.Time)) return if OrderEvent.Status == OrderStatus.Filled: if orderId == self.longChunkLimitId: self.longChunkLimitId = None elif orderId == self.shortChunkLimitId: self.longChunkLimitId = None elif orderId == self.longChunkMarketId: self.longChunkMarketId = None elif orderId == self.shortChunkMarketId: self.shortChunkMarketId = None self.algo.Log("{} was filled. Symbol: {}. Quantity: {}. Direction: {}" .format(str(OrderEvent.Type), str(OrderEvent.Symbol), str(OrderEvent.FillQuantity), str(OrderEvent.Direction))) return def getUnorderedQuantity(self,algo,target): holdings= algo.Portfolio[target.Symbol].Quantity open = sum([x.Quantity for x in algo.Transactions.GetOpenOrders(target.Symbol)]) if target.Quantity < 0 : open = open * -1 targetQ = target.Quantity remainder = targetQ - holdings - open return remainder def OnSecuritiesChanged(self, algo, changes): '''Event fired each time the we add/remove securities from the data feed Args: algo: The algo instance that experienced the change in securities changes: The security additions and removals from the algo but this was already done by our alpha model so no need to duplicate on execution side ''' self.pair = [x for x in changes.AddedSecurities] self.algo.pair = self.pair for added in changes.AddedSecurities: if added.Symbol not in self.symbolData: self.symbolData[added.Symbol] = SymbolData(algo, added, self.period, self.resolution) algo.Log("successfully added {}" .format(added.Symbol)) for removed in changes.RemovedSecurities: # clean up data from removed securities symbol = removed.Symbol if symbol in self.symbolData: if self.IsSafeToRemove(algo, symbol): data = self.symbolData.pop(symbol) algo.SubscriptionManager.RemoveConsolidator(symbol, data.Consolidator) def IsSafeToRemove(self, algo, symbol): '''Determines if it's safe to remove the associated symbol data''' # confirm the security isn't currently a member of any universe return not any([kvp.Value.ContainsMember(symbol) for kvp in algo.UniverseManager]) def OnOrderEvent(self, OrderEvent): orderId = self.Transactions.GetOrderById(OrderEvent.OrderId) self.algo.Log("Event detected: {0} {1}".format(self.orderTypeDict[OrderEvent.Type], self.orderDirectionDict[OrderEvent.Direction])) self.algo.Log("{0}".format(OrderEvent)) if OrderEvent.Status == OrderStatus.Invalid: if orderId == self.longChunkLimitId: self.longChunkLimitId = None elif orderId == self.shortChunkLimitId: self.longChunkLimitId = None elif orderId == self.longChunkMarketId: self.longChunkMarketId = None elif orderId == self.shortChunkMarketId: self.shortChunkMarketId = None self.algo.Log("Time {} ERROR : Invalid order " . format(self.algo.Time)) return if OrderEvent.Status == OrderStatus.Filled: if orderId == self.longChunkLimitId: self.longChunkLimitId = None elif orderId == self.shortChunkLimitId: self.longChunkLimitId = None elif orderId == self.longChunkMarketId: self.longChunkMarketId = None elif orderId == self.shortChunkMarketId: self.shortChunkMarketId = None self.algo.Log("{} was filled. Symbol: {}. Quantity: {}. Direction: {}" .format(str(OrderEvent.Type), str(OrderEvent.Symbol), str(OrderEvent.FillQuantity), str(OrderEvent.Direction))) return class SymbolData: def __init__(self, algo, security, period, resolution): symbol = security.Symbol self.Security = security self.Consolidator = algo.ResolveConsolidator(symbol, resolution) smaName = algo.CreateIndicatorName(symbol, f"SMA{period}", resolution) self.SMA = SimpleMovingAverage(smaName, period) algo.RegisterIndicator(symbol, self.SMA, self.Consolidator) stdName = algo.CreateIndicatorName(symbol, f"STD{period}", resolution) self.STD = StandardDeviation(stdName, period) algo.RegisterIndicator(symbol, self.STD, self.Consolidator) # warmup our indicators by pushing history through the indicators history = algo.History(symbol, period, resolution) if 'close' in history: history = history.close.unstack(0).squeeze() for time, value in history.iteritems(): self.SMA.Update(time, value) self.STD.Update(time, value)