docs/developer_guide/adapters.md
Adapters connect NautilusTrader to venues and data providers. A good adapter does more than move bytes: it preserves venue semantics, produces valid Nautilus domain events, and makes uncertain outcomes explicit. The work is exacting, but the repository already provides strong contracts and useful examples.
Use reference adapters selectively. Their layouts reflect different venue protocols, product families, and implementation histories.
| Adapter | Useful reference |
|---|---|
| Bybit | Multi‑product HTTP and WebSocket clients, options data, and execution outcome handling. |
| OKX | Public, private, and business WebSocket endpoints with broad instrument coverage. |
| Binance | Spot and futures product splits, trading WebSockets, and SBE market data. |
| Kraken | Spot and futures submodules with distinct HTTP, WebSocket, data, and execution paths. |
| Lighter | Layer‑2 signing, canonical benchmarks, coverage‑guided fuzzing, and detailed execution state handling. |
| Derive | JSON‑RPC data and execution, EIP‑712 signing, canonical benchmarks, and invariant‑based fuzzing. |
This guide distinguishes four kinds of guidance:
The Rust crate is the source of truth for protocol behavior. An adapter commonly separates these concerns:
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:
python/nautilus_trader/adapters/<adapter>/ # Public package and generated stubs
python/examples/<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:
common/.http/.websocket/.data.rs and execution.rs, or in product
submodules when the venue exposes materially different protocols.python/.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.
Python package files live under python/nautilus_trader/adapters/<adapter>/. In current Rust‑native
adapters, the package usually re‑exports generated bindings. Change Rust binding metadata or other
generator inputs, then run make py-stubs-v2; do not edit generated .pyi files.
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. | Workspace metadata and targeted Cargo checks discover the crate. |
| Workspace test inventory | Add the crate to ADAPTER_CRATES in the Makefile. | The workspace coverage check requires one test inventory. |
| PyO3 crate | Add the optional dependency and feature propagation in 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. | 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 treats the PyO3 module list as a public API allowlist. The PyO3 conventions hook also enforces:
nautilus_trader.adapters.<adapter>.nautilus_trader.core.nautilus_pyo3.<adapter>.#[pyo3(name = ...)] has a py_ Rust name.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.
Exit: The integration guide contains an initial capability matrix, known gaps, and a test plan.
Exit: The crate compiles, protocol fixtures parse, applicable signing vectors pass, and mock or controlled requests can authenticate and exchange raw venue messages.
InstrumentId mapping.Exit: Distinct fixtures cover every supported instrument family, invalid definitions fail clearly, and the data client emits or returns complete Nautilus instruments.
Exit: Unit and mock transport tests prove complete domain events for the supported request and subscription matrix.
Exit: Mock transport tests cover every supported command, definitive rejection, uncertain transmission, duplicate or out‑of‑order updates, and startup reconciliation.
Exit: Each optional capability is independently testable and does not weaken the established base paths.
CacheView inputs.Exit: Rust factory tests and PyO3 boundary tests pass, package imports resolve, and generated output matches its Rust inputs.
Exit: The applicable data and execution testing specifications pass, and every advertised capability has deterministic and venue evidence.
Exit: Canonical benchmark and fuzz suites run with representative fixtures, documented invariants, and no mandatory categories that the adapter does not use.
Exit: A user can configure, test, operate, and diagnose the adapter without reading its source.
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 enforces this rule and documents its scoped exception marker.
config.rs)Follow the shared configuration guide. 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:
Option<T> only when absence has a distinct meaning, including runtime credential fallback.Debug for any config that can hold secrets.Resolve credentials at a credential, factory, or client construction boundary. Environment fallback
may be part of that boundary, and a presence check may inspect the environment. Do not spread
environment lookup through request methods or Python wrappers. Never include credentials, signed
payloads, or secret material in Debug, errors, or logs.
Use the repository's established environment names for a venue and environment. Document exact names in the adapter's integration guide, where users need them, instead of copying them into this guide.
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.
Separate venue symbols from Nautilus InstrumentId values. A symbol module commonly owns:
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.
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.
Model the wire format, not an imagined stable subset:
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.
data.rs, execution.rs, factories.rs)The shared DataClient,
ExecutionClient, and
client factory traits define the adapter boundary.
Implement the supported methods and leave unsupported capabilities explicit in the integration
guide.
Factories receive a downcast ClientConfig and a read‑only
CacheView. 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.
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.
Choose collections from ownership and update behavior:
AHashMap or AHashSet for state owned by one task.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.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.
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 transport, and required bootstrap state is observable. |
disconnect | Stop new network work and close transports. | The client no longer sends or receives venue traffic. |
stop | End client‑owned work using an idempotent path. | Repeated teardown is safe. |
reset | Clear reconnectable caches, counters, cancellation state, and stale in‑flight state. | A later start or connection does not inherit invalid session state. |
dispose | Release background tasks, threads, and external handles. | No client‑owned resource remains active. |
Do not report connected until public commands can use the transport and required engine‑side state
is observable. In particular, an execution client that emits initial account state asynchronously
waits until the engine cache contains the account before calling set_connected; reconciliation
and strategy startup treat connected as a readiness signal. Apply the same rule to required
instrument or stream bootstrap state. When transport connection completes before the socket becomes
active, use a bounded wait_until_active step before subscribing or reporting readiness. On partial
connection failure, clean up resources already started and leave state consistent for retry or
disposal.
When an execution client uses
ExecutionEventEmitter, install its sender during
start before any task can emit.
Connection code varies, but its dependencies do not. A data client typically:
An execution client typically:
Treat these as dependency constraints, not required function names. A venue can combine or reorder steps when tests prove the same postconditions. If any step fails after resources start, tear down those resources before returning the error.
Subscriptions express ongoing intent. Requests ask the provider for current or historical data. Keep their freshness semantics distinct:
The shared DataEvent envelope determines how data
enters the engine:
| Variant | Use | Contract to preserve |
|---|---|---|
DataEvent::Instrument | Instrument definitions from bootstrap, requests, or updates. | Preserve complete identity, precision, and venue timestamps when available. |
DataEvent::InstrumentStatus | Trading or availability status changes. | Emit meaningful transitions rather than unchanged polling snapshots. |
DataEvent::Data | Trades, quotes, order‑book data, bars, and other typed market data. | Complete parsing and event boundary construction before emission. |
DataEvent::Response | Results for current or historical data requests. | Preserve request correlation, parameters, filters, and freshness semantics. |
DataEvent::FundingRate | Funding rate updates for derivatives. | Preserve the venue's effective or event time and instrument identity. |
DataEvent::OptionGreeks | Venue‑provided option greeks. | Preserve the source instrument and distinguish venue values from local calculation. |
DataEvent::DeFi | Feature‑gated decentralized finance data. | Emit only when the adapter and build expose the shared defi feature. |
Add a regression test that changes the upstream instrument response between two requests. The second response must reflect the new venue state rather than a private cache entry.
Publish typed data through the engine's data event path. Parse and validate before emission, and do not hold mutable adapter state across downstream dispatch. A closed event receiver normally means the engine is stopping: log the send failure and let lifecycle teardown own recovery rather than retrying the same event indefinitely.
For order‑book deltas, follow the
delta flag and event boundary contract.
Every logical update ends with F_LAST; snapshots use F_SNAPSHOT and end with
F_SNAPSHOT | F_LAST, including an empty snapshot represented only by Clear.
When a venue exposes instrument status only as a polled snapshot, diff it against the prior full
snapshot and emit changes rather than repeating every status. Treat an instrument removed from the
snapshot according to the venue contract. Map removal to NotAvailableForTrading only when
disappearance means the instrument is unavailable. Update the full private cache even when
emissions are filtered to active subscriptions.
Execution clients translate commands, preserve order identity, publish account state, and generate reports for reconciliation. They must support these boundaries consistently:
OrderSubmitted only when the command enters the adapter's submission path.Do not infer support from a venue API alone. Implement and test the Nautilus command and event semantics, then advertise the capability.
Route execution updates according to order ownership, independent of the dispatch module layout:
OrderStatusReport and FillReport values so the
execution engine can reconcile or create the external order.Do not invent strategy or client identity for an untracked order. Preserve available venue identity in the report and let the engine apply external order ownership. The adapter may use any state structure that proves this routing decision.
A venue can report the same transition through an order response, private stream, query, and reconciliation result. Deduplicate by stable venue identity, not by the transport that delivered the update:
For a tracked order, a definitive fill can arrive before an acknowledgement or open‑order update. Emit any required preceding lifecycle event only when the adapter has complete order identity and the venue evidence proves that state. Record the synthesized transition so a later acknowledgement does not duplicate it. Untracked orders continue through reports rather than synthesized strategy events.
When a venue implements modify as cancel‑replace, update the venue order ID mapping before routing the replacement leg. Distinguish a stale cancel for the old leg from cancellation of the active replacement, and calculate replacement quantity from current cumulative fills. This behavior is venue‑specific and needs focused race tests; it does not imply a shared dispatch state layout.
Separate three evidence classes:
OrderDenied before OrderSubmitted. For cancel or modify preparation, emit the
matching rejection only when the failure is attributable to that command and proves it was not
sent. Otherwise, log the failure without inventing a rejection.flowchart TD
command[Submit order] --> valid{Deterministic local validation passes?}
valid -->|No| denied[OrderDenied]
valid -->|Yes| submitted[OrderSubmitted]
submitted --> evidence{Definitive venue evidence?}
evidence -->|Accepted or updated| event[Apply the venue event]
evidence -->|Explicit rejection| rejected[Emit OrderRejected]
evidence -->|No| unknown[Keep the outcome unknown]
unknown --> recovery[Resolve from stream, query, polling, or reconciliation]
recovery --> event
recovery --> rejected
If submit validation fails after OrderSubmitted, leave the order in flight unless definitive
venue evidence resolves it.
Transport errors, timeouts, disconnects, task cancellation, retry exhaustion, HTTP 5xx responses, rate limits, missing acknowledgements, and parse failures after transmission usually leave an unknown outcome. Do not convert them into a venue rejection.
For batch commands, apply evidence per order. A whole‑request failure does not prove that every child command failed. Treat venue messages such as "not found" or "already closed" according to documented venue semantics; they may describe a race with a fill or cancellation rather than an unambiguous command rejection.
Keep this policy independent of the HTTP or WebSocket path used to send a command.
A common design has two layers:
Use one layer when the protocol is small and the split would only add forwarding methods. Split by product when endpoints, signatures, or response models change for different product families.
Keep typed request construction separate from sending. This makes signatures and canonical query encoding testable without a server. Put response conversion in pure parser functions when it does not need live state.
Typed request and query builders preserve the difference between an omitted field, an explicit zero, and an empty value. Keep required venue parameters required, omit absent optional parameters from the wire representation, and test the exact serialized query or body. Pagination code also tests cursor direction, inclusive boundaries, duplicate boundary records, and a repeated cursor or empty page so it cannot loop forever.
Build the exact canonical bytes required by the venue, then sign those bytes once. Test:
Keep nonce or sequence ownership explicit. If commands can run concurrently, define how the adapter serializes, allocates, or rejects conflicting nonces. Never retry a signed state‑changing request with a new identity unless venue semantics make that safe.
Treat request identity, timestamp, and nonce as separate protocol fields even when the venue packs them into one signed payload. The component that allocates a nonce also owns its ordering rule. Build and sign from the same reserved value, then handle pre‑send failure, uncertain transmission, and venue nonce rejection according to documented venue consumption semantics. On a sequence mismatch, resynchronize from an authoritative source before issuing further state‑changing commands. Test deterministic vectors, concurrent allocation, monotonicity or uniqueness, and recovery after a rejected sequence.
Map transport, HTTP status, venue error, parse, and validation failures without erasing their source. Retry policy follows operation safety:
Use the shared RetryManager when its cancellation and backoff
model fits. An adapter‑specific classifier remains responsible for venue codes and operation
semantics.
The shared HttpClient supports one or more rate
limiters. Scope limiter state to the venue quota, not to a convenient Rust object:
Match the venue's actual meter: window shape, burst behavior, endpoint weights, and shared external traffic. A token bucket at the headline rate can still exceed a strict rolling window after an idle burst. Do not assume wire latency creates headroom.
When the venue separately caps concurrent unacknowledged commands, add a closed‑loop in‑flight gate beside the send‑rate limiter. Release its slot on every terminal acknowledgement, rejection, or send failure, and reset the gate on reconnect. A rate limiter alone cannot observe acknowledgement latency.
Do not copy one adapter's bucket names or quotas into another. Document user‑visible limits and configuration in the integration guide.
WebSocket dispatch organization has more repository variance than most adapter boundaries and is under active standardization. Keep new code aligned with the shared network abstractions and the nearest protocol peers, but do not treat one adapter's dispatch modules or state structs as the target architecture.
A common pattern separates an outer client from a handler task:
WebSocketClient, serializes commands, decodes frames, and emits typed
messages.Some adapters use stream mode and perform reconnection in the adapter. Others use the network client's handler mode and automatic reconnection. Both are legitimate. Split market data and trading handlers only when endpoints, authentication, throughput, or protocol semantics justify the extra lifecycle.
Choose client boundaries from protocol facts:
| Protocol shape | Structure to consider | Obligation |
|---|---|---|
| One endpoint and one multiplexed protocol | One client and handler | Route by typed channel identity without duplicating lifecycle state. |
| One protocol across separate product endpoints | One orchestrator with a client collection | Connect, close, and replay intent for every active product client. |
| Separate public, private, or trading endpoints | Separate transports with shared models where useful | Authenticate and recover each endpoint according to its own contract. |
| Different wire formats, signing, or reconnect rules | Separate protocol modules behind shared data or exec logic | Keep shared instrument and order identity above the protocol‑specific code. |
This table is a decision aid, not a target dispatch architecture. Do not split a client only to match another adapter's filenames, and do not combine endpoints when doing so hides independent authentication, quotas, or recovery.
flowchart LR
subgraph client["Client (orchestrator)"]
cmd_tx["cmd_tx
├ Subscribe { args }
├ PlaceOrder { params }
└ MassCancel { id }"]
out_rx["out_rx
<- {Venue}WsMessage
<- Authenticated
<- ChannelData"]
end
subgraph handler["Handler (I/O boundary)"]
cmd_rx[cmd_rx]
out_tx[out_tx]
ws[WebSocket]
end
cmd_tx --> cmd_rx
cmd_rx -->|"serialize"| ws
ws -->|"parse -> transform"| out_tx
out_tx --> out_rx
The diagram shows the common ownership boundary, not required type or field names.
SetClient)Several adapters move a connected WebSocketClient into an already running handler through a
SetClient command. Others construct or publish the handler differently. Preserve this invariant
in either design:
No public subscribe, order, or control command can overtake handler initialization.
Queue initialization before publishing a command sender or connected state, or use another mechanism that proves the same ordering. Test a command issued at the connection boundary so a race cannot silently drop it.
Use AuthTracker when authentication state must be
shared across the client, handler, and reconnect path. The adapter still owns protocol details:
Refreshable tokens, multiple account sessions, and mixed public/private endpoints need adapter‑specific state. Keep that state close to the credential and subscription paths and cover rotation or expiry with focused tests.
Use SubscriptionState when the venue has
acknowledged subscriptions or reconnect replay. It separates intent from confirmation and includes
reference counts for duplicate subscribers.
| State | Meaning |
|---|---|
| Pending subscribe | The client intends to subscribe and awaits venue confirmation or first data. |
| Confirmed | The venue acknowledged the subscription or sent authoritative data for it. |
| Pending unsubscribe | The client intends to remove the subscription and awaits confirmation. |
| Trigger | Shared operation | Result |
|---|---|---|
| First local subscriber | try_mark_subscribe or mark_subscribe | Record pending subscribe intent and send when required. |
| Subscribe acknowledgement or authoritative first frame | confirm_subscribe | Move pending intent to confirmed. |
| Subscribe failure | mark_failure | Keep subscribe intent pending for recovery. |
| Last local subscriber | mark_unsubscribe | Remove active intent and record pending unsubscribe. |
| Unsubscribe acknowledgement | confirm_unsubscribe | Remove pending unsubscribe without erasing a later resubscribe. |
Confirm from an explicit venue acknowledgement when the protocol provides one. If acknowledgements
are absent or unreliable, authoritative first data can confirm the topic. Both paths can coexist
because confirmation is idempotent. Never confirm from local send success alone. On a negative
subscribe result, call mark_failure so reconnect retains the intent. Correlate unsubscribe
results separately so a late subscribe acknowledgement cannot revive removed intent and a stale
unsubscribe acknowledgement cannot erase a later resubscription.
Derive a stable topic key from the venue subscription arguments, but keep the original arguments when replay would otherwise require lossy parsing. On reconnect:
Do not replay pending unsubscriptions. Handle late or stale acknowledgements without reviving
removed subscriptions. SubscriptionState provides these state transitions; the adapter provides
the wire correlation.
Keep the routing boundary auditable:
Dispatch module layout, intermediate enum names, and state containers remain adapter‑specific. Prefer the smallest design that makes protocol ownership and state transitions testable.
Reconnection must restore protocol state, not only the socket:
Support both WebSocket control frames and venue text heartbeats when applicable. Let the shared client handle protocol control frames; keep application heartbeat messages in the venue handler.
Shutdown signals tasks, asks the transport to close, and then joins or aborts owned work according
to a bounded policy. Make repeated shutdown safe. Do not assume a handler JoinHandle has one
owner when client objects can be cloned.
Current shared WebSocket transport and adapter event paths use unbounded Tokio channels so receive loops do not wait for queue capacity. Preserve that convention for live event paths. Introducing a bounded channel, coalescing, dropping, or disconnect‑on‑full policy changes platform semantics and needs an explicit shared design, not an adapter‑local change.
An unbounded queue trades backpressure for memory growth. Keep receive‑loop work focused, expose handler failure, and test recovery from a disconnected consumer. Never drop execution events. Market data can use snapshot and resynchronization only when its protocol contract defines that recovery.
spawn_task)Synchronous client trait methods must not block an active Tokio runtime. Clone owned inputs, spawn
the asynchronous operation, and return the local validation result. Use
nautilus_common::live::get_runtime().spawn() for adapter production tasks so native Rust and
Python FFI use the configured runtime.
The Tokio usage hook rejects tokio::spawn in
adapter production code and requires fully qualified Tokio spawn, time, and sync paths.
Keep the synchronous boundary small:
fn spawn_request<F>(&self, description: &'static str, future: F)
where
F: Future<Output = anyhow::Result<()>> + Send + 'static,
{
let handle = get_runtime().spawn(async move {
if let Err(error) = future.await {
log::warn!("{description} failed: {error:?}");
}
});
self.tasks.push(handle);
}
Validate the command and clone every input before constructing the future. Do not capture a
RefCell borrow, cache guard, clock borrow, or reference to the command in work that outlives the
trait call.
Use TaskHandles for client‑owned tasks when a collection
is needed. push prunes completed handles, abort_all drains and aborts them, and take_all
transfers them to a client‑specific join policy. Give each task:
RefCell or engine borrows.block_on in trait methodsLive runners call synchronous data and execution methods from within Tokio. Calling block_on
there can panic because a runtime is already active.
| Boundary | Adapter action | Reason |
|---|---|---|
Synchronous DataClient or ExecutionClient | Clone owned inputs, spawn the operation, and return. | The live runner may already be executing the method inside Tokio. |
| Async client, handler, or task method | Await the operation or select it with cancellation. | The async boundary already participates in the active runtime. |
| Top‑level binary or dedicated non‑Tokio thread | Block only when that boundary owns the runtime lifecycle. | No ambient runtime exists when the boundary is constructed correctly. |
| Test | Use #[tokio::test] or a test‑owned runtime. | The harness owns runtime setup and avoids nested block_on calls. |
Do not use the top‑level and test exceptions to justify blocking inside a live client trait method. Redesign an ambiguous boundary as async.
CancellationTokenUse CancellationToken when several tasks share a lifecycle. Select cancellation alongside
streams, timers, or response channels. Cancel before joining tasks, and replace the token during
reset. Also replace a canceled token before a reconnect or any other path starts new work. A
reused canceled token causes every new task to exit immediately.
Tests prove adapter semantics at progressively wider boundaries. Store canonical valid fixtures
under test_data/ and keep network access out of ordinary unit and integration tests. Source valid
payloads from official venue documentation or captured venue responses; do not hand‑fabricate
them. Synthetic malformed or mutated inputs remain useful for negative, property, and fuzz tests
when the test marks them as such.
| Boundary | Typical location | Required proof |
|---|---|---|
| Pure protocol logic | src/** test modules | Symbols, enums, timestamps, decimals, signatures, codecs, parsers, and malformed input. |
| Public Rust client boundary | tests/ | Typed HTTP and WebSocket behavior through mock servers, event dispatch, lifecycle, and retries. |
| Rust PyO3 boundary | tests/python.rs or another feature‑gated crate test | Module registration, conversion, constructors, and representative async calls. |
| Public Python package | python/tests/unit/adapters/ | Package imports, config, factories, and user‑visible behavior not proved by Rust tests. |
| Live venue acceptance | Adapter examples or test nodes | Authentication, subscriptions, execution, reports, recovery, and advertised limitations. |
Use exact fixture values and assert every stable output field. Distinct inputs should expose field swaps, omitted values, wrong precision, and accidental defaults.
Parser and serializer tests should cover:
Keep the complete venue envelope when status fields, pagination cursors, timestamps, or nested result wrappers affect behavior. Record fixture provenance in the fixture, a nearby README, or a source manifest. Use separate real payloads for structurally distinct states such as long, short, flat, empty, and partially filled; do not mutate one happy‑path fixture into every valid case.
When HTTP and WebSocket tests share fixture loaders or model builders, place test‑only code in a
common::testing module rather than copying it into production modules. This pattern is optional
when no test code is shared.
Client tests should drive public methods through mock HTTP or WebSocket servers. Assert emitted
events, requests, connection state, subscription state, retry count, and shutdown behavior. Wait
on observable state with wait_until_async when possible. A
short sleep is valid when the time window itself is under test or no protocol signal exists, but
it should not mask a missing synchronization point.
Shared repository test policy uses #[rstest] for Rust test functions, permits
#[tokio::test] for async tests, and rejects arrange/act/assert comments. The
testing conventions hook enforces these
repository‑wide rules.
Exercise each public boundary with both successful and adverse protocol evidence:
| Surface | Successful evidence | Failure and recovery evidence |
|---|---|---|
| HTTP client | Exact method, path, query or body, authentication, and typed response. | Missing credentials, venue errors, malformed bodies, retry classification, and pagination termination. |
| WebSocket client | Connection, authentication, heartbeat, subscription acknowledgement, and typed routing. | Authentication failure, malformed frames, stale acknowledgements, disconnect, replay, and shutdown. |
| Data client | Requests and subscriptions produce complete domain events with correct identity and time. | Freshness, filtering, malformed input, stream gaps, unsubscribe, and reconnect behavior. |
| Execution client | Commands produce ordered events, account state, and reconciliation reports. | Local denial, definitive rejection, unknown outcome, duplicates, partial batches, and startup recovery. |
Mock transports should expose enough state to assert requests, connection count, authentication, subscriptions, and emitted events. Wait for those observable conditions instead of sleeping. Assert both sides of the boundary: the exact venue request and the resulting Nautilus event or report.
Data tests cover each advertised request and subscription, plus:
Execution tests cover each advertised command and report, plus:
Keep adapter tests focused on adapter behavior. The data testing specification and execution testing specification define the shared scenario catalogs and skip rules; link to them instead of copying partial lists into an adapter README.
Run acceptance tests only after deterministic tests pass. Use testnet or a controlled account and record:
Acceptance tests must verify events and venue state, not only the absence of errors. Clean up open orders and positions according to the test plan, and never infer production support from one happy path.
Provide the applicable tester entry points:
crates/adapters/<adapter>/examples/node_data_tester.rs and
node_exec_tester.rs, with product subdirectories when protocols split by product.python/examples/<adapter>/data_tester.py and exec_tester.py, using LiveNode and
the Rust config and factory classes.Python v2 tester scripts build without connecting by default and require --run to connect.
Execution testers require the separate --live-orders opt‑in before order submission. Preserve
that safety boundary. Rust tester controls currently vary; inspect them before running, and make
any new or revised execution tester default to ExecTester dry‑run behavior.
For Python‑exposed adapters, test the Rust module before testing broad Python workflows. Verify:
Use instrument_any_to_pyobject and pyobject_to_instrument_any at Python instrument boundaries
to preserve the concrete instrument variant in both directions.
Regenerate stubs with make py-stubs-v2 after changing exported Rust types or signatures. The
generated drift check verifies that generator
inputs and committed .pyi output agree.
Add these suites late, after functional, integration, and acceptance work establishes correct behavior. They deepen assurance for confirmed hot paths and untrusted venue input; they do not replace conformance tests.
Use Criterion for a deep performance pass on production boundaries that measurements identify as important. The Lighter and Derive suites provide the reference structure:
| Suite | Canonical boundary | Reference |
|---|---|---|
benches/data.rs | Raw venue frame or payload through decoding, parsing, cache lookup where required, and Nautilus domain construction. | Lighter data, Derive data |
benches/exec.rs | Order command through serialization and signing; where applicable, inbound execution payload through event dispatch. | Lighter execution, Derive execution |
benches/micros.rs | Decode‑only, parse‑only, and focused component costs that localize a regression found at a pipeline boundary. | Lighter micros, Derive micros |
Put shared realistic instruments, payloads, signer state, and other fixtures in
benches/common/. Construct stable setup, allocation, and state outside the timed region when
production does not pay that cost per operation. Include setup when it is part of the real hot
path.
Measure representative end‑to‑end pipelines first. Add diagnostic components to explain a regression, not to inflate the suite. Set throughput when bytes, messages, orders, or another unit clarifies operational capacity.
Add venue‑specific suites for confirmed hot paths such as signing, hashing, binary codecs, or authentication. Lighter has focused cryptographic suites, and Derive has a signing suite. Do not require a category that the adapter does not use.
Follow the repository benchmarking guide for tool choice, baselines, noise control, and result reporting. Use the Criterion practitioner guide for benchmark structure and local commands.
Coverage‑guided fuzzing adds assurance where arbitrary venue bytes or values cross a trust boundary. Prioritize:
Seed parser and decoder corpora with representative payloads from test_data/ when they improve
coverage. Keep harnesses below live network and runtime layers unless the target specifically
needs one of those boundaries.
Panic freedom is only the baseline. Assert deterministic properties such as:
Use differential fuzzing when a sufficiently independent reference exists. Lighter's scalar multiplication target and Derive's nonce model show how to compare implementations without putting network state in the harness.
Canonical adapter wiring is:
| Surface | Required wiring | Enforcement or use |
|---|---|---|
Adapter [features] | fuzz = ["nautilus-live/fuzz"] | Enables the shared fuzz support without changing normal builds. |
Adapter [package.metadata] | cargo-fuzz = true | Lets cargo fuzz treat the adapter manifest as a fuzz package. |
Adapter [[bin]] | One entry per target with the fuzz feature and test, doc, and bench false. | Registers discoverable binaries without adding them to ordinary test runs. |
fuzz/fuzz_targets/ | Focused targets below live network and runtime layers. | Keeps arbitrary input at the parser, codec, normalization, or model boundary. |
scripts/fuzz-adapter.sh | Adapter target discovery and repeated time‑sliced runs. | Uses the registered binaries and preserves corpus and artifact locations. |
Adapter crates must not depend directly on libfuzzer-sys; the
Cargo conventions hook enforces the shared
feature path. Use the focused Lighter fuzz README and
Derive fuzz README for setup, corpus, artifact, and target commands instead of
copying every invocation here.
Create or update docs/integrations/<adapter>.md with:
Keep capability claims testable and name legitimate exceptions. Link to shared configuration, benchmarking, and testing guides instead of copying their policy.
Follow the repository documentation guide and Markdown style guide. Change generator inputs and regenerate generated output.
Use these shared specifications to plan and report adapter conformance: