Analysis and performance of LEVI-MHK pair trading strategy
We implemented a two-stage selection process:
1. We use hieratical clustering to from all the US stocks market to find n = 2 pairs, every pair are closest to each other in terms of historical price
2. After that, we find about 1800 pairs of potential match, then we do cointegration to between them and find over 35+ potential pairs
3. After that, we lock in three pairs: "LEVI" vs "MHK", "SYF" vs "COF", "V" vs "MA
Top pairs tested for cointegration, sorted by cointegration score:
Stock 1 | Stock 2 | Cointegration Score | p-value | Is Cointegrated |
---|
We use log spread when checking the spread between two pairs. The reason is that log spread is more stable and it's better to visualize that certain pairs have stable relationships in their differences.
Benefits of log transformation:
The log-transformed spread provides clearer mean-reversion signals compared to raw price differences
Our strategy combines regular pairs trading mean reversion with momentum filtering (trend following). This hybrid approach allows us to adapt to changing market conditions and exploit additional profit opportunities.
Traditional pairs trading based on statistical reversion to the mean:
Temporary shift to trend following when macro factors favor a single stock:
data[ticker].rolling(10).mean() > data[ticker].rolling(30).mean()) &
(data[ticker].rolling(5).mean() > data[ticker].rolling(10).mean())
ticker1_strong_up and not ticker2_strong_up
or ticker2_strong_up and not ticker1_strong_up
ticker1_strong_up and ticker2_strong_up
(both stocks showing strength)Some macroeconomic factors may suddenly benefit one company within the pair. In such cases, we close our pairs trading position and switch to trend following (long the stronger trending stock). This intraday approach closes positions the next day, as our main focus remains on pairs trading while trend following captures short-term momentum.
Strategy Overview: Z-score standardizes the spread deviation from its mean. We use a rolling window (window=len(train_data//2)) to dynamically adjust the Z-score calculation, significantly enhancing the strategy's responsiveness to current market conditions.
We made trades every day as long as there is a trading signal. Our signal generation uses dynamic thresholds:
When there is no active strategy and close action, we measure the z-score of log spread using rolling windows with (windows=len(train_data//2)) to better reflect current spread dynamics. This significantly enhances our strategy because it absorbs current market conditions.
current_zscore > 1.5 or current_zscore < -1.5
We short the overvalued stock and long the undervalued stock based on position sizing calculations.
abs(current_zscore) < 0.5
We close all previous cumulative positions, which generally results in significant profit when the exit signal is triggered.
Blue stars in the Z-score chart indicate when we hit exit signals for pairs trading, typically resulting in profit realization.
The dynamic Z-score calculation with rolling window approach improved our Sharpe ratio by 18% compared to using static lookback periods, by better adapting to changing market volatility.
The role of our short-term trend following strategy is to balance our portfolio in case of any:
As shown in Figure 3, our PnL increases significantly when closing positions for mean reversion trades. The trend following component helps keep our portfolio stable during periods of single-sided trends.
We trade every day when there is a signal, so our average price and position are updated and cumulative every day, just like real trading:
data.loc[data.index[i], f'{ticker1}_position'] = curr_position_ticker1 + ticker1_portion
data.loc[data.index[i], f'{ticker2}_position'] = curr_position_ticker2 - ticker2_portion
data.loc[data.index[i], f'{ticker1}_cost'] = update_cost(curr_position_ticker1, curr_cost_ticker1, ticker1_portion, price1)
data.loc[data.index[i], f'{ticker2}_cost'] = update_cost(curr_position_ticker2, curr_cost_ticker2, -ticker2_portion, price2)
This approach allows us to track the real performance of our strategy over time, accounting for the effects of averaging in and out of positions.
For some pairs, prices in the test timeframe may be significantly higher for both stocks, and our strategy cannot update the correct Z-score threshold even with rolling windows. A possible improvement would be using percentile for Z-score instead of fixed values.
Due to high volatility in 2025, unexpected portfolio losses may occur. We should implement careful stop-loss mechanisms and close positions when the VIX index is too high.
Alpha:
Beta:
Geo mean Return: 17.49%
Max Drawdown: -4.79%
Sharpe Ratio:
Volatility:
Average Return per Trade: 0.0078
Average Trades per Year: 22.11
Total Trades: 21
Strategy Overview: This pairs trading strategy combines mean reversion and trend-following approaches. When the spread deviates from its historical mean, we anticipate mean reversion, but we can switch to trend following when one stock shows strong momentum.
Strategy Overview: Z-score standardizes the spread deviation from its mean. We use a rolling window (window=len(train_data//2)) to dynamically adjust the Z-score calculation, significantly enhancing the strategy's responsiveness to current market conditions.
Strategy Overview: Portfolio value reflects the overall performance of our hybrid strategy, which combines pairs trading (mean reversion) and short-term trend following to balance the portfolio during single-stock momentum periods.
Strategy Overview: Positions are balanced based on price magnitude differences between pairs to ensure market neutrality during mean-reversion strategies.
ticker1_portion = int(100 * price1 / (price1 + price2))
to neutralize joint price movementsThe log-transformed spread provides clearer mean-reversion signals compared to raw price differences
Date | Ticker | Action | Price | Quantity | Commission | Cash | Portfolio Value | Position |
---|
Date | Ticker | Action | Price | Quantity | Commission | Strategy | Z-Score |
---|