Nautilus Developer Guide - Adapters (The Adapter Authoring Bible)
The
developer_guide/adapters/page is the canonical specification for authoring a Nautilus integration adapter end-to-end. Where the concept page nautilus-adapters.md tells you what an adapter is (5-component bundle, factory-registered intoLiveNode), this page tells you how to build it: directory layout (crates/adapters/<venue>/Rust core +nautilus_trader/adapters/<venue>/Python wiring), file taxonomy (config.rs/py,factories.rs/py,data.rs/py,execution.rs/py,providers.py,parsing.py,__init__.py), the seven-phase dependency-driven implementation sequence, theLiveDataClient/LiveMarketDataClient/LiveExecutionClient/InstrumentProviderPython contract (every overridable abstract method enumerated), the WebSocket two-layer client/handler architecture ({Venue}WebSocketClientouter +{Venue}WsFeedHandlerinner withArc<ArcSwap<AtomicU8>>connection state,SubscriptionStateshared viaArc<DashMap>,cmd_tx/out_rxchannel discipline,RECONNECTEDsentinel + replay logic), error/retry classification (Retryable / NonRetryable / Fatalwithretry_after), credential resolution patterns (credential_env_vars()+Credential::resolve()), rate-limit quotas ({VENUE}_REST_QUOTA,{VENUE}_WS_*_QUOTA), order book deltaF_LAST/F_SNAPSHOTflag invariants, and four reconciliation report variants (OrderStatusReport,FillReport,OrderWithFills,PositionStatusReport). For Cortana MK3, this is The Adapter Authoring Bible - the page the UW WebSocket adapter blueprint will be built against. The concept-page LOC estimate of ~870 Python LOC for v0 is preserved; this page adds the file-by-file blueprint that makes that estimate executable.
This page complements Nautilus Adapters (concept)
- that page is the conceptual contract; this page is the implementation contract. It also pairs with Nautilus Developer Guide (extension+contribution) (the higher-level “how does this all fit into the project” view) and with the parallel testing-spec pages Data Testing Spec and the execution-testing equivalent. Read this page when authoring or reviewing any new adapter; cite the concept page when reasoning about adapter boundaries.
Core claim
A Nautilus adapter is a two-tree, seven-phase, file-templated authoring
problem. The Rust tree at crates/adapters/<venue>/ owns wire I/O,
parsing, retry, and PyO3 bindings. The Python tree at
nautilus_trader/adapters/<venue>/ owns the engine-facing
LiveDataClient / LiveMarketDataClient / LiveExecutionClient /
InstrumentProvider subclasses plus user-facing LiveDataClientConfig
and LiveExecClientConfig and the factory functions that wire them. The
seven-phase sequence (Rust core → instruments → market data → execution
→ advanced features → config/factories → testing+docs) is
dependency-ordered, not optional - each phase requires the previous.
For a data-only adapter (UW, Databento, Tardis), Phase 4 collapses
to skipped; Phase 2 collapses to a near-no-op when the venue does not
own its own instrument universe.
Two-tree directory layout
The page is explicit that adapters split across two trees. The Rust
tree is the production path; the Python tree is the engine-facing
seam. Cortana’s UW adapter v0 can ship Python-only by collapsing the
Rust tree into a thin Python wrapper around httpx + websockets,
but the structure of the Python tree is the same.
Rust core (crates/adapters/<venue>/)
crates/adapters/your_adapter/
├── src/
│ ├── common/ # Shared types and utilities
│ │ ├── consts.rs # Venue constants / broker IDs
│ │ ├── credential.rs # API key storage and signing helpers
│ │ ├── enums.rs # Venue enums mirrored in REST/WS payloads
│ │ ├── error.rs # Adapter-level error aggregation (when applicable)
│ │ ├── models.rs # Shared model types
│ │ ├── parse.rs # Shared parsing helpers
│ │ ├── retry.rs # Retry classification (when applicable)
│ │ ├── urls.rs # Environment & product aware base-url resolvers
│ │ └── testing.rs # Fixtures reused across unit tests
│ ├── http/ # HTTP client implementation
│ │ ├── client.rs # HTTP client with authentication
│ │ ├── error.rs # HTTP-specific error types
│ │ ├── models.rs # Structs for REST payloads
│ │ ├── parse.rs # Response parsing functions
│ │ └── query.rs # Request and query builders
│ ├── websocket/ # WebSocket implementation
│ │ ├── client.rs # WebSocket client (outer)
│ │ ├── dispatch.rs # Execution event dispatch and order routing
│ │ ├── enums.rs # WebSocket-specific enums
│ │ ├── error.rs # WebSocket-specific error types
│ │ ├── handler.rs # Feed handler (I/O boundary)
│ │ ├── messages.rs # Frame and message enums
│ │ ├── parse.rs # Message parsing functions
│ │ └── subscription.rs # Subscription topic helpers (optional)
│ ├── python/ # PyO3 Python bindings
│ │ ├── enums.rs # Python-exposed enums
│ │ ├── http.rs # Python HTTP client bindings
│ │ ├── urls.rs # Python URL helpers
│ │ ├── websocket.rs # Python WebSocket client bindings
│ │ └── mod.rs # Module exports
│ ├── config.rs # Configuration structures
│ ├── data.rs # Data client implementation
│ ├── execution.rs # Execution client implementation
│ ├── factories.rs # Factory functions
│ └── lib.rs # Library entry point
├── tests/ # Integration tests with mock servers
│ ├── data_client.rs # Data client integration tests
│ ├── exec_client.rs # Execution client integration tests
│ ├── http.rs # HTTP client integration tests
│ └── websocket.rs # WebSocket client integration tests
└── test_data/ # Canonical venue payloads (frozen JSON)
Python layer (nautilus_trader/adapters/<venue>/)
nautilus_trader/adapters/your_adapter/
├── config.py # Configuration classes (LiveDataClientConfig / LiveExecClientConfig subclasses)
├── constants.py # Adapter constants
├── data.py # LiveDataClient or LiveMarketDataClient
├── execution.py # LiveExecutionClient
├── factories.py # Factory functions + venue-payload→Nautilus-domain converters
├── providers.py # InstrumentProvider
└── __init__.py # Package initialization
The Python tree is flat by design - there are no subdirectories. Six files do the entire job. Cortana’s UW v0 collapses to the Python tree only.
File taxonomy - what each file owns
This is the load-bearing reference table. Every adapter author needs this map memorized.
| File | Layer | Owns | Cortana UW v0 size |
|---|---|---|---|
crates/.../src/common/credential.rs | Rust | API key storage (Box<[u8]> zeroized), HMAC signing, credential_env_vars() + Credential::resolve() for env-var resolution | n/a (Python: ~30 LOC inline in config.py) |
crates/.../src/common/error.rs | Rust | Top-level error enum aggregating VenueHttpError, VenueWsError, VenueBuildError via #[from] | n/a (Python uses exceptions directly) |
crates/.../src/common/retry.rs | Rust | Retryable / NonRetryable / Fatal enum + retry_after: Option<Duration> from rate-limit headers | n/a |
crates/.../src/common/urls.rs | Rust | const fn get_ws_base_url(testnet: bool) resolvers; config overrides | n/a (Python: in constants.py) |
crates/.../src/http/client.rs | Rust | Wraps nautilus_network::http::HttpClient; raw + domain layer; Arc-backed for PyO3 cloning | ~100 LOC (Python httpx async wrapper) |
crates/.../src/http/parse.rs | Rust | Venue-response → Nautilus-domain conversion functions | inlined in parsing module |
crates/.../src/websocket/client.rs | Rust | Outer {Venue}WebSocketClient: connection-mode Arc<ArcSwap<AtomicU8>>, cmd_tx, out_rx, task_handle, auth_tracker, subscription_state, subscription_args | ~150 LOC (Python websockets wrapper + reconnect) |
crates/.../src/websocket/handler.rs | Rust | Inner {Venue}WsFeedHandler: I/O loop, owns WebSocketClient exclusively, pending_requests: AHashMap, pending_messages: VecDeque, RECONNECTED sentinel | merged with client.rs in Python v0 |
crates/.../src/websocket/messages.rs | Rust | Two enums: {Venue}WsFrame (wire shapes, handler-internal) + {Venue}WsMessage (handler→client output) | n/a (Python: msgspec types in parsing.py) |
crates/.../src/websocket/dispatch.rs | Rust | Execution dispatch state (WsDispatchState with order_identities, emitted_accepted, triggered_orders, filled_orders, clearing); cross-source fill BoundedDedup<T> | n/a (UW is data-only) |
crates/.../src/python/mod.rs | Rust | PyO3 re-exports: m.add_class::<>() for every Python-facing struct/enum | n/a |
crates/.../src/config.rs | Rust | bon::Builder + Default config struct; T for sensible defaults, Option<T> only when None is semantic | merged into config.py |
crates/.../src/data.rs | Rust | Domain-side data client; emits DataEvent::Instrument, ::InstrumentStatus, ::Data, ::Response, ::FundingRate | merged into data.py |
crates/.../src/execution.rs | Rust | Domain-side execution client; reconciliation report generation; account-state emitter | n/a (UW is data-only) |
crates/.../src/factories.rs | Rust | Factories that take config + supporting context, return live clients | merged into factories.py |
nautilus_trader/adapters/<venue>/config.py | Python | LiveDataClientConfig / LiveExecClientConfig subclasses with api_key, api_secret, base_url plus venue-specific knobs | ~60 LOC |
nautilus_trader/adapters/<venue>/data.py | Python | LiveMarketDataClient (or LiveDataClient for venues without market-data primitives) subclass; method ordering convention (connect → subscribe → unsubscribe → request) | ~200 LOC (heart of UW adapter) |
nautilus_trader/adapters/<venue>/execution.py | Python | LiveExecutionClient subclass; reconciliation report generators | n/a (skipped) |
nautilus_trader/adapters/<venue>/factories.py | Python | Factory function: def venue_data_client_factory(loop, name, config, msgbus, cache, clock) -> LiveDataClient | ~60 LOC |
nautilus_trader/adapters/<venue>/providers.py | Python | InstrumentProvider subclass with load_all_async, load_ids_async, load_async | ~30 LOC (near-no-op) |
nautilus_trader/adapters/<venue>/parsing.py | Python (extension) | Frame → custom-data type conversion | ~80 LOC |
nautilus_trader/adapters/<venue>/__init__.py | Python | __all__ exports + the VENUE ClientId constant | ~15 LOC |
nautilus_trader/adapters/<venue>/constants.py | Python | URL constants, channel-name constants, ENV var keys | ~30 LOC |
The seven-phase implementation sequence
The page is explicit: each phase depends on the previous one. No shortcuts. For a data-only adapter (UW), Phase 4 collapses to “skipped”; Phase 2 collapses to a near-no-op when the venue proxies instrument identity (UW references IB-listed underlyings, doesn’t define its own).
Phase 1 - Rust core infrastructure
| Step | Component | Description |
|---|---|---|
| 1.1 | HTTP error types | Define HTTP-specific error enum with retryable/non-retryable variants (http/error.rs) |
| 1.2 | HTTP client | Implement credentials, request signing, rate limiting, retry logic |
| 1.3 | HTTP API models | Define request/response structs for REST endpoints (http/models.rs, http/query.rs) |
| 1.4 | HTTP parsing | Convert venue responses to Nautilus domain models (http/parse.rs, common/parse.rs) |
| 1.5 | WebSocket error types | Define WebSocket-specific error enum (websocket/error.rs) |
| 1.6 | WebSocket client | Implement connection lifecycle, authentication, heartbeat, reconnection |
| 1.7 | WebSocket messages | Define streaming payload types (websocket/messages.rs) |
| 1.8 | WebSocket parsing | Convert stream messages to Nautilus domain models (websocket/parse.rs) |
| 1.9 | Python bindings | Expose Rust functionality via PyO3 (python/mod.rs) |
Milestone: Rust crate compiles, unit tests pass, HTTP/WebSocket clients can authenticate and stream/request raw data.
For Cortana UW v0 (Python-only): collapse to a single data.py that
bundles HTTP + WS + parsing + dispatch.
Phase 2 - Instrument definitions
| Step | Component | Description |
|---|---|---|
| 2.1 | Instrument parsing | Parse venue instrument definitions into InstrumentAny variants (spot, perpetual, future, option) |
| 2.2 | Instrument provider | Implement InstrumentProvider to load, filter, and cache instruments |
| 2.3 | Symbol mapping | Handle venue-specific symbol formats and Nautilus InstrumentId conversion |
Milestone: InstrumentProvider.load_all_async() returns valid
Nautilus instruments.
For UW: near-no-op. UW does not define instruments; the IBKR adapter
already loaded them. UW’s load_all_async returns empty list; or if
we want to proxy, query the shared Cache populated by the IBKR
adapter and return what’s already there.
Phase 3 - Market data
| Step | Component | Description |
|---|---|---|
| 3.1 | Public WebSocket streams | Subscribe to order books, trades, tickers, public channels |
| 3.2 | Historical data requests | Fetch historical bars/trades/snapshots via HTTP |
| 3.3 | Data client (Python) | Implement LiveDataClient or LiveMarketDataClient wiring Rust clients to data engine |
Milestone: Data client connects, subscribes to instruments, emits market data to the platform.
For UW: this is the core work. The UWLiveDataClient’s
_subscribe(SubscribeData) translates a DataType(UWFlowAlert)
subscription into a UW WS channel join; _on_ws_message parses each
frame into a UWFlowAlert(@customdataclass) and pushes through
self._handle_data(alert).
Phase 4 - Order execution
| Step | Component | Description |
|---|---|---|
| 4.1 | Private WebSocket streams | Subscribe to order/fill/position/balance changes |
| 4.2 | Basic order submission | Implement market and limit orders via HTTP or WebSocket |
| 4.3 | Order modification/cancel | Implement amendment and cancellation |
| 4.4 | Execution client (Python) | Implement LiveExecutionClient wiring Rust clients to execution engine |
| 4.5 | Execution reconciliation | Generate order, fill, and position status reports for startup reconciliation |
Milestone: Execution client submits orders, receives fills, reconciles state on connect.
For UW: skipped entirely. UW is data-only; execution stays on
IBKR. The MK3 wiring registers UW data + IBKR data + IBKR exec on
the same LiveNode.
Phase 5 - Advanced features
| Step | Component | Description |
|---|---|---|
| 5.1 | Advanced order types | Conditional orders, stop-loss, take-profit, trailing stops, iceberg |
| 5.2 | Batch operations | Batch order submission, batch cancellation, mass cancel |
| 5.3 | Venue-specific features | Options chains, funding rates, liquidations, venue-specific data types |
For UW: this is where the REST option-chain snapshot path lives -
a _request(RequestData) for OptionChainSnapshot custom data type.
Phase 6 - Configuration and factories
| Step | Component | Description |
|---|---|---|
| 6.1 | Configuration classes | LiveDataClientConfig / LiveExecClientConfig subclasses |
| 6.2 | Factory functions | Implement factory functions to instantiate clients from config |
| 6.3 | Environment variables | Support credential resolution from env vars |
For UW: standard. UWLiveDataClientFactory plus UWConfig with
api_key: str | None, api_secret: str | None (env-fallback),
channels: list[str], optional historical_lookback_secs.
Phase 7 - Testing and documentation
| Step | Component | Description |
|---|---|---|
| 7.1 | Rust unit tests | Test parsers, signing helpers, business logic in #[cfg(test)] blocks |
| 7.2 | Rust integration tests | Test HTTP/WebSocket clients against mock Axum servers in tests/ |
| 7.3 | Python integration tests | Test data/execution clients in tests/integration_tests/adapters/<adapter>/ |
| 7.4 | Example scripts | Provide runnable examples demonstrating data subscription and order execution |
For UW: pytest under tests/integration_tests/adapters/uw/ with
mock-WS fixtures replaying canonical UW frames; spec-acceptance
subset (the DataTester matrix from
Data Testing Spec) for any data
type the adapter claims to support.
DataClient / LiveDataClient - Python contract
The page enumerates the complete abstract method set for both
LiveDataClient (non-market data; news, custom streams) and
LiveMarketDataClient (market data; books, ticks, bars, instrument
status, option greeks).
LiveDataClient minimal surface
from nautilus_trader.live.data_client import LiveDataClient
from nautilus_trader.data.messages import RequestData, SubscribeData, UnsubscribeData
class TemplateLiveDataClient(LiveDataClient):
async def _connect(self) -> None: ...
async def _disconnect(self) -> None: ...
async def _subscribe(self, command: SubscribeData) -> None: ...
async def _unsubscribe(self, command: UnsubscribeData) -> None: ...
async def _request(self, request: RequestData) -> None: ...Five hooks. This is the surface UW v0 implements.
LiveMarketDataClient full surface
For venues that expose standard market-data primitives (quotes, trades, books, bars, mark/index prices, funding rates, option Greeks, instrument status). The page lists 30+ abstract methods covering subscribe/unsubscribe/request triplets per data type. The full set:
- Connection:
_connect,_disconnect - Generic subscribe/unsubscribe/request:
_subscribe,_unsubscribe,_request - Instruments:
_subscribe_instruments,_subscribe_instrument,_unsubscribe_instruments,_unsubscribe_instrument,_request_instrument,_request_instruments - Order book:
_subscribe_order_book_deltas,_subscribe_order_book_depth,_unsubscribe_order_book_deltas,_unsubscribe_order_book_depth,_request_order_book_deltas,_request_order_book_depth,_request_order_book_snapshot - Quotes:
_subscribe_quote_ticks,_unsubscribe_quote_ticks,_request_quote_ticks - Trades:
_subscribe_trade_ticks,_unsubscribe_trade_ticks,_request_trade_ticks - Bars:
_subscribe_bars,_unsubscribe_bars,_request_bars - Mark/Index/Funding:
_subscribe_mark_prices,_subscribe_index_prices,_subscribe_funding_rates(+ unsubscribe + request) - Instrument status/close:
_subscribe_instrument_status,_subscribe_instrument_close(+ unsubscribe) - Option greeks:
_subscribe_option_greeks,_unsubscribe_option_greeks
Cortana UW v0 implements only the generic triplet because UW
publishes custom data (UWFlowAlert), not standard market-data
primitives. This is the right choice - LiveDataClient (non-market
data superclass) is sufficient.
Method ordering convention
The page mandates this group order in adapter classes:
- Connection handlers:
_connect,_disconnect - Subscribe handlers:
_subscribe,_subscribe_* - Unsubscribe handlers:
_unsubscribe,_unsubscribe_* - Request handlers:
_request,_request_*
Keeps related functionality together rather than interleaving subscribe/unsubscribe pairs. This is a doc-blessed style rule, not a personal preference.
ExecutionClient - Python contract
For data-only adapters this section is for reference; UW will not implement it. The full surface, verbatim:
class TemplateLiveExecutionClient(LiveExecutionClient):
async def _connect(self) -> None: ...
async def _disconnect(self) -> None: ...
# Reconciliation reports - non-optional in live mode
async def generate_order_status_report(self, command: GenerateOrderStatusReport) -> OrderStatusReport | None: ...
async def generate_order_status_reports(self, command: GenerateOrderStatusReports) -> list[OrderStatusReport]: ...
async def generate_fill_reports(self, command: GenerateFillReports) -> list[FillReport]: ...
async def generate_position_status_reports(self, command: GeneratePositionStatusReports) -> list[PositionStatusReport]: ...
async def generate_mass_status(self, lookback_mins: int | None = None) -> ExecutionMassStatus | None: ...
# Order command handlers
async def _submit_order(self, command: SubmitOrder) -> None: ...
async def _submit_order_list(self, command: SubmitOrderList) -> None: ...
async def _modify_order(self, command: ModifyOrder) -> None: ...
async def _cancel_order(self, command: CancelOrder) -> None: ...
async def _cancel_all_orders(self, command: CancelAllOrders) -> None: ...
async def _batch_cancel_orders(self, command: BatchCancelOrders) -> None: ...The five generate_* methods are the four reconciliation report
variants + an aggregate generate_mass_status. Required for live
adapters; the spec acceptance suite (ExecTester) checks them.
InstrumentProvider - Python contract
from nautilus_trader.common.providers import InstrumentProvider
from nautilus_trader.model import InstrumentId
class TemplateInstrumentProvider(InstrumentProvider):
async def load_all_async(self, filters: dict | None = None) -> None: ...
async def load_ids_async(self, instrument_ids: list[InstrumentId], filters: dict | None = None) -> None: ...
async def load_async(self, instrument_id: InstrumentId, filters: dict | None = None) -> None: ...Three abstract methods. For UW these are near-no-ops; for IBKR these are the venue-walk for the full SPY 0DTE chain.
Lifecycle hooks - beyond what the concept page covers
The concept page describes the connect/disconnect/subscribe/request hooks. The dev-guide page goes deeper on what each hook must do internally.
_connect() - strict initialization order
The page is explicit: the platform waits for all clients to signal
connected before running reconciliation or starting strategies, so
all initialization must complete within _connect(). Don’t defer
work to on_start() - by then reconciliation has already run.
For data clients, the canonical sequence:
// Pseudo-Rust from the doc; Python equivalent is the same idea
async fn connect(&mut self) -> Result<()> {
let instruments = self.bootstrap_instruments().await?;
ws.cache_instruments(instruments);
ws.connect().await?;
ws.wait_until_active(10.0).await?;
Ok(())
}For execution clients, more involved:
async fn connect(&mut self) -> Result<()> {
self.ensure_instruments_initialized_async().await?;
self.ws_client.connect().await?;
self.ws_client.wait_until_active(10.0).await?;
// ... subscribe channels, start stream ...
self.refresh_account_state().await?;
self.await_account_registered(30.0).await?;
self.core.set_connected();
Ok(())
}The await_account_registered(30.0) step is critical: it polls the
cache at 10ms intervals until the account appears or the timeout
expires, blocking connect so the portfolio can process orders
during reconciliation. Skipping this step causes orders to be
denied during early reconciliation because the portfolio doesn’t
yet know about the account.
Watchdog reconnect - the RECONNECTED sentinel pattern
The underlying WebSocketClient from nautilus_network sends a
RECONNECTED sentinel message when reconnection completes,
triggering resubscription logic in the handler. The handler:
- Receives
{Venue}WsMessage::Reconnectedfrom the inner. - If authenticated: re-authenticates and waits for confirmation
via
AuthTracker::begin()→succeed(). - Restores all tracked subscriptions via handler commands by
replaying the preserved subscription arguments (not the
topic strings - store the original args in a separate
Arc<DashMap<String, SubscriptionArgs>>).
Why preserve original args, not just topic strings: parsing topics back into args is brittle (multiple parameter types, nested encoding, optional fields). Preserve once, replay verbatim.
Disconnection - the three-step shutdown sequence
async fn close(&mut self) -> Result<()> {
// 1. Send Disconnect command so handler cleans up gracefully
self.cmd_tx.read().await.send(HandlerCommand::Disconnect)?;
// 2. Set stop signal so handler loop exits after processing disconnect
self.signal.store(true, Ordering::Release);
// 3. Await task handle with timeout, abort if stuck
if let Some(task_handle) = self.task_handle.take() {
match Arc::try_unwrap(task_handle) {
Ok(handle) => {
let abort_handle = handle.abort_handle();
match tokio::time::timeout(Duration::from_secs(2), handle).await {
Ok(Ok(())) => {/* completed */}
Ok(Err(e)) => tracing::error!("Handler task error: {e:?}"),
Err(_) => {
tracing::warn!("Timeout waiting for handler task, aborting");
abort_handle.abort();
}
}
}
Err(arc_handle) => arc_handle.abort(),
}
}
Ok(())
}Send Disconnect before setting the stop signal so the handler
processes it before exiting. Use Ordering::Release so the handler
sees the write. Extract abort_handle before awaiting so it
remains available after timeout.
Backoff - the RetryManager pattern
The page mandates nautilus_network::RetryManager for both HTTP
and WebSocket retry. WebSocket sends use send_with_retry:
async fn send_with_retry(&self, payload: String, rate_limit_keys: Option<Vec<String>>) -> Result<(), MyWsError> {
self.retry_manager.execute_with_retry(
"websocket_send",
|| async {
client.send_text(payload.clone(), rate_limit_keys.clone()).await
.map_err(|e| MyWsError::ClientError(format!("Send failed: {e}")))
},
should_retry_error,
create_timeout_error,
).await
}
fn should_retry_error(error: &MyWsError) -> bool {
match error {
MyWsError::NetworkError(_) | MyWsError::Timeout(_) => true,
MyWsError::AuthenticationError(_) | MyWsError::ParseError(_) => false,
}
}Send failures (after retries exhausted) emit SendFailed so the
exec client dispatch can produce OrderRejected. Never panic on
a send failure; emit a typed event.
Error handling - the load-bearing taxonomy
Adapter-level error enum (common/error.rs)
#[derive(Debug, thiserror::Error)]
pub enum VenueError {
#[error("HTTP error: {0}")]
Http(#[from] VenueHttpError),
#[error("WebSocket error: {0}")]
WebSocket(#[from] VenueWsError),
#[error("Build error: {0}")]
Build(#[from] VenueBuildError),
}Top-level enum aggregates component-level errors via #[from] so
? propagates cleanly. Component errors stay specific for
debugging; the top-level type provides unified handling at the
adapter boundary.
Retry classification (common/retry.rs)
#[derive(Debug, thiserror::Error)]
pub enum VenueError {
#[error("Retryable error: {source}")]
Retryable {
#[source]
source: VenueRetryableError,
retry_after: Option<Duration>,
},
#[error("Non-retryable error: {source}")]
NonRetryable {
#[source]
source: VenueNonRetryableError,
},
#[error("Fatal error: {source}")]
Fatal {
#[source]
source: VenueFatalError,
},
}Three categories. Retryable carries retry_after: Option<Duration>
for venues that emit hint headers (e.g., Retry-After: 60). Helpers
from_http_status(), from_rate_limit_headers(), is_retryable(),
is_fatal(), retry_after() standardize classification. BitMEX
and Bybit adapters are the reference implementations.
Client-side vs handler-side propagation
| Boundary | Failure type | Behavior |
|---|---|---|
| Client → handler (channel) | cmd_tx.send(...) failure | Propagate immediately as Result<(), Error>. Handler unavailable = caller’s problem. |
| Handler → network (WebSocket) | Transient send failure | Retry via RetryManager with backoff. |
| Handler → network (after retries exhausted) | Permanent send failure | Emit SendFailed event on out_tx; exec dispatch converts to OrderRejected. |
Never silently swallow. Every failure produces either a
propagated Result::Err or a typed event.
Rate limiting - the quota pattern
The page is explicit about naming conventions:
- REST quotas:
{VENUE}_REST_QUOTA(e.g.,OKX_REST_QUOTA) - WebSocket quotas:
{VENUE}_WS_{OPERATION}_QUOTA(e.g.,OKX_WS_CONNECTION_QUOTA,OKX_WS_ORDER_QUOTA) - Rate limit keys:
{VENUE}_RATE_LIMIT_KEY_{OPERATION}(e.g.,OKX_RATE_LIMIT_KEY_SUBSCRIPTION)
Standard WebSocket keys:
| Key | Operations |
|---|---|
*_RATE_LIMIT_KEY_SUBSCRIPTION | Subscribe, unsubscribe, login |
*_RATE_LIMIT_KEY_ORDER | Place orders (regular and algo) |
*_RATE_LIMIT_KEY_CANCEL | Cancel orders, mass cancel |
*_RATE_LIMIT_KEY_AMEND | Amend/modify orders |
Defined as LazyLock<Quota> static variables:
pub static OKX_REST_QUOTA: LazyLock<Quota> =
LazyLock::new(|| Quota::per_second(NonZeroU32::new(250).unwrap()));
pub static OKX_WS_SUBSCRIPTION_QUOTA: LazyLock<Quota> =
LazyLock::new(|| Quota::per_hour(NonZeroU32::new(480).unwrap()));
pub const OKX_RATE_LIMIT_KEY_ORDER: &str = "order";Pass keys when sending: self.send_with_retry(payload, Some(vec![OKX_RATE_LIMIT_KEY_ORDER.to_string()])).await.
For UW: REST is rare (chain snapshot pulls); WS has no operation
quotas because UW doesn’t expose order operations. UW v0 needs only
UW_REST_QUOTA: ~120/min matching UW’s standard tier.
Credential resolution - the canonical pattern
credential_env_vars() + Credential::resolve()
Each adapter’s common/credential.rs provides:
use nautilus_core::env::resolve_env_var_pair;
pub fn credential_env_vars(is_testnet: bool) -> (&'static str, &'static str) {
if is_testnet {
("{VENUE}_TESTNET_API_KEY", "{VENUE}_TESTNET_API_SECRET")
} else {
("{VENUE}_API_KEY", "{VENUE}_API_SECRET")
}
}
impl Credential {
pub fn resolve(
api_key: Option<String>,
api_secret: Option<String>,
is_testnet: bool,
) -> Option<Self> {
let (key_var, secret_var) = credential_env_vars(is_testnet);
let (k, s) = resolve_env_var_pair(api_key, api_secret, key_var, secret_var)?;
Some(Self::new(k, s))
}
}Naming conventions
| Environment | API Key Variable | API Secret Variable |
|---|---|---|
| Mainnet/Live | {VENUE}_API_KEY | {VENUE}_API_SECRET |
| Testnet | {VENUE}_TESTNET_API_KEY | {VENUE}_TESTNET_API_SECRET |
| Demo | {VENUE}_DEMO_API_KEY | {VENUE}_DEMO_API_SECRET |
Some venues require additional credentials (OKX uses
OKX_API_PASSPHRASE).
Key principles
- Environment variable names centralized in
credential_env_vars(), never duplicated as string literals. - Resolution happens in core Rust code, not Python bindings.
get_or_env_var_optfor optional creds (returnsNoneif missing).get_or_env_varwhen required (returns error if missing).- Invalid credentials must fail fast with an error, never silently degrade to unauthenticated mode.
For UW Python v0: config dataclass with api_key: str | None = None
falling back to os.environ["UW_API_KEY"] in
__post_init__/factory.
WebSocket two-layer architecture - outer client + inner handler
This is the most architecturally distinctive part of the page and the
hardest to internalize. The key insight: the handler exclusively
owns the WebSocketClient (no RwLock); the outer client sends
commands via lock-free mpsc channel and receives events via
another mpsc channel.
Outer client ({Venue}WebSocketClient)
pub struct MyWebSocketClient {
cmd_tx: Arc<tokio::sync::RwLock<UnboundedSender<HandlerCommand>>>,
out_rx: Option<Arc<UnboundedReceiver<MyWsMessage>>>,
task_handle: Option<Arc<JoinHandle<()>>>,
connection_mode: Arc<ArcSwap<AtomicU8>>, // Lock-free connection state
signal: Arc<AtomicBool>, // Cancellation signal
auth_tracker: AuthTracker, // From nautilus_network
subscription_state: Arc<SubscriptionState>,
subscription_args: Arc<DashMap<String, SubscriptionArgs>>,
}Responsibilities:
- Orchestrates connection lifecycle, authentication, subscriptions.
- Maintains state Python might query via
Arc<DashMap>. - Tracks subscription state for reconnection logic.
- Stores instruments cache for replay on reconnect.
- Sends commands to handler via
cmd_txchannel. - Receives venue events via
out_rxchannel.
Inner handler ({Venue}WsFeedHandler)
pub(super) struct MyWsFeedHandler {
inner: Option<WebSocketClient>, // Exclusively owned - no RwLock
cmd_rx: UnboundedReceiver<HandlerCommand>,
raw_rx: UnboundedReceiver<Message>,
out_tx: UnboundedSender<MyWsMessage>,
pending_requests: AHashMap<String, RequestData>, // Single-threaded - no locks
pending_messages: VecDeque<MyWsMessage>, // Multi-message buffer
retry_manager: RetryManager<MyWsError>,
}Responsibilities:
- Runs in dedicated Tokio task as stateless I/O boundary.
- Owns
WebSocketClientexclusively (noRwLockneeded). - Processes commands from
cmd_rx→ serializes to JSON → sends via WebSocket. - Receives raw frames → deserializes into
{Venue}WsFrame→ converts to{Venue}WsMessage→ emits viaout_tx. - Owns pending request state using
AHashMap<K, V>(single-threaded, no locking). - Uses
VecDeque<{Venue}WsMessage>to buffer multi-message yields from a single frame parse.
Key principles
- No shared locks on hot path: handler owns
WebSocketClient; client sends commands via lock-freempscchannel. - Command pattern for all sends: subscriptions, orders, cancels
all route through
HandlerCommandenum. - Event pattern for state: handler emits
{Venue}WsMessageevents (includingAuthenticated); client maintains state from events. - Pending state ownership: handler owns
AHashMapfor matching responses (noArc<DashMap>between layers). - Message buffering: handler uses
VecDeque<{Venue}WsMessage>for frames that produce multiple output messages. Thenext()method drains the queue before polling channels. - Python constraint: client uses
Arc<DashMap>only for state Python might query; handler usesAHashMapfor internal matching.
Connection-state tracking - Arc<ArcSwap<AtomicU8>>
use arc_swap::ArcSwap;
pub struct MyWebSocketClient {
connection_mode: Arc<ArcSwap<AtomicU8>>,
signal: Arc<AtomicBool>,
}Pattern:
- Outer
Arc: shared across all clones. ArcSwap: enables atomic pointer replacement via.store().- Inner
Arc<AtomicU8>: actual connection state fromWebSocketClient::connection_mode_atomic().
Initialize with placeholder atomic (ConnectionMode::Closed); in
connect() call .store(client.connection_mode_atomic()) to atomically
swap to the real client’s state. All clones see updates instantly
through lock-free .load() calls in is_active().
Subscription lifecycle (the SubscriptionState machine)
| Trigger | Method | From state | To state | Notes |
|---|---|---|---|---|
| User subscribes | mark_subscribe() | - | Pending | Topic added to pending set |
| Venue confirms | confirm() | Pending | Confirmed | Moved from pending to confirmed |
| Venue rejects | mark_failure() | Pending | Pending | Stays pending for retry on reconnect |
| User unsubscribes | mark_unsubscribe() | Confirmed | Pending | Temporarily pending until ack |
| Unsubscribe ack | clear_pending() | Pending | Removed | Topic fully removed |
Key principles:
subscription_count()reports only confirmed subscriptions, not pending.- Failed subscriptions remain pending and are automatically retried on reconnect.
- Both confirmed and pending subscriptions are restored after reconnection.
- Unsubscribe operations check the
opfield in acknowledgments to avoid re-confirming topics.
WsDispatchState - execution dispatch state
For execution adapters only. Lives in websocket/dispatch.rs. Tracks
which lifecycle events have already been emitted to prevent duplicates
across reconnections and fast-fill races:
#[derive(Debug, Default)]
pub struct WsDispatchState {
pub order_identities: DashMap<ClientOrderId, OrderIdentity>,
pub emitted_accepted: DashSet<ClientOrderId>,
pub triggered_orders: DashSet<ClientOrderId>,
pub filled_orders: DashSet<ClientOrderId>,
clearing: AtomicBool,
}Each DashSet is bounded by DEDUP_CAPACITY (typically 10,000).
When full, evict_if_full() clears atomically using
compare-exchange on clearing flag.
Cross-source fill dedup - BoundedDedup<T>
For adapters receiving fills from multiple sources (WebSocket user data + HTTP reconciliation):
struct BoundedDedup<T> {
order: VecDeque<T>,
set: AHashSet<T>,
capacity: usize,
}Fixed-capacity FIFO. insert() returns true if value already
present (signals duplicate). Used for trade IDs, typically
(Ustr, i64) of (symbol, trade_id). Capacity 10,000 sufficient
for most venues without unbounded memory growth.
Order book delta F_LAST / F_SNAPSHOT - the silent-bug invariant
When implementing _subscribe_order_book_deltas or streaming book
data, RecordFlag flags must be set correctly:
F_LAST: set on the last delta of every logical event group.DataEngineuses this as the flush signal whenbuffer_deltasis enabled. Without it, deltas accumulate indefinitely and are never published to subscribers.F_SNAPSHOT: set on all deltas belonging to a snapshot sequence (aClearaction followed byAddactions reconstructing the book).- Empty book snapshot: the
Cleardelta must haveF_SNAPSHOT | F_LAST. - Incremental updates: each venue update message ends with a
delta with
F_LASTset. If the venue batches multiple updates into one message, terminate each logical group withF_LAST.
from nautilus_trader.model.enums import RecordFlag
# Incremental update (single event)
delta = OrderBookDelta(
instrument_id=instrument_id,
action=BookAction.UPDATE,
order=order,
flags=RecordFlag.F_LAST,
sequence=sequence,
ts_event=ts_event,
ts_init=ts_init,
)
# Snapshot sequence
clear_delta = OrderBookDelta(
action=BookAction.CLEAR,
flags=RecordFlag.F_SNAPSHOT, # Not last
...
)
last_add_delta = OrderBookDelta(
action=BookAction.ADD,
flags=RecordFlag.F_SNAPSHOT | RecordFlag.F_LAST, # End of snapshot
...
)A missing F_LAST is a silent bug: no error raised, but
subscribers never receive the data when buffering is enabled.
For UW: not relevant (UW doesn’t publish books). For any future adapter with book data, this is non-negotiable.
Reconciliation report variants - the four shapes
The page (paired with Nautilus Execution)
mandates that every LiveExecutionClient must produce one of four
reconciliation report variants on demand:
| Variant | Use case |
|---|---|
OrderStatusReport | Standalone order state update (Accepted, PartiallyFilled, Canceled, Expired). |
FillReport | Standalone execution. Used by venues that surface fills for venue-initiated closures without opening user-level orders (e.g., Hyperliquid liquidations). |
OrderWithFills | Status update bundled with fills atomically. Used by Binance Futures for ADL/liquidation/settlement. |
PositionStatusReport | Position snapshot from venue. Logged but advisory; positions are derived from fills, not bootstrapped from this report. |
For UW: skipped (data-only). For IBKR (already shipped adapter): all four are implemented.
Testing requirements (link forward)
The page details a comprehensive testing matrix. Full detail in the two parallel pages:
- Data testing (nautilus-dev-spec-data-testing.md) -
the
DataTestermatrix (TC-D01..TC-D60+: instruments, book deltas, book at interval, book depth, snapshots, managed-book-from-deltas, quotes, trades, bars, mark/index/funding, custom data). - Execution testing (nautilus-dev-testing.md) -
the
ExecTestermatrix (TC-E01..TC-E50+: market BUY/SELL, IOC/FOK/GTC TIF, limit, post-only, cancel, modify, stop-loss, take-profit, brackets, batch, rejection, reconnect reconciliation).
Each adapter must pass the subset matching its supported capabilities - this is the contract for shippability.
The page itself enumerates HTTP integration coverage:
- Happy paths - fetch a public resource, verify Nautilus-domain conversion.
- Credential guard - call private endpoint without creds, assert structured error; repeat with creds, prove success.
- Rate limiting / retry mapping - surface venue rate-limit responses and verify correct error variant.
- Query builders - exercise paginated/time-bounded endpoints, assert emitted query string matches venue spec.
- Error translation - non-2xx responses map to adapter error enums with original code/message.
And WebSocket integration coverage:
- Login handshake - successful + failed cases.
- Ping/Pong - both text-based and control-frame pings.
- Subscription lifecycle - public + private channels.
- Reconnect behaviour - re-auth + subscription replay.
- Message routing - payloads arrive as correct
{Venue}WsMessagevariant. - Quota tagging - operations tagged with appropriate quota label.
CI robustness:
- Never use bare
tokio::time::sleep()with arbitrary durations. Tests become flaky under CI load. - Use
wait_until_asynctest helper to poll for conditions with timeout. - Prefer event-driven assertions with shared state.
Cortana MK3 implications - the UW adapter authoring plan
This is the load-bearing section. The concept page nautilus-adapters.md gave the LOC estimate (~870 Python LOC for v0). This section gives the complete file-by-file blueprint that turns that estimate into a build-able artifact.
Why UW v0 ships Python-only (not Rust)
The page is clear that the Rust core is the supported production
path for new work. But Python-only Phase 1-3 collapse into
data.py + parsing.py is a documented escape hatch for venues
where the Python WS parse path is not a measured bottleneck. UW
peaks at ~10 alerts/sec; the Python path is fine. v0 = Python-only.
v1 = Rust + PyO3 only if profiling shows hot path.
Component-status reminder
| Component | Status | Cortana UW v0 path |
|---|---|---|
HttpClient | Required | httpx.AsyncClient wrapper in data.py (REST chain pulls) |
WebSocketClient | Required | websockets library wrapper in data.py (alerts) |
InstrumentProvider | Required (near-no-op) | providers.py returns empty list |
LiveDataClient | Required (the main work) | data.py - UWLiveDataClient subclass |
LiveExecutionClient | Skipped | UW is read-only; execution stays on IBKR |
File-by-file blueprint for cortana_mk3/adapters/uw/
The adapter lives under the Cortana MK3 source tree, shaped after the Python tree of a Nautilus adapter. Each file has a target LOC budget derived from the dev guide patterns + the concept-page estimate. Total target: ~870 LOC Python, including ~300 LOC of tests.
| File | LOC | Purpose |
|---|---|---|
__init__.py | ~15 | Re-exports public surface (UnusualWhalesLiveDataClientFactory, UWConfig, UWFlowAlert, OptionChainSnapshot, UW ClientId constant); declares __all__. |
constants.py | ~30 | URL constants (UW_WS_URL, UW_REST_URL); env-var keys (UW_API_KEY); rate-limit quota constants (UW_REST_QUOTA = 120/min); UW = ClientId("UW") constant. |
config.py | ~60 | UWConfig(LiveDataClientConfig) dataclass: api_key: str | None, api_secret: str | None (env-fallback in __post_init__), channels: list[str], historical_lookback_secs: int = 60, reconnect_max_attempts: int = 10, reconnect_backoff_max_secs: int = 60, ws_heartbeat_secs: int = 30. Inherits all LiveDataClientConfig knobs. |
credential.py | ~30 | resolve_credentials(api_key, api_secret) -> tuple[str, str] mirroring the Rust Credential::resolve() pattern. Centralized env-var resolution; fail-fast on invalid creds; never silently degrade to unauthenticated. |
parsing.py | ~80 | Frame → typed payload converters. parse_flow_alert(raw: dict) -> UWFlowAlert (the load-bearing parser ported from cortanaroi/data/uw_ws_parser.py - battle-tested against UW quirks #54 strike-format and #59 timestamp-unit). parse_option_chain(raw: dict) -> OptionChainSnapshot. Each parser returns a @customdataclass instance with ts_event = int(raw["ts_ms"]) * 1_000_000 (ms → ns) and ts_init = self._clock.timestamp_ns(). |
messages.py | ~50 | Wire-message dataclasses (msgspec) for UW’s WS frames: UWFlowAlertFrame, UWChannelAck, UWPing, UWHeartbeat, UWErrorFrame. Mirrors Rust {Venue}WsFrame enum. Internal to the adapter; never exposed to consumers. |
custom_data.py | ~80 | The two @customdataclass types Cortana publishes: UWFlowAlert (instrument_id, strike, expiry, option_side, aggressor_side, premium_usd, size_contracts, is_sweep, is_block, flow_score, underlying_price, raw_id) and OptionChainSnapshot (instrument_id, expiry, chain_json). Imported by every consumer (strategy, scoring actor, brain-logger). |
providers.py | ~30 | UWInstrumentProvider(InstrumentProvider) - near-no-op. load_all_async returns empty list (UW does not define instruments). load_async and load_ids_async delegate to the shared Cache populated by the IBKR adapter when an InstrumentId is requested. |
http.py | ~100 | UWHttpClient wrapping httpx.AsyncClient. Methods: connect() (auth handshake), disconnect(), get_option_chain(ticker, expiry) -> dict, get_net_flow(expiry) -> dict. Uses retry classification (Retryable/NonRetryable/Fatal) implemented as Python exceptions; respects UW_REST_QUOTA via aiolimiter.AsyncLimiter. |
websocket.py | ~150 | UWWebSocketClient wrapping websockets.connect. Mirrors the dev-guide outer/inner pattern as a single-process simplification: connection-state asyncio.Event, cmd_queue: asyncio.Queue, out_queue: asyncio.Queue, subscription_state: dict[str, Literal["pending","confirmed"]], subscription_args: dict[str, dict] (for replay). On RECONNECTED sentinel: re-auth, re-subscribe via subscription_args replay. Three-step close() (send Disconnect → set stop signal → await task with timeout, abort if stuck). Heartbeat / ping handling. |
data.py | ~200 | The heart of the adapter. UWLiveDataClient(LiveDataClient) subclass. Method-ordering convention: connect → subscribe → unsubscribe → request. _connect: opens WS via UWWebSocketClient, opens REST via UWHttpClient, primes instrument cache (no-op), signals connected. _disconnect: three-step shutdown of both clients. _subscribe(SubscribeData): matches on data_type.type; if UWFlowAlert, joins UW channel; tracks subscription via subscription_state.mark_subscribe. _unsubscribe(UnsubscribeData): parallel path. _request(RequestData): matches on data_type.type; if OptionChainSnapshot, issues REST chain-pull, parses, calls self._handle_data_response(request_id, snapshot). _on_ws_message(frame): parses to UWFlowAlert, calls self._handle_data(alert). |
factories.py | ~60 | UnusualWhalesLiveDataClientFactory.create(loop, name, config: UWConfig, msgbus, cache, clock) -> UWLiveDataClient - the registration seam called by LiveNode.builder().add_data_client(...). Wires the config to the live client via UWHttpClient + UWWebSocketClient instantiation in correct order (REST first, then WS). Also factory for execution-side noop (raises clear error: “UW is data-only; route execution through IBKR”). |
tests/test_data_client.py | ~120 | Pytest integration tests against mock UW WebSocket fixtures. Covers connect/disconnect, subscribe → confirm → emit, reconnect → re-subscribe, frame-parse error handling (drop with warn, never panic), _handle_data invocation count. Uses wait_until_async-style helpers, no bare asyncio.sleep. |
tests/test_parsing.py | ~80 | Frame-parser unit tests. Property tests for ts_ms × 1e6 == ts_event_ns invariant. Regression tests for GH #54 strike-format and GH #59 timestamp-unit issues. |
tests/test_factories.py | ~50 | Factory wiring tests; config validation; env-var fallback verification; fail-fast on invalid creds. |
tests/test_providers.py | ~50 | UWInstrumentProvider returns empty list; load_async delegates correctly to shared Cache; multi-tenant isolation (per-tenant UW ClientId). |
Total: ~1,185 LOC (15+30+60+30+80+50+80+30+100+150+200+60+120+80+50+50). The concept-page estimate of ~870 LOC is the non-test core (~885 LOC: 15+30+60+30+80+50+80+30+100+150+200+60 = 885). Tests add ~300 LOC. Numbers align.
Realistic v0 ship: 2-3 working days for the non-test core, plus 1-2 days for tests. Matches the mk3-roadmap estimate.
Phase mapping
| Phase | UW v0 work |
|---|---|
| 1 - Rust core | Skipped (Python-only). Equivalent: http.py + websocket.py + parsing.py (~330 LOC). |
| 2 - Instruments | Near-no-op. providers.py (~30 LOC). |
| 3 - Market data | The bulk. data.py + custom_data.py (~280 LOC). |
| 4 - Execution | Skipped. UW is data-only. |
| 5 - Advanced features | data.py::_request for option-chain snapshot (~30 LOC inside data.py). |
| 6 - Config + factories | config.py + factories.py + credential.py + constants.py (~180 LOC). |
| 7 - Testing + docs | tests/* (~300 LOC). |
Wiring into LiveNode (Cortana MK3 sketch)
from nautilus_trader.live.node import LiveNode, Environment
from nautilus_trader.config import LiveDataEngineConfig
from nautilus_trader.adapters.interactive_brokers.factories import (
InteractiveBrokersLiveDataClientFactory,
InteractiveBrokersLiveExecClientFactory,
)
from cortana_mk3.adapters.uw.factories import UnusualWhalesLiveDataClientFactory
from cortana_mk3.adapters.uw.config import UWConfig
uw_config = UWConfig(
api_key=None, # falls back to UW_API_KEY env
api_secret=None, # falls back to UW_API_SECRET env
channels=["flow-alerts:SPY"],
)
node = (
LiveNode.builder("CORTANA-PAPER", TraderId("CORTANA-001"), Environment.LIVE)
.with_data_engine_config(LiveDataEngineConfig(...))
# UW data only
.add_data_client(None, UnusualWhalesLiveDataClientFactory(), uw_config)
# IBKR data + execution
.add_data_client(None, InteractiveBrokersLiveDataClientFactory(), ibkr_data_cfg)
.add_exec_client(None, InteractiveBrokersLiveExecClientFactory(), ibkr_exec_cfg)
.build()
)Two add_data_client calls, one add_exec_client. UW never sees an
order command.
Anti-patterns (page-derived)
- Subscribing without preserving original args. Reconnect replay
via topic-string parsing is brittle; preserve original
SubscriptionArgsin a separateArc<DashMap<String, SubscriptionArgs>>. - Setting stop signal before sending Disconnect. Handler exits before processing the disconnect command. Send Disconnect first.
- Bare
tokio::time::sleep/asyncio.sleepin tests. Tests flake under CI load. Usewait_until_asyncpolling helpers. - Centralizing env-var name strings as duplicated literals.
Always go through
credential_env_vars()(Rust) or the equivalent Python helper. - Plain
tokio::spawnfrom a Rust adapter. Tasks must usenautilus_common::live::get_runtime().spawn(...). Plain spawn panics from Python threads. Pre-commitcheck_tokio_usage.shenforces. - Skipping
await_account_registeredin execution_connect. Orders denied during reconciliation if portfolio doesn’t yet know the account. - Forgetting
F_LASTon the last delta of a book event group. Silent bug: subscribers never receive the data when buffering is enabled. - Holding object references to emulated orders. They transform
on release; query the cache by
client_order_id. - Using
Arc<DashMap>between client/handler for state only the handler needs. Handler owns single-threadedAHashMap; no locking required because the handler runs on a dedicated task. - Manually ordering subscribe/unsubscribe pairs in adapter classes. Use the doc-blessed grouping: connect → subscribe → unsubscribe → request.
- Documenting individual parameters in
# Argumentssections. The doc explicitly says: don’t. Type signatures + descriptive names should be self-explanatory. - Putting credential resolution in Python bindings. Resolution happens in core Rust code; Python only receives the resolved values.
- Returning
Result::Errfromextern "C"boundaries. Rust panics must never unwind across the FFI boundary. Wrap incrate::ffi::abort_on_panic(|| { ... }). - Using
as_legacy_cython=Truefor IMBALANCE/STATISTICS. PyO3-only types; raisesValueError. (Nautilus-data quirk; mentioned for cross-reference completeness.)
When this concept applies
- Authoring the UW WebSocket adapter for MK3 (the primary use case).
- Authoring any future data adapter (Tradier, Polygon options, CME data feed, Tardis pull-through).
- Authoring any future execution adapter (alternate broker, prime broker integration).
- Reviewing the shipped IBKR adapter source code for pattern conformance.
- Triaging adapter test failures by knowing what spec the test is enforcing.
- Estimating effort for a “venue X support” feature request.
When it does not apply
- The page does not document the
LiveNodebuilder or strategy registration - see nautilus-live.md / nautilus-architecture.md. - The page does not document custom-data-class authoring ergonomics
- The page does not document the test-tier ladder (unit →
parametrized → property → integration → fuzz → spec → DST → formal)
- see nautilus-developer-guide.md Testing section.
- The page does not document the spec-acceptance test matrices in detail - see nautilus-dev-spec-data-testing.md and the execution-spec parallel.
- IBKR-specific quirks (OCA / OCO not auto-creating IB-side OCA
groups,
fetch_all_open_orders=True, IB pacing limits) are in nautilus-ib.md, not here.
See Also
- Nautilus Adapters (concept) - the conceptual contract; this page is the implementation contract.
- Nautilus Developer Guide (extension+contribution) - the higher-level “how does this all fit into the project” view; Cortana → Nautilus translation table.
- Nautilus Data Model - built-in
Datatypes,ts_event/ts_init, ParquetDataCatalog. Adapters emit these types. - Nautilus Custom Data - defining
UWFlowAlertandOptionChainSnapshotas@customdataclasstypes the adapter emits. - Nautilus Execution - ExecutionClient routing, four reconciliation report variants, OMS handling.
- Nautilus Data Testing Spec -
the
DataTestermatrix every data adapter must pass a subset of. - Nautilus Developer Testing - execution
testing spec parallel; the
ExecTestermatrix. - Nautilus Interactive Brokers Integration - the shipped IBKR adapter Cortana wires through; data + execution side.
- Nautilus Databento Integration - the closest data-only adapter shape to UW; recommended reading order before writing UW v0.
- Spike plan:
~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-nautilus-spike.md- Step 4 (Cortana → Nautilus mapping) and Step 7 question 7 (“How hard does the UW custom DataClient look?”). - MK3 roadmap:
~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-mk3-roadmap.md- the milestone that owns the UW adapter build (estimated 2-3 days for v0 Python). - Source: https://nautilustrader.io/docs/latest/developer_guide/adapters/
Timeline
2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 7 (developer guide).