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)