Skip to content

Migrating from exchange APIs

Moving a bot off an exchange’s own API, or off several.

This page is about the differences that actually cost you time. It is not a sales pitch; where the platform is worse for your case, it says so.

One client instead of N. Every venue’s REST is its own dialect: its own symbols, its own rate limits, its own funding convention, its own idea of what a “24h change” is. Here there is one request shape and one answer shape, and the venue’s own spelling comes back in venue_symbol for when you need to send an order.

One question instead of N round trips. “Which instruments have positive funding on at least three venues, where the funding reading is current” is one query:

count(venues where funding_8h > 0 and status(funding) = present) >= 3

Against exchange APIs that is a fan-out, a join, and a set of decisions about what to do when one venue is slow to answer — decisions you would be making in your own code, silently, differently each time.

Normalisation you did not write. Funding is per-interval as the venue publishes it, and funding_8h is the comparable figure wherever the registry holds the listing’s interval, or a continuous venue’s rate unit (elsewhere it is null, and the listing is not compared); both, and their sources, are stated per listing in GET /v2/instruments so you can check them rather than trust them.

A missing value is not a zero. Exchange APIs are inconsistent about this — some omit a field, some send null, some send 0, some send the last value they had. Here a value is always an object with a status, and five things are told apart: present, stale, missing, not published by the venue, not collected by the platform. Code that treated absence as zero will need a branch. That branch is the bug you had.

Comparisons can answer unknown. If a reading is stale, funding > 0 is neither true nor false. all(...) over a set with one stale reading is unknown, not false. Ask for include_unknown and you get those instruments back labelled, instead of a screen that quietly narrowed itself.

Time is receipt time. A historical answer uses what was visible at that instant, not what is known now. A reading that arrived late was not available to you then, and a backtest that used it would be measuring information you did not have.

Venues are never averaged. There is no “the price of BTC”. If you want one, you compute it, and you will have to decide what to do about the venue that is stale — which is the decision the platform refuses to make for you.

what you did what you do here
GET /fapi/v1/premiumIndex per venue POST /v2/snapshots with where, or a DQL condition on funding
GET /fapi/v1/klines per symbol, per venue POST /v2/candles, many listings in one request
GET /api/v3/depth, then compute depth yourself POST /v2/books, or depth_bid_value(10bps) in a query
exchange info for tick size and lot size GET /v2/instruments
a websocket per venue, reconnect logic per venue GET /v2/stream for snapshot layers, or GET /v2/trades/stream for raw trades
a venue’s trade-history endpoint POST /v2/trades, bounded stored events with explicit gap coverage
your own staleness heuristics status(...) and age(...), stated per value

Said plainly, because finding out later is worse:

  • No orders. This is data. It will tell you the tick size; it will not place anything.
  • DQL and Agent are perpetual-only. The market-data endpoints can read spot with kind: "spot" and both kinds with kind: "all".
  • No trade-tape functions in DQL. Stored trades are available through POST /v2/trades and GET /v2/trades/stream; the stream has bounded subscribers and no replay.
  • No per-venue websocket parity. The stream carries the layers the platform collects, at the platform’s cadence, not every message the venue emits.
  • Only the venues in Venues in DQL. A venue the platform does not collect is not there, and the list is read live rather than promised.
  1. GET /v2/venues and GET /v2/dql/catalogue — no key. Learn what exists and what a query may name. Venues and instruments are paged (limit defaults to 500); follow next_cursor until null and keep each page’s registry_revision the same before treating the catalogue as complete.
  2. Write the condition you care about and send it to POST /v2/dql/parse until it parses. Still no key.
  3. Get a key, run it on POST /v2/screen with include_unknown: true, and look at what came back unknown — that is your real coverage.
  4. Replace your staleness heuristics with status(...) and delete them.