Skip to content
Code Archaeology Read the codebase like a book. Argue with every chapter.

Chapter 090: Adapter Boom — Databento, Bybit, dYdX, Polymarket, OKX (2023-10 → 2024-09)

Period: 2023-10-30 → 2024-09-21 (~11 months) Tags: v1.181.0v1.200.0 Why this chapter exists: This chapter is about breadth. While chapter 8 moves Redis and clocks to Rust (vertical depth), this chapter is the moment the platform’s adapter portfolio explodes. Five major venues land in 11 months. Each adapter forces refinements in the platform — RetryManagerPool (common HTTP retry logic), composite bar types, the @customdataclass decorator, OrderBook batching with F_LAST, error modeling, and more. By v1.200 the platform looks like a serious multi-venue engine. This chapter runs in parallel with chapter 8 and chapter 10, but is presented separately because the adapter additions are independent of the engine port. It overlaps chronologically with both.

First-commit dateAdapterNotes
2020-09-21binance(chapter 2 era; long-running, many releases)
2020-09-21bitmex(chapter 2 era; goes through several rewrites)
2021-03-18betfair(chapter 4 era)
2022-05-29deribit(chapter 6 era; long-running)
2023-10-30databentoMarket-data only: Tier-1 US equities, futures, options.
2023-12-04bybitFirst major hand-written CEX since Binance.
2024-08-22dydxFirst DEX adapter (gRPC + websockets).
2024-09-18polymarketFirst prediction-market / BinaryOption adapter.
2024-09-21okxMajor CEX.

(Hyperliquid 2025-07, Kraken 2025-11, blockchain 2025-04, Architect AX 2026-01, Coinbase 2026-04 are chapter 13–15.)

DateTagHeadline
2023-10-22v1.179.0Pre-Databento.
2023-11-03v1.180.0
2023-12-23v1.182.0Bybit lands. (chapter 8 milestone too)
2024-01-12v1.183.0Atomic clock + Rust logger (chapter 8)
2024-01-29v1.184.0
2024-03-15v1.189.0(continuing)
2024-03-22v1.190.0
2024-05-31v1.194.0
2024-06-17v1.195.0
2024-07-05v1.196.0Composite bar types (#1859), DataEngine OrderBookDelta F_LAST buffering.
2024-08-02v1.197.0MessageBus v2 in Rust, DataEngine v2 in Rust. (chapter 10 milestone)
2024-08-09v1.198.0@customdataclass decorator (#1828) — custom data finally low-friction.
2024-08-19v1.199.0Refactor of error modeling in Rust. Reconciliation robustness.
2024-09-07v1.200.0dYdX integration lands (a 20-PR mega-merge from @davidsblom). Composite bar types stable. RetryManagerPool for adapter retries.

Architecture moves driven by adapter requirements

Section titled “Architecture moves driven by adapter requirements”

OrderBookDeltas.batchF_LAST flag-based batching (v1.196)

Section titled “OrderBookDeltas.batch — F_LAST flag-based batching (v1.196)”

Crypto venues (Bybit, dYdX, Polymarket) deliver order book updates as groups of deltas in a single websocket message; they all share an update sequence. Naively processing them one at a time would publish intermediate inconsistent states (book briefly missing one side mid-update). Solution: introduce a F_LAST flag on each delta; the data engine buffers deltas until it sees F_LAST, then publishes the whole batch atomically. This also solves the catalog story — Parquet data needs deterministic batching boundaries to reproduce live behaviour.

By 2024 several adapters (Polymarket, Databento, Tardis) had adapter-specific data types (e.g. BinanceFuturesMarkPriceUpdate, DYDXOraclePrice). Each needed boilerplate: register Arrow schema, register serializer, register publication topic. The @customdataclass decorator collapses all of that into one decorator. From this point, adding a custom data type to an adapter is a 5-line PR, not a 50-line one.

Every adapter implemented its own retry logic (exponential backoff, jitter, max retries). This drifted across adapters and produced subtle behavioural differences. RetryManagerPool centralises retry logic with a shared pool of retry managers, configurable per-call.

Reconciliation overhaul (v1.197 → v1.199)

Section titled “Reconciliation overhaul (v1.197 → v1.199)”

Live reconciliation — making the engine’s view of orders and positions match the venue’s after a restart — is the hardest correctness problem the project faces. v1.197–v1.199 add:

  • LiveExecEngineConfig.generate_missing_orders — generate inferred orders when the venue reports a position that the engine doesn’t know about.
  • Improved robustness when internal positions disagree with external.
  • Inferred orders also flow through the matching pipeline.

This is the seed of the much larger reconciliation rework in chapters 13–14, but the engine first acknowledges here that internal and venue state can disagree and needs tools to reconcile.

Bars aggregated from other bar types (e.g. 5-minute bars from 1-minute bars). Requested by users running multi-timeframe strategies. The implementation requires the data engine to be aware of bar lineage. This proves easier after MessageBus v2 (chapter 10) — composite bars are a pub/sub layered subscription.

MessageBus v2 + DataEngine v2 in Rust (v1.197)

Section titled “MessageBus v2 + DataEngine v2 in Rust (v1.197)”

These are the chapter 10 headline items, but they ship during the adapter boom because the boom requires them. The pre-v2 MessageBus was Cython-bound and couldn’t be called from Rust adapters cleanly. The v2 version is Rust-native; PyO3 bindings let Python call into it. Adapters can now publish events directly from their Rust paths without bouncing through Python.

Add adapters in order of demand, not difficulty

Section titled “Add adapters in order of demand, not difficulty”

The order is suspicious: Databento (data only — easy), Bybit (CEX, but similar to Binance), dYdX (DEX — hard), Polymarket (prediction market — weird), OKX (CEX, complicated). The team did the easy ones first and let the hard ones bake. This is also why dYdX needed 20 PRs to land (#1861, #1868, #1873, #1874, …).

dYdX uses gRPC for the chain-side stuff and websockets for the order book. Neither pattern existed in the codebase. The dYdX integration forced introducing tonic (gRPC), and the gRPC retry policy (6ee1d7b386 Add retry policy to gRPC client with fallback (#1887)). Future DEX adapters (Hyperliquid, Polymarket additions) borrow this infrastructure.

Polymarket trades binary outcomes (event happens or doesn’t), so it needs an instrument type that doesn’t fit the standard “price × quantity = value” mold. Adding BinaryOption validates the data model’s ability to absorb new instrument shapes — it’s been a goal since chapter 4’s Data abstract base.

Errors-as-data: error modeling overhaul (v1.199, v1.200)

Section titled “Errors-as-data: error modeling overhaul (v1.199, v1.200)”

Rust adapters were using a mix of anyhow::Error, custom enums, and Result<_, String>. The v1.199–v1.200 work standardises on per-domain error enums. Python sees them as HttpClientError, WebSocketClientError etc. This is what enables (chapter 14+) the due_post_only flag on OrderRejected and similar fine-grained error reporting.

  • Generic Ticker data type — removed (chapter 8); replaced by per-adapter Ticker subtypes.
  • OrderBook subscription naming: subscribe_order_book_snapshotssubscribe_order_book_at_interval (clarity).
  • Single-source retry logic in adapters — replaced by RetryManagerPool.
  • Custom data type boilerplate — replaced by @customdataclass.
  • Hard-coded WebSocket reconnection per adapter — moved to WebSocketClient (Rust) with shared retry logic.

DEXs are not exchanges. They have:

  • A chain (block height, finality semantics, transaction nonce).
  • An on-chain orderbook with a specific gRPC API.
  • A separate websocket for indexer-derived market data.
  • A wallet abstraction that’s specific to the chain.
  • Margin and account state computed from chain state, not API state.

Mapping that onto Nautilus’s CEX-shaped abstractions took iteration. Each PR shaved off another mismatch (gRPC retries, account state parsing, websocket schema, position sign conventions, candle parsing, etc.). Reading the v1.200 release notes’ dYdX PR list end-to-end is a tour of “what’s hard about DEX integration.”

Why did Polymarket need its own instrument type?

Section titled “Why did Polymarket need its own instrument type?”

A binary outcome’s price is bounded [0, 1]. Existing instrument types assume unbounded price ranges and have margin / pnl / leverage fields that don’t apply to binary outcomes. BinaryOption lets the risk engine and portfolio compute the right notional / exposure / PnL for these markets without special-cases scattered through the engine. (Polymarket also brought USDC.e (PoS) as a recognised currency and the compute_effective_deltas config option.)

Why are these adapters all in nautilus_trader/adapters/<name>/

Section titled “Why are these adapters all in nautilus_trader/adapters/<name>/”

and in crates/adapters/<name>/?

Because the migration to Rust is in progress. New adapters target Rust implementations with PyO3 bindings. The Python directory often holds the factory (which the user imports) and the config classes; the Rust crate holds the actual data/exec client. Older adapters (Binance, Betfair, IB) still have a substantial Python tail.

What’s the difference between an _pyo3 suffixed adapter directory

Section titled “What’s the difference between an _pyo3 suffixed adapter directory”

and the un-suffixed one?

interactive_brokers_pyo3 is the PyO3-bound adapter; the un-suffixed interactive_brokers is the older Cython implementation. Both are shipped during the migration so users can choose. The PyO3 one is the future. (See chapter 14 for the IB Rust adapter.)

  • Adapter README’s are not a substitute for reading the venue’s API docs. Each adapter’s README explains how Nautilus maps to that venue, but quirks (e.g. Bybit’s empty-string position side, Polymarket’s USDC.e currency, dYdX’s marketType parameter) only become obvious when you read the venue API.
  • The RetryManagerPool pattern is the canonical retry surface. Don’t hand-roll exponential backoff in a new adapter.
  • @customdataclass is the canonical custom-data path. Don’t subclass Data by hand unless you have a reason.
  • The F_LAST / F_SNAPSHOT flag bitmask is the way to express batch boundaries. Adapters that emit one delta per message should set F_LAST on the single delta. Adapters that emit batched deltas should set F_LAST only on the final one.
  • Reading reconciliation code: OrderStatusReport is “what the venue thinks”, Order is “what Nautilus has”. Reconciliation reconciles the two. Inferred orders/fills (the engine creating events to align state) are emitted with venue IDs but Inferred=True. They are recorded in the catalog like real events.