ARIMA & GARCH
The previous chapter told you which world a series lives in. This one gives you the classical models that put numbers on it. ARIMA is the workhorse for the conditional mean of a series — where the next value is expected to land — and GARCH is the workhorse for the conditional variance — how big the next move is likely to be. The punchline you should carry into every design: for tradeable assets, mean models of returns are usually weak, while variance models are reliably useful. Understanding why is the whole lesson.
AR, MA, ARMA, ARIMA: the mean model#
An ARIMA model describes today's value as a linear combination of its own past values and past forecast errors. It has three integer orders, written : autoregressive terms, differences to reach stationarity, and moving-average terms. Build it up one piece at a time.
AR(p) — autoregression#
An autoregressive model regresses the series on its own recent history. The workhorse special case is AR(1): today is a constant plus a fraction of yesterday plus noise.
The single coefficient is the entire story. When the process is stationary and mean-reverting: it is pulled back toward its long-run mean , and a shock decays geometrically at rate per step. When the constant vanishes into a unit root and you are back to a pure random walk — no mean to revert to, which is exactly the case the ADF test hunts for. When the process explodes. The general order- form simply stacks more lags:
MA(q) — moving average of shocks#
A moving-average model makes today a blend of past forecast errors rather than past levels — it gives the series a short, finite memory of shocks that dies out completely after steps.
ARMA(p,q) is just the two glued together — AR terms for persistent level-memory, MA terms for transient shock-memory — and ARIMA(p,d,q) wraps an ARMA around the -th difference of the raw series. For daily prices you almost always take (one difference = returns), so an ARIMA on prices is an ARMA on returns.
Reading the order off ACF and PACF#
You do not guess and — you read them from the autocorrelation and partial-autocorrelation functions you met in the time-series chapter. The classic Box-Jenkins division of labour:
- PACF cuts off after lag p, ACF decays gradually → an AR(p) process. The PACF isolates the direct effect of each lag once the shorter lags are partialled out, so an AR(p) shows exactly significant partial spikes then silence.
- ACF cuts off after lag q, PACF decays gradually → an MA(q) process — the mirror image.
- Both decay gradually → a mixed ARMA; pick the smallest orders that whiten the residuals.
edgekit gives you both functions directly (the PACF via the Durbin-Levinson recursion, no statsmodels), plus the white-noise band to judge which spikes are real.
import edgekit as ek
rets = ek.timeseries.log_returns(bars.close) # stationarise: prices -> returns
ac = ek.timeseries.acf(rets, nlags=40) # ac[0] == 1 by construction
pc = ek.timeseries.pacf(rets, nlags=40) # partial autocorrelations
band = 1.96 / len(rets) ** 0.5 # +/- significance band
# how many PACF spikes poke outside the band -> candidate AR order p
p_hat = sum(1 for v in pc[1:11] if abs(v) > band)
print("suggested AR order:", p_hat)
The twist: variance is predictable even when direction is not#
Here is the fact that redirects the entire modelling effort. Run the ACF on returns and it is flat. Run the same ACF on squared returns (a proxy for variance) and you find strong, slowly-decaying positive autocorrelation. Direction is unpredictable; magnitude is not. Big moves cluster with big moves, calm with calm — the volatility clusteringyou saw last chapter. Returns are, in the famous phrase, “serially uncorrelated but not independent.”
# direction: essentially no memory
lb_ret = ek.timeseries.ljung_box(rets, lags=10) # small Q -> can't reject white noise
# magnitude: strong, persistent memory
lb_sq = ek.timeseries.ljung_box(rets ** 2, lags=10) # large Q -> squared returns ARE autocorrelatedThis asymmetry is why the profitable modelling target for most assets is variance, not mean. You may not know which way the next move goes, but you can forecast how big it will be — and that forecast is what sizes a position, sets a stop, and decides whether to be in the market at all.
GARCH(1,1): the variance model#
The ARCH/GARCH family models the conditional variance as its own recursion. The GARCH(1,1) — the version that wins in practice almost regardless of asset — says tomorrow's variance is a weighted blend of three things: a constant long-run level, yesterday's squared shock, and yesterday's variance.
Read each term. is the baseline variance the process relaxes toward. is the reaction: how sharply variance jumps in response to a fresh shock . is the memory: how much of yesterday's variance carries into today. Their sum is the single most important diagnostic:
Stationarity requires ; the closer that sum sits to 1, the more slowly a volatility shock decays and the longer high-vol regimes persist. Typical equity and crypto fits land around and , i.e. persistence near — shocks fade over weeks, not days. The implied long-run (unconditional) variance is:
edgekit fits the model by maximum likelihood using a derivative-free pattern search in pure numpy (no scipy), enforcing . It returns the parameters, the fitted conditional-volatility path, a one-step forecast, and the persistence:
g = ek.timeseries.garch11(rets, max_iter=200)
# g == {"omega", "alpha", "beta",
# "cond_vol": ndarray of sigma_t, # the fitted per-bar volatility path
# "forecast": next-step sigma, # sqrt(omega + alpha*r[-1]**2 + beta*sigma[-1]**2)
# "persistence": alpha + beta}
print(f"alpha={g['alpha']:.3f} beta={g['beta']:.3f} persistence={g['persistence']:.3f}")
print("next-bar vol forecast:", g["forecast"])
so daily — an annualised jump from to (). The model just told you, quantitatively, that tomorrow's expected range is nearly double last week's. If your stop was a 2× multiple of forecast vol, it should widen accordingly; if you size to constant risk, your position should roughly halve. That is the forecast doing its one job.
Forecasting volatility forward#
The one-step forecast is mechanical — plug the latest shock and variance into the recursion. Multi-step forecasts mean-revert toward the long-run level at rate per step:
With persistence near , the half-life of a vol shock is roughly bars — so a spike today still meaningfully elevates your risk estimate a couple of weeks out. That horizon is the practical output: it tells you how long to keep stops wide and size trimmed after a shock.
What you actually do with this#
You will not usually trade an ARIMA point-forecast of returns — the ACF already warned you it is noise. What GARCH buys you is volatility-aware everything, and edgekit's strategy templates are built to consume it:
- Volatility targeting. Feed the conditional-vol forecast into
ek.sizing.vol_targetso position size shrinks when GARCH says a storm is coming and grows in calm — keeping risk, not notional, constant. - Adaptive stops. A stop set as a multiple of forecast vol widens automatically in stress, so you are not shaken out by a normal move that merely looks large against a stale fixed distance.
- Regime awareness. A persistence near 1 with an elevated current is a quantitative “high-vol regime” flag — which is the exact idea the next-but-one chapter formalises with a hidden Markov model.
# GARCH-forecast vol -> size to a constant risk target, in a BaseStrategy subclass
g = ek.timeseries.garch11(rets, max_iter=200)
forecast_vol = g["forecast"] # next-bar sigma
# annualise for the sizing call (per-bar -> per-year), then target e.g. 15% vol
ann_vol = forecast_vol * (252 ** 0.5)
# vol_target scales an existing return/position stream toward the target vol pathNext: Cointegration & pairs — when two non-stationary prices move together closely enough that their spread is stationary, and how to estimate the hedge ratio, test the residual, and model the spread as a mean-reverting process you can actually trade.

