Library/Market Data Engineering/Order-Book Feed Engineering

D01-F05-A04 / Complete engineering topic

Sequence-Gap Detection and Recovery

A production-minded guide to Sequence-Gap Detection and Recovery.

D01 · MARKET DATA ENGINEERING
D01-F05-A04Canonical / Tested / Open
D01 / D01-F05

How do you stop a book from advancing across missing messages while accepting replayed data and buffered live traffic?

The dangerous failure is not always a crash. It is a plausible price or depth view built from the wrong sequence, quantity semantics, clock, or venue policy. This tutorial builds contiguous applied prefix and recovery state with enough lineage to decide whether the state is usable.

The one idea to remember

Recovery is a commit barrier. Forward messages may be stored, but none may mutate published state until every earlier sequence is present.

The contract before the code

The canonical receiver buffers forward arrivals, releases the contiguous prefix, and records duplicates; session resets must be handled outside the stream. Sequence scope, wrap, recovery limits, and reset rules are protocol-specific.

System map

Contract dimensionCanonical choiceWhy it is visible
PriceInteger ticks or atoms plus scalePrevents ambiguous floating-point keys
SequenceOne documented session/unit domainMakes gaps and anchors testable
TimeEvent and receive clocks remain separatePrevents arrival time from impersonating venue causality
QuantityTopic-selected semanticsPrevents replacement, delta, and order quantity from being conflated
Invalid stateReject, buffer, recover, or exclude explicitlyPrevents false precision

Nearby variant. Gap limits, sequence wrap, reset, and spin escalation belong to the protocol profile.

State and mathematics

The core relationship is apply sequence s only when s = next_expected; buffer s > next_expected. The equality boundary sequence = next_expected is the only commit condition; larger values wait in the buffer.

Sequence boundary

Work the synthetic trace by hand

The input is deliberately synthetic and redistributable. It contains enough events to expose the normal path and the boundaries; it is not an exchange observation.

State trace

  1. Apply the expected sequence.
  2. Buffer forward arrivals and expose the missing range.
  3. Release the entire contiguous prefix after replay.
  4. Diagnose late duplicates and unresolved tails.

Exact calculation

Only next_expected commits. Forward arrivals wait in the buffer until replay closes the missing interval.

JSON
{
  "applied_sequences": [
    100,
    101,
    102,
    103,
    104,
    105,
    106,
    107,
    108,
    109,
    110,
    111,
    112,
    113,
    114,
    115,
    116
  ],
  "next_expected": 117,
  "recovery_requests": [
    {
      "from_sequence": 101,
      "to_sequence": 101
    },
    {
      "from_sequence": 104,
      "to_sequence": 104
    },
    {
      "from_sequence": 106,
      "to_sequence": 106
    },
    {
      "from_sequence": 109,
      "to_sequence": 109
    },
    {
      "from_sequence": 113,
      "to_sequence": 113
    }
  ],
  "duplicate_sequences": [
    107
  ],
  "replay_arrival_count": 6,
  "state": "current"
}

Independent check. The applied prefix is contiguous from 100 through 116. The values are author-derived from labeled synthetic inputs, not provider observations.

Transition diagram

Diagnostics are part of the answer

DiagnosticDecision it supports
Next expected sequenceExplains whether the output is safe to publish or why it is bounded
Missing rangeExplains whether the output is safe to publish or why it is bounded
Buffered sequencesExplains whether the output is safe to publish or why it is bounded
Recovery requestsExplains whether the output is safe to publish or why it is bounded
Duplicate arrivalsExplains whether the output is safe to publish or why it is bounded
Live versus replay sourceExplains whether the output is safe to publish or why it is bounded

An API that returns only prices and sizes forces downstream code to guess whether the state is current. This package returns the decision evidence beside the state.

Reproduce and challenge the case

The canonical input, expected output, five named scenarios, Python implementation, and TypeScript implementation ship together. Each lab step adds or changes a material synthetic event instead of padding a short trace.

Open the guided playground, then compare canonical-input.json with expected-output.json.

Use the lab in this order:

  1. Run the canonical scenario to the final state.
  2. Switch to the nearest boundary and identify the first changed diagnostic.
  3. Run the rejection or exclusion scenario.
  4. Explain why the previous valid state must not silently absorb the bad input.

Failure diagnosis

Failure map

SymptomLikely contract defectSafe response
Sequence jumps forwardMissing causal inputBuffer and recover; do not advance published state
Quantity becomes impossibleWrong semantics or lifecycle referenceReject and inspect the adapter
Snapshot and replay disagreeAnchor, update, or source driftPublish the difference ledger and rebuild
Venue view is stale or haltedAs-of eligibility failureExclude with a reason
Output looks valid but lineage is absentSilent assumptionTreat state as unsupported

Highest-risk misuse: Never apply a forward message merely because it arrived over reliable transport.

Implementation walkthrough

The reference implementation is intentionally explicit:

  1. validate identity, units, clocks, and provider-adapted fields;
  2. apply one deterministic transition;
  3. refuse or isolate unsafe state;
  4. return values and diagnostics together;
  5. compare Python and TypeScript against the same frozen JSON.

The implementation does not optimize away checks and does not mutate caller input. Production systems can use more compact structures only if they preserve the same observable semantics.

The D11 handoff

D11 handoff

No microstructure statistic is trustworthy while the underlying book is recovering or has an unresolved gap. That boundary keeps feed correctness separate from economic interpretation and avoids treating Queue Imbalance, Microprice, slope, resiliency, or Hawkes intensity as data-cleaning tools.

Evidence and historical boundary

The directly relevant primary sources are Nasdaq MoldUDP64, Cboe Multicast PITCH, Coinbase Exchange WebSocket channels. They establish only the provider or plan behaviors named in the source ledger. The provider-neutral contract and all displayed numbers are package choices and author-derived synthetic calculations.

Historical decision: deferred. A named incident would require licensed packets, the exact protocol release, session/unit identity, recovery and correction logs, and redistribution permission. A famous outage without those records would add drama, not reproducibility.

What the tests prove

The tests prove canonical arithmetic, invalid-state rejection, and Python/TypeScript parity. They do not prove feed uptime, latency superiority, prediction, execution quality, or profitability.

Builder’s publication checklist

  • Can every price be traced to integer ticks or atoms and a declared scale?
  • Are event, receive, availability, and as-of clocks named rather than merged?
  • Is the sequence scope and reset boundary explicit?
  • Does every unsafe state reject, recover, or exclude with a reason?
  • Do full-depth totals remain distinguishable from an output window?
  • Do Python, TypeScript, the article, visuals, and lab show the same result?
  • Is the D11 consumer prevented from reading recovering or drifted state?

Next step

After this family, use the reconciled state with All listed D11-F03 and D11-F04 order-book topics and retain the same venue, sequence, and clock boundaries.

Primary sources

See the complete source ledger, including Nasdaq ITCH and GLIMPSE, MoldUDP64, FIX book-management practices, Cboe PITCH, Coinbase Exchange channels, and UTP quotation specifications.

Sequence-Gap Detection and Recovery system flow

Rendering system map…

Takeaway: a downstream statistic cannot repair an upstream causal or sequence-integrity defect.

ReferencesPrimary sources and evidence notes

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

Topic-specific source roles

SourceRole in this package
SRC-MOLDDirect primary support for this topic's selected behavior or boundary
SRC-CBOEDirect primary support for this topic's selected behavior or boundary
SRC-COINBASEDirect primary support for this topic's selected behavior or boundary

Provider specifications establish their own message and recovery semantics. They do not establish this package's provider-neutral implementation choice, synthetic values, performance, or universal market behavior. Historical versioned documents are retained for the behaviors they directly define; living/current sources carry a 2026-07-30 access boundary.

SRC-ITCH — Nasdaq TotalView-ITCH 5.0

SRC-GLIMPSE — Nasdaq GLIMPSE 5.0

SRC-MOLD — MoldUDP64 protocol

  • Organization or authors: Nasdaq
  • Source type: Official transport protocol specification
  • Publication or effective date: Not stated
  • Version: 1.00
  • URL or DOI: https://nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/moldudp64.pdf
  • Accessed: 2026-07-30
  • Jurisdiction: Provider protocol
  • Supports: packet sequence numbers, gap detection, and retransmission requests
  • Limitations: Application message semantics are defined by the higher-level feed.

SRC-FIX — FIX recommended practices for book management

  • Organization or authors: FIX Trading Community
  • Source type: Official industry recommended practice
  • Publication or effective date: 2007-01
  • Version: 2.0
  • URL or DOI: https://www.fixtrading.org/wp-content/uploads/download-manager-files/MDOWG_Book_Mgt-v20.pdf
  • Accessed: 2026-07-30
  • Jurisdiction: Cross-market technical standard
  • Supports: snapshot plus incremental workflows, book entries, and recovery
  • Limitations: Venue profiles choose supported fields and actions.

SRC-CBOE — Cboe Multicast PITCH specification

SRC-COINBASE — Coinbase Exchange WebSocket channels

  • Organization or authors: Coinbase
  • Source type: Official provider documentation
  • Publication or effective date: Living documentation
  • Version: Current page accessed 2026-07-30
  • URL or DOI: https://docs.cdp.coinbase.com/exchange/websocket-feed/channels
  • Accessed: 2026-07-30
  • Jurisdiction: Coinbase Exchange
  • Supports: L2 snapshot/update and L3 queue-snapshot workflow; L2 size is absolute and zero deletes
  • Limitations: Coinbase semantics must not be generalized to other feeds.

SRC-UTP — UTP Quote Data Feed

  • Organization or authors: UTP Plan
  • Source type: Official SIP specification
  • Publication or effective date: 2015-11
  • Version: 14.5
  • URL or DOI: https://www.utpplan.com/DOC/uqdfspecification.pdf
  • Accessed: 2026-07-30
  • Jurisdiction: U.S. NMS
  • Supports: eligible-participant National BBO calculation and price/size/time tie handling
  • Limitations: Current production requirements and odd-lot changes must be checked separately.

SRC-UTP-2026 — UTP odd-lot data service update

  • Organization or authors: UTP Plan / Nasdaq Trader
  • Source type: Official vendor alert
  • Publication or effective date: Published 2026-02-03; production change 2026-04-27
  • Version: UTP Vendor Alert 2026-5
  • URL or DOI: https://m.nasdaqtrader.com/TraderNews.aspx?id=UTP2026-05
  • Accessed: 2026-07-30
  • Jurisdiction: U.S. NMS
  • Supports: Current top-of-book odd-lot dissemination boundary and deferred deeper odd-lot requirement
  • Limitations: This package does not implement SIP eligibility or protected-quote law.

SRC-UTP-CURRENT — UTP Data Feed Services Specification

  • Organization or authors: UTP Plan
  • Source type: Official current SIP technical specification
  • Publication or effective date: November 2025 with releases through 2026-04-27
  • Version: Binary output 3.0a
  • URL or DOI: https://utpplan.com/DOC/UtpBinaryOutputSpec-3.0a.pdf
  • Accessed: 2026-07-30
  • Jurisdiction: U.S. NMS, Tape C
  • Supports: Current round-lot sizes, actual-share quote sizes, fractional-share trade changes, and odd-lot quotation dissemination context
  • Limitations: It does not turn this package's local receive-time view into an official SIP NBBO or compliance result.
order-book-feed.ts
/* Reference algorithms for D01-F05. This file intentionally uses
   JavaScript-compatible TypeScript so the parity runner can execute the same
   source as an ES module without a build dependency. */

const asInt = (name, value, minimum = null) => {
  if (!Number.isInteger(value)) throw new TypeError(`${name} must be an integer`);
  if (minimum !== null && value < minimum) throw new RangeError(`${name} must be >= ${minimum}`);
  return value;
};
const side = value => {
  if (value !== "bid" && value !== "ask") throw new RangeError("side must be bid or ask");
  return value;
};
const sortedLevels = book => ({
  bids: [...book.bid.entries()].filter(([,q])=>q>0).sort((a,b)=>b[0]-a[0]).map(([price_ticks,quantity])=>({price_ticks,quantity})),
  asks: [...book.ask.entries()].filter(([,q])=>q>0).sort((a,b)=>a[0]-b[0]).map(([price_ticks,quantity])=>({price_ticks,quantity})),
});
const bookFromLevels = levels => {
  const book={bid:new Map(),ask:new Map()};
  for (const [plural,s] of [["bids","bid"],["asks","ask"]]) {
    for (const [index,row] of (levels[plural]??[]).entries()) {
      const price=asInt(`${plural}[${index}].price_ticks`,row.price_ticks,1);
      const quantity=asInt(`${plural}[${index}].quantity`,row.quantity,0);
      if (book[s].has(price)) throw new RangeError(`duplicate ${s} price level ${price}`);
      if (quantity) book[s].set(price,quantity);
    }
  }
  return book;
};

export function normalizeEvents(events) {
  const output=[], lastSequence=new Map();
  for (const [index,raw] of events.entries()) {
    const venue=String(raw.venue??"").trim(), instrument=String(raw.instrument??"").trim();
    if (!venue||!instrument) throw new RangeError("venue and instrument are required");
    const sequence=asInt(`events[${index}].sequence`,raw.sequence,0);
    if (lastSequence.has(venue)&&sequence<=lastSequence.get(venue)) throw new RangeError(`sequence must increase within venue ${venue}`);
    lastSequence.set(venue,sequence);
    const event_time_ns=asInt("event_time_ns",raw.event_time_ns,0);
    const receive_time_ns=asInt("receive_time_ns",raw.receive_time_ns,0);
    if (receive_time_ns<event_time_ns) throw new RangeError("receive_time_ns must not precede event_time_ns in this contract");
    const scale=asInt("price_scale",raw.price_scale,1);
    const common={schema_version:1,venue,instrument,sequence,event_time_ns,receive_time_ns,latency_ns:receive_time_ns-event_time_ns,price_scale:scale};
    if (raw.kind==="trade") {
      const price_atoms=asInt("price_atoms",raw.price_atoms,1);
      output.push({...common,kind:"trade",trade_id:String(raw.trade_id??""),price_atoms,price:price_atoms/scale,quantity:asInt("quantity",raw.quantity,1),aggressor_side:raw.aggressor_side??null});
    } else if (raw.kind==="quote") {
      const bid=asInt("bid_price_atoms",raw.bid_price_atoms,1), ask=asInt("ask_price_atoms",raw.ask_price_atoms,1);
      if (bid>ask) throw new RangeError("crossed quote is outside the normalization contract");
      output.push({...common,kind:"quote",bid_price_atoms:bid,bid_price:bid/scale,bid_quantity:asInt("bid_quantity",raw.bid_quantity,0),ask_price_atoms:ask,ask_price:ask/scale,ask_quantity:asInt("ask_quantity",raw.ask_quantity,0),quote_state:bid===ask?"locked":"normal"});
    } else throw new RangeError("kind must be trade or quote");
  }
  return {schema_version:1,event_count:output.length,trade_count:output.filter(x=>x.kind==="trade").length,quote_count:output.filter(x=>x.kind==="quote").length,venue_count:lastSequence.size,max_latency_ns:output.reduce((n,x)=>Math.max(n,x.latency_ns),0),events:output,state:"normalized"};
}

export function reconstructL2(snapshot,{snapshot_sequence,deltas}) {
  const anchor=asInt("snapshot_sequence",snapshot_sequence,0), book=bookFromLevels(snapshot);
  let expected=anchor+1; const applied_sequences=[],discarded_sequences=[];
  for (const [index,delta] of deltas.entries()) {
    const sequence=asInt(`deltas[${index}].sequence`,delta.sequence,0);
    if (sequence<=anchor) {discarded_sequences.push(sequence);continue;}
    if (sequence!==expected) throw new RangeError(`sequence gap: expected ${expected}, received ${sequence}`);
    const s=side(delta.side), price=asInt("price_ticks",delta.price_ticks,1), quantity=asInt("quantity",delta.quantity,0);
    if (quantity) book[s].set(price,quantity); else book[s].delete(price);
    applied_sequences.push(sequence); expected++;
  }
  const levels=sortedLevels(book), best_bid_ticks=levels.bids[0]?.price_ticks??null, best_ask_ticks=levels.asks[0]?.price_ticks??null;
  if (best_bid_ticks!==null&&best_ask_ticks!==null&&best_bid_ticks>best_ask_ticks) throw new RangeError("reconstruction produced a crossed book");
  return {quantity_semantics:"absolute-replacement",snapshot_sequence:anchor,last_sequence:expected-1,applied_sequences,applied_count:applied_sequences.length,discarded_sequences,discarded_count:discarded_sequences.length,...levels,best_bid_ticks,best_ask_ticks,state:"current"};
}

export function aggregatePriceLevels(orders,{depth_limit=null}={}) {
  const totals={bid:new Map(),ask:new Map()}; let order_count=0;
  for (const [index,order] of orders.entries()) {
    const s=side(order.side), price=asInt(`orders[${index}].price_ticks`,order.price_ticks,1), quantity=asInt("quantity",order.quantity,1);
    const current=totals[s].get(price)??[0,0]; current[0]+=quantity; current[1]++; totals[s].set(price,current); order_count++;
  }
  const limit=depth_limit===null?null:asInt("depth_limit",depth_limit,1);
  let bids=[...totals.bid.entries()].sort((a,b)=>b[0]-a[0]).map(([price_ticks,v])=>({price_ticks,quantity:v[0],order_count:v[1]}));
  let asks=[...totals.ask.entries()].sort((a,b)=>a[0]-b[0]).map(([price_ticks,v])=>({price_ticks,quantity:v[0],order_count:v[1]}));
  const full_bid_level_count=bids.length, full_ask_level_count=asks.length,full_bid_quantity=bids.reduce((n,x)=>n+x.quantity,0),full_ask_quantity=asks.reduce((n,x)=>n+x.quantity,0);
  if (limit!==null) {bids=bids.slice(0,limit);asks=asks.slice(0,limit);}
  const visible_bid_quantity=bids.reduce((n,x)=>n+x.quantity,0),visible_ask_quantity=asks.reduce((n,x)=>n+x.quantity,0);
  return {order_count,full_bid_level_count,full_ask_level_count,depth_limit:limit,bids,asks,visible_bid_quantity,visible_ask_quantity,full_bid_quantity,full_ask_quantity,hidden_by_depth_limit_bid_quantity:full_bid_quantity-visible_bid_quantity,hidden_by_depth_limit_ask_quantity:full_ask_quantity-visible_ask_quantity,state:"aggregated"};
}

export function reconstructL3(snapshot_orders,{snapshot_sequence,events}) {
  const anchor=asInt("snapshot_sequence",snapshot_sequence,0), orders=new Map(),action_counts={add:0,reduce:0,execute:0,delete:0,replace:0}; let rank=0, expected=anchor+1;
  for (const raw of snapshot_orders) {
    const order_id=String(raw.order_id??"");
    if (!order_id||orders.has(order_id)) throw new RangeError("snapshot order IDs must be non-empty and unique");
    rank++; orders.set(order_id,{order_id,side:side(raw.side),price_ticks:asInt("price_ticks",raw.price_ticks,1),quantity:asInt("quantity",raw.quantity,1),priority_rank:rank});
  }
  for (const [index,event] of events.entries()) {
    const sequence=asInt(`events[${index}].sequence`,event.sequence,0);
    if (sequence<=anchor) continue;
    if (sequence!==expected) throw new RangeError(`sequence gap: expected ${expected}, received ${sequence}`);
    const action=event.action, order_id=String(event.order_id??"");
    if (action==="add") {
      if (!order_id||orders.has(order_id)) throw new RangeError("add requires a new order_id");
      rank++; orders.set(order_id,{order_id,side:side(event.side),price_ticks:asInt("price_ticks",event.price_ticks,1),quantity:asInt("quantity",event.quantity,1),priority_rank:rank});
    } else if (action==="reduce"||action==="execute") {
      if (!orders.has(order_id)) throw new RangeError(`${action} references unknown order`);
      const reduction=asInt("quantity",event.quantity,1), row=orders.get(order_id);
      if (reduction>row.quantity) throw new RangeError("reduction exceeds resting quantity");
      row.quantity-=reduction; if (!row.quantity) orders.delete(order_id);
    } else if (action==="delete") {
      if (!orders.has(order_id)) throw new RangeError("delete references unknown order");
      orders.delete(order_id);
    } else if (action==="replace") {
      if (!orders.has(order_id)) throw new RangeError("replace references unknown order");
      const original=orders.get(order_id);orders.delete(order_id);
      const new_order_id=String(event.new_order_id??"");
      if (!new_order_id||orders.has(new_order_id)) throw new RangeError("replace requires a new unique new_order_id");
      rank++;orders.set(new_order_id,{order_id:new_order_id,side:original.side,price_ticks:asInt("price_ticks",event.price_ticks,1),quantity:asInt("quantity",event.quantity,1),priority_rank:rank});
    } else throw new RangeError("action must be add, reduce, execute, delete, or replace");
    action_counts[action]++;
    expected++;
  }
  const rows=[...orders.values()].sort((a,b)=>a.priority_rank-b.priority_rank), aggregation=aggregatePriceLevels(rows);
  return {snapshot_sequence:anchor,last_sequence:expected-1,order_count:rows.length,event_count:Object.values(action_counts).reduce((a,b)=>a+b,0),action_counts,orders:rows,bids:aggregation.bids,asks:aggregation.asks,state:"current"};
}

export function recoverSequenceStream(arrivals,{start_sequence}) {
  let expected=asInt("start_sequence",start_sequence,0),lastRequest=null; const buffer=new Map(), applied_sequences=[], duplicate_sequences=[], trace=[],recovery_requests=[];
  for (const [index,arrival] of arrivals.entries()) {
    const sequence=asInt(`arrivals[${index}].sequence`,arrival.sequence,0), source=String(arrival.source??"live");
    if (sequence<expected||buffer.has(sequence)) duplicate_sequences.push(sequence); else buffer.set(sequence,source);
    while (buffer.has(expected)) {buffer.delete(expected);applied_sequences.push(expected);expected++;}
    const buffered=[...buffer.keys()].sort((a,b)=>a-b), stop=buffered.length?buffered[0]:expected;
    const missing=Array.from({length:stop-expected},(_,i)=>expected+i);
    if (missing.length) {
      const key=`${missing[0]}:${missing.at(-1)}`;
      if (key!==lastRequest) {recovery_requests.push({from_sequence:missing[0],to_sequence:missing.at(-1)});lastRequest=key;}
    }
    trace.push({arrival_sequence:sequence,source,next_expected:expected,missing,buffered});
  }
  const buffered_sequences=[...buffer.keys()].sort((a,b)=>a-b), stop=buffered_sequences.length?buffered_sequences[0]:expected;
  return {start_sequence:asInt("start_sequence",start_sequence,0),applied_sequences,next_expected:expected,missing_sequences:Array.from({length:stop-expected},(_,i)=>expected+i),buffered_sequences,duplicate_sequences,recovery_requests,recovery_request_count:recovery_requests.length,replay_arrival_count:arrivals.filter(x=>(x.source??"live")==="replay").length,trace,state:buffer.size?"recovering":"current"};
}

export function reconcileSnapshotIncrementals(snapshot,{snapshot_sequence,incrementals,checkpoint,checkpoint_sequence}) {
  const anchor=asInt("snapshot_sequence",snapshot_sequence,0), check=asInt("checkpoint_sequence",checkpoint_sequence,anchor);
  const eligible=incrementals.filter(row=>asInt("sequence",row.sequence,0)<=check),pending=incrementals.filter(row=>asInt("sequence",row.sequence,0)>check), reconstructed=reconstructL2(snapshot,{snapshot_sequence:anchor,deltas:eligible});
  if (reconstructed.last_sequence!==check) throw new RangeError("incrementals do not reach checkpoint_sequence contiguously");
  const observed=bookFromLevels(checkpoint), rebuilt={bid:new Map(reconstructed.bids.map(x=>[x.price_ticks,x.quantity])),ask:new Map(reconstructed.asks.map(x=>[x.price_ticks,x.quantity]))}, differences=[];
  for (const s of ["bid","ask"]) {
    const prices=[...new Set([...rebuilt[s].keys(),...observed[s].keys()])].sort((a,b)=>s==="bid"?b-a:a-b);
    for (const price_ticks of prices) {
      const left=rebuilt[s].get(price_ticks)??0,right=observed[s].get(price_ticks)??0;
      if (left!==right) differences.push({side:s,price_ticks,reconstructed_quantity:left,snapshot_quantity:right,difference:left-right,kind:left===0?"missing-reconstructed-level":right===0?"missing-checkpoint-level":"quantity-mismatch"});
    }
  }
  const mismatch_counts={"quantity-mismatch":0,"missing-reconstructed-level":0,"missing-checkpoint-level":0};for(const row of differences)mismatch_counts[row.kind]++;
  return {snapshot_sequence:anchor,checkpoint_sequence:check,compared_level_count:new Set([...rebuilt.bid.keys(),...observed.bid.keys()]).size+new Set([...rebuilt.ask.keys(),...observed.ask.keys()]).size,difference_count:differences.length,mismatch_counts,replayed_incremental_count:eligible.length,pending_after_checkpoint_count:pending.length,differences,reconstructed:{bids:reconstructed.bids,asks:reconstructed.asks},checkpoint:sortedLevels(observed),state:differences.length?"drift-detected":"reconciled"};
}

export function consolidateVenues(quotes,{as_of_receive_time_ns,max_staleness_ns}) {
  const as_of=asInt("as_of_receive_time_ns",as_of_receive_time_ns,0), tolerance=asInt("max_staleness_ns",max_staleness_ns,0), latest=new Map();
  for (const [index,quote] of quotes.entries()) {
    const venue=String(quote.venue??"").trim(); if (!venue) throw new RangeError("venue is required");
    const receive=asInt(`quotes[${index}].receive_time_ns`,quote.receive_time_ns,0); if (receive>as_of) continue;
    if (!latest.has(venue)||receive>latest.get(venue).receive_time_ns) latest.set(venue,{...quote});
  }
  const eligible=[],excluded=[];
  for (const [venue,quote] of [...latest.entries()].sort()) {
    const age=as_of-asInt("receive_time_ns",quote.receive_time_ns,0);
    if (quote.eligible===false) excluded.push({venue,reason:"ineligible"});
    else if ((quote.status??"open")!=="open") excluded.push({venue,reason:"not-open"});
    else if (age>tolerance) excluded.push({venue,reason:"stale"});
    else {
      const levels=("bids" in quote||"asks" in quote)?sortedLevels(bookFromLevels(quote)):{bids:[{price_ticks:asInt("bid_price_ticks",quote.bid_price_ticks,1),quantity:asInt("bid_quantity",quote.bid_quantity,0)}],asks:[{price_ticks:asInt("ask_price_ticks",quote.ask_price_ticks,1),quantity:asInt("ask_quantity",quote.ask_quantity,0)}]};
      if (!levels.bids.length||!levels.asks.length) {excluded.push({venue,reason:"empty-local-side"});continue;}
      if (levels.bids[0].price_ticks>levels.asks[0].price_ticks) {excluded.push({venue,reason:"crossed-local-book"});continue;}
      eligible.push({venue,bids:levels.bids,asks:levels.asks,receive_time_ns:quote.receive_time_ns});
    }
  }
  if (!eligible.length) return {state:"no-eligible-quote",eligible_venue_count:0,excluded};
  const depth={bid:new Map(),ask:new Map()};
  for (const row of eligible) {
    for (const [plural,s] of [["bids","bid"],["asks","ask"]]) for (const level of row[plural]) {
      if (!depth[s].has(level.price_ticks)) depth[s].set(level.price_ticks,{quantity:0,venues:[]});
      const cell=depth[s].get(level.price_ticks);cell.quantity+=level.quantity;cell.venues.push(row.venue);
    }
  }
  const bids=[...depth.bid.entries()].sort((a,b)=>b[0]-a[0]).map(([price_ticks,v])=>({price_ticks,quantity:v.quantity,venues:v.venues.sort()})),asks=[...depth.ask.entries()].sort((a,b)=>a[0]-b[0]).map(([price_ticks,v])=>({price_ticks,quantity:v.quantity,venues:v.venues.sort()}));
  const best_bid_ticks=bids[0].price_ticks,best_ask_ticks=asks[0].price_ticks;
  return {as_of_receive_time_ns:as_of,max_staleness_ns:tolerance,eligible_venue_count:eligible.length,excluded,best_bid_ticks,best_bid_quantity:bids[0].quantity,best_bid_venues:bids[0].venues,best_ask_ticks,best_ask_quantity:asks[0].quantity,best_ask_venues:asks[0].venues,consolidated_book:{bids,asks},consolidated_levels:[...bids.map(x=>({side:"bid",...x})),...asks.map(x=>({side:"ask",...x}))],state:best_bid_ticks>best_ask_ticks?"crossed-consolidated":best_bid_ticks===best_ask_ticks?"locked-consolidated":"normal"};
}

export function calculate(topicId, inputs) {
  if (topicId==="D01-F05-A01") return normalizeEvents(inputs.events);
  if (topicId==="D01-F05-A02") return reconstructL2(inputs.snapshot,{snapshot_sequence:inputs.snapshot_sequence,deltas:inputs.deltas});
  if (topicId==="D01-F05-A03") return reconstructL3(inputs.snapshot_orders,{snapshot_sequence:inputs.snapshot_sequence,events:inputs.events});
  if (topicId==="D01-F05-A04") return recoverSequenceStream(inputs.arrivals,{start_sequence:inputs.start_sequence});
  if (topicId==="D01-F05-A05") return aggregatePriceLevels(inputs.orders,{depth_limit:inputs.depth_limit??null});
  if (topicId==="D01-F05-A06") return reconcileSnapshotIncrementals(inputs.snapshot,{snapshot_sequence:inputs.snapshot_sequence,incrementals:inputs.incrementals,checkpoint:inputs.checkpoint,checkpoint_sequence:inputs.checkpoint_sequence});
  if (topicId==="D01-F05-A07") return consolidateVenues(inputs.quotes,{as_of_receive_time_ns:inputs.as_of_receive_time_ns,max_staleness_ns:inputs.max_staleness_ns});
  throw new RangeError(`unsupported topic_id: ${topicId}`);
}
Full-height labplaygroundOpen full screen