The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryOHLCOHLC is a four-field summary containing the open, high, low, and close prices for a declared interval and eligibility policy.
- PrerequisiteTradeA trade is an executed transaction in which a quantity of a financial instrument changes hands at agreed terms.
- ImportantAdjusted PriceAdjusted price is a transformed price expressed on a declared comparison basis after applying specified corporate-action or other adjustment factors.
- ImportantLast Traded PriceLast traded price is the price of the most recent trade that qualifies under a stated source and eligibility policy.
- ImportantMissing ValueA missing value indicates that the expected information is unavailable, unobserved, inapplicable, or withheld under a stated data contract.
- ImportantNull ValueA null value is an explicit data representation indicating that no ordinary value is present in a field.
- ImportantNumerical ToleranceNumerical tolerance is a declared bound used to decide when two numeric results are acceptably close for a specific comparison.
- ImportantOHLCVOHLCV is a bar record containing open, high, low, close, and trading-volume fields for one declared interval.
- ImportantPrecisionPrecision describes the fineness or number of digits with which a value or timestamp is represented or computed.
- ImportantPrevious ClosePrevious close is the governed closing price carried from the preceding applicable trading session.
- ImportantUnadjusted PriceUnadjusted price is the reported or normalized market price before retrospective corporate-action adjustment.
- ImportantValid ZeroA valid zero is an observed or derived numeric value of exactly zero that is meaningful under the field's contract.
Practical promise. Detect impossible candle geometry with explicit tick tolerance, preserve the original evidence, and avoid turning a validation alert into an unsupported accusation or silent repair.
A candle can look ordinary while carrying an impossible relationship: its reported high may sit below its open, or its low may sit above its close. If that row enters volatility, return, chart, or risk calculations, the defect becomes harder to trace. An OHLC consistency validator is a small but valuable ingestion control because its decision can be reproduced from one row.
The control is deliberately narrow. It decides whether a bar satisfies a declared structural contract. It does not decide that a vendor made an error, reconstruct a replacement value, or certify that a passing bar is economically correct.
What the source fields mean
The current NYSE Closing Prices specification describes a daily product containing Open, High, Low, Last, and Total Volume. For that product, Open is the first qualifying trade price, High is the highest trade price, Low is the lowest, and Last is the final trade price. It also states that price fields are expressed in dollars to penny precision. Those definitions provide authoritative field context, but they do not create a universal repair algorithm.
Before validating, pin the actual contract for your source:
- instrument and venue scope;
- interval and session;
- currency and stored scale;
- adjusted or unadjusted basis;
- qualifying-trade and correction policy;
- tick size effective at the bar's time; and
- schema and source version.
Comparing fields with different bases can manufacture an alert even when each source value was valid in its own context.
The three invariants
Let , , , and be normalized open, high, low, and close prices. Let be tick size and be the allowed tolerance in ticks. Convert the business tolerance once:
Then require all three relationships:
The first rule prevents an inverted range. The second makes high contain both body endpoints. The third makes low contain both endpoints. Equality passes.
| Symbol | Meaning | Unit |
|---|---|---|
| normalized bar prices | declared price unit | |
| tick size | price units per tick | |
| allowed tolerance | ticks | |
| comparison tolerance | price units |
Do not substitute an unexplained percentage. A one-tick allowance communicates a concrete source convention; a percentage changes with price and may conceal a parsing defect.
Boundary arithmetic you can check by hand
Set and , giving . Consider:
| Field | Value |
|---|---|
| Open | 100.01 |
| High | 100.00 |
| Low | 99.50 |
| Close | 99.90 |
The high misses the body maximum by exactly . Because equality is allowed, the bar passes. If only Open changes to , the gap becomes , which is beyond one tick and produces HIGH_BELOW_BODY.
This distinction is worth testing. Decimal prices such as 100.01 are not represented exactly in binary floating point. The implementations therefore add a tiny machine-roundoff guard—eight floating-point epsilons times the largest price magnitude. It prevents representation noise from flipping the equality case; it is not a hidden business tolerance.
One row can fail three ways
Now use zero tolerance with , , , and .
The range is inverted because . High is below the body because . Low is above the body because . The useful output is the complete ordered list:
HIGH_BELOW_LOW
HIGH_BELOW_BODY
LOW_ABOVE_BODY
Stopping after the first issue throws away diagnostic evidence. Changing High to 103 or Low to 101 may produce valid geometry, but either change invents a value unless a downstream repair process has stronger source evidence.
Normalize scale before comparing
Some feeds store 100.01 as integer 10001. The package makes that convention explicit with price_scale=0.01. It multiplies all four prices by the same positive scale, then calculates the gaps in normalized price units. Tick size must use those same units.
The scale parameter is not a universal decoder. If one field is in cents and another is already in dollars, a single row-level scale cannot repair the schema mismatch. Reject or normalize the source record earlier, while preserving the raw payload.
Field failures are different from geometry failures
The reference implementations accept finite numeric prices only. Missing values, booleans, numeric strings, NaN, and infinities produce field-specific issues. Geometry is evaluated only when all four prices are valid. A non-empty timestamp must be ISO 8601 with Z or an explicit UTC offset. Optional volume may be zero but cannot be nonfinite or negative.
This ordering avoids a misleading result. If high="100" arrives as a string, the validator reports INVALID_HIGH; it does not coerce the value and pretend the source contract was satisfied.
The separation at the end is the most important operational rule: detection and repair are distinct stages.
The output is an audit record
Each result includes the input index, timestamp, validity flag, ordered issues, effective tolerance in price units, normalized prices when available, selected provenance, and a shallow copy of the raw row. The detector never edits the input.
In production, attach more lineage than the teaching implementation can assume: source file or message ID, ingestion timestamp, schema version, calendar version, adjustment version, and validator version. Quarantine can then preserve evidence while preventing propagation.
Why a failure is not automatically a provider error
An inconsistent row is evidence that the row and your declared contract disagree. Possible causes include:
- wrong decimal scale;
- a missing marker parsed as zero;
- mixed adjusted and unadjusted fields;
- fields aggregated from different venue scopes;
- an interval or timezone join error;
- a stale partial update; or
- an upstream publication defect.
The last explanation is only one candidate. Confirming it requires a reproducible source sample plus authoritative correction evidence, such as an official correction file, notice, or documented version change.
Historical-case decision: valuable, but not fabricated
A real historical case would make the operational value obvious if it included a dated original row, an official correction artifact, corrected values, and a measurable downstream effect. This review did not establish that complete evidence chain for a named OHLC bar. Therefore, this article does not publish a company, date, or alleged vendor error.
If two providers disagree, record the discrepancy as a research lead. Do not write “provider X was wrong” until the field bases match and an authoritative record confirms the correction. The fixtures and playground in this package are labeled synthetic for exactly this reason.
Use the playground as a validator console
Open the interactive OHLC validator. It begins with an informative first bar and provides three 36-row scenarios:
- Canonical mix: clean controls plus high, low, inverted-range, and volume failures;
- One-tick boundary: exact-tolerance rows and just-beyond-tolerance rows; and
- Malformed input: missing, string, nonfinite, and invalid timestamp cases.
Change tolerance in ticks, then use Back, Step, Play, Pause, and Reset. The diagram, current rule, issue list, raw row, provenance, and audit window recompute together. Reset is deterministic. Reduced-motion users retain the full step sequence without automatic animation.
Implementation and parity
The Python and TypeScript implementations follow the same order:
- validate configuration;
- copy raw input and provenance;
- validate timestamp, price, and optional volume fields;
- normalize price scale;
- evaluate every geometric gap; and
- emit one immutable diagnostic result.
The shared fixture fixes the equality policy, issue ordering, malformed input behavior, and provenance output. Separate tests cover scaled integer prices, invalid configuration, nonfinite values, and the 120-bar synthetic corpus.
Passing those tests proves agreement with this package definition. It does not prove that the configured tick size is point-in-time correct, that a passing source row represents every qualifying trade, or that any downstream trading decision is useful.
Practical failure modes
- Overbroad tolerance: hides a repeatable rounding or parsing problem.
- Wrong tick table: treats an instrument's current increment as historical truth.
- Silent coercion: turns a schema violation into plausible geometry.
- Automatic repair: destroys the raw evidence needed to identify the cause.
- Passing equals correct: overlooks stale, incomplete, adjusted-basis, or wrong-session bars.
- Provider disagreement equals proof: skips the official correction and provenance check.
The validator should run early, but it is only one control. Follow it with timestamp ordering, duplicate and gap detection, session membership, corporate-action basis checks, source reconciliation, and local outlier detection.
Summary
An OHLC consistency validator is valuable because its boundary is explicit and auditable. Normalize the declared scale, convert tick tolerance to price units, report every field and geometry issue, and preserve the original row. Treat the result as a detection—not a repair and not a verdict about a provider.
Continue with Hampel Bad-Tick Filter to detect observations that are internally possible but locally implausible.
Primary references
- NYSE Closing Prices Client Specification v2.2 — field definitions, price units, and product version context.
- NYSE Daily TAQ Client Specification v4.3 — lower-level timestamp, blank-field, symbol, sequence, and provenance context.
- SEC Final Data Quality Assurance Guidelines — reproducibility and correction-evidence discipline; not an OHLC methodology.
Rendered from the canonical Mermaid sources linked by this article.
Detection, investigation, and repair flow
This flow shows why an invariant failure is a detection result rather than an automatic provider-error verdict.
Takeaway: preserve every issue and the raw record; repair only after a separate evidence and governance decision.
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.
Accessed 2026-07-22. No licensed market payload is redistributed; package datasets are synthetic.
NYSE-CLOSE-2.2 — Closing Prices Client Specification
- Organization or authors: New York Stock Exchange / Intercontinental Exchange
- Source type: Official exchange product specification
- Publication or effective date: 2025-04-10
- Version: 2.2
- URL or DOI: https://www.nyse.com/publicdocs/nyse/data/TAQ_Closing_Prices_Client_Spec_v2.2.pdf
- Jurisdiction: United States
- Supports: Current product context; daily Open/High/Low/Last and Total Volume fields; dollar and penny field convention; Open, High, Low, and Last descriptions; file/version provenance.
- Limitations: Product-specific daily definitions. It does not prescribe this package's tolerance, error classification, or repair policy and does not prove that a detected discrepancy is a provider error.
NYSE-DTAQ-4.3 — Daily TAQ Client Specification
- Organization or authors: New York Stock Exchange / Intercontinental Exchange
- Source type: Official exchange historical market-data specification
- Publication or effective date: 2026-03-03
- Version: 4.3
- URL or DOI: https://www.nyse.com/publicdocs/nyse/data/Daily_TAQ_Client_Spec_v4.3.pdf
- Jurisdiction: United States
- Supports: Current file-format, timestamp-history, blank-field, symbol, sequence, and event provenance context needed to reconstruct bars from lower-level records.
- Limitations: Licensed product specification; no payload is reproduced. It does not define a universal OHLC validator or authorize inferring provider fault from a structural failure.
SEC-DQA — Final Data Quality Assurance Guidelines
- Organization or authors: U.S. Securities and Exchange Commission
- Source type: Official regulator data-quality policy
- Publication or effective date: 2016-12-16
- Version: Final guidelines
- URL or DOI: https://www.sec.gov/data-research/final-data-quality-assurance-guidelines
- Jurisdiction: United States federal agency publications
- Supports: Reproducibility, supporting-data, correction-request, factual-basis, and documentation principles used here as an evidence discipline.
- Limitations: Governs SEC-disseminated information; it is not a market-data vendor correction protocol and does not define OHLC mathematics.
Evidence classification
- Sourced fact: the NYSE product fields, field descriptions, version dates, timestamp and blank-field conventions.
- Package definition: the three inequalities, issue codes, tick-based tolerance, price-scale behavior, and issue ordering.
- Implementation choice: strict numeric input, eight-epsilon arithmetic guard, selected provenance fields, and detection-only output.
- Synthetic demonstration: all numerical bars in fixtures, article examples, SVGs, and playground.
- Not established: any named historical bad bar or confirmed provider error.
Full dependency-light reference implementations in both supported languages.
/** Detection-only OHLC consistency validation with explicit tick tolerance. */
export type Bar = {
timestamp?: unknown;
open?: unknown;
high?: unknown;
low?: unknown;
close?: unknown;
volume?: unknown;
source?: unknown;
symbol?: unknown;
bar_id?: unknown;
[key: string]: unknown;
};
export type ValidationConfig = {
tickSize?: number;
toleranceTicks?: number;
priceScale?: number;
};
export type ValidationResult = {
index: number;
timestamp: string;
valid: boolean;
issues: string[];
tolerancePriceUnits: number;
normalizedPrices: Record<"open" | "high" | "low" | "close", number> | null;
provenance: Record<string, unknown>;
rawBar: Bar;
};
function finiteNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
function resolvedConfig(config: ValidationConfig) {
const tickSize = config.tickSize ?? 0.01;
const toleranceTicks = config.toleranceTicks ?? 0;
const priceScale = config.priceScale ?? 1;
if (!finiteNumber(tickSize) || tickSize <= 0) throw new Error("tickSize must be a finite positive number");
if (!finiteNumber(toleranceTicks) || toleranceTicks < 0) throw new Error("toleranceTicks must be a finite nonnegative number");
if (!finiteNumber(priceScale) || priceScale <= 0) throw new Error("priceScale must be a finite positive number");
return {tickSize, toleranceTicks, priceScale, tolerancePriceUnits: tickSize * toleranceTicks};
}
function timestampIssue(value: unknown): {timestamp: string; issue?: string} {
if (typeof value !== "string" || value.trim() === "") return {timestamp: "", issue: "MISSING_TIMESTAMP"};
const timestamp = value.trim();
const hasOffset = /(Z|[+-]\d{2}:\d{2})$/i.test(timestamp);
if (!hasOffset || !Number.isFinite(Date.parse(timestamp))) return {timestamp, issue: "INVALID_TIMESTAMP"};
return {timestamp};
}
function exceeds(gap: number, tolerance: number, values: number[]): boolean {
const magnitude = Math.max(1, ...values.map(Math.abs));
const arithmeticSlack = 8 * Number.EPSILON * magnitude;
return gap > tolerance + arithmeticSlack;
}
export function validateBars(bars: Bar[], config: ValidationConfig = {}): ValidationResult[] {
const {tickSize, toleranceTicks, priceScale, tolerancePriceUnits} = resolvedConfig(config);
void tickSize;
void toleranceTicks;
return bars.map((bar, index) => {
const issues: string[] = [];
const ts = timestampIssue(bar.timestamp);
if (ts.issue) issues.push(ts.issue);
const prices: Partial<Record<"open" | "high" | "low" | "close", number>> = {};
for (const field of ["open", "high", "low", "close"] as const) {
const raw = bar[field];
if (!finiteNumber(raw)) issues.push(`INVALID_${field.toUpperCase()}`);
else prices[field] = raw * priceScale;
}
let normalizedPrices: ValidationResult["normalizedPrices"] = null;
if (Object.keys(prices).length === 4) {
normalizedPrices = prices as ValidationResult["normalizedPrices"];
const {open, high, low, close} = normalizedPrices!;
const values = [open, high, low, close];
if (exceeds(low - high, tolerancePriceUnits, values)) issues.push("HIGH_BELOW_LOW");
if (exceeds(Math.max(open, close) - high, tolerancePriceUnits, values)) issues.push("HIGH_BELOW_BODY");
if (exceeds(low - Math.min(open, close), tolerancePriceUnits, values)) issues.push("LOW_ABOVE_BODY");
}
if (bar.volume !== undefined && bar.volume !== null) {
if (!finiteNumber(bar.volume)) issues.push("INVALID_VOLUME");
else if (bar.volume < 0) issues.push("NEGATIVE_VOLUME");
}
const provenance: Record<string, unknown> = {};
for (const key of ["source", "symbol", "bar_id"] as const) {
if (key in bar) provenance[key] = bar[key];
}
return {
index,
timestamp: ts.timestamp,
valid: issues.length === 0,
issues,
tolerancePriceUnits,
normalizedPrices,
provenance,
rawBar: {...bar},
};
});
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.