Market Data WebSocket changelog
Consumer-facing history of the client wire (asyncapi.yaml is the contract; this file says what changed, for the people who parse it). Every client-visible change has its section here, newest first, each stating what each side of the deploy window sees.
The composite channels — Binance spot rejoins: its connector states each value’s minimal scale
What changed
- Nothing in the gateway, its schema or its frames. What moved is upstream, in
binance-md-connector, which no longer states the eight places Binance renders on every
surface as the instrument’s precision. Every price and size is now read at its minimal
scale — the trailing zeros stripped,
64887.68000000read as64887.68at 2 — and stated, as every moved connector states a value, at its channel’s working scale, which therefore settles at the instrument’s grid instead of at eight: a BTCUSDT book is stated at one pair, 2/5 (its 0.01 tick and 0.00001 step), instead of 8/8, inside the record’s 4/7, and an ETHUSDT book at 2/4 inside its 4/6. The rule is the venue’s, chosen because Binance’s width carries no information about the instrument: a venue that pads to its grid (Kraken, Bybit) is unchanged. - Every Binance spot book enters the merge. A composite holding a Binance spot source
beside others stops naming it
precisionand merges its levels, and one built on Binance spot alone serves. The rejoin is the one the re-cut’s section below (aprecisionexclusion reason) defines: once the source’s next snapshot is stated within the topic’s output scales it is named in arecoveredframe — whether it had left the merge or, as every Binance spot source since its move, had never entered it — and appears insrcfrom the next full image. A topic on Binance spot alone that ackedsyncingand never served saysrecovered, then serves. precisionno longer stands for padding. A source is toldprecisionbecause its book is stated finer than the topic’s output scales, and the two ways that happens are the ones already in this file: the venue outgrew its reference record (theprecisionis reachable section), or — after a record narrows — a running connector keeps stating the finer scale it had learned until it restarts (the transient cause the section directly below names). Never any more because a venue renders more digits than it trades.- The per-instrument channels are unchanged. They render shortest form and always
did, so
64887.68000000read as64887.68before this release and reads the same after; no digit a client saw changes. What a client can notice:agg_booklevels attributed to a Binance spot source appear where before the source was absent; and onbook, raw or conflated, a Binance spot book’s restatements now come from real precision moves rather than never — over a connector’s first frames after a restart a book channel can widen as it climbs to the grid, and each book-channel widen is one moresnapshot: trueframe, the form the contract has always used. Expect none in the common case: the venue’s capture widened twice, both on a trade channel, which emits nobookframe. rescaledis unchanged by this, and no reference record moves: the alternative — widening the record to the venue’s rendering once a record’s pair can move — was not taken, so no composite holding a Binance spot source is rescaled to eight places.
Deploy window: nothing in the gateway deploys for this section; binance-md-connector
does, and its restart is the window. A gateway beside the connector before its deploy
is the precision is reachable section’s world for this venue: every Binance spot book out
of every composite as precision, a composite on Binance spot alone syncing and never
serving. After:
the connector’s next snapshot of each book is stated at the minimal pair, and every
composite holding that source says recovered — or serves for the first time — and merges
it at its next tick. Nothing a client must do: one that already treats precision as a
standing word and parses decimal strings without assuming a digit count sees a source
appear, and that is all. The gateway has no “before” of its own here, for the reason the
precision is reachable section gives.
The composite channels — rescaled becomes ordinary: a source’s scales can now change
What changed
- Nothing in the gateway, its schema or its frames — the re-cut’s section below taught
the two words and they are unchanged. What changes is upstream, and it changes when a
client hears one of them. Until now the reference-data owner froze each instrument’s
scale pair at first listing and refused any catalog row that needed another, so a
composite’s output scales — derived from its sources’ records — could only move in an
incident. From this release the owner derives the pair from the catalog row at every
poll and publishes a changed pair as the record’s next version. A topic’s output
priceScale/qtyScalecan therefore move under a live subscription in normal operation, andstatus: "rescaled"is how the client is told — one frame per affected topic per change, before the first frame rendered at the new pair, exactly as the re-cut’s section below specifies. Never per tick. - Both directions happen. The scales widen when a source venue starts quoting finer
than the catalog had registered and the catalog catches up. They can narrow when a
venue coarsens its price or size grid, or when a first registration that was too fine is
corrected. A client must not assume a
rescaledpair is at or above the one it replaces. - A narrowing can leave a
vol_bookladder behind — and a re-subscribe can then answerbad_ladder.bucketSizesare held at the topic’s quantity scale, so a rescale restates them exactly or not at all. A widening always can. A narrowing cannot when a bucket size uses a decimal the narrower scale drops ("2.55"at a quantity scale going from 2 to 1): that topic keeps the pair it advertised — still exact, and norescaledis sent for it — but the same config sent again later (a reconnect re-subscribes with the full config) is judged against the current scale and refusedbad_ladder, naming the scale. Send bucket sizes no finer than the instrument’s size increment and neither can happen. - Expect a burst once, then rarity. Be ready for
rescaledon many topics at the first catalog poll after this release deploys, and for it to recur, rarely, over a subscription’s life — it is not a once-in-a-listing event. The burst: every instrument whose pair had been held at a stale first sighting corrects at once, and each affected topic — one whose output pair moves with it — saysrescaledthen; a source’s pair can change under an output pair another source still sets, and then nothing is said. The rarity: after that a topic’s scales move only when a source’s catalog row stops fitting the scales its record holds — a venue making its grid finer than the record’s headroom absorbs, or a registration being corrected — while a one-decimal tick move is absorbed and says nothing (a week of published catalog history, 2026-09-20, held no other kind of move). reason: "precision"is live already, and gains one transient cause. The re-cut’s section below saidprecisioncould not be sent until a later release let a connector state a venue’s own digits; the section directly below is that release, and the connectors moved to it venue by venue — every market-data connector states the venue’s own digits now, and that section names them. For a moved connectorprecisionmeans what the re-cut’s section says: the source’s book is stated finer than the topic’s output scales, which derive from the reference records. Two things clear it. The record widens — which, from this release, the owner does at the next catalog poll that shows the finer precision, nobody acting — or the venue’s rendering changes. (A venue that padded its rendering past its own grid — Binance spot, eight places on every value above its record — stayedprecisionon either side of this until its connector stripped the padding: the section above, which lands in the same release.) A narrowing can put a source out until its connector restarts. A stated scale never narrows within a connector’s process — the rule the section below states — so after a record narrows, a connector whose channel had learned digits above the new pair keeps stating them, every value still exact. If that leaves the source above a topic’s output scales, it is announced out of the merge by an ordinarydegradedframe withreason: "precision", and rejoins through an ordinaryrecoveredframe when the connector restarts and its channels learn the venue’s digits afresh. Nothing is mis-rendered in between: a level is never rounded into a merge. It takes a venue coarsening by more than the record’s headroom, so expect it rarely.
Deploy window: the gateway and the reference-data owner deploy independently, and only
the owner changes here. A gateway carrying the re-cut’s section below, beside an owner
before this release: rescaled only in an incident, as that section says; precision
already, from the connectors that state a venue’s own digits — and standing wherever a
venue has outgrown a record the owner still refuses to widen. The
same gateway beside an owner at this release: rescaled on each affected topic
whenever a source’s pair change moves its output pair; precision clearing within a poll
of the catalog showing the finer precision; and the transient cause above. There is no
third combination — a gateway older than the
re-cut’s section below cannot read the re-cut market-data schema this owner’s estate runs
on. What a client must do is what that section already asked: parse decimal strings rather
than assuming a digit count; if it formats to the ack’s priceScale / qtyScale, take
the new pair from the rescaled frame — in either direction; treat an unknown status as
ignorable and an unknown reason as stale.
The composite channels — precision is reachable: the connectors state the venue’s own digits
What changed
- Nothing in the gateway, its schema or its frames. No field, enum member or frame
shape moves, and this section adds no word the section below did not teach. What moved
is upstream, in the connectors, and it makes one sentence of that section true sooner
than it said:
reason: "precision"“cannot be sent until a later release lets a connector state a venue’s own digits”. This is that release — it carries the section below’s re-cut and the connectors’ move together, so a client meets both at once. - Which sources can now be announced
precision, and why. A moved connector states every price and size at the digits the venue renders on that channel — a working scale learned from the wire — where before it stated the instrument’s reference-record pair on every value. A composite’s output scales are still derived from its sources’ records (thepriceScale/qtyScalethe ack echoed), and each source is judged on the pair its book was last stated at. A record’s pair is derived with headroom — at least two places past the instrument’s tick and past its step — so a venue that renders inside that headroom enters the merge as it always did, and one that renders past it is out asprecision. One kind of venue does: one that outgrew its record — it re-gridded an instrument finer than the record’s headroom absorbs. (A venue that padded its rendering past its own grid, Binance spot, did too between its move and the section at the top of this file, which strips the padding at the connector.) The sources this can be said of are the moved venues’ books — every market-data venue’s, since this release moves them all: Kraken, Coinbase, Gate.io, Bybit, Binance futures, Binance spot, OKX, KuCoin futures and MEXC. KuCoin spot moved too but publishes no book yet, so none of its instruments is a composite source. - How it clears. As the section below says, the source rejoins through an ordinary
recoveredframe once its book is within the topic’s scales again, and two things get it there. The record widens: the catalog restates the instrument’s pair, the gateway rebinds every composite holding that source, saysrescaledwith the new pair wherever the output scales moved, and the source rejoins. Today that widening is an operator’s act — the reference-data owner refuses a changed pair in normal operation, so a record moves only when the instrument is re-listed, the incident shape the section below names — and aprecisionsource waits on it. The release that makes the pair a versioned attribute turns the wait into a poll: the owner derives the pair from the catalog row at every poll and publishes the wider pair as the record’s next version, so the source rejoins within a poll of the catalog showing the finer precision, nobody acting — that release’s own section says what else it changes. The venue’s rendering changes: a stated scale never narrows within a connector’s process — a venue that coarsens its digits, or stops padding, keeps being stated at the finer pair, exactly, with trailing zeros — so a coarser rendering reaches the gateway at that connector’s next restart, when its channels learn the venue’s digits afresh and the source’s next snapshot states the coarser pair. - Binance spot stood excluded from its move until the section at the top of this
file. Binance spot renders eight places on every surface whatever the instrument’s
grid — BTCUSDT trades on a 0.01 tick and a 0.00001 step, and every price and size still
reads
64887.68000000and0.00200000— and its moved connector stated 8/8 on every channel while the record derives 4/7, so every Binance spot book was out of every composite asprecisionfrom its first tick, and a composite on Binance spot alone never served. The fix is at the connector, in the same release as this section — the section at the top of this file: the connector states each value’s minimal scale, its books settle at the grid (BTCUSDT 2/5) and enter the merge with the topic’s scales where they are, so no client meets the exclusion. - The per-instrument channels are unchanged in shape and, nearly everywhere, in content.
trade,tob,book,mark,oi,candlestickandtickeradvertise no scale and render shortest form, so a value the venue pads reads as it did — Binance spot’s64887.68000000was64887.68before the move and is after — and a level’ssrcattribution onagg_book, rendered from the venue’s stated pair, is the same string at any stated scale. Two things a client can notice. A value the record’s grid could not hold was quarantined by the unmoved connector — never published — and a moved connector publishes it exactly, so on a venue that outgrew its record a client sees prices or sizes at more decimals than the record’s pair where before it saw nothing. And when a moved connector’s book channel learns a finer digit than it had seen, it restates the held book at the finer pair before deltas resume: onbook, raw or conflated, that is one moresnapshot: trueframe — the form the contract has always used for a restatement — carrying the same digits. On a shortest-form venue (Coinbase, Gate.io) expect those over a connector’s first frames after a restart, then rarely. rescaledis unchanged by this: still only the reference-data incident the section below names, until the release that lets a record’s pair move.
Deploy window: nothing in the gateway deploys for this section; the connectors do, one
venue at a time, and each is its own window. A gateway carrying the section below beside a
connector before its move is that section’s world for the venue: the connector states
the record’s pair, so the venue’s sources sit at or below the output scales by construction,
precision is unreachable in normal operation, and a value off the record’s grid does not
reach the wire. The same gateway beside a connector after its move: that venue’s books,
where it publishes one (KuCoin spot does not yet), can be told precision under the rule
above, and the per-instrument channels
carry every value the venue renders, exactly. The gateway has no “before” of its own here: a
gateway older than the section below cannot read the re-cut market-data schema the moved
connectors publish on (a clean break under a schema floor, taken with an environment reset),
so it never runs beside one. What a client must do is what the section below already asked —
treat an unknown reason as stale, and parse decimal strings rather than assuming a digit
count — plus one thing new with this section: expect precision to be a standing word, not
a transient one. A source told precision waits on the catalog or on the venue, not on a
restate interval.
The composite channels — a precision exclusion reason, a rescaled status, and one rule for ts
What changed
-
Two new enum members on
agg_bookandvol_bookstatus frames. The exclusionreasonvocabulary gainsprecision, and thestatusvocabulary gainsrescaled. Both enums were documented closed (“complete so consumers bind once”), so a client that generated an exhaustive enum from the spec must regenerate — or, better, treat an unknownreasonas it treatsstaleand ignore an unknownstatus, which was always the safe reading. -
reason: "precision"rides an ordinarydegradedframe: the named source’s book is stated at more decimal places than the topic’s output scales (thepriceScale/qtyScalethe subscribe ack echoed) hold, so its levels cannot enter the merge without dropping digits, and a level is never rounded into a merge. It is judged on the scale, not on the digits the book happens to hold, so a source does not flap in and out with its content. The source rejoins through an ordinaryrecoveredframe once its book is within the topic’s scales again: the topic was rescaled wide enough, or the venue’s next snapshot states a coarser scale. A rescale that widens only part of the way leaves it out. -
precisionis the one exclusion announced for a source that never entered the merge. A source that is merely not synced yet is covered by the ack’ssyncingpromise and joins silently, as it always has. Aprecisionsource is different — its book is servable, and it stays out until the catalog or the venue moves — so itsdegradedframe is sent once whether the source left the merge or was never in it. A composite whose only source is born that way answerssyncingand then, at its first tick:No
ts— nothing is included to timestamp — and nostalemarker after it, because the topic has not served yet andsyncingstill stands. A topic that was serving and loses its last source toprecisiongets the same frame first (naming every source that left with it) and then thestalemarker, so the silence that follows has a stated cause. -
precisionis also the one reason said over another. A source already announcedstaleorfx_expiredwhose exclusion turns intoprecision— its book comes back from a resync, or its rate returns, and the book is finer than the topic — is named again in adegradedframe carrying the new reason. The first word promised a heal within a restate interval or a rate tick; this one waits on the catalog, and a client should not be left waiting on the wrong one. Onerecoveredanswers both. No other change of reason is re-announced: aprecisionsource whose chain breaks and heals stays toldprecision. -
status: "rescaled"is a new status frame with two new integer fields,priceScaleandqtyScale— the ack’s own field names, carrying the topic’s new output scales:The output scales are derived from the sources’ reference records. When a source’s record restates its scale pair — a venue starts quoting finer than the catalog had registered, and the catalog catches up — the gateway rebinds every composite holding that source, and because the client sent no request for an ack to answer, the frame says so. It is emitted once, at the tier’s tick, before the first frame rendered at the new pair (so before any
recoveredframe the rebind causes, and before the image). It carries nots: it is a fact about the subscription, not about any source’s book. A rebind that leaves the output scales where they were says nothing, and so do two that land back on the pair the client was last told. -
tsis written by one rule on every composite frame. It is the newest included source’s exchange timestamp: present when an included source carries one, absent otherwise — never"0". Images and thestalemarker already behaved this way;degradedandrecoveredwrote"ts":"0"when no included source carried a timestamp, and now omit the field like the other forms. The schema never requiredts. -
Nothing else moves. Image frames, the ack,
degraded/recovered/stale, every field name and type are unchanged, and every number on the topic is still a canonical shortest-form decimal string: a client that parses decimals — rather than assuming a digit count — sees the same values before and after a rescale. Forvol_book, the ladder’sbucketSizesare restated at the new quantity scale, so the buckets a client subscribed to are the buckets it keeps receiving. An identical re-subscribe after a rescale is answered with the new pair. The per-instrument channels are untouched: the release that carries this section re-cuts the sequenced market-data schema underneath the gateway (every price and quantity now states its own scale), buttrade,tob,book,mark,oi,candlestickandtickeradvertise no scale and render shortest form, so every string on them is byte-identical before and after.
Deploy window: a gateway before the upgrade never sends either word, so a client built
to this section meets nothing new against it and must simply not wait for the words. A
gateway after the upgrade can send them to a client that has not learned them, but only
when the data gives it cause, and through this release the data does not: every connector
still states its reference record’s scale pair, which sits at or below every composite’s
output scales by construction, so precision cannot be sent until a later release lets a
connector state a venue’s own digits finer than its record; and rescaled needs a
reference record whose pair changed, which the catalog refuses in normal operation until
the release that makes a pair an attribute — the rescaled becomes ordinary section of
this file, which also gives precision its one transient cause; the connectors that now
state a venue’s own digits are named in the section directly above. The one way either
happens earlier is an incident — a reference-data rebuild that re-lists an instrument at
other scales
— and there the frames are the better outcome: before this release the composite stayed
bound to the pair the catalog had moved past and said nothing. That is the window to teach a
client the two words. A client that has not learned them by then sees, at worst, a
degraded frame whose
reason it does not recognise (treat as stale: the source is out of the merge, and the
image’s src says which sources are in) and a status frame whose status it does not
recognise (safe to ignore if it parses decimal strings; a client that fixed its formatting
to the ack’s priceScale / qtyScale must take the new pair from this frame). The ts
correction is visible on both sides at once: an old gateway may send "ts":"0" on a
degraded or recovered frame, a new one omits the field — treat ts as optional on every
composite frame, which is what the schema has always said.
The ticker channel — turnover* is the contract-aware quote notional
What changed
turnover1h/turnover24hnow carry the quote notional, not the bareprice × quantitysum. For spot instruments nothing moves: the sum IS the quote notional. For contract-sized instruments the numbers change visibly: a linear contract (FUTURES,PERPETUAL_SWAP,QUANTO_*) is the sum times the instrument’scontractMultiplier(rounded half-even atpriceScale + qtyScale, so the string keeps its scale — a 0.01-multiplier perpetual’s turnover is a hundredth of what it read before, which is what it was worth), and an inverse contract isvolume × contractValue.- Null now also means “not valued”, beside the warming window it already meant: an
instrument whose shape the rule cannot value — a linear contract with no stated
multiplier, an inverse contract with no stated contract value (every inverse row
today: reference data does not publish the field yet), an option, an index — renders
null, never0and never the raw sum. The AsyncAPI description states the rule. - No field, type or frame-shape change: both fields stay
[string, "null"]at the same scale; every othertickerfield is untouched.
Deploy window: before the upgrade a contract-sized instrument’s turnover* read the
raw contract-count sum (a mismatched unit — the correction this section lands); after, it
reads the quote notional or null. A client that already treated null as “no value”
needs no change; one that summed turnover across instruments of mixed shapes was summing
mismatched units before and gets comparable quote amounts after. The candlestick
channel carries no turnover field and is unaffected.
The ticker channel — trailing stats that decay on the stream frontier
What’s new
- The
tickerchannel is served:ticker.SYMBOL[.tier]— last price plus derived trailing 1h/24h statistics off the rolling fold, re-rendered on trades (conflated per tier) and once per elapsed stream minute — the decay edge for quiet instruments; an actively trading topic simply receives one extra image at the minute edge.asOf(the consumed event-time frontier at render) advances on decay re-renders whileseq/seqTs(fact provenance, the last trade folded) stay unchanged:asOfmoving alone IS the decay tell. - Per-window cold-start completeness rides nullability: a window’s derived fields
are null until that window’s history exists post-cold-start (between +1h and +24h the
1h numbers stand while the 24h ones are null);
windowCompleteis the 24h window’s flag; null never renders as 0, and a warm window with no trades is flat (zero sums, ratio"0", extremes carried), not null.pctChange*are ratios (not percent) at 4 decimals;turnover*are exact decimal strings atpriceScale + qtyScale(sums never pass through floating point).
Deploy window: before this deploy, ticker.* topics answer per-topic errors —
probe-and-fall-back works as for the candlestick channel’s section below.
The candlestick channel — closes on the stream frontier, and the object topic form
What’s new
- The
candlestickchannel is served:candlestick.SYMBOL.INTERVAL[.tier], the interval segment mandatory fromcomponents.schemas.candleInterval(the 8-interval set shared with the candle-history surface). Subscribing answersliveand pushes onesnapshot: trueframe — the last two closed bars plus the forming bar, oldest to newest; a never-traded instrument answersbars: []withseq/seqTsomitted, so “no data exists” is distinguishable from “snapshot lost”. The raw lane streams a forming-bar partial per folded trade; tiers serve the latest forming bar per tick. Closes never conflate: every elapsed interval closes — zero-trade intervals as a carried close (OHLC = previous close, volume"0", count 0, provenance inherited) — released once the stream’s consumed event-time frontier passes the bucket boundary, never wall clock: under catch-up closes arrive late, never wrong. Afterclosed: truefor a bucket no further update frame for that bucket arrives, and the close precedes any frame of the next bucket. Around the subscribe itself, snapshot and topic broadcasts may overlap — re-applying a final keyed on the bucket timestamp is a no-op, never desync.seq/seqTsare fact provenance (repeats legal, gaps meaningless): apply keyed on the bucket timestamp. - Subscribe requests accept the object topic form
(
components.schemas.topicSelector):{channel, instrument, interval, throttle}, exactly those keys, string-valued, normalized and echoed as the dotted string — one ack shape with the string form. An unknown key or a non-string value (e.g. a numeric throttle) answers operror/malformedrather than silently arming different semantics. The required top-levelinstrumentkey is what distinguishes a selector from a composite entry (aggBookEntry/volBookEntrycarryname/tier/sourcesand never a top-levelinstrument— an object with one always parses as a selector).
Deploy window: the old gateway answers every candlestick.* topic with a per-topic
error, and mis-collects an object entry’s inner strings as individual topic strings,
answering the subscribe ack with per-topic errors echoing those fragments (never
malformed). A probing client should treat an ack whose per-topic errors echo its
object’s field names/values as the old deploy and fall back to dotted strings. Bare
literals (numbers, true, null) in a topics array are skipped without an answer on
both sides of the window, unchanged.
Composite sources — a contract multiplier of exactly 1 is no longer a unit mismatch
What changed
agg_book/vol_booksources whosecontractMultiplieris exactly1are now accepted. The rule was always about units: a source is refused when its quantities do not speak the base asset. A multiplier of one says one contract IS one unit of the base asset, so those quantities already are base units and nothing is being mixed. The check previously refused every stated multiplier, one included.- Everything else still answers
unit_mismatch, unchanged: any multiplier other than one —0.01,100, whatever the venue states — is a genuine unit change, and conversion through the multiplier remains deferred. - No code, field, or frame shape changes:
unit_mismatchstays in the error enum with the same meaning and the samemessagetext, and an admitted source serves exactly like any other (scales echo, margins and MID normalisation apply, images render identically).
Who this unblocks: reference data states 1 on rows other venues leave unstated —
every OKX spot instrument carries it, as do linear perpetuals on several venues. Those
instruments could not be used as a composite source at all; now they can.
Deploy window: before the upgrade, a source with a stated multiplier of 1 answers
unit_mismatch per topic and nothing arms; after, it validates and serves. Sources with a
multiplier of one are the only behaviour that moves — 0 (unstated) sources and genuinely
contract-sized sources behave identically on both sides, so a client whose subscriptions
were succeeding before sees no change. A client that retried on unit_mismatch should
expect those topics to start serving; one that treats unit_mismatch as permanent needs no
change beyond re-subscribing to pick the instrument up.
Composite books, phase D — vol_book volume ladders
What’s new
- The
vol_bookchannel is served. Subscribe with an object entry (channel: "vol_book") carrying the same source vocabulary asagg_book(margins and MID normalisation included) plus the ladder:pricingModel(VWAP|BEST|WORST),bucketSizes(decimal strings at the resolved outputqtyScale— positive, distinct by value, any order: the server sorts, the response ascends, and the optional positionalbidAdjBps/askAdjBpspair with the sizes as submitted). Identity,replace, budgets, status frames, and reconnect behave exactly asagg_book; the session composite budget counts both channels together. - Frames are full ladder images (
event: "vol_book"): ascending buckets, each present side an object ofcum(the achieved cumulative quantity — the threshold when filled, the side’s whole depth when it exhausts, withfilled: falseand every deeper bucket sharing that tail),px(the model price),adjPx(with the bucket’s directional margin — bid down, ask up), andnotional— the exact swept cost atpriceScale + qtyScale. It can exceed a 64-bit integer: parse it as the arbitrary-precision decimal string it is. A side with no levels is absent (one-sided markets publish);spreadBpsrides every bucket both sides price — partial fills included and the adjusted mid nonzero (a maximal bid margin can floor a sub-tick-scale price to an adjusted zero; the field is omitted rather than divided by zero) — over the adjusted mid at a fixed two-decimal scale, negative when independent venues cross. VWAP rounds to nearest with ties away from zero (an estimate, not a quote);BEST/WORSTquote actual levels. bad_ladderis answered. Present-but-wrong ladder values (an unknown model, a non-positive/duplicate/finer-than-scale size, more thancomposites.max.bucketsbuckets, an adj array that does not pair) answerbad_ladderper topic; an absent required field (pricingModel,bucketSizes) answersmalformed, as doesmaxDepthon avol_bookentry (it isagg_bookvocabulary — ladders walk as deep as their buckets need) or a ladder field on anagg_bookentry.
Deploy window: before the upgrade, any vol_book entry answers
unknown_channel per topic (nothing arms); after, it validates and serves.
bad_ladder was already in the documented code enum (bind-once). One agg_book
edge tightens: a ladder field attached to an agg_book entry was silently ignored
before (the fields did not exist) and now answers malformed — a schema-valid frame
is unaffected, and everything else behaves identically on both sides.
Composite books, phase C — MID normalisation
What’s new
normalisationMode: "MID"is served. A MID source (with the now-meaningfulnormalisedAsset) has its prices normalised through the internal index: the current (source-quote → target) rate multiplies each price before margins, rounding directionally (bids down, asks up — an adjustment never flatters a price). Rates are read fresh at every render, and a rate refresh itself marks the composite dirty exactly like a source-book tick — a standing image re-renders on the next tier tick when its rate moves, even while every source book is quiet.fx_expiredis emitted. A MID source whose pair has no current rate (absent, or older than the index’s 30 s consumer window) leaves the merge and the topic says so —status: "degraded"withreason: "fx_expired"; a returning rate announcesrecovered. Expiry is watched on the gateway’s health cadence (1 s by default), so the announcement lands within about a second even while every source book is quiet.livenow also requires FX: a composite acksliveonly when every enabled source book is synced and every required pair is current; otherwisesyncing, kept by the first computable tick.- Validation:
normalisedAssetis required with MID and must resolve to a known asset that differs from the composite’s base asset and from the source’s own quote asset — each violation answersbad_normalisation. The touch modes stay reserved and answer the same code. - Normalisation never changes a topic’s scale identity: output
priceScale/qtyScalestay the max over the configured sources, exactly as phase B advertised them.
Deploy window: before the upgrade, any normalisationMode: "MID" entry answers
bad_normalisation per topic (nothing arms); after, it validates and serves. Nothing
else changes shape: fx_expired was already in the documented reason enum (bind-once),
and NONE-mode composites behave identically on both sides.
Composite books, phase B — agg_book
What’s new
- Object-form subscribe entries.
params.topicsnow accepts objects beside strings. An object entry subscribes a client-configured composite:channel(a required const —agg_book),name(your handle, session-scoped, 1–64 of[A-Za-z0-9._-]),tier(required — composites are always conflated), optionalmaxDepth, and 1–16sources(canonical symbols, optional per-sidebidMarginBps/askMarginBps, optionaldisabled). All sources must share one base asset; contract-sized derivatives are rejected (unit_mismatch) in v1. - The composite ack row.
live/syncingrows echo the resolved outputpriceScale/qtyScale— they bound every number the topic can carry (bind your parser’s precision once; areplacethat only togglesdisablednever changes them). Error rows carry a closedcode(see the AsyncAPI enum) plus a humanmessageyou should not parse.livemeans every enabled source book is synced and the full image pushes immediately;syncingpromises the first image at the first tick the composite becomes computable. agg_bookdata frames are full images at each tier tick: idempotent, no chain semantics, both sides always present ([]when empty — one-sided markets publish). Levels are objects —px/qtyat the output scales plussrcper-venue attribution (venue-native raw prices at each venue’s own scales).tsis the newest included source’sexchangeTsNs;seq/seqTsare stream-position provenance, not a per-topic chain.- Degradation is explicit. A source whose book cannot be served leaves the merge and
a status frame says so (
status: "degraded",excluded: [{s, reason}]); recovery announces itself (status: "recovered"). All enabled sources out → onestatus: "stale"marker, then silence, then the healing image. Thereasonenum is complete in the contract (stale,fx_expired,delisted,disabled) so you bind once; this release servesstale—fx_expiredarrives with FX normalisation. - Reconfigure with
"replace": trueon a live(name, tier): the swap is atomic (no unsubscribe, no gap, nosyncingflash while sources stay synced), a failing replace answers its error code and leaves the old config serving, and an identical re-subscribe (your reconnect replay) is an idempotent re-ack. A different config without the flag answersname_in_use— reconfiguration is never an accident. The tier is topic identity: changing it is unsubscribe + resubscribe. - Unsubscribe by the composed topic string alone (
agg_book.<name>.<tier>, byte-identical to the echo in acks and frames) — the config is never re-sent. - Budgets: at most 16 composites per session, 16 sources per composite, depth ≤ 50
(deployment-configurable). Over-budget subscribes answer
budget_exceededper topic. - Reserved, not yet served:
normalisationMode: "MID"(+normalisedAsset) answersbad_normalisationuntil MID normalisation is served (phase C, above);vol_bookentries answerunknown_channeluntil volume ladders are (phase D).
Deploy window (a not-yet-upgraded gateway vs an upgraded one):
- Do not mix object entries into frames an old gateway might serve. The old parser
reads the topics array as loose quoted strings with no nesting awareness: an object
entry comes back as a flood of spurious per-string
errorrows (its keys and values each read as a “topic”, so the ack can carry MORE rows than entries you sent, breaking order-correlation for that frame), and collection stops at the first]the parser meets — the entry’s ownsourcesarray’s — so every entry after the object in the same frame, plain string topics included, is silently dropped: never subscribed, never answered. A client-chosennamethat happens to look like a servable topic string could even be subscribed by the old parser. During the window: send composites in their own subscribe frame, after your string subscriptions, and treat “no ack row matching my composedagg_book.<name>.<tier>topic” as “old gateway — retry later”, never as an error. - On the upgraded gateway, string-topic behaviour is unchanged, including acks, all
existing channels, and unsubscribe. The ack row’s new fields
(
priceScale/qtyScale/code/message) appear only on composite rows, so existing parsers are unaffected. - A bare-string subscribe of a composite topic (
"agg_book.X.250ms"as a string entry) answerserroron both sides of the window: the config is the subscription.