Library/Volatility and Covariance/Conditional Volatility/EGARCH

D10-F03-A03 / Released engineering topic

EGARCH: explain the sign effect on the log-variance scale

Compare signed standardized shocks on the log-variance scale.

Compare signed standardized shocks on the log-variance scale.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.

Equal-sized positive and negative returns receive the same response in symmetric GARCH. EGARCH can distinguish them, but it does so on a different scale: it updates log variance using a standardized shock, then exponentiates.

The important implementation consequence is immediate. Constraints copied from variance-level GARCH can reject perfectly valid EGARCH parameters. In particular, a negative log-variance intercept is not an error.

This tutorial makes the scale, sign convention and numerical boundaries explicit. It evaluates a supplied EGARCH(1,1,1) recursion; parameter fitting is outside scope.

Calculated behavior: Compare signed standardized shocks on the log-variance scale. Actual synthetic reference outputs.

Open this figure at full size.

Standardize using the previous variance

Define z_(t−1)=ε_(t−1)/√h_(t−1). Under the Gaussian-centering convention used here,

loght=ω+βloght1+α(zt12/π)+γzt1.\log h_t=\omega+\beta\log h_{t-1} +\alpha(|z_{t-1}|-\sqrt{2/\pi})+\gamma z_{t-1}.

The EGARCH technical reference documents this form. The magnitude term is centered by the expected absolute standard-normal shock. A different innovation convention can require different centering; the constant is not decorative.

ω, α and γ may be negative. The package requires |β|<1 as its declared log-persistence subset and a positive explicit initial variance. These checks do not claim to establish every moment-existence condition for every innovation distribution.

Work through a negative standardized shock

Let h_0=0.0001 and ε_0=−0.02. The previous conditional standard deviation is 0.01, so z_0=−2. Choose ω=−0.5, β=0.9, α=0.1 and γ=−0.2.

The carried log variance is 0.9×ln(0.0001)≈−8.289306335. The centered magnitude contribution is 0.1×(2−0.797884561)≈0.120211544. The sign contribution is(−0.2)×(−2)=0.4.

Adding the intercept gives log h_1≈−8.269094791. Exponentiate only after completing that sum; the resulting variance is approximately 0.000256317. The fixture checks both the log state and its exponentiated result.

Independent numeric checkpoint for EGARCH

Open this figure at full size.

Download the exact worked input and expected values.

The paired-sign experiment has an exact answer

Replace ε_0=−0.02 with +0.02 while holding h_0 and all coefficients fixed. The magnitude term stays the same. The sign term changes from +0.4 to −0.4. Thus negative-shock log variance exceeds positive-shock log variance by 0.8, and their variance ratio is e^0.8≈2.22554.

This is a controlled model comparison, not evidence that a real asset's negative returns always multiply volatility by that amount. The size and direction depend on the supplied γ. With γ<0, a negative standardized shock raises log variance relative to an equally large positive one; with γ=0, that sign asymmetry disappears.

Standardization matters too. The same raw residual has a smaller |z| when the prior variance is already high. Comparing two responses with different seeds can therefore confound sign asymmetry with different shock scaling.

A log contribution is not a variance contribution

Displayed quantityUnits or scaleHow to combine it
εDecimal returnDivide by previous standard deviation
zDimensionlessEnters magnitude and sign terms
ω, carried state, magnitude and sign termsLog-variance scaleAdd them
hSquared-return scaleExponentiate the completed log state

Do not add the displayed log terms to h directly. Do not standardize using the newly computed h_t; that would create a different, implicit update. The lab retains the old log state in its decomposition to make this ordering inspectable.

Comparison of EGARCH conventions, outcomes and limitations.

Open this figure at full size.

Positivity is not permission to hide overflow

Exponentiation produces a positive mathematical variance, but finite-precision arithmetic has limits. Extreme coefficients or standardized shocks can send the exponent outside a safe numerical range.

The runtime rejects log states outside [−700,700] with a numerical error. It does not silently clamp the exponent and then present the capped number as the model's estimate. That bound is an implementation safety policy, not a financial calibration rule.

Missing or nonfinite residuals and nonpositive initial variance are also rejected. A negative ω remains valid and is used in the worked example, specifically to prevent the old variance-level constraint mistake from returning.

What to do in the playground

Use the 64-observation synthetic residual path. Step advances one update and shows the previous standardized shock, carried log state, centered magnitude, sign term and final variance. Change γ and compare the two shock signs at a fixed seed.

Try γ=0 and predict that the sign effect vanishes. Then inspect an extreme invalid update: the lab should explain the numerical rejection, not leave an old plausible chart without a warning. Reset restores the same path and parameters.

Four calculation stages: Standardize using previous variance; Center the shock magnitude; Add signed shock and carried log state; Exponentiate or reject numerical overflow

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.

When this extra structure is useful

EGARCH is useful for representing sign-sensitive responses while keeping a log-variance state. It does not automatically outperform GARCH, solve heavy tails or remove the need for a mean model. Estimation, innovation diagnostics and chronological forecast evaluation remain separate tasks.

For another asymmetric construction, compare GJR-GARCH. Its threshold term is additive in variance, whereas this sign term is additive in log variance. Holding the information set fixed makes that distinction far clearer than comparing two independently fitted charts.

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 egarch import calculate

data = json.loads(Path("teaching-path.json").read_text())
result = calculate(data)
print(result["latest"])
TypeScript
import {calculate} from './egarch.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.

EGARCH(1,1,1), Gaussian magnitude centering; omega/alpha/gamma may be negative, abs(beta)<1. Explicit initial variance; reject numerical overflow, never clamp.

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.

EGARCH — calculation-flow

EGARCH — 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 — EGARCH 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

EGARCH(1,1,1), Gaussian magnitude centering; omega/alpha/gamma may be negative, abs(beta)<1. Explicit initial variance; reject numerical overflow, never clamp.

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.

egarch.ts
/** Standalone D10-F03-A03 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,"egarch");
  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-A03",title:"EGARCH",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.