Overall Statistics
Total Orders
503
Average Win
0.32%
Average Loss
-0.31%
Compounding Annual Return
0.819%
Drawdown
2.000%
Expectancy
0.023
Start Equity
100000
End Equity
101487.50
Net Profit
1.487%
Sharpe Ratio
-2.673
Sortino Ratio
-2.463
Probabilistic Sharpe Ratio
15.988%
Loss Rate
49%
Win Rate
51%
Profit-Loss Ratio
1.01
Alpha
-0.038
Beta
-0.01
Annual Standard Deviation
0.015
Annual Variance
0
Information Ratio
-0.789
Tracking Error
0.146
Treynor Ratio
3.98
Total Fees
$508.42
Estimated Strategy Capacity
$190000000.00
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
38.50%
#region imports
from AlgorithmImports import *
#endregion
from datetime import timedelta, datetime

class SMAPairsTrading(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2022, 6, 1)   
        self.SetEndDate(2024, 6, 1)
        self.SetCash(100000)
        
        symbols = [
            Symbol.Create("QQQQ", SecurityType.Equity, Market.USA), 
            Symbol.Create("SPY", SecurityType.Equity, Market.USA)
        ]
        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.spreadMean1 = SimpleMovingAverage(12)
        self.spreadMean2 = SimpleMovingAverage(26)
        self.period = timedelta(hours=4)
        
    def Update(self, algorithm, data):
        spread = self.pair[1].Price - self.pair[0].Price
        self.spreadMean1.Update(algorithm.Time, spread)
        self.spreadMean2.Update(algorithm.Time, spread)
        
        sprmean1_t1 = self.spreadMean1[1]
        sprmean2_t1 = self.spreadMean2[1]
        sprmean1_t2 = self.spreadMean1[2]
        sprmean2_t2 = self.spreadMean2[2]
        sprmean1_t3 = self.spreadMean1[3]
        sprmean2_t3 = self.spreadMean2[3]

        indicator_valid = all([ ele is not None for ele in [sprmean1_t1, sprmean2_t1, sprmean1_t2, sprmean2_t2, sprmean1_t3, sprmean2_t3] ])

        if indicator_valid:
            buy_signal = (sprmean1_t1>sprmean2_t1) and (sprmean1_t2<sprmean2_t2) and (sprmean1_t3<sprmean2_t3)
            sell_signal = (sprmean1_t1<sprmean2_t1) and (sprmean1_t2>sprmean2_t2) and (sprmean1_t3>sprmean2_t3)
        else:
            buy_signal = False
            sell_signal = False

        if indicator_valid and buy_signal:
            return Insight.Group(
                [
                    Insight.Price(self.pair[0].Symbol, self.period, InsightDirection.Up),
                    Insight.Price(self.pair[1].Symbol, self.period, InsightDirection.Down)
                ])
        
        if indicator_valid and sell_signal:
            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.spreadMean1.Update(tuple[0], tuple[2]-tuple[1])
            self.spreadMean2.Update(tuple[0], tuple[2]-tuple[1])