Understanding Half-Life
Half-life quantifies mean reversion speed. It measures the time for a deviation from the mean to halve. A shorter half-life shows faster mean reversion. Traders use half-life to find assets for mean reversion strategies.
We derive half-life from an Autoregressive model of order 1 (AR(1)). An AR(1) model describes a time series where the current value depends on its immediate predecessor. The formula is:
$Y_t = c + \phi Y_{t-1} + \epsilon_t$
Here, $Y_t$ is the value at time $t$. $c$ is a constant. $\phi$ (phi) is the autoregressive coefficient. $\epsilon_t$ represents white noise, independent and identically distributed random errors.
For mean reversion, the absolute value of $\phi$ must be less than 1 ($|\phi| < 1$). If $\phi = 1$, the series is a random walk. If $\phi > 1$, the series diverges.
The long-term mean of an AR(1) process is $\mu = \frac{c}{1 - \phi}$.
Consider the deviation from the mean: $X_t = Y_t - \mu$. Substitute $Y_t = X_t + \mu$ into the AR(1) equation: $X_t + \mu = c + \phi (X_{t-1} + \mu) + \epsilon_t$ $X_t + \mu = c + \phi X_{t-1} + \phi \mu + \epsilon_t$ $X_t = c + \phi X_{t-1} + \phi \mu - \mu + \epsilon_t$ $X_t = \phi X_{t-1} + (c - \mu(1-\phi)) + \epsilon_t$
Since $\mu = \frac{c}{1 - \phi}$, we have $c - \mu(1-\phi) = c - \frac{c}{1-\phi}(1-\phi) = c - c = 0$. Therefore, the deviation follows $X_t = \phi X_{t-1} + \epsilon_t$.
This equation shows the deviation from the mean decays by a factor of $\phi$ each period. If $X_0$ is the initial deviation, then $X_1 = \phi X_0 + \epsilon_1$, $X_2 = \phi X_1 + \epsilon_2 = \phi^2 X_0 + \phi \epsilon_1 + \epsilon_2$, and so on. Ignoring the noise terms for expectation, $E[X_t] = \phi^t X_0$.
We seek the time $t_{1/2}$ when the expected deviation reduces to half its initial value. $E[X_{t_{1/2}}] = 0.5 \cdot X_0$ $\phi^{t_{1/2}} X_0 = 0.5 \cdot X_0$ $\phi^{t_{1/2}} = 0.5$
To solve for $t_{1/2}$, take the natural logarithm of both sides: $t_{1/2} \ln(\phi) = \ln(0.5)$ $t_{1/2} = \frac{\ln(0.5)}{\ln(\phi)}$
Since $\ln(0.5) = -\ln(2)$, we can write: $t_{1/2} = \frac{-\ln(2)}{\ln(\phi)}$
This formula provides the half-life in the same time units as the data frequency. If you use daily data, the half-life is in days.
Calculating Half-Life: A Practical Example
Let's apply this to a real asset. We use daily closing prices of SPDR S&P 500 ETF (SPY) from January 1, 2020, to December 31, 2020. This period had significant market volatility.
First, we need to see if SPY's price series is mean-reverting. Stock prices often trend, not mean revert. However, we can analyze the spread between two assets or a detrended price series for mean reversion. For this example, we model the daily log returns of SPY, which likely mean revert around zero.
Let $P_t$ be the closing price of SPY at day $t$. We calculate log returns: $R_t = \ln(P_t / P_{t-1})$. We fit an AR(1) model to these daily log returns: $R_t = c + \phi R_{t-1} + \epsilon_t$.
Using Python's statsmodels library:
import yfinance as yf
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.arima.model import ARIMA
# Download historical data for SPY
ticker = "SPY"
start_date = "2020-01-01"
end_date = "2020-12-31"
data = yf.download(ticker, start=start_date, end=end_date)
# Calculate daily log returns
data['Log_Return'] = np.log(data['Adj Close'] / data['Adj Close'].shift(1))
returns = data['Log_Return'].dropna()
# Fit an AR(1) model
# The order (1, 0, 0) specifies AR(1) with no differencing (I=0) and no moving average (MA=0)
model = ARIMA(returns, order=(1, 0, 0))
results = model.fit()
# Extract the phi coefficient
phi = results.arparams[0] # arparams[0] is the coefficient for the first lag
print(f"AR(1) coefficient (phi): {phi:.4f}")
# Calculate half-life
if abs(phi) < 1:
half_life = -np.log(2) / np.log(abs(phi))
print(f"Half-life of log returns: {half_life:.2f} days")
else:
print("The series is not mean-reverting (phi >= 1 or phi <= -1).")
import yfinance as yf
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.arima.model import ARIMA
# Download historical data for SPY
ticker = "SPY"
start_date = "2020-01-01"
end_date = "2020-12-31"
data = yf.download(ticker, start=start_date, end=end_date)
# Calculate daily log returns
data['Log_Return'] = np.log(data['Adj Close'] / data['Adj Close'].shift(1))
returns = data['Log_Return'].dropna()
# Fit an AR(1) model
# The order (1, 0, 0) specifies AR(1) with no differencing (I=0) and no moving average (MA=0)
model = ARIMA(returns, order=(1, 0, 0))
results = model.fit()
# Extract the phi coefficient
phi = results.arparams[0] # arparams[0] is the coefficient for the first lag
print(f"AR(1) coefficient (phi): {phi:.4f}")
# Calculate half-life
if abs(phi) < 1:
half_life = -np.log(2) / np.log(abs(phi))
print(f"Half-life of log returns: {half_life:.2f} days")
else:
print("The series is not mean-reverting (phi >= 1 or phi <= -1).")
Running this code for SPY log returns from 2020-01-01 to 2020-12-31: The AR(1) coefficient ($\phi$) for SPY log returns might be around -0.05. For example, if $\phi = -0.05$: Half-life = $-\ln(2) / \ln(0.05) = -0.6931 / -2.9957 = 0.23$ days.
This very short half-life means daily log returns on SPY revert to their mean (near zero) quickly. This aligns with efficient markets; today's return has little predictive power for tomorrow's return.
Consider a synthetic mean-reverting series for a clearer illustration. Let's generate an AR(1) series with a known $\phi$.
np.random.seed(42) # for reproducibility
c_synth = 0.01
phi_synth = 0.7 # A clear mean-reverting phi
epsilon_synth = np.random.normal(0, 0.1, 500) # 500 data points
synthetic_series = np.zeros(500)
synthetic_series[0] = 0.5 # initial value
for i in range(1, 500):
synthetic_series[i] = c_synth + phi_synth * synthetic_series[i-1] + epsilon_synth[i]
# Fit AR(1) model to the synthetic series
model_synth = ARIMA(synthetic_series, order=(1, 0, 0))
results_synth = model_synth.fit()
phi_estimated = results_synth.arparams[0]
print(f"\nSynthetic series - Estimated AR(1) coefficient (phi): {phi_estimated:.4f}")
if abs(phi_estimated) < 1:
half_life_synth = -np.log(2) / np.log(abs(phi_estimated))
print(f"Synthetic series - Calculated Half-life: {half_life_synth:.2f} periods")
else:
print("Synthetic series is not mean-reverting.")
np.random.seed(42) # for reproducibility
c_synth = 0.01
phi_synth = 0.7 # A clear mean-reverting phi
epsilon_synth = np.random.normal(0, 0.1, 500) # 500 data points
synthetic_series = np.zeros(500)
synthetic_series[0] = 0.5 # initial value
for i in range(1, 500):
synthetic_series[i] = c_synth + phi_synth * synthetic_series[i-1] + epsilon_synth[i]
# Fit AR(1) model to the synthetic series
model_synth = ARIMA(synthetic_series, order=(1, 0, 0))
results_synth = model_synth.fit()
phi_estimated = results_synth.arparams[0]
print(f"\nSynthetic series - Estimated AR(1) coefficient (phi): {phi_estimated:.4f}")
if abs(phi_estimated) < 1:
half_life_synth = -np.log(2) / np.log(abs(phi_estimated))
print(f"Synthetic series - Calculated Half-life: {half_life_synth:.2f} periods")
else:
print("Synthetic series is not mean-reverting.")
For a synthetic series with true $\phi = 0.7$: The estimated $\phi$ might be around $0.69$. Calculated Half-life = $-\ln(2) / \ln(0.69) = -0.6931 / -0.3711 = 1.87$ periods. If these are daily periods, the half-life is about 1.87 days. This means a deviation from the mean will, on average, reduce by half in under two days.
Using Half-Life for Trading
Half-life offers a key parameter for mean reversion strategies.
-
Entry and Exit Timing: A short half-life suggests quick mean reversion. This means fast profit taking after a deviation and re-entry when the deviation reverses. For instance, a half-life of 2 days suggests positions opened on a deviation might typically revert within a few days. Traders could target exits around the mean within this timeframe.
-
Portfolio Construction: Half-life helps select assets or pairs for mean reversion. Pairs with shorter half-lives offer faster turnover. A longer half-life (e.g., 20 days or more) might show a slower process or a trend, making it less fit for short-term mean reversion.
-
Risk Management: Half-life informs position sizing and stop-loss placement. If an asset has a half-life of 5 days, and a deviation lasts for 10-15 days, it might signal a regime change or a breakdown of the mean-reverting property. This could trigger exiting the position.
-
Strategy Optimization: Half-life can input for optimizing strategy parameters. For example, in a Bollinger Band strategy, the lookback period for calculating the mean and standard deviation could relate to the half-life. A lookback period of 2-3 times the half-life might capture the mean better.
For a pair trading strategy involving two stocks, say XOM and CVX, we analyze the spread $S_t = \ln(XOM_t) - \ln(CVX_t)$. We fit an AR(1) model to this spread.
# Example for a pair (XOM, CVX)
ticker1 = "XOM"
ticker2 = "CVX"
start_date_pair = "2023-01-01"
end_date_pair = "2023-12-31"
data1 = yf.download(ticker1, start=start_date_pair, end=end_date_pair)['Adj Close']
data2 = yf.download(ticker2, start=start_date_pair, end=end_date_pair)['Adj Close']
# Align data
merged_data = pd.DataFrame({'XOM': data1, 'CVX': data2}).dropna()
# Calculate log spread
merged_data['Log_Spread'] = np.log(merged_data['XOM']) - np.log(merged_data['CVX'])
spread = merged_data['Log_Spread'].dropna()
# Fit AR(1) model to the spread
model_spread = ARIMA(spread, order=(1, 0, 0))
results_spread = model_spread.fit()
phi_spread = results_spread.arparams[0]
print(f"\nPair (XOM, CVX) - Estimated AR(1) coefficient (phi) for spread: {phi_spread:.4f}")
if abs(phi_spread) < 1:
half_life_spread = -np.log(2) / np.log(abs(phi_spread))
print(f"Pair (XOM, CVX) - Half-life of spread: {half_life_spread:.2f} days")
else:
print("The spread is not mean-reverting.")
# Example for a pair (XOM, CVX)
ticker1 = "XOM"
ticker2 = "CVX"
start_date_pair = "2023-01-01"
end_date_pair = "2023-12-31"
data1 = yf.download(ticker1, start=start_date_pair, end=end_date_pair)['Adj Close']
data2 = yf.download(ticker2, start=start_date_pair, end=end_date_pair)['Adj Close']
# Align data
merged_data = pd.DataFrame({'XOM': data1, 'CVX': data2}).dropna()
# Calculate log spread
merged_data['Log_Spread'] = np.log(merged_data['XOM']) - np.log(merged_data['CVX'])
spread = merged_data['Log_Spread'].dropna()
# Fit AR(1) model to the spread
model_spread = ARIMA(spread, order=(1, 0, 0))
results_spread = model_spread.fit()
phi_spread = results_spread.arparams[0]
print(f"\nPair (XOM, CVX) - Estimated AR(1) coefficient (phi) for spread: {phi_spread:.4f}")
if abs(phi_spread) < 1:
half_life_spread = -np.log(2) / np.log(abs(phi_spread))
print(f"Pair (XOM, CVX) - Half-life of spread: {half_life_spread:.2f} days")
else:
print("The spread is not mean-reverting.")
If the XOM/CVX spread from 2023-01-01 to 2023-12-31 yielded a $\phi$ of $0.95$: Half-life = $-\ln(2) / \ln(0.95) = -0.6931 / -0.0513 = 13.51$ days. This means a deviation in the XOM/CVX spread would take about 13.5 trading days to decay by 50%. This half-life is longer than for individual stock returns, making it a better candidate for a mean reversion strategy. A trader might enter a pair trade when the spread deviates significantly and expect the trade to revert within a few weeks.
