Overall Statistics
Total Trades
109
Average Win
0.32%
Average Loss
-0.02%
Compounding Annual Return
57.930%
Drawdown
1.500%
Expectancy
2.979
Net Profit
3.909%
Sharpe Ratio
5.652
Probabilistic Sharpe Ratio
90.435%
Loss Rate
73%
Win Rate
27%
Profit-Loss Ratio
13.85
Alpha
0.436
Beta
0.091
Annual Standard Deviation
0.082
Annual Variance
0.007
Information Ratio
1.18
Tracking Error
0.156
Treynor Ratio
5.052
Total Fees
$123.12
Estimated Strategy Capacity
$3400000.00
from datetime import timedelta, datetime

class SMAPairsTrading(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2021, 3, 1)   
        self.SetEndDate(2021, 3, 31)
        self.SetCash(100000)

        pairIndex   = int(self.GetParameter("pairIndex"))
        symbolPairs = []
        symbolPairs.append ( [Symbol.Create("DISH", SecurityType.Equity, Market.USA), Symbol.Create("UNM", SecurityType.Equity, Market.USA)] )
        symbolPairs.append ( [Symbol.Create("SBUX", SecurityType.Equity, Market.USA), Symbol.Create("IBM", SecurityType.Equity, Market.USA)] )
        symbolPairs.append ( [Symbol.Create("TSLA", SecurityType.Equity, Market.USA), Symbol.Create("SPY", SecurityType.Equity, Market.USA)] )

        symbols = symbolPairs[pairIndex]
        
        self.AddUniverseSelection(ManualUniverseSelectionModel(symbols))
        self.UniverseSettings.Resolution = Resolution.Hour        
        self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw
        self.AddAlpha(PairsTradingAlphaModel())
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetExecution(ImmediateExecutionModel())
        
    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):
        self.pair = [ ]
        self.spreadMean = SimpleMovingAverage(500)
        self.spreadStd = StandardDeviation(500)
        self.period = timedelta(hours=2)
        
    def Update(self, algorithm, data):
        spread = self.pair[1].Price - self.pair[0].Price
        self.spreadMean.Update(algorithm.Time, spread)
        self.spreadStd.Update(algorithm.Time, spread) 
        
        upperthreshold = self.spreadMean.Current.Value + self.spreadStd.Current.Value
        lowerthreshold = self.spreadMean.Current.Value - self.spreadStd.Current.Value

        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 OnSecuritiesChanged(self, algorithm, changes):
        self.pair = [x for x in changes.AddedSecurities]
        
        #1. Call for 500 bars of history data for each symbol in the pair and save to the variable history
        history = algorithm.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])