| 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
# Define trading pair with lowest ADF
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 # No position initially
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
# Get closing prices
closes = history['close'].unstack(level=0)
spread = np.log(closes[self.symbol1]) - np.log(closes[self.symbol2])
# Compute z-score
mean = spread.mean()
std = spread.std()
zscore = (spread.iloc[-1] - mean) / std
# Calculate position sizes based on portfolio allocation
weight1 = 1 / (1 + self.GetBeta())
weight2 = self.GetBeta() / (1 + self.GetBeta())
capital_per_stock = self.Portfolio.Cash / 2
qty1 = int(capital_per_stock / closes[self.symbol1].iloc[-1])
qty2 = int(capital_per_stock / closes[self.symbol2].iloc[-1])
# Trading logic
if self.position == 0:
if zscore > self.entry_threshold:
self.SetHoldings(self.symbol1, -weight1) # Short MSFT
self.SetHoldings(self.symbol2, weight2) # Long NVDA
self.position = -1
elif zscore < -self.entry_threshold:
self.SetHoldings(self.symbol1, weight1) # Long MSFT
self.SetHoldings(self.symbol2, -weight2) # Short NVDA
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):
# Estimate beta using past returns
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
def OnEndOfAlgorithm(self):
# Compute performance metrics
returns = self.Portfolio.TotalPortfolioValue / 1000000 - 1
sharpe_ratio = self.Portfolio.TotalPortfolioValue / np.std(returns) if np.std(returns) != 0 else 0
self.Debug(f"Final Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")
self.Debug(f"Backtest Return: {returns:.2%}")
self.Debug(f"Sharpe Ratio: {sharpe_ratio:.2f}")