How do you turn provider-shaped trade and BBO messages into one loss-aware event contract without erasing venue clocks or sequence identity?
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 versioned normalized events with enough lineage to decide whether the state is usable.
The one idea to remember
Normalization is a loss-aware translation boundary. It standardizes names and units while keeping raw integer prices, provider sequence, venue event time, consumer receive time, and explicit unknowns.
The contract before the code
Provider adapters map documented trade and BBO fields into a versioned common schema; arrival order is preserved and no trade side is inferred. Normalization is not correction, ordering across venues, trade signing, or order-book reconstruction.
| Contract dimension | Canonical choice | Why it is visible |
|---|---|---|
| Price | Integer ticks or atoms plus scale | Prevents ambiguous floating-point keys |
| Sequence | One documented session/unit domain | Makes gaps and anchors testable |
| Time | Event and receive clocks remain separate | Prevents arrival time from impersonating venue causality |
| Quantity | Topic-selected semantics | Prevents replacement, delta, and order quantity from being conflated |
| Invalid state | Reject, buffer, recover, or exclude explicitly | Prevents false precision |
Nearby variant. Production adapters also need correction/break semantics, condition codes, session identity, and an unknown-message policy.
State and mathematics
The core relationship is price = integer price atoms / declared price scale. Integer atoms plus an explicit scale keep the source price basis reproducible; monotone venue sequence checks detect duplicate or reversed adapter output without claiming cross-venue order.
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
- Preserve integer atoms and scale beside decimal prices.
- Preserve supplied fields without inventing trade side.
- Compute receive minus event time per record.
- Retain arrival order without manufacturing cross-venue causality.
Exact calculation
For every row, latency_ns = receive_time_ns - event_time_ns; price atoms remain exact while the decimal view divides by 10,000.
{
"event_count": 24,
"trade_count": 8,
"quote_count": 16,
"venue_count": 1,
"max_latency_ns": 350,
"first_event": {
"schema_version": 1,
"venue": "XNAS",
"instrument": "SYN1",
"sequence": 100,
"event_time_ns": 1000000,
"receive_time_ns": 1000210,
"latency_ns": 210,
"price_scale": 10000,
"kind": "quote",
"bid_price_atoms": 1000000,
"bid_price": 100.0,
"bid_quantity": 70,
"ask_price_atoms": 1000200,
"ask_price": 100.02,
"ask_quantity": 65,
"quote_state": "normal"
}
}
Independent check. 8 trades + 16 quotes = 24 total events. The values are author-derived from labeled synthetic inputs, not provider observations.
Diagnostics are part of the answer
| Diagnostic | Decision it supports |
|---|---|
| Event count by kind | Explains whether the output is safe to publish or why it is bounded |
| Venue count | Explains whether the output is safe to publish or why it is bounded |
| Maximum observed latency | Explains whether the output is safe to publish or why it is bounded |
| Raw atoms plus price scale | Explains whether the output is safe to publish or why it is bounded |
| Locked versus normal quote state | Explains 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:
- Run the canonical scenario to the final state.
- Switch to the nearest boundary and identify the first changed diagnostic.
- Run the rejection or exclusion scenario.
- Explain why the previous valid state must not silently absorb the bad input.
Failure diagnosis
| Symptom | Likely contract defect | Safe response |
|---|---|---|
| Sequence jumps forward | Missing causal input | Buffer and recover; do not advance published state |
| Quantity becomes impossible | Wrong semantics or lifecycle reference | Reject and inspect the adapter |
| Snapshot and replay disagree | Anchor, update, or source drift | Publish the difference ledger and rebuild |
| Venue view is stale or halted | As-of eligibility failure | Exclude with a reason |
| Output looks valid but lineage is absent | Silent assumption | Treat state as unsupported |
Highest-risk misuse: Do not sort independent venues by timestamp and call the result exchange causality.
Implementation walkthrough
The reference implementation is intentionally explicit:
- validate identity, units, clocks, and provider-adapted fields;
- apply one deterministic transition;
- refuse or isolate unsafe state;
- return values and diagnostics together;
- 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
Normalized quote and trade events feed Trade Classification; reconstructed depth, not this schema alone, feeds Queue Imbalance. 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 TotalView-ITCH 5.0, 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 D11-F03-A02 — Queue Imbalance 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.
Rendered from the canonical Mermaid sources linked by this article.
Trade-and-Quote Event Normalization system flow
Takeaway: a downstream statistic cannot repair an upstream causal or sequence-integrity defect.
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.
Topic-specific source roles
| Source | Role in this package |
|---|---|
SRC-ITCH | Direct primary support for this topic's selected behavior or boundary |
SRC-COINBASE | Direct 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
- Organization or authors: Nasdaq
- Source type: Official exchange feed specification
- Publication or effective date: 2015-03-06
- Version: 5.0
- URL or DOI: https://www.nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/NQTVITCHSpecification_5.0.pdf
- Accessed: 2026-07-30
- Jurisdiction: Nasdaq U.S.
- Supports: sequenced order lifecycle messages, integer prices, timestamps, and order identifiers
- Limitations: Provider-specific; it does not define this package's provider-neutral schema.
SRC-GLIMPSE — Nasdaq GLIMPSE 5.0
- Organization or authors: Nasdaq
- Source type: Official snapshot/recovery specification
- Publication or effective date: 2015-03-19
- Version: 5.0
- URL or DOI: https://www.nasdaqtrader.com/content/technicalsupport/specifications/dataproducts/NQGlimpseSpecification_5.0.pdf
- Accessed: 2026-07-30
- Jurisdiction: Nasdaq U.S.
- Supports: snapshot sequence anchors and recovery positioning
- Limitations: Snapshot semantics are feed-specific.
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
- Organization or authors: Cboe Global Markets
- Source type: Official exchange feed specification
- Publication or effective date: Living specification
- Version: Current page accessed 2026-07-30
- URL or DOI: https://www.cboe.com/document/tech-spec/document/technical-specifications/cboe-titanium-u.s.-equitiesoptions-multicast-pitch-specification
- Accessed: 2026-07-30
- Jurisdiction: Cboe U.S.
- Supports: sequenced units, gap replay, spin images, buffering, and order events
- Limitations: Exact rules vary by Cboe market and release.
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.
Full dependency-light reference implementations in both supported languages.
/* 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}`);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.