← Back to articles
July 10, 2026
5 min read

GARCH(1,1): Forecasting Crypto Volatility

#volatility
#GARCH
#risk
#forecasting
#crypto
#algorithmic-trading

Open a chart of BTC daily returns and you will notice something the textbooks about random walks never prepared you for: the calm and the chaos come in clusters. A 6% down day is rarely alone. It sits inside a week of 4-8% swings, then the market exhales and drifts through a month of sleepy 1% sessions before the next storm. Returns themselves look close to unpredictable — you cannot reliably say whether tomorrow is up or down — but their magnitude is deeply predictable. Today's turbulence tells you a lot about tomorrow's.

Almost every risk tool a trader reaches for quietly assumes this is not true. Black-Scholes prices an option with a single constant σ\sigma. A static Value-at-Risk number multiplies one volatility estimate by a normal quantile. A fixed 3% stop-loss treats a dead sideways Tuesday and the hours around an FOMC print or a major exchange de-peg as if they carry the same risk. Each of these breaks in exactly the same way: it collapses a time-varying quantity into a constant, and then gets surprised when the constant turns out to move.

This article is Part 1 of a four-part series on volatility modeling for crypto. It builds the foundation: the GARCH(1,1) model, why it fits crypto returns so well, how to estimate it honestly by maximum likelihood with the arch library, and how to turn a conditional-variance forecast into two immediately useful things — a position size and a stop width that both breathe with the market. Part 2 adds asymmetry and heavy tails, Part 3 goes multivariate, and Part 4 assembles the full volatility-targeting backtest. We keep the application here deliberately simple; the honest, walk-forward-validated strategy is the subject of Part 4.

The Stylized Facts of Crypto Returns

Before modeling anything, it pays to be precise about what we are trying to reproduce. Empirical financial returns — equities, FX, and crypto especially — share a small set of robust statistical regularities that have been documented for decades. They are usually called stylized facts, and three of them drive everything that follows.

1. Volatility clustering. Large moves tend to be followed by large moves (of either sign), and small moves by small moves. Mandelbrot noticed this in cotton prices in 1963. Formally, while the returns rtr_t are close to serially uncorrelated, the squared returns rt2r_t^2 (a proxy for realized variance) show strong, slowly-decaying positive autocorrelation.

2. Fat tails (leptokurtosis). The unconditional distribution of returns has far more mass in the extremes than a Gaussian. Where a normal distribution has kurtosis 3, BTC daily log-returns routinely sit above 8-10, and higher-frequency crypto returns can be worse. Six-sigma days, which a normal model says should happen roughly once per million years, show up several times a decade.

3. No linear autocorrelation in returns, strong autocorrelation in squared returns. This is the fingerprint that separates a genuine volatility process from a trivial trend. If you regress rtr_t on its own lags you get nothing exploitable. If you regress rt2r_t^2 on its lags, you find a clear, persistent signal. This is precisely the structure a variance model should capture — and precisely what a constant-σ\sigma model throws away.

We can eyeball all three in a few lines. Nothing here requires a special data source; use ccxt in production, but for a reproducible snippet yfinance is fine.

import numpy as np
import pandas as pd
import yfinance as yf
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.stats.diagnostic import acorr_ljungbox
from scipy import stats

px = yf.download("BTC-USD", start="2021-01-01", end="2025-12-31")["Close"]
ret = np.log(px / px.shift(1)).dropna()          # log returns
ret = ret.rename("btc")

print(f"Observations:     {len(ret)}")
print(f"Ann. volatility:  {ret.std() * np.sqrt(365):.2%}")
print(f"Skewness:         {stats.skew(ret):.2f}")
print(f"Excess kurtosis:  {stats.kurtosis(ret):.2f}")   # 0 == Gaussian

lb_ret  = acorr_ljungbox(ret,        lags=[10], return_df=True)
lb_ret2 = acorr_ljungbox(ret ** 2,   lags=[10], return_df=True)
print("\nLjung-Box p-value, returns   (lag 10):", float(lb_ret["lb_pvalue"].iloc[0]))
print("Ljung-Box p-value, squared   (lag 10):", float(lb_ret2["lb_pvalue"].iloc[0]))

The typical reading (illustrative — your window will differ): excess kurtosis well above 3, a Ljung-Box p-value on raw returns that fails to reject "no autocorrelation," and a p-value on squared returns that is effectively zero. That last contrast is the whole game. There is nothing to trade in the sign of returns at the daily horizon, but there is a great deal of structure in their variance, and that structure is forecastable.

A note on the 24/7 nature of crypto. Unlike equities, there is no overnight gap and no weekend close, so the "day" is a clean 24-hour bar and the annualization factor is 365\sqrt{365}, not 252\sqrt{252}. Volatility clustering also survives at intraday scales, which matters if you run GARCH on hourly bars — funding-rate flips and liquidation cascades inject sharp, clustered variance bursts that a daily model smooths over.

From ARCH to GARCH

The problem is now sharply posed: model a variance that is not constant but depends on the recent past. The first model to do this properly was Engle's ARCH (Autoregressive Conditional Heteroskedasticity, 1982), which won him the Nobel prize in 2003.

Write the return as a conditional mean plus a shock:

rt=μt+εt,εt=σtzt,zti.i.d. (0,1)r_t = \mu_t + \varepsilon_t, \qquad \varepsilon_t = \sigma_t z_t, \qquad z_t \sim \text{i.i.d. } (0, 1)

Here σt2\sigma_t^2 is the conditional variance — the variance of rtr_t given everything known up to time t1t-1 — and ztz_t is a standardized innovation (standard normal in the simplest case). The word "conditional" is doing all the work: unconditionally the variance may be constant, but conditioned on yesterday it moves.

Engle's ARCH(qq) makes today's variance a weighted sum of the last qq squared shocks:

σt2=ω+i=1qαiεti2\sigma_t^2 = \omega + \sum_{i=1}^{q} \alpha_i \, \varepsilon_{t-i}^2

with ω>0\omega > 0 and αi0\alpha_i \ge 0 to keep the variance positive. This captures clustering directly: a big shock εt12\varepsilon_{t-1}^2 pushes up σt2\sigma_t^2, which raises the chance of another big shock, which keeps the variance elevated. The trouble is empirical decay. Volatility persistence in real markets stretches over many lags, so to fit it an ARCH model needs a large qq — often 8, 10, or more — and that means estimating a long, unstable vector of αi\alpha_i that tends to overfit.

Bollerslev's insight in 1986 was to add a term that soaks up all that persistence with a single parameter. The GARCH(1,1) — Generalized ARCH — recursion is:

σt2=ω+αεt12+βσt12\sigma_t^2 = \omega + \alpha \, \varepsilon_{t-1}^2 + \beta \, \sigma_{t-1}^2

Three parameters, three clean interpretations:

  • ω>0\omega > 0 — the baseline or floor. A constant that anchors the long-run level of variance. Variance never decays below what ω\omega supports.
  • α0\alpha \ge 0 — the reaction to news. How violently the variance responds to yesterday's surprise εt12\varepsilon_{t-1}^2. A large α\alpha means the conditional variance is jumpy and shock-sensitive.
  • β0\beta \ge 0 — the persistence or memory. How much of yesterday's variance carries into today. A large β\beta means volatility is smooth and slow to fade — calm stays calm, storms stay stormy.

The elegance is in the recursion. Because σt12\sigma_{t-1}^2 itself contained a βσt22\beta \sigma_{t-2}^2 term, expanding backwards shows that GARCH(1,1) is an ARCH(\infty) with geometrically decaying weights αβk\alpha \beta^{k} on past squared shocks:

σt2=ω1β+αk=0βkεt1k2\sigma_t^2 = \frac{\omega}{1-\beta} + \alpha \sum_{k=0}^{\infty} \beta^{k}\, \varepsilon_{t-1-k}^2

So the single β\beta buys you an infinite, exponentially-weighted memory of past shocks. This is why a mere GARCH(1,1) — three parameters — routinely beats ARCH models with ten, and why it became the workhorse of applied volatility modeling. It is, in fact, a close cousin of the RiskMetrics EWMA variance estimator, which is the special case ω=0\omega = 0, α+β=1\alpha + \beta = 1 with β\beta fixed at 0.94. GARCH generalizes it by letting the data choose α\alpha, β\beta, and a genuine mean-reversion level.

Properties: Stationarity, Long-Run Variance, and Half-Life

The GARCH(1,1) recursion has a few properties worth deriving, because they are what let you reason about the model rather than just fit it blindly.

Unconditional (long-run) variance. Assume the process is covariance-stationary so that the unconditional variance σˉ2=E[εt2]=E[σt2]\bar{\sigma}^2 = \mathbb{E}[\varepsilon_t^2] = \mathbb{E}[\sigma_t^2] exists and is constant over time. Take expectations of both sides of the recursion. Since E[εt12]=E[σt12]=σˉ2\mathbb{E}[\varepsilon_{t-1}^2] = \mathbb{E}[\sigma_{t-1}^2] = \bar{\sigma}^2:

σˉ2=ω+ασˉ2+βσˉ2        σˉ2=ω1αβ\bar{\sigma}^2 = \omega + \alpha\, \bar{\sigma}^2 + \beta\, \bar{\sigma}^2 \;\;\Longrightarrow\;\; \bar{\sigma}^2 = \frac{\omega}{1 - \alpha - \beta}

This is the level volatility mean-reverts to. It only exists — and is only positive — when α+β<1\alpha + \beta < 1.

Stationarity condition. That same inequality,

α+β<1,\alpha + \beta < 1,

is the covariance-stationarity condition for GARCH(1,1). The quantity α+β\alpha + \beta is the persistence of the variance process: it is the AR(1) coefficient governing how a variance shock decays back toward σˉ2\bar{\sigma}^2. If α+β1\alpha + \beta \ge 1, the unconditional variance is infinite (or undefined) and shocks never fully die out.

We can see the mean-reversion explicitly. Define the variance deviation σt2σˉ2\sigma_t^2 - \bar{\sigma}^2. A little algebra on the recursion (substituting ω=σˉ2(1αβ)\omega = \bar{\sigma}^2(1 - \alpha - \beta)) gives, in expectation:

Et[σt+k2]σˉ2=(α+β)k(σt+12σˉ2)\mathbb{E}_t[\sigma_{t+k}^2] - \bar{\sigma}^2 = (\alpha + \beta)^{k}\,\bigl(\sigma_{t+1}^2 - \bar{\sigma}^2\bigr)

The gap between the current variance and its long-run level shrinks by a factor of (α+β)(\alpha + \beta) each step. This is exactly the multi-step forecast we use later.

Volatility half-life. How long does it take for a variance shock to decay halfway back to normal? Set (α+β)h=1/2(\alpha+\beta)^{h} = 1/2 and solve:

h1/2=ln0.5ln(α+β)h_{1/2} = \frac{\ln 0.5}{\ln(\alpha + \beta)}

For α+β=0.95\alpha + \beta = 0.95 the half-life is about 13.5 days; for 0.980.98 it is about 34 days; for 0.990.99 it is about 69 days. This single number is often more intuitive than the raw parameters — it tells you, in the units of your bars, how sticky volatility is.

The near-IGARCH problem in crypto. Here is the crypto-specific wrinkle. When you fit GARCH(1,1) to BTC or ETH returns, you almost always find α+β\alpha + \beta very close to 1 — values of 0.98, 0.99, sometimes 0.995 are routine. This is the near-IGARCH (Integrated GARCH) regime. It has real consequences:

  • The half-life becomes enormous (weeks to months), so the model treats volatility as very persistent and barely mean-reverting.
  • The estimate of σˉ2=ω/(1αβ)\bar{\sigma}^2 = \omega/(1-\alpha-\beta) becomes extremely sensitive: a small change in α+β\alpha+\beta from 0.99 to 0.995 doubles the implied long-run variance. Never trust the point estimate of long-run vol in this regime without a confidence interval.
  • Multi-step forecasts mean-revert so slowly that, for practical horizons under a few weeks, GARCH behaves almost like a random-walk-in-variance (which is what EWMA assumes).

Whether near-integration is genuine or an artifact of structural breaks (a permanent shift in the volatility level being read by the model as one long persistent episode) is a real debate. It is one more reason to refit on rolling windows rather than fit once on all history, a point we return to in the pitfalls. Regime structure specifically is better handled by an explicit switching model — see regime detection with hidden Markov models, which is complementary to GARCH rather than a replacement.

Estimation by Maximum Likelihood

GARCH parameters are estimated by maximum likelihood. The logic is direct: given θ=(μ,ω,α,β)\theta = (\mu, \omega, \alpha, \beta), the recursion produces a full path of conditional variances σt2(θ)\sigma_t^2(\theta), and under an assumed distribution for the innovations ztz_t we can write down how likely the observed returns are. We then pick θ\theta to maximize that likelihood.

Assume Gaussian innovations ztN(0,1)z_t \sim \mathcal{N}(0,1), so rtFt1N(μ,σt2)r_t \mid \mathcal{F}_{t-1} \sim \mathcal{N}(\mu, \sigma_t^2). The conditional density of one observation is

f(rtFt1)=12πσt2exp ⁣((rtμ)22σt2).f(r_t \mid \mathcal{F}_{t-1}) = \frac{1}{\sqrt{2\pi\sigma_t^2}}\exp\!\left(-\frac{(r_t - \mu)^2}{2\sigma_t^2}\right).

Because the model is written conditionally, the joint likelihood factors into a product of one-step-ahead densities, and the log-likelihood is a plain sum:

(θ)=12t=1T[ln(2π)+lnσt2(θ)+(rtμ)2σt2(θ)].\ell(\theta) = -\frac{1}{2}\sum_{t=1}^{T}\left[\ln(2\pi) + \ln \sigma_t^2(\theta) + \frac{(r_t - \mu)^2}{\sigma_t^2(\theta)}\right].

Two structural facts to notice. First, σt2\sigma_t^2 appears both as a penalty (lnσt2\ln \sigma_t^2 — the model is punished for claiming high variance) and in the standardized residual ((rtμ)2/σt2(r_t-\mu)^2/\sigma_t^2 — the model is punished for being surprised). The optimum balances the two, which is what makes the variance track. Second, the recursion needs a seed σ12\sigma_1^2; the usual choice is the sample variance of the returns, and with a few thousand observations the seed barely matters.

There is no closed form for the maximizer, so we optimize numerically (arch uses a quasi-Newton method with analytic or numerical gradients). The likelihood surface is generally well-behaved for GARCH(1,1), but two things bite in practice: the positivity constraints (ω,α,β0\omega,\alpha,\beta \ge 0) and the near-boundary behavior when α+β1\alpha+\beta \to 1, where the optimizer can crawl slowly. Both are handled for you by a good library — and you should use one. Hand-rolling a GARCH MLE is a fine learning exercise but a poor production choice.

The arch library

The arch package by Kevin Sheppard is the standard tool in Python. The whole fit is four lines.

from arch import arch_model

r = ret * 100.0

model  = arch_model(r, mean="Constant", vol="Garch", p=1, q=1, dist="normal")
res    = model.fit(disp="off")
print(res.summary())

A word on the argument names, because they are a common source of confusion. In arch, p is the number of lagged variances (β\beta terms, the GARCH order) and q is the number of lagged squared residuals (α\alpha terms, the ARCH order). So p=1, q=1 is the GARCH(1,1) we derived. (Bollerslev's original notation writes it GARCH(p,qp,q) with pp for the ARCH order — the two conventions are transposed. Trust the library's own docs, not your memory.)

Reading the summary, the coefficient table looks roughly like this (illustrative values for BTC daily returns, not a real experiment):

                     Volatility Model
==========================================================
                 coef    std err      t      P>|t|
----------------------------------------------------------
omega          0.4821     0.201    2.40    0.016
alpha[1]       0.0912     0.021    4.34    0.000
beta[1]        0.8994     0.024   37.5     0.000
==========================================================

How to read it:

  • alpha[1] + beta[1] = 0.0912 + 0.8994 = 0.9906. Persistence just under 1 — the near-IGARCH regime, exactly as warned. Half-life ln(0.5)/ln(0.9906)73\approx \ln(0.5)/\ln(0.9906) \approx 73 days.
  • omega = 0.4821, so the long-run variance is 0.4821/(10.9906)=51.30.4821 / (1 - 0.9906) = 51.3 in percent-squared units, i.e. a long-run daily volatility of 51.37.2%\sqrt{51.3} \approx 7.2\%, or roughly 7.2%×365138%7.2\%\times\sqrt{365}\approx 138\% annualized. That is a plausible BTC number.
  • Both alpha and beta are strongly significant. alpha being small relative to beta is typical: crypto variance is mostly persistence (memory), with a modest but real reaction to fresh shocks.

The ×100 scaling gotcha

This is the single most common way to get nonsense out of arch, so it earns its own subsection. The optimizer works best when the numbers it sees are O(1)O(1) to O(100)O(100). Daily log-returns are O(0.01)O(0.01), so their squares are O(0.0001)O(0.0001) and ω\omega has to be around 10610^{-6} — down in a range where numerical gradients lose precision and the fit can silently fail to converge or return garbage standard errors.

The fix is to fit on returns scaled by 100 (i.e. in percent), as above. arch will even emit a DataScaleWarning if you forget. Everything you read out of the model is then in percent or percent-squared units, and you must unscale consistently:

sigma_pct     = np.sqrt(res.forecast(horizon=1).variance.iloc[-1, 0])
sigma_decimal = sigma_pct / 100.0
print(f"1-day-ahead conditional vol: {sigma_decimal:.2%}")

Mixing scaled and unscaled quantities — feeding a percent volatility into a position-sizing formula that expects decimals, say — produces errors of exactly 100x, which are easy to miss because the code runs fine. Pick a convention (I keep everything decimal outside the fit and only scale at the arch boundary) and never cross it.

Forecasting Conditional Variance

A fitted model is only useful if it forecasts. GARCH gives clean, analytic forecasts at any horizon.

One step ahead. At time TT (the end of the sample) we know εT\varepsilon_T and σT2\sigma_T^2, so the next variance is deterministic:

σT+12=ω+αεT2+βσT2.\sigma_{T+1}^2 = \omega + \alpha\,\varepsilon_T^2 + \beta\,\sigma_T^2.

No expectation needed — everything on the right is observed.

Multi-step ahead. For h2h \ge 2 we do not yet know the intervening shocks, so we take conditional expectations. Using ET[εT+k2]=ET[σT+k2]\mathbb{E}_T[\varepsilon_{T+k}^2] = \mathbb{E}_T[\sigma_{T+k}^2] (because E[z2]=1\mathbb{E}[z^2]=1), the recursion collapses to a simple AR(1) in the forecasted variance:

ET[σT+h2]=ω+(α+β)ET[σT+h12].\mathbb{E}_T[\sigma_{T+h}^2] = \omega + (\alpha + \beta)\,\mathbb{E}_T[\sigma_{T+h-1}^2].

Iterating this from the one-step forecast gives the closed form, which is the mean-reversion result we derived earlier written out explicitly:

ET[σT+h2]=σˉ2+(α+β)h1(σT+12σˉ2),σˉ2=ω1αβ.\mathbb{E}_T[\sigma_{T+h}^2] = \bar{\sigma}^2 + (\alpha+\beta)^{\,h-1}\bigl(\sigma_{T+1}^2 - \bar{\sigma}^2\bigr), \qquad \bar{\sigma}^2 = \frac{\omega}{1-\alpha-\beta}.

Read this carefully, because it is the geometry of every GARCH forecast. The term structure of variance starts at today's conditional variance σT+12\sigma_{T+1}^2 and decays geometrically toward the long-run level σˉ2\bar{\sigma}^2. If today is calmer than average, the forecast curve rises toward σˉ2\bar\sigma^2; if today is a crisis, it falls toward it. The speed of that decay is set entirely by (α+β)(\alpha+\beta) — and in the near-IGARCH crypto regime, where α+β0.99\alpha+\beta \approx 0.99, the decay is so slow that for horizons under a couple of weeks the forecast barely moves off today's level. That is worth internalizing: for short holding periods, the crypto GARCH forecast is essentially "tomorrow looks like today, only very slowly reverting."

Aggregating to a holding horizon. Traders rarely care about the variance of a single future day. If you hold a position for HH days and returns are conditionally uncorrelated (the stylized fact from the start), the variance of the cumulative HH-day return is the sum of the one-day forecast variances:

σT2(H)=h=1HET[σT+h2],σT(H)=σT2(H).\sigma_{T}^{2}(H) = \sum_{h=1}^{H} \mathbb{E}_T[\sigma_{T+h}^2], \qquad \sigma_T(H) = \sqrt{\sigma_T^2(H)}.

This is the number you actually size against — the volatility of the P&L over your holding period. Note it is emphatically not the naive HσT+1\sqrt{H}\,\sigma_{T+1} scaling, which is only correct if variance is constant. When today's variance is above σˉ2\bar\sigma^2, the mean-reverting forecast makes the true HH-day vol lower than the square-root rule; when today is calm, it is higher. Getting this right is the difference between a stop that respects the term structure and one that does not.

In code:

H = 10
fc = res.forecast(horizon=H, reindex=False)

var_path_pct2 = fc.variance.iloc[-1].values        # [E_T sigma^2_{T+1}, ..., T+H]
var_path      = var_path_pct2 / (100.0 ** 2)       # back to decimal variance

daily_vol   = np.sqrt(var_path)
print("Forecast daily vol path:", np.round(daily_vol * 100, 2), "%")

H_day_var = var_path.sum()
H_day_vol = np.sqrt(H_day_var)
print(f"{H}-day holding-period vol: {H_day_vol:.2%}")

naive = np.sqrt(var_path[0] * H)
print(f"Naive sqrt(H) * sigma_1:   {naive:.2%}")

For longer horizons GARCH also supports simulation forecasts (method="simulation"), which propagate the innovation distribution forward and give you the full forecast density, not just its variance — useful when the innovations are non-Gaussian, as they will be once we move to Student-t and skewed distributions in Part 2. For the linear-in-variance quantities above, the analytic path is exact and free.

Diagnostics: Did the Model Actually Work?

Fitting a model is not the same as validating it. The whole point of GARCH is to absorb the conditional heteroskedasticity — the volatility clustering — so that what remains is (close to) i.i.d. The right check is therefore to look at the standardized residuals

z^t=rtμ^σ^t\hat{z}_t = \frac{r_t - \hat\mu}{\hat\sigma_t}

and ask: is the clustering gone? If the model captured the variance dynamics, the z^t\hat z_t should have unit variance and, crucially, their squares z^t2\hat z_t^2 should show no remaining autocorrelation. We run three tests.

1. Ljung-Box on standardized residuals. Checks that no linear autocorrelation is left in the level of z^t\hat z_t (this is really testing the mean model, not the variance model). Should not reject.

2. Ljung-Box on squared standardized residuals. This is the important one. If z^t2\hat z_t^2 still has significant autocorrelation, the variance model failed to remove the clustering — there is structure GARCH(1,1) did not capture, and you may need a higher order, an asymmetric variant, or a different innovation distribution. Should not reject.

3. ARCH-LM test (Engle's Lagrange-multiplier test). Regress z^t2\hat z_t^2 on its own lags and test for joint significance. It is essentially a formal version of test 2 and directly asks "is there ARCH effect left over?" A non-significant result confirms the conditional heteroskedasticity has been modeled away.

from statsmodels.stats.diagnostic import acorr_ljungbox
from arch.unitroot.cointegration import engle_granger  # (unrelated; shown for import clarity)

z  = res.std_resid.dropna()          # standardized residuals
z2 = z ** 2

lb_z  = acorr_ljungbox(z,  lags=[10, 20], return_df=True)
lb_z2 = acorr_ljungbox(z2, lags=[10, 20], return_df=True)
print("Ljung-Box on standardized residuals:\n", lb_z, "\n")
print("Ljung-Box on SQUARED standardized residuals:\n", lb_z2, "\n")

lm = res.arch_lm_test(lags=10, standardized=True)
print(lm)

print(f"\nStd-resid excess kurtosis: {stats.kurtosis(z):.2f}")

What good output looks like: the Ljung-Box p-values on z^t2\hat z_t^2 jump from near-zero (on the raw squared returns) to comfortably above 0.05, and the ARCH-LM test fails to reject. That is your evidence the model did its job on the second moment.

What imperfect output looks like — and what you should expect with a plain Gaussian GARCH(1,1) on crypto — is that the clustering tests pass but the standardized-residual kurtosis is still elevated (say 4-6 rather than 0). GARCH removes the clustering but a single fat-tailed unconditional distribution remains, because Gaussian innovations cannot reproduce the tails. That residual fat-tailedness is not a bug to be fixed here; it is the motivation for Part 2, asymmetric GARCH and the leverage effect in crypto, where Student-t and skewed-t innovations and the GJR/EGARCH asymmetry term address exactly this.

Application: Volatility-Scaled Sizing and Stops

We now have a forecast of tomorrow's (and the next HH days') volatility. What do we do with it? The two simplest, highest-value uses are position sizing and stop placement. We keep both deliberately basic here — the full vol-targeting strategy with all its practical machinery is Part 4.

Volatility-targeted position sizing

The idea is to hold a position whose risk contribution is roughly constant through time, rather than a position whose notional is constant. If you always deploy the same dollar size, your risk balloons in high-vol regimes and shrivels in calm ones — the opposite of what you want. Volatility targeting inverts this: aim for a fixed target volatility of P&L, and let the forecast dictate the size.

For a target annualized volatility σtarget\sigma_{\text{target}} (say 20%) and a forecast annualized volatility σ^t\hat\sigma_t, the position weight is

wt=σtargetσ^t.w_t = \frac{\sigma_{\text{target}}}{\hat{\sigma}_t}.

When forecast vol is high, you scale down; when it is low, you scale up. That is the entire mechanism. Because σ^t\hat\sigma_t is forecast — known at tt before the return at t+1t+1 is realized — there is no look-ahead, provided you are disciplined about timing (more on this in the pitfalls).

def vol_target_weight(sigma_forecast_annual, sigma_target_annual=0.20,
                      w_max=3.0):
    """Volatility-scaled position weight. Inputs/outputs in decimals.
    w_max caps leverage so a tiny forecast vol can't demand insane size."""
    w = sigma_target_annual / sigma_forecast_annual
    return float(np.clip(w, 0.0, w_max))

sigma_1d   = np.sqrt(res.forecast(horizon=1).variance.iloc[-1, 0]) / 100.0
sigma_ann  = sigma_1d * np.sqrt(365)
w          = vol_target_weight(sigma_ann, sigma_target_annual=0.20)
print(f"Forecast annual vol: {sigma_ann:.1%}  ->  position weight: {w:.2f}x")

This is a first cousin of proper capital-allocation rules. Volatility targeting answers "how much should risk scale with volatility," while the Kelly criterion answers "how much should risk scale with edge" — and the two multiply together in a full sizing stack: size \propto edge / variance. Note that Kelly's variance term is exactly the GARCH forecast you just computed, which is why a live volatility model materially sharpens Kelly sizing over a static historical estimate. If your edge estimate itself carries quantified uncertainty, conformal prediction gives a distribution-free way to widen or shrink the size to match, and it composes cleanly with vol targeting.

The cap w_max is not optional. In the near-IGARCH regime a quiet stretch can push the forecast vol quite low, and σtarget/σ^t\sigma_{\text{target}}/\hat\sigma_t will demand leverage that is fine on paper and ruinous when the calm breaks — which, per volatility clustering, it eventually does, often abruptly. Capping leverage is the crude-but-effective acknowledgment that your forecast is a conditional mean, not a guarantee, and that the payoff to being wrong is asymmetric. That asymmetry — a blown-up account is not recoverable by a symmetric win — is exactly the loss-vs-profit asymmetry that should make you systematically more conservative than a variance-only rule suggests.

Volatility-scaled stops

A fixed-percentage stop has the same disease as a fixed position size: a 3% stop is a hair-trigger in a calm market and a rounding error in a violent one. It gets you knocked out of good positions by ordinary noise during high-vol regimes and gives back far too much during transitions. The fix is to set the stop distance in units of forecast volatility.

stop distancet=kσ^t(H)\text{stop distance}_t = k \cdot \hat\sigma_t^{(H)}

where σ^t(H)\hat\sigma_t^{(H)} is the forecast volatility over your expected holding horizon HH (the aggregated quantity from the forecasting section) and kk is a multiple — typically 1.5 to 3 — chosen so the stop sits outside normal fluctuation but inside a genuine adverse move.

def vol_scaled_stop(entry_price, side, sigma_H, k=2.0):
    """
    entry_price : fill price
    side        : +1 long, -1 short
    sigma_H     : forecast volatility over the holding horizon (decimal)
    k           : stop width in vol units
    Returns the stop price.
    """
    stop_frac = k * sigma_H
    return entry_price * (1.0 - side * stop_frac)

var_path = res.forecast(horizon=10, reindex=False).variance.iloc[-1].values / (100.0 ** 2)
sigma_H  = np.sqrt(var_path.sum())

entry = float(px.iloc[-1])
stop  = vol_scaled_stop(entry, side=+1, sigma_H=sigma_H, k=2.0)
print(f"Entry {entry:,.0f}  |  10-day vol {sigma_H:.2%}  |  2-sigma stop {stop:,.0f}")

Because σ^t(H)\hat\sigma_t^{(H)} uses the mean-reverting term-structure forecast rather than a flat historical number, the stop automatically widens going into turbulent regimes and tightens as volatility subsides — the term structure does the adapting for you. This is the same forecast feeding both the size and the stop, which is a feature: in a high-vol regime you simultaneously hold less and give the position more room, and the two effects compound into materially lower tail risk. Sizing and stops are two projections of one volatility view, not two independent knobs.

That is as far as we take the application in Part 1. A real strategy has to handle transaction costs from constant rebalancing, the timing of when the forecast is computed versus when the trade is placed, turnover control, and — above all — honest out-of-sample evaluation. All of that is Part 4: the volatility-targeting GARCH strategy, where we build and walk-forward-test the whole thing.

Pitfalls

GARCH is easy to fit and easy to fool yourself with. The failure modes are consistent.

Return scaling. Covered above, but it is the number-one bug, so it bears repeating: fit arch on returns × 100, and unscale every output (variance by 1002100^2, volatility by 100100). A silent 100x error here poisons every downstream sizing and stop calculation.

Look-ahead in fitting. The subtle killer. If you fit the model on the entire history and then compute "forecasts" over that same history, every forecast has secretly seen the future — the parameters were estimated using data from after the forecast date. The in-sample fit will look wonderful and the live performance will not resemble it at all. Every backtested forecast must come from a model fit only on data available at that moment: refit on an expanding or rolling window, forecast one step, roll forward. This is non-negotiable and it is the entire subject of walk-forward optimization. The gap between an in-sample GARCH and a properly walk-forward one is the gap between a demo and a system that survives contact with live markets — see also backtest-live parity.

Timing of the forecast. Related but distinct. The forecast for day t+1t+1 must be computed from information available at the close of day tt (or whenever your bar closes), and the position must be executable at a price you could actually get. Computing the forecast using day t+1t+1's close and then "trading" at day t+1t+1's open is a look-ahead that quietly inflates every result.

Overfitting high orders. GARCH(1,1) is almost always enough. The temptation to fit GARCH(2,2) or GARCH(3,1) because it nudges the in-sample log-likelihood up is usually noise-fitting; the extra parameters rarely improve out-of-sample forecasts and often make the optimizer unstable near the boundary. Prefer the parsimonious model, and if you must compare orders, compare them by out-of-sample forecast loss on a walk-forward split, not by in-sample AIC. When the residual diagnostics still show a problem, the fix is usually a better innovation distribution or an asymmetry term (Part 2), not a higher order.

Structural breaks read as persistence. As noted, a permanent shift in the volatility level (a new market regime, a change in market microstructure) can be absorbed by GARCH as spuriously high persistence, pushing α+β\alpha+\beta toward 1. If your long-run vol estimate looks unstable across windows, suspect a break rather than trusting the near-IGARCH point estimate. Rolling refits and, where appropriate, an explicit regime model guard against this.

Treating volatility forecasts as return forecasts. GARCH forecasts the magnitude of moves, not their direction. It tells you how big tomorrow's swing is likely to be, not which way. This is exactly why its natural home is risk management — sizing, stops, VaR — rather than signal generation. Do not confuse a good variance forecast for an edge.

Where This Goes Next

GARCH(1,1) is the foundation, and it is deliberately incomplete. The series builds on it in three directions:

  • Asymmetry and heavy tails — real crypto volatility responds more to down moves than up moves (the leverage effect), and Gaussian innovations cannot reproduce the tails. GJR-GARCH, EGARCH, and Student-t / skewed-t innovations are Part 2.
  • Multivariate volatility — correlations between crypto assets are themselves time-varying and spike in crashes. Modeling the whole covariance matrix dynamically is Part 3: DCC-GARCH, which connects directly to Markowitz mean-variance and CVaR-based allocation once the covariance is dynamic.
  • The full strategy — sizing, stops, costs, turnover, and honest walk-forward evaluation come together in Part 4.

And where GARCH marginals feed joint risk: the univariate conditional-variance model here is exactly the first stage of the GARCH-EVT-copula pipeline for portfolio VaR/CVaR. Once you have standardized residuals from a per-asset GARCH fit, you transform them and glue them together with a copula — the marginals are GARCH, the dependence is the copula. That construction, including tail dependence and the EVT tail treatment, is covered in depth in copula models for joint crypto risk; this article is the univariate engine that sits underneath it.

Summary

  • Crypto returns exhibit volatility clustering, fat tails, and no return autocorrelation but strong squared-return autocorrelation. Any tool that assumes constant volatility — Black-Scholes with a single σ\sigma, static VaR, fixed-percentage stops — is mis-specified against these facts.
  • GARCH(1,1), σt2=ω+αεt12+βσt12\sigma_t^2 = \omega + \alpha\varepsilon_{t-1}^2 + \beta\sigma_{t-1}^2, models time-varying conditional variance with three parameters: a baseline ω\omega, a shock reaction α\alpha, and a persistence β\beta. It is an ARCH(\infty) with geometrically decaying memory, which is why it beats high-order ARCH.
  • Stationarity requires α+β<1\alpha+\beta<1; the long-run variance is ω/(1αβ)\omega/(1-\alpha-\beta), persistence is α+β\alpha+\beta, and the volatility half-life is ln0.5/ln(α+β)\ln 0.5 / \ln(\alpha+\beta). Crypto sits in the near-IGARCH regime (α+β0.99\alpha+\beta\approx 0.99): very persistent, slow to mean-revert, and with a fragile long-run-variance estimate.
  • Estimate by maximum likelihood. The Gaussian log-likelihood is a sum of one-step densities; fit it with arch_model(r*100, vol="Garch", p=1, q=1). Remember the ×100 scaling and unscale every output consistently.
  • Forecasts mean-revert geometrically toward the long-run variance at rate (α+β)h1(\alpha+\beta)^{h-1}. Aggregate daily variance forecasts to get holding-horizon volatility — not the naive H\sqrt{H} rule.
  • Validate with Ljung-Box on squared standardized residuals and the ARCH-LM test. Passing these confirms the clustering was modeled away; residual fat tails that remain motivate Part 2.
  • Apply it to volatility-targeted sizing (wt=σtarget/σ^tw_t = \sigma_{\text{target}}/\hat\sigma_t, capped) and volatility-scaled stops (kσ^t(H)k\cdot\hat\sigma_t^{(H)}). One forecast drives both, so high-vol regimes get smaller size and wider stops simultaneously.
  • The pitfalls that matter: return scaling, look-ahead in fitting (fit only on past data, always walk-forward), forecast timing, over-ordering, and never mistaking a variance forecast for a direction forecast.

References:

  • Engle, R. F. (1982). Autoregressive Conditional Heteroscedasticity with Estimates of the Variance of United Kingdom Inflation. Econometrica, 50(4), 987-1007. DOI
  • Bollerslev, T. (1986). Generalized Autoregressive Conditional Heteroskedasticity. Journal of Econometrics, 31(3), 307-327. DOI
  • Mandelbrot, B. (1963). The Variation of Certain Speculative Prices. The Journal of Business, 36(4), 394-419. DOI
  • Nelson, D. B. (1991). Conditional Heteroskedasticity in Asset Returns: A New Approach. Econometrica, 59(2), 347-370. DOI
  • Katsiampa, P. (2017). Volatility estimation for Bitcoin: A comparison of GARCH models. Economics Letters, 158, 3-6. DOI
  • Chu, J., Chan, S., Nadarajah, S., & Osterrieder, J. (2017). GARCH Modelling of Cryptocurrencies. Journal of Risk and Financial Management, 10(4), 17. DOI
  • Sheppard, K. (2023). arch: Autoregressive Conditional Heteroskedasticity (ARCH) and other tools for financial econometrics in Python. GitHub.
Disclaimer: The information provided in this article is for educational and informational purposes only and does not constitute financial, investment, or trading advice. Trading cryptocurrencies involves significant risk of loss.

MarketMaker.cc Team

Quantitative Research & Strategy

Discuss in Telegram
Newsletter

Stay Ahead of the Market

Subscribe to our newsletter for exclusive AI trading insights, market analysis, and platform updates.

We respect your privacy. Unsubscribe at any time.