Module 1 · Chapter 6 · Lesson 7

Rolling Half-Life Estimation for Regime Detection

5 min readHalf-Life of Mean Reversion
The Black Book of Day Trading Strategies
Free Book

The Black Book of Day Trading Strategies

1,000 complete strategies · 31 chapters · Full trade plans

Understanding Mean Reversion Half-Life

Mean reversion strategies profit from prices returning to an average. The speed of this return directly impacts strategy profitability. Half-life quantifies this speed. It measures the time for a deviation from the mean to decay by 50%. A shorter half-life indicates faster mean reversion. A longer half-life suggests slower reversion or even a trend.

Calculate half-life from the Ornstein-Uhlenbeck (OU) process. This stochastic process models mean-reverting behavior. The OU process equation is $dX_t = \theta(\mu - X_t)dt + \sigma dW_t$. Here, $X_t$ is the asset price at time $t$. $\mu$ is the long-term mean. $\theta$ is the speed of reversion. $\sigma$ is the volatility. $dW_t$ is a Wiener process.

The half-life, $T_{1/2}$, relates to $\theta$ by the formula: $T_{1/2} = \frac{\ln(2)}{\theta}$. Estimate $\theta$ by fitting an AutoRegressive (AR(1)) model to the price series. The AR(1) model is $X_t - X_{t-1} = \alpha + \beta X_{t-1} + \epsilon_t$. The speed of reversion $\theta$ equals $-\beta$.

Consider a stock pair, MSFT and QQQ. We form a spread $S_t = \text{MSFT}t - \text{QQQ}t$. We expect this spread to mean revert. We fit an AR(1) model to the spread's daily changes: $\Delta S_t = \alpha + \beta S{t-1} + \epsilon_t$. Suppose regression yields $\beta = -0.05$. Then $\theta = 0.05$. The half-life is $T{1/2} = \frac{\ln(2)}{0.05} \approx \frac{0.693}{0.05} \approx 13.86$ days. This means a deviation from the spread's mean will halve in about 14 trading days. A trader uses this to set profit targets and holding periods.

Rolling Half-Life for Regime Detection

Market regimes change. Mean reversion characteristics change with them. A fixed half-life estimate becomes stale. Rolling half-life estimation addresses this. It calculates half-life over a moving window of data. This reveals shifts in mean reversion speed. Such shifts signal regime changes.

A rolling window of 60 trading days is common for daily data. For each window, we perform the AR(1) regression. We extract the $\beta$ coefficient. We calculate the half-life. Plotting these half-life values over time reveals trends.

Example: We analyze the spread between SPY and IVV from January 1, 2020, to December 31, 2023. We use a 60-day rolling window.

  1. Data Acquisition: Download daily adjusted close prices for SPY and IVV.
  2. Spread Calculation: Compute the daily spread $S_t = \text{SPY}_t - \text{IVV}_t$.
  3. Rolling Regression:
    • For each 60-day window, starting from day 61:
      • Take $S_t$ for days $t-59$ to $t$.
      • Calculate daily changes $\Delta S_k = S_k - S_{k-1}$ for $k \in [t-58, t]$.
      • Run linear regression: $\Delta S_k$ on $S_{k-1}$.
      • Extract the $\beta$ coefficient.
      • Calculate $\theta = -\beta$.
      • Compute half-life $T_{1/2} = \frac{\ln(2)}{\theta}$. If $\theta \le 0$, mean reversion is not present or very slow; assign an arbitrarily large half-life (e.g., 500 days) or mark as undefined.
  4. Plotting: Graph the rolling half-life values.

Consider these hypothetical results for the SPY-IVV spread:

  • Early 2020 (COVID-19 onset): Rolling half-life spikes from 20 days to 80 days. The market became highly volatile and trend-driven. Mean reversion slowed significantly. A trader would reduce or close mean reversion positions.
  • Mid-2021 (Stable Market): Rolling half-life stabilizes around 15-20 days. Mean reversion strategies become more viable. The spread reverts quickly.
  • Late 2022 (Bear Market): Rolling half-life gradually increases to 40 days. This indicates weakening mean reversion. The trader might tighten stop-losses or reduce position sizes.

These shifts provide actionable insights. A short half-life favors aggressive mean reversion. A long half-life indicates caution.

Half-Life Thresholds and Strategy Adjustment

Define thresholds for half-life values. These thresholds trigger specific trading actions. They formalize regime detection.

Typical thresholds:

  • Short Half-Life (e.g., < 20 days): Strong mean reversion. Increase position size. Widen profit targets. Increase trading frequency.
  • Medium Half-Life (e.g., 20-40 days): Moderate mean reversion. Maintain standard position size. Use standard profit targets.
  • Long Half-Life (e.g., > 40 days or $\theta \le 0$): Weak or no mean reversion. Reduce position size. Tighten stop-losses. Consider pausing trading the pair.

For the SPY-IVV spread, a trader might implement these rules:

  • If rolling half-life drops below 15 days, double the standard position size.
  • If rolling half-life exceeds 35 days, halve the standard position size.
  • If rolling half-life exceeds 60 days, close all open positions and halt new trades.

This systematic approach removes emotional bias. It adapts the strategy to changing market conditions. It protects capital during non-reverting periods. It maximizes profits during strong mean-reverting regimes.

The choice of window size and thresholds requires backtesting. Different asset classes and timeframes demand different parameters. A 60-day window works for daily equity data. Intraday strategies require much shorter windows (e.g., 100 bars). Commodity spreads might need longer windows (e.g., 120 days).

Implement the calculation in Python. Use pandas for data handling and statsmodels for regression.

python
import pandas as pd
import numpy as np
import statsmodels.api as sm

def calculate_half_life(series, window):
    half_lives = []
    dates = []
    for i in range(window, len(series)):
        window_data = series.iloc[i-window:i]
        
        # Ensure sufficient data points for regression
        if len(window_data) < 2:
            half_lives.append(np.nan)
            dates.append(series.index[i])
            continue

        delta_series = window_data.diff().dropna()
        lagged_series = window_data.shift(1).dropna()
        
        # Align indices after differencing and lagging
        common_index = delta_series.index.intersection(lagged_series.index)
        if len(common_index) < 2: # Need at least 2 points for regression
            half_lives.append(np.nan)
            dates.append(series.index[i])
            continue

        y = delta_series.loc[common_index]
        X = sm.add_constant(lagged_series.loc[common_index])

        try:
            model = sm.OLS(y, X)
            results = model.fit()
            beta = results.params[1] # Coefficient for the lagged series
            
            if beta < 0: # Ensure mean reversion
                theta = -beta
                half_life = np.log(2) / theta
                half_lives.append(half_life)
            else: # Not mean reverting, or trending
                half_lives.append(np.nan) # Or a large number like 500
        except Exception:
            half_lives.append(np.nan)
        
        dates.append(series.index[i])
            
    return pd.Series(half_lives, index=dates)

# Example usage (replace with actual data loading)
# Simulate SPY and IVV data
np.random.seed(42)
dates = pd.date_range(start='2020-01-01', periods=1000, freq='D')
spy_prices = 100 + np.cumsum(np.random.randn(1000) * 0.5) + np.sin(np.arange(1000)/50) * 10
ivv_prices = 99 + np.cumsum(np.random.randn(1000) * 0.5) + np.sin(np.arange(1000)/50) * 10.1

df = pd.DataFrame({'SPY': spy_prices, 'IVV': ivv_prices}, index=dates)
df = df.asfreq('B') # Convert to business days

spread = df['SPY'] - df['IVV']

# Calculate rolling half-life with a 60-day window
rolling_hl = calculate_half_life(spread, window=60)

# Print some results
print(rolling_hl.dropna().head())
print(rolling_hl.dropna().tail())

# Plotting the rolling half-life (requires matplotlib)
# import matplotlib.pyplot as plt
# plt.figure(figsize=(12, 6))
# rolling_hl.plot(title='Rolling Half-Life of SPY-IVV Spread (60-day window)')
# plt.axhline(y=20, color='g', linestyle='--', label='Short HL Threshold (20 days)')
# plt.axhline(y=40, color='orange', linestyle='--', label='Medium HL Threshold (40 days)')
# plt.axhline(y=60, color='r', linestyle='--', label='Long HL Threshold (60 days)')
# plt.ylabel('Half-Life (Days)')
# plt.xlabel('Date')
# plt.legend()
# plt.show()

This code snippet provides a practical implementation for calculating rolling half-life. It handles potential issues like insufficient data and non-mean-reverting scenarios. Incorporate this into a live trading system to dynamically adjust strategy parameters based on market conditions.