Overall Statistics
Total Orders
32
Average Win
1.44%
Average Loss
-2.54%
Compounding Annual Return
-12.620%
Drawdown
18.100%
Expectancy
-0.314
Start Equity
1000000
End Equity
873943.39
Net Profit
-12.606%
Sharpe Ratio
-1.492
Sortino Ratio
-1.467
Probabilistic Sharpe Ratio
0.803%
Loss Rate
56%
Win Rate
44%
Profit-Loss Ratio
0.57
Alpha
-0.12
Beta
-0.119
Annual Standard Deviation
0.094
Annual Variance
0.009
Information Ratio
-2.111
Tracking Error
0.147
Treynor Ratio
1.176
Total Fees
$291.86
Estimated Strategy Capacity
$910000000.00
Lowest Capacity Asset
AAPL R735QTJ8XC9X
Portfolio Turnover
4.42%
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 = ("AAPL", "MSFT")
        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