Library/Volatility and Covariance/Historical Estimators/Yang-Zhang Volatility

D10-F01-A05 / Released engineering topic

Yang–Zhang volatility: show where overnight and intraday variation enter

Separate overnight, weighted body and weighted range contributions.

Separate overnight, weighted body and weighted range contributions.D10 / D10-F01

For analysts and developers who know decimal log returns, sample variance, and consistently adjusted OHLC bars. 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.

If an asset gaps at the open and then trades quietly, an intraday range estimator can look calm while a close-based estimate looks agitated. Yang–Zhang gives those two parts of the day separate places in the calculation.

Its most useful output is not just another volatility number. It is an explanation: how much came from overnight dispersion, how much from open-to-close dispersion, and how much from within-session range geometry?

You will assemble that decomposition, handle its extra price dependency, and learn why changing the window also changes a coefficient. Review sample variance and Rogers–Satchell first if either is unfamiliar.

Calculated behavior: Separate overnight, weighted body and weighted range contributions. Actual synthetic reference outputs.

Open this figure at full size.

Three ingredients, with different weights

For each session, compute the overnight return o_t=ln(O_t/C_(t-1)) and open-to-close return c_t=ln(C_t/O_t). Compute the Rogers–Satchell contribution from that session's OHLC prices too.

Across w sessions, calculate sample variances s_o² and s_c² using denominator w−1, plus the arithmetic mean of the RS contributions using denominator w. Combine them as

v=A[so2+ksc2+(1k)RS],k=0.341.34+(w+1)/(w1).v=A\left[s_o^2+k s_c^2+(1-k)\overline{RS}\right],\qquad k=\frac{0.34}{1.34+(w+1)/(w-1)}.

The Yang–Zhang publication is the source of the estimator; the TTR reference makes this implementation convention explicit. The estimate uses separate components under model assumptions, not an arbitrary equal-weight average of three volatility figures.

Do not take square roots of the components before adding them. Variance components are combined first. Volatility is the square root of the final variance.

A small example with an auditable decomposition

Use three synthetic bars whose close log-price offsets are 0,0.01 and−0.01. In each bar, the open is 0.005 log units below its close; high is 0.010 above close and low 0.015 below close.

For the two sessions with a previous close, overnight returns are +0.005 and−0.025. Their mean is −0.01, deviations are±0.015, and sample variance is 0.00045. Both open-to-close returns equal 0.005, so their sample variance is zero. Each RS contribution is 0.0003.

At w=2, k=0.34/4.34≈0.0783410. With A=1, the total is 0.00045 + 0 + (1−0.0783410)×0.0003, approximately 0.0007264977. Volatility is approximately 2.69536%. The range component shown in the decomposition is already weighted: approximately 0.0002764977.

The constant intraday gain did not disappear from the data. Its centered dispersion is zero. That distinction prevents a common mistake: replacing s_c² with the mean of squared intraday returns and calling it the same estimator.

Independent numeric checkpoint for Yang-Zhang Volatility

Open this figure at full size.

Download the exact worked input and expected values.

Why w sessions require w+1 bars

Every overnight return needs a previous close, including the first session inside the window. For w=2, the first two bars alone are insufficient; the first lacks a previous close in the supplied dataset. At zero-based index 2, three bars provide the two complete sessions.

The implementation withholds output until this dependency is satisfied. It never substitutes the current close for a missing previous close. That shortcut creates a fictitious overnight return and can make the initial chart look deceptively smooth.

The prior close must be the appropriate previous session close under the stated calendar. A timestamp ordering check does not establish that an omitted trading day is a holiday. Calendar and completeness checks remain an upstream responsibility.

Read the components before comparing totals

Change in the inputComponent to inspectQuestion to ask
Larger opening gapsOvernight sample varianceWere gaps genuine or adjustment artifacts?
More variable session bodiesWeighted open-to-close varianceDid body dispersion change, not just its mean?
Wider within-session excursionsWeighted RS meanWere extrema measured consistently?
Different window lengthAll three, plus kAre you comparing the same information set?

The returned component values sum to the total variance: overnight + weighted open-close + weighted range. k itself is a coefficient, not a fourth variance contribution. This convention makes the displayed accounting directly checkable.

Comparison of Yang-Zhang Volatility conventions, outcomes and limitations.

Open this figure at full size.

The playground should explain a disagreement

Step through the 64-bar synthetic path with the decomposition visible. Change the window and check k as well as the included observations. A new window is not merely the same formula applied to a different number of rows: its finite-window weight changes too.

At the warm-up boundary, confirm that the estimate appears only when the previous-close dependency is available. Then compare the flat-path edge case and an invalid OHLC input. Unavailable, zero and invalid are different states and should be labeled differently.

Use close-to-close volatility as a companion. Both can respond to opening gaps, but only this decomposition tells you which overnight and within-session terms produced its estimate.

Four calculation stages: Previous close → opening return; Sample overnight/body variances; Compute w-dependent k and RS mean; Add weighted variance components

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.

The adjustment-basis trap

Mixing adjusted previous closes with unadjusted opens creates artificial overnight returns. A split can then look like an enormous risk event, even though every individual price is positive and every candle is correctly bracketed. Validate cross-session consistency, not just within-bar geometry.

Define the opening auction, session boundaries, timezone, and any extended-hours inclusion. A 24-hour market's chosen daily boundary is still a convention. Calling a component “overnight” does not make that convention universal across assets.

The package uses synthetic observations, supplied prices and no market-calendar inference. It rejects malformed timestamps and missing/nonfinite numeric inputs rather than filling them. Its direct rolling calculation is intentionally inspectable; an optimized implementation should preserve the same decomposition and warm-up behavior.

Where the extra complexity earns its place

Use Yang–Zhang when explaining the division between opening and within-session variation matters to your risk question. It is not automatically the best estimator for every feed or horizon. Four reliable prices per session and a consistent previous close are a stronger data requirement than a close-only series.

The practical payoff is accountability: when the headline number moves, you can identify the component, the observations, and the weighting responsible. That is more useful than presenting a sophisticated estimator name with no explanation of what changed.

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

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

w+1 bars; both variances use w-1, RS uses w; independent overnight/intraday components are not equally weighted.

Continue the investigation

Yang-Zhang Volatility — calculation-flow

Yang-Zhang Volatility — 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: TTR — volatility: close and range estimator conventions — 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.
  • S2: Yang and Zhang — Drift-Independent Volatility Estimation (2000) — accessed 2026-09-10. Original research publication; not a current market observation. Supports the definition and declared convention, not investment performance. Jurisdiction: not applicable to this mathematical reference.

Access limitation: the original publication metadata/abstract was available, not its full text. The exact implemented coefficient and denominator convention were checked against the accessible TTR documentation; no full-paper review is claimed.

Scope of evidence

w+1 bars; both variances use w-1, RS uses w; independent overnight/intraday components are not equally weighted.

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.

yang_zhang_volatility.ts
/** Standalone D10-F01-A05 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 timestamps(values: unknown, n: number, name: string, regular = false): number[] {
  requireValue(Array.isArray(values) && values.length === n, 'TIME', `${name} must match observation count`);
  const result: number[] = [];
  for (const value of values) {
    requireValue(typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value), 'TIME', 'use UTC ISO timestamps');
    const instant = Date.parse(value);
    const canonical = value.length === 20 ? value.replace('Z', '.000Z') : value;
    requireValue(Number.isFinite(instant) && Number(value.slice(0, 4)) >= 1000 && new Date(instant).toISOString() === canonical, 'TIME', 'invalid calendar timestamp');
    requireValue(!result.length || instant > result[result.length - 1], 'TIME', 'timestamps must strictly increase');
    result.push(instant);
  }
  if (regular && n > 2) requireValue(result.slice(2).every((v, i) => v - result[i + 1] === result[1] - result[0]), 'ALIGNMENT', 'returns need a regular grid');
  return result;
}

function bars(data: RecordValue): any[][] {
  const values = data.bars;
  requireValue(Array.isArray(values) && values.length >= 2 && values.every(b => b && typeof b === 'object' && !Array.isArray(b)), 'SHAPE', 'bars needs at least two OHLC objects');
  timestamps(values.map(b => b.timestamp), values.length, 'bars.timestamp');
  return values.map(b => {
    const [o, h, l, c] = ['open', 'high', 'low', 'close'].map(k => finite(b[k], k));
    requireValue(Math.min(o, h, l, c) > 0 && h >= Math.max(o, c) && l <= Math.min(o, c), 'OHLC', 'positive bracketed OHLC required');
    return [b.timestamp, o, h, l, c];
  });
}

function variance(x: number[]): number {
  const mean = sum(x) / x.length;
  return sum(x.map(v => (v - mean) ** 2)) / (x.length - 1);
}

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 historical(data: RecordValue, p: RecordValue, kind: string): RecordValue {
  const values = bars(data), w = integer(option(p, 'window', 12), 'window', 2), scale = positive(p, 'annualization_factor', 252);
  const contributions = values.map(([, o, h, l, c], i) => {
    const rs = Math.log(h / c) * Math.log(h / o) + Math.log(l / c) * Math.log(l / o);
    const choices: RecordValue = {
      close_to_close: i === 0 ? null : Math.log(c / values[i - 1][4]),
      parkinson: Math.log(h / l) ** 2 / (4 * Math.log(2)),
      garman_klass: .5 * Math.log(h / l) ** 2 - (2 * Math.log(2) - 1) * Math.log(c / o) ** 2,
      rogers_satchell: rs,
      yang_zhang: [i === 0 ? null : Math.log(o / values[i - 1][4]), Math.log(c / o), rs],
    };
    return choices[kind];
  });
  const series = values.map((bar, i) => {
    const block = contributions.slice(Math.max(0, i - w + 1), i + 1);
    if (block.length < w || block.some(v => v === null) || (kind === 'yang_zhang' && block[0][0] === null)) return null;
    let estimate: number, components: RecordValue = {};
    if (kind === 'yang_zhang') {
      const overnight = variance(block.map(v => v[0])), intraday = variance(block.map(v => v[1]));
      const rs = sum(block.map(v => v[2])) / w, k = .34 / (1.34 + (w + 1) / (w - 1));
      estimate = scale * (overnight + k * intraday + (1 - k) * rs);
      components = {overnight: scale * overnight, open_close: scale * k * intraday, range: scale * (1 - k) * rs, k};
    } else estimate = scale * (kind === 'close_to_close' ? variance(block) : sum(block) / w);
    requireValue(estimate >= 0, 'NUMERIC', 'negative variance: inspect inputs and precision');
    return {timestamp: bar[0], variance: estimate, volatility: Math.sqrt(estimate), contributions: block, components, window_start: i - w + 1, window_end: i};
  });
  return seriesResult(series, {input_count: values.length, annualization_factor: scale, causal: true});
}
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=historical(data,p,"yang_zhang");
  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-F01-A05",title:"Yang-Zhang Volatility",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.