# Crossed/Locked Market Detector

## Executive summary

A top-of-book snapshot is normal when its best ask is above its best bid, locked when the two are equal within an explicitly declared comparison tolerance, and crossed when the best bid is above the best ask beyond that tolerance. This package classifies that geometry while retaining the instrument, market scope, feed, event time, receive time, sequence, side sources, tick size, spread, and comparison boundary.

The result is a diagnostic observation. A lock is not automatically bad data. A cross is not automatic proof of corrupt data, misconduct, or a rule violation. Operational and regulatory conclusions require venue, protection, session, timing, sequence-health, and jurisdiction context.

## Problem and learning contract

An analyst may see `bid >= ask` and be tempted to delete the row. That shortcut destroys evidence and mixes three different questions:

1. What geometry did the selected snapshot show?
2. Was the snapshot assembled from comparable and timely inputs?
3. What, if any, downstream or regulatory policy applies?

After this topic, a reader can calculate signed spread, distinguish normal/locked/crossed observations, configure a tolerance tied to tick size and price scale, implement the same contract in Python and TypeScript, and explain why classification alone does not justify repair.

Prerequisites are bid/ask semantics, top-of-book data, event versus receive time, and basic floating-point behavior. Building a protected National Best Bid and Offer (NBBO), deciding quote eligibility, and applying venue- or jurisdiction-specific rules are outside the core classifier.

## Definitions and scope

- **Top of book:** the selected best bid and ask for one declared market scope.
- **Market scope:** for example, one venue's direct top of book or a consolidated best quote. The label `NBBO` is appropriate only when the upstream construction satisfies the applicable definition.
- **Locked observation:** bid and ask coincide within the declared comparison boundary.
- **Crossed observation:** bid exceeds ask beyond the declared comparison boundary.
- **Diagnostic:** a retained description for review; not a repair instruction or causal conclusion.

The algorithm is pointwise. Consecutive-state duration, attribution, de-crossing, quote selection, and order-routing policy are separate processes.

## Input data contract

Each input row must contain:

| Field | Type | Rule |
|---|---|---|
| `instrument` | string | Stable instrument identifier for the row |
| `market_scope` | string | Declares single-venue, consolidated, or another precise scope |
| `feed` | string | Feed or dataset identity/version |
| `event_time` | ISO-8601 string | Source event timestamp with `Z` or an explicit offset |
| `receive_time` | ISO-8601 string | Observer receipt timestamp with `Z` or an explicit offset |
| `sequence` | nonnegative integer | Source or normalized sequence identifier |
| `bid`, `ask` | finite nonnegative numbers | Same currency, unit, scale, instrument, and comparable snapshot |
| `tick_size` | finite positive number | Tick size applicable to this instrument and time |
| `bid_source`, `ask_source` | strings, recommended | Side lineage, especially for consolidated snapshots |

The implementation retains a receive-before-event warning because clock offsets can make transport age negative. It does not erase otherwise classifiable geometry. A missing required field, unusable timestamp, nonfinite/negative price, or nonpositive tick size produces `INVALID` with a reason.

The fixture is synthetic, CC0 educational data. It is not a real venue or NBBO history. `datasets/rich_nbbo.csv` is a geometry-only synthetic stress corpus; tests enrich it with explicit context before classification.

## Output contract

Every row is retained and augmented with:

- `state`: `NORMAL`, `LOCKED`, `CROSSED`, or `INVALID`;
- `spread` and `spread_ticks`;
- `effective_tolerance` and `numeric_guard`;
- a neutral `diagnostic` label;
- `context_warnings` and an invalid `reason` when applicable;
- all original lineage fields.

The classifier never drops, rewrites, merges, or de-crosses a quote.

## Mathematical formulation

For bid $b_i$ and ask $a_i$ in the same price units, the signed spread is

$$
s_i = a_i - b_i.
$$

Let $q_i > 0$ be the declared tick size, $\tau \ge 0$ the tolerance in ticks, $\rho \ge 0$ the relative tolerance in parts per million (ppm), and $p_i = \max(|a_i|, |b_i|, 1)$. The configured comparison tolerance is

$$
\epsilon_i = \max(q_i\tau,\ p_i\rho\times 10^{-6}).
$$

The implementations also add the disclosed binary-floating-point guard

$$
g_i = 8u p_i,
$$

where $u$ is the language's double-precision machine epsilon. Then:

$$
\operatorname{state}_i =
\begin{cases}
\mathrm{LOCKED}, & |s_i| \le \epsilon_i + g_i,\\
\mathrm{CROSSED}, & s_i < -(\epsilon_i + g_i),\\
\mathrm{NORMAL}, & s_i > \epsilon_i + g_i.
\end{cases}
$$

The machine guard handles representation noise at a declared decimal boundary. It is not a business tolerance. The default business tolerance is zero ticks and zero ppm. A nonzero tolerance must be justified by the input precision and used consistently; it must not silently convert a genuine one-tick spread or cross into a lock.

### Worked examples

With tick size $0.01$, zero business tolerance, bid $100.03$, and ask $100.02$:

$$
s = 100.02 - 100.03 = -0.01 = -1\text{ tick},
$$

so the observation is `CROSSED`. If bid and ask are both $100.01$, $s=0$ and the observation is `LOCKED`. This lock remains a valid observed input unless another validation rule says otherwise.

For bid $100.0000$, ask $100.0025$, tick size $0.01$, and $\tau=0.25$, the tick-based tolerance is $0.0025$. The observation is at the declared lock boundary. With $\tau=0$, the same positive spread is `NORMAL`. That comparison is pedagogical; it is not a recommendation to use a quarter-tick tolerance.

## Algorithm and pseudocode

```text
validate nonnegative tolerance parameters
for each snapshot, without changing row order:
  require instrument, market scope, feed, timestamps, and sequence
  validate bid, ask, tick size, and timestamp offsets
  spread = ask - bid
  price scale = max(abs(bid), abs(ask), 1)
  configured tolerance = max(tick size * tolerance ticks,
                             price scale * relative ppm * 1e-6)
  add disclosed machine-precision guard
  classify spread as locked, crossed, or normal
  attach geometry, context, warnings, and neutral diagnostic
return every input row, including invalid rows
```

```mermaid
flowchart LR
    A["Snapshot plus feed, scope, and clocks"] --> B{"Context and values valid?"}
    B -->|No| C["Retain as INVALID with reason"]
    B -->|Yes| D["Compute signed spread and tick/scale tolerance"]
    D --> E{"Compare with explicit boundary"}
    E -->|Above| F["NORMAL observation"]
    E -->|Within| G["LOCKED observation"]
    E -->|Below| H["CROSSED observation"]
    F --> I["Attach lineage and warnings"]
    G --> I
    H --> I
    I --> J["Separate review or policy layer"]
```

Takeaway: geometry classification precedes, but does not decide, data repair or regulatory treatment.

## Implementation guide and validation

The reference implementations are in `implementations/python/market_state.py` and `implementations/typescript/marketState.ts`. Both use the same option names and snake-case result fields so shared-fixture parity is direct. The implementation is $O(n)$ in time and output space for $n$ rows.

Tests cover:

- normal, exact lock, cross, and invalid rows;
- zero versus quarter-tick tolerance;
- price-scale ppm tolerance;
- a one-tick floating-point boundary;
- missing context, nonfinite/negative values, invalid tick size, and bad timestamps;
- receive-before-event clock warnings;
- Python/TypeScript fixture parity;
- the 144-row synthetic geometry stress corpus.

Passing these tests establishes definition-level consistency. It does not show that a tolerance is economically appropriate, that a feed is synchronized, or that any downstream strategy has value.

## Operational interpretation and evidence

Official U.S. sources illustrate why policy must remain outside the geometry function. The SEC's Rule 605 FAQ, modified April 1, 2026, says a locked NBBO is still used for the stated Rule 605 reporting purposes while a crossed NBBO receives a separate time-aware procedure. That is one reporting context, not a universal cleaning rule. SEC material also shows that tick-size requirements and locked/crossed provisions are subject to current rulemaking and compliance timing. Pin the rule version and effective date before compliance use.

The historical-example opportunity was reviewed but no real market interval is published here. A defensible event would require a redistributable, synchronized top-of-book record with feed identity, side lineage, applicable tick size, sequence health, session, and timestamps. The package does not have that evidence, so inventing or relabeling a synthetic interval as real would be misleading. The official Rule 605 examples are regulatory teaching examples, not claimed historical observations.

## Failure modes and limitations

- Comparing sides from different instruments, currencies, scales, or market scopes.
- Calling a locally assembled best quote an NBBO without eligibility and consolidation evidence.
- Treating asynchronous side updates as simultaneous.
- Choosing an arbitrary absolute epsilon that means different things at different prices or tick regimes.
- Allowing a large tolerance to hide a real one-tick spread or cross.
- Treating every lock as invalid or every cross as corruption, misconduct, or a violation.
- Inferring duration from row count instead of event-time intervals.
- Ignoring duplicate, out-of-order, stale, or sequence-gap context.
- Repairing or dropping observations inside the detector and losing audit evidence.

This classifier does not establish causation, protected-quote status, rule compliance, execution quality, predictive value, profitability, or investment advice.

## Visual explanations and related topics

- `visuals/static/spread-geometry.svg` shows the exact zero boundary, contextual fields, and tick/scale tolerance.
- `visuals/mermaid/market-flow.md` separates geometry from review policy.
- `visuals/animated/playground.html` compares exact and nonzero-tolerance classifications over 36 synthetic, context-rich observations with Back, Step, Play, Pause, and Reset.

The preceding topics **Stale-Quote Detector** and **Duplicate-Trade Resolver** provide freshness and lifecycle context. Continue with **Previous-Tick Interpolation**, which uses quote timestamps to carry values onto a grid and therefore introduces an explicit staleness limit.

See [REFERENCES.md](./REFERENCES.md) for source roles, current status, and limitations.
