Library/Market Data Engineering/Time Synchronization

D01-F03-A05 / Complete engineering topic

Asynchronous Return Alignment: An Interval-Overlap Diagnostic

A production-minded guide to Asynchronous Return Alignment.

D01 · MARKET DATA ENGINEERING
D01-F03-A05Canonical / Tested / Open
D01 / D01-F03
Key concepts

The governed definitions this build depends on. Read them first if a term is unfamiliar.

Practical promise. Determine exactly how two return intervals overlap and when the pair became jointly knowable—without pretending that geometry alone estimates covariance or produces an aligned return.

Asynchronous return intervals shown as a diagnostic pipeline

Two securities rarely update at identical instants. A tempting response is to pair returns whose end timestamps look close. That shortcut hides the time covered by each return. The reliable first step is narrower: represent every return as an event interval, compare the intervals, and preserve availability and partition evidence.

This article teaches that first step. The result is a diagnostic record, not a statistical estimator and not an investment signal.

The boundary that prevents a false claim

An overlap rule answers which intervals share event time. It does not say how price innovation was distributed within either interval. Therefore this package never prorates a return by overlap, multiplies return pairs, estimates covariance, or claims to remove nonsynchronous-sampling bias.

The Hayashi–Yoshida method is a complete covariance estimator that uses overlap as one part of its definition under a stochastic model. This package is not a partial implementation of that estimator. It supplies an auditable geometry diagnostic that a properly specified downstream method may consume.

Define event time and availability separately

For return ii, let the event interval be

Ii=(si,ei].I_i=(s_i,e_i].

The start is exclusive and the end is inclusive. Positive duration still uses a strict boundary test: two intervals that only meet at one endpoint have zero shared duration.

available_at is the first time the resolved return record was knowable. A pair becomes jointly knowable at

aij=max(ai,aj).a_{ij}=\max(a_i,a_j).

At historical evaluation time qq, the pair is available only when aijqa_{ij}\le q. This prevents an ex-post overlap from silently becoming a point-in-time feature before both source returns existed.

In an append-only view, a record's knowledge interval is [available_at,)[\texttt{available\_at},\infty). If a correction later supersedes it, the version interval ends at the correction time. The reference implementation expects that version resolution to happen upstream and rejects duplicate IDs or overlapping return intervals rather than choosing a winner silently.

Calculate overlap without inventing weights

For left interval Ii=(si,ei]I_i=(s_i,e_i] and right interval Jj=(uj,vj]J_j=(u_j,v_j], define

ωij=max(0,min(ei,vj)max(si,uj)).\omega_{ij}=\max\left(0,\min(e_i,v_j)-\max(s_i,u_j)\right).

The package reports how much of each interval is covered:

fijL=ωijeisi,fijR=ωijvjuj.f^L_{ij}=\frac{\omega_{ij}}{e_i-s_i},\qquad f^R_{ij}=\frac{\omega_{ij}}{v_j-u_j}.

These fractions are descriptive. They are not return weights. Using them to split a return would assume something about the path inside an interval that the endpoints do not reveal.

Six valid geometry states

Exact, nested, partial, touching, and disjoint return-interval geometry

The diagnostic classifies valid pairs as:

  • exact: both boundaries match;
  • left_within_right or right_within_left: one interval is nested;
  • partial: the intersection has positive duration without containment;
  • touching: one interval ends exactly where the other begins;
  • disjoint: a positive gap separates them.

Zero-length and reversed intervals are invalid inputs, not geometry classes. Intervals in different session or analysis partitions are never paired.

A worked partial-overlap example

Suppose the left return owns (0,4](0,4] seconds and the right return owns (3,5](3,5] seconds. Their intersection is (3,4](3,4], so:

  • overlap duration is 1 second;
  • left coverage is 1/4=25%1/4=25\%;
  • right coverage is 1/2=50%1/2=50\%.

If the left record was available at 4.2 seconds and the right record at 5.25 seconds, the pair became jointly knowable at 5.25 seconds. A query at 5.0 seconds must report it as unavailable even though the event intervals overlap.

No return value appears in this calculation. Tests deliberately change the stored returns while holding the timestamps fixed and verify that the geometry does not change.

The diagnostic flow

Rendering system map…

The implementations expose diagnostic_only: true, estimator: null, and weighting_or_return_allocation: none. These are not decorative labels; they make the scope machine-readable.

Explore the guided lab

Open the interval-overlap playground. Its initial screen already shows a complete partial-overlap case. Choose exact, nested, touching, disjoint, or unavailable scenarios, then use Back and Step to separate geometry from point-in-time availability. Play and Pause traverse the same deterministic stages; Reset restores the canonical partial case.

The lab's six cases are synthetic. They teach decision boundaries and do not stand in for market evidence.

Implementation contract

Python and TypeScript perform the same sequence:

  1. validate required fields and RFC 3339 UTC timestamps at second or millisecond precision;
  2. reject zero-length, reversed, overlapping, or unresolved duplicate records;
  3. partition before comparison;
  4. compute event overlap and geometry;
  5. compute both descriptive coverage fractions;
  6. use the later source availability timestamp for the pair;
  7. emit exhaustive diagnostics without multiplying return values.

The shared fixture contains three partitions and every important boundary state. Tests cover exact arithmetic, availability cutoffs, invalid inputs, correction ambiguity, partition isolation, and Python/TypeScript parity.

What not to infer

Positive overlap does not prove contemporaneous price discovery, causality, correlation, covariance, predictability, or profitability. High coverage does not show that the intervals contain evenly distributed information. The diagnostic does not correct bid-ask bounce, latency, endogenous sampling, discrete prices, jumps, or measurement error.

If the downstream question is covariance estimation, specify a complete estimator, stochastic assumptions, noise treatment, sampling regime, normalization, inference, and matrix-level checks. Do not rename this overlap report as an estimator after the fact.

Why there is no named historical security example yet

A defensible example needs more than two public price points. It requires traceable event timestamps, first-availability timestamps, correction state, security identity, session-calendar rules, and redistribution permission. The current package has no source satisfying that complete evidence contract.

The honest choice is to defer the example. A later enhancement can add one when the evidence is archived and reproducible; until then, a named company and date would look concrete while leaving the decisive time semantics unverified.

Summary and next topic

You can now distinguish exact, nested, partial, touching, and disjoint return intervals; calculate shared duration and coverage; and determine when a pair was jointly knowable. You can also state the crucial limit: overlap is a diagnostic input, not a return estimator.

Continue with Missing-Bar Gap Classifier, which uses session and source evidence to explain why an expected bar is absent.

Primary references and visual asset map

Source roles and limitations are recorded in REFERENCES.md. The canonical definition is maintained in the topic README.

AssetTeaching roleSourceStatus
Article overviewDiagnostic boundary and workflow../visuals/static/article-hero.svgReady
Geometry comparisonExact, nested, partial, touching, disjoint../visuals/static/return-overlaps.svgReady
Guided playgroundGeometry stages and availability cutoff../visuals/animated/async-return-lab.htmlReady

Diagnostic interval-overlap flow

This diagram keeps correction resolution, event geometry, and point-in-time availability visible as separate gates.

Rendering system map…

Takeaway: interval overlap can support a downstream method, but it does not define that method's allocation, weighting, assumptions, or inference.

References2 primary sources and evidence notes

Expand the source trail, evidence role, and limitations behind the engineering choices.

Accessed 2026-07-22.

R01 — On covariance estimation of non-synchronously observed diffusion processes

  • Organization or authors: Takaki Hayashi and Nakahiro Yoshida
  • Source type: Original peer-reviewed paper, Bernoulli 11(2), 359–379
  • Publication date: 2005
  • Version: Published article
  • URL or DOI: https://doi.org/10.3150/bj/1116340299
  • Author-hosted copy: https://www.ms.u-tokyo.ac.jp/~nakahiro/mypapers_for_personal_use/hayyos03.pdf
  • Accessed: 2026-07-22
  • Jurisdiction: Statistical methodology
  • Supports: A complete covariance estimator can use products of increments selected by overlapping observation intervals; nonsynchronous estimation requires assumptions beyond interval geometry
  • Limitations: Continuous diffusion and asymptotic setting; it does not define the package's engineering schema, correction policy, availability fields, or coverage fractions

R02 — RFC 3339: Date and Time on the Internet: Timestamps

  • Organization or authors: Internet Engineering Task Force; Graham Klyne and Chris Newman
  • Source type: Standards-track timestamp specification
  • Publication date: July 2002
  • Version: RFC 3339, updated by RFC 9557
  • URL or DOI: https://www.rfc-editor.org/info/rfc3339/
  • Accessed: 2026-07-22
  • Jurisdiction: Global technical standard
  • Supports: Unambiguous UTC timestamp serialization used by the package
  • Limitations: Does not define market event time, availability time, return ownership, correction policy, or interval arithmetic

Evidence classification

  • Sourced fact: The Hayashi–Yoshida estimator is a complete covariance method whose selection rule uses overlapping observation intervals (R01).
  • Implementation choice: Half-open interval ownership, explicit partitions, correction-resolved inputs, availability cutoff logic, and diagnostic coverage fractions are this package's engineering contract.
  • Pedagogical simplification: Synthetic whole-second examples make boundary cases hand-checkable.
  • Empirical claim: None. No market dataset or historical security example is used.
  • Author inference: A named historical example without event, availability, correction, partition, identity, and licensing evidence would be misleading.
asyncReturns.ts
/** Event-time interval-overlap diagnostics for asynchronous returns.
 *
 * This module does not estimate covariance, allocate returns, or claim to
 * remove nonsynchronous-sampling bias.
 */

export type ReturnInterval = {
  return_id: string;
  instrument: string;
  partition: string;
  event_start: string;
  event_end: string;
  available_at: string;
  return_value?: number;
};

type ValidatedInterval = ReturnInterval & {
  _start_ms: number;
  _end_ms: number;
  _available_ms: number;
};

const milliseconds = (value: string, field: string): number => {
  if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value)) {
    throw new Error(`${field} must be an RFC 3339 UTC timestamp at second or millisecond precision`);
  }
  const parsed = Date.parse(value);
  if (!Number.isFinite(parsed)) throw new Error(`${field} is not a valid timestamp`);
  return parsed;
};

const timestamp = (value: number): string => new Date(value).toISOString();

function validateStream(rows: ReturnInterval[], side: string): ValidatedInterval[] {
  if (!Array.isArray(rows) || rows.length === 0) {
    throw new Error(`${side} stream must contain at least one return interval`);
  }
  const required: (keyof ReturnInterval)[] = [
    "return_id", "instrument", "partition", "event_start", "event_end", "available_at",
  ];
  const seen = new Set<string>();
  const validated = rows.map((row) => {
    const missing = required.filter((key) => !(key in row));
    if (missing.length) throw new Error(`${side} interval is missing ${missing.join(",")}`);
    for (const key of ["return_id", "instrument", "partition"] as const) {
      if (typeof row[key] !== "string" || !row[key]) throw new Error(`${key} must be a non-empty string`);
    }
    if (seen.has(row.return_id)) throw new Error("return_id values must be unique after correction resolution");
    seen.add(row.return_id);
    const start = milliseconds(row.event_start, "event_start");
    const end = milliseconds(row.event_end, "event_end");
    const available = milliseconds(row.available_at, "available_at");
    if (start >= end) throw new Error("event_start must be strictly earlier than event_end");
    if (available < end) throw new Error("available_at cannot precede event_end");
    if (row.return_value !== undefined && !Number.isFinite(row.return_value)) {
      throw new Error("return_value must be finite when supplied");
    }
    return { ...row, _start_ms: start, _end_ms: end, _available_ms: available };
  }).sort((a, b) => a.partition.localeCompare(b.partition) || a._start_ms - b._start_ms || a._end_ms - b._end_ms);

  if (new Set(validated.map((row) => row.instrument)).size !== 1) {
    throw new Error(`${side} stream must contain exactly one instrument`);
  }
  const previous = new Map<string, ValidatedInterval>();
  for (const row of validated) {
    const prior = previous.get(row.partition);
    if (prior && prior._end_ms > row._start_ms) {
      throw new Error("return intervals within a stream cannot overlap");
    }
    previous.set(row.partition, row);
  }
  return validated;
}

const publicInterval = (row: ValidatedInterval): ReturnInterval => ({
  return_id: row.return_id,
  instrument: row.instrument,
  partition: row.partition,
  event_start: row.event_start,
  event_end: row.event_end,
  available_at: row.available_at,
  ...(row.return_value === undefined ? {} : { return_value: row.return_value }),
});

function classifyValidatedPair(left: ValidatedInterval, right: ValidatedInterval, evaluationMs: number | null) {
  const availableMs = Math.max(left._available_ms, right._available_ms);
  let geometry: string;
  let overlapStart: string | null = null;
  let overlapEnd: string | null = null;
  let overlapMs = 0;

  if (left.partition !== right.partition) {
    geometry = "partition_mismatch";
  } else {
    const low = Math.max(left._start_ms, right._start_ms);
    const high = Math.min(left._end_ms, right._end_ms);
    overlapMs = Math.max(0, high - low);
    if (overlapMs > 0) {
      overlapStart = timestamp(low);
      overlapEnd = timestamp(high);
    }
    if (high < low) geometry = "disjoint";
    else if (high === low) geometry = "touching";
    else if (left._start_ms === right._start_ms && left._end_ms === right._end_ms) geometry = "exact";
    else if (left._start_ms >= right._start_ms && left._end_ms <= right._end_ms) geometry = "left_within_right";
    else if (right._start_ms >= left._start_ms && right._end_ms <= left._end_ms) geometry = "right_within_left";
    else geometry = "partial";
  }

  return {
    left_return_id: left.return_id,
    right_return_id: right.return_id,
    partition: left.partition === right.partition ? left.partition : null,
    geometry,
    event_overlap_start: overlapStart,
    event_overlap_end: overlapEnd,
    event_overlap_ms: overlapMs,
    left_overlap_fraction: overlapMs / (left._end_ms - left._start_ms),
    right_overlap_fraction: overlapMs / (right._end_ms - right._start_ms),
    pair_available_at: timestamp(availableMs),
    jointly_available_at_evaluation: evaluationMs === null ? null : availableMs <= evaluationMs,
  };
}

export function classifyIntervalPair(left: ReturnInterval, right: ReturnInterval, evaluatedAt?: string) {
  const a = validateStream([left], "left")[0];
  const b = validateStream([right], "right")[0];
  return classifyValidatedPair(a, b, evaluatedAt === undefined ? null : milliseconds(evaluatedAt, "evaluated_at"));
}

export function diagnoseAsynchronousReturnAlignment(left: ReturnInterval[], right: ReturnInterval[], evaluatedAt?: string) {
  const a = validateStream(left, "left");
  const b = validateStream(right, "right");
  const evaluationMs = evaluatedAt === undefined ? null : milliseconds(evaluatedAt, "evaluated_at");
  const pairs = a.flatMap((leftRow) => b
    .filter((rightRow) => leftRow.partition === rightRow.partition)
    .map((rightRow) => classifyValidatedPair(leftRow, rightRow, evaluationMs)));
  const positive = pairs.filter((pair) => pair.event_overlap_ms > 0);
  const counts: Record<string, number> = {};
  for (const pair of pairs) counts[pair.geometry] = (counts[pair.geometry] ?? 0) + 1;
  const geometryCounts = Object.fromEntries(Object.entries(counts).sort(([x], [y]) => x.localeCompare(y)));
  const totalOverlapMs = positive.reduce((sum, pair) => sum + pair.event_overlap_ms, 0);
  const leftTotalMs = a.reduce((sum, row) => sum + row._end_ms - row._start_ms, 0);
  const rightTotalMs = b.reduce((sum, row) => sum + row._end_ms - row._start_ms, 0);
  return {
    diagnostic_only: true,
    estimator: null,
    weighting_or_return_allocation: "none",
    left_intervals: a.map(publicInterval),
    right_intervals: b.map(publicInterval),
    pairs,
    positive_overlap_pairs: positive,
    geometry_counts: geometryCounts,
    total_event_overlap_ms: totalOverlapMs,
    left_event_time_coverage: totalOverlapMs / leftTotalMs,
    right_event_time_coverage: totalOverlapMs / rightTotalMs,
    all_pairs_available_at_evaluation: evaluationMs === null || pairs.length === 0
      ? null
      : pairs.every((pair) => pair.jointly_available_at_evaluation),
  };
}

export const alignAsynchronousReturns = diagnoseAsynchronousReturnAlignment;
Full-height labasync return labOpen full screen