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.
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
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
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.
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.
| State | Output | Correct interpretation |
|---|---|---|
| Positive finite TQ, Z above threshold | jump_detected=true | Evidence under the declared asymptotic model |
| Positive finite TQ, Z below threshold | false | This test did not reject; absence is not proved |
| Zero TQ | Null statistic and null decision | Uncertainty estimate is unavailable |
| Invalid input or alpha | Error | No statistical statement was made |
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.
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.
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"])
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.
Rendered from the canonical Mermaid sources linked by this article.
Jump-Variation Detector — calculation-flow
Jump-Variation Detector — 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: 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.
Full dependency-light reference implementations in both supported languages.
/** 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};
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.
