Overall Statistics
Total Orders
34
Average Win
2.51%
Average Loss
-3.39%
Compounding Annual Return
2.272%
Drawdown
12.900%
Expectancy
0.088
Start Equity
1000000
End Equity
1022691.39
Net Profit
2.269%
Sharpe Ratio
-0.361
Sortino Ratio
-0.284
Probabilistic Sharpe Ratio
17.130%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
0.74
Alpha
-0.053
Beta
0.106
Annual Standard Deviation
0.096
Annual Variance
0.009
Information Ratio
-1.552
Tracking Error
0.132
Treynor Ratio
-0.326
Total Fees
$319.09
Estimated Strategy Capacity
$700000000.00
Lowest Capacity Asset
MSFT R735QTJ8XC9X
Portfolio Turnover
4.75%
from AlgorithmImports import *
from statsmodels.tsa.stattools import adfuller
import numpy as np

class PairsTradingStrategy(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2023, 12, 1)
        self.SetEndDate(2024, 12, 1)
        self.SetCash(1000000)  # $1M starting capital
        
        self.pair = ("MSFT", "NVDA")
        self.symbol1 = self.AddEquity(self.pair[0], Resolution.Daily).Symbol
        self.symbol2 = self.AddEquity(self.pair[1], Resolution.Daily).Symbol
        
        self.lookback = 20
        self.entry_threshold = 2
        self.exit_threshold = 0
        self.position = 0
        
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(15, 45), self.TradeLogic)

    def TradeLogic(self):
        history = self.History([self.symbol1, self.symbol2], self.lookback, Resolution.Daily)
        if history.empty:
            return
        
        closes = history['close'].unstack(level=0)
        spread = np.log(closes[self.symbol1]) - np.log(closes[self.symbol2])
        
        mean = spread.mean()
        std = spread.std()
        zscore = (spread.iloc[-1] - mean) / std
        
        weight1 = 1 / (1 + self.GetBeta())
        weight2 = self.GetBeta() / (1 + self.GetBeta())
        
        if self.position == 0:
            if zscore > self.entry_threshold:
                self.SetHoldings(self.symbol1, -weight1)
                self.SetHoldings(self.symbol2, weight2)
                self.position = -1
            elif zscore < -self.entry_threshold:
                self.SetHoldings(self.symbol1, weight1)
                self.SetHoldings(self.symbol2, -weight2)
                self.position = 1
        elif self.position == 1 and zscore >= self.exit_threshold:
            self.Liquidate()
            self.position = 0
        elif self.position == -1 and zscore <= -self.exit_threshold:
            self.Liquidate()
            self.position = 0

    def GetBeta(self):
        history = self.History([self.symbol1, self.symbol2], self.lookback, Resolution.Daily)
        returns = history['close'].unstack(level=0).pct_change().dropna()
        if returns.empty:
            return 1
        
        beta = np.cov(returns[self.symbol1], returns[self.symbol2])[0, 1] / np.var(returns[self.symbol2])
        return beta