Library/Volatility and Covariance/Realized Measures/Jump-Variation Detector

D10-F02-A04 / Released engineering topic

A jump-variation detector with an honest uncertainty boundary

Separate the observed variation difference from its uncertainty and threshold.

Separate the observed variation difference from its uncertainty and threshold.D10 / D10-F02

For analysts and developers who know decimal interval returns, regular sampling grids, and variance versus volatility. 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.

“Realized variance exceeded bipower variation” is an observation. “A statistically significant jump occurred” is an inference that needs a calibrated statistic. Those statements should never be treated as synonyms.

This package implements a specific educational version of the Barndorff–Nielsen–Shephard linear jump test: bipower variation for the continuous component, tripower quarticity for uncertainty, and a one-sided upper-tail threshold. It does not claim exact small-sample error rates or identify the economic cause of a flagged move.

Calculated behavior: Separate the observed variation difference from its uncertainty and threshold. Actual synthetic reference outputs.

Open this figure at full size.

Start with the quantity being standardized

Compute RV=Σr_i² and BV=(π/2)Σ|r_i r_(i−1)| on the same n-return window. The numerator is the signed difference RV−BV. Do not clip it before standardizing.

The linear statistic is

Z=RVBV(π2/4+π5)TQ/n.Z=\frac{RV-BV}{\sqrt{(\pi^2/4+\pi-5)\,TQ/n}}.

The BNS test reference describes this linear structure and the role of an integrated-quarticity estimator. We freeze one combination rather than silently switching among linear, ratio, log-transformed or adjusted variants.

The denominator is not √(2BV²/n). It uses TQ, a separately calculated estimate with return-to-the-fourth units. Omitting that distinction can produce a plausible-looking but unsupported significance score.

Where tripower quarticity comes from

Let μ_(4/3)=E|N(0,1)|^(4/3), approximately 0.830860925. Our finite-sample convention is

TQ=n2n2μ4/33i=3nriri1ri24/3.TQ=\frac{n^2}{n-2}\,\mu_{4/3}^{-3} \sum_{i=3}^{n}|r_i r_{i-1}r_{i-2}|^{4/3}.

The tripower quarticity documentation gives this normalization. At least three returns are required to form a triple product. That minimum does not make a three-return significance test reliable.

Units provide a powerful check: TQ has fourth-power return units, its square root has variance units, and Z is dimensionless. If returns are multiplied by c, RV−BV scales by c² and TQ by c⁴, leaving Z unchanged for positive finite scaling.

A numerical example that does not flag a jump

For [0.01,−0.02,0.03,−0.01], RV=0.0015 and BV≈0.001727876. The numerator is negative. Both absolute triple products equal 0.000006 before raising to 4/3.

With n=4, TQ is 8×2×0.000006^(4/3)/μ_(4/3)³. The fixture evaluates that expression independently and checks the resulting statistic in both languages. Because the standard error is positive and the numerator negative, Z is below any positive upper-tail critical value. The example teaches the calculation; four returns do not validate the asymptotic approximation.

Independent numeric checkpoint for Jump-Variation Detector

Open this figure at full size.

Download the exact worked input and expected values.

Alpha is a tail probability here

The package's alpha is the upper-tail significance level. At alpha=0.05, the critical value is Φ⁻¹(0.95)≈1.644853627, not 1.959963985. The decision is Z>critical_value. At alpha=0.01, use Φ⁻¹(0.99)≈2.326347874.

Some software names a confidence-quantile argument alpha; the cited R interface uses a different convention. Never copy a parameter name without checking its mathematical meaning. Our supported range is 0.000001≤alpha<0.5, and the inverse-normal approximation is checked against an independent scientific library.

StateOutputCorrect interpretation
Positive finite TQ, Z above thresholdjump_detected=trueEvidence under the declared asymptotic model
Positive finite TQ, Z below thresholdfalseThis test did not reject; absence is not proved
Zero TQNull statistic and null decisionUncertainty estimate is unavailable
Invalid input or alphaErrorNo statistical statement was made

Comparison of Jump-Variation Detector conventions, outcomes and limitations.

Open this figure at full size.

Use the lab to separate data from the decision rule

The synthetic 64-return path supports a clean control, an injected-shock comparison, and a degenerate zero-path boundary. Step changes the observations available to the estimator. Changing alpha changes the threshold, not RV, BV or TQ.

Predict that separation before moving the alpha control. Then inspect the zero-quarticity case: the lab must say the decision is withheld, not declare “no jump” using an arbitrary small denominator. The diagnostics expose all components so a boolean cannot hide its basis.

Four calculation stages: Compute RV and unadjusted BV; Compute tripower quarticity; Standardize signed RV minus BV; Compare Z with upper-tail quantile

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.

Why a flag still needs investigation

The asymptotic argument presumes a suitable price process and sufficiently informative sampling. Bid–ask bounce, recording errors, stale prices, irregular grids and a short window can undermine the approximation. The package does not claim that the nominal 5% level is the realized false-positive rate for your dataset.

Repeated overlapping windows create another problem: a per-test alpha is not a whole-dashboard false-alert guarantee. A research or surveillance system must define its testing family, dependence assumptions and multiplicity treatment. A single jump may also appear in many overlapping windows; do not count those flags as independent events.

A useful alert retains the raw observations, sampling rule, RV, BV, TQ, statistic, threshold and data-quality flags. Investigate corporate actions and feed anomalies before assigning an economic explanation. The detector is a reproducible evidence-producing component, not a trading signal or a proof of a particular market event.

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

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

One-sided linear BNS asymptotic test; alpha is upper-tail probability, not the R confidence-quantile argument. TQ=w²/(w-2) sum|r_i r_(i-1) r_(i-2)|^(4/3)/mu_(4/3)^3. Zero TQ withholds the decision.

Continue the investigation

  • Realized Variance: compare its assumptions and information boundary before comparing the numbers.
  • Realized Kernel: compare its assumptions and information boundary before comparing the numbers.

Jump-Variation Detector — calculation-flow

Jump-Variation Detector — 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: highfrequency — BNSjumpTest estimator documentation — 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: highfrequency — rTPQuar estimator documentation — 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

One-sided linear BNS asymptotic test; alpha is upper-tail probability, not the R confidence-quantile argument. TQ=w²/(w-2) sum|r_i r_(i-1) r_(i-2)|^(4/3)/mu_(4/3)^3. Zero TQ withholds the decision.

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.

jump_variation_detector.ts
/** Standalone D10-F02-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 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 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 normalQuantile(probability: number): number {
  const a = [-39.69683028665376, 220.9460984245205, -275.9285104469687, 138.3577518672690, -30.66479806614716, 2.506628277459239];
  const b = [-54.47609879822406, 161.5858368580409, -155.6989798598866, 66.80131188771972, -13.28068155288572];
  const c = [-.007784894002430293, -.3223964580411365, -2.400758277161838, -2.549732539343734, 4.374664141464968, 2.938163982698783];
  const d = [.007784695709041462, .3224671290700398, 2.445134137142996, 3.754408661907416];
  const poly = (co: number[], x: number) => co.slice(1).reduce((v, coefficient) => v * x + coefficient, co[0]);
  if (probability < .02425 || probability > .97575) {
    const q = Math.sqrt(-2 * Math.log(Math.min(probability, 1 - probability)));
    const value = poly(c, q) / (poly(d, q) * q + 1);
    return probability < .5 ? value : -value;
  }
  const q = probability - .5;
  return poly(a, q * q) * q / (poly(b, q * q) * q * q + 1);
}

function realized(data: RecordValue, p: RecordValue, kind: string): RecordValue {
  const key = kind === 'realized_covariance' ? 'returns_x' : 'returns', r = vector(data[key], key);
  const w = integer(option(p, 'window', 24), 'window', kind === 'jump_detector' ? 3 : 2), scale = positive(p, 'annualization_factor', 1);
  let y: number[] = [];
  if (kind === 'realized_covariance') {
    y = vector(data.returns_y, 'returns_y');
    const tx = timestamps(data.timestamps_x, r.length, 'timestamps_x', true), ty = timestamps(data.timestamps_y, y.length, 'timestamps_y', true);
    requireValue(tx.length === ty.length && tx.every((v, i) => v === ty[i]), 'ALIGNMENT', 'x/y intervals must match exactly');
  } else if (Object.hasOwn(data, 'timestamps')) timestamps(data.timestamps, r.length, 'timestamps', true);
  const alpha = kind === 'jump_detector' ? param(p, 'alpha', .05) : null;
  if (alpha !== null) requireValue(alpha >= 1e-6 && alpha < .5, 'RANGE', 'alpha must be in [0.000001, 0.5)');
  const bandwidth = kind === 'realized_kernel' ? integer(option(p, 'bandwidth', Math.min(4, w - 1)), 'bandwidth', 0, w - 1) : 0;
  const series = r.map((_, i) => {
    if (i + 1 < w) return null;
    const x = r.slice(i - w + 1, i + 1), squares = x.map(v => v * v), rv = scale * sum(squares);
    const products = x.slice(1).map((v, j) => Math.abs(v * x[j])), bv = scale * Math.PI / 2 * sum(products);
    let item: RecordValue;
    if (kind === 'realized_variance') item = {variance: rv, volatility: Math.sqrt(rv), contributions: squares.map(v => scale * v)};
    else if (kind === 'realized_covariance') {
      const paired = x.map((v, j) => scale * v * y[i - w + 1 + j]);
      item = {covariance: sum(paired), contributions: paired};
    } else if (kind === 'bipower_variation') item = {realized_variance: rv, bipower_variation: bv, signed_difference: rv - bv, jump_variation: Math.max(rv - bv, 0), contributions: products.map(v => scale * Math.PI / 2 * v)};
    else if (kind === 'jump_detector') {
      const mu43 = .8308609250295592;
      const tq = scale ** 2 * w * w / (w - 2) * sum(x.slice(2).map((v, j) => Math.abs(v * x[j + 1] * x[j]) ** (4 / 3))) / mu43 ** 3;
      const se = Math.sqrt((Math.PI ** 2 / 4 + Math.PI - 5) * tq / w), z = se > 0 ? (rv - bv) / se : null;
      const q = normalQuantile(1 - alpha!);
      item = {realized_variance: rv, bipower_variation: bv, tripower_quarticity: tq, standard_error: se, statistic: z,
        critical_value: q, jump_detected: z === null ? null : z > q, decision_status: z === null ? 'withheld-zero-quarticity' : 'asymptotic', signed_difference: rv - bv};
    } else {
      const lags = Array.from({length: bandwidth + 1}, (_, h) => {
        const gamma = sum(x.slice(h).map((v, j) => v * x[j])), weight = 1 - h / (bandwidth + 1);
        return {lag: h, weight, gamma, contribution: scale * (h === 0 ? 1 : 2) * weight * gamma};
      });
      const value = sum(lags.map(v => v.contribution));
      requireValue(value >= -1e-12 * Math.max(rv, 1e-300), 'NUMERIC', 'Bartlett quadratic form became negative');
      item = {realized_kernel: Math.max(value, 0), volatility: Math.sqrt(Math.max(value, 0)), bandwidth, lags};
    }
    return {...item, window_start: i - w + 1, window_end: i};
  });
  return seriesResult(series, {causal: true, input_count: r.length, annualization_factor: scale});
}
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=realized(data,p,"jump_detector");
  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-F02-A04",title:"Jump-Variation Detector",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.