Library/Volatility and Covariance/Conditional Volatility/GJR-GARCH

D10-F03-A04 / Released engineering topic

GJR-GARCH: isolate the extra response to a negative shock

Isolate the extra term activated by a negative lagged residual.

Isolate the extra term activated by a negative lagged residual.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.

GJR-GARCH asks a focused question: should a negative residual receive an additional squared-shock contribution? You can answer it with a paired experiment—change only the shock's sign and compare the next variance.

The model is often introduced with a “leverage effect” label. Here the mechanism comes first. A sign-dependent statistical response does not, by itself, establish a corporate-finance causal explanation for an asset's behavior.

Calculated behavior: Isolate the extra term activated by a negative lagged residual. Actual synthetic reference outputs.

Open this figure at full size.

One extra branch in a familiar recurrence

The supplied-parameter recursion is

ht=ω+αϵt12+γ1[ϵt1<0]ϵt12+βht1.h_t=\omega+\alpha\epsilon_{t-1}^2+ \gamma\,\mathbf 1[\epsilon_{t-1}<0]\epsilon_{t-1}^2+ \beta h_{t-1}.

The GARCH-family documentation includes this asymmetric power-two construction. The indicator uses a strictly negative residual. A zero residual does not activate it.

For positive shocks, the squared-shock coefficient is α. For negative shocks it is α+γ. Positivity therefore requires α≥0, α+γ≥0 and β≥0, with ω>0. Requiring γ itself to be nonnegative would unnecessarily exclude a valid opposite-direction asymmetry.

The contract also imposes α+β+γ/2<1 under symmetric standardized innovations. The half reflects the symmetry assumption in the second-moment calculation. It should not be applied uncritically under an arbitrary asymmetric innovation distribution.

Compute the threshold contribution separately

Let ω=0.000002, α=0.1, β=0.8, γ=0.1 and h_0=0.0001. For ε_0=−0.02, the next update contains four terms:

  • Intercept:0.000002.
  • Symmetric shock term:0.1×0.0004=0.00004.
  • Negative-shock term:0.1×0.0004=0.00004.
  • Carried variance:0.8×0.0001=0.00008.

Their sum is h_1=0.000162. If the next residual is +0.01, its threshold term is zero and h_2=0.000002+0.00001+0.8×0.000162=0.0001416.

Independent numeric checkpoint for GJR-GARCH

Open this figure at full size.

Download the exact worked input and expected values.

The sign-flip comparison is exact

Replace the first residual with +0.02. Every term except the indicator contribution stays the same. The positive-shock state is 0.000122, so the negative-shock state exceeds it by γ×0.0004=0.00004.

That difference is an excellent test. It is also an excellent explanation. If a sign flip changes the intercept or the carried variance in a one-step paired comparison with the same seed, the implementation or the comparison setup is wrong.

Subsequent states can differ because the changed h_1 enters the β channel. Along the same fixed future residual path, the difference decays by β per update. Expected multi-step dynamics under the model involve the innovation distribution as well; do not confuse that expectation with a fixed-path counterfactual.

Compare the two kinds of asymmetry

FeatureGJR-GARCHEGARCH
State updatedVarianceLog variance
Shock inputSquared residual with sign indicatorStandardized magnitude and signed shock
Paired-sign differenceAdditive γ ε²Multiplicative variance ratio after exponentiation
Positivity handlingCoefficient restrictionsExponentiation, with numerical limits

Both models can describe sign-sensitive variance behavior, but matching a parameter called gamma does not make their responses comparable. The units, scale and standardization differ. Compare the actual paired response at the same information cutoff instead.

Comparison of GJR-GARCH conventions, outcomes and limitations.

Open this figure at full size.

Follow the branch in the playground

The 64-residual synthetic path includes positive and negative observations. Step shows which lagged residual drives the next state and whether its indicator is active. Change γ while keeping α, β, the seed and the residual path fixed.

Predict the result at γ=0: the model should agree with symmetric GARCH for the same other parameters. The sign-flip scenario then demonstrates the isolated threshold difference. An invalid α+γ combination is rejected rather than producing negative variance under a sufficiently large negative shock.

The displayed current residual must not enter the current pre-shock forecast. It affects the next update. This timing rule is as important as the indicator itself.

Four calculation stages: Read previous residual sign; Compute ordinary squared-shock term; Activate γ term only below zero; Add intercept and carried 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.

Initialization, fitting and interpretation

h_0 is an explicit positive initial variance, labeled as a seed. The implementation does not derive it from future observations or insert the symmetric GARCH unconditional formula while forgetting the asymmetric term. Supplied coefficients remain teaching parameters, not fitted market estimates.

A real application would define the mean model, estimate coefficients, choose an innovation distribution, inspect residual diagnostics and evaluate chronological forecasts against a baseline. Parameter restrictions alone do not establish that an asset has the modeled asymmetry or that the forecast improves a decision.

Inputs must be finite decimal residuals. Missing values are not zero shocks. The recursion takes O(n) time and retains its decomposition for audit. Tests cover γ=0 equivalence, paired signs, zero shocks, invalid persistence and prefix invariance.

My practical test of understanding is this: can you identify the exact observation that activated the threshold, calculate its additional contribution, and distinguish that direct contribution from the variance carried into later states? If so, the model is no longer a black-box “bad news” story.

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

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

omega>0; alpha,beta>=0; alpha+gamma>=0; alpha+beta+gamma/2<1 assumes symmetric standardized innovations. Explicit seed; 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.

GJR-GARCH — calculation-flow

GJR-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

omega>0; alpha,beta>=0; alpha+gamma>=0; alpha+beta+gamma/2<1 assumes symmetric standardized innovations. Explicit seed; 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.

gjr_garch.ts
/** Standalone D10-F03-A04 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,"gjr_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-A04",title:"GJR-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.