Created with Highcharts 12.1.2EquityJan 2023Mar 2023May 2023Jul 2023Sep 2023Nov 2023Jan 2024Mar 2024May 2024Jul 2024Sep 2024Nov 2024Jan 2025500k1,000k1,500k2,000k-20-10000.51-2020250M500M050M100M0100
Overall Statistics
Total Orders
143
Average Win
2.45%
Average Loss
-1.20%
Compounding Annual Return
13.682%
Drawdown
14.700%
Expectancy
0.413
Start Equity
1000000
End Equity
1292145.98
Net Profit
29.215%
Sharpe Ratio
0.276
Sortino Ratio
0.595
Probabilistic Sharpe Ratio
9.731%
Loss Rate
54%
Win Rate
46%
Profit-Loss Ratio
2.05
Alpha
0.07
Beta
-0.037
Annual Standard Deviation
0.238
Annual Variance
0.057
Information Ratio
-0.17
Tracking Error
0.263
Treynor Ratio
-1.754
Total Fees
$6884.93
Estimated Strategy Capacity
$130000000.00
Lowest Capacity Asset
EW RTKJ7O7ZTI79
Portfolio Turnover
10.81%
import numpy as np
import pandas as pd
import statsmodels.api as sm
from AlgorithmImports import *

class PairsTradingStrategy(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2022, 12, 1)
        self.SetEndDate(2024, 12, 1)
        self.SetCash(1_000_000)  # $1M initial capital
        
        self.lookback = 90  # Lookback period for z-score calculation
        self.entry_threshold = 0.6
        self.exit_threshold = 0
        self.risk_threshold = 1.5
        self.pair = ["BSX", "EW"]  # Replace with the selected pair with lowest ADF
        
        # Add securities
        self.symbols = [self.AddEquity(ticker, Resolution.Daily).Symbol for ticker in self.pair]
        
        # Store historical data
        self.history = {sym: [] for sym in self.symbols}
        self.portfolio_state = 0  # 1: long, -1: short, 0: no position
        
        # Track current hedge ratio
        self.hedge_ratio = None
    
    def OnData(self, data):
        if not all(symbol in data for symbol in self.symbols):
            return
        
        # Store latest closing prices
        for symbol in self.symbols:
            self.history[symbol].append(data[symbol].Close)
            if len(self.history[symbol]) > self.lookback:
                self.history[symbol].pop(0)  # Maintain rolling window
        
        if len(self.history[self.symbols[0]]) < self.lookback:
            return  # Wait until we have enough data
        
        # Perform linear regression on log prices
        log_prices_P = np.log(self.history[self.symbols[0]])
        log_prices_Q = np.log(self.history[self.symbols[1]])
        
        model = sm.OLS(log_prices_P, sm.add_constant(log_prices_Q)).fit()
        self.hedge_ratio = model.params[1]  # Beta coefficient
        
        # Compute spread and z-score
        spread = log_prices_P - self.hedge_ratio * log_prices_Q
        mean_spread = np.mean(spread)
        std_spread = np.std(spread)
        zscore = (spread[-1] - mean_spread) / std_spread if std_spread != 0 else 0
        
        self.wt1 = 1/(1+self.hedge_ratio)
        self.wt2 = self.hedge_ratio/(1+self.hedge_ratio)


        # **Risk Management: Liquidate all positions if |zscore| > 3**
        if abs(zscore) > self.risk_threshold:
            self.Liquidate()
            self.portfolio_state = 0
            return


        # Entry conditions
        if self.portfolio_state == 0:
            if zscore < -self.entry_threshold:
                # Long P, short Q
                self.SetHoldings(self.symbols[0], self.wt1)
                self.SetHoldings(self.symbols[1], -self.wt2)
                self.portfolio_state = 1
            
            elif zscore > self.entry_threshold:
                # Short P, long Q
                self.SetHoldings(self.symbols[0], -self.wt1)
                self.SetHoldings(self.symbols[1], self.wt2)
                self.portfolio_state = -1
        
        # Exit conditions
        elif self.portfolio_state == 1 and zscore > self.exit_threshold:
            self.Liquidate()  # Close all positions
            self.portfolio_state = 0
        elif self.portfolio_state == -1 and zscore < self.exit_threshold:
            self.Liquidate()  # Close all positions
            self.portfolio_state = 0