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.
A large return often makes the next risk estimate larger. ARCH makes that mechanism explicit: today's conditional variance is a weighted sum of past squared shocks plus a positive intercept.
There is no hidden memory state in a finite ARCH model. You can point to each lag, its coefficient and its contribution. That makes ARCH a useful place to learn volatility dynamics before adding GARCH persistence or sign asymmetry.
This package evaluates supplied teaching coefficients. It does not estimate them, maximize a likelihood, or establish forecast skill.
Open this figure at full size.
Returns are not automatically shocks
Write the observed return as a conditional mean plus a residual ε_t. ARCH models the conditional variance of that residual. If you pass raw returns, you are implicitly choosing a zero conditional mean; say so rather than hiding the assumption.
For q lags, the recursion is h_t=ω+Σ_(j=1)^q α_j ε_(t−j)². The ARCH reference documentation describes this finite squared-residual specification.
Our contract requires ω>0, every α_j≥0, and Σα_j<1. Nonnegative coefficients guarantee positive conditional variances. The sum restriction selects a finite-unconditional-variance teaching subset under the usual standardized-innovation assumptions; positivity and stationarity are not the same statement.
Each α_j is dimensionless. ω and h have squared-return units. Enter residual 0.01 for 1%, not 1. Squaring the latter would multiply the shock contribution by 10,000.
Calculate an ARCH(2) state
Let ω=0.000002, α_1=0.2 and α_2=0.1. The observed residuals are ε_0=−0.02 and ε_1=0.01. Before observing ε_2, the model gives
The newest residual contributes 0.00002; the older, larger residual contributes 0.00004. Its larger magnitude outweighs its smaller coefficient. The square-root conditional volatility is approximately 0.7874% for the modeled return interval.
Changing ε_2 cannot change h_2. That residual has not yet entered the information set. This one-index distinction is essential in a backtest: using the current shock to compute the current pre-shock forecast leaks information.
Open this figure at full size.
Download the exact worked input and expected values.
Memory is a finite queue
Suppose future residuals become zero. The old shock moves from one lag coefficient to the next and eventually leaves after q steps. Its effect can increase or decrease as it ages if the coefficient vector is not monotone. “Older means less important” is not guaranteed by the model name.
An isolated residual of magnitude s contributes α_j s² when it occupies lag j. That expression is the model's impulse-response fingerprint. Flip the residual's sign and nothing changes. Double its magnitude and its contribution quadruples.
The runtime accepts an explicit vector of coefficients rather than repeating one scalar across an inferred order. The vector length determines q. It withholds the first q states because the required lag history is absent; it does not pretend that missing presample residuals were observed zeros.
Compare mechanisms, not just smooth lines
| Model | Where memory lives | What a sign flip does |
|---|---|---|
| ARCH(q) | q lagged squared shocks | Nothing |
| GARCH(1,1) | Latest squared shock plus prior variance | Nothing |
| GJR-GARCH | Same, with a negative-shock branch | Changes the threshold contribution |
| EGARCH | Log-variance recursion and standardized shock | Changes the sign term |
An ARCH model with many lags can describe long response patterns, but it also introduces many coefficients to estimate. GARCH offers a parsimonious recursive alternative. Neither choice earns a forecast-performance claim merely because its line is smoother.
Open this figure at full size.
The playground is a lag audit
Step through 64 synthetic residuals. At each state, inspect the lagged residuals, coefficients and weighted squares. The current residual belongs to the next update, not the current forecast. The displayed information cutoff makes that timing visible.
Change a coefficient and predict which aged shock contributions will respond. Use the sign-reversal comparison to verify symmetry. In the quiet edge case, once all retained shocks are zero, h equals ω. The invalid case rejects coefficients whose declared persistence reaches or exceeds one.
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 fitting and evaluation would add
A real modeling workflow must specify a mean model, estimate parameters using training data, choose an innovation distribution and evaluate held-out forecasts. Residual diagnostics can reveal dependence left unexplained by the model. Those tasks are deliberately outside this deterministic recursion.
Do not choose q by examining the final test period and then report that same period as an untouched evaluation. Rolling or expanding estimation should retain the parameter vintage, available observations and forecast timestamp. The package's synthetic coefficients are explanatory inputs, not recommended settings for an asset.
The direct implementation takes O(nq) time. Tests check the explicit small state, lag order, sign invariance, positivity, insufficient history, malformed coefficients and prefix invariance. A zero residual is valid; a missing residual is not silently replaced with zero.
The value to carry forward
ARCH turns “the model remembers a shock” into a visible queue of contributions. If you can predict when a shock enters, which weight it receives and when it exits, you understand the core mechanism. Next, GARCH shows how a recursive variance state extends that memory without storing a long coefficient vector.
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.
import json
from pathlib import Path
from arch import calculate
data = json.loads(Path("teaching-path.json").read_text())
result = calculate(data)
print(result["latest"])
import {calculate} from './arch.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.
Explicit coefficient vector; omega>0, alpha_j>=0, sum(alpha)<1. First q states withheld. Residuals, not raw nonzero-mean returns.
Continue the investigation
Rendered from the canonical Mermaid sources linked by this article.
ARCH — calculation-flow
ARCH — decision-boundary
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
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 — ARCH 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
Explicit coefficient vector; omega>0, alpha_j>=0, sum(alpha)<1. First q states withheld. Residuals, not raw nonzero-mean returns.
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.
Full dependency-light reference implementations in both supported languages.
/** Standalone D10-F03-A01 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,"arch");
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-A01",title:"ARCH",parameters:p,...result};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.
