| 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)