Library/Volatility and Covariance/Conditional Volatility/GARCH

D10-F03-A02 / Released engineering topic

GARCH: distinguish yesterday's shock from yesterday's variance

Separate new shock information from carried variance.

Separate new shock information from carried variance.D10 / D10-F03

For analysts and developers who know residuals from a mean model, conditional variance, and lagged information. The goal is to reproduce the mechanism, inspect its failure states, and decide what the output can legitimately tell you—not to fit or endorse a trading strategy.

GARCH has two memory channels. One reacts to the latest squared shock; the other carries forward the previous conditional variance. If you combine them into a single unexplained line, you miss the reason the model is useful.

Here you will calculate a GARCH(1,1) update, understand the role of initialization, and separate realized zero-shock decay from expected multi-step persistence. Those are related ideas, but they do not use the same coefficient.

Calculated behavior: Separate new shock information from carried variance. Actual synthetic reference outputs.

Open this figure at full size.

The information set comes first

The recursion is h_t=ω+αε_(t−1)²+βh_(t−1). h_t is the conditional variance for residual ε_t using information through t−1. The GARCH documentation describes the variance-level family and its lagged terms.

The input array contains decimal residuals after the chosen mean model. This teaching contract requires ω>0, α≥0, β≥0 and α+β<1. The final inequality selects the usual finite-second-moment case under standardized innovations. It does not prove that an estimated model forecasts well or that its assumptions fit the data.

The initial h_0 is explicit and positive. By default it is 0.0001. It is a seed, not an estimated observation and not silently replaced by ω/(1−α−β). The first output is labeled as initialization; later states are actual updates.

Two updates, checked by hand

Let ω=0.000002, α=0.1, β=0.8 and h_0=0.0001. With ε_0=−0.02,

h1=0.000002+0.1(0.0004)+0.8(0.0001)=0.000122.h_1=0.000002+0.1(0.0004)+0.8(0.0001)=\mathbf{0.000122}.

The intercept contributes 0.000002, new shock information 0.00004, and carried variance 0.00008. If ε_1=0.01, then h_2=0.000002+0.00001+0.8×0.000122=0.0001096.

The smaller second shock does not immediately erase the elevated state. That is the β channel doing its job. Replacing h_1 with the last squared return would be a different model.

Independent numeric checkpoint for GARCH

Open this figure at full size.

Download the exact worked input and expected values.

Two decay questions with two answers

First ask: what happens along a controlled future path whose residuals are all zero? The update becomes h_next=ω+βh_current. Deviations from the zero-shock fixed point ω/(1−β) decay by β per step.

Now ask: what happens to the expected multi-step variance forecast under the fitted model? Future squared residuals have conditional expectation equal to their conditional variance. The expected recurrence therefore uses α+β, and its long-run level is ω/(1−α−β) when that moment exists.

These are not competing formulas. The first conditions on a deliberately quiet realized path; the second averages over future shocks generated under the model. For the worked coefficients, the zero-shock level is 0.00001 and the unconditional model level is 0.00002. A tutorial that uses “persistence” without stating which question it means can confuse both.

Initialization has a measurable footprint

Run the same observed residual path from two seeds. Because the shocks are held fixed, the difference between the resulting variance states is β^t times the initial difference. This gives a direct way to assess seed sensitivity for a supplied path.

It does not justify estimating the seed from future observations before a historical forecast. If you use a backcast, rolling fit or training-sample estimate in production, record how and when it was constructed. The reference chooses explicit initialization so that this dependency cannot hide.

ChangeImmediate effectFollow-on effect
Larger absolute last residualαε² increasesCarried through subsequent states
Flip residual sign onlyNo changeSymmetric model
Increase β at a fixed valid parameter setMore prior variance retainedSlower same-path seed decay
α+β reaches oneContract rejectionOutside this finite-moment teaching subset

Comparison of GARCH conventions, outcomes and limitations.

Open this figure at full size.

Use the playground to separate the three terms

Step through the 64-residual synthetic path. The diagnostic panel breaks each updated state into intercept, shock contribution and carried variance. Change α or β while keeping the data and seed visible. A parameter change recomputes the path; it is not an animation-speed setting.

Compare the large-shock path with a quiet path and reverse the shock sign. GARCH should respond to magnitude but not sign. Then try an invalid persistence combination. The reference rejects it instead of drawing an explosive path and labeling it an ordinary stationary GARCH forecast.

Four calculation stages: Read explicit initial variance; Square previous residual; Add intercept and α shock term; Carry β times previous variance

Open this figure at full size.

Open the standalone guided playground. The embedded playground and runnable code are available on this page. Download the 64-observation teaching input.

What this implementation does—and does not validate

The recursion is O(n) and uses only previous state and previous residual, while retaining the full trace for teaching. It validates finite inputs, positive initialization and the declared parameter constraints. Prefix tests confirm that future residuals cannot alter earlier states.

No coefficients are fitted here. There is no likelihood estimate, distribution selection, confidence interval or out-of-sample score. Forecast evaluation should compare honest chronological forecasts against a clear target and baseline. A synthetic impulse response establishes mechanism, not economic usefulness for a particular asset.

For sign-asymmetric responses, continue to GJR-GARCH or EGARCH. Keep the same residual path and seed when comparing mechanisms so the difference you see has an identifiable cause.

Reproduce and inspect the calculation

The Python and TypeScript tabs contain standalone implementations, not imports into an unseen runtime. Both expose calculate(input_data). Feed the worked JSON's input object into that entry point. For the longer experiment, use the teaching-path JSON directly.

Python
import json
from pathlib import Path
from garch import calculate

data = json.loads(Path("teaching-path.json").read_text())
result = calculate(data)
print(result["latest"])
TypeScript
import {calculate} from './garch.ts';
const result = calculate(inputData); // inputData is the downloaded JSON object
console.log(result.latest);

Place the downloaded input beside your script and the standalone source on its import path. The Python reference uses the standard library; the TypeScript reference has no external runtime dependency. Shared tests include independent numeric anchors, valid boundaries, rejected inputs and cross-language output comparisons. They establish arithmetic, not forecasting performance.

Evidence and scope

This article uses authored synthetic calculations and primary technical references, reviewed 2026-09-10. Historical market examples are deferred until identity, adjustment basis, chronology and redistribution rights can be verified. No personal trading history or search-ranking superiority is asserted.

GARCH(1,1), declared positive initial_variance at index 0; omega>0, alpha,beta>=0 and alpha+beta<1. No fitting.

Continue the investigation

  • ARCH: compare its assumptions and information boundary before comparing the numbers.
  • HAR-RV: compare its assumptions and information boundary before comparing the numbers.

GARCH — calculation-flow

GARCH — decision-boundary

ReferencesPrimary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

Reviewed 2026-09-10. Primary technical documentation and papers; synthetic arithmetic is author-derived. This is a targeted source review, not a verified review of Google's top ten results and not a claim of ranking superiority.

  • S1: arch — GARCH model reference — accessed 2026-09-10. Rolling official documentation snapshot; exact package versions used for numerical comparisons are recorded in the repair numeric-evidence.json. Supports the definition and declared convention, not investment performance. Jurisdiction: not applicable to this mathematical reference.

Scope of evidence

GARCH(1,1), declared positive initial_variance at index 0; omega>0, alpha,beta>=0 and alpha+beta<1. No fitting.

Historical case: deferred. No public provider dataset, historical performance claim, or personal trading anecdote is used. Synthetic examples demonstrate arithmetic, not market efficacy. Sources are not copied as article prose.

Accessed: 2026-09-10.

Supports: estimator definition and the explicitly declared variants.

Limitations: technical documentation does not verify a real market feed, author experience, forecast efficacy or search-result superiority. Original-paper access limitations are recorded in the repair report.

garch.ts
/** Standalone D10-F03-A02 reference. Generated from validated D10 v2 source. */
export class ContractError extends Error {}
type RecordValue=Record<string, any>;
type Matrix=number[][];
const sum=(x:number[]):number=>x.reduce((a,b)=>a+b,0);


function requireValue(ok: unknown, code: string, message: string): asserts ok {
  if (!ok) throw new ContractError(`${code}: ${message}`);
}

function finite(x: unknown, name: string): number {
  requireValue(typeof x === 'number' && Number.isFinite(x), 'NUMBER', `${name} must be a finite number`);
  return x;
}

function integer(x: unknown, name: string, minimum = 0, maximum = 10000): number {
  const v = finite(x, name);
  requireValue(Number.isInteger(v) && v >= minimum && v <= maximum, 'INTEGER', `${name} must be an integer in [${minimum}, ${maximum}]`);
  return v;
}

function param(p: RecordValue, key: string, fallback: number): number {
  return finite(Object.hasOwn(p, key) ? p[key] : fallback, key);
}

function option(p: RecordValue, key: string, fallback: any): any {
  return Object.hasOwn(p, key) ? p[key] : fallback;
}

function positive(p: RecordValue, key: string, fallback: number): number {
  const v = param(p, key, fallback);
  requireValue(v > 0, 'RANGE', `${key} must be positive`);
  return v;
}

function vector(value: unknown, name: string, minimum = 1): number[] {
  requireValue(Array.isArray(value) && value.length >= minimum, 'SHAPE', `${name} needs ${minimum} or more values`);
  return value.map((v, i) => finite(v, `${name}[${i}]`));
}

function seriesResult(series: (RecordValue | null)[], diagnostics: RecordValue): RecordValue {
  const at = series.findIndex(v => v !== null);
  return {series, latest: series.at(-1) ?? null, ready: series.length > 0 && series.at(-1) !== null,
    ready_at: at < 0 ? null : at, diagnostics};
}

function conditional(data: RecordValue, p: RecordValue, kind: string): RecordValue {
  const r = vector(data.returns, 'returns'), omega = param(p, 'omega', kind === 'egarch' ? -.5 : .000002);
  const initial = positive(p, 'initial_variance', .0001), series: (RecordValue | null)[] = r.map(() => null);
  if (kind !== 'egarch') requireValue(omega > 0, 'RANGE', 'omega must be positive');
  if (kind === 'arch') {
    const alphas = vector(option(p, 'alphas', [.15, .1, .05]), 'alphas');
    requireValue(Math.min(...alphas) >= 0 && sum(alphas) < 1, 'PERSISTENCE', 'ARCH coefficients need nonnegative sum below one');
    for (let i = alphas.length; i < r.length; i++) {
      const terms = alphas.map((a, j) => a * r[i - j - 1] ** 2);
      series[i] = {index: i, variance: omega + sum(terms), intercept: omega, lag_contributions: terms};
    }
  } else if (kind === 'figarch') {
    const d = param(p, 'd', .35), m = integer(option(p, 'truncation', 24), 'truncation', 2, 1000);
    const backcast = positive(p, 'backcast_variance', initial);
    requireValue(d > 0 && d < 1, 'RANGE', 'd must be in (0,1)');
    const weights = [d];
    for (let j = 2; j <= m; j++) weights.push((j - 1 - d) / j * weights.at(-1)!);
    for (let i = 0; i < r.length; i++) {
      const used = Math.min(i, m), terms = weights.slice(0, used).map((v, j) => v * r[i - j - 1] ** 2);
      const mass = sum(weights.slice(0, used)), tail = (1 - mass) * backcast;
      series[i] = {index: i, variance: omega + sum(terms) + tail, intercept: omega, weights, lag_contributions: terms, backcast_contribution: tail, memory_mass: mass};
    }
  } else {
    const alpha = param(p, 'alpha', .08), beta = param(p, 'beta', .9), gamma = param(p, 'gamma', kind === 'egarch' ? -.12 : .02);
    if (kind === 'egarch') requireValue(Math.abs(beta) < 1, 'PERSISTENCE', 'EGARCH requires abs(beta)<1 in this contract');
    else {
      requireValue(alpha >= 0 && beta >= 0 && (kind !== 'gjr_garch' || alpha + gamma >= 0), 'RANGE', 'invalid variance coefficients');
      requireValue(alpha + beta + (kind === 'gjr_garch' ? gamma / 2 : 0) < 1, 'PERSISTENCE', 'persistence must be below one');
    }
    series[0] = {index: 0, variance: initial, initialization: true};
    for (let i = 1; i < r.length; i++) {
      const previous = series[i - 1]!.variance, shock = r[i - 1];
      if (kind === 'egarch') {
        const z = shock / Math.sqrt(previous), carry = beta * Math.log(previous), magnitude = alpha * (Math.abs(z) - Math.sqrt(2 / Math.PI)), sign = gamma * z;
        const lv = omega + carry + magnitude + sign;
        requireValue(lv >= -700 && lv <= 700, 'NUMERIC', 'EGARCH exponent outside supported safe range');
        series[i] = {index: i, variance: Math.exp(lv), log_variance: lv, standardized_shock: z, intercept: omega, carry, magnitude_contribution: magnitude, sign_contribution: sign};
      } else {
        const news = alpha * shock * shock, threshold = kind === 'gjr_garch' && shock < 0 ? gamma * shock * shock : 0, carry = beta * previous;
        series[i] = {index: i, variance: omega + news + threshold + carry, intercept: omega, shock_contribution: news, threshold_contribution: threshold, carry};
      }
    }
  }
  return seriesResult(series, {causal: true, input_count: r.length, fitted_parameters: false, model: kind});
}
export function calculate(data: RecordValue): RecordValue {
  requireValue(data && typeof data === 'object' && !Array.isArray(data),'SHAPE','input must be an object');
  const p=Object.hasOwn(data,'parameters')?data.parameters:{};
  requireValue(p && typeof p === 'object' && !Array.isArray(p),'SHAPE','parameters must be an object');
  const result=conditional(data,p,"garch");
  function check(v:any):void {
    if(typeof v==='number')requireValue(Number.isFinite(v),'NUMERIC','nonfinite computed output');
    else if(Array.isArray(v))v.forEach(check);
    else if(v && typeof v==='object')Object.values(v).forEach(check);
  }
  check(result);
  return {topic_id:"D10-F03-A02",title:"GARCH",parameters:p,...result};
}
Full-height labguided labOpen full screen
Written by

Fintech engineer building market-data and financial systems, and the author of every article, glossary record, and reference implementation on The Fintech Builder.