Home/Quant Research/Time Series Analysis
Back
📈

Quant Research Guide

Time Series Analysis

Financial data is time series. Almost every quant research task — return prediction, risk modeling, pairs trading, volatility forecasting — requires understanding stationarity, autocorrelation structure, and the right model class for each problem. Non-stationarity is the most common modeling failure in practice.

Time SeriesARIMAGARCHStationarityCointegrationVolatility

Core Theory

  • 1Stationarity (weak/covariance): a process is weakly stationary if E[Xₜ] = μ (constant), Var(Xₜ) = σ² (constant), and Cov(Xₜ, Xₜ₋ₖ) = γ(k) depends only on lag k, not t. Stock prices are non-stationary; log returns are approximately stationary. Always test before modeling.
  • 2ADF and KPSS tests: ADF null is unit root (non-stationary) — reject to support stationarity. KPSS null is stationarity — reject to support non-stationarity. Run both: ADF rejects + KPSS fails to reject = strong evidence for stationarity. Conflicting results suggest near-integration.
  • 3ACF and PACF identification: ACF plots Corr(Xₜ, Xₜ₋ₖ). PACF removes effects of intermediate lags. AR(p): PACF cuts off at lag p, ACF decays. MA(q): ACF cuts off at lag q, PACF decays. ARMA: both decay. Memorize this table — it appears in every time-series interview.
  • 4ARIMA(p,d,q): d = differences needed for stationarity; p = AR order from PACF cutoff; q = MA order from ACF cutoff. Use AIC/BIC for model selection among candidates. Check residuals are white noise with the Ljung-Box test — if not, model is misspecified.
  • 5GARCH(1,1) for volatility clustering: financial returns have time-varying variance. GARCH(1,1): σ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁. Stationarity requires α + β < 1. Long-run variance = ω/(1-α-β). Half-life of shock = -ln(2)/ln(α+β). For equities, α+β ≈ 0.97 → shocks persist ~23 days.
  • 6Cointegration: two I(1) series Xₜ and Yₜ are cointegrated if Yₜ - βXₜ is I(0). Engle-Granger test: (1) regress Y on X by OLS, (2) ADF test the residuals. Pairs trading exploits cointegration: the spread mean-reverts, generating entry and exit signals.
  • 7Spurious regression: regressing two independent random walks gives high R² and significant t-statistics by chance. Always test for cointegration before interpreting regressions between non-stationary series. This is Granger & Newbold (1974) — a famous result every quant must know.
  • 8Newey-West HAC standard errors: when OLS residuals are serially correlated, conventional SEs are too small. Newey-West SEs are consistent under both heteroskedasticity and autocorrelation. Always use them for time-series predictive regressions.

Key Formulas

01AR(1): Xt=ϕXt1+εtX_t = \phi X_{t-1} + \varepsilon_t, stationary iff ϕ<1|\phi| < 1
02GARCH(1,1): σt2=ω+αεt12+βσt12\sigma^2_t = \omega + \alpha \varepsilon^2_{t-1} + \beta \sigma^2_{t-1}
03Long-run GARCH variance: σLR2=ω/(1αβ)\sigma^2_{LR} = \omega / (1 - \alpha - \beta)
04GARCH half-life: t1/2=ln(2)/ln(α+β)t_{1/2} = -\ln(2) / \ln(\alpha + \beta)
05Pairs z-score: zt=(spreadtμspread)/σspreadz_t = (spread_t - \mu_{spread}) / \sigma_{spread}
06Ljung-Box: Q=n(n+2)k=1hρ^k2/(nk)χ2(h)Q = n(n+2) \sum_{k=1}^h \hat{\rho}_k^2 / (n-k) \sim \chi^2(h)
python
1import numpy as np
2import pandas as pd
3from scipy import stats
4from statsmodels.tsa.stattools import adfuller, coint
5from statsmodels.stats.diagnostic import acorr_ljungbox
6
7np.random.seed(42)
8T = 756 # 3 years of daily data
9
10# --- Simulate returns with GARCH(1,1) volatility clustering ---
11omega, alpha_g, beta_g = 0.00001, 0.08, 0.90 # persistence = 0.98
12returns = np.zeros(T)
13sigma2 = np.zeros(T)
14sigma2[0] = omega / (1 - alpha_g - beta_g) # unconditional variance
15
16for t in range(1, T):
17 sigma2[t] = omega + alpha_g * returns[t-1]**2 + beta_g * sigma2[t-1]
18 returns[t] = np.sqrt(sigma2[t]) * np.random.randn()
19
20ret = pd.Series(returns)
21
22# --- ADF test: are returns stationary? ---
23adf_stat, adf_p, *_ = adfuller(ret, autolag='AIC')
24print(f"ADF on returns: stat={adf_stat:.3f}, p={adf_p:.4f} -> {'Stationary ✓' if adf_p < 0.05 else 'Non-stationary'}")
25
26# --- Ljung-Box on r² detects ARCH effects ---
27lb = acorr_ljungbox(ret**2, lags=[10], return_df=True)
28print(f"Ljung-Box r^2: p={lb['lb_pvalue'].values[0]:.4f} -> {'ARCH effects ✓' if lb['lb_pvalue'].values[0] < 0.05 else 'No ARCH'}")
29
30# --- Rolling realized volatility (GARCH proxy) ---
31roll_vol = ret.rolling(21).std() * np.sqrt(252)
32print(f"Ann. rolling vol: mean={roll_vol.mean():.3f}, max={roll_vol.max():.3f}")
33
34# --- GARCH half-life ---
35half_life = -np.log(2) / np.log(alpha_g + beta_g)
36print(f"GARCH half-life: {half_life:.1f} trading days")
37
38# --- Cointegration + Pairs Trading ---
39X = np.cumsum(np.random.randn(T) * 0.01) # I(1) random walk
40Y = 2.0 * X + np.random.randn(T) * 0.02 # cointegrated: Y = 2X + noise
41
42t_stat, p_val, _ = coint(Y, X)
43print(f"\nCointegration p: {p_val:.4f} -> {'Cointegrated ✓' if p_val < 0.05 else 'Not cointegrated'}")
44
45# Dynamic OLS hedge ratio + z-score signal
46beta_hat = np.polyfit(X, Y, 1)[0]
47spread = Y - beta_hat * X
48z_score = (spread - spread.mean()) / spread.std()
49
50long_entry = (z_score < -2).sum()
51short_entry = (z_score > 2).sum()
52print(f"Long entries: {long_entry}, Short entries: {short_entry}")
53print(f"Mean reversion speed: spread AR(1) = {pd.Series(spread).autocorr(lag=1):.3f}")

Full time-series workflow: ADF stationarity testing, Ljung-Box detection of ARCH effects in squared returns, rolling realized volatility as a GARCH proxy, computing the GARCH shock half-life, Engle-Granger cointegration test, and building the pairs trading z-score signal with entry/exit logic.

Worked Interview Problems

3 problems

Problem

A researcher regresses SPY prices on VIX levels (daily closes, 5 years) and finds R² = 0.72, t-stat = 31.4. Is this meaningful?

Solution

1

Test both series for stationarity. SPY prices: random walk, ADF p ≈ 0.85 (non-stationary). VIX: highly persistent, ADF p ≈ 0.12 (borderline non-stationary).

2

Both series are I(1) or near-I(1). Regressing two non-stationary series is spurious regression — Granger & Newbold (1974) showed R² and t-stats are inflated by shared trend, not a real relationship.

3

Fix option 1: test for cointegration with Engle-Granger. If cointegrated, use an Error Correction Model (ECM).

4

Fix option 2: work with stationary variables — regress SPY daily log-returns on ΔVIX (daily change). Now R² ≈ 0.18, t ≈ -12.4 — still significant but honest.

5

Lesson: always ADF-test before running any time-series regression. High R² between levels means nothing without a cointegration test first.

Answer

No. Both series are I(1) so the regression is spurious. The R²=0.72 and t=31.4 are artifacts of shared trending. Use first differences or test for cointegration.

Common Mistakes

!

Regressing two I(1) series without testing cointegration first — produces spurious regression with misleadingly high R² and t-stats. Always run ADF on both variables before any time-series regression.

!

Confusing ADF and KPSS null hypotheses. ADF null = unit root (non-stationary). KPSS null = stationary. Getting this backward signals a fundamental gap to interviewers.

!

Selecting ARIMA order only from ACF/PACF without checking AIC. The plots give a starting point but model comparison with information criteria is mandatory. BIC penalizes complexity more and selects sparser models.

!

Fitting ARIMA to raw price levels. Prices are I(1). Set d=1 or (better) take log returns first, then fit ARMA(p,q) on the stationary series.

!

Forgetting to account for volatility clustering when computing signal significance. Time-varying variance makes OLS standard errors inconsistent. Always use White or Newey-West standard errors for predictive regressions on returns.

!

Using a static OLS hedge ratio for pairs trading. The hedge ratio β drifts over time. Use rolling OLS (60-90 day window) or a Kalman filter to track the dynamic relationship.

Only works in the Electron app

<webview> is an Electron-only tag. Run npm run electron:dev to use this.

25:00Focus
0

25 min focus · 5 min break · long break every 4 sessions

The Ultimate Grind