Esc

    Reference

    Aggregation

    How a node turns the quotes of several venues into one price, one confidence interval and status flags, and when it publishes them.

    Each node aggregates on its own. For each feed, it turns the latest quotes of the configured venues into one price, one conf and a set of status flags. Then it decides whether to publish.

    The governing bias

    Degrade loudly, halt rarely, never silently narrow. A wider conf is the pressure-release valve. A node halts a feed in two cases only, because no honest price exists to widen around:

    • too few live sources;
    • a price outside the sanity band.

    The pipeline

    text
    observations ──▶ per-venue quote     (microprice, staleness, crossed books)
                 ──▶ outlier rejection   (MAD, unweighted, never below quorum)
                 ──▶ weight cap          (no source may buy the median)
                 ──▶ weighted median     (plain median when venues disagree and none can go)
                 ──▶ confidence          (disagreement ∨ spread ∨ floor)
                 ──▶ circuit breakers    (jump corroboration, sanity band)
                 ──▶ publication policy  (rate cap, then deviation ∨ heartbeat)

    The node evaluates a feed when one of its inputs changes: a new observation, a disconnect, or a new reading of its peg. A timer also evaluates every feed every 25 ms (tick_interval_ms). The timer catches heartbeats and stale venues.

    The aggregator has no clock and no I/O. A recorded sequence of observations therefore replays to the same prices on any machine.

    Source price per venue

    For each venue of a feed, the node derives one number. The source config lists the observation kinds in order of preference. For a spot feed, the default is the top of the book, then the last trade.

    text
    mid          = (bid + ask) / 2
    micro        = (bid × ask_qty + ask × bid_qty) / (bid_qty + ask_qty)
    source_price = micro if both sides are present, else the last trade

    The microprice leans toward the side with pressure, so it degrades better than the mid on a thin book. The rules for a book quote are:

    • The node drops a crossed book (bid ≥ ask). It never repairs it.
    • A price that is zero or negative is invalid.
    • A fallback to the last trade does not degrade the feed by itself. The missing spread term makes conf wider instead.

    Impact price from the book

    A feed can set impact_notional. The node then prices each venue at the average fill for that notional, from the local book of the venue:

    text
    impact_bid, impact_ask = average fill prices for impact_notional, sell side and buy side
    source_price = (impact_bid + impact_ask) / 2
                 + (mid_now − mid_at_snapshot)     when the top of book is newer than the book
    half_spread  = (impact_ask − impact_bid) / 2   a thin book makes conf wider

    The depth sets the level, and the top of the book sets the timing. One large order at the top of the book cannot lean an impact price. The rules are:

    • A stale book falls back to the microprice.
    • A book with too few levels for the notional gives no impact price.
    • The impact price replaces only a top-of-book price. A mark or index from a venue stays as the venue computes it.

    The node keeps each venue book from a snapshot and the changes after it. Where the venue numbers its changes or sends a checksum, the node proves that each change follows the last one. A failed proof drops the connection, and the reconnect brings a fresh snapshot.

    Venue Stream Proof
    Kraken book, depth 100 CRC32 of the top 10 levels
    OKX books, 400 levels seqId and prevSeqId, and the CRC32 of the top 25 levels when the checksum is not 0
    Deribit book.<instrument>.100ms change_id and prev_change_id
    Bybit orderbook.50 Update id u rises by one; u = 1 or a snapshot resets
    Coinbase Advanced Trade level2, 25 products a connection sequence_num on every message
    Binance @depth@100ms diffs on a REST snapshot Each diff follows the last update id
    Hyperliquid l2Book, 20 levels Not necessary: each message is the whole book
    Bitfinex book, 25 levels CRC32 of all 25 levels, every few seconds
    MEXC spot@public.limit.depth.v3.api.pb on spot, sub.depth.full on perpetuals; 20 levels Not necessary: each message is the whole book
    KuCoin /spotMarket/level2Depth50 and /contractMarket/level2Depth50, 50 levels Not necessary: each message is the whole book
    Bitstamp order_book, 100 levels Not necessary: each message is the whole book
    Bitget books, 1,000 levels Each change names the seq of the last one as pseq
    Gate spot.order_book and futures.order_book, 50 levels Not necessary: each message is the whole book
    Lighter order_book, the whole book Each change names the nonce of the last one as begin_nonce
    dYdX v4_orderbook from the indexer, the whole book None: the changes carry no number. A crossed book ends the connection

    OKX swaps and the perpetuals of KuCoin, MEXC and Gate count in contracts. Bitget's coin-margined perpetuals count in USD, which the node divides by the price, as for Deribit. Each such source must set contract_size, or the node refuses the config: the ctVal of an OKX swap, the multiplier of a KuCoin contract, the contractSize of a MEXC contract, or the quanto_multiplier of a Gate contract. The node then reports its sizes in coins.

    Coinbase sends its ticker only on a trade. The node therefore takes the Coinbase top of book from the level2 book and uses the ticker only for trades.

    Staleness filter

    The node drops a venue whose newest usable observation is too old. Two ages count:

    • Confirmed age: the time since the connection of the venue last confirmed the value. Each frame on the connection (a data message, a heartbeat or a pong) confirms each book, mark, index and funding value that the connection has delivered. After 1 s with no frame, the node sends a WebSocket ping. A trade is an event, not a state, so only its own receipt counts.
    • Unchanged age: the time since the value itself arrived.
    Feed type Confirmed age Unchanged age
    Spot, mark, index, mid 2000 ms 5 min
    Slow feeds (funding, open interest) 90 s 5 min

    A quiet value on a live connection stays current. A silent connection confirms nothing, and its values go stale after 2 s. The unchanged limit catches a connection that stays up while the venue stops the stream of one instrument. It is long because a quiet value can rest: Bybit sends the fields of a perpetual only when they change.

    A feed can set its own confirmed-age limit with max_age_ms.

    The node measures both ages with its own receipt clock, never with the venue timestamp. After a reconnect, the node drops the old state of that venue at once.

    USDT to USD normalisation

    Binance, Bybit, OKX, KuCoin, MEXC, Bitget and Gate quote in USDT, and the feeds use the USDT books of Bitfinex. A /USD feed converts each of these sources through the peg feed CRYPTO.USDT/USD, which the node prices from Coinbase, Kraken and Bitstamp. Hyperliquid, Lighter and dYdX quote in USDC, and the feeds take it at par. See Feeds for the peg symbol.

    text
    usd_price = usdt_price × peg_price
    rel_conf² = (conf / price)² + (conf_peg / price_peg)²

    The node converts before outlier rejection. Otherwise a depeg would look like every USDT venue disagreeing with the USD venues.

    Peg state Effect on each dependent feed
    Live, conf up to 50 bp Converted. SYNTHETIC_QUOTE set. conf widened in quadrature.
    Live, conf above 50 bp (max_peg_conf_bps) Converted. SYNTHETIC_QUOTE and DEGRADED set.
    Halted or missing The converted sources drop out, with status peg_unavailable. The native USD sources price the feed, and DEGRADED is set. With fewer native sources than min_sources, the feed halts.

    A halted peg has no current reading, so the node stops using its last value at once. Without a peg, only the native USD venues count. The node never publishes a USDT number under a /USD symbol.

    A peg feed cannot itself need conversion, and a feed cannot be its own peg. The node refuses such a config.

    Outlier rejection

    The node uses the median absolute deviation (MAD):

    text
    m    = median(source_prices)
    mad  = median(|xᵢ − m|)
    σ̂    = 1.4826 × mad
    drop xᵢ where |xᵢ − m| > max(k × σ̂, floor_bps × m)      k = 4, floor = 10 bp

    The floor prevents a rejection over rounding noise when all venues agree and σ̂ goes to zero. The test is unweighted, because a venue controls its own weight.

    The node never drops below min_sources. If rejection would go below it, the node keeps every quote and sets DEGRADED.

    Two sources. Two quotes sit at the same distance from their midpoint, so the test can never reject one of them. If they differ by more than the 10 bp floor, the node keeps both and sets DEGRADED.

    Weighted aggregate

    Each surviving source gets a weight:

    text
    wᵢ = w_venue      static weight from the config, 1 by default
       × w_depth      sqrt(depth within 1 bp / 100000), capped at 2; only when every source has a book
       × w_spread     1 / (1 + spread_bps)

    Age has no weight, because every quote that reaches the median is current within the staleness limit.

    The node then caps each weight at 3 times the median weight. A venue controls its own spread and depth, and a sample with more than half the total weight is the weighted median. The cap stops a tight book from buying the price.

    text
    price = weighted_median(xᵢ, wᵢ)

    The node uses a weighted median, not a weighted mean. One venue with an absurd value cannot move a median, whatever its weight.

    When rejection was blocked. If the node kept every quote because of the quorum, the price is the plain median. With two sources, the price is the quote nearer to the last published price. One moved venue makes conf wider but cannot move the price. conf widens to cover the farthest quote that the node kept.

    Confidence

    text
    conf = max(
        max(weighted_mad, unweighted_mad) × 1.4826,   how much the venues disagree
        weighted mean of the half-spreads,             nobody trades inside the book
        price × 1 bp                                   the floor
    )

    The unweighted MAD is necessary. A source with more than half the weight makes the weighted MAD zero, however far away the other sources are.

    Publish policy

    The node publishes when one of the triggers fires and the rate cap allows it:

    text
    publish if:
        |price − last_published| / last_published ≥ deviation_bps
        OR now − last_publish ≥ heartbeat
        OR the flags changed
    AND now − last_publish ≥ min_interval
    Setting Default Slow feeds
    deviation_bps 1 bp 1 bp
    heartbeat (fixed) 1 s 10 s
    min_interval_ms (rate cap) 50 ms 1 s

    The rate cap comes first. It bounds the signing cost and the subscriber bandwidth during volatility. The cap does not drop a trigger: a change of flags publishes at the next allowed instant.

    deviation_bps defaults to the conf floor. A smaller move is inside the uncertainty of the oracle. Each feed can set deviation_bps and min_interval_ms.

    The heartbeat is a protocol rule, not a setting. FUNDING_8H and OPEN_INTEREST feeds are slow; every other feed is not. Consumers derive their staleness limit from the heartbeat. The Rust SDK allows 2 s for a price feed and 25 s for a slow feed.

    observation_time_ns in the update is the newest receipt time among the contributing observations.

    Circuit breakers

    Condition Action
    Live sources below min_sources HALTED. The node publishes the last good price at heartbeat cadence.
    Outlier rejection would go below min_sources DEGRADED, conf widened.
    Move of 300 bp or more from the last published price, and fewer than 2 venues moved that far DEGRADED, conf widened to cover the jump. The price is not suppressed.
    Peg feed halted or missing DEGRADED on each feed that needs conversion, priced from its native sources; HALTED if they are fewer than min_sources.
    Peg conf above max_peg_conf_bps (50 bp) DEGRADED on each feed that needs conversion.
    Price outside 0.5× to 2× of the reference EMA (half-life 5 minutes) HALTED.

    The sanity band catches the case where every venue agrees and every venue is wrong, such as a bad instrument mapping or a redenomination. Slow feeds turn the sanity band off, because a funding rate crosses zero routinely. Halted prices do not update the reference EMA.

    Status flags

    Each signed update carries a 16-bit flags field.

    Bit Flag Meaning to a consumer
    0 HALTED Do not use the price for settlement or liquidation. The update carries the last good price so that you can display something. conf is set to the size of the price itself.
    1 DEGRADED The price is usable, but the inputs are impaired and conf is wider. Act on conf.
    2 SYNTHETIC_QUOTE At least one source went through a peg conversion. The price carries the uncertainty of the peg.
    3–15 reserved Always 0.

    A decoder rejects an update with a reserved bit set. It does not mask the bit. An old client that cannot read a new warning must not conclude that no warning exists.

    The SDK combines the flags of the nodes with its own checks into one status: ok, degraded or halted. See Trust model.

    Per-feed settings

    An operator sets these values for each feed in the node config:

    Setting Default Effect
    min_sources 2, or 1 for slow feeds Fewest live sources for a usable price.
    deviation_bps 1 Move that triggers a publication.
    min_interval_ms 50, or 1000 for slow Rate cap.
    max_age_ms 2000, or 90000 for slow Confirmed-age limit for each source. A quiet book on a live connection stays current, so the default suits a stablecoin pair too.
    impact_notional none Price each venue at the average fill for this notional, in quote units.
    peg none The peg feed for converted sources, such as CRYPTO.USDT/USD.
    max_peg_conf_bps 50 Peg conf above which the feed degrades.

    Each source also has venue, market, symbol, and optional kinds, weight, converted and contract_size. See Feeds.

    A slow feed (funding, open interest) also has a 90 s staleness limit and no sanity band. The kind of the symbol decides this, so the config has no setting for it.

    Funding is not normalised

    Caution

    A FUNDING_8H feed promises a rate normalised to 8 hours. Only Deribit publishes such a rate. Every other venue publishes a fraction over its own interval: hourly on Kraken Futures, 8, 4 or 1 hour on Binance by contract.

    Nothing converts between the intervals. The funding stream states the interval of each venue, but the aggregator does not use it yet. A FUNDING_8H feed with sources on different intervals therefore takes a median across numbers that do not compare. A 1-hour rate and an 8-hour rate differ by 8 times, and nothing halts or degrades.

    Safe configurations today:

    • Deribit alone, which publishes a real 8-hour rate;
    • a single venue;
    • several venues on a contract where you confirmed the same interval. BTC and ETH majors are 8 hours on Binance, Bybit and OKX.

    Treat funding from several venues as unsupported until the aggregator converts by interval. To compare venues now, read the funding stream and divide each rate by its interval.