The governed definitions this build depends on. Read them first if a term is unfamiliar.
- PrimaryInterpolationInterpolation estimates a value between known observations according to a declared mathematical rule.
- PrerequisiteMissing ValueA missing value indicates that the expected information is unavailable, unobserved, inapplicable, or withheld under a stated data contract.
- PrerequisiteQuoteA quote is a market expression of available buying or selling interest at stated prices and, when supplied, quantities.
- ImportantAvailability TimeAvailability time is the earliest evidenced instant at which information was usable by a specified system or decision process.
- ImportantCausal Knowledge CutoffThe time boundary that prevents information unavailable at that moment from influencing a calculation.
- ImportantEvent TimeEvent time is the timestamp assigned to when a market or business event occurred at its source.
- ImportantImputationImputation is the governed estimation or substitution of values where observations are missing.
- ImportantObservation TimeObservation time is the timestamp used to place a measured value on a time axis.
Practical promise. You will be able to reconstruct an interior bid/ask pair, state exactly when it first becomes computable, and prevent that retrospective result from leaking into a point-in-time decision.
A smooth synchronized chart is useful for retrospective inspection. It becomes misleading when the line at 10:00:01 quietly depends on a quote event from 10:00:02. The arithmetic may be correct and the chart may look natural, yet the value did not exist in the system's knowledge at the earlier time.
Linear quote interpolation is therefore two problems, not one:
- What straight-line bid and ask lie between two quote events?
- At what time were both endpoint records actually available?
The first question is numerical. The second is point-in-time data engineering. This article keeps them together.
All prices, symbols, venues, and timestamps in the worked examples are synthetic. They teach mechanics and do not describe a historical market event, an executable quote, or a profitable signal.
Event time is not knowledge time
An endpoint needs at least two clocks:
- Event time: when the source says the quote event occurred.
- Availability time: when the process performing this calculation could first use the record after collection and ingestion.
The SEC's Quote Life research methodology, for example, describes records with both exchange-generated and collection-point timestamps. That methodology does not prescribe this algorithm, but it is a primary-source example of why market data may need more than one time coordinate (SEC Quote Life methodology).
The query also has two times:
- Target time: the event-time coordinate whose quote we want to reconstruct.
- Evaluation time: the point-in-time cutoff. A record arriving after this cutoff is unavailable, even if it appears later in the completed dataset.
This distinction prevents hindsight from masquerading as live state.
The calculation
Let be the left bid/ask at event time , and let be the right bid/ask at . For an interior target , where , the interpolation weight is
Interpolate each side of the quote:
This is the standard linear interpolation identity (NIST DLMF, Section 3.3). Because , the derived spread is a convex combination of endpoint spreads:
If both endpoint quotes satisfy , the interpolated pair does too. That does not make the pair observable or executable. It only proves a geometric invariant.
A midpoint that is not yet knowable
Consider this synthetic bracket:
| Role | Event time | Available time | Bid | Ask |
|---|---|---|---|---|
| Left endpoint | 00:00:00.000 | 00:00:00.100 | 99 | 101 |
| Target | 00:00:01.000 | query cutoff | — | — |
| Right endpoint | 00:00:02.000 | 00:00:02.120 | 101 | 103 |
The target is halfway through the event-time bracket, so . The derived bid is 100 and the derived ask is 102.
At an evaluation cutoff of 00:00:01.000, however, the right endpoint will not
be available for another 1,120 milliseconds. The correct result is not
100/102. It is right_not_available with null prices. At 00:00:02.120 the
same arithmetic becomes computable and the status changes to linear.
Even then, the output carries observable=false. No source published the
interpolated pair at the target. The system derived it later from two retained
records.
The red knowledge boundary is the lesson: a value can belong at one event-time coordinate and become computable at a later availability-time coordinate.
The policy around the formula
Production behavior depends on boundary policies that the formula does not choose for you. This package makes the following conventions explicit:
| Case | Result |
|---|---|
| Exact quote at target | Return the source quote only after its availability time |
| No left or right endpoint | unavailable; never extrapolate |
Bracket equals max_gap_ms | Eligible; equality is inclusive |
Bracket exceeds max_gap_ms | gap_too_wide |
| Duplicate event time in one partition | Reject; zero interval is ambiguous |
| Missing or nonfinite side | Reject |
| Locked endpoint, where bid equals ask | Accept |
| Crossed endpoint, where bid exceeds ask | Reject rather than silently repair |
| Instrument, venue, or session change | Start a separate partition |
The maximum gap is not a universal exchange threshold. It is an engineering choice tied to the source, instrument, session, and downstream task. A long silent interval could contain a halt, outage, empty book, or discontinuous repricing. Drawing through it would manufacture a path.
Regulatory reporting can also have context-specific quote-time, locked-market, and crossed-market policies. As of this article's July 22, 2026 review, the cited SEC Rule 605 FAQs were forthcoming staff guidance scheduled to become effective August 1, 2026, not guidance already in effect. The page states that it represents staff views and has no legal force or effect. Its conventions are not this algorithm's policy and should not be inferred from this generic educational interpolator (forthcoming SEC Rule 605 FAQs).
An auditable algorithm
The implementation sequence is deliberately conservative:
- Validate identifiers, timestamps, finite prices, and .
- Partition by instrument, venue, and session.
- Sort by event time and reject duplicate event times.
- Find an exact quote or the nearest interior endpoints.
- Enforce no-extrapolation and maximum-gap policies.
- Compare each endpoint's availability time with the evaluation cutoff.
- Only then calculate bid and ask.
- Return value, status, reason, endpoint lineage, first-knowable time, and wait.
Doing the availability check after the arithmetic is not automatically wrong, but it makes accidental leakage easier: an already-computed number can escape through logging, caching, plotting, or downstream joins. Failing closed before calculation keeps the unsafe state visibly null.
Use the lab to cross the boundary
Open the interactive knowledge-time lab. Its opening state is already informative: the target, both event-time endpoints, and the future right endpoint's availability marker are visible.
Use Step to advance the evaluation clock. Before 2.120 seconds, the target is red and blocked. On the step where the right record arrives, the target changes to a purple derived marker and the panel states “first knowable now.” Use Back to cross that boundary in reverse. Reset always returns to the same target-time state.
The scenario selector compares three cases:
- Canonical delay: valid bracket, future endpoint not yet available.
- Gap too wide: both endpoints eventually arrive, but policy still rejects the four-second bridge.
- Locked endpoints: bid equals ask at both endpoints and remains locked under interpolation.
The lab's dashed line is a retrospective construction. It is never labeled as a stream of observed quotes.
Implementation and parity
The Python implementation and TypeScript implementation use the same input fields, statuses, and shared fixture. Their outputs preserve:
- endpoint event and availability timestamps;
first_knowable_at;wait_mswhen a record is unavailable;interpolatedversusobservable;- inclusive maximum-gap behavior;
- null prices for blocked or unavailable results.
Tests cover midpoint arithmetic, point-in-time blocking, exact-arrival equality, wide gaps, no extrapolation, missing partitions, locked and crossed states, nonfinite values, duplicate event times, clock reversal, and timestamp syntax. Test agreement proves that both implementations follow the package contract. It does not prove that interpolation is economically useful.
When linear interpolation is appropriate
It can be useful for a retrospective display, a controlled resampling study, or a sensitivity analysis where the delayed nature of the result is explicit. It can also help compare how a downstream calculation changes under different synchronization conventions.
It is not a live quote generator. It should not be used at target time in a backtest, execution benchmark, or signal unless the system deliberately delays the decision until both endpoint records are available—and even then the value is derived, not executable.
Nonsynchronous observations are not merely a plotting inconvenience. Original research on covariance estimation shows that asynchronous sampling requires methodological treatment rather than an assumed neutral synchronization step (Hayashi and Yoshida, 2005).
Why there is no historical quote-gap story here
A credible historical example needs more than two prices and exchange event times. It needs redistributable primary records, source identity, collection or availability timestamps, correction history, session state, and license terms. Without availability evidence, a completed historical file cannot prove what a reader could have known at the original target time.
For that reason, this article does not turn licensed provider data or a hindsight reconstruction into a factual market story. A future historical case should show the right endpoint absent at the original cutoff and present at a documented later collection time. Until that evidence can be published, the explicitly synthetic example is more honest and more reproducible.
Practical checklist
- Keep event time and availability time as separate fields.
- Partition before selecting endpoints.
- Reject ambiguous, crossed, missing, and nonfinite inputs.
- Set and record an inclusive maximum-gap policy.
- Never extrapolate unless a separately named model explicitly permits it.
- Emit null before both endpoints are available.
- Mark derived values as
observable=false. - Preserve endpoint lineage and the first-knowable timestamp.
- Test the same fixture in every implementation language.
- Treat a smooth chart as a visualization, not proof of a tradable quote.
Summary and next topic
Linear quote interpolation is a retrospective straight line with a delayed knowledge boundary. The arithmetic uses the left and right event-time endpoints. Point-in-time safety uses their availability times. The result first becomes computable only when both records are known, and it never becomes an observed quote merely because the calculation is valid.
Continue with Refresh-Time Sampling, which replaces single-series interpolation with a multistream clock that waits until every required stream has refreshed.
Rendered from the canonical Mermaid sources linked by this article.
Interpolation eligibility and knowledge boundary
This flow separates source-event geometry from point-in-time availability.
Takeaway: an interior value can be mathematically defined before it is knowable, and it remains derived after it becomes computable.
References5 primary 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. The interpolation policy in this package is an explicit engineering convention. No source below is presented as prescribing one universal market-data interpolation policy.
R01 — NIST Digital Library of Mathematical Functions, Section 3.3
- Organization or authors: National Institute of Standards and Technology
- Source type: Maintained official mathematical reference
- Publication or effective date: Maintained online reference; accessed version dated 2026
- Version: DLMF Section 3.3, including linear interpolation formula 3.3.14
- URL: https://dlmf.nist.gov/3.3
- Jurisdiction: General numerical methods
- Supports: Linear interpolation is a convex combination of endpoint values
- Limitations: Does not define quote validity, market observability, or a point-in-time data policy
R02 — SEC Quote Life Report Methodology
- Organization or authors: U.S. Securities and Exchange Commission, Division of Economic and Risk Analysis
- Source type: Official market-structure analytics methodology
- Publication or effective date: 2021; maintained online
- Version: Web methodology accessed 2026-07-22
- URL: https://www.sec.gov/securities-topics/market-structure-analytics/quote-life-report-methodology-market-structure
- Jurisdiction: U.S. equity-market analysis
- Supports: Market-data research may retain both an exchange-generated timestamp and a collection-point timestamp
- Limitations: Does not endorse this package's interpolation algorithm or identify
available_timeas a universal field name
R03 — Frequently Asked Questions: Rule 605 of Regulation NMS
- Organization or authors: U.S. Securities and Exchange Commission staff
- Source type: Official regulatory staff guidance
- Publication or effective date: Published April 1, 2026; includes a July 8, 2026 modification; scheduled to become effective August 1, 2026
- Version: Forthcoming staff FAQ page accessed 2026-07-22; not yet effective on the package review date
- URL: https://www.sec.gov/rules-regulations/staff-guidance/trading-markets-frequently-asked-questions/frequently-asked-questions-rule-605-regulation-nms
- Jurisdiction: U.S. Regulation NMS, Rule 605 reporting
- Supports: Time-of-receipt and quote-time conventions materially affect point-in-time benchmarking; crossed and locked quote states require explicit policy
- Limitations: As of this package's 2026-07-22 review, the FAQs were forthcoming staff guidance scheduled to become effective 2026-08-01. The page states that it represents staff views, has no legal force or effect, and does not alter applicable law. Its Rule 605 reporting conventions are not this algorithm's policy and are not a general command to interpolate quotes.
R04 — On covariance estimation of non-synchronously observed diffusion processes
- Organization or authors: Takaki Hayashi and Nakahiro Yoshida
- Source type: Original peer-reviewed methodology paper
- Publication or effective date: 2005
- Version: Bernoulli 11(2), 359–379
- DOI: https://doi.org/10.3150/bj/1116340299
- Jurisdiction: Statistical methodology
- Supports: Nonsynchronous observations are a substantive estimation problem; synchronization choice must not be treated as neutral
- Limitations: Studies covariance estimation and does not prescribe top-of-book linear interpolation
R05 — RFC 3339: Date and Time on the Internet
- Organization or authors: IETF; Graham Klyne and Chris Newman
- Source type: Standards-track timestamp specification
- Publication or effective date: July 2002
- Version: RFC 3339
- DOI: https://doi.org/10.17487/RFC3339
- Jurisdiction: Global technical standard
- Supports: Timestamp representation used by the package
- Limitations: Does not define event-time or availability-time business semantics
Evidence classification
- Sourced facts: the linear interpolation identity; the existence and value of keeping distinct source/collection timestamp layers; the importance of nonsynchronous observations.
- Package conventions: inclusive
max_gap_ms; no extrapolation; rejection of crossed inputs; acceptance of locked inputs; bid/ask interpolation as a pair; normalized-clock requirement; exact status names. - Synthetic examples: every price, timestamp, venue, and instrument in the datasets, article, tests, SVGs, and playground.
- Claims not made: that interpolated quotes were displayed or executable; that linear interpolation predicts price; or that it improves a strategy.
Full dependency-light reference implementations in both supported languages.
export interface Quote {
instrument: string;
venue: string;
session_id: string;
event_time: string;
available_time: string;
bid: number;
ask: number;
}
export interface Target {
instrument: string;
venue: string;
session_id: string;
target_time: string;
evaluation_time: string;
}
export type InterpolationStatus =
| "exact"
| "linear"
| "unavailable"
| "gap_too_wide"
| "exact_not_available"
| "left_not_available"
| "right_not_available";
export interface InterpolationResult extends Target {
bid: number | null;
ask: number | null;
status: InterpolationStatus;
reason: string;
interpolated: boolean;
observable: boolean;
left_event_time: string | null;
right_event_time: string | null;
left_available_time: string | null;
right_available_time: string | null;
first_knowable_at: string | null;
weight: number | null;
gap_ms: number | null;
wait_ms: number;
}
const UTC_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/;
function ms(value: unknown, field: string): number {
if (typeof value !== "string") {
throw new Error(`${field} must be an RFC 3339 UTC timestamp ending in Z`);
}
const match = UTC_RE.exec(value);
if (!match) throw new Error(`${field} must be an RFC 3339 UTC timestamp ending in Z`);
const parsed = Date.parse(value);
if (!Number.isFinite(parsed)) throw new Error(`${field} is not a valid timestamp`);
const date = new Date(parsed);
const expected = [Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4]), Number(match[5]), Number(match[6]), Number((match[7] ?? "0").padEnd(3, "0"))];
const actual = [date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), date.getUTCMilliseconds()];
if (actual.some((part, index) => part !== expected[index])) throw new Error(`${field} is not a valid timestamp`);
return parsed;
}
function partition(record: Partial<Quote & Target>): string {
const values = [record.instrument, record.venue, record.session_id];
if (!values.every((value) => typeof value === "string" && value.trim().length > 0)) {
throw new Error("instrument, venue, and session_id must be non-empty strings");
}
return values.join("\u0000");
}
function empty(target: Target, status: InterpolationStatus, reason: string, diagnostics: Partial<InterpolationResult> = {}): InterpolationResult {
return {
...target,
bid: null,
ask: null,
status,
reason,
interpolated: false,
observable: false,
left_event_time: diagnostics.left_event_time ?? null,
right_event_time: diagnostics.right_event_time ?? null,
left_available_time: diagnostics.left_available_time ?? null,
right_available_time: diagnostics.right_available_time ?? null,
first_knowable_at: diagnostics.first_knowable_at ?? null,
weight: null,
gap_ms: diagnostics.gap_ms ?? null,
wait_ms: diagnostics.wait_ms ?? 0,
};
}
export function linearQuoteInterpolation(quotes: Quote[], targets: Target[], maxGapMs: number): InterpolationResult[] {
if (!Number.isInteger(maxGapMs) || maxGapMs <= 0) throw new Error("maxGapMs must be a positive integer");
const grouped = new Map<string, Array<Quote & { eventMs: number; availableMs: number }>>();
const seen = new Set<string>();
for (const quote of quotes) {
const key = partition(quote);
const eventMs = ms(quote.event_time, "event_time");
const availableMs = ms(quote.available_time, "available_time");
if (availableMs < eventMs) throw new Error("available_time must not precede event_time on the normalized clock");
if (!Number.isFinite(quote.bid) || !Number.isFinite(quote.ask)) throw new Error("bid and ask must be finite numbers");
if (quote.bid > quote.ask) throw new Error("crossed endpoint quote: bid must not exceed ask");
const identity = `${key}\u0000${eventMs}`;
if (seen.has(identity)) throw new Error("duplicate event_time within an instrument/venue/session partition");
seen.add(identity);
const rows = grouped.get(key) ?? [];
rows.push({ ...quote, eventMs, availableMs });
grouped.set(key, rows);
}
for (const rows of grouped.values()) rows.sort((a, b) => a.eventMs - b.eventMs);
return targets.map((target) => {
const rows = grouped.get(partition(target));
const targetMs = ms(target.target_time, "target_time");
const evaluationMs = ms(target.evaluation_time, "evaluation_time");
if (!rows) return empty(target, "unavailable", "no_partition");
let rightIndex = rows.findIndex((row) => row.eventMs >= targetMs);
if (rightIndex < 0) rightIndex = rows.length;
if (rightIndex < rows.length && rows[rightIndex].eventMs === targetMs) {
const quote = rows[rightIndex];
const diagnostics = {
left_event_time: quote.event_time,
right_event_time: quote.event_time,
left_available_time: quote.available_time,
right_available_time: quote.available_time,
first_knowable_at: quote.available_time,
gap_ms: 0,
};
const waitMs = Math.max(0, quote.availableMs - evaluationMs);
if (waitMs > 0) return empty(target, "exact_not_available", "exact_record_arrives_after_evaluation", { ...diagnostics, wait_ms: waitMs });
return { ...empty(target, "exact", "observed_quote", diagnostics), bid: quote.bid, ask: quote.ask, observable: true, weight: 0 };
}
if (rightIndex === 0 || rightIndex === rows.length) return empty(target, "unavailable", "extrapolation_disabled");
const left = rows[rightIndex - 1];
const right = rows[rightIndex];
const gapMs = right.eventMs - left.eventMs;
const diagnostics = {
left_event_time: left.event_time,
right_event_time: right.event_time,
left_available_time: left.available_time,
right_available_time: right.available_time,
first_knowable_at: left.availableMs >= right.availableMs ? left.available_time : right.available_time,
gap_ms: gapMs,
};
if (gapMs > maxGapMs) return empty(target, "gap_too_wide", "maximum_gap_exceeded", diagnostics);
if (left.availableMs > evaluationMs) return empty(target, "left_not_available", "left_endpoint_arrives_after_evaluation", { ...diagnostics, wait_ms: left.availableMs - evaluationMs });
if (right.availableMs > evaluationMs) return empty(target, "right_not_available", "right_endpoint_arrives_after_evaluation", { ...diagnostics, wait_ms: right.availableMs - evaluationMs });
const weight = (targetMs - left.eventMs) / gapMs;
return {
...empty(target, "linear", "both_endpoints_available", diagnostics),
bid: left.bid + weight * (right.bid - left.bid),
ask: left.ask + weight * (right.ask - left.ask),
interpolated: true,
observable: false,
weight,
};
});
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.