Building Conformant Rust Adapters for Trading Venues
Summary
This engineering guide explains how to build Rust-native adapters that connect NautilusTrader to exchanges and data providers. It covers venue-specific data and execution clients, configuration and Python exposure through PyO3, plus contracts for credentials, symbol identity, payload precision, client lifecycle, backpressure, task management, HTTP requests, authentication, WebSocket subscriptions, and reconnection. It distinguishes shared requirements from common patterns, examples, and venue-specific exceptions.
A major focus is reliable execution handling: reconciliation, order identity, event ordering and deduplication, command outcomes, commissions, and strategy-facing rejection reasons. The guide points to shared components and testing specifications, and emphasizes documenting exceptions with evidence and tests. It also covers deterministic simulation, fuzzing, and integration documentation. This is platform implementation guidance rather than a trading method or evidence of strategy performance; its value is in making venue behavior explicit and adapter behavior consistent and testable.
Key ideas
- Adapters should preserve venue semantics while emitting valid platform data and execution events.
- Use shared contracts for transport, task lifecycle, authentication, subscription state, and execution evidence where applicable.
- Document venue-specific exceptions and test them against the behavior that requires them.
- Execution clients need careful reconciliation, event ordering, deduplication, and outcome reporting.
- Deterministic simulation, fuzzing, and capability-focused documentation support adapter reliability.
Tags
Full text
# Adapters # Adapters ## Introduction Use this guide to build or extend a Rust-native adapter for NautilusTrader. Adapters connect the platform to venues and data providers, preserve venue semantics, produce valid Nautilus domain events, and make uncertain outcomes explicit. They implement the platform data and execution client traits in Rust, then expose configs, factories, and selected low-level APIs to Python through PyO3. :::note For out-of-tree adapters implemented in Python or an independent Rust/PyO3 package, use the [Python adapter interface](python_adapters.md). This guide covers in-tree Rust adapters. ::: Use reference adapters selectively. Their layouts reflect different venue protocols, product families, and implementation histories. | Adapter | Useful reference | | ------------------ | ------------------------------------------------------------------------------------------------------ | | [Bybit][bybit] | Multi-product HTTP and WebSocket clients, options data, and execution outcome handling. | | [OKX][okx] | Public, private, and business WebSocket endpoints with broad instrument coverage. | | [Binance][binance] | Spot and futures product splits, trading WebSockets, and SBE market data. | | [Kraken][kraken] | Spot and futures submodules with distinct HTTP, WebSocket, data, and execution paths. | | [Lighter][lighter] | Layer-2 signing, canonical benchmarks, coverage-guided fuzzing, and detailed execution state handling. | | [Derive][derive] | JSON-RPC data and execution, EIP-712 signing, canonical benchmarks, and invariant-based fuzzing. | This guide distinguishes four kinds of guidance: - **Shared rules** come from common traits, network abstractions, hooks, or CI. - **Common patterns** appear in several adapters but allow other designs. - **Examples** show one sound implementation without making it mandatory. - **Exceptions** are valid when venue semantics or protocol boundaries require them. ## Conformance An adapter conforms when it satisfies each rule below that applies to it, or documents an exception. Name the venue behavior that forces the exception, keep it inside the adapter, and cover it with a test that fails if the venue stops requiring it. [Phase 7](#phase-7-prove-conformance) sequences the work that proves conformance. ### Adapter foundations | Rule | Applies to | | ------------------------------------------------------------------------------------------ | ------------------ | | [Repository and Python wiring](#repository-and-python-wiring) | New adapter crates | | [Credentials and secret handling](#credentials-and-secret-handling) | Every adapter | | [Configurations](#configurations-configrs) | Every adapter | | [Symbols and instrument identity](#symbols-and-instrument-identity) | Every adapter | | [Venue payload modeling and precision](#modeling-venue-payloads) | Every adapter | | [Client traits and factories](#client-traits-and-factories-datars-executionrs-factoriesrs) | Every adapter | ### Runtime and client lifecycle | Rule | Applies to | | ----------------------------------------------------- | -------------------------- | | [Connection lifecycle](#connection-lifecycle-connect) | Data and execution clients | | [Data events and request freshness](#data-client) | Data clients | | [Backpressure](#backpressure) | Every adapter | | [Task management](#task-management) | Every adapter | | [Deterministic simulation](#deterministic-simulation) | Maintained adapters | ### Execution and reconciliation | Rule | Applies to | | --------------------------------------------------------------------------------------------- | ----------------- | | [Execution client boundaries](#execution-client) | Execution clients | | [Reconciliation reports](#reconciliation-reports) | Execution clients | | [Commission failure handling](#commission-failure-handling) | Execution clients | | [Bounded mass-status reports](#bounded-mass-status-reports) | Execution clients | | [Instrument resolution during reconciliation](#instrument-resolution-during-reconciliation) | Execution clients | | [Tracked and external execution updates](#tracked-and-external-execution-updates) | Execution clients | | [Event ordering and deduplication](#event-ordering-and-deduplication) | Execution clients | | [Order command outcome policy](#order-command-outcome-policy) | Execution clients | | [Naming the evidence classes](#naming-the-evidence-classes) | Execution clients | | [Diagnostics and strategy-facing reasons](#separate-diagnostics-from-strategy-facing-reasons) | Execution clients | ### Transport and streaming | Rule | Applies to | | ------------------------------------------------------------------------------- | -------------------------------- | | [Request flow](#request-flow) | HTTP clients | | [Request signing and authentication](#request-signing-and-authentication) | HTTP and WebSocket request paths | | [Error handling and retry logic](#error-handling-and-retry-logic) | HTTP and WebSocket request paths | | [Rate limiting](#rate-limiting) | HTTP and WebSocket clients | | [Handler initialization handshake](#handler-initialization-handshake-setclient) | WebSocket clients | | [Authentication](#authentication) | WebSocket clients | | [Subscription management](#subscription-management) | WebSocket clients | | [Message routing](#message-routing) | WebSocket clients | | [Reconnection and shutdown](#reconnection-and-shutdown) | WebSocket clients | The [data testing specification](spec_data_testing.md) and [execution testing specification](spec_exec_testing.md) hold the scenarios that prove these contracts against a venue. ### Shared baseline Use the shared implementation of each piece below, then use any state structure that satisfies the contract it implements. The shared type carries that contract with it and keeps behavior comparable across venues, so a local structure has to prove the same contract on its own terms. Two execution clients implement the same trait without trading through a venue API, so the baseline does not apply to them: [sandbox](../../crates/adapters/sandbox/src/execution.rs) simulates fills locally, and [blockchain](../../crates/adapters/blockchain/src/execution/client.rs) executes on-chain behind the `defi` feature. Deterministic simulation is a maintained-adapter requirement rather than an optional capability: every maintained adapter must satisfy the [adapter DST contract](../concepts/dst.md#adapter-dst-contract) or carry a venue-scoped migration record tracking the gap. OKX is the reference implementation; [deterministic simulation](#deterministic-simulation) defines the seams, gates, and the bar for new adapters. | Target | Shared piece | Contract | | -------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | | Command outcome evidence | [`CommandFailure`](../../crates/live/src/execution/failure.rs) | [Naming the evidence classes](#naming-the-evidence-classes) | | Order identity and context | [`OrderIdentity` and `OrderContext`](../../crates/live/src/execution/context.rs) | [Tracked and external updates](#tracked-and-external-execution-updates) | | Replay deduplication | [`FifoCache` and `FifoCacheMap`](../../crates/common/src/cache/fifo.rs) | [Event ordering and deduplication](#event-ordering-and-deduplication) | | Order denial reasons | [`OrderDeniedReason`](../../crates/model/src/events/order/denied_reason.rs) | [Diagnostics and reasons](#separate-diagnostics-from-strategy-facing-reasons) | | Task lifecycle | [`TaskGroup`](../../crates/live/src/task.rs) and [`TaskHandles`](../../crates/common/src/live/task.rs) | [Task management](#task-management) | | Ingestion precision | [Domain numeric types](rust.md#domain-numeric-types) | [Venue payload modeling](#modeling-venue-payloads) | | HTTP transport | [`HttpClient`](../../crates/network/src/http/client.rs) | [Request flow](#request-flow) | | Authentication state | [`AuthTracker`](../../crates/network/src/websocket/auth.rs) | [Authentication](#authentication) | | Subscription identity | [`SubscriptionState`](../../crates/network/src/websocket/subscription.rs) | [Subscription management](#subscription-management) | | Reconnect requests | [`request_reconnect`](../../crates/network/src/websocket/client.rs) | [Reconnection and shutdown](#reconnection-and-shutdown) | | Retry machinery | [`RetryManager`](../../crates/network/src/retry.rs) | [Error handling and retry logic](#error-handling-and-retry-logic) | | Inferred fill commission | [`ExecutionClient`](../../crates/common/src/clients/execution.rs) | [Commission failure handling](#commission-failure-handling) | | Time, tasks, and runtime | [`nautilus_common::live::dst`](../../crates/common/src/live/dst.rs) | [Deterministic simulation](#deterministic-simulation) | | Wall-clock reads | [`duration_since_unix_epoch`](../../crates/core/src/time.rs) | [Deterministic simulation](#deterministic-simulation) | Where a venue transmits a discrete value as an IEEE-754 field rather than a decimal string or JSON number, contain that at the parsing boundary as a documented exception instead of letting `f64` spread inward from it. Retry classification is the exception to this table: it stays adapter-owned because venue status codes and rate-limit semantics differ. The shared machinery around it is not. See [error handling and retry logic](#error-handling-and-retry-logic) for both halves. ### Deterministic simulation Every maintained adapter, an Official-tier adapter per [ADAPTERS.md](../../ADAPTERS.md#adapter-tiers), must satisfy the [adapter DST contract](../concepts/dst.md#adapter-dst-contract). An adapter that does not yet conform carries a venue-scoped migration record tracking the gap. Unclaimed capabilities stay outside the contract until a slice proves them. OKX is the reference implementation. It proves the contract through shared seams, static gates, and behavioral gates: - **Seams:** the `nautilus_common::live::dst` facade for time, tasks, runtime, and signals; the `nautilus_core::time` wall-clock seam; the simulated HTTP and WebSocket transport in `nautilus-network`; and the shared subscription, reconnect, and retry machinery in the baseline table above. - **Static gates:** `check-dst-conventions` covers every DST-path production file (`ADAPTER_PATHS` in `.pre-commit-hooks/check_dst_conventions.sh`), and the nightly `dst-smoke` gate runs the simulation Clippy and test legs. - **Behavioral gates:** `crates/adapters/okx/tests/integration/dst.rs` pins exact subscribe bytes and exact per-operation wire fields against controlled local peers; complete wire-to-domain fresh-process comparison lives in the downstream harness. The [OKX integration guide's DST section](../integrations/okx.md#deterministic-simulation-testing) records the audited slice. A new adapter proves the contract from its first transport: gate DST-path files as they are added, drive every endpoint from configuration to a local peer, and pin wire bytes before expanding the slice. Do not introduce a shared abstraction until a second adapter proves the same boundary is needed. ## Structure of an adapter The Rust crate is the source of truth for protocol behavior. An adapter commonly separates these concerns: ```text crates/adapters/<adapter>/ ├── Cargo.toml ├── src/ │ ├── common/ # Shared credentials, enums, models, parsing, symbols, and URLs │ ├── http/ # Typed requests, responses, signing hooks, and transport client │ │ ├── client.rs │ │ ├── error.rs │ │ ├── models.rs │ │ ├── parse.rs │ │ └── query.rs │ ├── websocket/ # Streaming transport, protocol messages, parsing, and routing │ │ ├── client.rs │ │ ├── handler.rs │ │ ├── messages.rs │ │ ├── parse.rs │ │ ├── subscription.rs # When subscription identity or replay needs a boundary │ │ └── dispatch.rs # When execution routing needs a boundary │ ├── config.rs │ ├── data.rs # Or data/ when product implementations need a split │ ├── execution.rs # Or execution/ when product implementations need a split │ ├── factories.rs │ ├── python/ # PyO3 projection │ ├── signing/ # When authentication or transaction signing is a subsystem │ └── lib.rs ├── tests/ # Public Rust boundary tests ├── test_data/ # Canonical venue payloads and protocol vectors ├── benches/ # When confirmed hot paths warrant benchmarks │ ├── common/ # Shared benchmark fixtures │ ├── data.rs │ ├── exec.rs │ └── micros.rs ├── fuzz/ # When untrusted codecs warrant coverage-guided fuzzing │ ├── fuzz_targets/ │ └── README.md ├── examples/ # Rust tester nodes and focused usage examples ├── bin/ # Optional protocol inspection or capture tools └── README.md ``` Python and documentation surfaces sit outside the crate: ```text python/nautilus_trader/adapters/<adapter>/ # Public package and generated stubs examples/live/<adapter>/ # Python data and execution testers python/tests/unit/adapters/<adapter>/ # Public Python package tests docs/integrations/<adapter>.md # User-facing integration guide ``` Only `Cargo.toml` and `src/lib.rs` are universal crate boundaries. Add the other modules when the adapter needs them: - Put symbols, credentials, URLs, shared enums, and shared parsing under `common/`. - Put transport models, typed requests, signing, and HTTP clients under `http/`. - Put frames, messages, subscription state, routing, and WebSocket clients under `websocket/`. - Implement live data and execution traits in `data.rs` and `execution.rs`, or in product submodules when the venue exposes materially different protocols. - Keep PyO3 projection code under `python/`. - Organize integration tests by public boundary or product. Do not force all adapters into the same filenames. Product-specific splits are legitimate when product families have different protocols. A shared client can also span distinct endpoints when request and state semantics remain common. Match the venue's real boundaries and keep shared behavior above those splits. An adapter's public Python package lives under `python/nautilus_trader/adapters/<adapter>/` and usually re-exports generated bindings. Change Rust binding metadata or other generator inputs, then run `make py-stubs`; do not edit generated `.pyi` files. ### Repository and Python wiring A new adapter crate must be discoverable by each build surface that owns it: | Surface | Required change | Enforcement or proof | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Root Rust workspace | Add the crate to the members and workspace dependencies in [`Cargo.toml`](../../Cargo.toml). | Workspace metadata and targeted Cargo checks discover the crate. | | Workspace test inventory | Add the crate to `ADAPTER_CRATES` in the [`Makefile`](../../Makefile). | The [workspace coverage check](../../scripts/ci/check-workspace-test-coverage.sh) requires one test inventory. | | PyO3 crate | Add the optional dependency and feature propagation in [`crates/pyo3/Cargo.toml`](../../crates/pyo3/Cargo.toml). | Building the matching PyO3 feature compiles the adapter projection. | | PyO3 root module | Register the adapter module in [`crates/pyo3/src/lib.rs`](../../crates/pyo3/src/lib.rs). | The conventions hook treats this module list as the public API allowlist. | | Adapter PyO3 registry | Register each applicable factory and config extractor with `get_global_pyo3_registry()`. | Factory boundary tests prove Python config objects reach the Rust factories. | | Python package and user guide | Add package projection, tests, examples, and an integration guide only for capabilities the adapter exposes. | Import, generated drift, example build, and documentation checks cover these surfaces. | The [Nautilus conventions hook](../../.pre-commit-hooks/check_nautilus_conventions.sh) treats the PyO3 module list as a public API allowlist. The [PyO3 conventions hook](../../.pre-commit-hooks/check_pyo3_conventions.sh) also enforces: - Stub metadata uses `nautilus_trader.adapters.<adapter>`. - Runtime extension imports use `nautilus_trader._libnautilus.<adapter>`. - A Rust function renamed with `#[pyo3(name = ...)]` has a `py_` Rust name. - Python exceptions use the project error conversion functions. ## Adapter implementation sequence Use these phases to organize the work. They describe dependencies, not release gates. A market-data-only adapter omits execution, and an adapter can complete one product before starting another. Keep the capability matrix current throughout the work rather than waiting for the final documentation phase. Omit phases and steps that do not apply to the adapter. ### Phase 0: Define scope | Step | Component | Work | | ---- | ------------------- | --------------------------------------------------------------------------------------------------------- | | 0.1 | Capability matrix | List the products, environments, account modes, data types, order types, and reports in scope. | | 0.2 | Venue constraints | Record venue restrictions, unsupported capabilities, and testnet differences. | | 0.3 | Protocol boundaries | Identify separate product APIs, public and private endpoints, and binary or JSON transports. | | 0.4 | Initial slice | Choose the smallest slice that proves an end-to-end path. | | 0.5 | Repository wiring | Add the crate to the Rust workspace and test inventory, then add only the projection surfaces it exposes. | **Exit:** The integration guide contains an initial capability matrix, known gaps, and a test plan. ### Phase 1: Build the protocol core | Step | Component | Work | | ---- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | 1.1 | HTTP error types | Model transport, HTTP status, venue, parsing, and validation failures; classify retryability when supported. | | 1.2 | HTTP client | Implement endpoint resolution and typed requests, plus credentials, signing, rate limits, retries, and pagination as needed. | | 1.3 | HTTP API models | Define typed requests and responses, commonly under `http/` or its product-specific modules. | | 1.4 | HTTP parsing | Convert venue responses to domain types at deterministic boundaries in `http/parse.rs` or `common/parse.rs`. | | 1.5 | WebSocket error types | Model connection, protocol, and parsing failures, plus authentication and command failures when applicable. | | 1.6 | WebSocket client | Implement lifecycle and shutdown, plus authentication, heartbeat, subscription state, and reconnection when applicable. | | 1.7 | WebSocket messages | Define frames and messages under `websocket/` or product-specific modules; include acknowledgements and venue errors as needed. | | 1.8 | WebSocket parsing | Decode each frame once, convert domain events, and route data or execution messages by typed identity. | | 1.9 | Protocol tests | Prove fixtures, canonical requests, applicable signing vectors, lifecycle, and raw exchanges with mock peers. | **Exit:** The crate compiles, protocol fixtures parse, applicable signing vectors pass, and mock or controlled requests complete any required authentication and exchange raw venue messages. ### Phase 2: Implement instruments | Step | Component | Work | | ---- | ------------------ | -------------------------------------------------------------------------------------------------------- | | 2.1 | Instrument parsing | Parse every supported family with complete identity, precision, currency, and contract fields. | | 2.2 | Instrument loading | Load, filter, cache, and emit definitions at each parsing boundary that needs context. | | 2.3 | Symbol mapping | Define bidirectional venue symbol and `InstrumentId` conversion without collapsing distinct instruments. | | 2.4 | Instrument updates | Implement fresh instrument requests and any supported definition or status updates. | **Exit:** Distinct fixtures cover every supported instrument family, invalid definitions fail clearly, and the data client emits or returns complete Nautilus instruments. ### Phase 3: Implement market data Start with one public stream and one instrument before adding product or endpoint fan-out. | Step | Component | Work | | ---- | ------------------------ | ----------------------------------------------------------------------------------------------------------- | | 3.1 | Public WebSocket streams | Subscribe and unsubscribe each advertised live data type while preserving subscription intent. | | 3.2 | Historical data requests | Request supported bars, trades, quotes, or order book snapshots with exact correlation and freshness rules. | | 3.3 | Data client | Implement `DataClient` requests, subscriptions, lifecycle, and complete `DataEvent` emission. | | 3.4 | Order book handling | Preserve snapshot, incremental update, sequence, clear, and batch boundaries. | | 3.5 | Stream recovery | Handle malformed input, gaps, unsubscribe, disconnect, reconnect, and subscription replay. | **Exit:** Unit and mock transport tests prove complete domain events for the supported request and subscription matrix. ### Phase 4: Implement execution Establish account state and reconciliation before enabling order flow. | Step | Component | Work | | ---- | ---------------------- | ------------------------------------------------------------------------------------------------------------ | | 4.1 | Account bootstrap | Establish account identity, initial account state, private subscriptions, and connected readiness. | | 4.2 | Reconciliation reports | Generate applicable order, fill, position, and mass-status reports at startup and on demand. | | 4.3 | Basic order submission | Implement supported market and limit order submission with deterministic local validation. | | 4.4 | Order modification | Implement supported modify and cancel commands, including cancel-replace venue semantics. | | 4.5 | Execution client | Implement `ExecutionClient` commands, lifecycle, tracked and external routing, and ordered event emission. | | 4.6 | Outcome recovery | Preserve unknown outcomes, deduplicate fills, and resolve state through streams, queries, or reconciliation. | **Exit:** Mock transport tests cover every supported command, definitive rejection, uncertain transmission, duplicate or out-of-order updates, and startup reconciliation. ### Phase 5: Add optional venue capabilities Add these only after the base lifecycle is stable. | Step | Component | Work | | ---- | -------------------------- | ------------------------------------------------------------------------------------------------- | | 5.1 | Advanced order types | Add applicable conditional, stop, take-profit, trailing-stop, or other advanced orders. | | 5.2 | Batch operations | Add batch submission, batch cancellation, and mass cancel with per-order result handling. | | 5.3 | Venue-specific data | Add funding, greeks, liquidations, or venue extensions as separate capability slices. | | 5.4 | Product or endpoint splits | Split ownership only when protocol, authentication, quota, or recovery boundaries require it. | | 5.5 | Capability proof | Add fixtures, functional tests, acceptance cases, and documented limitations for each capability. | **Exit:** Each optional capability is independently testable and does not weaken the established base paths. ### Phase 6: Complete factories and projection | Step | Component | Work | | ---- | --------------------- | ------------------------------------------------------------------------------------------------ | | 6.1 | Configuration structs | Finalize typed data and execution configs, defaults, environment fallback, and secret redaction. | | 6.2 | Client factories | Implement Rust factories with `CacheView` inputs and the data client clock. | | 6.3 | PyO3 registration | Register applicable factories and config extractors with the PyO3 registry. | | 6.4 | Python package | Add the public package and Python boundary tests for the capabilities exposed to Python. | | 6.5 | Generated stubs | Add Rust stub metadata and regenerate the `.pyi` output with `make py-stubs`. | **Exit:** Rust factory tests and PyO3 boundary tests pass, package imports resolve, and generated output matches its Rust inputs. ### Phase 7: Prove conformance | Step | Component | Work | | ---- | ---------------------- | ------------------------------------------------------------------------------------------------------ | | 7.1 | Rust unit tests | Prove parsers, serializers, symbols, signatures, state transitions, and malformed input. | | 7.2 | Rust integration tests | Exercise public HTTP, WebSocket, data, and execution boundaries against deterministic mock transports. | | 7.3 | Python boundary tests | Prove imports, config extraction, factories, type conversion, and representative async calls. | | 7.4 | Acceptance tests | Run every applicable `DataTester` and `ExecTester` case on testnet or a controlled account. | | 7.5 | Recovery tests | Exercise connection failure, reconnect, shutdown, rate limits, and state recovery. | | 7.6 | Specification gaps | Record every skipped specification case with a venue or capability reason. | **Exit:** The applicable data and execution testing specifications pass, and every advertised capability has deterministic and venue evidence. ### Phase 8: Measure performance and robustness | Step | Component | Work | | ---- | -------------------- | --------------------------------------------------------------------------------------------------------- | | 8.1 | Canonical benchmarks | Measure confirmed end-to-end data and execution hot paths with representative fixtures. | | 8.2 | Microbenchmarks | Isolate confirmed signing, hashing, authentication, codec, parsing, or serialization costs. | | 8.3 | Fuzz targets | Fuzz untrusted parsing, decoding, normalization, signing, and encoding boundaries with realistic corpora. | | 8.4 | Invariants | Assert domain and protocol properties stronger than panic freedom. | **Exit:** Canonical benchmark and fuzz suites run with representative fixtures, documented invariants, and no mandatory categories that the adapter does not use. ### Phase 9: Finish documentation and operations | Step | Component | Work | | ---- | ------------------- | ---------------------------------------------------------------------------------------------- | | 9.1 | Capability matrix | Reconcile every support claim and exception with the tested implementation. | | 9.2 | Integration guide | Document credentials, config, limits, reconciliation, environment differences, and known gaps. | | 9.3 | Tester entry points | Provide applicable Rust and Python data and execution testers with safe defaults. | | 9.4 | Operations | Document recovery, troubleshooting, and any venue behavior an operator must understand. | | 9.5 | Final verification | Verify links, generated output, examples, and the focused documentation checks. | **Exit:** A user can configure, test, operate, and diagnose the adapter without reading its source. ## Rust adapter patterns Repository-wide import policy applies to adapter code: import Nautilus types and use their short names instead of fully qualifying them at call sites. The [Nautilus conventions hook](../../.pre-commit-hooks/check_nautilus_conventions.sh) enforces this rule and documents its scoped exception marker. ### Configurations (`config.rs`) Follow the shared [configuration guide](../concepts/configuration.md). In particular, Rust configs use typed fields, strict Serde decoding, one source of truth for defaults, and `bon::Builder`. Adapter configs then add only venue semantics: - Use an enum for a closed set such as environment, product family, account mode, or endpoint. - Use `Option<T>` only when absence has a distinct meaning, including runtime credential fallback. - Keep data and execution config separate when their capabilities or credentials differ. - Store fields that must not appear in `Debug` as `SecretString`. Derive `Debug` when every sensitive field uses a redacting type; write a custom implementation only when a field cannot use one or the type requires more restrictive output. - Keep Python config projection thin. It converts types and delegates to the Rust config. Centralize default HTTP and WebSocket endpoint resolution so one environment selection cannot mix live and test endpoints. Keep explicit URL overrides only where custom gateways, mock servers, or venue deployments require them. Test every supported environment and any precedence between an environment choice and an explicit override. Lay out config fields in this order: | Order | Field group | Placement rule | | ----- | ------------------------------------------ | ---------------------------------------------------------------------- | | 1 | Account identity, credentials, environment | Venue equivalents count: `network`, `deployment`, `region`. | | 2 | URL overrides | One contiguous block: `base_url_http` first, then each `base_url_ws*`. | | 3 | `proxy_url` | Immediately after the URL block. | | 4 | Everything else | Timeouts, retries, venue-specific behavior. | Resolve each `None` override to the environment default in a config helper method, and pass the resolved URL to the client constructor; constructors never read the `Option` fields directly. Keep the same relative order across the struct fields, `bon::Builder` accessors, pyo3 getter lists, and Python `__init__` signatures. Published Python signatures keep their positional order: new parameters are appended, and existing ones are not reordered, so a signature may lag the struct order. An intentional reorder of a published signature is a breaking change; note it under Breaking Changes in `RELEASES.md`. ### Credentials and secret handling When HTTP and WebSocket clients use the same key material, centralize credential handling in a type, commonly under `common/credential.rs`. Keep configs as data transfer objects: resolve credentials when constructing the credential, factory, or client, not in Python wrappers or individual request methods. #### Classify sensitive values Classify a value before choosing its type and diagnostic output. Apply the more restrictive rule when a venue gives one value more than one role. | Value class | Examples | Diagnostic output | | ---------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | Secret material | Passwords, private keys, API secrets, passphrases, bearer and session tokens, refresh tokens, and signatures. | Always show `<redacted>`. | | Credential identity | API keys, client IDs, usernames, and account identifiers used during authentication. | Redact by default. Use a masked API key only when operational correlation requires it. | | Secret-bearing location | Proxy URLs, RPC URLs, request paths, and query parameters that can contain credentials. | Redact the complete location from logs and errors. | | Deliberately public identity | Wallet addresses, vault addresses, and public account names that the venue exposes publicly. | Show only when the type and adapter contract deliberately classify the value as public. | Do not infer that an API key, username, or URL is safe to print because it is not sufficient to authenticate by itself. Configs often cross logging, exception, and Python representation boundaries where partial credential identity remains sensitive. #### Use the common secret types Use `nautilus_core::string::secret` and `zeroize` instead of defining adapter-local redaction or zeroization conventions. | Mechanism | Use | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `SecretString` | Own a string that must zeroize on drop and render as `<redacted>` with `Debug`. | | `REDACTED` | Replace an unconditional secret field in a custom `Debug` or `Display` implementation. | | `redact_option` | Preserve `Some` versus `None` while redacting an optional field in a custom `Debug`. | | `mask_api_key` | Correlate an API key only through an explicit masked-identity method. Do not use it for secret material. | | `Zeroizing<T>` | Bound the lifetime of an owned plaintext `String`, byte buffer, decoded key, canonical payload, or serialized authentication message. | | `Zeroize` and `ZeroizeOnDrop` | Clear secret-bearing fields in structs that cannot use `SecretString`, including byte arrays and signing types. | | `zeroize_json_value` | Clear owned strings in a mutable JSON value after serializing secret-bearing fields. | #### Use `SecretString` safely - Treat serialization as plaintext. `SecretString` uses the underlying string for wire-format compatibility, so never serialize a config, credential, or authentication model for diagnostics. - Do not use ordinary `SecretString` equality to verify attacker-controlled secrets; it is not constant-time. - Borrow plaintext through `expose_secret()` only at the signing, encoding, or transport boundary that needs it. - Consume with `into_inner()` only to transfer ownership. If the receiving API requires `String`, create that final copy at the call boundary and do not retain it in adapter code. - Take `&SecretString` when a function only reads the value. Take it by value when the function retains or consumes it. - Put secret-bearing fields in the authenticated wire model instead of creating a second model only to change `Debug`. Derive `Serialize` and derive `Debug` when every sensitive field redacts. - Write a custom `Debug` for credentials backed by byte arrays, signing keys, or other types that cannot store their secret fields as `SecretString`. - Avoid `Display` for secret-bearing types unless a caller requires it. Any implementation must redact secret material. #### Resolve and share credentials - Define environment variable names once and select them from typed environment and product values. - Document the established environment variable names in the adapter's integration guide. - Register every adapter environment variable in `scripts/strip-adapter-env.bash`. `make pre-flight` runs through that wrapper with all of them unset, so an unregistered variable can let a test pass locally while depending on ambient credentials. - Resolve all fields as one credential set. Public clients may remain unauthenticated, but an authenticated client rejects an incomplete or invalid set before sending a request. - Convert config and environment strings into zeroizing owners at the credential boundary. Do not retain a non-zeroizing plaintext copy in adapter state after conversion. - Share credential storage across transports only when they use the same key material. Keep HTTP, WebSocket, and transaction signing methods separate when their canonical payloads differ. #### Project credentials into Python - Convert credential strings accepted by a Python constructor to `SecretString` at the Rust boundary. - Apply the Rust `Debug` and `Display` redaction rules to Python `__repr__` and `__str__`. - Expose only a presence check for secret material and secret-bearing locations. - Return credential identity, such as a username, only when an existing public API or another explicit caller needs it. Document the choice and keep the value out of diagnostics. - Never expose passwords, private keys, API secrets, passphrases, tokens, or signatures through plaintext getters. #### Bound plaintext lifetime - Zeroize each owned plaintext allocation after its final use, including normalized and decoded keys, secret-bearing signing payloads, serialized authentication messages, encoded form values, and mutable request models. - Prefer borrowed slices and existing zeroizing owners over intermediate `String` and `Vec<u8>` copies. - Limit the guarantee to allocations the adapter owns. Serialization libraries, transports, TLS, and the operating system may make copies the adapter cannot reach. - Keep plaintext lifetimes short; do not promise process-wide or transport-wide erasure. #### Redact diagnostics and transport errors - Never include credentials, signatures, secret material, or secret-bearing URLs in errors or logs at any level. - Log request metadata such as the method, field count, and byte lengths instead of credentials or authentication payloads. Shared transports log metadata rather than payload contents. - Treat TRACE as developer-facing diagnostic output. Raw inbound payloads are allowed when their schema cannot contain credential material. - Treat raw private-stream TRACE output as sensitive because it can disclose orders, balances, positions, and account identity. Redact it before sharing. - Prefer metadata or a sanitized, bounded excerpt when either can diagnose the protocol. - Clear mutable source models after serialization when they own another plaintext copy. - Never log a raw authentication request or response, or any frame whose schema can contain secret material. | Surface | Required handling | Zeroization boundary | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | HTTP secret body | Use `HttpClient::request_with_secret_body`. | The client retains the zeroizing owner; lower layers may copy it. | | HTTP path or `HashMap` query | Use `HttpClient::request_with_url_redacted`. | The URL is omitted from transport errors. | | HTTP typed query | Use `HttpClient::request_with_params_url_redacted`. | The URL is omitted from transport errors. | | HTTP headers and proxy | Create credential-bearing strings at the client boundary, avoid clones, and do not retain them in adapter state. | The shared client or transport may retain copies. | | WebSocket authentication | Keep fields and serialized frames in `SecretString`; create the final `String` immediately before `send_text`. | The shared client has no secret-owner-preserving send method. | | Unsupported combination | Extend the common client instead of implementing adapter-local URL or error scrubbing. | The common API must define the resulting ownership and redaction rule. | :::warning Disable redirects for authenticated requests Clients that send credentials or signed payloads must set `HttpRedirectPolicy::Reject`. Use an equivalent no-redirect policy with other HTTP transports so redirects cannot forward credentials or signed payloads to another destination. ::: #### Verify credential handling Test the secret-handling contract as well as successful authentication: - Cover explicit values, environment fallback, incomplete credentials, and invalid credentials. - Assert that config, credential, request, response, and client `Debug` output omits the exact input secrets. Test `Display` separately for every secret-bearing type that implements it. - Assert that Python `__repr__` and `__str__` omit credential identity and secret material. Test presence checks and every deliberately exposed identity getter. - Assert that serialization and transport preserve the exact wire value where the venue requires plaintext. - Force transport failures for credential-bearing URLs and assert that both `Display` and `Debug` error output omit the URL, path secret, and query secret. - Use compile-time trait assertions for `Zeroize` or `ZeroizeOnDrop`, and test explicit clearing for mutable request and response models. - Keep deterministic signature vectors so redaction and zeroization changes cannot alter signing bytes, field order, or encoding. ### Symbols and instrument identity Separate venue symbols from Nautilus `InstrumentId` values. A symbol module commonly owns: - Parsing and formatting venue symbols. - Product or contract suffixes required for a unique Nautilus symbol. - Validation of venue and product identity. - Round-trip tests for supported forms and rejection tests for ambiguous forms. Choose the mapping from the venue's identity scheme: | Venue identity | Nautilus representation | Example | | --------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------- | | Native symbol distinguishes the product | Preserve the symbol and add the venue. | `BTC-USDT-SWAP` -> `BTC-USDT-SWAP.OKX`. | | Raw symbol is reused across product families | Add and validate a stable product suffix. | Bybit linear `BTCUSDT` -> `BTCUSDT-LINEAR.BYBIT`. | | Nautilus and the venue use different contract marks | Implement both directions at one boundary. | Binance USD-M `BTCUSDT` -> `BTCUSDT-PERP.BINANCE`. | | Transport casing differs from canonical identity | Convert only when building the transport value. | Binance stream `BTCUSDT-PERP.BINANCE` -> `btcusdt`. | The [`BybitSymbol`](../../crates/adapters/bybit/src/common/symbol.rs) wrapper and [Binance symbol conversions](../../crates/adapters/binance/src/common/symbol.rs) show the suffix and bidirectional conversion patterns. Treat them as examples, not a shared suffix scheme. Do not normalize distinct venue instruments to the same `InstrumentId`. Give test fixtures distinct symbols, precisions, currencies, and contract fields so swaps and omissions fail visibly. For every supported product family, test venue symbol -> `InstrumentId` -> venue symbol. Normalize case once at the identity boundary and preserve venue-significant case elsewhere. When the mapping requires a product marker, reject a missing or ambiguous marker before caching the instrument. Construct instruments from current venue definitions. Validate required identity and precision before caching or emission. Keep parsing functions deterministic and independent of live client state where practical. ### Modeling venue payloads Model the wire format, not an imagined stable subset: - Use typed request and response structs for known fields. - Use Serde aliases or custom deserializers only when supported payloads require them. - Reject unknown values for closed sets whose meaning affects domain behavior. - Preserve or explicitly classify unknown values for open venue sets that may expand without a protocol version change. - Keep raw models separate from Nautilus domain objects. Convert at one auditable boundary. - Pass required parsing context explicitly, including instrument precision, currencies, account identity, and `ts_init`. Keep live client state outside parsers. - Treat missing, null, and empty values according to the venue schema. Do not collapse them into one fallback when they carry different meanings. - Use the venue timestamp for `ts_event` when the payload supplies one. Assign `ts_init` from the adapter clock when it receives or constructs the event. Use receipt time as event time only when the venue has no authoritative timestamp, and cover that fallback with a test. Avoid permissive fallbacks that silently turn a new venue value into an existing semantic value. Stable error handling is part of the parser contract. #### Numeric precision Deserialize prices, quantities, money, fees, and other discrete values as `Decimal`. Construct domain values with `Price::from_decimal`, `Price::from_decimal_dp`, `Quantity::from_decimal`, `Quantity::from_decimal_dp`, `Money::from_decimal`, or `Money::zero`; never route wire values through `f64`. See [domain numeric types](rust.md#domain-numeric-types). Choose domain precision from the field contract, not incidental payload formatting: | Field contract | `"25.000"` result | Conversion | | -------------------------------------------------- | ----------------------- | ------------------------------------------------------------------- | | Venue-declared scale is meaningful | `25.000` at precision 3 | Use `Price::from_decimal` or `Quantity::from_decimal`. | | Documented trailing zeros are non-semantic padding | `25` at precision 0 | Call `Decimal::normalize`, then use the scale-inferred constructor. | | Instrument or currency precision governs the value | `25.00` at precision 2 | Use `Price::from_decimal_dp` or `Quantity::from_decimal_dp`. | Use instrument or currency precision for event and report values when available. A venue may send the same value as `"25"`, `"25.0"`, or `"25.000"`, so do not infer precision per payload unless the adapter defines and tests an explicit compatibility fallback. The declared-precision constructors apply banker's rounding when a value has excess non-zero digits; validate round-trip equality when the field contract requires exact representation. During reconciliation, follow [instrument resolution](#instrument-resolution-during-reconciliation) when precision metadata is missing. #### Venue enum fallbacks Venues extend wire enums without notice: new order states, order types, and category codes appear in production before clients update. Give each extensible venue enum a forward-compatible fallback variant (`Unknown` for venue states, `Other` for open value sets such as types and categories) with `#[serde(other)]`, so one new value cannot fail deserialization of the message carrying it. Closed sets the adapter defines stay strict. The fallback changes where strictness lives, not whether it exists: - Never panic on an unknown wire variant; the fallback keeps the connection and the sibling records in the same payload alive. - Never map an unknown variant onto an existing domain value. Make the domain mapping fallible (`TryFrom`) so the fallback variant is rejected explicitly at the mapping boundary. - Preserve safety-critical payload data even when a sibling classification is unmapped. A fill must still be parsed and emitted when its order state or order type is unknown, because fill fields carry their own prices, quantities, and fees. - Skip only the unmappable classification and log a warning with the venue identifiers (order ID, instrument) needed to investigate. When the message carries no data worth preserving, fail the record explicitly instead of inventing a status. Reconciliation heals the gap once the order reaches a mapped state; an unmapped value fails the same way on the reconciliation path, so treat the warning as the signal to add the mapping. #### Separate authority from projections Use separate response models when one endpoint returns both evidence that establishes permission or authorizes state mutation and data needed for a narrower read. | Boundary | Purpose | Validation | Meaning of success | | -------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | **Authoritative response** | Establish permission or authorize state mutation. | Requires all authoritative fields; rejects legacy conflicts and semantic duplicates before mapping. | The response can support the authority decision it models. | | **Narrow projection** | Read a balance, health value, or metadata without using authority. | Decodes returned fields only; its type cannot expose, grant, or infer omitted authority. | Only the projected value; omitted permission or account evidence is unknown. | Use the projection when malformed authority fields must not block the narrower read. Keep the authoritative model strict. ### Client traits and factories (`data.rs`, `execution.rs`, `factories.rs`) The shared [`DataClient`](../../crates/common/src/clients/data.rs), [`ExecutionClient`](../../crates/common/src/clients/execution.rs), and [client factory](../../crates/common/src/factories/client.rs) traits define the adapter boundary. Implement the supported methods and leave unsupported capabilities explicit in the integration guide. The client traits use `#[async_trait(?Send)]`. Client objects are not intended to move across threads and may hold non-`Send` Python state. Move owned, `Send` inputs into explicit runtime tasks when asynchronous work must outlive a synchronous trait call. #### Client naming and registration Name each client family symmetrically: `<Venue>DataClient`, `<Venue>DataClientConfig`, and `<Venue>DataClientFactory` for data; `<Venue>ExecutionClient`, `<Venue>ExecutionClientConfig`, and `<Venue>ExecutionClientFactory` for execution. Each factory consumes its corresponding client config directly. Do not add a separate factory config wrapper. The live node passes its `LiveNodeConfig.trader_id` to execution factories, while venue-specific values such as `account_id` belong on the execution client config. Within a Python module, order client-family `add_class` registrations alphabetically by exported type name so the data and execution families remain grouped. Do not prefix the ordinary client family with `Live`: a connected client is the default, while names such as `SandboxExecutionClient` and `DatabentoHistoricalClient` state alternate behavior. Retain `Live` only when it distinguishes explicit runtime or protocol siblings. Runtime types such as `LiveNode`, `LiveClock`, and the `Live*EngineConfig` family retain the qualifier. Do not shorten `Execution` in public, project-owned PascalCase type names. Internal implementation types may retain established `Exec` names. Also keep `Exec` where the [general naming convention](coding_standards.md#naming-conventions) allows it, including venue protocol terms such as `ExecType`. Name protocol-specific wire models after the venue concept, such as `HyperliquidExchangeAction`. Preserve established public names, and apply this convention to new APIs. #### Factory inputs and cache ownership Factories receive a downcast `ClientConfig` and a read-only [`CacheView`](../../crates/common/src/cache/mod.rs). Data factories also receive the shared clock. Use the view to resolve instruments and existing state. Engine cache writes stay in the engines: emit domain events and reports instead of mutating the engine cache from an adapter. A private protocol cache is valid when parsing, subscription replay, or response correlation needs it. ### Adapter-owned state Choose collections from ownership and update behavior: - Use a plain `AHashMap` or `AHashSet` for state owned by one task. - Use `AtomicMap` or `AtomicSet` for read-heavy immutable snapshots with infrequent writes. Use `rcu` when writers can race; a separate load and store can lose another writer's update. - Use `DashMap` or `DashSet` for independent keys that receive concurrent entry updates. Adapters use these patterns in different combinations. Keep the collection behind the component that owns its invariant instead of sharing it merely to avoid passing a message. Use `Ustr` for repeated protocol strings when interning reduces allocation or comparison cost; keep unique request IDs and short-lived payload text in their natural types. ### Connection lifecycle (`connect`) Treat each lifecycle method as a contract: | Method | Responsibility | Successful postcondition | | ------------ | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | `start` | Install local event plumbing and start client-owned background work. | Local event paths exist before any task can publish. | | `connect` | Establish transports, authenticate, load required definitions or account state, and start stream processing. | Public commands can use the tra
Shown in full with attribution under the source's licence. Licence: LGPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.