# NautilusTrader Documentation Source: https://nautilustrader.io/docs/latest/ # NautilusTrader Documentation NautilusTrader is an open-source, production-grade, Rust-native trading engine infrastructure for multi-asset, multi-venue trading systems. Its core spans research, deterministic simulation, portfolio and risk modeling, and live execution under one event-driven architecture. Python serves as the control plane for strategy logic, configuration, and orchestration, while entire systems can also be built in Rust. NautilusTrader is engineered to minimize the gap between research and production. The same strategy and execution-algorithm code runs in both backtests and live systems, reducing deployment risk and unexpected behavior. Modular adapters keep venue integration neutral. Current integrations span crypto venues, traditional markets, betting exchanges, and data providers, with custom adapters available for REST, WebSocket, and provider-specific APIs. ## Where to start - [Architecture](concepts/architecture): event-driven design, core components, and crash-only design. - [Backtesting](concepts/backtesting): simulation APIs, data-loading contracts, and post-run analysis. - [Live trading](concepts/live): command outcomes, execution reconciliation, and startup recovery. }> Install NautilusTrader and run your first backtests. }> Architecture, data, execution, backtesting, live trading, and core domain model guides. }> Goal-oriented recipes for data workflows, backtesting, live trading, and common operational tasks. }> Runnable walkthroughs for backtesting, data workflows, strategy patterns, options, and Rust. }> Supported venue and data-provider adapters, status, and setup guides. }> Rust and Python internals, testing standards, adapter specs, and release process. # Accounting Source: https://nautilustrader.io/docs/latest/concepts/accounting/ The accounting subsystem tracks balances, margins, and PnL for every account the platform interacts with. This guide covers the data model, the query API that strategies use, and the conventions adapter authors must follow to stay consistent across venues. It applies equally to backtest and live trading. For backtest-specific configuration (starting balances, margin-model selection per venue), see [Backtesting](backtesting/). ## Account types When you attach a venue to the engine for either live trading or a backtest, you pick one of three accounting modes via `account_type`: Cash, Margin, or Betting. A fourth account type, Wallet, models on-chain wallet state. The Blockchain adapter selects it; its execution client supports locally signed Uniswap V3 market swaps but is not production-ready. | Account type | Typical use case | What the engine locks | | ------------ | ----------------------------------------------- | ------------------------------------------------------------------------- | | Cash | Spot trading (e.g., BTC/USDT, stocks) | Notional for pending buy orders; quantity for pending sell orders. | | Margin | Derivatives or any product that allows leverage | Initial margin for each order plus maintenance margin for open positions. | | Betting | Sports betting, bookmaking | Stake required by the venue; no leverage. | | Wallet | Blockchain wallets (DeFi) | Amounts reserved locally for pending orders; no leverage or borrowing. | ### Cash accounts Cash accounts **settle trades in full**; there is no leverage and therefore no concept of margin. Locked balances reflect the value reserved for pending orders: the notional value of each pending buy and the quantity each pending sell would deliver. ### Margin accounts Margin accounts support instruments that require collateral, such as futures or leveraged crypto perps. They track account balances, reserve margin for open orders and positions, and apply a configurable leverage per instrument. Margin is tracked in two scopes; see [Margin scopes](#margin-scopes) below. **Terms**: - **Leverage**: amplifies exposure relative to account equity. Higher leverage raises both potential returns and risk. - **Initial margin**: collateral reserved when an order is submitted. - **Maintenance margin**: minimum collateral required to keep an open position. - **Locked balance**: funds reserved as collateral, not available for new orders. :::note Reduce-only orders do not contribute to `balance_locked` on cash accounts and do not add to initial margin on margin accounts, since they can only decrease exposure. Wallet orders still reserve the input asset because the on-chain transaction spends that asset even when the order reduces a position. ::: ### Betting accounts Betting accounts are specialized for venues where you stake an amount to win or lose a fixed payout (prediction markets, sports books). The engine locks only the stake required by the venue; leverage and margin do not apply. ### Wallet accounts Wallet accounts represent blockchain wallets: unleveraged, multi-currency holdings of native and ERC-20 token balances with no margin and no borrowing. For reported states, `total` is the observed on-chain balance; `locked` tracks local pending-order reservations, and `free = total - locked`. Account state events **contribute totals only**: the account ignores incoming `locked` and `free` values, retains its local reservations, and rederives `free`. It rebuilds transient reservations from submitted and open orders during live startup. While an amendment is pending, the account reserves the full observed balance of the debit currency because the pending event does not carry the requested terms. If the reserved amount exceeds the latest observed total, `locked` is capped at `total` and `free` remains zero until the balance or reservation changes. A balance with a negative total is rejected rather than applied. ERC-20 allowances are spender authorizations and are never represented as balances or locked funds. ## Balance model An `AccountBalance` holds three values in the same currency: - `total`: the venue-reported total balance figure (wallet, net liquidation, or margin balance, depending on the venue). - `locked`: amount reserved against open orders and positions. - `free`: amount available for new orders (`total - locked`). The **balance invariant** `total == locked + free` must always hold at currency precision. The Python `AccountBalance(total, locked, free)` constructor requires all three fields up front. Adapter code written in Rust has two additional derived constructors that enforce the invariant centrally; prefer them over `AccountBalance::new` whenever the venue reports only two of the three values: | Rust constructor | When to use | | --------------------------------------- | ------------------------------------------------------------------------ | | `AccountBalance::from_total_and_locked` | Venue reports total and locked; `free` is derived from the two. | | `AccountBalance::from_total_and_free` | Venue reports total and free; `locked` is derived from the two. | | `AccountBalance::new` | All three values are already known and consistent (tests, pass-through). | Each derived constructor clamps the venue-reported field into `[0, total]` when `total >= 0`, so transient overshoots from venue rounding never leave the account in a broken state. ## Currency and valuation contracts Accounting values retain their source currency until an explicit conversion succeeds. This prevents a valid number from being labeled with the wrong currency or an unavailable value from being treated as zero. | Value | Currency contract | | ---------------------------- | -------------------------------------------------------------- | | Instrument cost currency | Base for inverse, settlement for quanto, and quote otherwise. | | Position PnL | Instrument cost currency captured when the position opens. | | Calculated locks and margins | Each calculated amount's currency, converted independently. | | Portfolio aggregates | Native buckets, or the account base after conversion succeeds. | Aggregations combine only compatible `Money` values. An account without a base currency keeps separate native currency buckets. A single-instrument realized PnL query returns unavailable instead of combining mixed currencies. The accounting and valuation paths also follow these rules: - Invalid or unrepresentable notional, PnL, fee, locked-balance, and margin results produce an error, unavailable value, or unpriced state. They do not substitute zero. A failed realized PnL recalculation also clears any earlier cached result. - `equity()` counts a credited non-inverse base asset once for a multi-currency cash account without a base currency. `mark_values()` remains a gross position-value query and includes that asset. - MTM snapshots distinguish carried stale inputs from positions that have never had complete valuation data. Stale-price metadata covers only open instrument and position-side pairs. See [Portfolio](portfolio.md#equity-and-mark-to-market) for equity formulas, price and xrate selection, snapshot metadata, and missing-price query scope. ## Margin scopes A `MarginBalance` has four fields: `initial`, `maintenance`, `currency`, and an `Optional[InstrumentId]` that selects one of two scopes. ### Per-instrument scope `MarginBalance.instrument_id` is set to a concrete instrument. Use this for: - Isolated margin (per-position collateral). - Backtest or calculated margin, where the `AccountsManager` derives margin locally from open orders and positions per instrument. ### Account-wide scope `MarginBalance.instrument_id` is `None`. The entry is keyed by its `currency` (the collateral currency). Use this for cross-margin venues that report a single aggregate per collateral currency. A venue may emit one account-wide entry (single-collateral cross margin) or several (one per collateral coin). Both scopes coexist on the same `MarginAccount` in separate internal stores. An `AccountState` event may carry entries in either or both scopes, and `MarginAccount.apply()` routes each entry to the correct store based on whether `instrument_id` is set. :::warning `MarginAccount.apply()` **replaces** both stores from the incoming event. It does not merge with prior state, and an event carrying neither balances nor margins leaves the prior stores in place. Adapters that emit partial snapshots must include every live margin entry on each update or those entries will be dropped until the next full snapshot. Balances the event carries replace the stored entry for their currency; currencies the event omits are retained. ::: ## Strategy query API Use the query that matches the venue's reporting shape. If a venue reports per-instrument margins, ask by `InstrumentId`. If it reports account-wide margins, ask by `Currency`. | Scope | Queries | | -------------- | ---------------------------------------------------------------------------- | | Per-instrument | `margin`, `initial_margin`, and `maintenance_margin` | | Account-wide | `account_margin`, `account_initial_margin`, and `account_maintenance_margin` | | Both scopes | `total_initial_margin` and `total_maintenance_margin` | The signatures below describe the Python bindings. Point queries return `None` when the entry is absent; total queries always return a `Money` (zero for the currency if nothing matches). ### Per-instrument queries (`MarginAccount`) - `margin(instrument_id) -> MarginBalance | None` - `initial_margin(instrument_id) -> Money | None` - `maintenance_margin(instrument_id) -> Money | None` - `margins() -> dict[InstrumentId, MarginBalance]` (all per-instrument entries) - `initial_margins() -> dict[InstrumentId, Money]` - `maintenance_margins() -> dict[InstrumentId, Money]` These methods only see the per-instrument store. On a cross-margin venue they return empty dicts or `None`. Use the account-wide queries below. ### Account-wide queries (`MarginAccount`) - `account_margin(currency) -> MarginBalance | None` - `account_initial_margin(currency) -> Money | None` - `account_maintenance_margin(currency) -> Money | None` - `account_margins() -> dict[Currency, MarginBalance]` (all account-wide entries) - `account_initial_margins() -> dict[Currency, Money]` - `account_maintenance_margins() -> dict[Currency, Money]` ### Totals (`MarginAccount`) These sum across per-instrument and account-wide entries for a given currency: - `total_initial_margin(currency) -> Money` - `total_maintenance_margin(currency) -> Money` Useful when a strategy trades on a venue where both scopes may appear (for example, isolated positions alongside cross-margin collateral). ### Python binding boundary This query surface does not expose the internal Rust mutation methods `update_margin`, `clear_margin`, `clear_account_margin`, `clear_initial_margin`, `clear_maintenance_margin`, or `set_margin_model`. Python does expose other mutation methods, including `update_initial_margin`, `update_maintenance_margin`, `set_default_leverage`, and `set_leverage`. ### Portfolio-level queries Margin queries: - `portfolio.instrument_initial_margins(venue=..., account_id=...) -> dict[InstrumentId, Money] | None` - `portfolio.instrument_maintenance_margins(venue=..., account_id=...) -> dict[InstrumentId, Money] | None` When a margin account resolves, these return the same per-instrument money views as `MarginAccount.initial_margins` and `MarginAccount.maintenance_margins`; otherwise, they return `None`. For account-wide data on cross-margin venues, query the account directly via `portfolio.account(venue=venue).account_initial_margin(ccy)`. The returned account is a **detached snapshot** and cannot mutate Portfolio state. PnL, exposure, mark-to-market, and equity queries all accept `venue` and an optional `account_id` to scope multi-account venues: - `portfolio.unrealized_pnls(venue=..., account_id=..., target_currency=...) -> dict[Currency, Money]` - `portfolio.realized_pnls(venue=..., account_id=..., target_currency=...) -> dict[Currency, Money]` - `portfolio.total_pnls(venue=..., account_id=..., target_currency=...) -> dict[Currency, Money]` - `portfolio.net_exposures(venue=..., account_id=..., target_currency=...) -> dict[Currency, Money] | None` - `portfolio.mark_values(venue=..., account_id=...) -> dict[Currency, Money]` - `portfolio.equity(venue=..., account_id=...) -> dict[Currency, Money]` - `portfolio.missing_price_instruments(venue, account_id=...) -> list[InstrumentId]` If both scope arguments are present, they must identify the same account. A missing price, failed target-currency conversion, or arithmetic overflow invalidates the whole affected collection: a query never returns partial or mixed-currency totals. See the [Portfolio guide](portfolio.md#equity-and-mark-to-market) for the equity formula, price fallback chain, base-currency conversion behavior, and the warn-once missing-price tracker. ### Worked examples Single-collateral cross margin (one account-wide entry): ```python usdc_margin = margin_account.account_initial_margin(USDC) usdc_total = margin_account.total_initial_margin(USDC) ``` Per-coin cross margin (one entry per collateral currency): ```python for ccy, margin_balance in margin_account.account_margins().items(): print(ccy, margin_balance.initial, margin_balance.maintenance) ``` ## Margin models NautilusTrader provides flexible margin calculation models for the calculated path (backtests, and live strategies running with `calculate_account_state=True` for reconciliation). Reported margins from a venue flow straight into the account's `margins` or `account_margins` stores without going through a model. ### Overview Different venues treat leverage differently: - **Traditional brokers** (e.g., Interactive Brokers): fixed margin percentages regardless of leverage. - **Crypto exchanges** (Binance, others): leverage may reduce margin requirements. Both built-in models compute margin as a percentage of notional using the instrument's `margin_init` and `margin_maint` fields. They differ only in whether leverage reduces the reservation. For venues with true per-contract fixed margin (CME / ICE), set `instrument.margin_init` and `margin_maint` so the percentage recovers the desired dollar amount. ### HEDGING-mode netting Under `OmsType.HEDGING`, the first fill for a new position ID opens a `Position`; later fills can update that position. An account can therefore hold many open sub-positions for the same instrument. The accounts manager nets those sub-positions onto a hypothetical NETTING position in `ts_opened` order, then runs the margin model once on the resulting net signed quantity and average open price. The replay follows the same rules as `Position.apply`: same-side fills produce a quantity-weighted average open price, opposite-side fills partial-close at the existing average, and a fill that crosses zero makes the residual take the flipping fill's price. Sub-positions sharing a `ts_opened` fold in `(ts_opened, position_id)` order so the result does not depend on cache iteration order. HEDGING and NETTING accounts compute the same maintenance margin when the folded net quantity and average open price match under the same margin model and leverage; the requirement scales with **net economic exposure**. ### Available models #### `StandardMarginModel` Uses **fixed percentages without leverage division**, matching traditional broker behavior. ```python # Fixed percentages - leverage ignored margin = notional * instrument.margin_init ``` - Initial margin: `notional_value * instrument.margin_init` - Maintenance margin: `notional_value * instrument.margin_maint` **Use cases**: traditional brokers (Interactive Brokers), forex brokers with fixed margin requirements. #### `LeveragedMarginModel` Divides margin requirements **by leverage**. ```python # Leverage reduces margin requirements adjusted_notional = notional / leverage margin = adjusted_notional * instrument.margin_init ``` - Initial margin: `(notional_value / leverage) * instrument.margin_init` - Maintenance margin: `(notional_value / leverage) * instrument.margin_maint` **Use cases**: crypto exchanges that reduce margin with leverage, venues where leverage affects margin requirements. ### Default behavior `MarginAccount` uses `LeveragedMarginModel` by default. Backtests select `StandardMarginModel` by passing it directly to `BacktestVenueConfig.margin_model`. ### Worked example: EUR/USD - **Instrument**: EUR/USD - **Quantity**: 100,000 EUR - **Price**: 1.10000 - **Notional**: $110,000 - **Leverage**: 50x - **`instrument.margin_init`**: 3% | Model | Calculation | Result | Percentage | | --------- | ---------------------- | ------ | ---------- | | Standard | $110,000 × 0.03 | $3,300 | 3.00% | | Leveraged | ($110,000 ÷ 50) × 0.03 | $66 | 0.06% | On a $1,000 account: the standard model blocks the trade; the leveraged model allows it. ### Python model selection Pass `StandardMarginModel()` or `LeveragedMarginModel()` directly to the backtest venue. The Python binding does not accept custom margin model subclasses or a `MarginModelConfig` wrapper. See [Backtesting](backtesting/accounts-and-margin.md#margin-models). ## Adapter convention Live adapters translate venue responses into `AccountBalance` and `MarginBalance` instances. The convention that adapter authors must follow: ### Building `AccountBalance` Prefer the derived constructors so that clamping and the `total == locked + free` invariant are enforced centrally. Hand-computing three fields and passing them to `AccountBalance::new` is only appropriate for pass-through paths where all three values are already authoritative (e.g., tests). ### Building `MarginBalance` Pick the scope that matches what the venue reports: | Venue reports | Scope | Emit with | | ---------------------------------------------- | -------------- | ---------------------------------------------------------- | | Per-instrument (isolated positions) | Per-instrument | `MarginBalance::new(initial, maint, Some(id))` | | Single aggregate per collateral (cross margin) | Account-wide | `MarginBalance::new(initial, maint, None)` | | Multiple aggregates, one per collateral | Account-wide | One `MarginBalance` per currency with `instrument_id=None` | :::info Synthetic `ACCOUNT.{VENUE}` or `ACCOUNT-{COIN}.{VENUE}` `InstrumentId` placeholders are not used. Account-wide entries carry `instrument_id=None` and are keyed by `currency`. ::: ## Related guides - [Backtesting](backtesting/): starting balances, margin models, and backtest-specific account setup. - [Portfolio](portfolio.md): portfolio-level PnL, exposures, and currency conversion. - [Positions](positions.md): position lifecycle, aggregation, and PnL. - [Adapters](adapters.md): requirements and best practices for adapter authors. - [Blockchain](../integrations/blockchain.md): the adapter that selects wallet accounts, and its execution status. # Actors Source: https://nautilustrader.io/docs/latest/concepts/actors/ A **data actor** receives requested and subscribed data, handles system events, and manages component state. In Python, extend the `DataActor` class; in Rust, implement the `DataActor` trait. A strategy adds order-management capabilities. **Capabilities**: - Market and custom data subscriptions and requests. - Custom data and signal publishing. - Event, timer, and alert handling. - Cache access. - Structured logging. ## Basic Python example Actors support configuration through a pattern similar to strategies. Declare custom fields as keyword-only arguments and accept `**_kwargs` so the base fields (`actor_id` and the log settings) pass through to `DataActorConfig.__new__`, which reads them from the same call. A positional argument would be matched against `actor_id` instead, raising a `TypeError`. ```python from nautilus_trader.common import DataActor from nautilus_trader.config import DataActorConfig from nautilus_trader.model import Bar from nautilus_trader.model import BarType class MyActorConfig(DataActorConfig): def __init__(self, *, bar_type: BarType, **_kwargs) -> None: super().__init__() self.bar_type = bar_type class MyActor(DataActor): def __init__(self, config: MyActorConfig) -> None: super().__init__(config) # Keep runtime state on the actor self.count_of_processed_bars: int = 0 def on_start(self) -> None: # Subscribe to bars matching the configured bar type self.subscribe_bars(self.config.bar_type) def on_bar(self, bar: Bar) -> None: self.count_of_processed_bars += 1 ``` ## Actor configuration and IDs Data actors can receive a `DataActorConfig` subclass. The base config accepts an optional `actor_id`. If supplied, the actor registers with that ID; otherwise a Python actor registers under its class name. Give each instance an explicit `actor_id` when running more than one instance of the same actor, because a duplicate ID is rejected at registration (a `RuntimeError` in Python). Treat configuration as construction data for the actor. Read user-supplied settings through `self.config`, and keep runtime state on the actor itself. :::info Rust implementation Rust actors store runtime identity and state in `DataActorCore`. Read the runtime ID through `actor_id()` rather than expecting a generated ID to be written back into `DataActorConfig`. A Rust actor without a configured `actor_id` registers as `DataActor` whatever its type, so give each Rust actor an explicit `actor_id`. Rust authors implement `DataActor` and use the facade methods on `self`. `DataActorNative` is native-only access for runtime wiring and borrowed core state. Import it only for same-binary performance paths or internal runtime wiring. ::: ## Lifecycle Actors move through the main stable states shown below: ```mermaid stateDiagram-v2 [*] --> READY : register() READY --> RUNNING : start() RUNNING --> STOPPED : stop() STOPPED --> RUNNING : resume() RUNNING --> DEGRADED : degrade() DEGRADED --> RUNNING : resume() STOPPED --> READY : reset() RUNNING --> FAULTED : fault() STOPPED --> DISPOSED : dispose() ``` Main flow only: transitional states and less common valid edges are omitted. For actions with a lifecycle handler, the actor reaches the destination state only after that handler succeeds. Override these methods to hook into lifecycle events: | Method | When called | | -------------- | ---------------------------------------------------------------------------------------------------------------------- | | `on_start()` | Actor is starting; subscribe to data here. | | `on_stop()` | Actor is stopping; clean up actor-owned resources. | | `on_resume()` | Actor is resuming after it stopped or degraded. | | `on_reset()` | Actor is resetting, including between backtest runs; retained data subscriptions are released after the hook succeeds. | | `on_degrade()` | Actor is entering a degraded state and may provide only partial functionality. | | `on_fault()` | Actor is entering the faulted state after it encounters a fault. | | `on_dispose()` | Actor is being disposed and must release its remaining resources. | ## Timers and alerts Actors have access to a clock for scheduling: ```python from datetime import timedelta from nautilus_trader.common import TimeEvent def on_start(self) -> None: self._schedule_clock_events() def on_resume(self) -> None: self._schedule_clock_events() def on_stop(self) -> None: self._cancel_clock_events() def on_degrade(self) -> None: self._cancel_clock_events() def _cancel_clock_events(self) -> None: self.clock.cancel_timer("my_actor.timer") self.clock.cancel_timer("my_actor.alert") def _schedule_clock_events(self) -> None: # Set a recurring timer with a callback that fires every 5 seconds self.clock.set_timer( "my_actor.timer", timedelta(seconds=5), callback=self._on_timer, ) # Set a one-time alert with a callback self.clock.set_time_alert( "my_actor.alert", self.clock.utc_now() + timedelta(minutes=1), callback=self._on_alert, ) def _on_timer(self, event: TimeEvent) -> None: self.log.info("Timer fired!") def _on_alert(self, event: TimeEvent) -> None: self.log.info("Alert triggered!") ``` Pass a `callback` to direct `TimeEvent` objects to your own method. Without one, the actor runtime connects the clock's registered default handler to `on_time_event()`. When components share a clock: - Use **explicit callbacks** to route events to the intended component. - Use **unique timer names** within the clock's namespace. Registering the same name replaces the existing timer. ## System access Actors have access to core system components: | API | Description | | ------------------------------------------- | ---------------------------------------------------- | | `self.cache` | Shared state for instruments, orders, and positions. | | `self.clock` | Current time and timer or alert scheduling. | | `self.log` | Structured logging. | | `publish_data()` / `subscribe_data()` | Structured custom data messaging. | | `publish_signal()` / `subscribe_signal()` | Lightweight alerts and notifications. | | `publish_message()` | Publish a Python object on an application topic. | | `subscribe_topic()` / `unsubscribe_topic()` | Manage Python object callbacks. | | `subscribe_queue_state()` | Live runner queue pressure state changes. | | `subscribe_socket_state()` | Live socket transport state changes. | | `reconnect_socket()` | Request recovery of one live socket endpoint. | | `unsubscribe_queue_state()` | Stop receiving runner queue pressure state changes. | | `unsubscribe_socket_state()` | Stop receiving socket transport state changes. | | `on_queue_state()` | Handle a runner queue pressure state change. | | `on_socket_state()` | Handle a socket transport state change. | The Python `DataActor` and `Strategy` APIs do not expose `self.msgbus`. Use custom data for structured payloads, signals for lightweight values, or [topic messaging](message_bus.md#python-topic-messaging) for arbitrary in-process Python objects. ### Queue pressure state Actors can subscribe to runner queue pressure state changes: ```rust tab="Rust" use nautilus_common::{ actor::DataActor, messages::system::QueueStateChanged, }; impl DataActor for MyActor { fn on_start(&mut self) -> anyhow::Result<()> { self.subscribe_queue_state(None, Some(50)); Ok(()) } fn on_stop(&mut self) -> anyhow::Result<()> { self.unsubscribe_queue_state(None); Ok(()) } fn on_queue_state(&mut self, event: &QueueStateChanged) -> anyhow::Result<()> { log::warn!( "Queue {:?} changed {:?} to {:?} at depth {}", event.channel, event.condition, event.state, event.queue_depth, ); Ok(()) } } ``` ```python tab="Python" from nautilus_trader.common import QueueStateChanged def on_start(self) -> None: self.subscribe_queue_state(priority=50) def on_stop(self) -> None: self.unsubscribe_queue_state() def on_queue_state(self, event: QueueStateChanged) -> None: self.log.warning( f"Queue {event.channel} changed {event.condition} to {event.state} " f"at depth {event.queue_depth}", ) ``` #### Filters and delivery The optional priority controls delivery order among matching subscribers. **Higher values run first.** Subscribing again does not change an existing priority; unsubscribe before subscribing with a new priority. Pass `channel` to select one runner channel, for example `self.subscribe_queue_state(channel=SystemChannel.DATA_EVENTS)` in Python. Import `SystemChannel` from `nautilus_trader.common`. - Omitting `channel` subscribes to all monitored channels. - To remove a subscription, pass the **same channel** to `unsubscribe_queue_state`. Omitting it removes only the unfiltered subscription. - Overlapping subscriptions each invoke the callback for a matching event. Runner queues are shared across clients, so they have no client filter. #### Queue event contents `QueueStateChanged` includes the trader ID, runner channel, queue condition, condition state, queue depth, mean dispatch time, event ID, and timestamps. Delivery uses the typed in-process message bus and has no external wire representation. See [Queue pressure monitoring](live.md#queue-pressure-monitoring) for the trigger and clear semantics. ### Socket transport state Actors can subscribe to socket state changes from live adapters that report them: ```rust tab="Rust" use nautilus_common::{ actor::DataActor, messages::system::SocketStateChanged, }; impl DataActor for MyActor { fn on_start(&mut self) -> anyhow::Result<()> { self.subscribe_socket_state(None, None, Some(50)); Ok(()) } fn on_stop(&mut self) -> anyhow::Result<()> { self.unsubscribe_socket_state(None, None); Ok(()) } fn on_socket_state(&mut self, event: &SocketStateChanged) -> anyhow::Result<()> { log::info!( "Socket {} for {} changed to {:?}", event.endpoint, event.client_id, event.state, ); Ok(()) } } ``` ```python tab="Python" from nautilus_trader.common import SocketStateChanged def on_start(self) -> None: self.subscribe_socket_state(priority=50) def on_stop(self) -> None: self.unsubscribe_socket_state() def on_socket_state(self, event: SocketStateChanged) -> None: self.log.info( f"Socket {event.endpoint} for {event.client_id} changed to {event.state}", ) ``` #### Filters and delivery The optional priority controls delivery order among matching subscribers. **Higher values run first.** Subscribing again does not change an existing priority; unsubscribe before subscribing with a new priority. Pass `client_id`, `endpoint`, or both to filter socket events. In Python, `self.subscribe_socket_state(client_id=ClientId("BINANCE"), endpoint="binance-futures-market-streams")` selects one transport. Import `ClientId` from `nautilus_trader.model`. - Each omitted filter matches all values of that field. - Supplied values match literally, including dots and wildcard characters. - Pass the **same filters** to `unsubscribe_socket_state` to remove that subscription. Omitting both removes only the unfiltered subscription. - Overlapping subscriptions each invoke the callback for a matching event. #### Socket event contents `SocketStateChanged` includes the trader ID, client ID, optional venue, stable endpoint label, transport state, event ID, and timestamps. The endpoint is a non-secret logical label, not a raw connection URL. `SocketState.DISCONNECTED` reports the loss of an active transport. :::note `SocketState.CONNECTED` reports **transport availability**, not authentication, subscription replay, or adapter readiness. ::: Delivery uses the typed in-process message bus and has no external wire representation. See [Socket transport state](live.md#socket-transport-state) for supported adapters and the precise connection edge semantics. ### Reconnect a socket endpoint Live actors and strategies can request recovery of one endpoint without restarting its data or execution client. Pass the `client_id` and the endpoint label reported by `SocketStateChanged`: ```rust tab="Rust" use nautilus_common::actor::DataActor; use nautilus_model::identifiers::ClientId; impl MyActor { fn recover_market_socket(&self) -> anyhow::Result<()> { self.reconnect_socket( ClientId::from("POLYMARKET"), "polymarket-market-streams", )?; Ok(()) } } ``` ```python tab="Python" from nautilus_trader.model import ClientId def recover_market_socket(self) -> None: self.reconnect_socket( client_id=ClientId("POLYMARKET"), endpoint="polymarket-market-streams", ) ``` #### Observe recovery :::note This API is **fire-and-observe**. A successful return means the command passed local validation and was queued. It does not acknowledge that the kernel accepted the request or that recovery completed. ::: Subscribe with `subscribe_socket_state` using the same `client_id` and `endpoint` filters, then inspect the `SocketStateChanged` events. An accepted request reports: 1. `SocketState.DISCONNECTED` as the transport enters reconnect mode. 1. `SocketState.CONNECTED` after transport recovery. #### Request failures - **Synchronous failures**: Invalid endpoint labels and unavailable or closed runner channels fail synchronously. Endpoint labels accept only ASCII letters, digits, `.`, `-`, and `_`; pass a logical label rather than a raw URL. - **Kernel rejections**: The kernel logs unknown clients, unsupported clients, unknown or ambiguous endpoints, duplicate requests, disconnecting transports, and closed transports. These rejections do not emit a socket state change or affect another endpoint. ## Data handling and callbacks The system dispatches request responses separately from subscribed updates. The operation determines which callback handles the data. ### Request responses and subscriptions The system distinguishes between two data flows: 1. **Request responses**: - Obtained through methods like `request_bars()`, `request_quotes()`, etc. - Processed through type-specific batch handlers such as `on_historical_bars()` and `on_historical_quotes()`. - Custom data uses `on_historical_data()` once per response. A scalar `CustomData` arrives as that object, while a batch arrives as one list, including an empty list. - Used for initial data loading and historical analysis. 2. **Subscribed data**: - Obtained through methods like `subscribe_bars()`, `subscribe_quotes()`, etc. - Processed through specific handlers like `on_bar()`, `on_quote()`, etc. - Used for ongoing event processing. ### Callback handlers Common data operations map to these handlers: | Operation | Category | Handler | Purpose | | ------------------------------- | ------------ | ------------------------------- | ------------------------------------------ | | `subscribe_data()` | Subscription | `on_data()` | Custom data updates. | | `subscribe_signal()` | Subscription | `on_signal()` | Signal updates. | | `subscribe_instrument()` | Subscription | `on_instrument()` | Instrument definition updates. | | `subscribe_instruments()` | Subscription | `on_instrument()` | Instrument definition updates for a venue. | | `subscribe_book_deltas()` | Subscription | `on_book_deltas()` | Order book deltas. | | `subscribe_book_depth10()` | Subscription | `on_book_depth()` | Order book depth snapshots. | | `subscribe_book_at_interval()` | Subscription | `on_book()` | Order book snapshots at intervals. | | `subscribe_quotes()` | Subscription | `on_quote()` | Quote updates. | | `subscribe_trades()` | Subscription | `on_trade()` | Trade updates. | | `subscribe_mark_prices()` | Subscription | `on_mark_price()` | Mark price updates. | | `subscribe_index_prices()` | Subscription | `on_index_price()` | Index price updates. | | `subscribe_bars()` | Subscription | `on_bar()` | Bar updates. | | `subscribe_funding_rates()` | Subscription | `on_funding_rate()` | Funding rate updates. | | `subscribe_instrument_status()` | Subscription | `on_instrument_status()` | Instrument status updates. | | `subscribe_instrument_close()` | Subscription | `on_instrument_close()` | Instrument close updates. | | `subscribe_option_greeks()` | Subscription | `on_option_greeks()` | Option Greek updates. | | `subscribe_option_chain()` | Subscription | `on_option_chain()` | Option chain slice snapshots. | | `request_data()` | Request | `on_historical_data()` | Historical custom data. | | `request_book_deltas()` | Request | `on_historical_book_deltas()` | Historical order book deltas. | | `request_book_depth()` | Request | `on_historical_book_depth()` | Historical order book depth. | | `request_book_snapshot()` | Request | `on_book()` | Order book snapshot. | | `request_instrument()` | Request | `on_instrument()` | Instrument definition. | | `request_instruments()` | Request | `on_instrument()` | Instrument definitions. | | `request_quotes()` | Request | `on_historical_quotes()` | Historical quotes. | | `request_trades()` | Request | `on_historical_trades()` | Historical trades. | | `request_bars()` | Request | `on_historical_bars()` | Historical bars. | | `request_funding_rates()` | Request | `on_historical_funding_rates()` | Historical funding rates. | ### Request and subscription example This example shows both request and subscription handling: ```python from collections.abc import Sequence from nautilus_trader.common import DataActor from nautilus_trader.config import DataActorConfig from nautilus_trader.model import Bar from nautilus_trader.model import BarType class MyActorConfig(DataActorConfig): def __init__(self, *, bar_type: BarType, **_kwargs) -> None: super().__init__() self.bar_type = bar_type class MyActor(DataActor): def __init__(self, config: MyActorConfig) -> None: super().__init__(config) def on_start(self) -> None: # Limit the historical response, which is handled by on_historical_bars() self.request_bars( bar_type=self.config.bar_type, limit=100, ) # Deliver subscribed updates to on_bar() self.subscribe_bars(self.config.bar_type) def on_historical_bars(self, bars: Sequence[Bar]) -> None: for bar in bars: self.log.info(f"Received historical bar: {bar}") def on_bar(self, bar: Bar) -> None: self.log.info(f"Received subscribed bar: {bar}") ``` Separate request and subscription handlers let an actor distinguish bootstrap data from ongoing updates. Use historical bars to initialize indicators or baseline state, and apply different validation or logging to response batches and individual subscribed updates. :::tip When debugging data flow issues, check that you're looking at the correct handler for your data source. If you're not seeing data in `on_bar()` but see log messages about receiving bars, check `on_historical_bars()` because the data might be coming from a request rather than a subscription. ::: ## Order event handling Data actors do not manage orders or define order event callbacks. Handle order events in a `Strategy` through its specific order callbacks or `on_order_event()`. Use custom data or signals to pass derived values to a data actor when another component needs them. See [Strategies: order management](strategies.md#order-management) for the callback list. ## Related guides - [Strategies](strategies.md): Strategies extend actors with order-management capabilities. - [Data](data/): Data types and subscriptions available to actors. - [Message Bus](message_bus.md): The messaging system actors use for communication. # Adapters Source: https://nautilustrader.io/docs/latest/concepts/adapters/ Adapters connect data providers and trading venues to NautilusTrader. They translate venue-specific protocols into the domain objects and events used by the data and execution engines. Official Python adapters are available from `nautilus_trader.adapters`; the [integration guides](../integrations/index.md) document their supported capabilities. An adapter typically comprises these components: ```mermaid flowchart LR subgraph Venue ["Trading Venue"] API[REST API] WS[WebSocket] end subgraph Adapter ["Adapter"] HTTP[HttpClient] WSC[WebSocketClient] IP[InstrumentProvider] DC[DataClient] EC[ExecutionClient] end subgraph Core ["Nautilus Core"] DE[DataEngine] EE[ExecutionEngine] end API <--> HTTP WS <--> WSC HTTP --> IP HTTP --> DC HTTP --> EC WSC --> DC WSC --> EC DC <--> DE EC <--> EE ``` | Component | Purpose | | -------------------- | --------------------------------------------------------- | | `HttpClient` | REST API communication. | | `WebSocketClient` | Real-time streaming connection. | | `InstrumentProvider` | Loads and parses instrument definitions from the venue. | | `DataClient` | Handles market data subscriptions and requests. | | `ExecutionClient` | Handles order submission, modification, and cancellation. | ## Configuration and routing Each adapter exposes configuration types and factories for the clients it supports. Configs select venue-specific settings such as the product, environment, credentials, and instrument loading policy. Factories construct the clients when a `LiveNode` is built. Actors and strategies then use the common Nautilus APIs rather than calling adapter transports directly. A node can register multiple data and execution clients. Pass `client_id` from an actor or strategy when a specific client must handle a request, subscription, or order. Without an explicit client, the data and execution engines use the venue and default routes configured by the node. ## Custom adapters You can develop an adapter as a separate Python package without rebuilding NautilusTrader. Custom adapters implement the same data and execution responsibilities as built-in adapters, and can run alongside them in one `LiveNode`. Strategies continue to use the standard request, subscription, and order APIs; the node routes those operations to the registered clients. A custom package can implement its clients in Python or delegate venue operations to its own Rust/PyO3 extension. Both use the [Python client interface](../developer_guide/python_adapters.md). An independent extension exchanges Python objects from the installed NautilusTrader wheel through PyO3 and the GIL. It does not require a shared Rust ABI or a second copy of the Nautilus model types. Choose a package version tested against your installed NautilusTrader release. Register the package's factories and configs with the node under distinct client names. Custom configs can retain venue-specific fields, and importable configs support loading factories from package paths. Set venue and default routes as you would for built-in clients, or select a client explicitly from a strategy. The [deterministic Python template](../../examples/live/_template/README.md) demonstrates data delivery, execution reconciliation, and an order fill without a venue connection. ### Cache and state ownership Custom clients receive a read-only cache view. They can inspect instruments, quotes, orders, accounts, and positions, but cannot mutate the core cache. Returned objects are snapshots: changing a snapshot does not change the state seen by strategies or engines. Adapters operate outside the synchronous core boundary. They submit typed data, responses, and execution events for the core to process, so an adapter's output call does not mean the corresponding cache update has already occurred. This keeps cache mutation and engine state transitions under core ownership. Cache access and output remain bound to the owning node's thread and lifetime; holding the GIL alone does not permit access from another thread or after disposal. ### Running and stopping Use `node.run()` when the node owns its event loop, or await `node.run_async()` inside an existing asyncio application. Both launch modes support custom Python and independent PyO3 clients. The node binds clients to the running loop before connection, then uses its normal startup, reconciliation, and shutdown sequence. Constructors do not start networking or background tasks. Commands and subscription changes execute in admission order for each custom client. Historical requests and reconciliation can progress separately. A slow command delays later commands for that client, and a full command queue rejects new work rather than silently dropping it. Await shutdown before closing a host event loop. Disconnection stops admission and asks adapter work to terminate; requesting cancellation does not prove that work has finished. Incomplete cleanup is reported. Client instances belong to one node run and must not be reused for another. Redis/PostgreSQL cache backing is unsupported with custom clients in either launch mode. The interface also requires migration of v1 Cython adapters rather than accepting their source unchanged. See the [migration details and remaining limitations](../developer_guide/python_adapters.md#migration-from-v1) for historical request completion, revised bars, custom publication/subscription, and networking API differences. For independent extensions, see [package construction and installed-wheel validation](../developer_guide/python_adapters.md#independent-rustpyo3-packages). ## Instrument providers Instrument providers load venue definitions and parse them into Nautilus `Instrument` objects. Each adapter owns this behavior. Its Python API may expose a standalone loader, a dedicated provider config, loading behavior through its client config, or a combination of these. An `InstrumentProvider` serves two use cases: - Standalone discovery of available instruments for research or backtesting - Runtime loading in a `sandbox` or `live` [environment context](architecture.md#environment-contexts) for actors and strategies ### Research and backtesting This example loads one Binance USD-M instrument through the public Python API: ```python import asyncio from nautilus_trader.adapters.binance import BinanceDataClientConfig from nautilus_trader.adapters.binance import BinanceEnvironment from nautilus_trader.adapters.binance import BinanceInstrumentProviderConfig from nautilus_trader.adapters.binance import BinanceProductType from nautilus_trader.adapters.binance import load_binance_instruments async def main() -> None: config = BinanceDataClientConfig( product_type=BinanceProductType.USD_M, environment=BinanceEnvironment.LIVE, instrument_provider=BinanceInstrumentProviderConfig( load_all=False, load_ids=["BTCUSDT-PERP.BINANCE"], ), ) instruments = await load_binance_instruments(config) for instrument in instruments: print(instrument.id) if __name__ == "__main__": asyncio.run(main()) ``` ### Live trading Each integration handles startup loading differently. For example, the Binance provider config can load the full catalog: ```python from nautilus_trader.adapters.binance import BinanceInstrumentProviderConfig BinanceInstrumentProviderConfig(load_all=True) ``` It can instead load only specified instruments: ```python BinanceInstrumentProviderConfig( load_all=False, load_ids=["BTCUSDT-PERP.BINANCE", "ETHUSDT-PERP.BINANCE"], ) ``` `load_ids` contains Nautilus instrument IDs, including the venue suffix, rather than raw venue symbols. Instrument-loading settings, defaults, and filters vary by integration. Check the relevant integration guide before copying a config between adapters. Subscriptions, order submission, and execution reconciliation do not load instruments by themselves. Configure the adapter to load each required instrument at startup, or request it explicitly and wait until it reaches the cache before using it. For how reconciliation treats a report whose instrument is not loaded, see [instrument availability](execution/reconciliation.md#instrument-availability). ## Data clients Data clients handle market data subscriptions and requests for a venue. They connect to venue APIs and normalize incoming data into Nautilus types. ### Requesting data Actors and strategies can request data using built-in methods. Data returns via callbacks: ```python from collections.abc import Sequence from typing import Any from nautilus_trader.model import Bar from nautilus_trader.model import BarType from nautilus_trader.model import InstrumentId from nautilus_trader.trading import Strategy class MyStrategy(Strategy): def on_start(self) -> None: # Request an instrument definition self.request_instrument(InstrumentId.from_str("BTCUSDT-PERP.BINANCE")) # Request historical bars self.request_bars(BarType.from_str("BTCUSDT-PERP.BINANCE-1-HOUR-LAST-EXTERNAL")) def on_instrument(self, instrument: Any) -> None: self.log.info(f"Received instrument: {instrument.id}") def on_historical_bars(self, bars: Sequence[Bar]) -> None: self.log.info(f"Received {len(bars)} historical bars") ``` ### Subscribing to data For real-time data, use subscription methods: ```python from nautilus_trader.model import Bar from nautilus_trader.model import BarType from nautilus_trader.model import InstrumentId from nautilus_trader.model import TradeTick from nautilus_trader.trading import Strategy class MyStrategy(Strategy): def on_start(self) -> None: # Assumes the instrument has already been loaded into the cache self.subscribe_trades(InstrumentId.from_str("BTCUSDT-PERP.BINANCE")) self.subscribe_bars(BarType.from_str("BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL")) def on_trade(self, trade: TradeTick) -> None: self.log.info(f"Trade: {trade}") def on_bar(self, bar: Bar) -> None: self.log.info(f"Bar: {bar}") ``` :::tip See the [Actors](actors.md) documentation for a complete reference of available request and subscription methods with their corresponding callbacks. ::: ## Execution clients Execution clients handle order management for a venue. They translate Nautilus order commands into venue-specific API calls and process execution reports back into Nautilus events. Responsibilities: - Submit, modify, and cancel orders. - Process fills and execution reports. - Reconcile order state with the venue. - Handle account and position updates. Execution clients can declare the lower time limit applied to historical reconciliation and whether the required order, fill, and position sources completed. When an adapter supplies this contract, the engine can recover authoritative order state without applying historical position or portfolio economics that the available evidence cannot support. See [Bounded history safety](execution/reconciliation.md#bounded-history-safety). ### Reduce-only execution contract An execution client **must never silently discard** `reduce_only=true`. It must either send a documented venue instruction that enforces the same intent or reject the order before transport. The venue can still reject an encoded instruction when the product does not support it, the order would not reduce an open position, or another venue rule makes the combination invalid. An equivalent enforcing instruction can replace the literal venue flag. For example, a Binance Futures close-all conditional order retains `reduce_only=true` in the Nautilus order, but the adapter sends `closePosition=true` without Binance's incompatible `reduceOnly` field. See [Binance Futures close-position orders](../integrations/binance.md#close-position). Backtest and sandbox matching engines enforce reduce-only when `use_reduce_only` is enabled and reject reduce-only orders when it is disabled. Custom execution clients and external routes follow the same send-or-reject contract. Order commands and venue results are asynchronous. `OrderSubmitted` means that the adapter has started the submission path, not that the venue has accepted the order. A transport failure can leave the outcome unknown, so adapters use stream updates, queries, or reconciliation rather than assuming a rejection. For a new order, the `ExecutionEngine` uses an explicitly selected client, venue routing, or the configured default. Later commands for an existing order return to its originating client when known. See the [Execution](execution/) guide for order management from a strategy perspective. :::tip For building a custom adapter, see the [Adapter Developer Guide](../developer_guide/adapters.md). ::: ## Related guides - [Live trading](live.md): Configure and run live trading with adapters. - [Execution](execution/): Order execution through adapters. - [Data](data/): Market data provided by adapters. # Architecture Source: https://nautilustrader.io/docs/latest/concepts/architecture/ This page describes NautilusTrader's components, runtime boundaries, and data flows. The [design principles and policies](../developer_guide/design_principles.md) guide this structure. :::note For this guide, the **Nautilus system boundary** means the runtime of one Nautilus node instance. ::: ## Architectural style NautilusTrader uses these architectural techniques and design patterns: - [Domain-driven design (DDD)](https://en.wikipedia.org/wiki/Domain-driven_design) - [Event-driven architecture](https://en.wikipedia.org/wiki/Event-driven_programming) - [Messaging patterns](https://en.wikipedia.org/wiki/Messaging_pattern) (publish/subscribe, request/response, and point-to-point) - [Ports and adapters](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)) - [Crash-only design](#crash-only-design) The [design principles and policies](../developer_guide/design_principles.md) state the priorities and contracts these techniques support. ## System architecture NautilusTrader provides both a framework for composing trading systems and default implementations for several [environment contexts](#environment-contexts). ```mermaid flowchart LR data_clients[Data clients] exec_clients[Execution clients] cache_backing[(Optional cache backing)] bus_backing[(Optional message bus backing)] subgraph kernel[NautilusKernel] data[DataEngine] risk[RiskEngine] execution[ExecutionEngine] portfolio[Portfolio] trader[Trader: actors, strategies, algorithms] bus[MessageBus] cache[(Cache)] end data_clients -->|market data| data data -->|store| cache data -->|publish| bus bus -->|callbacks| trader trader -->|strategy portfolio access| portfolio trader -->|trading commands| risk risk -->|validated commands| execution execution <--> exec_clients execution -->|execution state| cache execution -->|events| bus bus -->|order and position events| risk risk -->|read state| cache risk -->|read portfolio state| portfolio bus -->|account, order, position, and price events| portfolio portfolio <-->|state| cache cache <--> cache_backing bus <--> bus_backing ``` The kernel owns the shared trading core; adapters exchange market data and execution messages through the engine boundaries. ### Core components #### `NautilusKernel` The central orchestration component: - Initializes and manages the shared core components. - Configures the messaging infrastructure. - Selects environment-specific clocks and behavior. - Coordinates shared resources and lifecycle management. - Provides one lifecycle boundary for system operations. #### `MessageBus` `MessageBus` centrally routes inter-component communication: - **Publish/subscribe**: Broadcasts events and data to multiple consumers. - **Request/response**: Correlates requests with their responses. - **Command/event messaging**: Routes actions and state changes through typed endpoints. - **Optional external backing**: Sends selected publications and, for live nodes, receives configured external streams through a backing such as Redis. These streams provide live transport; durable state recovery belongs to the cache or event store. #### `Cache` `Cache` keeps trading state in memory: - Stores instruments, accounts, orders, positions, and more. - Provides indexed reads for trading components. - Optionally persists configured state through a cache database backing. #### `DataEngine` Processes and routes market data throughout the system: - Handles quotes, trades, bars, order books, custom data, and other supported types. - Manages subscriptions and correlated request/response flows through data clients. - Keeps each client subscription active until its final owner releases it, retaining the original client route and parameters for the final unsubscribe. - Routes resulting data to consumers according to their subscriptions and requests. - Manages data flow from external sources to internal components. #### `ExecutionEngine` Manages order lifecycle and execution: - Routes trading commands to the appropriate execution clients. - Tracks order and position states. - Coordinates with risk management systems. - Handles execution reports and fills from venues. - Applies reconciliation events and reconciles individual venue reports. For live execution, `ExecutionManager` tracks reconciliation state and coordinates individual reconciliation operations. `LiveNode` owns recurring checks, deadlines, and cancellation. See the [reconciliation component diagram](execution/reconciliation.md#component-responsibilities) for their ownership and dependencies. #### `RiskEngine` Provides risk management: - Validates order fields, balances, quantities, notionals, reduce-only behavior, and trading state. - Applies configurable submission and modification rate limits. - Monitors order and position events used by its controls. #### `Portfolio` Maintains derived account and position state: - Tracks balances, net positions, margin, realized and unrealized profit and loss (PnL), and exposure. - Updates state and valuations from account, order, position, quote, bar, and mark-price events. #### `Trader` Coordinates user trading components: - Registers actors, strategies, and execution algorithms. - Manages their lifecycle, clocks, and event subscriptions. ### Environment contexts An environment context defines the data source and execution setting for a node: - `Backtest`: Historical data with simulated execution. - `Sandbox`: Real-time data with simulated execution. - `Live`: Real-time data with live venue connections, including paper or real accounts. ### Common core Backtest, sandbox, and live systems share the `NautilusKernel` struct from the `nautilus-system` crate. The kernel owns the common cache, portfolio, engines, trader, clock, and messaging infrastructure. The *ports and adapters* architectural style enables modular components to be integrated into the core through explicit client, backing-store, and component interfaces, including custom implementations. ### Data and execution flow patterns #### Data flow: life of a quote tick The following trace shows the path a `QuoteTick` takes from the network to a strategy. Trades and bars follow the same cache-then-publish path with different handler names. Order book deltas and depth snapshots take a different route (see the note below the steps). ```mermaid sequenceDiagram participant Adapter as DataClient adapter participant Channel as MPSC channel participant DE as DataEngine participant Cache as Cache participant MB as MessageBus participant Strategy as Strategy Adapter->>Channel: DataEvent::Data(Data::Quote(quote)) Channel->>DE: process_data(Data::Quote) DE->>DE: handle_quote(quote) DE->>Cache: add_quote(quote) DE->>MB: publish_quote(topic, quote) MB->>Strategy: on_quote(quote) ``` 1. **Adapter receives raw data.** A venue-specific `DataClient`, such as Binance or Bybit, receives a WebSocket message, parses it, and constructs a `QuoteTick`. 1. **Adapter sends a data event.** The adapter sends `DataEvent::Data(Data::Quote(quote))` through an MPSC channel. In live mode this is an async unbounded channel; in backtests the engine feeds data directly. 1. **`DataEngine` processes the event.** The channel receiver routes the event to `DataEngine::process_data`, which dispatches to `handle_quote`. 1. **`Cache` stores the quote.** `handle_quote` calls `cache.add_quote(quote)`. When the insertion succeeds, components can read it through `self.cache.quote(instrument_id)`. 1. **`MessageBus` publishes.** The engine publishes the quote on a topic derived from the instrument ID, such as `data.quotes.BINANCE.BTCUSDT-PERP`. The `MessageBus` finds all handlers subscribed to that topic. 1. **Strategy handler runs.** Each subscribed strategy's `on_quote(quote)` runs on the single-threaded core. `self.cache.quote(instrument_id)` returns the same quote only if insertion succeeded and no later quote has replaced it before the read. :::note For quotes, trades, and bars, the engine attempts cache insertion before publication. A synchronous persistence or enqueue error prevents the in-memory insertion, but the engine logs the error and still publishes the value. Built-in database backings perform the actual write asynchronously, so a later database error does not roll back the cache insertion. A published quote can therefore be absent from the cache after a synchronous insertion failure. A callback can also observe newer cache state than the event it handles, including during synchronous reentry. Delivery order does not provide an event-time snapshot, and the [queued dispatch requirements](../developer_guide/callback_dispatch.md#maintenance-and-observable-state) do not add one. Order book deltas and depth snapshots are published directly, while `BookUpdater` subscriptions maintain book state separately. ::: #### Execution flow: life of an order This simplified trace shows direct order submission with successful venue acceptance followed by a fill. It omits execution algorithms, emulation, and ambiguous transport outcomes; see [Execution](execution/index.md) and [command outcomes](execution/policies.md#command-outcomes) for those paths. Adapters translate venue messages into the execution events shown below: ```mermaid sequenceDiagram participant Strategy as Strategy participant RE as RiskEngine participant EE as ExecutionEngine participant EC as ExecutionClient participant Venue as Venue participant MB as MessageBus Strategy->>RE: submit_order(command) RE->>RE: pre-trade risk checks RE->>EE: route command EE->>EC: submit_order EC->>Venue: place order (REST/WS) Venue-->>EC: OrderAccepted EC->>EE: OrderAccepted event EE->>MB: publish OrderAccepted MB->>Strategy: on_order_accepted(event) Venue-->>EC: OrderFilled EC->>EE: OrderFilled event EE->>MB: publish OrderFilled MB->>Strategy: on_order_filled(event) ``` 1. **Strategy creates a command.** The strategy calls `self.submit_order(order)`. 1. **`RiskEngine` validates.** Configured order, balance, quantity, notional, trading-state, and rate checks run. If a check fails, the strategy receives `OrderDenied`, and the order never reaches the venue. 1. **`ExecutionEngine` routes.** The command is routed to the `ExecutionClient` for the target venue. 1. **`ExecutionClient` submits.** The adapter sends the order to the venue over REST or WebSocket. 1. **Events flow back.** The venue responds with acknowledgments and fills. Each event (`Accepted`, `Filled`, `Canceled`, `Rejected`, or `Expired`) flows through the `ExecutionEngine`, which updates order state in the `Cache` and delivers the event to the strategy's handler. Fill events also trigger position and portfolio updates. #### Component state management Types that implement the `Component` trait use a finite state machine. `ComponentState` defines stable and transitional states, while `ComponentTrigger` constrains valid transitions: ```mermaid stateDiagram-v2 [*] --> PRE_INITIALIZED PRE_INITIALIZED --> READY : initialize() READY --> STARTING : start() STARTING --> RUNNING STARTING --> STOPPING : stop() STARTING --> FAULTING : fault() RUNNING --> STOPPING : stop() STOPPING --> STOPPED STOPPING --> DISPOSING : dispose() STOPPING --> FAULTING : fault() STOPPED --> RESETTING : reset() RESETTING --> READY STOPPED --> RESUMING : resume() DEGRADED --> RESUMING : resume() RESUMING --> RUNNING RESUMING --> STOPPING : stop() RESUMING --> FAULTING : fault() RUNNING --> DEGRADING : degrade() DEGRADING --> DEGRADED DEGRADED --> STOPPING : stop() DEGRADED --> FAULTING : fault() RUNNING --> FAULTING : fault() STOPPED --> FAULTING : fault() FAULTING --> DISPOSING : dispose() FAULTING --> FAULTED READY --> RESETTING : reset() READY --> DISPOSING : dispose() STOPPED --> DISPOSING : dispose() DISPOSING --> DISPOSED DISPOSING --> FAULTING : on_dispose() error DISPOSED --> [*] ``` **Stable states:** - **PRE_INITIALIZED**: The component exists but is not ready to fulfill its specification. - **READY**: The component is configured and can start. - **RUNNING**: The component operates normally and can fulfill its specification. - **STOPPED**: The component has stopped successfully. - **DEGRADED**: The component may not meet its full specification. - **FAULTED**: The component has shut down because of a detected fault. - **DISPOSED**: The component has shut down and released its resources. **Transitional states:** - **STARTING**: The component is executing its `start` actions. - **STOPPING**: The component is executing its `stop` actions. - **RESUMING**: The component is executing its `resume` actions after a stop or degradation. - **RESETTING**: The component is executing its `reset` actions. - **DISPOSING**: The component is executing its `dispose` actions. - **DEGRADING**: The component is executing its `degrade` actions. - **FAULTING**: The component is executing its `fault` actions. Transitional states cover the corresponding lifecycle callback and should remain brief. If a callback returns an error, the transition halts in its transitional state. `dispose()` is the exception: a failing `on_dispose` moves the component to FAULTED so it can still be retired. A failed `on_stop` or `on_fault` leaves the component in its transitional state, from which trader retirement can still invoke `dispose()` and finish cleanup. After a successful reset, the component releases its retained data subscriptions before returning to READY. Its next start acquires fresh subscriptions against the reset data engine. If `on_reset` fails, the component remains in RESETTING with its subscriptions intact. During normal retirement, the trader runs `on_dispose`, releases the component's retained data subscriptions, and then removes its registry and bookkeeping entries. If `on_dispose` fails, the component remains registered with its subscriptions intact. Retiring the resulting FAULTED component releases those subscriptions without invoking the failed disposal hook again. Each component release removes its message bus handlers and decrements ownership on the routed data client. The data client sends an upstream unsubscribe only after the final owner releases the same physical subscription. Engine-managed resources, including book snapshots, synthetic feeds, spread quotes, internal bars, and option chains, follow the same final-owner rule. #### Actor vs Component traits The Rust implementation separates targeted message dispatch from lifecycle management: ```mermaid classDiagram class Actor { <> +id() Ustr +handle(message) } class Component { <> +component_id() ComponentId +state() ComponentState +register() +initialize() +start() +stop() +resume() +reset() +dispose() +degrade() +fault() } class ActorRegistry { +insert(actor) +get(id) shared actor handle } class ComponentRegistry { +insert(component) +get(id) shared component handle } Actor <|.. Throttler : implements Actor <|.. Strategy : implements Component <|.. Strategy : implements Component <|.. Trader : implements ActorRegistry --> Actor : manages ComponentRegistry --> Component : manages class Throttler { Actor only } class Strategy { Actor + Component } class Trader { Component only } ``` **`Actor` trait: message dispatch** - Provides the `handle` method for receiving messages dispatched through the actor registry. - Supports lookup by actor ID; typed unchecked accessors check the concrete actor type at runtime and return an `ActorRef` guard. - Used by types that receive targeted messages, such as strategies and throttlers. **`Component` trait: lifecycle management** - Manages state transitions such as `start`, `stop`, `resume`, `reset`, and `dispose`. - Registers a component with the trader ID, clock, and cache. - Tracks component state via the finite state machine described above. - Used by actors, strategies, execution algorithms, and the `Trader` when they need managed lifecycle behavior. The data, risk, and execution engines expose their own lifecycle methods but do not implement this trait. :::note Message bus access does not depend on the `Actor` trait. Code running on the node thread can use the thread-local `MessageBus` APIs, while `Actor` specifically enables registry-based dispatch to an actor ID. ::: This separation allows: - **Actor only**: Lightweight message handlers without lifecycle, such as `Throttler`. - **Component only**: Lifecycle-managed types without targeted actor dispatch, such as `Trader`. - **Both traits**: Data actors, including strategies and execution algorithms, that need lifecycle management and targeted dispatch. :::warning Separate thread-local registries support these access patterns. Both registry `get` methods return shared `Rc>` handles. Component lifecycle wrapper functions use a private borrow guard to reject overlapping lifecycle access; that protection does not apply to arbitrary access through a raw registry handle. Typed actor accessors return `ActorRef` guards, which do not prevent two simultaneous guards for the same actor. Creating overlapping mutable references is undefined behavior. Obtain, use, and drop an `ActorRef` within one synchronous scope. Never store one or hold it across an `.await` point. Same-actor re-entrant lookup is a constraint of the current dispatch model, not a safe aliasing guarantee. ::: For queued dispatch, releasing an actor guard alone does not establish a safe delivery boundary: enclosing mutable runtime borrows must also end. Subscriber admission order alone does not preserve publication order during nested fan-out. Pending deliveries need registration and lifecycle identity to enforce the [dispatch requirements](../developer_guide/callback_dispatch.md). ### Messaging The `MessageBus` passes data, commands, and events between components without requiring direct component references. #### Threading model Within a node, the core consumes and dispatches messages on a **single thread**. This includes: - The `MessageBus` and actor callback dispatch. - Strategy logic and order management. - Risk engine checks and execution coordination. - Cache reads and writes. Serial processing coordinates state changes within the node. Synchronous reentry still has the constraints described above; the [queued dispatch requirements](../developer_guide/callback_dispatch.md) define publication ordering across nested callbacks. Live inputs and latency can cause behavioral differences from backtests. Components consume messages synchronously in a pattern *similar* to the [actor model](https://en.wikipedia.org/wiki/Actor_model). :::note The [LMAX architecture](https://martinfowler.com/articles/lmax.html) is a related example of single-threaded transaction processing. ::: Background services use separate threads or the process-wide, multi-threaded Tokio runtime. The runtime's worker count is configurable: - **Network and adapters**: WebSocket connections, REST clients, and data feeds run as async tasks. - **Logging**: A worker receives log events outside the synchronous core. - **Persistence**: Redis and PostgreSQL cache backings queue writes to async tasks. DataFusion runs catalog query futures on a Tokio runtime. Async producers send data and execution events through channels. The node runner receives them and uses the thread-local `MessageBus` to dispatch them to engine endpoints on the core thread. Each thread has its own bus instance; channels bridge work from other threads or tasks. ## Crash-only design NautilusTrader draws on [crash-only design](https://en.wikipedia.org/wiki/Crash-only_software) when handling unrecoverable faults. Repository release builds abort on panic, allowing an external supervisor to restart the process instead of letting it continue with potentially invalid state. Recovery behavior: - **Startup recovery**: Configured cache and event-store recovery run through normal startup rather than through a separate crash-only entry point. Ordinary startup and focused recovery tests exercise the same initialization flow. - **External state**: Configured backing stores preserve selected state across process restarts, reducing recovery work and the risk of losing state. Durability depends on the backing store and its settings. - **Supervisor-managed restart**: An external process supervisor owns restart policy after an unrecoverable failure. Aborting skips graceful cleanup inside the failed process; actual downtime depends on the supervisor, configured state, and backing store. - **Prompt recovery**: The design aims to minimize downtime by using normal startup recovery after a supervisor restarts the process. Recovery time depends on the state to restore and its backing store. - **Execution recovery**: Venue commands are not generally safe to retry blindly; execution reconciliation handles that boundary. :::note Normal operation still uses graceful shutdown flows such as `stop` and `dispose`. They tear down clients and, when configured, save state and flush writers. Crash-only behavior applies to unrecoverable faults, where continuing normal cleanup may be unsafe. ::: This design complements the [data-integrity policy](../developer_guide/design_principles.md#data-integrity-and-failure): a panic caused by an unrecoverable invariant violation immediately terminates a process built with the repository release profile. **References:** - [Crash-Only Software](https://www.usenix.org/conference/hotos-ix/crash-only-software): Candea and Fox, HotOS 2003. - [Microreboot: A technique for cheap recovery](https://www.usenix.org/events/osdi04/tech/candea.html): Candea et al., OSDI 2004. - [The properties of crash-only software](https://brooker.co.za/blog/2012/01/22/crash-only.html): Marc Brooker. - [Crash-only software: More than meets the eye](https://lwn.net/Articles/191059/): LWN.net. - [Recovery-Oriented Computing (ROC) Project](http://roc.cs.berkeley.edu/): UC Berkeley and Stanford. ## Framework organization The Rust workspace groups related behavior into crates under `crates/`. The public package under `python/nautilus_trader/` provides Python facades and supporting utilities over the Rust implementation. ### Core and domain - `core`: Low-level time, string, serialization, and runtime primitives. - `model`: Trading domain types, including instruments, accounts, orders, positions, and market data. - `common`: Shared runtime services, including the cache, message bus, clocks, actors, components, and logging. - `serialization`: Schema and encoding support for model and event types. ### Trading and analysis - `analysis`: Trading performance statistics and analysis. - `indicators`: Technical indicators. - `data`: Market-data engines, aggregation, and data tooling. - `execution`: Order execution, emulation, and reconciliation primitives. - `portfolio`: Portfolio accounting and state. - `risk`: Pre-trade controls, position sizing, and trading state. - `trading`: Strategies and execution algorithms. ### Infrastructure and runtimes - `network` and `cryptography`: Networking clients, transport support, signing, and cryptographic providers. - `infrastructure`, `persistence`, and `event_store`: Database backings, data catalogs, object storage, and event-store integration. - `system`: The kernel shared by backtest, sandbox, and live [environment contexts](#environment-contexts). - `backtest` and `live`: Environment-specific engines and nodes. - `adapters/*`: Venue, broker, data, blockchain, and sandbox integrations. - `pyo3`: The Python extension aggregator. - `plugin`, `cli`, and `testkit`: Plugin interfaces, command-line tools, and test support. ## Code structure The `crates/` directory contains the Rust implementation. PyO3 collects its Python bindings into the `nautilus_trader._libnautilus` extension module, and `python/nautilus_trader/` exposes the public Python facades. The `nautilus-core` and `nautilus-model` crates retain an optional C FFI for native consumers. Other workspace crates use Rust APIs or PyO3 bindings. ### Dependency flow ```mermaid flowchart TB subgraph trader["python/nautilus_trader
Python"] end subgraph bindings["crates/pyo3
PyO3"] end subgraph core["crates
Rust"] end trader --> bindings bindings --> core ``` ### Rust crates Rust crate manifests declare workspace dependencies and optional feature flags. Features enable optional functionality without adding it to minimal builds. Selected direct workspace dependencies are shown below; arrows point to dependencies. The diagram omits edges that do not clarify the overall direction. ```mermaid flowchart BT subgraph Core["Core and domain"] core model common serialization end subgraph Trading trading data execution portfolio risk end subgraph Infrastructure network cryptography persistence end subgraph Runtime system live backtest end adapters pyo3 model --> core common --> core common --> model system --> common trading --> common serialization --> model network --> cryptography persistence --> serialization data --> common execution --> common portfolio --> common risk --> portfolio live --> system live --> trading backtest --> system backtest --> persistence adapters --> live adapters --> network pyo3 --> adapters ``` **Feature flags:** | Feature | Main crates | Effect | | ----------- | ----------------------------------------- | ---------------------------------------------------------------- | | `streaming` | `data`, `system`, `live`, `backtest` | Adds persistence support for catalog streaming. | | `cloud` | `persistence` | Adds AWS, Azure, GCP, and HTTP object-store backends. | | `python` | Python-facing crates | Adds PyO3 bindings and the transitive features each crate needs. | | `defi` | Domain, data, runtime, and binding crates | Adds DeFi and blockchain types and runtime paths. | :::note Source builds require Rust. Prebuilt Python wheels do not require a Rust toolchain at runtime. ::: ### Type safety The Rust codebase relies on the compiler's guarantees for safe code. Each `unsafe` block explicitly opts out of those guarantees, so memory and type safety depend on its documented invariants. See the Rust section of the [Developer Guide](../developer_guide/rust.md). PyO3 validates bound arguments and converts Rust errors into Python exceptions: :::info Passing an incompatible Python value to a typed PyO3 parameter raises a Python exception before the Rust method body runs. ::: ### Errors and exceptions API documentation describes expected errors from NautilusTrader and the conditions that produce them. :::warning Python's standard library and third-party dependencies can also raise exceptions outside those documented contracts. ::: ### Processes and threads :::warning[One node per process] Running multiple `LiveNode` or `BacktestNode` instances **concurrently** in the same process is not supported because their runtime state is not isolated: - **Logger mode and timestamps**: The logging subsystem uses global state; backtests switch the logging clock between static and real-time modes. - **Thread-local runtime state**: A node installs its message bus, actor and component registries, and channel senders for the thread that drives it. - **Process-wide runtime state**: The Tokio runtime and logging worker are shared by the process. Sequential execution of multiple nodes is supported when each node is disposed before the next one starts. Focused tests exercise sequential node construction and cache-backed state recovery across disposed nodes. For production deployments, add multiple strategies to one `LiveNode` within a process. For parallel execution or workload isolation, run each node in its own separate process. ::: ### Memory allocation The event-driven core allocates and frees objects during message dispatch, order event handling, and order book maintenance. Allocator choice can affect throughput and resident memory; the effect depends on the workload, platform, and build configuration. The `nautilus` CLI and Python wheels use [mimalloc](https://crates.io/crates/mimalloc) for Rust allocations. Compare allocators on representative workloads before choosing one for a custom binary. Record the source revision, build settings, environment, measurement method, throughput, and resident memory using the [benchmarking guide](../developer_guide/benchmarking.md). A Rust binary links exactly one global allocator, and libraries do not impose one, so the NautilusTrader crates remain allocator-neutral. When building directly against the crates, opt in from your own binary (see the [Rust guide](rust.md#memory-allocator)). ## Related guides - [Design principles](../developer_guide/design_principles.md): Principles, policies, and trade-offs. - [Identifier storage](../developer_guide/rust.md#identifier-storage): Identifier lifetimes and memory costs. - [Behavioral models](behavioral_models.md): Model representation and dispatch. - [Overview](overview.md): High-level introduction to NautilusTrader. - [Python](python.md): Python ownership, runtime, and public API boundaries. - [Rust](rust.md): Native Rust APIs and runtime use. - [Message Bus](message_bus.md): Core messaging infrastructure. # Behavioral Models Source: https://nautilustrader.io/docs/latest/concepts/behavioral_models/ This page describes the pluggable models that change or extend NautilusTrader behavior. A **behavioral model** supplies the rules for a specific calculation or decision made by the system. For example, a fill model determines simulated fill eligibility and liquidity, while a fee model calculates the commission on a fill. The engine calls the configured model through a defined interface, so users can change these rules without modifying the engine or their strategy. Models are supplied as objects in configuration or through runtime APIs. Users can select a built-in implementation, configure its parameters, or supply a custom implementation where the model family and API support it. Here, pluggable means replacing behavior through these interfaces; [runtime loading of native libraries](#native-extension-boundary) is a separate capability. ## Model families | Family | Controls | Built-in examples | | ------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------- | | Fill | Simulated limit-fill eligibility, slippage, and optional synthetic liquidity. | `DefaultFillModel`, `BestPriceFillModel`, `TwoTierFillModel`. | | Fee | Commission calculated from an order, fill, instrument, and optional pricing context. | `MakerTakerFeeModel`, `FixedFeeModel`, `PerContractFeeModel`. | | Latency | Simulated delays for order submission, modification, and cancellation. | `StaticLatencyModel`. | | Margin | Initial order margin and maintenance position margin. | `StandardMarginModel`, `LeveragedMarginModel`. | Fill models range from using the recorded book to supplying synthetic liquidity with tiered, partial-fill, size-aware, competition-aware, volume-sensitive, or market-hours behavior. See [fill models](backtesting/fill-models.md) for the complete built-in list and the effect of book type on fill simulation. Fee models also include `ProbabilityPriceFeeModel`, `CappedOptionFeeModel`, and `TieredNotionalOptionFeeModel` for probability-priced instruments and option fee schedules. `StaticLatencyModel` adds a base delay to separately configured insert, update, and cancel delays. The [margin models](backtesting/accounts-and-margin.md#margin-models) select whether instrument margin requirements are reduced by account leverage. Fill and latency models control simulated execution; they do not determine whether or when a live venue fills an order. Model behavior applies at the engine or account boundary that consumes it. Changing a model does not replace order-state validation, execution routing, or the rest of the runtime. ## Simulation modules [Simulation modules](backtesting/simulation-modules.md) provide a related extension point for behavior across the simulated exchange lifecycle. Instead of answering a fill, fee, latency, or margin calculation, a module processes exchange state and returns account adjustments. Built-in FX rollover and CFD swap modules use this interface. Custom modules can extend simulation behavior through the same lifecycle and acknowledgement contract. ## Supplying implementations Rust callers can implement the model family's trait and pass the implementation through its runtime handle. Python support depends on **both the family and the configuration API**: | Family | `BacktestVenueConfig` from Python | `BacktestEngine.add_venue()` from Python | | ------- | ---------------------------------------------------- | ---------------------------------------------------- | | Fill | Built-in model objects. | Built-in models or custom Python fill-model objects. | | Fee | Built-in models or custom Python commission objects. | Built-in models or custom Python commission objects. | | Latency | `StaticLatencyModel`. | `StaticLatencyModel`. | | Margin | `StandardMarginModel` or `LeveragedMarginModel`. | `StandardMarginModel` or `LeveragedMarginModel`. | A custom Python fill model implements the [fill-model protocol](backtesting/fill-models.md#configuration). A custom Python fee model supplies `get_commission`; it can also provide `get_commission_with_context` when its calculation needs the underlying price. The engine invokes these methods when it needs the corresponding decision or calculation. ## Model family structure Behavioral model families use a common representation across simulation and execution: - A Rust `Model` trait defines the behavioral contract. - Concrete Rust types implement the built-in models. - A `ModelAny` enum lists the core built-ins and any language bridges that require enum storage, then implements the trait through explicit enum dispatch. - A `ModelHandle` stores a shared trait object where runtime components accept linked Rust implementations beyond the enum variants. Supported concrete built-ins are exposed as PyO3 classes. Their [type stub annotations](../developer_guide/rust.md#type-stub-annotations) feed the [generated Python artifacts](../developer_guide/rust.md#generated-python-artifacts). Backtest configuration accepts these concrete model objects directly rather than using separate model configuration and factory wrappers. Adapter-specific models live in their adapter crate when a core enum variant would create a reverse dependency. Low-level Rust code passes these models through the corresponding handle. Python exposure uses an explicit bridge for the model family, either as an enum variant or as a trait implementation passed through the handle, depending on the storage boundary. Latency and margin configuration accept built-in models only from Python. ## Dispatch boundary | Form | Accepted implementations | Dispatch | Role | | ------------------------ | -------------------------------------- | ------------ | --------------------------------------------- | | Concrete type or generic | One concrete implementation | Static | Model internals and specialized callers. | | `ModelAny` | Declared built-ins and bridge variants | Enum match | Built-in and bridge configuration or storage. | | `ModelHandle` | Any accepted Rust trait implementation | Trait object | Shared runtime storage and custom types. | `ModelAny` uses enum dispatch. `ModelHandle` uses dynamic dispatch through a trait object, so a built-in converted from the enum into a handle crosses a vtable before its enum match. Built-in-only storage remains typed as `ModelAny` where avoiding trait-object dispatch matters. The handle has no separate built-in fast path; the simpler single representation remains because no measured performance case justifies the additional variant and dispatch complexity. ## Native extension boundary The open-source [plug-in crate](../developer_guide/plugins.md) defines an artifact ABI, but model registration and the loading host are not part of this repository. The open-source distribution does not provide runtime native model plugins. Native models are composed at compile time and passed through the corresponding enum or handle. [Simulation modules](backtesting/simulation-modules.md) use these representations at backtest configuration and runtime boundaries. # Cache Source: https://nautilustrader.io/docs/latest/concepts/cache/ The `Cache` is the central in-memory store for trading state and recent market data. Actors and strategies use it to read data maintained by the data and execution engines or to share raw bytes under application-defined keys. The cache: - Stores current order books and bounded histories of quotes, trades, bars, and other market data. - Tracks orders, positions, accounts, instruments, and currencies until they are purged or reset. - Shares caller-serialized data between components and persists it when a backing database is configured. ## How caching works The engines add built-in data to the `Cache` as events flow through the system. Live adapters feed events to the engine asynchronously, so the cache changes **when the engine processes an event**, not when the adapter first receives it. For quotes, trades, and bars, the `DataEngine` attempts to write to the `Cache` before publishing to subscribers. After a successful write, the latest value is available by the time the strategy handler runs. Order book deltas and depth snapshots are published directly; `BookUpdater` subscriptions maintain current book state separately: ```mermaid flowchart LR data[Data] engine[DataEngine] cache[Cache] callback["Strategy callback:
on_quote(...)"] data --> engine --> cache --> callback ``` For the full step-by-step trace, see [Data flow: life of a quote tick](architecture.md#data-flow-life-of-a-quote-tick). ### Basic example Within a strategy, access the shared `Cache` through `self.cache`: ```python def on_bar(self, bar: Bar) -> None: # Read recent bars from the cache. last_bar = self.cache.bar(self.bar_type, index=0) # Same bar after a successful cache write. previous_bar = self.cache.bar(self.bar_type, index=1) third_last_bar = self.cache.bar(self.bar_type, index=2) # Read current position state. if self.last_position_opened_id is not None: position = self.cache.position(self.last_position_opened_id) if position is not None and position.is_open: open_quantity = position.quantity # Read open orders for the instrument. open_orders = self.cache.orders_open(instrument_id=self.instrument_id) ``` ## Configuration Use the `CacheConfig` class to configure the `Cache` behavior and capacity. Pass it to a `BacktestEngine` or `LiveNode`, depending on the [environment context](architecture.md#environment-contexts). The same capacity settings apply in both environments: ```python from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.config import CacheConfig from nautilus_trader.config import LiveNodeConfig # For backtesting engine_config = BacktestEngineConfig( cache=CacheConfig( tick_capacity=10_000, # Store last 10,000 ticks per instrument bar_capacity=5_000, # Store last 5,000 bars per bar type ), ) # For live trading node_config = LiveNodeConfig( cache=CacheConfig( tick_capacity=10_000, bar_capacity=5_000, ), ) ``` :::tip By default, the `Cache` keeps up to 10,000 values in each per-instrument tick sequence and 10,000 bars for each bar type. These are separate limits, not combined totals. Set each capacity to a value in `[1, 1_000_000]`. Increase them when a strategy needs a longer in-memory lookback and the additional memory use is acceptable. ::: ### Configuration options The `CacheConfig` type supports these parameters: ```rust use nautilus_common::{cache::CacheConfig, enums::SerializationEncoding}; let config = CacheConfig { encoding: SerializationEncoding::MsgPack, timestamps_as_iso8601: false, buffer_interval_ms: None, bulk_read_batch_size: None, use_trader_prefix: true, use_instance_id: false, flush_on_start: false, drop_instruments_on_reset: true, tick_capacity: 10_000, bar_capacity: 10_000, persist_account_events: true, save_market_data: false, }; ``` :::note Each bar type maintains its own capacity. For example, if you use both 1-minute and 5-minute bars, each stores up to `bar_capacity` bars. When `bar_capacity` is reached, the `Cache` automatically removes the oldest data. ::: ### Database configuration Configure a database backing to recover successfully persisted, supported cache records after a restart. Restorable records include general data, currencies, instruments, instrument closes, accounts, orders, and positions. Startup does not restore bounded market-data histories or the running process. Instrument closes persist whenever a backing database is configured. `save_market_data` does not gate them because they are recovery snapshots rather than bounded market-data history. `CacheConfig` controls cache behavior. Connection settings belong to the concrete backing config, such as `RedisCacheConfig` or `PostgresCacheConfig`. A backing is a recovery mechanism, not a complete event archive or a synchronized distributed cache. Each node owns its in-memory cache; pointing multiple nodes at the same database namespace does not keep those caches coherent. Rust-native callers build a concrete database config and use the `CacheDatabaseFactory` trait to construct the adapter passed into the system builder: ```rust use nautilus_common::{ cache::{CacheConfig, database::CacheDatabaseFactory}, enums::SerializationEncoding, }; use nautilus_infrastructure::redis::cache::RedisCacheConfig; let config = CacheConfig { encoding: SerializationEncoding::MsgPack, timestamps_as_iso8601: true, buffer_interval_ms: Some(100), ..Default::default() }; let database = RedisCacheConfig { host: Some("localhost".to_string()), port: Some(6379), connection_timeout: 2, response_timeout: 2, ..Default::default() }; let cache_database = database .create(trader_id, instance_id, config.clone()) .await?; ``` For a Rust-native live node, attach the adapter before startup: ```rust let node_config = LiveNodeConfig { trader_id, ..Default::default() }; let mut node = LiveNode::build("LiveNode".to_string(), Some(node_config))?; node.set_cache_database(cache_database)?; node.run().await?; ``` :::warning With the default `LiveExecutionEngineConfig.load_cache = true`, the node restores persisted cache state and rebuilds derived indexes before connecting clients or reconciling execution state. Setting `CacheConfig.flush_on_start = true` clears the backing instead. ::: Python passes the same database config to `LiveNodeBuilder.with_cache_database_factory`. The node constructs and owns the adapter when it starts, so the connection opens only when the node runs: ```python from nautilus_trader.common import Environment from nautilus_trader.infrastructure import RedisCacheConfig from nautilus_trader.live import LiveNode from nautilus_trader.model import TraderId node = ( LiveNode.builder("LiveNode", TraderId("TRADER-001"), Environment.LIVE) .with_cache_database_factory(RedisCacheConfig(host="localhost", port=6379)) .build() ) try: node.run() finally: node.dispose() ``` Pass `PostgresCacheConfig` instead to back cache data with Postgres. Postgres does not support actor or strategy state persistence, so do not combine it with `load_state` or `save_state`. Both configs come from `nautilus_trader.infrastructure`. :::warning Always dispose the node. `dispose()` closes the backing, which flushes writes still held in the buffer when `CacheConfig.buffer_interval_ms` is set. Returning straight from `run()` can drop them. ::: ## Using the cache ### Accessing market data The `Cache` provides access to order books, quotes, trades, bars, and other market data. Bounded market-data sequences use reverse indexing, so the **most recent entry sits at index 0**. #### Bar access ```python # Get all cached bars for a bar type. bars = self.cache.bars(bar_type) # Returns list[Bar] or None. # Get the most recent bar. latest_bar = self.cache.bar(bar_type) # Returns Bar or None. # Get a historical bar by index (0 = most recent). second_last_bar = self.cache.bar(bar_type, index=1) # Returns Bar or None. # Check whether bars exist and get the count. bar_count = self.cache.bar_count(bar_type) has_bars = self.cache.has_bars(bar_type) ``` #### Quote ticks ```python # Get quotes. quotes = self.cache.quotes(instrument_id) # Returns list[QuoteTick] or None. latest_quote = self.cache.quote(instrument_id) # Returns QuoteTick or None. second_last_quote = self.cache.quote(instrument_id, index=1) # Returns QuoteTick or None. # Check quote availability. quote_count = self.cache.quote_count(instrument_id) has_quotes = self.cache.has_quote_ticks(instrument_id) ``` #### Trade ticks ```python # Get trades. trades = self.cache.trades(instrument_id) # Returns list[TradeTick] or None. latest_trade = self.cache.trade(instrument_id) # Returns TradeTick or None. second_last_trade = self.cache.trade(instrument_id, index=1) # Returns TradeTick or None. # Check trade availability. trade_count = self.cache.trade_count(instrument_id) has_trades = self.cache.has_trade_ticks(instrument_id) ``` #### Order book ```python # Get the current order book. book = self.cache.order_book(instrument_id) # Returns OrderBook or None. # Check whether an order book exists. has_book = self.cache.has_order_book(instrument_id) # Get the number of applied book updates. update_count = self.cache.book_update_count(instrument_id) ``` #### Price access ```python from nautilus_trader.model import PriceType # Get the current price by type. Returns Price or None. price = self.cache.price( instrument_id=instrument_id, price_type=PriceType.MID, # Options: BID, ASK, MID, LAST ) ``` #### Bar types ```python from nautilus_trader.model import AggregationSource, PriceType # Get all available bar types for an instrument. Returns list[BarType]. bar_types = self.cache.bar_types( instrument_id=instrument_id, price_type=PriceType.LAST, # Options: BID, ASK, MID, LAST aggregation_source=AggregationSource.EXTERNAL, ) ``` #### Simple example ```python from nautilus_trader.model import Bar, BarType from nautilus_trader.trading import Strategy class MarketDataStrategy(Strategy): def on_start(self) -> None: # Subscribe to 1-minute bars. self.bar_type = BarType.from_str(f"{self.instrument_id}-1-MINUTE-LAST-EXTERNAL") self.subscribe_bars(self.bar_type) def on_bar(self, bar: Bar) -> None: bars = (self.cache.bars(self.bar_type) or [])[:3] if len(bars) < 3: return # Access the latest three bars for analysis. current_bar = bars[0] prev_bar = bars[1] prev_prev_bar = bars[2] # Read the latest quote and trade. latest_quote = self.cache.quote(self.instrument_id) latest_trade = self.cache.trade(self.instrument_id) if latest_quote is not None: current_spread = latest_quote.ask_price - latest_quote.bid_price self.log.info(f"Current spread: {current_spread}") ``` ### Trading objects The `Cache` provides access to trading objects such as: - Orders - Positions - Accounts - Instruments #### Orders Query orders by venue, strategy, instrument, account, or order side. ##### Basic order access ```python # Get a specific order by its client order ID order = self.cache.order(ClientOrderId("O-123")) # Get all orders in the system orders = self.cache.orders() # Get orders filtered by specific criteria orders_for_venue = self.cache.orders(venue=venue) # All orders for a specific venue orders_for_strategy = self.cache.orders( strategy_id=strategy_id ) # All orders for a specific strategy orders_for_instrument = self.cache.orders( instrument_id=instrument_id ) # All orders for an instrument ``` ##### Order state queries ```python # Get orders by their current state open_orders = self.cache.orders_open() # Orders currently active at the venue closed_orders = self.cache.orders_closed() # Orders that have completed their lifecycle emulated_orders = self.cache.orders_emulated() # Orders being simulated locally by the system inflight_orders = ( self.cache.orders_inflight() ) # Orders submitted (or modified) to venue, but not yet confirmed local_active_orders = ( self.cache.orders_active_local() ) # Orders still managed locally (initialized, emulated, or released) # Check specific order states exists = self.cache.order_exists( client_order_id ) # Checks if an order with the given ID exists in the cache is_open = self.cache.is_order_open(client_order_id) # Checks if an order is currently open is_closed = self.cache.is_order_closed(client_order_id) # Checks if an order is closed is_emulated = self.cache.is_order_emulated( client_order_id ) # Checks if an order is being simulated locally is_inflight = self.cache.is_order_inflight( client_order_id ) # Checks if an order is submitted or modified, but not yet confirmed is_active_local = self.cache.is_order_active_local( client_order_id ) # Checks if an order is still managed locally ``` ##### Order statistics ```python # Get counts of orders in different states open_count = self.cache.orders_open_count() # Number of open orders closed_count = self.cache.orders_closed_count() # Number of closed orders emulated_count = self.cache.orders_emulated_count() # Number of emulated orders inflight_count = self.cache.orders_inflight_count() # Number of inflight orders local_active_count = ( self.cache.orders_active_local_count() ) # Number of locally active orders (initialized, emulated, or released) total_count = self.cache.orders_total_count() # Total number of orders in the system # Get filtered order counts buy_orders_count = self.cache.orders_open_count( side=OrderSide.BUY ) # Number of currently open BUY orders venue_orders_count = self.cache.orders_total_count( venue=venue ) # Total number of orders for a given venue ``` #### Positions The `Cache` retains positions until they are purged or reset and provides several ways to query them. ##### Position access ```python # Get a specific position by its ID position = self.cache.position(PositionId("P-123")) # Get positions by their state all_positions = self.cache.positions() # All positions in the system open_positions = self.cache.positions_open() # All currently open positions closed_positions = self.cache.positions_closed() # All closed positions # Get positions filtered by various criteria venue_positions = self.cache.positions(venue=venue) # Positions for a specific venue instrument_positions = self.cache.positions( instrument_id=instrument_id ) # Positions for a specific instrument strategy_positions = self.cache.positions( strategy_id=strategy_id ) # Positions for a specific strategy long_positions = self.cache.positions(side=PositionSide.LONG) # All long positions ``` ##### Position state queries ```python # Check position states exists = self.cache.position_exists(position_id) # Checks if a position with the given ID exists is_open = self.cache.is_position_open(position_id) # Checks if a position is open is_closed = self.cache.is_position_closed(position_id) # Checks if a position is closed # Get position and order relationships orders = self.cache.orders_for_position(position_id) # All orders related to a specific position position = self.cache.position_for_order( client_order_id ) # Find the position associated with a specific order ``` ##### Position statistics ```python # Get position counts in different states open_count = self.cache.positions_open_count() # Number of currently open positions closed_count = self.cache.positions_closed_count() # Number of closed positions total_count = self.cache.positions_total_count() # Total number of positions in the system # Get filtered position counts long_positions_count = self.cache.positions_open_count( side=PositionSide.LONG ) # Number of open long positions instrument_positions_count = self.cache.positions_total_count( instrument_id=instrument_id ) # Number of positions for a given instrument ``` #### Accounts ```python # Access account information account = self.cache.account(account_id) # Retrieve account by ID account = self.cache.account_for_venue(venue) # Retrieve account for a specific venue account_id = self.cache.account_id(venue) # Retrieve account ID for a venue ``` #### Instruments ```python # Get instrument information instrument = self.cache.instrument(instrument_id) # Retrieve a specific instrument by its ID all_instruments = self.cache.instruments() # Retrieve all instruments in the cache # Get instruments for a venue. venue_instruments = self.cache.instruments(venue=venue) # Instruments for a specific venue # Get instrument identifiers instrument_ids = self.cache.instrument_ids() # Get all instrument IDs venue_instrument_ids = self.cache.instrument_ids( venue=venue ) # Get instrument IDs for a specific venue ``` ### Purging cached data Long-running sessions accumulate closed orders, closed positions, account events, and unused instruments. The cache exposes targeted and bulk purge methods so strategies and the live trading engine can keep memory bounded without restarting the system. #### Targeted purges Use these to drop a single entity. Each refuses to purge while the entity is still active. - `cache.purge_order(client_order_id)`: removes the order and every order-keyed index entry. Skips open orders. - `cache.purge_position(position_id)`: removes the position, its snapshots, and position-keyed index entries. Skips open positions. - `cache.purge_instrument(instrument_id)`: removes the instrument and its transient per-instrument maps (order book, quotes, trades, mark/index/funding prices, instrument status and close, greeks, and bars referencing the instrument). Skips while any associated order is non-terminal (anything that has not reached a closed state, including initialized, submitted, accepted, emulated, released, and inflight orders) or any associated position is non-closed. :::warning `purge_instrument` is intended for actors and strategies with their own lifecycle logic for deciding when an instrument is no longer needed. Purging an instrument that another component still relies on causes missing instrument lookups and loses market-data history. Active subscriptions belong to the data engine, so unsubscribe before purging if you no longer want updates. ::: #### Bulk purges Use these to sweep older entries by age. They take the current timestamp and a buffer or lookback window in seconds. - `cache.purge_closed_orders(ts_now, buffer_secs)`: closed orders whose close timestamp is older than `buffer_secs`. - `cache.purge_closed_positions(ts_now, buffer_secs)`: closed positions whose close timestamp is older than `buffer_secs`. - `cache.purge_account_events(ts_now, lookback_secs)`: account state events older than `lookback_secs`. A value of `0` purges all events. #### Automatic purging in live trading `LiveExecutionEngineConfig` schedules the bulk purges on a timer. All purge intervals default to `None`, which disables the corresponding loop. Set an interval to enable a loop and set its buffer or lookback to control how recent entries remain protected. This example uses the recommended starting values from the live-trading configuration guide: ```python from nautilus_trader.config import LiveExecutionEngineConfig exec_engine = LiveExecutionEngineConfig( purge_closed_orders_interval_mins=15, purge_closed_orders_buffer_mins=60, purge_closed_positions_interval_mins=15, purge_closed_positions_buffer_mins=60, purge_account_events_interval_mins=15, purge_account_events_lookback_mins=60, ) ``` A shorter interval runs a purge more often, while a shorter buffer or lookback removes newer data. Choose each value separately based on memory limits and the recent execution context needed for reconciliation or analysis. See [Configure live trading: memory management](../how_to/configure_live_trading.md) for the full parameter reference. :::note The instrument purge has no automatic loop because the right time to drop an instrument depends on strategy state, not age. Call `cache.purge_instrument` from the actor or strategy that owns the instrument's lifecycle. ::: ### Custom data The `Cache` stores raw bytes under application-defined string keys. Serialize values before adding them and deserialize them after retrieval. Actors and strategies can use these entries to share small amounts of application data. #### Basic storage and retrieval ```python # Store serialized data. self.cache.add(key="my_key", value=b"some binary data") # Retrieve serialized data. stored_data = self.cache.get("my_key") # Returns bytes or None. ``` :::warning The `Cache` is not a general database. Use a dedicated store for large datasets or complex queries. ::: ## Best practices and common questions ### Cache vs. portfolio usage The `Cache` and `Portfolio` serve different purposes: **Cache**: - Retains execution objects, selected object histories, and bounded recent market data until purge or reset. - Applies local state changes immediately, such as initializing an order before submission. - Applies external events when the engine processes them, such as when an order fills. **Portfolio**: - Aggregates position, exposure, and account information. - Computes current portfolio values from cached state and market prices. ```python from nautilus_trader.model import PositionChanged from nautilus_trader.trading import Strategy class MyStrategy(Strategy): def on_position_changed(self, event: PositionChanged) -> None: # Read the fills retained by the cached position. position = self.cache.position(event.position_id) fills = position.events() if position is not None else [] # Read current aggregate exposure from the portfolio. current_exposure = self.portfolio.net_exposure(event.instrument_id) ``` ### Cache vs. strategy variables Use cache entries for shared, serialized data and strategy variables for local working state. **Cache storage**: - Available to actors and strategies that share the system cache. - Can persist general byte entries when a backing database is configured and writes complete. - Remains available when an individual strategy resets, but a cache or execution-engine reset clears the in-memory entries. **Strategy variables**: - Keep typed, strategy-specific calculations and intermediate values encapsulated. - Do not expose values to other components or persist them automatically. Actor and strategy state persistence across process restarts uses separate `on_save` and `on_load` hooks with a supported backing. See the [cache database configuration](../how_to/configure_live_trading.md#cache-database-configuration) section of the live-trading guide. Serialize shared data before adding it to the cache: ```python import json from nautilus_trader.trading import Strategy class MyStrategy(Strategy): def on_start(self) -> None: shared_data = { "last_reset": self.clock.timestamp_ns(), "trading_enabled": True, } self.cache.add("shared_strategy_info", json.dumps(shared_data).encode()) ``` Another strategy can retrieve the cached data as follows: ```python import json from nautilus_trader.trading import Strategy class AnotherStrategy(Strategy): def on_start(self) -> None: data_bytes = self.cache.get("shared_strategy_info") if data_bytes is not None: shared_data = json.loads(data_bytes) self.log.info(f"Shared data retrieved: {shared_data}") ``` ## Related guides - [Data](data/): Data types stored in the cache. - [Strategies](strategies.md): Strategies access cache for market data and state. - [Reports](reports.md): Generate reports from cached data. # Configuration Source: https://nautilustrader.io/docs/latest/concepts/configuration/ NautilusTrader uses typed configuration objects for data clients, execution clients, engines, and strategies. Higher-level configs compose these component configs. For example, `LiveNodeConfig` owns the node's core component settings; register adapter clients through `LiveNode.builder(...)`. Adapters keep separate data and execution client configs when their capabilities or credentials differ. ## Design principles ### Concrete fields carry resolved values Rust config fields normally carry concrete values when the component requires a resolved setting. For example, adapter timeouts, retry counts, backoff delays, and heartbeat intervals often use plain types such as `u64` or `u32`. Construction resolves these fields before the component starts, so downstream code can consume them without repeating defaulting logic. ### Option semantics are field-specific In a stored Rust config, `Option` contains either `Some(value)` or `None`. The stored value does not record whether a caller omitted an input. A component may interpret `None` as disabling a feature, leaving a lookback window unbounded, falling back to the runtime environment, or applying an internal default. The **field documentation defines its meaning**. This distinction makes config semantics visible in the type. A plain `u64` always has a value, while the code consuming an `Option` handles the absent case. ### Defaults are type-specific Rust config types define defaults through `#[builder(default = value)]` annotations, a custom `Default` implementation, or both. PyO3 constructors generally resolve omitted concrete parameters from the Rust `Default` implementation instead of maintaining separate Python defaults. Container-level `#[serde(default)]` on a config struct fills its missing serialized fields from that config's `Default` implementation. Field-level `#[serde(default)]` instead uses the field type's default, unless the attribute names another function. `Type::default()` and `Type::builder().build()` are separate construction paths. A custom `Default` implementation may delegate part of its construction to the builder, but this is type-specific. Do not assume that the two paths are interchangeable unless the implementation or documentation guarantees it. ### Unknown-field handling depends on the construction path Rust deserialization and Python constructor binding enforce unknown fields independently. `BybitDataClientConfig`, for example, uses `#[serde(deny_unknown_fields)]` and rejects extra serialized keys. A Rust type without that attribute may accept them. Fixed Python config constructors raise `TypeError` for unsupported keywords. `DataActorConfig`, `StrategyConfig`, and `ExecutionAlgorithmConfig` accept additional keywords for Python subclasses. Do not infer one construction path's strictness from another. ## Python configs Import core config types from `nautilus_trader.config`. Import adapter configs from the adapter's public module, such as `nautilus_trader.adapters.bybit`. Most runtime config classes are PyO3 wrappers around Rust config structs. In a Python constructor, omitting a parameter whose signature default is `None` is equivalent to passing `None` explicitly. The wrapper then either selects the Rust default or preserves an absent optional value, depending on the field. Check the field documentation rather than inferring its behavior from the Python annotation. Properties expose selected config values. Configs that hold secrets can omit their values or expose only presence checks; consult the config API before displaying or logging a config. Mutability is type-specific: many configs expose only read-only getters, while extensible component configs and some adapter configs expose documented setters. `DataActorConfig`, `StrategyConfig`, and `ExecutionAlgorithmConfig` also accept extra fields for Python subclasses. Python-owned analysis configs retain their documented dataclass behavior. ```python from nautilus_trader.adapters.bybit import BybitDataClientConfig omitted = BybitDataClientConfig() explicit_none = BybitDataClientConfig( http_timeout_secs=None, base_url_http=None, ) assert omitted.http_timeout_secs == explicit_none.http_timeout_secs == 60 assert omitted.base_url_http is explicit_none.base_url_http is None # Override the timeout config = BybitDataClientConfig(http_timeout_secs=30) # Read the resolved value assert config.http_timeout_secs == 30 ``` When a wrapper maps `None` to a non-`None` Rust default, Python cannot use that parameter to store Rust `None`. For example, passing `instrument_status_poll_secs=None` to `BybitDataClientConfig` retains its 60-second default. Rust callers can set `instrument_poll_interval_secs` to `None` to disable periodic instrument and status polling. ## Rust configs Many Rust config structs derive [`bon::Builder`](https://bon-rs.com), which generates a type-safe builder with compile-time checks for required fields. A builder can omit fields that declare a builder default. Use the construction style documented for the config type. For `DataEngineConfig`, the builder and struct update forms below both enable delta buffering and retain the declared defaults for other fields: ```rust use nautilus_data::engine::config::DataEngineConfig; let with_builder = DataEngineConfig::builder() .buffer_deltas(true) .build(); let with_struct_update = DataEngineConfig { buffer_deltas: true, ..Default::default() }; ``` Use `DataEngineConfig::default()` when no fields need an override. ## Adapter config fields Names recur across adapter configs, but their types and defaults depend on the adapter and client. For example, `BybitDataClientConfig` defines the fields below. The `Default` column shows the values from `BybitDataClientConfig::default()`: | Rust field | Rust type | Default | Purpose | | ------------------------------- | ------------- | ---------- | ------------------------------ | | `http_timeout_secs` | `u64` | `60` | REST request timeout. | | `max_retries` | `u32` | `3` | Maximum retry attempts. | | `retry_delay_initial_ms` | `u64` | `1_000` | Initial backoff delay. | | `retry_delay_max_ms` | `u64` | `10_000` | Maximum backoff delay. | | `heartbeat_interval_secs` | `u64` | `20` | WebSocket keepalive interval. | | `recv_window_ms` | `u64` | `5_000` | Signed request expiry window. | | `instrument_poll_interval_secs` | `Option` | `Some(60)` | Instrument and status polling. | Python exposes `instrument_poll_interval_secs` as `instrument_status_poll_secs`. `BybitDataClientConfig::builder().build()` instead leaves `instrument_poll_interval_secs` as `None`, which disables periodic instrument and status polling. This is one case where the type's **default and builder paths differ**. Adapter-specific fields such as rate limits, polling intervals, and margin modes are documented in the [integration guides](../integrations/index.md). ## Engine configs Engine configs use the same typed-field approach. In `LiveExecutionEngineConfig`, fields such as `reconciliation`, `inflight_check_interval_ms`, and `open_check_threshold_ms` have concrete defaults: | Field | Default | Purpose | | ---------------------------- | ------- | ------------------------------------------------------ | | `reconciliation` | `True` | Run reconciliation during startup. | | `inflight_check_interval_ms` | `2_000` | Check whether in-flight orders exceed their threshold. | | `open_check_threshold_ms` | `5_000` | Wait before acting on an open-order discrepancy. | Optional fields such as `open_check_interval_secs` and `position_check_interval_secs` enable or disable their periodic checks: ```python from nautilus_trader.config import LiveExecutionEngineConfig config = LiveExecutionEngineConfig( open_check_interval_secs=30.0, # Enable open order polling open_check_lookback_mins=60, # Look back 60 minutes ) assert config.open_check_interval_secs == 30.0 assert config.open_check_lookback_mins == 60 assert config.position_check_interval_secs is None # Disabled by default ``` After the live node completes startup, an available execution client lets this config schedule open-order report requests every 30 seconds and limit each request to the previous 60 minutes. It does not schedule periodic position report requests. Supplied periodic intervals must be positive, finite values of at least one nanosecond. The separate `reconciliation` field enables startup reconciliation and defaults to `True`; the interval fields control periodic checks independently. When startup reconciliation is enabled, `reconciliation_startup_delay_secs` also delays the first periodic check after startup. For the full set of live engine options, see [ExecutionEngine configuration](../how_to/configure_live_trading.md#executionengine-configuration). # Continuous Futures Source: https://nautilustrader.io/docs/latest/concepts/continuous_futures/ A **continuous future** is a derived series that splices consecutive futures contracts into one adjusted price stream. Each underlying contract expires, so the continuous series rolls to the next contract at a transition point. Each segment is adjusted into a common price frame so contract changes do not introduce artificial price jumps. Nautilus models a continuous future as a target `BarType` plus an explicit list of roll transitions supplied in request or subscription params. The data engine selects the real contract for each time segment, computes its cumulative price adjustment, and feeds the adjusted source data through the normal bar aggregation path. ## Adjustment modes `ContinuousFutureAdjustmentType` combines direction (backward or forward) with operation (spread or ratio): | Mode | Operation | Anchor segment | | ----------------- | -------------- | ----------------------------------- | | `BACKWARD_SPREAD` | Additive | Last contract in adjustment range. | | `FORWARD_SPREAD` | Additive | First contract in adjustment range. | | `BACKWARD_RATIO` | Multiplicative | Last contract in adjustment range. | | `FORWARD_RATIO` | Multiplicative | First contract in adjustment range. | The cumulative adjustment at segment `k` of `N` transitions is: ```text BACKWARD_SPREAD: sum over i in [k, N) of (post_i - pre_i) FORWARD_SPREAD: sum over i in [0, k) of (pre_i - post_i) BACKWARD_RATIO: product over i in [k, N) of (post_i / pre_i) FORWARD_RATIO: product over i in [0, k) of (pre_i / post_i) ``` Spread modes accumulate additive offsets. Ratio modes accumulate multiplicative factors and require strictly positive prices. ## Inputs A continuous-future request or subscription is any `RequestBars` or `SubscribeBars` that carries a `continuous_future_transitions` entry in `params`: ```python params = { "continuous_future_transitions": [ { "transition_time_ns": 1773671460000000000, # when ESH26 rolls to ESM26 "pre_instrument_id": "ESH26.XCME", "post_instrument_id": "ESM26.XCME", "pre_price": "6001.00", # last ESH26 price pre-roll "post_price": "5995.50", # first ESM26 price post-roll }, # ... more transitions ... ], "continuous_future_adjustment_mode": "BACKWARD_SPREAD", # Optional: cap the upper end of cumulative adjustment at the transition whose # post_instrument_id matches (the backward-mode anchor). # "last_post_instrument_id": "ESM26.XCME", # Optional: cap the lower end of cumulative adjustment at the transition whose # pre_instrument_id matches (the forward-mode anchor). # "first_pre_instrument_id": "ESM26.XCME", } ``` `continuous_future_adjustment_mode` defaults to `BACKWARD_SPREAD` when omitted. The `bar_type` on the request or command is the **target** continuous bar type, for example `"ES.XCME-1-MINUTE-LAST-INTERNAL@1-MINUTE-EXTERNAL"`. The root identifier (`ES.XCME`) is the continuous root, not a real contract. Each segment's raw source data comes from the real contract in the transitions list. The continuous target bar type must be **internally aggregated**. Externally aggregated bars are not supported as continuous targets, but they can serve as the per-segment source. ### Bounded chains The two optional bounds restrict which transitions contribute to the cumulative adjustment. They do not remove contract segments from the request or subscription: - `last_post_instrument_id` caps the upper end at the first transition whose `post_instrument_id` matches. Backward modes use the matching post contract as the zero-adjustment anchor; forward modes exclude later transitions from the cumulative adjustment. - `first_pre_instrument_id` caps the lower end at the first transition whose `pre_instrument_id` matches. Forward modes use the matching pre contract as the zero-adjustment anchor; backward modes exclude earlier transitions from the cumulative adjustment. These bounds let callers pass a wider transition table while choosing the adjustment range. ## Validation The request and subscription paths apply the same transition-parameter validation rules before allocating an aggregator or child segment state: - When supplied, `continuous_future_adjustment_mode` must parse as a valid `ContinuousFutureAdjustmentType`. - `continuous_future_transitions` must be a non-empty array of transition rows. - Each row must include a non-negative integer `transition_time_ns`, and transition times must be strictly increasing. - Each `pre_instrument_id` and `post_instrument_id` must parse as a valid `InstrumentId` whose venue equals the target venue. - The chain must be continuous: row `i`'s `post_instrument_id` must equal row `i + 1`'s `pre_instrument_id`. - Each row must include finite `pre_price` and `post_price`. Ratio modes additionally require both prices to be positive. - If the caller supplies `last_post_instrument_id`, it must parse as an `InstrumentId`, match the target venue, and appear as a `post_instrument_id` in the transition list. The same applies to `first_pre_instrument_id`. A validation error therefore returns before either path starts an aggregation workflow. After validation, the request path releases its request-scoped aggregators if setup or the initial segment dispatch fails. A failure while dispatching a later segment still ends the request with a completion response and normal aggregator cleanup. ## Target instrument auto-synthesis The continuous root (for example `ES.XCME`) is a synthetic id with no market data of its own, but downstream consumers (aggregators, cache lookups, serialization) still expect an `Instrument` in the cache. After validation, both the request and subscription paths ensure the target instrument exists: - If the target id is already cached, the target setup is a no-op. Callers can pre-register a custom continuous instrument and the engine respects it. - Otherwise the target setup fetches the first segment's instrument from the cache and clones it, overriding only `id`, `raw_symbol`, and clearing `activation_ns` and `expiration_ns` to `0`. Every other field (currency, precision, increment, multiplier, lot size, underlying, fees, margins, exchange, tick scheme, info) is reused from the segment. - If the first segment is not yet in the cache or is not a `FuturesContract`, the setup logs a warning and returns. The caller must then register the continuous instrument manually. ## Architecture overview ```mermaid flowchart TD User([User/Strategy]) -->|"params['continuous_future_transitions']"| Entry{"Entry point"} Entry -->|RequestBars| ReqPath[Request path] Entry -->|SubscribeBars| SubPath[Subscription path] ReqPath --> ReqSegments[Segment dispatcher] SubPath --> SubRoller[Active segment + time alert] ReqSegments -->|per segment| ChildReq[Child request for segment contract] SubRoller -->|active segment| ChildSub[Child subscription for segment contract] ChildReq --> Agg[(Primary aggregator
BarBuilder.set_adjustment)] ChildSub --> Agg2[(Live aggregator
BarBuilder.set_adjustment)] Agg -->|adjusted bars| ReqAgg[(Request-scoped aggregator chain)] ReqAgg -->|bars at every level| Cache[(Cache)] Agg2 -->|adjusted bars| MsgBus[(msgbus: data.bars.*)] ``` Request-path bars land in the cache; subscription-path bars publish to the message bus. Both paths use the same segmentation, source resolution, and adjustment calculation. The request path processes segments in sequence; the subscription path keeps one source active and switches it when the time alert for the next transition fires. ## Segments A **segment** is a contiguous time slice owned by one real contract. Transitions separate segments. Given `transitions[0..N)`: - Segment 0: `(-inf, transitions[0].time)` on `transitions[0].pre_instrument_id`. - Segment k, with k in `[1, N)`: `[transitions[k-1].time, transitions[k].time)` on `transitions[k].pre_instrument_id`. - Segment N: `[transitions[N-1].time, +inf)` on `transitions[N-1].post_instrument_id`. The request path clips each segment to the requested time range and dispatches the segments in order. The subscription path uses the engine clock to select the active segment and schedules the next remaining transition. ## Request flow The request path dispatches one child request at a time. When a child response arrives, the engine aggregates its data and advances to the next segment. ```mermaid sequenceDiagram participant User participant Engine as DataEngine participant Agg as Primary aggregator participant Client as DataClient User->>Engine: RequestBars with transitions Engine->>Agg: initialize aggregators and cursor loop one iteration per segment Engine->>Agg: BarBuilder.set_adjustment(offset, mode) Engine->>Client: child request for segment contract Client-->>Engine: DataResponse Engine->>Agg: aggregate child response Engine->>Engine: advance cursor end Engine->>User: completion response ``` :::info Adjusted bars are written to the cache as each child response is processed. The completion response signals that the request has finished and reports the source record count; it does not contain a combined vector of adjusted bars. ::: ### Chain aggregators If a request sets `bar_types = (bar_type_1, bar_type_2)` for multi-level internal aggregation, the engine creates an isolated request-scoped aggregator for each level. Segment source responses enter the primary continuous target, and its emitted bars feed matching downstream aggregators. Only the primary builder receives the adjustment; higher levels re-aggregate already adjusted data. ## Subscription flow A small state machine drives each active subscription via a single pending time alert: ```mermaid stateDiagram-v2 [*] --> Active: subscribe(segment_i active, timer for transition_i) Active --> Active: roll(deactivate segment_i, activate segment_{i+1}, schedule next timer) Active --> [*]: unsubscribe(cancel timer, deactivate segment) ``` When a transition fires, the engine deactivates the current segment (unsubscribes the source), applies the next segment's adjustment, subscribes to the new source, and arms the timer for the following transition. ## Source resolution For any continuous-future target `BarType`, the raw data feeding the primary aggregator lives on the **segment contract**, not the continuous id. The target's shape decides the source type: ```mermaid flowchart TD Target[target_bar_type] --> Check1{is_composite?} Check1 -->|yes| Ref[reference = target.composite] Check1 -->|no| RefNo[reference = target] Ref --> Check2{externally_aggregated?} RefNo --> Check2 Check2 -->|yes| Bars["source = bars (RequestBars / SubscribeBars)"] Check2 -->|no| Check3{price_type} Check3 -->|LAST| Trades["source = trades (TradeTicks)"] Check3 -->|MID/BID/ASK| Quotes["source = quotes (QuoteTicks)"] ``` For internally aggregated sources, `LAST` uses trades, while `BID`, `ASK`, and `MID` use quotes. Other price types are not supported by quote aggregation. ## BarBuilder adjustment The builder applies the adjustment **at ingress** on every `update(price, ...)` and `update_bar(bar, ...)` call. The running OHLC state therefore remains in the adjusted common frame. Changing the adjustment during a bar affects only subsequent input. ```mermaid flowchart LR Tick[raw price] --> AdjCheck{adjustment_mode} AdjCheck -->|inactive| Raw[pass through] AdjCheck -->|spread| SpreadApply[price + adjustment_raw] AdjCheck -->|ratio| RatioApply[price * adjustment_ratio] Raw --> Update[update OHLC state] SpreadApply --> Update RatioApply --> Update Update --> Build[build on trigger] ``` The `BarBuilder` uses the mode only to choose addition or multiplication. The engine resolves the adjustment direction into a cumulative value before calling `set_adjustment`. The `reset()` method clears per-bar OHLCV state for the next bar but preserves the segment-scoped adjustment. ## Mid-bar roll boundary If a roll lands inside an in-progress target bar, the builder keeps the current OHLC state and applies the new adjustment only to subsequent updates. The pre-boundary portion stays at the old offset; the post-boundary portion uses the new offset. Rewriting the existing OHLC under the new adjustment would require raw input that the builder does not retain. ## Limitations - The feature requires supplied transition metadata. The engine does not discover rolls, choose contracts, or infer roll prices: that is the caller's responsibility. - Ratio adjustment converts the factor and each price through `f64` before rebuilding the adjusted `Price`. For high-precision instruments, the result can differ from equivalent `Decimal` multiplication. Spread adjustment remains exact in the fixed-point representation because it adds directly to `PriceRaw`. # Custom Data Source: https://nautilustrader.io/docs/latest/concepts/custom_data/ NautilusTrader supports custom data authored in Python or Rust. Both forms use the same runtime routing, persistence, and query pipeline as built-in data. This document explains how custom data is: - Registered at runtime. - Wrapped across the Python/Rust boundary. - Serialized to and from Arrow/Parquet. - Routed through actors and strategies. ## Goals The custom-data architecture satisfies the following requirements: - Let users define custom data in pure Python without writing Rust code. - Let Rust-defined custom data use native Rust JSON and Arrow handlers. - Preserve a single user-facing `CustomData` wrapper at the PyO3 boundary. - Support persistence in `ParquetDataCatalog` using dynamic type registration instead of hardcoded schemas. - Make custom data routable through the normal data-engine, actor, and strategy subscription flow. ## High-level model There are two supported authoring modes: | Mode | Authoring form | Registration path | Encode/decode path | Wrapper backend | | ---------------- | ----------------------------------------------- | ----------------------------------------------------------------- | ----------------------------- | ------------------------- | | Pure Python | Class with JSON and Arrow methods | `register_custom_data_class(...)` | Python callback + Arrow C FFI | `PythonCustomDataWrapper` | | Same-binary Rust | `#[custom_data]` or `#[custom_data(pyo3)]` type | `ensure_custom_data_registered::()` and extractor registration | Native Rust | Native Rust payload | Both modes converge on the same outer PyO3 `CustomData` wrapper and the same `DataType` identity model. ## End-to-end flow ```mermaid sequenceDiagram participant U as User code participant P as Python layer participant R as Rust model/catalog participant G as Process-wide registries participant S as Storage U->>P: define class/type U->>P: register_custom_data_class(...) or module init P->>R: install type registration R->>G: store JSON/Arrow and optional extractor handlers U->>P: CustomData(data_type, data) P->>R: write_custom_data([...]) R->>G: lookup encoder by type_name G-->>R: encoder R->>S: write RecordBatch to Parquet U->>P: query(type_name, ...) P->>R: query catalog R->>S: read RecordBatch + metadata R->>G: lookup decoder by type_name G-->>R: decoder R-->>P: CustomData wrappers P-->>U: typed data via .data ``` ## Core components ### Registry module `crates/model/src/data/registry.rs` holds the process-wide JSON, Arrow, and Python extraction registries. Registration uses atomic `DashMap::entry()` operations so concurrent `register_*` and `ensure_*` calls do not race when claiming an entry. The module initializes its registry state through `OnceLock` and stores: - JSON deserializers keyed by `type_name`. - Arrow schemas, encoders, and decoders keyed by `type_name`. - Python extractors that convert a Python object into `Arc`. - Rust extractor factories that produce Python extractors for same-binary types. Instead of hardcoding every type into the main binary, NautilusTrader resolves handlers at runtime using the `type_name` stored in `DataType` and Parquet metadata. ### `CustomData` The outer PyO3 `CustomData` wrapper is the common container that crosses the FFI boundary. Constructor signature: `CustomData(data_type, data)` where `DataType` comes first, then the inner payload. It contains: - A `DataType`. - An inner custom payload implementing `CustomDataTrait` (wrapped in `Arc`). Timestamps (`ts_event`, `ts_init`) are delegated to the inner `CustomDataTrait` implementation and exposed as properties on the wrapper. On the Python side, `CustomData` implements `__eq__` and `__repr__`. The Rust `PartialEq` implementation compares the `DataType` and delegates payload equality to the inner value. Instances are intentionally unhashable so equality remains consistent with the payload comparison. This wrapper is shared across both custom-data modes. User code interacts with one API even though the underlying payload may be: - A Python-backed wrapper. - A same-binary Rust value. #### `CustomData` JSON envelope When serialized to JSON, such as for `to_json_bytes` / `from_json_bytes`, the SQL cache, or Redis, `CustomData` uses one canonical envelope. Deserialization therefore does not depend on user payload field names: - `type`: The custom type name (from `CustomDataTrait::type_name`). - `data_type`: An object with `type_name`, `metadata`, and optional `identifier`. - `payload`: The inner payload only (the result of `CustomDataTrait::to_json` parsed as a value). Registered deserializers pass only this value to `from_json`, so user structs can use any field names, including `value`, without conflicting with wrapper metadata. This envelope is produced by Rust `CustomData` serialization and consumed by the registry module when deserializing custom data from JSON. ### `DataType` `DataType` identifies custom data for routing and persistence. Constructor: `DataType(type_name, metadata=None, identifier=None)`. It includes: - `type_name`. - Optional `metadata`. - Optional `identifier` for persistence paths and cache database lookups. It does not affect routing, equality, or hashing. **Equality, hashing, and topic routing** are derived from `type_name` and `metadata` only. Two `DataType` values with the same type name and metadata but different identifiers compare equal and publish to the same message bus topic. The `identifier` selects the catalog path under `data/custom//` and participates in PostgreSQL and Redis filtering. Persistence stores the full `DataType` with each `CustomData` value and restores it on query, while handler lookup uses `type_name`. The same logical type can therefore carry different metadata or identifiers and still decode through the same registered handler. ## Registration architecture Registration bridges the gap between Python objects and Rust trait objects. ```mermaid flowchart TD A[User-defined custom type] --> B{Mode} B --> C[Pure Python] B --> D[Same-binary Rust] C --> F[register_custom_data_class] D --> G[ensure_custom_data_registered and native extractor] F --> I[Python callbacks registered] G --> J[Native JSON and Arrow handlers registered] I --> L[Process-wide registries] J --> L ``` ### Pure Python registration When Python code calls `register_custom_data_class(MyType)`: 1. Rust retains the class for JSON reconstruction. 1. Rust registers JSON and Arrow handlers that invoke the class callbacks. 1. When constructing `CustomData`, Rust uses a registered native extractor if it accepts the object. Otherwise, it wraps the object in `PythonCustomDataWrapper`. JSON and Arrow callbacks on this path run under the Python GIL. ### Same-binary Rust registration For Rust types compiled into the process: 1. `#[custom_data]` or `#[custom_data(pyo3)]` generates the trait and JSON implementations, plus Arrow implementations by default. 1. `ensure_custom_data_registered::()` inserts native schema/encoder/decoder handlers into the process-wide registries. 1. `ensure_rust_extractor_registered::()` registers an extractor factory for PyO3-exposed types. Once activated through Python class registration, the extractor can recover the concrete Rust type instead of using the Python wrapper. This path stays fully native in Rust for encode/decode. ### Registration precedence `register_custom_data_class(...)` resolves handlers in the following order: 1. Use a native extractor and existing native JSON/Arrow handlers when they are registered. 1. Otherwise, use the Python wrapper and callback handlers. The idempotent `ensure_*` registrations do not overwrite existing native handlers. ## Wrapper backends Internally, the outer `CustomData` wrapper can hold different payload implementations. ### `PythonCustomDataWrapper` Used for pure Python custom data. Responsibilities: - Stores a reference to the Python object. - Caches `ts_event`, `ts_init`, and `type_name`. - Implements `CustomDataTrait`. - Supports JSON and Arrow callback paths that invoke Python under the GIL. This is the construction fallback when no registered extractor accepts the object. Python JSON and Arrow decoders also produce this wrapper directly. ### Native same-binary Rust payload For Rust types compiled into the process, the inner payload is the concrete Rust type and can be downcast directly from `Arc`. No Python callback path is needed for serialization or decode. ## Persistence architecture ### Why dynamic Arrow registration is needed Built-in NautilusTrader data types have schemas and encoders known statically to the Rust binary. Custom data does not. The persistence layer therefore resolves custom data dynamically using the registered `type_name`. ### Catalog write flow `ParquetDataCatalog` expects custom writes to come in as `CustomData` values. The custom-data write path: 1. Takes `type_name` from the inner payload and `metadata` and `identifier` from the first value's `DataType`. 1. Looks up the Arrow encoder in the process-wide registry. 1. Encodes the values to a `RecordBatch`. 1. Appends a `data_type` column containing the persisted `DataType`. 1. Attaches `type_name` and metadata to the Arrow schema. 1. Writes the batch to Parquet under the custom-data path. The path layout is `data/custom//`. Identifiers are normalized before becoming path segments. ### Catalog read flow On query: 1. The catalog reads matching Parquet files. 1. Extracts `type_name` from schema metadata. 1. Asks the process-wide registry for the decoder. 1. Decodes the `RecordBatch` into `Vec`. 1. Reconstructs `CustomData` with the original `DataType`. This makes custom-data query resolution symmetric with write-time registration. When converting a Feather stream to Parquet, such as after a backtest, the custom-data branch is designed to transform the Arrow batches and write the result directly to the matching custom-data path. :::info The direct Python `StreamingFeatherWriter.write()` method rejects `CustomData` with an `OSError`. Write custom data directly to the catalog with `ParquetDataCatalog.write_custom_data` from Python. The Rust Feather writer supports custom data, but `convert_stream_to_data` does not currently convert custom-data Feather streams to Parquet. ::: ## The Arrow C FFI bridge Pure Python custom data does not provide native Rust Arrow encode logic. For these types, NautilusTrader uses the Arrow C FFI interface to pass `RecordBatch` data between Python and Rust without JSON or binary serialization. ```mermaid sequenceDiagram participant R as Rust encoder participant P as Python payload participant F as Arrow C FFI structs participant C as Parquet writer R->>P: encode_record_batch_py(items) P->>P: build pyarrow.RecordBatch P-->>F: _export_to_c (FFI_ArrowArray + FFI_ArrowSchema) F-->>R: reconstruct native RecordBatch R->>C: write Parquet ``` ### Pure Python encode path For pure Python classes: 1. Rust acquires the GIL. 1. Rust calls `encode_record_batch_py(...)` on the first Python payload. 1. Python converts objects to a `pyarrow.RecordBatch`. 1. Python exports the batch via `_export_to_c` into Arrow C FFI structs. 1. Rust reconstructs a native `RecordBatch` from the FFI structs and writes it. ### Pure Python decode path For the reverse direction: 1. Rust converts its `RecordBatch` into Arrow C FFI structs. 1. Python imports the batch via `RecordBatch._import_from_c`. 1. Python calls `decode_record_batch_py(metadata, batch)` on the class. 1. Rust wraps the returned Python objects in `PythonCustomDataWrapper`. ### Native paths The Arrow C FFI bridge is not used for same-binary Rust custom data. Those types use native Rust encode/decode handlers registered in the main process. ## Reconstruction on query When custom data is loaded back from the catalog, reconstruction depends on the backend: - Same-binary Rust types decode directly to native Rust values. - Pure Python types reconstruct through the registered class's `decode_record_batch_py(...)` callback. In all cases the caller receives the same outer `CustomData` wrapper at the PyO3 API boundary. ## Runtime integration Custom data also participates in NautilusTrader runtime routing. Relevant integrations include: - `crates/data/src/engine/mod.rs` publishes `CustomData` through the message bus. - `crates/common/src/msgbus/switchboard.rs` derives custom topics from `DataType`. - `crates/common/src/actor/*` routes custom data into actor subscriptions. - `crates/trading/src/python/strategy.rs` exposes custom data to Python strategy `on_data`. - `crates/backtest/src/engine.rs` treats `Data::Custom` as data-engine-delivered input rather than exchange-routed data. A registered custom type can be persisted, queried, subscribed to, and consumed through the same runtime interfaces as built-in data families. ## Cache database integration The PostgreSQL and Redis cache database implementations support `CustomData`. - PostgreSQL stores custom data in the `custom` table. - The stored record includes `data_type`, `metadata`, `identifier`, and full JSON payload. - Reads reconstruct `CustomData` using `CustomData::from_json_bytes(...)`. - Python SQL bindings expose `add_custom_data` and `load_custom_data`. - Redis cache stores custom data under keys `custom::` with full `CustomData` JSON as value. - Redis `add_custom_data` and `load_custom_data` filter by `DataType` (type_name, metadata, identifier) and return results sorted by `ts_init`; this is exposed via the PyO3 `RedisCacheDatabase` API. ## Practical implications Python-only authoring and native Rust encode/decode remain two backends of one conceptual custom-data system rather than separate Python-only and Rust-only feature sets. # DST Source: https://nautilustrader.io/docs/latest/concepts/dst/ Deterministic simulation testing (DST) runs NautilusTrader under a seed-controlled runtime. This guide defines: - The reproducibility guarantees for seed-controlled execution. - The conditions required for those guarantees. - The source seams and checks that enforce the contract. - The paths where deterministic execution stops. Source locations accompany each claim so users and auditors can check the contract against the code. :::note A downstream harness that depends on NautilusTrader's determinism consumes the version of this document at its pinned NautilusTrader commit. A change to this document is a contract change for those consumers and should be reviewed as one. ::: ## What DST is DST controls the sources of nondeterminism in a concurrent system. Within the contract defined below, one seed determines task scheduling, timer firings, and random values. Two runs with the same seed, binary, configuration, and platform produce identical observable behavior. Record a failing seed to replay the same execution. A conventional async runtime draws scheduling decisions from ambient process state, including: - Task wake order. - Timer resolution. - OS thread scheduling. - Randomized hash seeds. A conventional test harness does not control these sources, so a race that appears once in CI can be difficult to reproduce. DST replaces them with a seeded pseudorandom sequence. Varying the seed explores different interleavings; reusing it selects the same interleaving. [FoundationDB](https://apple.github.io/foundationdb/testing.html) uses the pattern to test a production distributed database. In the Rust ecosystem, [madsim](https://crates.io/crates/madsim) intercepts `tokio` primitives to provide a deterministic scheduler. DST targets concurrency defects such as: - Channel wakeup ordering. - Drain races during shutdown. - Startup sequencing. - Reconciliation ordering. - Recovery-path correctness. Other test layers cannot exhaustively cover these interleavings. A deterministic scheduler can explore them across seeds and replay a failing schedule. ## Goals | Goal | Requirement | | ----------------------------- | ----------------------------------------------------------------------------------------------------------- | | Seed-reproducible execution | The in-scope runtime produces the same observable behavior for the same seed and inputs. | | Explicit scope | Every fallback to real time, unseeded randomness, or other nondeterminism is documented. | | Enforcement in source | Static checks reject banned patterns on the DST path before they rely on reviewer attention. | | Minimum required interception | Time, task scheduling, and randomness route through deterministic sources only where the contract needs it. | ## Approach The implementation has two layers. The first replaces selected `tokio` primitives with `madsim`. The second controls sources of nondeterminism outside those primitives. ### Layer 1: runtime swap Under the `simulation` Cargo feature on `nautilus-common`, four facade submodules select simulation behavior when `RUSTFLAGS="--cfg madsim"` is set: | Submodule | Covers | Normal build | Simulation build | Adoption | | --------- | ----------------------------------------------- | ------------- | ---------------------------------------------------------------- | ------------------------------------------------- | | `time` | Timers, intervals, and monotonic `Instant`. | `tokio` | `madsim` | Complete. | | `task` | Spawning and joining async tasks. | `tokio` | `madsim` | Complete. | | `runtime` | Runtime builder and handle. | `tokio` | `madsim` | Complete. | | `signal` | Process signals such as `ctrl_c` and `SIGTERM`. | `tokio` or OS | `madsim` for `ctrl_c`; a never-completing future for `terminate` | Partial. See [Signal handling](#signal-handling). | These re-exports live in `nautilus_common::live::dst`. DST-path call sites for `time`, `task`, and `runtime` import from this module. Normal builds resolve the imports to `tokio`; `simulation` with `cfg(madsim)` resolves them to `madsim`. The `sync`, `io`, `fs`, and `net` submodules, plus the `select!` macro, continue to use real `tokio`. The network crate supplies a separate [transport boundary](#simulated-http-and-websocket-transport) for plaintext HTTP and Tungstenite WebSocket connections; it does not replace Tokio inside dependencies. ### Layer 2: nondeterminism substitution Nondeterminism outside the aliased runtime needs explicit seams. #### Wall-clock time Wall-clock reads route through `nautilus_core::time::duration_since_unix_epoch`. Under simulation, the seam calls `madsim::time::TimeHandle::try_current()` to preserve Unix-epoch semantics for order and fill timestamps. Plain `#[rstest]` bodies run outside a madsim runtime. In that context, the seam falls back to `SystemTime::now()`, which uses the same real syscall as a normal build. Production paths under simulation run inside a madsim runtime and receive virtual time. #### Monotonic time Monotonic reads route through `nautilus_common::live::dst::time::Instant`. Normal builds resolve the type to `tokio::time::Instant`, preserving compatibility with `#[tokio::test(start_paused = true)]`. Simulation builds resolve it to `madsim::time::Instant`. #### Network-local monotonic time Network code routes monotonic reads through `nautilus_network::dst::time`. `nautilus-network` sits below `nautilus-common` in the dependency graph, so it provides a local re-export module with the same behavior. #### Iteration order Observable iteration order uses `IndexMap`, `IndexSet`, or an explicit sort instead of relying on `AHashMap` or `AHashSet`. `AHash` randomizes its hasher per process. Stable order is required when iteration controls event publication or the sequence of draws from a seeded `FillModel`. #### Select polling order Every production `tokio::select!` site on the DST path starts with `biased;`. Without it, an unintercepted RNG chooses the branch polling order. ## Determinism contract Under the conditions below, a run identified by `(seed, binary hash, configuration hash)` on the same platform produces bitwise-identical: - Scheduling order of async tasks. - Timer firings (virtual monotonic and virtual wall-clock). - RNG output from `madsim::rand`. - Delivery order on `tokio::sync` channels. ### Required conditions The contract holds **only when every row below is satisfied**: | Source of nondeterminism | Required condition | Failure when bypassed | | ------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | Build selection | Enable the `simulation` feature and set `RUSTFLAGS="--cfg madsim"`. | Either setting alone falls back to real `tokio` without an error. The cfg also activates madsim's libc intercepts for `clock_gettime` and `getrandom`. | | `tokio::select!` | Put `biased;` first in every production block on the DST path. | An unintercepted RNG chooses the polling order. | | Monotonic time | Use `nautilus_common::live::dst::time` or `nautilus_network::dst::time`. | Direct `std::time::Instant::now` reads the host clock. | | Wall-clock time | Use `nautilus_core::time::duration_since_unix_epoch`. | Direct clock reads bypass virtual time. | | Randomness | Use `madsim::rand`. | `rand::thread_rng`, `rand::rng()`, `fastrand`, `getrandom`, and `OsRng` are not intercepted. | | Iteration order | Use `IndexMap`, `IndexSet`, or sort at the point of use. | Randomized hash iteration changes observable ordering. | | Local tasks | Gate out `tokio::task::LocalSet` under simulation. | `madsim` does not provide `LocalSet`; use `spawn_local` without it. | | Blocking tasks | Gate out or remove `tokio::task::spawn_blocking`. | The blocking call escapes the deterministic scheduler. | ## Static enforcement Static enforcement has two layers: | Layer | Enforces | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- | | Clippy policy | `clippy.toml` and `[workspace.lints.clippy]` reject direct `getrandom::{fill,u32,u64}` calls and `tokio::task::LocalSet`. | | `check-dst-conventions` | The pre-commit hook applies path-aware and cfg-aware structural checks that Clippy cannot express cleanly. | The hook lives at `.pre-commit-hooks/check_dst_conventions.sh` and runs in the standard pre-commit suite and CI. Rules 1 to 4 and 6 scan the 17 in-scope workspace crates and the selected OKX files listed below. Rule 5 covers its two audited files. Rule 7 scans the nine crates on the madsim build path, those OKX files, and `crates/network/src/websocket/client.rs`. | Rule | Rejects | Scope or exception | | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | 1 | Raw `std::time::Instant::now()`, `SystemTime::now()`, `jiff::Timestamp::now()`, and `jiff::Zoned::now()` reads, including imported bare forms. | Allows the wall-clock seam, audited log or progress timing, and marked lines. | | 2 | Raw `rand::thread_rng`, `rand::rng()`, `fastrand::`, `getrandom::`, `OsRng`, and `Uuid::new_v4()` usage. | Allows cfg-gated and marked lines. | | 3 | Production `tokio::select!` blocks without `biased;` in the first three lines. | Excludes tests and marked lines. | | 4 | `std::thread::spawn`, `std::thread::Builder::new`, and `tokio::task::spawn_blocking`. | Allows test-only, non-madsim, and marked sites. | | 5 | `AHashMap` or `AHashSet` in the reconciliation manager and matching engine. | Covers the two audited files; the remaining file set stays outside this static rule until audited. | | 6 | Direct `tokio::net::TcpStream::connect` and `tokio::net::TcpListener::bind` calls. | Callers must use `nautilus_network::net`, which swaps to `turmoil::net` under the `turmoil` feature. | | 7 | Raw `tokio::{time,task,runtime,signal}` paths in production code on the madsim build path. | Allows the facade, process-wide real Tokio runtime, test infrastructure, cfg-gated sites, and marked lines. | Supported Madsim HTTP and WebSocket paths use `nautilus_network::dst::net`; it selects the simulated byte stream and re-exports `nautilus_network::net` in normal builds. The hook supports two exception forms: - An inline `// dst-ok` marker on a specific line, typically accompanied by a short reason (for example, log-only wall-clock timing that does not affect state). - A small file-level allowlist in the hook script itself for sites classified as leave-alone in the codebase audit (log timing in the cache module, log-record timestamping in the logging bridge and writer, progress reporting in the DeFi module). The hook excludes: - Files under `tests/`, `python/`, and `ffi/` directories. - Files named `tests.rs`, `*_test.rs`, or `*_tests.rs`. - Lines inside an inline `#[cfg(test)]` module. These paths are not part of the production DST contract. ### In-scope crates The transitive closure of `nautilus-live` contains 16 in-scope crates: - `analysis` - `common` - `core` - `cryptography` - `data` - `execution` - `indicators` - `live` - `model` - `network` - `persistence` - `portfolio` - `risk` - `serialization` - `system` - `trading` The hook also covers `backtest`, bringing the total to 17 crates. Adapter crates and infrastructure crates (Redis, Postgres) are out of scope unless an audited slice is listed here. Audited OKX DST-path production files route state-affecting clock reads and timers through the DST seams and sort reconnect and bulk-unsubscribe subscription commands. The static hook covers `book_sync.rs`, `common/task.rs`, `data.rs`, `execution.rs`, `http/client.rs`, `websocket/client.rs`, `websocket/dispatch.rs`, and `websocket/handler.rs` in `crates/adapters/okx/src`. These files also serve paths outside a proven runtime slice: static coverage alone does not establish their runtime eligibility. Focused Madsim tests in `crates/adapters/okx/tests/integration/dst.rs` cover subscribe-wire bytes for public WebSocket quotes, trades, and books, business WebSocket bars, and multi-instrument quote reconnect in topic order. Reconnect also clears quote and funding caches in `data.rs` so a new generation cannot reuse prior values. Complete request-to-wire-to-domain fresh-process comparison stays in the downstream DST harness. Other public channels, private data, and execution share the DST facades and convention gate but remain unproven runtime slices. ## Simulated HTTP and WebSocket transport With `simulation` and `cfg(madsim)`, `nautilus-network` executes plaintext HTTP/1.1 requests and Tungstenite WebSocket connections over Madsim byte streams. Production and simulated HTTP share request defaults, URL and query encoding, and response validation, including buffered body limits and a deadline covering the body read. The simulation branch in `crates/network/src/http/simulation.rs` uses Hyper for the HTTP/1.1 exchange and owns its Madsim connection task. WebSocket traffic uses the Tungstenite codec; `crates/network/src/dst.rs` routes its owned tasks and transport streams to Madsim. Normal builds use pooled Hyper HTTP connections and Tokio tasks. Configure controlled `http://` and `ws://` endpoints and select `TransportBackend::Tungstenite`. Simulation rejects HTTPS/WSS, explicit proxies, and Sockudo before opening a connection. `HttpRedirectPolicy::Follow` is the default: a redirect response with `Location` returns an error instead of following it. `Reject` returns the redirect response to the caller. Sockudo's internal timers require a Tokio runtime. Ambient HTTP proxy settings do not affect the simulated exchange. The model covers HTTP request bytes, WebSocket message payloads, and domain events. It does not promise reproducible WebSocket handshake keys or frame masks, TLS records, OS TCP scheduling, partial writes, socket options, or TCP half-close. In `crates/network/src/dst/stream.rs`, stream shutdown flushes bytes; dropping the stream closes the connection. Combining `simulation`, `cfg(madsim)`, and `turmoil` is a compile error; run the two simulators in separate builds. Simulation sorts request headers by name for reproducible HTTP bytes. This canonical order does not promise byte-for-byte equality with live request header ordering. Each HTTP request opens a fresh connection; connection pooling and keep-alive reuse are not modeled. Network tests cover transport behavior; they do not establish end-to-end conformance for an adapter, product, or channel. ## Network seed soaks [Turmoil](https://crates.io/crates/turmoil) simulates the network under a seeded scheduler. The `nautilus-network` transport tests use it to reach link and reconnect orderings outside the madsim runtime swap. ### Fixed-seed nightly tests The nightly suite runs reproducible scenarios for: - Initial connection. - Reconnection. - Network partitions. - Closing during reconnection. - Closing during backoff. - Repeated server drops with exact message-order assertions. ### Reconnect seed soak An ignored reconnect test sweeps Turmoil seeds until stopped or until the configured limit is reached. For each seed, the soak runs the Tungstenite WebSocket backend first. It then runs Sockudo when the `transport-sockudo` feature is enabled, giving both backends the same schedule search path. | Variable | Default | Effect | | ----------------------------------------- | --------- | --------------------------------------------------- | | `NAUTILUS_TURMOIL_SOAK_START` | `0` | Selects the first seed, allowing a sweep to resume. | | `NAUTILUS_TURMOIL_SOAK_COUNT` | Unbounded | Stops after the specified number of seeds. | | `NAUTILUS_TURMOIL_SOAK_PROGRESS_INTERVAL` | `100` | Logs progress after this many seeds per backend. | ### Run the soak Run the continuous soak with: ```bash scripts/soak-network-turmoil.sh ``` Run a bounded soak with: ```bash env NAUTILUS_TURMOIL_SOAK_COUNT=100 scripts/soak-network-turmoil.sh ``` Each seed enables random node order and link latency from 1 ms to 25 ms. The scenario repeatedly drops the server, cycles the client through reconnect states, and asserts exact application-message order. The soak does not enable Turmoil `fail_rate`. For TCP, packet loss without a retransmit model would overstate the client delivery contract in an order-preservation test. ### Platform coverage The Turmoil tests use a simulated network and are not gated to Linux, so the seed sweep also runs on macOS. Several real localhost socket and WebSocket unit tests use `target_os = "linux"` for CI stability. A macOS run therefore leaves that host TCP coverage untouched. Treat the complete network test set as covered only after a run on Linux CI or a Linux workstation. ## Implementation notes These sections map each deterministic seam to its source, purpose, and exceptions. ### Iteration-order seams Production sites use ordered collections or explicit sorting when iteration is observable on the DST path. #### Matching engine `crates/execution/src/matching_engine/mod.rs` uses eleven ordered collections: - `execution_bar_types` - `execution_bar_deltas` - `account_ids` - `cached_filled_qty` - `post_match_order_ids` - `bid_consumption` - `ask_consumption` - `queue_pending` - `queue_ahead_orders` - `queue_ahead_total` - `queue_excess` Removals from `queue_pending`, `queue_ahead_orders`, and `queue_ahead_total` use `.shift_remove()` so deleting an entry does not reorder the remainder. #### Reconciliation manager Rule 5 covers `crates/live/src/execution/manager.rs`. The `orders` and `fills` maps in `ReconciliationResult` use `IndexMap` in `crates/execution/src/reconciliation/types.rs`. #### Account balances The account trait returns `IndexMap` from: - `balances` - `balances_total` - `balances_free` - `balances_locked` - `starting_balances` Balance and margin storage on `BaseAccount` and `MarginAccount` also uses `IndexMap`. The `commissions` and `leverages` fields remain `AHashMap`. #### Position commissions `Position::commissions` in `crates/model/src/position.rs` uses `IndexMap`. Position snapshots consume the map through `.values()` in `crates/model/src/events/position/snapshot.rs`, making its order observable. #### Portfolio aggregation `crates/portfolio/src/portfolio.rs` stores `unrealized_pnls`, `realized_pnls`, and `net_positions` in `IndexMap`. `accumulate_mark_values` builds an `IndexMap`. #### Data engine `crates/data/src/engine/` uses ordered storage for `book_snapshot_counts`, `bar_aggregators`, and `BookSnapshotInfos`. Iterated removals use `.shift_remove()`. #### Execution engine `ExecutionEngine.clients` uses `IndexMap`. The `client_ids` and `venues` accumulators in `get_clients_for_orders()` use `IndexSet`. #### Backtest engine and exchange `BacktestEngine.venues` and `SimulatedExchange.matching_engines` preserve venue and instrument iteration order for: - Settlement. - Expiration. - Liquidation. - Seeded `FillModel` draws. The source locations are `crates/backtest/src/engine.rs` and `crates/backtest/src/exchange.rs`. #### Trading algorithm `strategy_event_handlers` in `crates/trading/src/algorithm/core.rs` uses `IndexMap` to drive ordered `msgbus::unsubscribe_*` fan-out. #### Analyzer `account_balances` and `account_balances_starting` in `crates/analysis/src/analyzer.rs` use `IndexMap`. #### Cache API `get_orders_for_ids` and `get_positions_for_ids` in `crates/common/src/cache/mod.rs` sort returned vectors by `client_order_id` and `position_id`. The underlying storage keeps `AHashSet` because it has set semantics. #### Instrument store `InstrumentStore.instruments` in `crates/common/src/providers.rs` uses `IndexMap` with the `ahash` hasher. The order is observable because these adapters publish one `DataEvent::Instrument` per entry from `get_all()` or `list_all()`: - Betfair. - Derive. - Polymarket. #### Order emulator `on_reset` in `crates/execution/src/order_emulator/emulator.rs` sorts three drained sets before ordered `msgbus::unsubscribe_*` fan-out: - `subscribed_quotes` - `subscribed_trades` - `subscribed_strategies` The quote and trade paths also advance the seeded `UUID4::new` draw sequence. Storage remains on `AHashSet`. #### WebSocket subscriptions `topics_from_map` in `crates/network/src/websocket/subscription.rs` sorts its returned vector to preserve reconnect replay order behind `all_topics()` while storage remains on `DashMap` with `AHashSet` values. #### Unordered collection limits `AHashMap` / `AHashSet` sites in the `nautilus-live` closure are lookup-only, behind concurrent shared-ownership wrappers (`Arc`, `AtomicMap`), or feed into commutative aggregation. `backtest` contains additional hash collections outside rule 5's two-file enforcement scope, including pre-run validation and result maps. Treat their iteration order as outside the static guarantee until each path is audited. ### Time seams Remaining `Instant::now` and `SystemTime::now` sites are tests, file-allowlisted locations, or marked exceptions: | Location | Use | Treatment | | ------------------------------------ | -------------------------------------------------------- | ---------------------------------------------------------- | | `crates/common/src/testing.rs` | `wait_until` and `wait_until_async` timers. | Inline `// dst-ok`: test controls use real time by design. | | `crates/execution/src/engine/mod.rs` | Initialization log timing in `load_cache`. | Inline `// dst-ok`: timing does not affect DST state. | | `crates/common/src/cache/mod.rs` | Timing in `check_integrity` and `audit_own_order_books`. | File allowlist. | | `crates/model/src/defi/reporting.rs` | Progress logging. | File allowlist. | | `crates/core/src/time.rs` | Wall-clock seam definition. | Explicit seam exception. | `jiff::Timestamp::now` and `jiff::Zoned::now` are hook-banned in the in-scope crates. The remaining timestamp call sites are the logging bridge and writer, scoped out under [Logging runs on real OS threads](#logging-runs-on-real-os-threads). `crates/core/src/datetime.rs::is_within_last_24_hours` routes through `nautilus_core::time::nanos_since_unix_epoch()` and compares in `u64` nanos directly. ### Randomness seams Production randomness on the DST path uses the following seams. #### UUID generation `crates/core/src/uuid.rs::UUID4::new()` uses `madsim::rand::thread_rng()` inside a madsim runtime under simulation. Normal builds and plain `#[rstest]` bodies outside a madsim runtime use `rand::rng()`. Production simulation paths run inside the runtime and consume seeded bytes. Order and event factories in `nautilus-common` and `nautilus-risk` reach this seam. #### Fill model randomness `crates/execution/src/models/fill.rs::default_std_rng()` follows the same runtime split. `ProbabilisticFillState::new()` calls it when no seed is provided. When a caller supplies a seed, `StdRng::seed_from_u64` is deterministic without the seam. #### Random matching-engine IDs The `use_random_ids` path in `crates/execution/src/matching_engine/ids_generator.rs` calls `nautilus_core::UUID4::new()` for position and venue order IDs. The default ID scheme, `{venue}-{raw_id}-{count}`, is deterministic without random bytes. #### Reconnect jitter `crates/network/src/backoff.rs` samples `madsim::rand::thread_rng()` inside the simulation runtime. Normal builds and calls outside a Madsim runtime use `rand::rng()`. Restarting the runtime with the same seed restarts the simulated jitter sequence. ### Tokio submodule split The common facade routes `time`, `task`, `runtime`, and `signal` through `madsim`. Direct uses of Tokio's `sync`, `io`, `fs`, and `net` submodules, plus `select!`, stay on real `tokio` under simulation. The network-local `dst::net` boundary separately selects Madsim byte streams for the supported HTTP and WebSocket paths. A global Tokio network replacement would also affect dependencies such as: - `tokio-tungstenite` - `tokio-rustls` - `hyper-util` - `hyper-rustls` The network-local boundary avoids that global replacement by supplying a stream to Hyper and Tungstenite. TLS remains outside its simulation scope. The in-scope direct uses are: | Location | Real Tokio surface | Boundary | | ------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------ | | `crates/network/src/net.rs` | `tokio::net::{TcpListener, TcpStream}` | Re-exports the normal transport behind the `crate::net` seam. | | `crates/network/src/socket/client.rs` | `tokio::io::{AsyncReadExt, AsyncWriteExt}` | Performs I/O on the selected transport. | | `crates/network/src/tls.rs` | `tokio::io::{AsyncRead, AsyncWrite}` | Defines TLS I/O bounds. | | `crates/network/src/socket/types.rs` | `tokio::io::{ReadHalf, WriteHalf}` | Splits `MaybeTlsStream`; `TcpStream` comes from `crate::net`. | These paths use real sockets even under simulation. The `tokio::sync` channel implementation also remains real, but madsim schedules its sender and receiver tasks. Channel delivery order therefore remains part of the deterministic scheduling contract. ### Raw thread escape rules Rule 4 of the hook bans raw thread spawning outside three escape cases: - `#[cfg(test)]` test modules. - `#[cfg(not(madsim))]` or `#[cfg(not(all(feature = "simulation", madsim)))]` production sites (for example, the logging writer thread). - An inline `// dst-ok` marker. `tokio::task::LocalSet` and `tokio::task::spawn_blocking` are not supported under `madsim`. The codebase audit found no production sites for either inside the in-scope crates; new sites must carry a cfg gate or `// dst-ok` marker. ### Logging tests under simulation The logging writer thread is cfg-gated out under simulation; under `cfg(madsim)` log events are dropped. Tests that initialize the file-logging writer would either hang or assert against an empty log file, so the affected submodules are gated out at the module boundary: - `crates/common/src/logging/logger.rs::tests::serial_tests`. - `crates/common/src/logging/macros.rs::tests`. `logger.rs::tests::sim_tests::test_init_under_madsim_skips_writer_thread_and_forces_bypass` runs under simulation and pins the gated behavior. ## Scope boundaries The contract is limited by the boundaries below. Each subsection identifies behavior that can vary between runs or remains outside the audited DST path. ### Python and FFI are not in DST scope DST runs under a native Rust test harness and does not start a Python interpreter. The contract excludes: - PyO3 bindings under `crates/*/src/python/`. - Rust FFI modules under `crates/core/src/ffi/` and `crates/model/src/ffi/`. - The Python package under `python/nautilus_trader/`. Code reachable only through these bindings is out of scope. Any Rust path reachable from the native DST harness must satisfy the contract, even when the same type is also exported through a binding. The `check-dst-conventions` hook encodes this policy by skipping `/python/` and `/ffi/` paths in the in-scope crates. Clock, RNG, and threading calls behind those paths do not apply to the contract. DST primarily covers the order lifecycle, reconciliation, matching, risk, and execution state machines in the Rust engine. User strategies are replayable only when written in Rust or driven through a Rust-native test harness. A Python strategy can vary its command stream by: - Calling `time.time()`. - Issuing arbitrary network requests. - Relying on OS thread scheduling. The Rust core processes that command stream according to its deterministic contract, but DST does not guarantee end-to-end replay from a Python entry point. ### Platform-scoped `madsim`'s libc overrides for `clock_gettime` and `getrandom` are platform-specific. The contract does not claim cross-platform bitwise reproducibility. A seed that reproduces a failure on Linux x86_64 may not reproduce it on macOS aarch64. ### Non-aliased dependencies escape silently A dependency escapes the simulator without an error when it reaches the OS through: - Direct `libc` calls. - A `std::net` bypass. - Unrouted randomness such as `fastrand` or `OsRng`. The in-scope crates have been audited. Adapter and infrastructure crates require separate audits before entering the DST path. ### Transport scope limits The following dependencies use real `tokio` internally: - `tokio-tungstenite` - `tokio-rustls` - `hyper-util` - `hyper-rustls` - `redis` - `sqlx` The [simulated HTTP and WebSocket transport](#simulated-http-and-websocket-transport) supplies Madsim streams to the supported plaintext paths. Raw socket clients, TLS, Redis, and SQL remain outside that boundary. Turmoil provides the separate network simulation described in [Network seed soaks](#network-seed-soaks). The following test modules drive real localhost sockets and are cfg-gated out under `all(feature = "simulation", madsim)`: - `crates/network/src/http/client.rs::tests` - `crates/network/src/http/tests.rs` - `crates/network/src/socket/client.rs::tests` - `crates/network/src/socket/client.rs::rust_tests` - `crates/network/src/websocket/client.rs::tests` - `crates/network/src/websocket/client.rs::rust_tests` - `crates/network/tests/integration/websocket_proxy.rs` Their production paths reach madsim time primitives through `dst::time::*`, which panic when called from a `#[tokio::test]` runtime. The retry modules in `crates/network/src/retry.rs` run under both runtimes. Their test attributes switch between `#[tokio::test(start_paused = true)]` and `#[madsim::test]`; time reads and sleeps use `crate::dst::time`; explicit virtual-time advances use a cfg-gated `advance_clock` function. The same test bodies therefore cover normal and simulation builds. ### Signal handling `nautilus_common::live::dst::signal` exposes routed `ctrl_c` and `terminate` re-exports. The run loop in `crates/live/src/node/mod.rs` uses them. Under `cfg(madsim)`, tests can inject node shutdown through `madsim::runtime::Handle::send_ctrl_c`. Adapter binary entry points still call `tokio::signal::ctrl_c` directly and remain out of scope. ### Logging runs on real OS threads The logging subsystem spawns a writer thread via `std::thread::Builder` and uses `std::sync::mpsc`. Under simulation, the thread is not spawned and log events are dropped. Log output is outside the determinism contract: the writer only writes, never reads or mutates simulation state. ### Adapters End-to-end adapter behavior remains outside the upstream simulation test scope. Depending on the adapter, unaudited paths contain: - Direct `jiff::Timestamp::now` or `jiff::Zoned::now` calls. - Direct `SystemTime::now` calls. - Unrouted RNG calls. - Raw transport clients. An adapter must be audited for these sites before the DST contract covers its behavior. ## Relationship to other testing layers DST complements existing testing; it does not replace any of it. | Layer | Covers | DST relationship | | ----------------------- | ---------------------------------------------------- | ----------------------------------------------- | | Unit tests | Pure logic, calculations, parsers, transformers. | Unchanged. | | Integration tests | Component interaction, I/O boundaries. | Unchanged. DST runs alongside, not in place of. | | Property-based tests | Invariants over input domains (parsers, roundtrips). | Unchanged. | | Acceptance tests | End-to-end backtest and live scenarios. | Unchanged. | | Deterministic sim (DST) | Async timing, scheduling, recovery correctness. | Adds seed-replayable exploration. | DST covers async concurrency and state-machine correctness. Representative failures include: - A shutdown message dropped under one task wakeup order. - A reconciliation event lost when iteration order changes. The other testing layers retain responsibility for their existing scopes. ## Status ### Runtime swap Layer 1 is implemented. `nautilus_common::live::dst` exposes routed re-exports for `time`, `task`, `runtime`, and `signal`. Production call sites for `time`, `task`, and `runtime` use the seam. Signal adoption remains partial; see [Signal handling](#signal-handling). ### Nondeterminism substitution Layer 2 is implemented across the 17 in-scope crates. Seams cover wall-clock time, monotonic time, randomness, and observable iteration order. [Implementation notes](#implementation-notes) lists the audited paths and remaining exceptions. ### Static enforcement status `check-dst-conventions` runs in pre-commit and CI. It covers the load-bearing structural conditions and permits reviewed per-line exceptions through `// dst-ok`. ### Runtime verification limit Verification covers seam tests and static checks. This repository does not compare complete adapter runs across fresh processes. ### Simulation smoke gate The nightly workflow and local pre-flight use the same DST targets: | Entry point | Relevant order | Purpose | | ------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------- | | `.github/workflows/nightly-tests.yml` | `check-code-sim` > `cargo-test-sim` | Runs the nightly and manually dispatched DST smoke gate. | | `make pre-flight` | `check-code-sim` > `cargo-test-sim` > `cargo-test-extras` | Fails early on DST lint before the Rust test suites. | `check-code-sim` runs pinned stable Clippy with `--features simulation` and `cfg(madsim)` across `nautilus-common`, `nautilus-core`, `nautilus-event-store`, `nautilus-network`, `nautilus-execution`, and `nautilus-live`. A separate `--no-default-features` leg compiles and lints the OKX adapter without enabling OKX's default `high-precision` feature in the standard-precision core leg. `cargo-test-sim` uses three feature-coherent nextest invocations: | Precision | Packages | Features | Selection | | --------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------- | | Standard | `nautilus-common`, `nautilus-core`, `nautilus-event-store`, `nautilus-network`, `nautilus-execution`, `nautilus-live` | `simulation` | All compatible common, event-store, network, and execution tests; focused live and core tests. | | Standard | `nautilus-okx` | `simulation` | Integration `dst` tests only, without OKX's default `high-precision` feature. | | High | `nautilus-common`, `nautilus-execution` | `simulation,high-precision` | All tests in both packages. | Nextest compiles the selected library and test targets, so the gate does not run a separate Cargo build. The invocations resolve each feature set once across their package sets. Together they exercise seam-routed `QuantityRaw` (`u64` / `u128`) and `PriceRaw` (`i64` / `i128`) paths at both fixed-point widths. #### Common tests The standard-precision run executes all simulation-compatible `nautilus-common` tests. Its feature graph propagates `nautilus-core/simulation`, selecting the `wall_clock_now` cfg branch throughout the suite. Plain `#[rstest]` bodies run outside a madsim runtime and use the seam's `SystemTime::now()` fallback. This is the same path madsim's libc shim takes outside a runtime. The `LiveClock` test module is cfg-gated out because its plain `#[rstest]` cases start `LiveTimer` tasks without a madsim runtime, and most wait for wall-clock progress. `live::dst::tests::test_dst_wall_clock_advances_with_virtual_time` runs under `#[madsim::test]` and asserts that `nanos_since_unix_epoch` advances with `madsim::time::sleep`. This pins virtual wall-clock behavior inside the runtime. #### Event store tests The standard-precision run executes all simulation-compatible `nautilus-event-store` tests. It exercises the synchronous event and marker writers under `cfg(madsim)`, including deterministic sequence ordering. Tests that depend on blocking OS threads retain native coverage and are gated out because those threads run outside madsim's scheduling control. The event-store smoke lane compiles and tests its existing simulation implementation without extending the static convention hook to event-store production code. #### Live startup reconciliation The focused `nautilus-live` regression runs under madsim. It verifies that a pending mass-status request reaches its configured timeout, reports the expected error, and cleans up the node without entering a real Tokio timer. #### Network tests The run executes all `nautilus-network` tests except transport-bound modules cfg-gated out at the source. Coverage includes virtual-time seam tests for sleep, timeout, and the rate limiter, plus the retry suites that exercise backoff timing. `crates/network/tests/simulation.rs` adds WebSocket reconnect, unsupported endpoint and Sockudo rejection, and jitter reset checks. Unit tests in `crates/network/src/http/simulation.rs` cover request bytes, response limits, body deadlines, cancellation, redirect policy, and HTTPS/proxy rejection. `#[madsim::test]` uses a varying seed by default and reports it on failure. Set `MADSIM_TEST_SEED` to replay a schedule. The `dst smoke (cfg madsim)` job in `.github/workflows/nightly-tests.yml` selects five consecutive seeds from `GITHUB_RUN_NUMBER * MADSIM_TEST_NUM`. The jitter runtime-reset test pins its own seed and complements the cross-seed network checks. #### Execution tests The run executes all `nautilus-execution` tests. These plain `#[rstest]` cases exercise cfg-gated branches in the matching engine, fill model, and execution engine without entering a madsim runtime. `default_std_rng()` therefore takes its host-RNG fallback in these tests. #### Core seam tests The focused `nautilus-core` selection pins `wall_clock_now` against virtual time. #### OKX adapter tests The standard-precision OKX leg runs the integration `dst` tests under `simulation` without the crate's default `high-precision` feature. Those `#[madsim::test]` cases cover public WebSocket quotes, trades, and books, business WebSocket bars, and multi-instrument quote reconnect order. #### Overall gate coverage `#[madsim::test]` cases in `nautilus-common`, `nautilus-core`, `nautilus-network`, `nautilus-live`, and `nautilus-okx` provide deterministic-scheduler coverage. The complete gate catches drift in the cfg-gated seams but does not verify end-to-end adapter determinism. ## Further reading - `.pre-commit-hooks/check_dst_conventions.sh` defines the seven enforcement rules in full and documents the `// dst-ok` marker convention. - [FoundationDB testing philosophy](https://apple.github.io/foundationdb/testing.html). - [TigerBeetle simulation testing blog posts](https://tigerbeetle.com/blog/). - [madsim repository](https://github.com/madsim-rs/madsim), the deterministic runtime. - [Turmoil repository](https://github.com/tokio-rs/turmoil), the deterministic network simulator. # Event Sourcing Source: https://nautilustrader.io/docs/latest/concepts/event_sourcing/ Event sourcing gives NautilusTrader a durable, ordered record of the messages that change engine state. The event store records those messages at the system boundary, then readers, replay tools, and verifiers use the same log to reconstruct what happened and to rebuild state. **The core philosophy**: - The event store is the durable authority for state-affecting history. - The cache is a write-through projection, not the source of truth. - Cache replay rebuilds state by applying captured history to cache-owned state. - Market data stays in the data catalog; the event store records the messages that affect state. - External I/O becomes replayable only when Nautilus captures it as commands, raw reports, or other state-affecting inputs. :::note Event-store capture, replay, verification, recovery, and retention planning have targeted test coverage, but the API surface is still evolving. Treat this page as the design contract, and the [`nautilus-event-store` README](https://github.com/nautechsystems/nautilus_trader/blob/master/crates/event_store/README.md) plus [docs.rs](https://docs.rs/nautilus-event-store) as the API reference. ::: ## Why event sourcing The cache answers "what is true now". The event store answers "how did Nautilus get here". It gives readers, replay tools, and verifiers a run-scoped history that does not require strategy logic, venue queries, or the live cache to explain past state. The event store provides Nautilus with a durable basis to: - Prove whether a sealed run is clean before replay or archive. - Inspect the exact command, report, and event sequence behind an order or component intent. - Rebuild cache state from captured history, including a snapshot anchor plus the run tail. - Trace an intent through the engine-side messages that followed from it. - Seal stale run files before the next run starts after a process exit or writer halt. ## Terms - **Run**: one kernel session for one instance, binary, and config. - **Entry**: one captured message plus replay metadata. - `seq`: the per-run sequence assigned by the writer and used as replay order. - **High-watermark**: the largest `seq` durably acknowledged by the backend. - **Snapshot anchor**: the high-watermark recorded with a cache snapshot. - **Headers**: correlation and causation metadata propagated with captured messages. ## What the store records The event store records state-affecting message bus traffic for one trading instance and one run. A run starts when the kernel starts and ends when the process stops cleanly or crashes. **Captured entries include**: - Execution commands such as submit, modify, and cancel. - Data subscription commands that define the actor or strategy observation window. - Fired time events and generated order, position, and account events. - Raw venue execution reports before reconciliation synthesizes derived events. - Reconciliation outputs produced from those raw reports. - Request and response messages, or their audit-relevant metadata, that cross the bus and affect state. - Run lifecycle entries such as `RunStarted` and `RunEnded`. Streamed market-data observations stay in the data catalog. The event store records the command stream, raw reports, generated events, and metadata needed to replay how the engine reacted to that world. Data responses are the exception: every response to an engine request is captured, including book, option-chain reference price, and custom-data responses. Only some of them, listed under [Cache replay](#cache-replay), carry a rule that applies them back to cache state; the rest are inspection records. ## Boundaries The event store is intentionally narrow: - It does not replace the data catalog. - It does not provide analytics or OLAP queries. - It does not aggregate multiple trader instances into a consensus log. - It does not yet define redaction, encryption-at-rest, or tamper evidence. ## Capture flow Capture happens at the message bus dispatch boundary, so the tap sees every state-affecting message before downstream handlers observe it. ```mermaid flowchart LR Producer["Engine, adapter, strategy, or component"] --> Bus["MessageBus publish/send"] Bus --> Tap["Capture tap"] Tap --> Adapter["BusCaptureAdapter"] Adapter --> Writer["EventStoreWriter"] Writer --> Backend["redb run file"] Bus --> Handlers["Downstream handlers"] Backend --> Reader["Reader, replay, verifier"] ``` Capture branches off the same dispatch that feeds downstream handlers, and readers only ever reach the durable backend. Capture is **asynchronous**, not an acceptance gate on dispatch. A successful capture enqueues the entry to the writer; the writer thread then assigns the next `seq`, commits a batch, and advances the high-watermark once the backend acknowledges durability. Readers scan sealed or running backends over a surface that exposes no append operations. The writer takes entries over a bounded channel. Backpressure never silently drops an accepted entry: a submit that stalls past the configured `halt_threshold` fires the halt signal instead. A backend commit failure is different, and does lose the queued batch: the writer fires the halt signal, discards the pending batch, and ends its loop, so those entries never become durable. :::warning Fail-stop does not interrupt the run. The tap logs the failure and the message still reaches its handlers, and once halted the tap stops recording, so the rest of that session runs uncaptured. No runtime component polls the halt signal to stop the trader; the recovery sweep on the next boot is what seals the run, as `CrashedRecovered` when its tail is clean. ::: Some messages legitimately cross more than one tap-visible boundary: the execution engine sends an order event to the portfolio endpoint and publishes the same event on its strategy topic, and trading commands hop from strategy to risk to execution. Duplicate dispatches of one message land within a single engine cycle, so the capture adapter deduplicates against a bounded window of recently captured message identities (event id, command id). Each logical message becomes one entry, and replay does not apply the same event twice. ## Lifecycle options `EventStoreConfig` is the serializable run policy. Process-local construction policy lives in `EventStoreLifecycleOptions`, which advanced callers pass through `EventStoreLifecycle::boot_with_options(...)`. By default the lifecycle opens `RedbBackend` and installs the default encoder and data-marker extractor registries. Lifecycle options replace any of the three: - An encoder registry, or a factory that builds one per run, applied before the bus tap starts capture. - A backend opener that returns any `EventStore` implementation for the new run. - A data-marker extractor registry factory for the configured marker classes. The backend opener is the simulation-safe path for memory capture. A DST harness or focused test can open `MemoryBackend` through the normal lifecycle, keep the same bus tap and writer semantics, and read the captured entries in-process after seal. Under `cfg(madsim)`, the writer commits each submit synchronously, so the captured `seq` order is deterministic. With a `MemoryBackend` opener, capture needs no `redb` run file. ## Entry model Each event-store entry is one captured message plus metadata: - `seq`: the per-run replay-order authority. - `ts_init`: the domain timestamp on the captured message. - `ts_publish`: the bus-accepted or writer-receive timestamp. - `topic`: the bus topic or logical endpoint. - `payload_type`: the encoded message type. - `payload`: the encoded message bytes. - `headers`: correlation and causation metadata. - `entry_hash`: the canonical hash over the entry content. `seq` orders replay. Timestamps help explain the run, but they do not override `seq`. Secondary indices cover lookup by `client_order_id` and `venue_order_id`. A `correlation_id` index can be added when a concrete inspection caller needs that lookup pattern; until then, correlation scans can walk the captured stream. ## Correlation model The target model uses three identity levels so readers can answer scope, lineage, and message identity questions. - `correlation_id`: the logical workflow or chain. - `causation_id`: the direct parent message that caused this message. - `command_id`, `event_id`, or `report_id`: the identity of this specific message. ```mermaid flowchart TD Command["SubmitOrder command_id"] --> Event["OrderAccepted event_id"] Event --> Fill["OrderFilled event_id"] Correlation["correlation_id"] --> Command Correlation --> Event Correlation --> Fill Command -. "causation_id" .-> Event Event -. "causation_id" .-> Fill ``` One `correlation_id` spans the whole workflow, while `causation_id` links each message to its direct parent. :::warning Header propagation is incomplete, so most captured entries carry empty headers today. The default encoder registry registers extractors for trading commands, data commands, and data responses, and those extractors forward whatever the message carries. Of those, only a data request (which contributes its `request_id`) and a data response (which carries a required `correlation_id`) yield a populated header in practice: in-tree trading-command producers construct their commands with `correlation_id` and `causation_id` unset, and order, position, and account events, execution reports, and time events have no extractor at all. Treat the diagram above as the design contract, not as a description of what a captured run contains. ::: Where headers are populated, this lets operators ask two common questions: - "Show everything in this workflow": filter or scan by `correlation_id`. - "Show why this event happened": walk `causation_id` back to the direct parent. ## Run files and manifests The default backend is `redb`. It stores one file per run under: ```text //.redb ``` Each run file contains: - Entries keyed by `seq`. - Secondary indices for order identifiers. - A manifest written at run start and sealed at run end. - An optional snapshot anchor for cache restore. The manifest records the run identity and reproducibility inputs: - Run identity: - `run_id` - `parent_run_id` - `instance_id` - Build identity: - `binary_hash` - `schema_version` - `crate_versions` - `feature_flags` - `adapter_versions` - Configuration identity: - `config_hash` - `registered_components` - `seed` - Lifecycle state: - `start_ts_init` - `end_ts_init` - `high_watermark` - `status` Run status is one of `Running`, `Ended`, `CrashedRecovered`, or `Quarantined`. ## Run lifecycle ```mermaid flowchart TD Start["RunStarted entry"] --> Running["Running manifest"] Running --> Capture["Capture state-affecting entries"] Capture --> Anchor["Record optional snapshot anchors"] Anchor --> Capture Capture --> RunEnded["RunEnded entry"] RunEnded --> Ended["Ended manifest"] ``` A run opens with `RunStarted` and closes with `RunEnded`; snapshot anchors are optional points recorded while the manifest stays `Running`. - `RunStarted` is the first entry of a fresh run. A repeated `open()` in the same process seals the current session before it starts a new run. - While the manifest is `Running`, the bus tap records state-affecting entries and cache snapshots can record anchors against the durable high-watermark. - A clean shutdown, kernel drop, or reset/rerun seal appends `RunEnded` and seals the manifest as `Ended`. - A fail-stopped (halted) session skips the in-process seal; the recovery sweep on the next boot owns it. The halt signal is scoped to the run that fired it: a later `open()` re-arms a fresh signal, so one halt does not poison subsequent runs in the same process. ## Recovery sealing A predecessor is an older run file for the same instance whose manifest still says `Running`. This means the previous process did not finish the normal lifecycle, or the writer halted before the manifest seal completed. ```mermaid flowchart TD Predecessor["Running predecessor"] --> Scan["Scan durable tail"] Scan --> Empty["No durable entries"] Empty --> Recovered["Seal as CrashedRecovered"] Scan --> TailEnded["Tail contains RunEnded"] TailEnded --> Ended["Seal as Ended"] Scan --> CleanTail["Clean tail without RunEnded"] CleanTail --> Recovered Scan --> BadTail["Hash, gap, or structural failure"] BadTail --> Quarantined["Seal as Quarantined"] Recovered --> Parent["Eligible parent_run_id"] Ended --> NoParent["No parent link"] Quarantined --> NoParent ``` Boot recovery scans each `Running` predecessor and chooses a final manifest status from the durable tail: | Durable tail | Sealed status | Eligible parent | | ----------------------------------------- | ------------------ | --------------- | | No entries | `CrashedRecovered` | Yes | | Clean, without `RunEnded` | `CrashedRecovered` | Yes | | Clean, ending in `RunEnded` | `Ended` | No | | Hash mismatch, gap, or structural failure | `Quarantined` | No | The sweep never leaves the trader unbootable because one run file is damaged. A hard-killed process (SIGKILL, OOM kill, power loss) leaves a file that redb refuses to open read-only; the listing falls back to a writable open, which performs redb's repair pass before recovery proceeds. A file that still cannot be opened, or that lacks a manifest, is skipped with a logged error and retried on the next boot, so recovery and retention continue over the healthy runs. Only `CrashedRecovered` predecessors become `parent_run_id`. A configured `replay_from_run_id` overrides a recovered parent after validation. The read-only verifier is separate: it can inspect a sealed run without mutating it and reports `quarantine=not-performed`. ## Replay inputs Replay follows one ordering rule: apply event-store entries in `seq` order. `ts_init` and `ts_publish` explain when messages happened, but `seq` is the durable replay order. The Rust replay-input API keeps planning separate from execution: - Event-store-only replay inputs return entries only. - Catalog-joined replay inputs add caller-selected catalog slices for context analysis. Catalog planners take explicit `CatalogSliceSelector` values and a read-only `ReplayCatalog`. Planning resolves catalog time bounds from the event-store scan unless the selector supplies explicit bounds, reports missing catalog slices, and preserves `seq` as the entry ordering authority. Loading returns `ReplayInputs`: event-store entries in `seq` order plus catalog records grouped under their selected slice. Rust callers can enable the off-by-default `persistence` feature and wrap a `ParquetDataCatalog` with `nautilus_event_store::ParquetReplayCatalog` to plan selected catalog files and filename-derived intervals. The bridge loads `quotes`, `trades`, and `bars` into typed `CatalogReplayRecord` values. :::note The persistence bridge is read-only: it uses catalog discovery and query APIs but **does not write to the catalog**. Unsupported catalog classes fail loading until replay adds a typed payload contract for that class. ::: These APIs **do not**: - Open live venue clients - Run strategies or actors - Re-run reconciliation - Delete files - Replay the clock registration/cancel lifecycle ## Cache replay Kernel-managed replay uses `EventStoreConfig::replay_from_run_id`. When set, the kernel restores cache state from the sealed run, records that run as the parent of the fresh child run, and skips live engines, clients, startup, and venue reconciliation. Quarantined runs are rejected. Replay also requires `load_state=true`: with it disabled the kernel logs an error and returns without restoring the cache or opening a child run. The cache replay loader is **state-only**. It restores the cache-owned snapshot, scans the event-store tail in `seq` order, decodes supported cache-affecting payloads, and applies them directly to `Cache`. Supported payloads include: - Synthesized account, order, and position events - Captured order lists - Complete data responses for instruments, quotes, trades, funding rates, and bars The loader **does not**: - Publish replayed entries to the live message bus - Run strategy or actor code - Query venues - Run reconciliation - Derive identifiers again - Re-arm clocks Fired `TimeEvent`s and raw venue reports are inspection records on this path; replay applies the synthesized order, position, and account events captured later in the run. ## Data marker sidecar :::note The marker sidecar is opt-in via `EventStoreConfig.data_markers` and stays off by default. ::: Exact data delivery order is not inferred from catalog timestamps. The marker sidecar records data observed at the message-bus dispatch boundary, in a file beside the event-store run at `//.markers.redb`, without writing full market-data payloads into `EventStoreEntry` rows. The sidecar supports one audit claim: when marker capture is enabled, Nautilus observed data delivery in `marker_seq` order at the bus boundary for that run, and each marker carries enough identity to join back to candidate catalog rows. It cannot: - Prove that catalog timestamps alone define bus order. - Reconstruct a data point when the catalog row is absent or changed. - Prove venue send order before Nautilus observed the message. - Say anything about runs where marker capture was disabled. - Guarantee that every observed data message produced a marker. The sidecar trades completeness for isolation from the trading path, so it does not inherit the entry writer's backpressure contract. A marker submit that finds the bounded channel full drops the marker rather than stalling the caller or halting the run, and folds its sequence into a gap record: `Overflow` when a later submit flushes it, or `WriterClosed` when the writer closes while the dropped range is still pending. Markers do not consume event-store `seq` values and do not create gaps in the entry table. Each marker has its own monotonically increasing `marker_seq` plus `event_seq_before`, the largest event-store `seq` assigned before the marker was observed. A sealed-run analyzer can derive the next event-store entry after a marker from `event_seq_before + 1`; markers that share the same `event_seq_before` are ordered by `marker_seq`. Event-store `seq` remains the replay-order authority for state-affecting entries. The sidecar has two marker kinds: - **Cursor snapshots** (`DataCursorSnapshot`): the default capture mode. Each snapshot records `marker_seq`, `event_seq_before`, `ts_init`, and the `StreamCursor` entries that advanced since the previous snapshot. A `StreamCursor` carries the stream `slot`, the highest `ts_init` seen in that slot (`ts_init_hi`), and the record `count`. A `StreamDictEntry` maps each `slot` to its `data_cls` (`BookDeltas`, `BookDepth10`, `Quote`, `Trade`, `Bar`) and instrument `identifier`. - **High-fidelity markers** (`HiFiMarker`): opt-in per instrument via `DataMarkerConfig.high_fidelity`. Each records `marker_seq`, `event_seq_before`, `slot`, `ts_event`, `ts_init`, `same_ts_ordinal`, and a 32-byte `record_fingerprint` over the canonical typed row fields. `same_ts_ordinal` and `record_fingerprint` disambiguate duplicate same-timestamp data without storing prices, quantities, sizes, or MessagePack payloads. If two catalog rows are byte-identical for the same key and timestamp, the sidecar can prove that Nautilus observed two deliveries in a specific marker order; it cannot name a unique physical catalog row after catalog compaction rewrites row order. Marker verification proves that the `marker_seq` sequence is fully accounted for, counting recorded gaps as coverage. Read the gap records to find what was dropped. The stable contract is the marker schema, opt-in capture and reader primitives, marker sequence verification, and catalog join rules. Analysis tools can build on that contract to select windows, interpret venue-specific data, rank or cluster markers, present reports, and package run bundles. With marker capture disabled, no data marker writer is installed. Cache replay and live restart do not read this sidecar: snapshot-tail replay still applies event-store entries in `seq` order, and live restart still boots from cache-owned state plus the event-store parent link. ## Snapshot-anchored recovery Cache snapshots are owned by the cache. The event store stores only the snapshot anchor: the high-watermark at snapshot time, an opaque cache-owned `blob_ref` naming the snapshot, and the cache-owned `content_hash` for that blob. ```mermaid sequenceDiagram participant Cache participant Store as Event store participant Replay Cache->>Store: Record snapshot anchor at high-watermark N Replay->>Store: Read manifest and latest anchor Replay->>Cache: Load snapshot blob from anchor Replay->>Store: Scan entries with seq > N Replay->>Replay: Apply tail in seq order ``` Recovery loads the snapshot the anchor names, then applies only the entries after the anchor's high-watermark. Recovery cases are ordered by how far the message progressed: - Before enqueue: the message never reached the writer, so producer retry policy applies. - After enqueue, before commit: the in-flight batch is not durable, so the high-watermark does not advance. - After commit, before snapshot anchor: recovery loads the prior snapshot and replays the tail. - After snapshot anchor: recovery loads the latest snapshot and replays entries after the anchor. :::info Live restart still uses snapshot-plus-reconcile. Event-store recovery becomes the live restart path only after capture coverage and replay rules cover every state-affecting path. ::: Replay correctness depends on four checks: - Entries are addressed by immutable `seq` values. - Writes reject out-of-order commits. - Readers detect gaps inside the high-watermark. - Snapshot replay plans reject anchors that point past the durable high-watermark. ## Retention planning Retention uses whole run files as the reclaim unit. The event store exposes a non-destructive planner that lists sealed run manifests, inspects their latest snapshot-anchor status, and returns candidate run files for a later supervisor or operator process to reclaim. The planner supports three modes: - `Full`: keep every sealed run and return no reclaim candidates. - `Bounded { keep_last }`: keep the newest sealed runs and also keep at least one known-good restore point. - `SnapshotAnchored`: reclaim only sealed runs older than the newest known-good restore point. A known-good restore point is a sealed, non-`Quarantined` run with a valid snapshot anchor whose high-watermark does not exceed the run's durable high-watermark. The planner compares against the last entry actually on disk rather than the manifest's recorded value, so a tail-trimmed run cannot pose as a restore point. `Running` runs are never listed as sealed runs or selected as reclaim candidates. Missing, corrupt, or invalid snapshot anchors do not count as restore points, so the planner returns no candidates when it cannot prove that at least one structurally valid restore point remains. The check stops at the anchor: the planner never loads the snapshot blob, so it cannot rule out a restore that fails on a missing or altered blob. ## Integrity and verification Every entry carries a canonical hash over its full content. Readers and verifiers recompute the hash and report mismatches. The verifier also checks manifest/high-watermark status, validates secondary indices against the entry table, and reports snapshot anchors that fail to decode or point past the durable high-watermark. :::warning A `clean` verdict proves structural integrity, not restorability or capture completeness: - The verifier checks the snapshot anchor but never loads or hashes the blob it names, so a run whose blob is missing or altered verifies clean and fails at restore. The retention planner picks restore points on the same anchor-only evidence. - Marker verification counts recorded gaps as coverage, so a run that dropped markers under backpressure verifies clean. - A run that fail-stopped mid-session verifies clean over what it did capture, and says nothing about the messages that followed the halt. ::: Run verification is process-isolated. This matters because some corrupted `redb` files can panic on open or first read, and release builds use `panic = "abort"`. The verifier runs the scan in a worker subprocess so a bad file aborts the worker, not the caller. Verify a sealed run file: ```bash cargo run -p nautilus-event-store --bin verify -- ./event_store/trader-001/1700000000-cafe0001.redb ``` Clean output looks like: ```text clean run_id=1700000000-cafe0001 status=Ended high_watermark=3 entries_scanned=3 markers=absent ``` Corrupt output includes `quarantine=not-performed`: ```text corrupt run_id=1700000000-cafe0001 status=Ended high_watermark=3 entries_scanned=3 findings=1 marker_findings=0 markers=absent quarantine=not-performed - hash mismatch at seq 2 ``` The `markers=` field reports the sidecar scan. It reads `absent` when no `.markers.redb` sits beside the run file, `clean` or `corrupt` with the scanned snapshot, high-fidelity, gap, and dictionary counts when the sidecar was read, and `error` when a sidecar is present but cannot be opened or scanned. Exit codes: - `0`: the run is clean. - `1`: the run has corrupt findings, or the worker aborted or timed out. - `2`: the verifier could not open or run against the requested file. Increase the worker timeout for a large sealed run: ```bash env NAUTILUS_EVENT_STORE_VERIFY_TIMEOUT_SECS=120 \ cargo run -p nautilus-event-store --bin verify -- ./event_store/trader-001/1700000000-cafe0001.redb ``` Read a sealed run from Rust: ```rust use nautilus_event_store::{EventStoreReader, RedbBackend, ScanDirection}; fn inspect_run() -> Result<(), Box> { let backend = RedbBackend::open_sealed_file("./event_store/trader-001/1700000000-cafe0001.redb")?; let reader = EventStoreReader::new(backend); let high_watermark = reader.high_watermark()?; for entry in reader.scan_range(1, high_watermark, ScanDirection::Forward) { let entry = entry?; println!("{} {}", entry.seq, entry.topic); } Ok(()) } ``` :::note The verifier reports corruption but does not mutate run files. Quarantine is an operator or supervisor policy. ::: ## Verification coverage The event-store test suite pins the load-bearing correctness guarantees for the current alpha surface: - The default encoder registry covers the audited state-affecting capture surface. - Fired `TimeEvent`s hit the installed event-store tap through `TimeEventHandler::run`. - The writer halts under bounded backpressure instead of dropping accepted entries. - Entry hash verification detects byte-level payload corruption. - Process-isolated verification reports truncated or zero-tailed run files as corrupt. - Cache replay reconstructs the same observed account, order, and position state as a live cache for generated captured event streams. - The same order event dispatched across multiple bus boundaries is captured once. - Snapshot anchors that fail to decode or point past the durable high-watermark surface as verifier findings instead of verifying clean. - Catalog-joined replay input planning covers selected slices, missing slices, time bounds, and event-store `seq` ordering. - Crash recovery seals `Running` predecessors as `Ended`, `CrashedRecovered`, or `Quarantined` based on the durable tail, and only `CrashedRecovered` runs become parents. - Boot recovery repairs hard-crashed run files and skips unreadable ones instead of failing the sweep. ## Relationship to DST The event store and [deterministic simulation testing](dst.md) (DST) solve different parts of replay. - The event store supplies the captured input history. - DST controls scheduling, time, seeded randomness, and other in-scope nondeterminism. Together they let a run reproduce engine behavior inside the deterministic simulation scope. The manifest records the inputs that identify such a run alongside the captured log itself: `seed`, `binary_hash`, `config_hash`, and `schema_version`. Under `cfg(madsim)`, the writer commits synchronously instead of spawning its writer thread. When a simulation harness supplies a `MemoryBackend` opener through lifecycle options, capture stays in-process and does not require `redb` files. Redb remains the default durable backend outside that advanced options path. Adapter network I/O remains outside bit-identical replay unless Nautilus captures the relevant raw inputs and routes them through deterministic interfaces. # Greeks Source: https://nautilustrader.io/docs/latest/concepts/greeks/ Nautilus provides two paths for working with option Greeks, which measure how option prices respond to changes in market variables: 1. **Venue-provided Greeks**: real-time Greeks streamed from supported venues through the `OptionGreeks` data type and the option chain aggregation system. 1. **Local Greeks calculator**: `GreeksCalculator` computes Black-Scholes Greeks from cached market data, with support for portfolio aggregation, shock scenarios, and beta weighting. Use either path independently or combine them. Venue-provided Greeks arrive through the data subscription system and require no local computation. The local calculator covers venues that do not stream Greeks, backtesting, and custom adjustments such as shocks, beta weighting, and percent Greeks. ## Venue-provided Greeks ### OptionGreeks The `OptionGreeks` type represents venue-provided sensitivities for a single option contract. It is a Rust-native type exposed to Python through PyO3. | Field | Type | Description | | ------------------ | ------------------ | --------------------------------------------------- | | `instrument_id` | `InstrumentId` | The option contract these Greeks apply to. | | `convention` | `GreeksConvention` | Numeraire convention for the Greeks. | | `delta` | `float` | Rate of change of option price per unit underlying. | | `gamma` | `float` | Rate of change of delta per unit underlying. | | `vega` | `float` | Venue-reported vega. | | `theta` | `float` | Venue-reported theta. | | `rho` | `float` | Venue-reported rho; defaults to zero. | | `mark_iv` | `float` or None | Mark implied volatility. | | `bid_iv` | `float` or None | Bid implied volatility. | | `ask_iv` | `float` or None | Ask implied volatility. | | `underlying_price` | `float` or None | Underlying price at time of calculation. | | `open_interest` | `float` or None | Open interest for the contract. | | `ts_event` | `int` | UNIX timestamp (nanoseconds) of the event. | | `ts_init` | `int` | UNIX timestamp (nanoseconds) when initialized. | Subscribe from an actor or strategy: ```python self.subscribe_option_greeks(instrument_id, client_id=ClientId("DERIBIT")) ``` Handle updates: ```python def on_option_greeks(self, greeks: OptionGreeks) -> None: self.log.info(f"delta={greeks.delta:.4f} gamma={greeks.gamma:.6f}") ``` See the [Options](options.md) guide for the full subscription API, including option chain aggregation, strike range filtering, and snapshot modes. ### Persistence and replay `OptionGreeks` is a native member of the `Data` enum, so it persists to the data catalog and replays in backtests as built-in market data rather than custom data. Use the type-specific catalog methods to write and query it: ```python catalog.write_option_greeks(greeks) # greeks: list[OptionGreeks] greeks = catalog.query_option_greeks() ``` During replay, persisted Greeks reach a subscribed actor or strategy through the same `on_option_greeks` handler used for live data. They also feed option-chain aggregation. When a strategy subscribes to an `OptionChainSlice`, the backtest data engine joins replayed `OptionGreeks` with replayed `QuoteTick` BBO updates for each option instrument. The `underlying_price` field seeds ATM selection, and `delta` supports delta-based strike selection through `StrikeRange.delta(target, tolerance)`. ### Core schema versus custom data The native `OptionGreeks` fields form the core schema: the five standard Greeks (`delta`, `gamma`, `vega`, `theta`, and `rho`) plus implied volatility, underlying price, open interest, and convention. These field names are stable. No single schema covers every Greeks use case. Put venue-specific or model-specific values such as `vanna`, `volga`, `charm`, calibration inputs, or surface metadata in [custom data](custom_data.md), not the native type. Optional venue fields are nullable. `convention` is non-nullable and defaults to `GreeksConvention.BLACK_SCHOLES` in Python. ### Underlying Rust types The core Rust implementation spans `crates/model/src/data/greeks.rs` and `crates/model/src/data/option_chain.rs`: - `OptionGreekValues`: a plain struct with `delta`, `gamma`, `vega`, `theta`, and `rho` fields. Implements `Add` and `Mul` for aggregation. - `OptionGreeks`: wraps `OptionGreekValues` with `instrument_id`, `convention`, implied volatility fields, and timestamps. Implements `Deref` so Rust callers can access Greek fields directly. - `HasGreeks` trait: provides a `greeks()` method returning `OptionGreekValues`. Implemented by `OptionGreeks`, `GreeksData`, `PortfolioGreeks`, and `BlackScholesGreeksResult`. ### Black-Scholes functions Low-level pricing functions from `crates/model/src/data/greeks.rs` are also exposed to Python: ```python from nautilus_trader.model import ( black_scholes_greeks, imply_vol, imply_vol_and_greeks, refine_vol_and_greeks, ) # Compute Greeks given known volatility result = black_scholes_greeks(s=100.0, r=0.05, b=0.0, vol=0.20, is_call=True, k=100.0, t=0.25) # result.delta, result.gamma, result.vega, result.theta, result.price, result.vol # Imply volatility from market price, then compute Greeks result = imply_vol_and_greeks(s=100.0, r=0.05, b=0.0, is_call=True, k=100.0, t=0.25, price=5.0) # Refine volatility from a starting estimate with one Halley iteration result = refine_vol_and_greeks( s=100.0, r=0.05, b=0.0, is_call=True, k=100.0, t=0.25, target_price=5.0, initial_vol=0.18 ) ``` `refine_vol_and_greeks()` performs **one refinement step**, not a full convergence loop. Use it with a good starting estimate; use `imply_vol_and_greeks()` when a full implied-volatility solve is needed. The `BlackScholesGreeksResult` returned by these functions contains: `price`, `vol`, `delta`, `gamma`, `vega`, `theta`, and `itm_prob`. Conventions: - Vega is scaled by 0.01 (sensitivity to a 1 percentage point vol change). - Theta is scaled by 1/365.25 (daily decay). - American-style options are priced as European for Greeks computation. ## Local Greeks calculator ### GreeksCalculator `GreeksCalculator` computes Black-Scholes Greeks from cached market data. It is exposed from `nautilus_trader.common`, uses the cache and clock, and is accessible from actors and strategies. ```python from nautilus_trader.common import GreeksCalculator # Typically created in on_start() calculator = GreeksCalculator(cache=self.cache, clock=self.clock) ``` #### Instrument Greeks Compute Greeks for a single instrument (option or underlying) with quantity of 1: ```python greeks = calculator.instrument_greeks( instrument_id=option_id, flat_interest_rate=0.0425, # used if no yield curve in cache ) # Returns GreeksData or None while market data is warming up. ``` For option instruments, the calculator performs these steps: 1. Look up the instrument and its underlying in the cache. 1. Retrieve prices from the cache. Standard instruments prefer `MID` and fall back to `LAST`; true index instruments prefer the cached index price. 1. Look up yield curves from the cache, falling back to `flat_interest_rate`. 1. Imply volatility from the market price with `imply_vol_and_greeks`. 1. Return a `GreeksData` object with the computed values. Missing prices return `None`, which lets strategies treat warm-up as a normal no-op path. Setup errors such as a missing instrument definition raise a Python exception instead. For non-option instruments such as futures and equities, the calculator returns `GreeksData` with `delta=1` or beta-weighted delta and zero gamma, vega, theta, and rho. Option-specific fields retain their default values. #### Shock scenarios Apply hypothetical changes to spot, volatility, or time: ```python greeks = calculator.instrument_greeks( instrument_id=option_id, spot_shock=10.0, # +10 points on underlying vol_shock=0.02, # +2 percentage points of volatility time_to_expiry_shock=1 / 365.25, # roll forward one calendar day ) ``` #### Volatility update Refine implied volatility from a cached starting point: ```python greeks = calculator.instrument_greeks( instrument_id=option_id, update_vol=True, # use cached vol as starting point cache_greeks=True, # store result for next iteration ) ``` With cached Greeks, `update_vol=True` uses the single-iteration refiner described above. If the cache has no prior Greeks for the instrument, the calculator performs a full implied-volatility solve. #### Beta-weighted Greeks Express delta and gamma in terms of an index: ```python greeks = calculator.instrument_greeks( instrument_id=option_id, index_instrument_id=InstrumentId.from_str("SPX.CBOE"), beta_weights={underlying_id: 1.15}, percent_greeks=True, ) ``` #### Time-weighted vega Normalize vega across different expirations: ```python greeks = calculator.instrument_greeks( instrument_id=option_id, vega_time_weight_base=30, # normalize to 30-day vega ) ``` #### Portfolio Greeks Aggregate Greeks across all open positions matching filter criteria: ```python portfolio = calculator.portfolio_greeks( underlyings=["AAPL", "MSFT"], venue=Venue("CBOE"), strategy_id=StrategyId("DELTA_HEDGE-001"), flat_interest_rate=0.0425, index_instrument_id=InstrumentId.from_str("SPX.CBOE"), beta_weights=beta_dict, percent_greeks=True, ) # Returns PortfolioGreeks. ``` Filters: - `underlyings`: list of symbol prefixes. For example, `["AAPL"]` matches AAPL stock and all AAPL options. - `venue`: restrict to a single venue. - `instrument_id`: restrict to a single instrument. - `strategy_id`: restrict to a single strategy. - `side`: filter by position side, such as `LONG` or `SHORT`. - `greeks_filter`: callable that receives per-position `GreeksData` after `pnl`, `price`, and the Greek values are scaled by signed position quantity; return `True` to include it. ### GreeksData `GreeksData` carries the context of a single instrument's Greeks computation and is exposed from `nautilus_trader.model`. Passing `cache_greeks=True` stores the result in the cache. The Rust `GreeksCalculator` can also publish it to the `data.GreeksData.instrument_id={symbol}` topic; the Python surface does not expose that flag. | Field | Type | Description | | ------------------ | -------------- | ------------------------------------------------------------------- | | `ts_init` | `int` | Initialization timestamp in nanoseconds. | | `ts_event` | `int` | Event timestamp in nanoseconds. | | `instrument_id` | `InstrumentId` | Instrument for the calculation. | | `is_call` | `bool` | `True` for a call or non-option result; `False` for a put. | | `strike` | `float` | Strike price. | | `expiry` | `int` | Expiry date as a `YYYYMMDD` integer. | | `expiry_in_days` | `int` | Days to expiry. | | `expiry_in_years` | `float` | Years to expiry (`expiry_in_days / 365.25`). | | `multiplier` | `float` | Contract multiplier. | | `quantity` | `float` | Quantity, set to 1 by `instrument_greeks()`. | | `underlying_price` | `float` | Underlying price used in the calculation. | | `interest_rate` | `float` | Interest rate used in the calculation. | | `cost_of_carry` | `float` | Cost of carry (`r - dividend yield` when supplied; otherwise zero). | | `vol` | `float` | Implied volatility. | | `pnl` | `float` | PnL relative to the position entry, when a position is provided. | | `price` | `float` | Option model price; non-option position PnL when supplied. | | `delta` | `float` | Delta. | | `gamma` | `float` | Gamma. | | `vega` | `float` | Vega per one percentage point of volatility. | | `theta` | `float` | Daily theta. | | `rho` | `float` | Rho, set to zero by the local calculator. | | `itm_prob` | `float` | In-the-money probability. | Internally, `portfolio_greeks()` multiplies `pnl`, `price`, and the Greek values by each position's signed quantity before adding them to the portfolio result. The intermediate `quantity` field remains `1` and is not part of `PortfolioGreeks`. The calculation **does not apply** the `multiplier` field, and the public Python types do not expose arithmetic operators for this aggregation. Rust callers can apply the same scaling with `quantity * &greeks_data`, which returns `GreeksData` with scaled `pnl`, `price`, and Greek values. ### PortfolioGreeks `PortfolioGreeks` is the aggregated result from `portfolio_greeks()`: The Rust type implements `Add` to combine portfolio results. The Python type does not expose this operator. | Field | Type | Description | | ---------- | ------- | ---------------------------------------------------- | | `ts_init` | `int` | Initialization timestamp in nanoseconds. | | `ts_event` | `int` | Event timestamp in nanoseconds. | | `pnl` | `float` | Aggregate PnL after signed-quantity scaling. | | `price` | `float` | Aggregate model value after signed-quantity scaling. | | `delta` | `float` | Portfolio delta. | | `gamma` | `float` | Portfolio gamma. | | `vega` | `float` | Portfolio vega. | | `theta` | `float` | Portfolio theta. | | `rho` | `float` | Portfolio rho, zero for local calculator results. | ### Yield curves The Python API does not expose the Rust `YieldCurveData` type. Pass `flat_interest_rate` and `flat_dividend_yield` to `GreeksCalculator` methods when Python calculations need rates that differ from the defaults. Rust callers can use `YieldCurveData` for interpolated interest rate or dividend yield curves. ## Choosing between the two paths | Criterion | Venue-provided (`OptionGreeks`) | Local calculator (`GreeksCalculator`) | | --------------------- | ----------------------------------------------------- | --------------------------------------------------------- | | Computation | Done by the venue or broker | Local Black-Scholes | | Latency | Arrives with market data | Computed on demand | | Venues | Bybit, Deribit, Derive, Interactive Brokers, and OKX | Any cached option with required prices | | Shock scenarios | Not supported | Spot, vol, and time shocks | | Portfolio aggregation | Manual, such as iterating an `OptionChainSlice` | Built-in via `portfolio_greeks()` | | Beta weighting | Not supported | Built-in | | Backtest support | Via recorded `OptionGreeks` data | From cached prices at any point in time | | Values | delta, gamma, vega, theta, rho, IV, and open interest | delta, gamma, vega, theta, itm_prob, and vol; rho is zero | | Data type | `OptionGreeks` | `GreeksData` and `PortfolioGreeks` | ## Greek definitions These terms appear across both paths. The local Black-Scholes functions scale vega and theta as described above. `OptionGreeks` retains the values reported by each venue or broker and records their `convention`. | Greek | Field | Definition | | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------- | | Delta | `delta` | First derivative of option price with respect to underlying price (`dV/dS`). | | Gamma | `gamma` | Second derivative of option price with respect to underlying price (`d²V/dS²`). | | Vega | `vega` | Sensitivity to a change in implied volatility (`dV/dVol`). | | Theta | `theta` | Sensitivity to the passage of time (`dV/dt`). | | Rho | `rho` | Sensitivity to a change in the risk-free interest rate (`dV/dr`). | | ITM prob | `itm_prob` | Probability that the option finishes in the money: `P(ϕS_T > ϕK)`, where `ϕ = 1` for calls and `ϕ = -1` for puts. | ## Examples Complete working examples are available in the repository: - `examples/live/bybit/bybit_option_greeks.py`: subscribe to Bybit venue-provided Greeks. - `examples/live/deribit/deribit_option_greeks.py`: subscribe to Deribit venue-provided Greeks. - `examples/live/okx/okx_option_greeks.py`: subscribe to OKX venue-provided Greeks. ## Related guides - [Options](options.md): option instruments, chain subscriptions, and strike filtering. - [Data](data/): built-in data types, custom data, and the subscription model. - [Actors](actors.md): subscription and handler reference. - [Strategies](strategies.md): strategy implementation and handler methods. # Concepts Source: https://nautilustrader.io/docs/latest/concepts/ These guides explain the core components, architecture, and design of NautilusTrader. ## Foundations } /> } /> } /> } /> ## Domain model } /> } /> } /> } /> } /> } /> ## Data } /> } /> } /> } /> ## Execution and portfolio } /> } /> } /> } /> } /> } /> ## Components and runtime } /> } /> } /> } /> } /> } /> ## Running systems } /> } /> } /> } /> } /> } /> } /> } /> :::note If there are discrepancies between these guides and the API reference, the API reference is correct. ::: # Live Trading Source: https://nautilustrader.io/docs/latest/concepts/live/ The same strategy and execution-algorithm code can run across backtest and live environments. Live execution also introduces venue, transport, timing, persistence, external-activity, and reconciliation behavior that a simulation may not reproduce. :::warning **Live trading involves real financial risk.** Before deploying to production, understand system configuration, node operations, execution reconciliation, and the differences between backtesting and live trading. ::: ## Backtest and live differences Backtests advance a controlled clock from historical data and execute orders on simulated venues. A live node shares strategy and execution-algorithm code with a backtest, but coordinates with systems outside the process boundary. - **Venue**: Venue rules and adapter capabilities determine which order types, instructions, and events are available. See [Adapters](adapters.md). - **Transport**: A network failure can leave an order command outcome unknown. See [Command outcomes](execution/policies.md#command-outcomes). - **Timing**: Independent inputs can interleave, and the runner does not define one global FIFO order. See [Dispatch priority](#dispatch-priority-and-overload-behavior). - **Persistence**: Built-in cache backends process writes independently of venue transport, and event-store capture does not gate dispatch on durable commit. See [Persistence before transport](execution/policies.md#persistence-before-transport). - **External activity**: Venue reports can include orders created outside the node. See [External order creation](execution/reconciliation.md#external-order-creation). - **Reconciliation**: Startup and runtime checks align retained local state with venue reports. See [Execution reconciliation](execution/reconciliation.md). ## Live node lifecycle Rust `LiveNode::run()` prepares cached and venue state before starting trader components, then owns the event loop and coordinated shutdown. ```mermaid flowchart TD Build[Configure and build LiveNode] --> Cache[Restore cached state when configured] Cache --> Data[Connect data clients and cache instruments] Data --> Exec[Connect execution clients] Exec --> Recon{Startup reconciliation enabled?} Recon -->|Yes| Align[Fetch venue reports and align state] Recon -->|No| Trader[Start trader components] Align --> Trader Trader --> Run[Run event loop and periodic checks] Run -->|Stop or shutdown request| Stop[Stop trader and process residual events] Stop --> Final[Disconnect clients and finalize] ``` Live node lifecycle: instruments and execution state are prepared before strategies start trading. Cache restoration runs when a backing database is attached and cache loading is enabled. Connection, reconciliation, or trader startup failures abort startup and follow the coordinated cleanup path. ## Hosted event loops Use `run_async()` from Python to run a node on an event loop you already own, such as an ASGI server serving a dashboard beside the node. Use `run()` when the node should own the calling thread and signal handling. This lifecycle sketch leaves node configuration and request serving to the application: ```python import asyncio from nautilus_trader.live import LiveNode from nautilus_trader.live import LiveNodeHandle async def wait_until_running( handle: LiveNodeHandle, task: asyncio.Task[None], ) -> None: while not handle.is_running: if task.done(): await task raise RuntimeError("LiveNode stopped during startup") await asyncio.sleep(0.01) async def serve_with_node(node: LiveNode) -> None: cache, portfolio, handle = node.cache, node.portfolio, node.handle() run_task: asyncio.Task[None] | None = None service_task: asyncio.Task[None] | None = None try: run_task = asyncio.create_task(node.run_async()) await wait_until_running(handle, run_task) service_task = asyncio.create_task(serve_requests(cache, portfolio, handle)) done, _ = await asyncio.wait( (run_task, service_task), return_when=asyncio.FIRST_COMPLETED, ) if run_task in done: await run_task raise RuntimeError("LiveNode stopped while the service was running") await service_task finally: if service_task is not None and not service_task.done(): service_task.cancel() await asyncio.gather(service_task, return_exceptions=True) try: if run_task is not None: handle.stop() await run_task finally: node.dispose() ``` Both entry points run the same lifecycle, so a hosted node performs the same startup ordering, maintenance, reconciliation, and shutdown as an owned one. The mode decides only who owns signal handling: a hosted node installs no handlers, leaving `SIGINT` and `SIGTERM` to the host. ### Access and shutdown `run_async()` returns a coroutine and lends the node to it for the run's duration: - Capture `cache`, `portfolio`, and `handle()` **before starting**. Each stays usable while the node runs, whereas reading state through the node itself raises until the run returns it. - `handle()` works throughout, since it is how a host stops the node. `is_running` also answers throughout because it reads the same handle. - Call `dispose()` **after the run task finishes** to release the node's resources. Calling it during the run returns without doing anything; it does not defer disposal. `LiveNodeHandle` is safe to call from any thread, including a signal handler. `stop()` requests a graceful shutdown and returns immediately, so the awaiting task resolves only once shutdown finishes. Cancelling that task requests the same shutdown, waits for it, then re-raises the cancellation, which keeps `asyncio.timeout` and task groups behaving as their callers expect. ### Host integration and limits Compatibility is tested with the default asyncio loop and uvloop. An ASGI lifespan managed by Uvicorn can apply the same ownership pattern without transferring signal handling to the node. Before an ASGI lifespan reports startup complete, wait until the handle reports `Running` while checking whether the run task has failed. Keep supervising the task after startup, and treat unexpected completion as a service failure. While the node is running, it yields to the host loop periodically, so a burst of events cannot starve the host's own callbacks. Startup and shutdown drain their queues without yielding, so a large instrument load or a shutdown backlog can hold the loop for the length of that drain. :::warning[One LiveNode per process] Run one concurrent `LiveNode` per process. The runner binds its channel senders and message bus into thread-local storage, and other runtime state is process-wide. `run_async()` also rejects a second hosted node on the same event loop. Run additional nodes in separate processes. When an ASGI application lifespan constructs the node, run that application with one worker. Do not use hot reload for live trading because it restarts the worker and its node. Scale HTTP request handling with processes that do not construct a trading node. ::: A node configured with a cache database backing is rejected on a host loop. Those backings wait for their worker task by blocking the calling thread, which stalls the host loop rather than slowing it. Native-only nodes can use `run()` for database backing. Custom Python clients drive asyncio in both launch modes, so neither supports cache database backing. ## Configuration For how config structs handle defaults, `T` vs `Option` semantics, and builder patterns, see the [Configuration](configuration.md) concept guide. For node and execution engine settings, strategy configuration, cache backing, and multi-venue wiring, see the [Configure a live trading node](../how_to/configure_live_trading.md) how-to guide. ## Execution reconciliation For how submit, modify, and cancel commands resolve, see [Command outcomes](execution/policies.md#command-outcomes). At startup, reconciliation aligns cached order and position state with venue reports before trader components start. Continuous checks can then monitor in-flight orders, open orders, positions, and own order books while the node runs. When an adapter declares bounded historical reports, startup reconciliation applies their fill economics only when the report set and retained state prove a coherent position transition. Incomplete or ambiguous history can still recover exact order state without changing positions or portfolio economics. See [Execution reconciliation](execution/reconciliation.md) for configuration, recovery procedures, runtime checks, scenarios, and invariants. ## Rust live runner metrics Rust `LiveNode` exposes primitive runner metrics through `LiveNodeHandle::metrics_snapshot()`. Get the handle from the node before calling `run()`, then poll snapshots from another task and derive rates or utilization from deltas. ```rust use std::time::Duration; use nautilus_common::enums::Environment; use nautilus_live::node::{LiveNode, RunnerMetricsDelta}; let mut node = LiveNode::builder(trader_id, Environment::Live)? // Add clients, actors, and strategies here. .build()?; let metrics_handle = node.handle(); tokio::spawn(async move { let mut prev = metrics_handle.metrics_snapshot(); let mut interval = tokio::time::interval(Duration::from_secs(1)); loop { interval.tick().await; let next = metrics_handle.metrics_snapshot(); let delta = RunnerMetricsDelta::from_snapshots(prev, next); if delta.elapsed_ns == 0 { prev = next; continue; } let elapsed_s = delta.elapsed_ns as f64 / 1_000_000_000.0; let data_event_rate = delta.data_events as f64 / elapsed_s; let data_event_staleness_ns = if next.data_events.last_dispatch_at_ns == 0 { 0 } else { next.elapsed_ns .saturating_sub(next.data_events.last_dispatch_at_ns) }; log::info!( "Runner metrics: data_event_rate={data_event_rate:.0} \ data_event_staleness_ns={data_event_staleness_ns} \ dispatch_utilization={:.6} loop_utilization={:.6} \ mean_dispatch_ns={} data_queue_depth={}", delta.dispatch_utilization(), delta.loop_utilization(), delta.mean_dispatch_ns(), next.data_events.queue_depth, ); prev = next; } }); node.run().await?; ``` ### Snapshot scope and interpretation The snapshot covers `LiveNode::run` channel dispatch after startup, including residual dispatch during the shutdown grace period. It does **not** include startup buffering, startup flushes, or the final post-loop drain. - `dispatch_busy_ns` covers the five dispatch branches. `maintenance_busy_ns` and `external_msgbus_busy_ns` cover non-dispatch loop work. - Queue depths are point samples from the maintenance tick while the node is running, and can be stale during shutdown grace. - Snapshots are lock-free and may not be a consistent cross-field view. Derive rates from successive snapshots with **saturating deltas**. - Counters reset when `LiveNode::run` enters steady state. ## Dispatch priority and overload behavior The live runner's seven internal message channels are separate and unbounded. When several message channels are ready together, the runner polls in this order: 1. Time and system work. 1. Execution events. 1. Execution commands. 1. External message-bus ingress. 1. Data events. 1. Data commands. Events precede commands within the execution and data channel pairs. This polling order keeps a market-data backlog from taking priority over ready execution traffic. It does not define **one global FIFO order** across channels, adapters, or venues. Each selected branch runs to completion before the runner polls again, so a slow handler delays every channel. The runner yields to the host event loop periodically, but yielding does not change channel priority or shorten a slow handler. :::warning[Unbounded queues] Runner channels do not apply producer backpressure, coalesce messages, shed market data, or impose a maximum queue depth. Sustained input above dispatch capacity therefore increases queue depth, latency, and memory use. The runner does not automatically throttle a feed, halt trading, or shut down when a threshold is crossed. ::: Use runner metrics and queue-state events to detect this pressure. The thresholds are operational signals, not service-time guarantees. The application must decide how to alert, reduce input, halt new exposure, or stop the node when pressure persists. ## Queue pressure monitoring `LiveNode` converts runner queue samples into typed state transitions when `LiveNodeConfig.queue_monitor` is set. The monitor is **disabled by default** and publishes no queue-state events while the field is unset. ### Configure thresholds The following example sets the thresholds applied to every monitored runner channel: ```rust tab="Rust" use nautilus_live::config::{LiveNodeConfig, QueueMonitorConfig}; let config = LiveNodeConfig { queue_monitor: Some( QueueMonitorConfig::builder() .queue_depth_trigger(1_000) .queue_depth_clear(500) .mean_dispatch_ns_trigger(250_000) .mean_dispatch_ns_clear(150_000) .build(), ), ..Default::default() }; ``` ```python tab="Python" from nautilus_trader.live import LiveNodeConfig from nautilus_trader.live import QueueMonitorConfig config = LiveNodeConfig( queue_monitor=QueueMonitorConfig( queue_depth_trigger=1_000, queue_depth_clear=500, mean_dispatch_ns_trigger=250_000, mean_dispatch_ns_clear=150_000, ), ) ``` The four values apply to each monitored runner channel: - `time_events` - `exec_events` - `exec_commands` - `data_events` - `data_commands` Each clear threshold must be **lower than its trigger threshold**. Configuration validation rejects equal or inverted thresholds. ### State transitions The live runner evaluates the monitor on its 100 ms maintenance tick, after sampling current queue depths. Queue depth is a point-in-time value. Mean dispatch time uses the messages and dispatch busy time accumulated since the previous metrics snapshot. | Condition | Measure | `Triggered` | `Cleared` | | ------------ | ----------------------------------------------------- | ---------------------------------------------- | -------------------------------------------- | | `Backlogged` | Point-in-time queue depth. | `queue_depth >= queue_depth_trigger` | `queue_depth <= queue_depth_clear` | | `Slow` | Per-channel mean dispatch time for the sample window. | `mean_dispatch_ns >= mean_dispatch_ns_trigger` | `mean_dispatch_ns <= mean_dispatch_ns_clear` | Each channel tracks `Backlogged` and `Slow` independently: - A value between the clear and trigger thresholds retains the prior state, so it does not publish another event. - If both conditions cross on one tick, the node publishes two events. Each condition clears independently. - A sample window with no dispatches does not evaluate `Slow`. The condition retains its prior state until a window contains a dispatch. ### Typed delivery Each transition publishes a fresh `QueueStateChanged` value on `events.system.QueueStateChanged.`, where `` is the Rust variant name, such as `DataEvents` for Python `SystemChannel.DATA_EVENTS`. The event identifies the configured trader, runner channel, condition, and transition state. It also records the queue depth and mean dispatch time at the crossing, a fresh event ID, and event timestamps. Actors subscribe with `subscribe_queue_state(...)` and receive events through `on_queue_state(...)`. The Python API exposes `SystemChannel`, `QueueCondition`, `QueueState`, and `QueueStateChanged` from `nautilus_trader.common`. Publication stays on the in-process typed message bus, and the event has no wire representation for external message-bus streaming. See [Queue pressure state](actors.md#queue-pressure-state) for actor examples. ## Socket transport state ### Publication and routing Actors can observe transport availability for adapters that opt into socket state reporting. `LiveNode` publishes `SocketStateChanged` on `events.system.SocketStateChanged..` with the trader ID, client ID, optional venue, stable endpoint label, state, fresh event ID, and event timestamps. The endpoint label identifies one logical adapter transport without exposing its URL. `LiveNode` sets both timestamps from the kernel clock when it handles the transport's neutral state notification. Adapters send the notification through the runner's system-event channel, separately from market data. The internal channel is not part of queue-pressure monitoring. The client ID and endpoint components percent-encode bytes other than ASCII letters, digits, `-`, and `_`, so dots and wildcard characters in labels cannot change topic matching. Use `subscribe_socket_state` filters to select a client, endpoint, or both without constructing topic names. ### State semantics | Transport event or condition | Publication | | ----------------------------------- | ------------------------------------------------------ | | TCP or WebSocket becomes available. | `Connected`. | | Active transport is lost. | `Disconnected`. | | Connection or retry attempt fails. | No event. | | Deliberate shutdown. | No disconnect event. | | Reconnect attempts are exhausted. | No additional event after the reported transport loss. | `Connected` reports **transport availability**. It does not mean that authentication, subscription replay, or adapter recovery has completed. :::warning Socket state is operational evidence, not an execution-command outcome. A disconnect by itself does not reject, cancel, or resolve an in-flight command; stream updates, queries, or reconciliation provide that evidence under the [command outcome policy](execution/policies.md#command-outcomes). ::: ### Dead-peer detection A connection can stop delivering without closing: a NAT or load balancer drops it with no `FIN` and no `RST`, so writes keep succeeding into the send buffer and nothing surfaces the loss. #### Heartbeat timeout Handler-mode WebSocket and raw TCP clients reconnect on heartbeat timeout. With a configured heartbeat, the default timeout is **three heartbeat intervals**. An explicit `heartbeat_timeout_secs` overrides that window. WebSocket clients reset it on any inbound frame; raw TCP clients reset it when bytes arrive. The client expects the peer to answer heartbeats, so a transport with no heartbeat gets no default window. An explicit timeout can still enable detection without a heartbeat. For WebSocket clients, that window counts all frames, so a keepalive reply refreshes it and a quiet market does not trip it. #### Feed idle timeout An adapter that also needs to detect a feed which stopped flowing while the transport stays healthy sets a separate idle timeout, which only Text and Binary frames refresh. That second window suits a venue which pushes data on a known cadence. Where the venue answers the keepalive with a text payload, its reply refreshes the idle timeout exactly like real data does, so the window means something only when it sits below the heartbeat interval. ### Adapter and actor integration Adapter integrations construct a `SocketStateSink` and set it through the network client's `state_sink` builder option. Publication requires the `LiveNode` runner; the standalone `AsyncRunner` does not publish these events. Actors subscribe with `subscribe_socket_state(...)` and receive events through `on_socket_state(...)`. The Python API exposes `SocketState` and `SocketStateChanged` from `nautilus_trader.common`. Delivery stays on the typed in-process bus; external message-bus streaming and wire formats do not expose these events. ### Endpoint reconnect commands An actor or strategy can call `reconnect_socket(client_id, endpoint)` with an endpoint label from a state event. The runner routes the typed command through the kernel and the engine that owns the registered endpoint. The engine invokes only that transport's reconnect handle. It does not call the containing `DataClient` or `ExecutionClient` disconnect and connect lifecycle. :::note The API is **fire-and-observe**. A successful return means the command passed local validation and was queued. It does not acknowledge kernel acceptance or completed recovery. ::: An accepted request emits `SocketStateChanged` for the selected endpoint: 1. `SocketState.DISCONNECTED` as the transport enters reconnect mode. 1. `SocketState.CONNECTED` after transport recovery. The transport's normal reconnect controller preserves its authentication, subscription replay, and adapter recovery behavior. The kernel logs unknown clients, unsupported clients, unknown or ambiguous endpoints, duplicate requests, disconnecting transports, and closed transports. These rejections emit no socket state change and do not affect another endpoint. Endpoint labels use identifier characters only and never contain raw URLs. ## Shutdown on error Set `LiveNodeConfig.shutdown_on_error=True` so that a Rust error log requests a live node shutdown. The Rust logger records the first `log::error!` emitted after the kernel starts, including error logs from other threads, and the kernel publishes a `ShutdownSystem` command when the live event loop next checks for shutdown. The shutdown request follows the normal live node stop path. The node stops the trader, awaits the post-stop delay, disconnects clients, and stops the engines. It does not abort the process. ```python from nautilus_trader.config import LiveNodeConfig config = LiveNodeConfig(shutdown_on_error=True) ``` The trigger follows these rules: - Error logs suppressed by component filters or logging bypass mode still request shutdown. - A new kernel run clears and re-arms the trigger, so a process can restart a node without reinitializing the logging system. - Shutdown-on-error observes **Rust `log` records**, not Python `logging.error(...)` calls. :::note The per-engine `graceful_shutdown_on_error` option has been removed. Configure shutdown-on-error at the node/kernel level instead. ::: ## Related guides - [Execution reconciliation](execution/reconciliation.md) - State recovery and runtime consistency checks. - [Execution policies](execution/policies.md) - Command delivery, persistence, and recovery boundaries. - [Python](python.md) - Python ownership, runtime, and public API boundaries. - [Configure a live trading node](../how_to/configure_live_trading.md) - Node and engine configuration. - [Run live trading with Rust](../how_to/run_rust_live_trading.md) - Rust node setup and venue connection. - [Adapters](adapters.md) - Venue connectivity. - [Execution](execution/) - Command outcomes and order execution. - [Message bus](message_bus.md) - Typed in-process publish and subscribe behavior. - [Backtesting](backtesting/) - Testing strategies before deployment. # Logging Source: https://nautilustrader.io/docs/latest/concepts/logging/ The platform provides logging for both backtesting and live trading using a high-performance logging subsystem implemented in Rust with a standardized facade from the `log` crate. The core logger operates in a separate thread and uses a multi-producer single-consumer (MPSC) channel to receive log messages. This moves log output I/O off the calling thread. Message arguments are still formatted on the calling thread before the event is queued. Logging output is configurable and supports: - **stdout/stderr writer** for console output - **file writer** for persistent storage of logs :::tip Infrastructure such as [Vector](https://github.com/vectordotdev/vector) can be integrated to collect and aggregate events within your system. ::: ## Architecture The logging subsystem captures events from multiple sources and routes them through an MPSC channel to a dedicated logging thread: ```mermaid flowchart TB subgraph Sources["Log Sources"] PY["Python Logger"] NAUT["Nautilus Rust Components"] LOG["External Rust Libraries
(using log crate)
rustls, etc."] end subgraph Filtering["Filtering"] LF["stdout_level / fileout_level
(LoggerConfig)"] end subgraph Logger["Nautilus Logger"] NL["Logger
(implements log::Log)"] end subgraph Channel["MPSC Channel"] TX["Sender (tx)"] RX["Receiver (rx)"] end subgraph Thread["Logging Thread"] LT["Log Writer"] end subgraph Output["Output"] STDOUT["stdout/stderr"] FILE["Log Files"] end PY --> NL NAUT --> NL LOG --> LF --> NL NL --> TX --> RX --> LT LT --> STDOUT LT --> FILE subgraph Tracing["Tracing Subscriber (optional)"] TRACE["External Rust Libraries
(using tracing crate)
hyper_util, h2, tokio, etc."] EF["RUST_LOG
(EnvFilter)"] FMT["fmt::Layer"] end TRACE --> EF --> FMT --> STDOUT ``` - **Python and Nautilus components**: Log directly through the Nautilus Logger. - **External `log` crate users**: Filtered by `stdout_level`/`fileout_level` in `LoggerConfig`. - **External `tracing` crate users**: When enabled, output goes directly to stdout (separate from Nautilus logging), filtered by the `RUST_LOG` environment variable. - **Logging thread**: All Nautilus log events are sent through an MPSC channel to a dedicated thread, ensuring the main thread isn't blocked by I/O operations. ## Configuration Logging can be configured by importing the `LoggerConfig` object. By default, log events with an `INFO` `LogLevel` and higher are written to stdout/stderr. The following log levels are supported: - `OFF` - Disable logging. - `TRACE` - Most verbose level. - `DEBUG` - Detailed diagnostic information. - `INFO` - General operational messages. - `WARNING` - Potential issues that don't prevent operation. - `ERROR` - Errors that may affect functionality. See the `LoggerConfig` [API Reference](/docs/python-api-latest/common.html#nautilus_trader.common.LoggerConfig) for further details. Logging can be configured in the following ways: - Minimum `LogLevel` for stdout/stderr. - Minimum `LogLevel` for log files. - Maximum size before rotating a log file. - Maximum number of backup log files to maintain when rotating. - Automatic log file naming with date or timestamp components, or custom log file name. - Directory for writing log files. - Plain text or JSON log file formatting. - Filtering of individual components by log level. - ANSI colors in log lines. - Bypass logging entirely. - Print Rust config to stdout at initialization. - Truncate an existing log file on startup (`clear_log_file`). ### Standard output logging Log messages are written to the console via stdout/stderr writers. Set the minimum level with `stdout_level`. ### File logging Log files are written to the current working directory by default. The naming convention and rotation behavior are configurable and follow specific patterns based on your settings. Set the log directory and custom file basename with `FileWriterConfig.directory` and `FileWriterConfig.file_name`. **Log file formats:** - `None` (default) - Plain text format with `.log` extension. - `"json"` - JSON format with `.jsonl` extension, useful for log aggregation tools. For detailed information about log file naming conventions and rotation behavior, see the [Log file rotation](#log-file-rotation) and [Log file naming convention](#log-file-naming-convention) sections below. #### Log file rotation Rotation behavior depends on both the presence of a size limit and whether a custom file name is provided: - **Size-based rotation**: - Set `FileWriterConfig.file_rotate` to a `(max_file_size, max_backup_count)` tuple, such as `(100_000_000, 5)` for 100 MB and five backup files. - When writing a log entry would make the current file exceed this size, the file is closed and a new one is created. - Rotation file names have millisecond resolution. If a rotation resolves to the active path, logging continues to that file, which may briefly exceed the configured maximum size. - **Date-based rotation (default naming only)**: - Applies when `file_rotate` and `file_name` are both unset. - On the first write after each UTC date change (midnight), the current log file is closed and a new one is started, creating one file per UTC day. - **No rotation**: - When `file_name` is set without `file_rotate`, logs continue to append to the same file. - Note: Size-based rotation takes precedence: if both a custom name and size limit are provided, rotation still occurs. - **Backup file management**: - The second value in `file_rotate` limits the total number of rotated files kept. - When this limit is exceeded, the oldest backup files are automatically removed. #### Log file naming convention The default naming convention ensures log files are uniquely identifiable and timestamped. The format depends on whether file rotation is enabled: **With file rotation enabled**: - **Format**: `{trader_id}_{%Y-%m-%d_%H%M%S-%3f}_{instance_id}.{log|jsonl}` - **Example**: `TESTER-001_2025-04-09_210721-521_d7dc12c8-7008-4042-8ac4-017c3db0fc38.log` - **Components**: - `{trader_id}`: The trader identifier (e.g., `TESTER-001`). - `{%Y-%m-%d_%H%M%S-%3f}`: UTC datetime with millisecond resolution. - `{instance_id}`: A unique instance identifier. - `{log|jsonl}`: File suffix based on format setting. **Without size-based rotation (default naming)**: - **Format**: `{trader_id}_{%Y-%m-%d}_{instance_id}.{log|jsonl}` - **Example**: `TESTER-001_2025-04-09_d7dc12c8-7008-4042-8ac4-017c3db0fc38.log` - **Components**: - `{trader_id}`: The trader identifier. - `{%Y-%m-%d}`: Date only (YYYY-MM-DD). - `{instance_id}`: A unique instance identifier. - `{log|jsonl}`: File suffix based on format setting. - **Note**: With default naming and no size limit, logs rotate daily at UTC midnight. **Custom naming**: If `file_name` is set (e.g., `my_custom_log`): - With rotation disabled: The file will be named exactly as provided (e.g., `my_custom_log.log`). - With rotation enabled: The file will include the custom name and timestamp (e.g., `my_custom_log_2025-04-09_210721-521.log`). ### Component log filtering The `component_levels` parameter sets log levels for individual components. The input value should be a dictionary of component ID strings to log level strings: `dict[str, str]`. Below is an example of a trading node logging configuration that includes some of the options mentioned above: ```python from nautilus_trader.common import LogLevel from nautilus_trader.config import FileWriterConfig from nautilus_trader.config import LoggerConfig from nautilus_trader.config import LiveNodeConfig from nautilus_trader.model import TraderId config_node = LiveNodeConfig( trader_id=TraderId.from_str("TESTER-001"), logging=LoggerConfig( stdout_level=LogLevel.INFO, fileout_level=LogLevel.DEBUG, component_levels={"Portfolio": "INFO"}, file_config=FileWriterConfig(file_format="json"), ), ) ``` For backtesting, the `BacktestEngineConfig` class can be used instead of `LiveNodeConfig`, as the same options are available. ### Environment variable configuration The `NAUTILUS_LOG` environment variable provides an alternative way to configure logging using a semicolon-separated spec string. This is useful for Rust-only binaries or when you want to override logging settings without modifying code. ```bash export NAUTILUS_LOG="stdout=Info;fileout=Debug;RiskEngine=Error;is_colored" ``` **Supported keys:** | Key | Type | Description | | --------------------- | --------- | ------------------------------------------------ | | `stdout` | Log level | Maximum level for stdout output. | | `fileout` | Log level | Maximum level for file output. | | `is_colored` | Flag | Enable ANSI colors (default: true). | | `print_config` | Flag | Print config to stdout at startup. | | `log_components_only` | Flag | Only log components with explicit filters. | | `` | Log level | Component-specific level (exact match). | | `` | Log level | Module-specific level (prefix match, Rust only). | Flags are enabled by their presence in the spec string (no value needed). Log levels are case-insensitive: `Off`, `Trace`, `Debug`, `Info`, `Warn`, `Error`. :::note For Rust-only binaries, the logging subsystem initializes lazily on first use. Setting `NAUTILUS_LOG` configures it without requiring explicit `init_logging()` calls. ::: ### Components-only logging When focusing on a subset of noisy systems, enable `log_components_only` to log messages only from components listed in `component_levels`. All other components are suppressed regardless of the global stdout or file level. Example (Python configuration): ```python logging = LoggerConfig( stdout_level=LogLevel.INFO, component_levels={ "RiskEngine": "DEBUG", "Portfolio": "INFO", }, log_components_only=True, ) ``` If configuring via the environment using the Rust spec string, include `log_components_only` alongside component filters, for example: ```bash export NAUTILUS_LOG="stdout=Info;log_components_only;RiskEngine=Debug;Portfolio=Info" ``` ### Module path filtering (Rust only) When using the `NAUTILUS_LOG` environment variable, you can filter by Rust module paths in addition to component names. Keys containing `::` are treated as module path filters with prefix matching, while keys without `::` are component filters with exact matching. ```bash # Filter all OKX adapter modules to Warn, but allow Debug for the websocket modules export NAUTILUS_LOG="stdout=Info;nautilus_okx::=Warn;nautilus_okx::websocket=Debug" ``` The longest matching prefix takes precedence. In the example above, `nautilus_okx::websocket::handler` would use the `Debug` level (longer prefix), while `nautilus_okx::data` would use `Warn`. :::tip Rust log macros automatically capture the module path when no explicit component is provided. This enables module-level filtering to work with standard logging calls. ::: :::note Module path filtering is only available via the `NAUTILUS_LOG` environment variable. The Python `component_levels` configuration uses component name matching only. ::: :::warning If `log_components_only=True` (or `log_components_only` is present in the spec string) and `component_levels` is empty, no log messages will be emitted to stdout/stderr or files. Add at least one component filter or disable components-only logging. ::: ### Log colors ANSI color codes improve log readability in terminals. In environments that do not support ANSI color rendering (such as some cloud environments or text editors), these color codes may not be appropriate as they can appear as raw text. Set `LoggerConfig.is_colored=False` for these environments. ## Using a logger directly It's possible to use `Logger` objects directly, and these can be initialized anywhere (very similar to the Python built-in `logging` API). If you ***aren't*** using an object which already initializes a `NautilusKernel` (and logging) such as `BacktestEngine` or `LiveNode`, then you can activate logging in the following way: ```python from nautilus_trader.common import init_logging from nautilus_trader.common import Logger from nautilus_trader.common import LogLevel from nautilus_trader.core import UUID4 from nautilus_trader.model import TraderId log_guard = init_logging( trader_id=TraderId.from_str("TESTER-001"), instance_id=UUID4(), level_stdout=LogLevel.INFO, ) logger = Logger("MyLogger") ``` See the [`init_logging` API Reference](/docs/python-api-latest/common.html) for further details. **Keep the returned `LogGuard` alive** for as long as direct logging is needed. The logging subsystem supports up to 255 concurrent guards. ## LogGuard: managing log lifecycle `init_logging` returns a `LogGuard` that tracks one user of the process-global logging subsystem. `BacktestEngine` and `LiveNode` own their guards internally, so application code does not need to acquire a guard from an engine or node. ### Reference counting implementation The logging system uses reference counting to track active `LogGuard` instances: - **Counter increments**: When a new `LogGuard` is created, an atomic counter is incremented. - **Counter decrements**: When a `LogGuard` is dropped, the counter is decremented. - **Last guard**: When the counter reaches zero, pending file logs are flushed and synced. The process-global logging thread stays available for later guards. - **Maximum guards**: The system supports up to 255 concurrent `LogGuard` instances. Attempting to create more raises a `ValueError` from `init_logging`, or a `RuntimeError` from engine or node creation. Abrupt termination can still lose buffered logs. Dispose engines and nodes normally, and retain the guard returned by direct `init_logging` calls until the application no longer needs logging. ## Tracing subscriber for external Rust libraries External Rust crates that use the `tracing` crate can have their log output displayed by enabling the tracing subscriber. This is useful for debugging external dependencies or when integrating custom Rust components (such as feature extractors or adapters) compiled as separate PyO3 extensions. ### Enabling the subscriber Initialize the tracing subscriber directly: ```python from nautilus_trader.common import init_tracing init_tracing() ``` ### Filtering with RUST_LOG The `RUST_LOG` environment variable controls which tracing events are displayed: ```bash # Show debug logs from your crate, warn and above from hyper RUST_LOG=my_feature_extractor=debug,hyper=warn python my_script.py ``` If `RUST_LOG` is not set, the default filter level is `warn`. ### How it works The tracing subscriber uses a `tracing-subscriber` fmt layer with a custom formatter to output directly to stdout. This is separate from the Nautilus logging infrastructure - tracing output uses a Nautilus-aligned format with nanosecond timestamps. Example tracing output: ``` 2026-01-24T05:51:42.809619000Z [DEBUG] hyper_util::client::legacy::connect::http: connecting to 104.18.5.240:443 2026-01-24T05:51:42.810543000Z [DEBUG] hyper_util::client::legacy::pool: pooling idle connection for ("https", api.example.com) ``` **Differences from Nautilus logging:** - Tracing output goes directly to stdout, not through the Nautilus logging thread. - Tracing events are not written to Nautilus log files. - Filtering is controlled exclusively by `RUST_LOG`, independent of `LoggerConfig`. For external libraries that use the `log` crate (such as `rustls`), their events go through the Nautilus logger and are filtered by `stdout_level`/`fileout_level` in `LoggerConfig`. :::tip `RUST_LOG` only affects crates using `tracing`. For crates using `log`, configure verbosity through `LoggerConfig` or the `NAUTILUS_LOG` environment variable (e.g., `NAUTILUS_LOG=stdout=Debug`). ::: :::note The tracing subscriber can only be initialized once per process. A second `init_tracing()` call raises an error. ::: ## Platform-specific considerations ### Windows shutdown behavior On Windows, non-deterministic garbage collection during interpreter shutdown can occasionally delay the final `LogGuard` drop until after interpreter teardown has begun. Dropping the last guard is what flushes and syncs pending file logs, so a delayed drop can result in truncated logs. ## Related guides - [Architecture](architecture.md) - System architecture including logging infrastructure. # Message Bus Source: https://nautilustrader.io/docs/latest/concepts/message_bus/ The `MessageBus` enables communication between system components through message passing. This design creates a loosely coupled architecture where components interact without direct dependencies. The *messaging patterns* include: - Point-to-Point - Publish/Subscribe - Request/Response Messages exchanged via the `MessageBus` fall into three categories: - Data - Events - Commands ## Topic hierarchy Nautilus keeps market data topics under the `data` root. Live data publications use the direct `data....` topics, for example `data.book.deltas.XCME.ESZ24`. When requested, replayed, or workflow-generated data flows over the message bus as topic-addressable data, the `DataEngine` publishes it under `data.pipeline....`. Long requests, grouped requests, and aggregation chains can split, transform, and fan data back in before the parent request completes. These messages are still data messages, but they do not claim the same live ordering and timing semantics as normal real-time publications. For example, book deltas on the pipeline path use `data.pipeline.book.deltas.XCME.ESZ24`. Correlated request responses are delivered through response handlers keyed by correlation ID. The `data.response` topic is a capture channel for response publications, not the pipeline data path. ## Message integrity Once a message is created, **its fields must not be mutated**. This includes container fields such as `params` maps. Components can read a message and derive local state from it, but they must not rewrite the original. Immutable messages keep every consumer seeing the same input, preserve what was true at emission time, and remove a class of shared-state races. Replay, debugging, and audit all depend on messages remaining stable after dispatch. Three ownership rules follow from this: - Caller-supplied request options stay on the message. - Response metadata returned to the caller stays on the response. - Component workflow state (bounded date ranges, grouping state, replay cursors, counters, processing flags) stays in component-owned context keyed by message or request ID. When a component needs a derived message, it creates a new one with the required values instead of rewriting the original. ## Data and signal publishing While the `MessageBus` is a lower-level component that users typically interact with indirectly, `DataActor`, `Strategy`, and `ExecutionAlgorithm` provide typed methods built on top of it: ```python def publish_data(self, data_type: DataType, data: CustomData) -> None: def publish_signal(self, name: str, value, ts_event: int = 0) -> None: ``` These methods publish custom data and signals without exposing the raw message bus to Python. ## Python topic messaging Registered Python `DataActor`, `Strategy`, and `ExecutionAlgorithm` components publish arbitrary Python objects through `publish_message(topic, message)`. Use `subscribe_topic(topic, handler, priority=0)` and `unsubscribe_topic(topic, handler)` to manage callbacks on the same runtime bus. These methods do not construct, replace, or dispose the bus, and do not expose `self.msgbus`. Subscribe from a component callback such as `on_start`: ```python from nautilus_trader.common import DataActor class RiskObserver(DataActor): def on_start(self) -> None: self.subscribe_topic("app.risk.*", self.on_risk, priority=10) def on_risk(self, message: object) -> None: self.log.info(f"Risk update: {message}") ``` From another registered component, publish on a matching topic: ```python self.publish_message("app.risk.limit", {"instrument": "BTCUSDT", "limit": 3}) ``` Publication requires a non-empty concrete topic without wildcards. Subscription patterns accept `*` for zero or more characters and `?` for exactly one character. Wildcards can cross dots, so `app.risk.*` also matches `app.risk.limit.eu`. Handlers receive the original object without copying, string conversion, or serialization. Follow [message integrity](#message-integrity): treat published objects as immutable. Use application-owned topics such as `app.risk.limit`; system data and event topics carry their own payload types. These callbacks receive objects published through `publish_message`, not typed market data or signals. Use the corresponding typed subscription methods for those payloads. Patterns that also match signals or custom data log an error for each incompatible payload instead of invoking the Python callback. Python object publication does not send the object to external message-bus storage or transport. Delivery is **synchronous**. Nested publication finishes before the publishing callback continues. Higher subscription priorities run first; equal priority does not imply subscription insertion order. Use distinct priorities when callback order matters, including reproducible backtests. A handler exception is logged, and delivery continues to the remaining handlers. This delivery model does not make it safe to re-enter a component that is already executing in the publishing call stack. Subscriptions belong to the subscribing component: - Repeating the same pattern and callable is a no-op, including attempts to change its priority. Unsubscribe first to change priority. - Python-defined bound methods match by receiver and function, so repeated `self.on_risk` lookups identify the same handler. Other callables, including bound built-in methods, match by object identity. Keep a reference to these other callables for later unsubscription. Neither equality nor representation methods run. - Unsubscription removes only the exact pattern and callable owned by that component. An absent subscription is a no-op. Other components' subscriptions remain independent. - A callback already selected for a publication can still run if it is unsubscribed during that publication. Stopping retains subscriptions, and raw topic callbacks can run while a component is stopped. Resuming keeps those subscriptions. Successful reset and disposal release the component's subscriptions and retained callables; successful reset requires subscribing again. A failed disposal retains subscriptions until retirement cleanup succeeds. Fault cleanup also releases subscriptions. These methods raise `RuntimeError` before runtime registration, after successful disposal, from a foreign thread, while releasing subscriptions, or when the registered bus is unavailable or replaced. Invalid topics and patterns raise `ValueError`; a non-callable handler raises `TypeError`. Priority must be an integer in `[0, 4294967295]`; values outside this range raise `OverflowError`. ## Messaging styles NautilusTrader is an **event-driven** framework where components communicate by sending and receiving messages. Understanding the different messaging styles helps when building trading systems. This guide explains the four primary messaging patterns available in NautilusTrader: | **Messaging style** | **Purpose** | **Best for** | | :------------------------------------------------------------- | :----------------------------------- | :---------------------------------------------------- | | **Custom data publish/subscribe** | Structured trading data exchange | Trading metrics, indicators, data needing persistence | | **Signal publish/subscribe** | Lightweight notifications | Simple alerts, flags, and status updates | | [**Python object publish/subscribe**](#python-topic-messaging) | In-process Python object exchange | Application topics shared by Python components | | **Rust MessageBus publish/subscribe** | Low-level, typed topic communication | Native runtime components | Each approach serves different purposes. Use this guide to decide which pattern to use. ### Rust MessageBus publish/subscribe to topics #### Concept The `MessageBus` is the central hub for all messages in NautilusTrader. Rust components can publish typed messages to named topics and subscribe handlers to those topics. This low-level interface is not part of the Python actor or strategy surface. #### Benefits and use cases Direct message-bus access is for native components that need: - **Cross-component communication** within the system. - **Flexibility** to define typed topics and payloads. - **Decoupling** between publishers and subscribers who don't need to know about each other. - **Global reach** where messages can be received by multiple subscribers. - Working with events that do not fit the data actor model. - Advanced scenarios requiring full control over messaging. #### Considerations - You must track topic names manually (typos could result in missed messages). - You must define handlers manually. ### Custom data publish/subscribe #### Concept Custom data exchanges structured values between data actors and strategies. A `CustomData` value carries a `DataType`, payload, event timestamp, and initialization timestamp for routing and event ordering. #### Benefits and use cases The Data publish/subscribe approach works well when you need: - **Exchange of structured trading data** like market data, indicators, custom metrics, or option greeks. - **Event timestamps** (`ts_event`, `ts_init`) for ordering inputs during backtests; publication itself does not sort messages. - **Data persistence and serialization** through registered custom data classes, integrating with NautilusTrader's data catalog system. - **Standardized trading data exchange** between system components. #### Considerations - The payload must expose `ts_event` and `ts_init`. - Persistence requires registering a serializable custom data class. #### Quick overview code ```python from dataclasses import dataclass from nautilus_trader.model import CustomData from nautilus_trader.model import DataType @dataclass class GreeksData: delta: float gamma: float ts_event: int ts_init: int data_type = DataType("GreeksData") data = CustomData( data_type, GreeksData( delta=0.75, gamma=0.1, ts_event=1_630_000_000_000_000_000, ts_init=1_630_000_000_000_000_000, ), ) self.publish_data(data_type, data) self.subscribe_data(data_type) def on_data(self, data: CustomData) -> None: if data.data_type == data_type: greeks = data.data self.log.info(f"Delta: {greeks.delta}, Gamma: {greeks.gamma}") ``` See [Custom data](custom_data.md) for registration and persistence. ### Signal publish/subscribe #### Concept **Signals** are a lightweight way to publish and subscribe to simple notifications within the actor framework. This is the simplest messaging approach, requiring no custom class definitions. #### Benefits and use cases The Signal messaging approach works well when you need: - **Simple, lightweight notifications/alerts** like "RiskThresholdExceeded" or "TrendUp". - **Quick, on-the-fly messaging** without defining custom classes. - **Broadcasting alerts or flags** as simple primitive values. - **Easy API integration** with straightforward methods (`publish_signal`, `subscribe_signal`). - **Multiple subscriber communication** where all subscribers receive signals when published. - **Minimal setup overhead** with no class definitions required. #### Considerations - Each signal carries a single value. The value is converted to a string on publish, so handlers always receive `signal.value` as a `str`; complex data structures are not preserved. - Differentiate between signals in the `on_signal` handler with `signal.name`. #### Quick overview code ```python # Define signal constants for better organization (optional but recommended) import types from nautilus_trader.common import LogColor from nautilus_trader.core.datetime import unix_nanos_to_dt signals = types.SimpleNamespace() signals.NEW_HIGHEST_PRICE = "NewHighestPriceReached" signals.NEW_LOWEST_PRICE = "NewLowestPriceReached" # Subscribe from a DataActor or Strategy self.subscribe_signal(signals.NEW_HIGHEST_PRICE) self.subscribe_signal(signals.NEW_LOWEST_PRICE) # Publish from a DataActor or Strategy self.publish_signal( name=signals.NEW_HIGHEST_PRICE, value=signals.NEW_HIGHEST_PRICE, # value can be the same as name for simplicity ts_event=bar.ts_event, # timestamp from triggering event ) # Handler (fixed callback name) def on_signal(self, signal): match signal.name: case signals.NEW_HIGHEST_PRICE: self.log.info( f"New highest price was reached. | " f"Signal value: {signal.value} | " f"Signal time: {unix_nanos_to_dt(signal.ts_event)}", color=LogColor.GREEN, ) case signals.NEW_LOWEST_PRICE: self.log.info( f"New lowest price was reached. | " f"Signal value: {signal.value} | " f"Signal time: {unix_nanos_to_dt(signal.ts_event)}", color=LogColor.RED, ) ``` ### Summary and decision guide #### Decision guide: Which style to choose? | **Use case** | **Recommended approach** | **Setup required** | | :------------------------------------- | :---------------------------------- | :---------------------------------------- | | Native system-level communication | Rust `MessageBus` publish/subscribe | Typed topic and handler | | Structured Python component data | `DataActor` custom data methods | `DataType`, `CustomData`, and `on_data()` | | Arbitrary in-process Python objects | Component topic methods | Application topic and callable | | Simple Python alerts and notifications | `DataActor` signal methods | Signal name and `on_signal()` | ## External egress and ingress The `MessageBus` can write serialized messages to external streams. This section describes the external egress and ingress sides of the external bus. Rust-native live nodes use injected `MessageBusExternalEgress` and `MessageBusExternalIngress` surfaces, so the core node does not depend on Redis, a broker, shared-memory implementation, or socket protocol. :::info Redis is the built-in external backing for serializable messages. The minimum supported Redis version is 6.2, required for the `MINID` stream trimming used by autotrim. ::: When external egress is configured, outgoing publish messages are first dispatched to in-process subscribers, then serialized into the existing `BusMessage` wire record: - `topic`: the exact message bus topic used by the internal publish call, for example `data.quotes.BINANCE.BTCUSDT` or `events.order.S-001`. - `type`: the canonical payload type name, for example `QuoteTick` or `OrderEventAny`. - `payload_kind`: present with the value `typed` for control, execution, and reconciliation payloads whose fixed type names could also identify custom payloads. - `encoding`: the payload encoding selected from the message bus encoding policy. - `payload`: serialized bytes encoded with the selected encoding. An external producer that writes directly to a Redis stream must include `topic`, `type`, and `payload`. The `topic` must be a valid publish topic and cannot contain `*` or `?`. The `encoding` field is optional and defaults to JSON when omitted. The receiving node skips entries without `type` because it cannot select a payload decoder. Set `payload_kind` to `typed` for a fixed typed payload; without this discriminator, the receiving node treats the same type name as custom data. External egress receives that record as `publish(BusMessage)`. This outbound call must not block the node's bus thread. Bounded egress implementations drop on a full queue instead of applying back-pressure to the trading loop. Closing the message bus closes the configured egress. Inbound external streams are exposed through the separate Rust `MessageBusExternalIngress` trait. Ingress yields the same `BusMessage { topic, payload_type, encoding, payload }` shape. `republish_external_message` decodes supported inbound messages and republishes them internally without forwarding the message back out. Normal internal republishing requires the inbound payload type to be registered for streaming and skips unregistered types without decoding. `LiveNode::add_stream_processor` in Rust and `LiveNode.add_stream_processor` in Python register callbacks for typed JSON or MessagePack payloads. Processors run in registration order before the internal streaming registration check, so they receive supported typed messages even when internal republishing is disabled. Python processors receive a mapping with an added `payload_type` field. A processor error stops later processors and skips internal republishing for that message. For custom data, egress writes and ingress expects an envelope in the Redis `payload` field, not the bare custom object. The canonical JSON envelope is: ```json { "type": "MyData", "data_type": { "type_name": "MyData", "metadata": { "source": "external" }, "identifier": "optional-storage-key" }, "payload": { "value": 42, "ts_event": 0, "ts_init": 0 } } ``` The envelope requires `type` and `payload`; `data_type` is optional on ingress and defaults to the message type with no metadata or identifier. In the canonical emitted form, `data_type.type_name` uses the same custom type name, `metadata` is an object that may be empty, and `identifier` is present only when assigned. The envelope `type` must match the Redis stream `type`. The envelope `payload` is the bare object passed to the registered class's `from_json(...)` method. MessagePack uses the same map fields encoded as MessagePack bytes. For Python custom data, register the class before starting the node: ```python from nautilus_trader.model import register_custom_data_class register_custom_data_class(MyData) ``` The external-client subscription registers the payload type for streaming, while `register_custom_data_class(...)` installs the process-wide JSON decoder. Both registrations are required. See [Custom data](custom_data.md#registration-architecture) for the class requirements. For Redis, messages are transmitted via a Multiple-Producer Single-Consumer (MPSC) channel to a separate Rust task. That task writes the message to Redis streams. Offloading I/O to a separate task keeps the publishing thread unblocked. With MessagePack or JSON, Rust-native external egress forwards serializable typed publications. This includes instruments, quotes, trades, bars, book deltas, depth-10 snapshots, mark/index/funding updates, option greeks (`OptionGreeks`), account state, portfolio snapshots, order events, position events, and custom data. With the `defi` feature this also includes DeFi blocks, pools, liquidity updates, fee collects, and flash events. Full order book snapshots, `GreeksData` records, option chain slices, and DeFi pool swaps are not forwarded because those types do not implement Serde serialization. When `external_streams` is non-empty, JSON or MessagePack egress also forwards subscription commands, trading commands, execution mass-status requests, order status reports, fill reports, position status reports, and execution mass-status reports. These payloads remain local when no external streams are configured. With SBE or Cap'n Proto, Rust-native external egress forwards the built-in market data payloads with schema codecs: quotes, trades, bars, book deltas, depth-10 snapshots, mark price updates, index price updates, funding rate updates, and option greeks. Other payload types are dropped with a debug log when those schema encodings are selected. ## Configuration The message bus external backing technology uses a behavior config plus a technology-owned backing config. `MessageBusConfig` controls message bus behavior. `RedisMessageBusConfig` owns Redis connection settings and implements `MessageBusBackingFactory`. ```rust use nautilus_common::{ enums::SerializationEncoding, msgbus::{MessageBusBackingFactory, MessageBusConfig}, }; use nautilus_infrastructure::redis::msgbus::RedisMessageBusConfig; let config = MessageBusConfig { encoding: SerializationEncoding::Json, encoding_market_data: Some(SerializationEncoding::Sbe), timestamps_as_iso8601: true, buffer_interval_ms: Some(100), autotrim_mins: Some(30), use_trader_prefix: true, use_trader_id: true, use_instance_id: false, streams_prefix: "streams".to_string(), types_filter: Some(vec!["QuoteTick".to_string(), "TradeTick".to_string()]), ..Default::default() }; let redis_config = RedisMessageBusConfig::default(); let backing = redis_config.create(trader_id, instance_id, config.clone())?; ``` Existing Rust callers can continue using `RedisMessageBusFactory::new(redis_config)`, which delegates to the config implementation. ### Backing config A `RedisMessageBusConfig` is required when using the built-in Redis backing. For a default Redis setup on the local loopback you can pass `RedisMessageBusConfig::default()`. Redis selection is explicit in the Rust type. The config does not use a user-facing selector such as `type = "redis"` or `backing_type = "redis"`. Rust-native callers that inject `MessageBusExternalEgress` with `LiveNodeBuilder::with_external_msgbus_egress` pass concrete connection details when they construct that egress surface. The core message bus does not require a `RedisMessageBusConfig` for injected egress. The Rust live runtime accepts `external_streams` in `MessageBusConfig`, and consumes inbound `BusMessage`s when callers inject a `MessageBusExternalIngress` with `LiveNodeBuilder::with_external_ingress`. The config names the external stream keys; the injected ingress is the concrete runtime source. Rust callers can install `RedisMessageBusConfig` with `LiveNodeBuilder::with_external_msgbus_factory`. Building fails when a factory is combined with separately injected egress or ingress. A factory always installs egress and creates ingress only when `external_streams` is non-empty. Python exposes the same builder method for built-in backing configs, currently `RedisMessageBusConfig`. The existing `RedisMessageBusFactory` wrapper remains supported. Python does not accept arbitrary factory classes. :::warning The built-in Redis ingress starts each configured stream at the current timestamp, so entries that already exist when the node starts are not replayed. After startup it advances the last-seen ID for each stream and preserves those IDs across connection retries. Use cache recovery or the event store when durable pre-start replay is required; `external_streams` provides live forwarding, not a consumer-group backlog. ::: ### Encoding Rust-native external message bus egress supports these encoding names: - JSON (`json`) - MessagePack (`msgpack`) - Cap'n Proto (`capnp`, with the Rust `capnp` feature) - SBE (`sbe`, with the Rust `sbe` feature) Use the `encoding` config option to control the message writing encoding. Use `encoding_market_data` to override the encoding for market data payloads backed by the external bus binary codecs. Use `encoding_builtin` to override account state, portfolio snapshot, order event, and position event payloads. Custom and unmapped payload types always use `encoding`. `MessageBusConfig::validate` requires the default `encoding` to support custom payloads, so it must be JSON or MessagePack. Category overrides must be supported by every published payload type in that category. SBE and Cap'n Proto can currently be used only for `encoding_market_data`, and only when the matching Rust feature is enabled. `encoding_builtin = "sbe"` and `encoding_builtin = "capnp"` fail validation until those schema codecs cover the built-in event category. The Redis cache payload path supports MessagePack and JSON only. SBE and Cap'n Proto are schema payload encodings for Rust-native external message bus egress, not Redis cache encodings, and selecting either for a Redis cache payload is an error. :::tip The `json` encoding is used by default for human readability and interoperability. Use `msgpack` when payload size and serialization performance are a primary concern. ::: ### Timestamp formatting By default timestamps are formatted as UNIX epoch nanosecond integers. Alternatively you can configure ISO 8601 string formatting by setting `timestamps_as_iso8601` to `true`. ### Message stream keys Message stream keys identify individual trader nodes and organize messages within streams. The `trader-` prefix, trader ID, and instance ID segments are optional and controlled by the options below; the streams prefix is always included. With every segment enabled, a trader key has the following structure: ``` trader-{trader_id}:{instance_id}:{streams_prefix} ``` With the default options (`use_trader_prefix` and `use_trader_id` enabled, `use_instance_id` disabled) the base stream key is `trader-{trader_id}:{streams_prefix}`. These options control Redis stream keys. They do not rewrite the `topic` passed to an injected `MessageBusExternalEgress`; that topic remains the internal message bus publish topic. When `stream_per_topic` is `True`, Redis egress appends the topic to the stream key. When it is `False`, Redis stores all messages on the base stream key and keeps the topic as a message field. The following options are available for configuring message stream keys: #### Trader prefix If the key should begin with the `trader-` prefix. #### Trader ID If the key should include the trader ID for the node. #### Instance ID Each trader node is assigned a unique instance ID, which is a UUIDv4. This instance ID helps distinguish individual traders when messages are distributed across multiple streams. You can include the instance ID in the trader key by setting the `use_instance_id` configuration option to `True`. This is particularly useful when you need to track and identify traders across various streams in a multi-node trading system. #### Streams prefix The `streams_prefix` string enables you to group all streams for a single trader instance or organize messages for multiple instances. Configure this by passing a string to the `streams_prefix` configuration option, ensuring other prefixes are set to false. #### Stream per topic Indicates whether the producer will write a separate stream for each topic. This is particularly useful for Redis backings, which do not support wildcard topics when listening to streams. If set to False, all messages will be written to the same stream. :::info Redis does not support wildcard stream topics. For better compatibility with Redis, it is recommended to set this option to False. ::: ### Types filtering When messages are published on the message bus, they are serialized and written to a stream if a backing for the message bus is configured and enabled. To prevent flooding the stream with data like high-frequency quotes, you may filter out certain types of messages from external publication. To enable this filtering mechanism, pass a list of payload type names to the `types_filter` parameter in the message bus configuration. Listed types are excluded from external publication. A name shared by a custom payload and a fixed typed payload excludes both. ```python from nautilus_trader.config import MessageBusConfig # Create a MessageBusConfig instance with types filtering message_bus = MessageBusConfig(types_filter=["QuoteTick", "TradeTick"]) ``` ### Stream auto-trimming Use `autotrim_mins` to set a lookback window in minutes and `autotrim_maxlen` to set an approximate maximum number of entries for each Redis stream. You can configure either policy or both. When both are set, the message bus removes entries that exceed either the time window or the entry-count threshold. Redis applies `autotrim_maxlen` with approximate trimming for better write performance, so a stream may contain slightly more entries than the configured threshold. :::info The Redis implementation trims each stream at most once per minute, so entries can remain up to roughly a minute longer than the `autotrim_mins` window. ::: ## External streams The message bus within a `LiveNode` (node) is referred to as the "internal message bus". A producer node is one which publishes messages onto an external stream (see [external egress and ingress](#external-egress-and-ingress)). The consumer node listens to external streams to receive and publish deserialized message payloads on its internal message bus. ```mermaid flowchart TB producer[Producer Node] stream[Stream] consumer1[Consumer Node 1] consumer2[Consumer Node 2] producer --> stream stream --> consumer1 stream --> consumer2 ``` :::info Set the `LiveDataEngineConfig.external_clients` with the list of `client_id`s intended to represent the external streaming clients. The `DataEngine` will filter out subscription commands for these clients, ensuring that the external streaming provides the necessary data for any subscriptions to these clients. When the Rust `DataEngine` skips an external-client subscription, it registers the corresponding streaming payload type for inbound republishing on the message bus. ::: ### Example configuration The following example details a streaming setup where a producer node publishes Binance data externally, and a downstream consumer node publishes these data messages onto its internal message bus. #### Producer node We configure the `MessageBus` of the producer node to publish to a `"binance"` stream. The settings `use_trader_id`, `use_trader_prefix`, and `use_instance_id` are all set to `false` to ensure a simple and predictable stream key that the consumer nodes can register for. ```rust let message_bus = MessageBusConfig { use_trader_id: false, use_trader_prefix: false, use_instance_id: false, streams_prefix: "binance".to_string(), // <--- stream_per_topic: false, autotrim_mins: Some(30), ..Default::default() }; let redis_config = RedisMessageBusConfig { connection_timeout: 2, response_timeout: 2, ..Default::default() }; let mut node = LiveNode::builder(trader_id, Environment::Live)? .with_msgbus_config(message_bus) .with_external_msgbus_factory(Box::new(redis_config)) .build()?; node.run().await?; ``` #### Consumer node We configure the `MessageBus` of the consumer node to receive messages from the same `"binance"` stream. A `RedisMessageBusConfig` creates ingress from `external_streams`, and `LiveNode::run` publishes the received messages onto the node's internal message bus. We declare the client ID `"BINANCE_EXT"` as an external client so the `DataEngine` does not attempt to send data commands to this client ID. ```rust let data_engine = LiveDataEngineConfig { external_clients: Some(vec![ClientId::from("BINANCE_EXT")]), ..Default::default() }; let message_bus = MessageBusConfig { external_streams: Some(vec!["binance".to_string()]), // <--- ..Default::default() }; let redis_config = RedisMessageBusConfig { connection_timeout: 2, response_timeout: 2, ..Default::default() }; let mut node = LiveNode::builder(trader_id, Environment::Live)? .with_data_engine_config(data_engine) .with_msgbus_config(message_bus) .with_external_msgbus_factory(Box::new(redis_config)) .build()?; node.run().await?; ``` ## Related guides - [Actors](actors.md) - Actors use the message bus for event handling. - [Architecture](architecture.md) - Message bus role in system architecture. # Networking Source: https://nautilustrader.io/docs/latest/concepts/networking/ NautilusTrader adapters use the shared `nautilus-network` clients for HTTP request/response APIs, WebSocket streams, and suffix-framed TCP protocols. These clients add trading-system policy around the underlying Rust transports: rate limits, connection reuse, liveness checks, reconnect control, replay coordination, and bounded reads. | Client | Underlying transport | Use when | Added policy | | -------------- | ----------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------- | | HTTP | Hyper | Finite request/response operations | Layered quotas, pooled connections, keepalive, timeouts, proxy routing, and bounded bodies | | WebSocket | `tokio-tungstenite` or `sockudo-ws` | Long-lived framed streams | Runtime backend selection, quotas, heartbeats, liveness checks, reconnects, and session fencing | | Raw TCP socket | Tokio and `rustls` | Suffix-framed byte streams | Framing, initial retries, heartbeats, liveness checks, reconnects, and ordered replay | The [Adapters](adapters.md) guide explains how venue clients translate these transports into Nautilus domain messages. This page covers the shared transport behavior beneath that boundary. ## HTTP client [`HttpClient`](../../crates/network/src/http/client.rs) wraps one reusable Hyper client and one or more shared rate limiters. A request waits for every applicable quota before the inner client builds and executes it. ```mermaid flowchart LR adapter[Adapter HTTP client] subgraph network[nautilus-network] client[HttpClient] limiter[RateLimiter] inner[InnerHttpClient] end hyper["Hyper client
pool and keepalive"] endpoint[HTTP endpoint] adapter --> client client -->|await quotas| limiter client -->|execute| inner inner <--> hyper hyper <--> endpoint ``` The outer client applies quota policy; the reusable inner client owns connection and response policy. The Rust API exposes `http::Method`, `http::StatusCode`, and `url::Url`. Requests return `HttpResponse` or `HttpResponseStream`, and failures return `HttpClientError`. ### Rate limiting and requests The rate limiter uses the generic cell rate algorithm (GCRA) with a default quota and optional per-key overrides. A request can carry several keys, such as an endpoint and an order scope, and waits for them together. Multiple limiters let one request consume independent budgets, such as per-IP and per-account limits. Sharing their `Arc` values across HTTP clients keeps those budgets process-wide instead of creating one allowance per connection. The client accepts default and per-request headers, query parameters with repeated values, raw request bodies, and `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` methods. A client-level timeout applies to all requests unless a request supplies its own timeout. An optional proxy applies to both HTTP and HTTPS traffic. **HTTP status errors remain normal `HttpResponse` values** so each adapter can interpret the venue's body and retry rules. The transport retries requests canceled before transmission on reused connections, and allows two retries for remote HTTP/2 `GOAWAY(NO_ERROR)` or `REFUSED_STREAM` errors. Other transport failures and HTTP status codes do not trigger retries. Adapters can wrap retryable operations with [`RetryManager`](../../crates/network/src/retry.rs), but the adapter must decide which venue errors and operations are safe to retry. ### Connection reuse and response bounds Each production `HttpClient` enables `TCP_NODELAY`, keeps up to 32 idle connections per host, and retains an idle connection for up to 60 seconds. HTTP/2 connections send keepalive probes every 30 seconds even while idle and use adaptive flow-control windows. Reusing a client preserves the pool and avoids a new TCP and TLS handshake for each request. Buffered responses contain the status, only the header names selected when the client was built, and the raw body bytes. The client rejects a declared body larger than 100 MiB before reading it. For chunked or unbounded responses, it stops as soon as accumulated bytes would cross the same limit. Endpoints whose path or query can contain credentials can use the redacted request path, which removes the URL from transport errors and logs. `HttpClient::get_stream` returns status and body chunks without accumulating the complete response or applying the buffered size limit. One absolute deadline covers headers and the whole body, including time spent processing chunks. No rate-limit keys are supplied, so no quota is consumed. Dropping an unfinished response releases the exchange, including its owned connection task [under simulation](dst.md#simulated-http-and-websocket-transport). Dataset downloads use this path to stream to a temporary file before renaming it. Downloads configure a separate timeout for response headers and for each body read, allowing a progressing transfer to exceed that duration. ### HTTP transport benchmarks The [HTTP comparison](../../crates/network/benches/BENCHMARKS.md#http-transport-comparison), measured 2026-09-09 on an AMD Ryzen Threadripper 9980X, compares the previous Reqwest 0.13.4 client with the direct Hyper implementation. Both run in the same `bench-lto` binary with fat LTO and one codegen unit. The CPU governor is `performance`, ASLR is disabled per process, and client and server threads are pinned to separate physical cores. Accepted sessions have no sampled Cargo or compiler activity. Five independent sessions provide 60 paired samples per workload. The following 64 KiB cases summarize GET and POST at concurrency 1 and 16; the full report includes 1 KiB and 1 MiB responses, p99 values, uncertainty intervals, and resource measurements. | 64 KiB workload | Reqwest req/s | Hyper req/s | Paired throughput change | Paired p99 change | | -------------------- | ------------- | ----------- | ------------------------ | ----------------- | | GET, concurrency 1 | 29,516 | 30,645 | +3.6% | -3.1% | | POST, concurrency 1 | 27,054 | 28,223 | +4.1% | -3.4% | | GET, concurrency 16 | 55,870 | 55,460 | -0.5% | +0.4% | | POST, concurrency 16 | 47,779 | 47,631 | -0.3% | +0.2% | Throughput columns are medians of sample summaries. Changes are medians of paired within-round ratios; positive throughput changes and negative p99 changes favor Hyper. Serial throughput for 1 KiB and 64 KiB responses improves by 3.6% to 5.0%. Concurrent cases range from -2.6% to +0.2%, and the 1 KiB concurrent POST case has a paired p99 increase of 1.8%. These results support a modest serial improvement, with regressions in some concurrent workloads. The benchmark exercises complete requests and validates response bodies, status, headers, and connection reuse over loopback HTTP/1.1. It excludes TLS, HTTP/2, proxies, WAN latency, and adapter parsing, so the results do not establish a production-wide speedup. ## WebSocket client [`WebSocketClient`](../../crates/network/src/websocket/client.rs) separates connection lifecycle from frame transport. A controller owns reconnect and shutdown transitions, one writer serializes all sink access, and handler mode assigns each connection to one reader task. An optional heartbeat task sends liveness traffic through the same writer. ```mermaid flowchart LR adapter[Adapter] subgraph client[WebSocketClient] limiter[RateLimiter] controller[Controller] reconnect[Reconnect handle] reader[Reader task] writer[Writer task] heartbeat[Heartbeat task] state[SocketStateSink] end transport["WsTransport
Message and TransportError"] tungstenite[tokio-tungstenite] sockudo[sockudo-ws] endpoint[WebSocket endpoint] adapter -->|send| limiter --> writer adapter -->|request reconnect| reconnect --> controller reader -->|messages| adapter controller -->|availability edges| state --> adapter controller -->|replace| reader controller -->|replace| writer heartbeat --> writer writer <--> transport --> reader transport -. runtime backend .-> tungstenite transport -. runtime backend .-> sockudo tungstenite <--> endpoint sockudo <--> endpoint ``` The lifecycle tasks use one neutral transport interface, so adapters do not depend on a concrete WebSocket library. ### Connection modes | Mode | Reader ownership | Automatic reconnect | Liveness behavior | | ------- | ---------------------- | ------------------------------ | ---------------------------------------------- | | Handler | Internal callback task | Exponential backoff and jitter | Heartbeat and application-data idle timeouts | | Stream | Caller-owned reader | Disabled | Caller reports failure and replaces the client | Handler mode is the usual choice for long-lived adapter connections. Stream mode suits adapters that need direct stream backpressure or own a protocol-specific reconnect sequence. ### Transport backends The `WsTransport` abstraction normalizes text, binary, Ping, Pong, and Close frames together with transport errors. `WebSocketConfig.backend` selects either backend at runtime: | Backend | Availability | Upgrade headers | Proxy behavior | | ----------------------------------------------------------------- | -------------------------------------------- | ----------------------------------------- | -------------------------------- | | [`tokio-tungstenite`](https://crates.io/crates/tokio-tungstenite) | Always compiled | Passed through the WebSocket handshake | HTTP and HTTPS `CONNECT` tunnels | | [`sockudo-ws`](https://crates.io/crates/sockudo-ws) | Default with the `transport-sockudo` feature | Passed through a local HTTP/1.1 handshake | HTTP and HTTPS `CONNECT` tunnels | Disabling default Cargo features removes `sockudo-ws` and makes Tungstenite the default. Both backends use `rustls` for `wss://` connections and set `TCP_NODELAY` on paths where Nautilus creates the TCP stream. :::warning A recognized SOCKS proxy URL logs a warning and **connects directly** because WebSocket SOCKS tunneling is not implemented. Malformed proxy URLs and other unsupported schemes return an error. ::: ### Liveness and recovery The configured heartbeat sends either an RFC 6455 Ping or a venue-specific text message at a fixed interval. Configuring one also arms a response deadline: the client expects the peer to answer, so an unset `heartbeat_timeout_secs` defaults to three intervals. Set the field to choose a different window. A transport with no heartbeat gets no default, because nothing would guarantee the inbound frames needed to keep the window open. The **heartbeat timeout** resets on every inbound frame, including Ping and Pong, so it detects a peer that has stopped sending anything. The separate **idle timeout** resets only on text or binary application data, so control traffic cannot hide a silent market-data stream. A venue that answers the keepalive with a text payload refreshes the idle timeout exactly like real data does, so that window means something only when it sits below the heartbeat interval. An unset timeout leaves that detection off, except that an unset `heartbeat_timeout_secs` still derives three intervals when a heartbeat is configured. A zero timeout is rejected. Adapters that expose a non-optional integer map zero to unset rather than passing it through. A read failure, write failure, Close frame, heartbeat timeout, idle timeout, or explicit reconnect request moves a handler-mode client into reconnecting state. Reconnect uses exponential backoff with bounded jitter and allows unlimited attempts by default. A replacement connection that remains active for at least 10 seconds resets the attempt count and backoff. A configured maximum closes the client after that many consecutive failed or short-lived attempts. ```mermaid stateDiagram-v2 [*] --> Active: initial connection succeeds Active --> Reconnecting: I/O failure, Close, timeout, or explicit request Reconnecting --> Active: replacement succeeds Reconnecting --> Reconnecting: attempt fails Reconnecting --> Closed: configured attempt limit reached Active --> Disconnecting: deliberate disconnect Reconnecting --> Disconnecting: deliberate disconnect Disconnecting --> Closed: shutdown completes ``` Handler mode publishes `Disconnected` on entry to `Reconnecting` and `Connected` on recovery; individual attempts and deliberate disconnects add no state-sink edges. The writer installs the replacement sink before the controller starts its reader and publishes the reconnect notification. A **connection epoch** advances with each sink replacement. Reader fences drop frames from retired sessions, while epoch-aware handlers and sends let an adapter bind work to the transport that produced it. Mutable reconnect headers apply to later handshakes without interrupting the active connection. Adapters can register an `AuthTracker` so a disconnect invalidates authentication. They can also gate the reconnect buffer on that tracker, making messages wait for the new session to authenticate and discarding the remaining buffer if authentication fails. `SubscriptionState` separately records confirmed, pending subscribe, and pending unsubscribe intent for adapter-driven resubscription; it never sends protocol messages itself. ### Reconnect throttling Once three reconnect attempts occur inside a rolling two-minute window, each further attempt waits at least one second, regardless of the configured backoff. The window is purely time-based: a replacement connection that survives the stability threshold still resets the backoff and attempt count, but has no effect on the floor. Throttling lifts by itself once fewer than three attempts remain inside the window. Venues rate-limit new connections per IP (Binance permits 300 connections per five minutes, OKX three per second), so an unthrottled reconnect loop can otherwise escalate a transient drop into an IP-level throttle or ban affecting every client behind that address. The first three attempts in any window incur no additional throttling delay; configured backoff still applies. A single drop can still trigger an immediate first reconnect. ### State reporting and explicit reconnect Clients configured with a `SocketStateSink` publish ordered `Connected` and `Disconnected` availability edges. A successful initial connection publishes `Connected`; transport loss or an accepted explicit reconnect publishes `Disconnected`; and a successful replacement publishes `Connected`. Initial connection failure, individual retry attempts, retry exhaustion, deliberate disconnect, and client drop do not add events. The sink therefore describes transport availability, not every internal `ConnectionMode` transition. Its callback runs synchronously and serializes edges, so it must return promptly and must not request another transition through the same sink. `request_reconnect()` atomically asks a handler-mode controller to replace its active transport. A cloneable `WebSocketReconnectHandle` gives adapter tasks the same capability without ownership of the client and distinguishes accepted, already reconnecting, disconnecting, closed, and unsupported requests. An accepted request invalidates registered authentication state and publishes `Disconnected` before the replacement can become active. Stream mode reports `Unsupported` because its reader is caller-owned. ### Send semantics Application text and binary sends wait for their rate-limit keys and for an active connection. The ordinary send methods return after enqueueing the frame, so **success does not prove delivery**. The writer keeps FIFO order for application messages buffered during reconnect or after a failed write and replays them on a replacement connection. A control frame belongs to the connection it was issued on, so a failed Ping, Pong, or Close is dropped rather than replayed. This in-memory buffer provides reconnect continuity, not durable or exactly-once delivery. Ownership-bound text sends take an expected connection epoch and wait for the writer result. They fail if ownership changes and never replay on another connection. Connection-bound Pong sends use the same epoch check so a response cannot leak onto the connection after the one that received its Ping. :::warning If a bound write times out after it starts, **delivery is undetermined** and the caller must not retry blindly. ::: ### Backend benchmarks The [WebSocket benchmark](../../crates/network/benches/BENCHMARKS.md) was measured on 2026-07-29. The following 512 B results are the median of three back-to-back runs on the same AMD Ryzen Threadripper 9980X host: | Metric | `tokio-tungstenite 0.30.0` | `sockudo-ws 2.0.1` | | --------------------------------- | -------------------------: | -----------------------: | | Round-trip text latency, p99 | 3.305 us | 0.651 us | | One-way binary burst latency, p99 | 17.647 us | 15.053 us | | Text receive throughput | 7.187 million messages/s | 8.504 million messages/s | | Text send throughput | 6.400 million messages/s | 7.207 million messages/s | | Text round-trip throughput | 0.530 million messages/s | 1.852 million messages/s | Across the measured 64 B, 512 B, and 4,096 B payloads, `sockudo-ws 2.0.1` reduced round-trip p99 latency by 73% to 82%. At 512 B it processed 18% more receives, 13% more sends, and 250% more round trips. These are backend frame-transport microbenchmarks over established, uncompressed 1 MiB in-memory Tokio duplex streams. They exclude DNS, TCP connect, TLS, HTTP upgrade, kernel network I/O, external latency, keepalive traffic, and the reconnecting client lifecycle. These WebSocket measurements do not cover HTTP or raw TCP clients, and their absolute values should only be compared on the same machine. ## Raw TCP socket client [`SocketClient`](../../crates/network/src/socket/client.rs) supports plain and TLS byte streams for protocols that delimit messages with a fixed suffix. A controller coordinates the connection, one reader splits inbound frames, and one writer appends the suffix while serializing concurrent sends. ```mermaid flowchart LR adapter[Adapter] subgraph client[SocketClient] controller[Controller] reconnect[Reconnect handle] reader["Reader task
split and strip suffix"] writer["Writer task
append suffix"] heartbeat[Heartbeat task] state[SocketStateSink] end replay[Reconnect replay] stream[Plain or TLS TCP stream] endpoint[TCP endpoint] adapter -->|send| writer adapter -->|request reconnect| reconnect --> controller reader -->|complete message| adapter controller -->|availability edges| state --> adapter controller -->|replace| reader controller -->|replace| writer heartbeat --> writer replay -->|before buffered sends| writer writer <--> stream --> reader stream <--> endpoint ``` The writer owns framing and replay order; the adapter receives complete messages without the configured suffix. ### Framing and liveness The suffix must contain at least one byte and applies in both directions. The reader retains a partial frame across reads and strips the suffix before invoking the callback. While the session remains active, it emits complete messages in arrival order. If an unterminated frame grows past 10 MiB, the reader stops and the controller reconnects instead of allowing unchecked memory growth. An optional heartbeat task sends a configured byte payload at a fixed interval; the writer appends the same suffix as it does for application messages. A raw socket has no Ping frames, so the payload is required. `heartbeat_timeout_secs` stops the reader when no bytes arrive within the window. Unset, it defaults to three intervals when a heartbeat is configured and leaves detection off otherwise. A zero timeout is rejected. The socket enables `TCP_NODELAY` to avoid Nagle delays for small protocol messages. ### Connection and TLS policy The client accepts `host:port` or URL input and supports plain or TLS mode. TLS uses the standard web PKI roots. A certificate directory can add trusted roots and, when it contains a matching certificate and private key, supply a client identity for mutual TLS. Initial connection establishment makes up to five attempts by default, with a 10-second bound per attempt and exponential backoff. Once connected, transport loss uses the configurable reconnect timeout, exponential backoff, bounded jitter, and unlimited attempts by default. As with the WebSocket client, 10 seconds of stable uptime resets the reconnect cycle, and the same reconnect throttling bounds its attempt rate once reconnects flap. An optional state sink reports semantic connection loss and recovery. ### State reporting and explicit reconnect The optional `SocketStateSink` has the same availability contract as the WebSocket client. It publishes `Connected` after successful initial connection, `Disconnected` when an active transport enters reconnect, and `Connected` after recovery. It omits initial failures, individual attempts, retry exhaustion, deliberate disconnect, and drop. Its synchronous callback must return promptly and must not request another transition through the same sink. `request_reconnect()` atomically asks the controller to replace an active plain or TLS transport. The cloneable `SocketReconnectHandle` lets adapter tasks make that request without owning the client and reports whether it was accepted or rejected because the client is already reconnecting, disconnecting, or closed. An accepted request publishes `Disconnected` before waking the controller; normal reconnect replay and buffer ordering then apply to the replacement. ### Replay and delivery boundaries During reconnect, the writer buffers application messages in FIFO order. After installing a replacement writer, it can first send protocol replay messages supplied by the adapter, such as a logon or session setup sequence, and then drain the buffered application messages. The replacement reader starts only after that drain succeeds. A post-reconnection callback runs after the writer, buffer, and reader are ready. ```mermaid sequenceDiagram participant C as Controller participant W as Writer task participant P as Replacement peer participant R as Reader task participant A as Adapter C->>C: Establish replacement connection C->>W: Install writer and optional replay batch opt replay is configured W->>P: Send protocol replay end W->>P: Drain buffered sends in FIFO order alt drain succeeds W-->>C: Confirm completion C->>R: Retire previous reader C->>C: Enter Active and publish Connected C->>R: Start replacement reader opt callback is configured C-->>A: Run post-reconnection callback end else send fails or times out W-->>C: Report failure C->>C: Keep reconnecting and retry end ``` A raw TCP replacement becomes active only after optional protocol replay and buffered application messages drain successfully; the reader and post-reconnection callback start afterward. :::warning `send_bytes` returns when the message enters the writer channel, not when the peer receives it. A concurrent disconnect can still prevent delivery. Reconnect replay and buffering are process memory, so protocols that require durable or exactly-once delivery must enforce those guarantees above the socket client. ::: ## TCP socket options The WebSocket and raw TCP socket clients apply the same options to every outbound connection, including the hop to an HTTP `CONNECT` proxy. The HTTP client uses a separate Hyper connector with `TCP_NODELAY`, keepalive after 15 seconds idle with 15 seconds between probes and three retries, and a 30-second `TCP_USER_TIMEOUT` on Linux, Android, and Fuchsia. The table below applies to WebSocket and raw TCP clients. | Option | Value | Detects or prevents | | ------------------ | ------------------------------- | --------------------------------------------------------- | | `TCP_NODELAY` | Enabled | Nagle delaying a small frame behind an unacknowledged one | | Keepalive | 20 s idle, 10 s apart, 3 probes | An idle peer that has gone away without closing | | `TCP_USER_TIMEOUT` | 1 minute, Linux only | Outbound data that is never acknowledged | These catch a connection that stops delivering without closing, which a NAT or load balancer produces when it drops state with no `FIN` and no `RST`. Writes keep succeeding into the send buffer and return `Ok` for messages the peer will never receive. Kernel defaults take roughly 15 minutes to give up; these bound that at about a minute. `TCP_USER_TIMEOUT` is sized to exceed the keepalive probe budget. On Linux it also overrides `TCP_KEEPCNT`, so detection there follows the timeout rather than the probe count, which applies on macOS and Windows. Treat them as a backstop. The heartbeat timeout usually fires first, and unlike it these need no configuration and still bound a connection whose reader task has stopped making progress. A socket that rejects an option is still usable, so failures are logged and the connection proceeds. ## Testing The network crate separates algorithm checks from operating-system I/O and simulated failure topologies. This keeps a failure local: a state-machine invariant should fail without a socket, wire behavior should fail against a small loopback peer, and reconnect races should fail under a reproducible network schedule. ### Unit and component tests Tests beside the implementation cover configuration validation, state transitions, rate limits, backoff, retry budgets, framing, transport conversion, authentication, subscription state, and reconnect buffer policy. Pure logic uses fake clocks and direct state models. Async task tests use paused Tokio time, in-memory duplex streams, injected transports, or an ephemeral loopback server so they can exercise the real reader, writer, heartbeat, and controller tasks without an external service. The client suites then test their own protocol boundary. HTTP tests cover request serialization, response headers and body limits, timeouts, proxy behavior, and URL redaction. WebSocket and raw TCP tests cover concurrent sends, liveness timeouts, framing, connection epochs, state sinks, explicit reconnect, replay order, and shutdown races. The shared TLS tests cover certificate loading and a complete mutual-TLS handshake. A separate loopback integration suite exercises the WebSocket HTTP `CONNECT` proxy path for plain `ws://` upstreams. ### Property tests [`proptest`](https://github.com/proptest-rs/proptest) generates values and operation traces for invariants that example cases cannot enumerate. The suites compare GCRA decisions with a reference model, check backoff and retry bounds, round-trip transport messages through both WebSocket backends, and exercise authentication, subscription, and reconnect-buffer state machines. Selected suites persist minimized failures in `crates/network/proptest-regressions` so a discovered case becomes a permanent regression test. ### Deterministic network simulation [`turmoil`](https://crates.io/crates/turmoil) tests compile the production raw TCP and WebSocket clients against simulated TCP types through the crate's `net` seam. Fixed seeds make failures reproducible. Stressed runs vary task order and message latency, while scenarios inject connection drops, partitions and repairs, stalled peers, handshake failures, and disconnects during backoff or recovery. Assertions cover eventual state, attempt limits, heartbeat behavior, message ordering, authentication gating, and clean shutdown. Separate suites exercise the Tungstenite and Sockudo backends over the same simulated protocol. Default tests keep real Tokio loopback networking and exclude the simulation-only suites. Enabling the `turmoil` feature swaps the TCP layer and includes those suites: ```bash cargo nextest run -p nautilus-network cargo nextest run -p nautilus-network --features turmoil ``` # Options Source: https://nautilustrader.io/docs/latest/concepts/options/ Nautilus provides first-class support for options trading across traditional and crypto markets. This includes option-specific instrument types, venue-provided Greeks streaming, option chain aggregation, and a local Black-Scholes Greeks calculator for risk management. ## Option instrument types The platform defines several option instrument types: | Instrument | Description | | -------------------- | ---------------------------------------------------------------------------- | | `OptionContract` | Exchange-traded option on an underlying with strike and expiry. | | `OptionSpread` | Exchange-defined multi-leg option strategy as one line. | | `CryptoOption` | Crypto option with crypto quote/settlement; inverse or quanto style. | | `CryptoOptionSpread` | Crypto option spread with inverse, settlement currency, and fractional size. | | `BinaryOption` | Fixed-payout option that settles to 0 or 1. | Greeks-relevant metadata varies by instrument type: - `OptionContract`, `CryptoOption`: full Greeks inputs including `strike_price`, `option_kind` (CALL/PUT), `expiration_ns`, `underlying`, `multiplier`. - `OptionSpread`, `CryptoOptionSpread`: an exchange-defined multi-leg strategy published as a single tradable instrument. Has `underlying`, `expiration_ns`, and `strategy_type` (a venue-defined code). The spread itself carries no `strike_price` or `option_kind`; venue-provided leg details are stored in `info` when the adapter supplies them. Orders execute against the spread as one line. `CryptoOptionSpread` additionally carries `is_inverse` and `settlement_currency` for venues like Deribit. - `BinaryOption`: has `expiration_ns` and `outcome`/`description`, but no `strike_price`, `option_kind`, or `underlying`. ## Subscribing to Greeks Venues like Deribit, Bybit, and OKX publish real-time Greeks alongside their options markets. Nautilus provides two subscription levels: - **Per-instrument Greeks**: subscribe to individual option contracts. - **Option chain slices**: subscribe to an aggregated view of an entire option series. ### Per-instrument Greeks Subscribe to venue-provided Greeks for a single option contract from an actor or strategy: ```python from nautilus_trader.model import ClientId client_id = ClientId("DERIBIT") self.subscribe_option_greeks(instrument_id, client_id=client_id) ``` Handle incoming updates by implementing the `on_option_greeks` handler: ```python def on_option_greeks(self, greeks) -> None: self.log.info( f"{greeks.instrument_id}: " f"delta={greeks.delta:.4f} gamma={greeks.gamma:.6f} " f"vega={greeks.vega:.4f} theta={greeks.theta:.4f} " f"mark_iv={greeks.mark_iv} underlying={greeks.underlying_price}" ) ``` To stop receiving updates: ```python self.unsubscribe_option_greeks(instrument_id, client_id=client_id) ``` ### Option chain subscriptions An option chain subscription aggregates quotes and Greeks across all strikes in an option series into `OptionChainSlice` snapshots. The `DataEngine` creates one Rust `OptionChainManager` per series and owns the lifecycle: creating the manager, routing incoming data, running snapshot timers, and draining wire subscription changes. ```python from nautilus_trader.model import OptionSeriesId from nautilus_trader.model import StrikeRange series_id = OptionSeriesId(...) # venue, underlying, settlement currency, expiry # Subscribe to 5 strikes above and below ATM, snapshot every 1000ms strike_range = StrikeRange.atm_relative(strikes_above=5, strikes_below=5) self.subscribe_option_chain( series_id, strike_range=strike_range, snapshot_interval_ms=1000, ) ``` Handle snapshots by implementing the `on_option_chain` handler: ```python def on_option_chain(self, chain) -> None: for strike in chain.strikes(): call = chain.get_call(strike) put = chain.get_put(strike) if call and call.greeks: self.log.info(f"Call {strike}: delta={call.greeks.delta:.4f}") ``` ### Strike range filtering `StrikeRange` controls which strikes are active in a chain subscription: | Variant | Description | Example | | ------------- | --------------------------------------------------- | -------------------------------- | | `Fixed` | Subscribe to an explicit set of strikes. | `StrikeRange.fixed([...])` | | `AtmRelative` | N strikes above and N below the current ATM strike. | `StrikeRange.atm_relative(5, 5)` | | `AtmPercent` | All strikes within a percentage band around ATM. | `StrikeRange.atm_percent(0.10)` | | `Delta` | Strikes whose call or put delta is near a target. | `StrikeRange.delta(0.25, 0.05)` | For dynamic strike ranges, subscriptions are **deferred until the ATM price is determined**. ATM is derived from the venue reference price in `OptionGreeks.underlying_price`. It can also be seeded from a reference price fetched for the option series via HTTP, allowing instant bootstrap before live WebSocket ticks arrive. As ATM shifts, the active strike set rebalances automatically. `Delta` resolves from venue-provided Greeks: a strike is active when its call or put delta magnitude (calls positive, puts negative, compared by absolute value) falls within `tolerance` of `target`. A typical out-of-the-money target such as `0.25` selects a strike on each side of ATM. Before the ATM reference price is known, `Delta` is deferred like other dynamic ranges. After ATM is known, when no active strike's Greeks match the band (including before any Greeks arrive), `Delta` falls back to an ATM-relative window of five strikes either side of ATM. Before switching from the fallback window to selected delta strikes, the aggregator waits until every fallback leg has Greeks so partial early updates do not drop neighboring strikes. ### Snapshot vs. raw mode The `snapshot_interval_ms` parameter controls publishing behavior: - **Snapshot mode** (`snapshot_interval_ms=1000`): Quotes and Greeks accumulate in a buffer and publish as an `OptionChainSlice` on a timer. Suitable for periodic portfolio rebalancing or UI display. - **Raw mode** (`snapshot_interval_ms=None`): Each quote or Greeks update for an active instrument publishes a slice immediately. Suitable for latency-sensitive strategies that react to individual updates. ## Backtesting option chains Option-chain backtests use the same `OptionChainManager` and `OptionChainAggregator` path as live subscriptions. The prerequisite is a Nautilus Parquet catalog that already contains the option instruments and the per-instrument data needed for the chain: - `QuoteTick` records for each option contract, carrying the replayed best bid and offer. - `OptionGreeks` records for each option contract, carrying delta, implied volatility, convention, and the `underlying_price` used to seed ATM. - `CryptoOption` or `OptionContract` instruments for the same instrument IDs. Tardis replays satisfy this contract when option book snapshots or quotes are written as `QuoteTick` and `option_summary` messages are written as `OptionGreeks`. The backtest does not download or request missing catalog data during the run. Configure a `BacktestNode` run with both data streams for the option instruments in the series: ```python data = [ BacktestDataConfig( data_type="QuoteTick", catalog_path="/path/to/catalog", instrument_ids=option_instrument_ids, ), BacktestDataConfig( data_type="OptionGreeks", catalog_path="/path/to/catalog", instrument_ids=option_instrument_ids, ), ] ``` Then subscribe from the strategy: ```python strike_range = StrikeRange.delta(0.25, 0.05) self.subscribe_option_chain( series_id, strike_range=strike_range, snapshot_interval_ms=1000, ) ``` Use `snapshot_interval_ms=None` for raw mode. Raw mode publishes a slice after each quote or Greeks update for an active instrument. Use an integer interval for thinned snapshots. Thinned mode accumulates the latest BBO and Greeks per instrument and publishes the chain on the timer cadence, reducing event volume for large chains. Each `OptionChainSlice` joins the latest BBO and Greeks by instrument, then groups the result by strike and option kind. A quote can arrive before Greeks, and Greeks can arrive before a quote; the aggregator keeps latest state and attaches both when available. The `underlying_price` in `OptionGreeks` drives ATM detection. Selection can happen either in the subscription range or inside the strategy: - Moneyness: use `StrikeRange.atm_relative(...)` or `StrikeRange.atm_percent(...)`. - Delta: use `StrikeRange.delta(target, tolerance)`, or inspect `entry.greeks.delta` in `on_option_chain`. - Strike: use `StrikeRange.fixed([...])`, or read `chain.get_call(strike)` and `chain.get_put(strike)`. Matching is quote-driven for options. Market orders and marketable limits fill as takers against the opposing replayed BBO. Passive limit orders rest on the simulated book and can fill as makers when later BBO updates trade through the limit price. The model does not simulate L2 queue position for options. Structural option fee models are configured on the simulated venue, not inferred from the venue name: ```python from decimal import Decimal from nautilus_trader.execution import CappedOptionFeeModel from nautilus_trader.execution import TieredNotionalOptionFeeModel deribit_like = CappedOptionFeeModel( maker_rate=Decimal("0.0003"), taker_rate=Decimal("0.0003"), ) okx_like = TieredNotionalOptionFeeModel( maker_rate=Decimal("0.0002"), taker_rate=Decimal("0.0005"), ) ``` Pass one of these objects as `fee_model` on `BacktestVenueConfig`. The Rust surface uses `FeeModelAny::CappedOption(CappedOptionFeeModel::new(...))` and `FeeModelAny::TieredNotionalOption(TieredNotionalOptionFeeModel::new(...))`. See `examples/backtest/tardis_option_chain.py` and the Rust `tardis-option-chain` example in `crates/backtest/examples/`. ## Option chain architecture The option chain system is event-driven and built around per-series isolation. The `DataEngine` creates one Rust `OptionChainManager` per subscribed option series. The manager wraps `OptionChainAggregator` and `AtmTracker`, registers message bus handlers, publishes snapshots, and queues wire subscription changes for the engine to drain. ```mermaid flowchart TD subgraph DataEngine DE[DataEngine] end subgraph "OptionChainManager (per series)" MGR[OptionChainManager] AGG[OptionChainAggregator] ATM[AtmTracker] TMR[SnapshotTimer] end DC[DataClient] -- QuoteTick --> DE DC -- OptionGreeks --> DE DE -- "handle_quote()" --> MGR DE -- "handle_greeks()" --> MGR MGR --> AGG MGR --> ATM ATM -- "reference price" --> AGG TMR -- "timer tick" --> MGR MGR -- "OptionChainSlice" --> MB((MessageBus)) MB -- "on_option_chain" --> S[DataActor / Strategy] DE -- "sub/unsub" --> DC ``` ### Component responsibilities #### DataEngine Holds one `OptionChainManager` per active `OptionSeriesId`. On `SubscribeOptionChain`, it resolves instruments from the cache, requests a series reference price for dynamic strike ranges, creates the manager, subscribes active instruments to the data client, and sets up the snapshot timer. On each timer tick, the manager checks for rebalances, publishes a snapshot, and queues any wire subscription changes for the engine to drain. On `UnsubscribeOptionChain` or when all instruments expire, it tears down the manager, cancels the timer, and unsubscribes wire-level feeds. #### OptionChainManager A per-series Rust manager around `OptionChainAggregator` and `AtmTracker`. The `DataEngine` feeds it market data through `handle_quote()` and `handle_greeks()`. In snapshot mode, timer callbacks call `publish_slice()`. In raw mode, each active quote or Greeks update calls `publish_slice()` immediately. The manager bootstraps the active instrument set internally on the first ATM price. #### OptionChainAggregator Accumulates quotes and Greeks into call/put buffers using **keep-latest semantics**. Instruments that did not update since the last snapshot are still included. Greeks that arrive before any quote for an instrument are held in a `pending_greeks` buffer and attached when the first quote arrives. On each `snapshot()` call, the aggregator produces an immutable `OptionChainSlice`. #### AtmTracker Derives the ATM price reactively from the `underlying_price` field in incoming `OptionGreeks` events. It can be pre-seeded from an HTTP reference price for the option series, allowing instant bootstrap without waiting for WebSocket ticks. ### Bootstrap and rebalancing For dynamic strike ranges (`AtmRelative`, `AtmPercent`, and `Delta`), the active instrument set cannot be determined until the ATM price is known. There are two bootstrap paths: **Instant bootstrap (reference price available):** 1. `DataEngine` receives `SubscribeOptionChain`, resolves all instruments for the series from the cache, and requests a reference price from the data client. 2. When the reference price response arrives, the engine creates the manager with the ATM price pre-seeded. The manager computes the active strike set during construction. 3. The engine subscribes the active instruments immediately. **Deferred bootstrap (no reference price):** 1. The engine has no matching client or cached option instrument, the client reports no reference price, the request fails, or the request times out after 30 seconds. 2. The engine creates the manager with no initial ATM price. The active set is empty. When the request reached a client with a cached sample option, the engine subscribes that sample's Greeks as the bootstrap source. Without a client or sample, bootstrap still depends on relevant Greeks data already flowing from another subscription. 3. When the engine feeds an `OptionGreeks` event with `underlying_price` through `handle_greeks()`, the manager bootstraps the active instrument set, registers message bus handlers, and queues the new wire subscriptions for the engine to drain. The sample subscription becomes part of the active set or is released. Once bootstrapped, the aggregator monitors ATM drift. On each snapshot timer tick, the manager calls the aggregator's `check_rebalance()` which returns any instruments to add or remove. A hysteresis threshold and cooldown period prevent thrashing near strike boundaries. ## OptionGreeks data type `OptionGreeks` carries venue-provided sensitivities and implied volatility for a single option contract: | Field | Type | Description | | ------------------ | ------------------ | --------------------------------------------------- | | `instrument_id` | `InstrumentId` | The option contract these Greeks apply to. | | `convention` | `GreeksConvention` | Numeraire convention for the Greeks. | | `delta` | `float` | Rate of change of option price per unit underlying. | | `gamma` | `float` | Rate of change of delta per unit underlying. | | `vega` | `float` | Venue-reported vega. | | `theta` | `float` | Venue-reported theta. | | `rho` | `float` | Venue-reported rho; defaults to zero. | | `mark_iv` | `float` or None | Mark implied volatility. | | `bid_iv` | `float` or None | Bid implied volatility. | | `ask_iv` | `float` or None | Ask implied volatility. | | `underlying_price` | `float` or None | Underlying price at time of calculation. | | `open_interest` | `float` or None | Open interest for the contract. | | `ts_event` | `int` | UNIX timestamp (nanoseconds) of the event. | | `ts_init` | `int` | UNIX timestamp (nanoseconds) when initialized. | ## OptionChainSlice data type `OptionChainSlice` is a point-in-time snapshot of an entire option series. Properties: | Property | Type | Description | | ------------ | ---------------- | ----------------------------------- | | `series_id` | `OptionSeriesId` | The option series identifier. | | `atm_strike` | `Price` or None | Current ATM strike (if determined). | | `ts_event` | `int` | UNIX timestamp (nanoseconds). | | `ts_init` | `int` | UNIX timestamp (nanoseconds). | Call and put data are accessed through methods, not as direct properties. Each `OptionStrikeData` returned by these methods contains a `quote` (`QuoteTick`) and an optional `greeks` (`OptionGreeks`) for that strike. Methods: - `strikes()`: all unique strike prices in the chain. - `strike_count()`, `call_count()`, `put_count()`: counts. - `get_call(strike)`, `get_put(strike)`: full `OptionStrikeData`. - `get_call_greeks(strike)`, `get_put_greeks(strike)`: Greeks only. - `get_call_quote(strike)`, `get_put_quote(strike)`: quote only. - `is_empty()`: true if the chain has no data. ## Adapter support The following adapters support option Greeks subscriptions: | Adapter | Per-instrument Greeks | Option chains | | ------------------- | --------------------- | ------------- | | Deribit | Yes | Yes | | Bybit | Yes | Yes | | Derive | Yes | Yes | | Interactive Brokers | Yes | Yes | | OKX | Yes | Yes | ## See also - [Greeks](greeks.md) - Local Greeks calculation and portfolio risk management. - [Data](data/) - Built-in data types and the subscription model. - [Actors](actors.md) - Subscription and handler reference table. # Order Book Source: https://nautilustrader.io/docs/latest/concepts/order_book/ NautilusTrader implements its order books in Rust. `OrderBook` maintains public market depth for an instrument. `OwnOrderBook` tracks your own orders separately so filtered views can subtract them from public liquidity. :::note This guide uses the Rust model API for book operations. Subscription and handler examples use the Python strategy and actor API. Python exposes the book types as `nautilus_trader.model.OrderBook` and `nautilus_trader.model.OwnOrderBook`; see the [model API reference](/docs/python-api-latest/model/book.html) for the Python interface. ::: ## Book types `OrderBook` instances are maintained per instrument for both backtesting and live trading: - `L3_MBO`: Level 3 market-by-order (MBO) data. Tracks every order at every price level, keyed by order ID. On each book side, an order ID maps to exactly one price level: re-adding an ID at a different price moves the order to the new level. MBP-style input uses a price-derived ID. A zero order ID likewise signals missing identity, except that top-of-book input uses the order side as its ID. - `L2_MBP`: Level 2 market-by-price (MBP) data. Aggregates orders by price level (one entry per price). - `L1_MBP`: Level 1 market-by-price (MBP) top-of-book data, also known as best bid and offer (BBO). Captures only the best prices. :::note Quote, trade, and bar data (`QuoteTick`, `TradeTick`, and `Bar`) can also drive `L1_MBP` books. ::: ## Subscribing to book data Strategies and actors subscribe to order book updates through the following methods. Subscriptions and handlers are part of the Python strategy/actor layer: ```python from nautilus_trader.model import BookType from nautilus_trader.model import OrderBook from nautilus_trader.model import OrderBookDeltas from nautilus_trader.model import OrderBookDepth10 # Incremental book deltas self.subscribe_book_deltas(instrument_id, BookType.L2_MBP) # Aggregated depth snapshots (up to 10 levels) self.subscribe_book_depth10(instrument_id, BookType.L2_MBP) # Full book snapshots at a timed interval self.subscribe_book_at_interval(instrument_id, BookType.L2_MBP, interval_ms=1000) ``` Each subscription type delivers data to the corresponding handler: ```python def on_book_deltas(self, deltas: OrderBookDeltas) -> None: ... def on_book_depth(self, depth: OrderBookDepth10) -> None: ... def on_book(self, order_book: OrderBook) -> None: ... ``` ## Accessing the book The `OrderBook` exposes top-of-book accessors: ```rust let best_bid: Option = book.best_bid_price(); let best_ask: Option = book.best_ask_price(); let spread: Option = book.spread(); let midpoint: Option = book.midpoint(); ``` ## Analysis methods The `OrderBook` supports market depth analysis and execution simulation: ```rust // Average fill price for a given quantity let avg_fill_px = book.get_avg_px_for_quantity(quantity, OrderSide::Buy); // Average price, filled quantity, and worst price for a target exposure let (avg_px, filled_qty, worst_px) = book.get_avg_px_qty_for_exposure(target_exposure, OrderSide::Buy); // Cumulative quantity available at or better than a price let qty = book.get_quantity_for_price(price, OrderSide::Buy); // Quantity at a specific price level only let qty = book.get_quantity_at_level(price, OrderSide::Buy, 2); // Simulate fills against the book let fills: Vec<(Price, Quantity)> = book.simulate_fills(&order); // All crossed levels regardless of order quantity let levels = book.get_all_crossed_levels(OrderSide::Buy, price, 2); ``` ## Integrity checks Call `book_check_integrity` to validate that the book state is consistent with its type: - **L1_MBP**: No more than one level per side. - **L2_MBP**: No more than one order per price level. - **L3_MBO**: No additional per-level constraint; multiple orders may share a price. - **All types**: Best bid must not exceed best ask (crossed book). Locked markets (bid == ask) are considered valid. This is an **explicit check**: applying a delta does not call it. The Rust `apply_delta` and `apply_deltas` methods separately validate the incoming instrument ID against the book and return `BookIntegrityError::InstrumentMismatch` on mismatch. For a nonzero order ID, a delta whose side is `None` first tries to resolve the side from the ladder cache. If no side is cached, an `Add` returns `BookIntegrityError::NoOrderSide`, while an `Update` or `Delete` is skipped. If the ID exists on both sides, an `Add` returns `BookIntegrityError::AmbiguousOrderSide`, while an `Update` or `Delete` is skipped with a warning. Out-of-order deltas and depth snapshots are **applied rather than rejected**, so a venue that replays or reorders events still reaches the state those events describe. Only the book metadata is protected: `sequence` and `ts_last` are high-water marks and never regress. A stale update logs one warning for each field that regressed, `sequence` and `ts_event` independently, and how often it logs depends on how the update arrives: - **Incremental deltas**: Once per stale delta. - **Snapshot deltas**: Once per snapshot, whether it arrives as an `F_SNAPSHOT` batch or as a single `F_SNAPSHOT` delta, since every delta in a rebuild shares the snapshot's sequence and timestamp. - **Depth snapshots**: Once, since an `OrderBookDepth10` replaces the book in a single update. A snapshot report describes the incoming snapshot, so it does not depend on whether each of its deltas reaches the book. An `L1_MBP` book driven by quotes or trades is the exception to all of this: a stale `QuoteTick` or `TradeTick` is skipped with a warning and leaves the book unchanged. ## Pretty printing Both `OrderBook` and `OwnOrderBook` provide a `pprint` method that returns the book as a human-readable table: ```rust println!("{}", book.pprint(5, None)); println!("{}", book.pprint(5, Some(Decimal::new(1, 2)))); // group_size = 0.01 ``` The `group_size` parameter buckets price levels into coarser groups for instruments with fine tick sizes. The output is a formatted table with bids on the left, prices in the center, and asks on the right. ## Own order book The `OwnOrderBook` tracks your own working orders separately from the public book. Market making and other quoting strategies use it to estimate available liquidity at each price level after subtracting their own orders. Execution engines maintain own books when `manage_own_order_books` is enabled. The cache updates an existing own book as order events change state. Eligible orders have a price and do not use `IOC` or `FOK` time in force. Terminal events may still clean up an existing own book entry, even when the order would not otherwise be eligible for tracking. ### Order lifecycle The `OwnOrderBook` tracks orders through their lifecycle. Orders are added during submission or materialized from reconciliation. Nonterminal states such as `OrderStatus::Accepted`, `OrderStatus::PendingUpdate`, `OrderStatus::PendingCancel`, and `OrderStatus::PartiallyFilled` update the entry. The closed states `OrderStatus::Denied`, `OrderStatus::Rejected`, `OrderStatus::Canceled`, `OrderStatus::Expired`, `OrderStatus::Filled`, and `OrderStatus::Voided` remove it. Each `OwnBookOrder` carries: - `trader_id`: Trader ID that owns the order. - `client_order_id`: Client order ID used to reconcile the own book with cache state. - `venue_order_id`: Venue order ID when one has been assigned. - `side`, `price`, and `size`: Order side, price, and remaining (leaves) quantity. - `order_type` and `time_in_force`: Order metadata retained for inspection. - `status`: Current order status, such as `SUBMITTED`, `ACCEPTED`, or `PENDING_CANCEL`. - `ts_last`: Timestamp of the latest order event applied to this own-book order. - `ts_accepted`: Timestamp when the venue accepted the order, or zero before acceptance. - `ts_submitted`: Timestamp when the order was submitted, or zero before submission. - `ts_init`: Timestamp when the order was initialized. The `status` and `ts_accepted` fields drive the optional filters described in [Status and time filtering](#status-and-time-filtering). ### Auditing The `audit_open_orders` method reconciles an own book against a set of valid client order IDs. Any own-book order not in the provided set is removed and logged as an audit error. `Cache::audit_own_order_books` builds this set from open, in-flight, and active-local orders so non-terminal entries remain during normal event-processing and venue-latency windows. Live systems can run this audit periodically through the own-books audit interval. ### Querying ```rust // Check if a specific order is tracked let in_book = own_book.is_order_in_book(&client_order_id); // Get all tracked order IDs per side let bid_ids = own_book.bid_client_order_ids(); let ask_ids = own_book.ask_client_order_ids(); // Aggregated quantities per price level let bid_qty = own_book.bid_quantity(None, None, None, None, None); let ask_qty = own_book.ask_quantity(None, None, None, None, None); // Pretty print println!("{}", own_book.pprint(5, None)); ``` ### Filtered views Subtract your own orders from the public book to see net available liquidity: ```rust // Filtered maps of price -> quantity (own orders subtracted) let net_bids = book.bids_filtered_as_map(Some(10), Some(&own_book), None, None, None); let net_asks = book.asks_filtered_as_map(Some(10), Some(&own_book), None, None, None); // Full filtered OrderBook with all analysis methods available let filtered = book.filtered_view(Some(&own_book), Some(10), None, None, None); let avg_px = filtered.get_avg_px_for_quantity(quantity, OrderSide::Buy); ``` The `filtered_view` method returns a new `OrderBook` with your own sizes subtracted, giving access to the full set of analysis methods (`spread`, `midpoint`, `get_avg_px_for_quantity`, etc.) on the net book. ### Status and time filtering Filtered views support optional status and time-based filtering for own orders: ```rust let statuses = AHashSet::from([OrderStatus::Accepted]); // Only subtract ACCEPTED orders (ignore SUBMITTED, PENDING_CANCEL, etc.) let filtered = book.filtered_view(Some(&own_book), None, Some(&statuses), None, None); ``` The `accepted_buffer_ns` parameter provides a grace period. When `ts_now` is set, the view includes an own order only when `ts_accepted + accepted_buffer_ns <= ts_now`. This excludes recently accepted orders that may not yet appear in the public book feed. The time check applies regardless of order status, so combine it with a status filter to exclude non-accepted orders. Omitting `ts_now` disables acceptance-time filtering, and a positive `accepted_buffer_ns` requires `ts_now`. ```rust // Only subtract orders accepted at least 500ms ago let filtered = book.filtered_view( Some(&own_book), None, None, Some(500_000_000), Some(clock.timestamp_ns().as_u64()), ); ``` ## Binary markets Binary markets can expose complementary outcome instruments, such as Polymarket YES and NO tokens. For a known complementary pair, the parity transform maps a price `p` on one outcome to `1 - p` on the other. Under this transform, a NO bid at 0.40 becomes a YES ask at 0.60. The `OwnOrderBook::combined_with_opposite` method handles this transformation, merging orders from both outcome instruments into a view for the first book: ```rust let yes_own = own_yes_book .cloned() .unwrap_or_else(|| OwnOrderBook::new(yes_instrument_id)); let no_own = own_no_book .cloned() .unwrap_or_else(|| OwnOrderBook::new(no_instrument_id)); // Merge NO orders with the parity price transform (1 - price) let combined = yes_own.combined_with_opposite(&no_own).unwrap(); // Filter the public YES book using the combined own book let filtered = book.filtered_view(Some(&combined), None, None, None, None); ``` The transformation works as follows: - NO asks at price `p` become bids at price `1 - p` in the combined book. - NO bids at price `p` become asks at price `1 - p` in the combined book. :::warning The method rejects matching instrument IDs, but it cannot verify that the two instruments are complementary. The caller must supply the actual opposite instrument. The resulting own book can filter the public YES book against your orders in either outcome instrument. ::: # Overview Source: https://nautilustrader.io/docs/latest/concepts/overview/ ## Introduction NautilusTrader is an open-source, production-grade, Rust-native engine for multi-asset, multi-venue trading systems. The system spans research, deterministic simulation, and live execution within a single event-driven architecture, with Python serving as the control plane for strategy logic, configuration, and orchestration. This separation provides the performance and safety of a compiled trading engine with the flexibility of Python for system composition and strategy development. Trading systems can also be written entirely in Rust for mission-critical workloads. The same strategy and execution-algorithm code can run across backtest and live systems, reducing deployment divergence. Live execution still introduces venue, transport, timing, persistence, external-activity, and reconciliation behavior that a simulation may not reproduce. See [Backtest and live differences](live.md#backtest-and-live-differences). NautilusTrader is asset-class-agnostic. Any venue with a REST API or WebSocket feed can be integrated through modular adapters. Integrations span centralized and decentralized crypto exchanges (CEX and DEX), foreign exchange (FX), equities, futures, options, and betting exchanges. ## Features - **Fast**: Rust core with asynchronous networking using [tokio](https://crates.io/crates/tokio). - **Reliable**: Rust provides type safety and thread safety, with optional state persistence backed by Redis or PostgreSQL. - **Portable**: Runs on Linux, macOS, and Windows. Deploy using Docker. - **Flexible**: Modular adapters integrate any REST API or WebSocket feed. - **Advanced orders**: Time-in-force options include `IOC`, `FOK`, `GTC`, `GTD`, `DAY`, `AT_THE_OPEN`, and `AT_THE_CLOSE`. The domain model also supports conditional triggers, `post-only`, `reduce-only`, iceberg, and `OCO`, `OUO`, and `OTO` contingency orders. Venue support varies by adapter. - **Customizable**: User-defined components, or assemble entire systems from scratch using the [cache](cache.md) and [message bus](message_bus.md). - **Backtesting**: Run multiple venues, instruments, and strategies simultaneously using historical quotes, trades, bars, order books, and custom data with nanosecond resolution. - **Live**: Identical strategy implementations between research and live deployment. - **Multi-venue**: Run market-making and cross-venue strategies across multiple venues simultaneously. - **AI training**: High-throughput simulation supports workloads such as training AI trading agents with reinforcement learning (RL) or evolutionary strategies (ES). ## Why NautilusTrader? Trading strategy research typically happens in Python using vectorized approaches, while production trading systems are built separately using event-driven architectures in compiled languages. NautilusTrader removes this separation. A Rust-native core provides a deterministic event-driven runtime for both research and live execution, while Python serves as the control plane. The same architecture, execution semantics, and time model operate across both environments, allowing strategies to move from research to production without reimplementation. Python bindings for the Rust-native runtime are provided via [PyO3](https://pyo3.rs). Installing an official prebuilt Python wheel does not require a Rust toolchain. ## Use cases NautilusTrader supports three main use cases: - Backtest trading systems on historical data (`backtest`). - Simulate trading systems with real-time data and virtual execution (`sandbox`). - Deploy trading systems live on real or paper accounts (`live`). NautilusTrader provides backtest and live node implementations for both Python and Rust. The sandbox adapter supplies simulated execution for a `sandbox` environment. :::note - Examples use these node implementations unless stated otherwise. - A trading strategy is one component of an end-to-end trading system, which also includes application and infrastructure layers. ::: ## Distributed The platform integrates into larger distributed systems. The [external message bus](message_bus.md#encoding) supports JSON and MessagePack payloads, plus Cap'n Proto and Simple Binary Encoding (SBE) for schema-covered market data. Apache Arrow and Parquet provide columnar interchange and persistence through the [data catalog](data/index.md#data-catalog). Format support varies by payload type. ## Common core The common system core is used by all node [environment contexts](architecture.md#environment-contexts): `backtest`, `sandbox`, and `live`. User-defined actors, strategies, and execution algorithms use the same lifecycle across these contexts. ## Backtesting Feed data to a `BacktestEngine` either directly or through a higher-level `BacktestNode` and `ParquetDataCatalog`, then run the data through the system with nanosecond resolution. See [Backtesting](backtesting/) for the APIs and execution model. ## Live trading A `LiveNode` ingests data and events from multiple data and execution clients, supporting demo, paper, and real accounts. The Rust-native node, including its PyO3 interface, runs the kernel event loop on the calling thread, while asynchronous I/O and background tasks use a shared multi-threaded Tokio runtime. See [Live trading](live.md) for the node lifecycle and risk considerations, and [Execution reconciliation](execution/reconciliation.md) for state recovery. ## Domain model The trading domain model includes [value types](value_types.md) such as `Price` and `Quantity`, plus [orders](orders/) and [positions](positions.md) that aggregate events to determine state. ## Timestamps NautilusTrader represents system timestamps as **UNIX nanoseconds**. Its standard ISO 8601 (RFC 3339) formatter uses UTC and preserves all nine fractional digits. A millisecond formatter preserves three fractional digits for selected displays, such as good-till-date (GTD) expiry times. A timestamp string consists of: - Full date component always present: `YYYY-MM-DD`. - `T` separator between date and time components. - Nine fractional digits for nanosecond output, or three for millisecond output. - UTC timezone designated by the `Z` suffix. Example: `2024-01-05T15:30:45.123456789Z` For the complete specification, refer to [RFC 3339: Date and Time on the Internet](https://datatracker.ietf.org/doc/html/rfc3339). ## UUIDs The `UUID4` value type provides random Universally Unique Identifier (UUID) version 4 values for events, commands, reports, and other internal messages. It uses the `uuid` crate to validate version and variant bits when parsing strings. A valid UUID v4 under RFC 9562 consists of: - 32 hexadecimal digits displayed in 5 groups. - Groups separated by hyphens: `8-4-4-4-12` format. - Version 4 designation (indicated by the third group starting with "4"). - IETF variant designation (indicated by the fourth group starting with "8", "9", "a", or "b"). Example: `2d89666b-1a1e-4a75-b193-4eb3b454c757` For the complete specification, see [RFC 9562: Universally Unique Identifiers (UUIDs)](https://www.rfc-editor.org/rfc/rfc9562.html). ## Data types NautilusTrader defines the following built-in market and reference data types. Availability for historical requests and live subscriptions depends on the provider and adapter. See [Data](data/index.md) for their fields and behavior. - `OrderBookDelta` (single order book change) - `OrderBookDeltas` (container type) - `OrderBookDepth10` (fixed depth of 10 levels per side) - `QuoteTick` - `TradeTick` - `Bar` - `MarkPriceUpdate` - `IndexPriceUpdate` - `FundingRateUpdate` - `OptionGreeks` - `Instrument` - `InstrumentStatus` - `InstrumentClose` Use [custom data](custom_data.md) for application-specific types. The following `PriceType` options select granular data for internal bar aggregation: - `BID` - `ASK` - `MID` - `LAST` `BID`, `ASK`, and `MID` use `QuoteTick` data, while `LAST` uses `TradeTick` data. Composite bar types aggregate smaller bars instead. ## Bar aggregations The following `BarAggregation` methods are available: - `MILLISECOND` - `SECOND` - `MINUTE` - `HOUR` - `DAY` - `WEEK` - `MONTH` - `YEAR` - `TICK` - `VOLUME` - `VALUE` (also known as dollar bars) - `RENKO` (price-based bricks) - `TICK_IMBALANCE` - `TICK_RUNS` - `VOLUME_IMBALANCE` - `VOLUME_RUNS` - `VALUE_IMBALANCE` - `VALUE_RUNS` All listed aggregations are implemented for internal aggregation. Information-driven aggregations require `TradeTick` data. A `BarSpecification` combines a price type, aggregation method, and positive step size. Fixed-subunit time bars have divisibility limits; see [Bar types](data/index.md#bar-types) for the validation rules. Internal aggregation can run during live trading when the required input data is available. ## Account types The [accounting engine](accounting.md#account-types) supports the following configurations in both live and backtest environments: - `Cash` single-currency (base currency) - `Cash` multi-currency - `Margin` single-currency (base currency) - `Margin` multi-currency - `Betting` single-currency - `Wallet` multi-currency (blockchain wallets; execution client in development) ## Order types The [order model](orders/) supports the following types, subject to venue adapter support: - `MARKET` - `LIMIT` - `STOP_MARKET` - `STOP_LIMIT` - `MARKET_TO_LIMIT` - `MARKET_IF_TOUCHED` - `LIMIT_IF_TOUCHED` - `TRAILING_STOP_MARKET` - `TRAILING_STOP_LIMIT` ## Value types The following fixed-point value types are backed by either 128-bit or 64-bit raw integers, depending on the [precision mode](../getting_started/installation.md#precision-mode) used during compilation. - `Price` - `Quantity` - `Money` Official Python wheels use high-precision mode on all supported platforms. Pure Rust builds default to standard precision unless the `high-precision` feature is enabled. ### High-precision mode (128-bit) When the `high-precision` feature flag is **enabled**, values use the specification: | Type | Raw backing | Max precision | Min value | Max value | | :--------- | :---------- | :------------ | :------------------ | :----------------- | | `Price` | `i128` | 16 | -17,014,118,346,046 | 17,014,118,346,046 | | `Money` | `i128` | 16 | -17,014,118,346,046 | 17,014,118,346,046 | | `Quantity` | `u128` | 16 | 0 | 34,028,236,692,093 | ### Standard-precision mode (64-bit) When the `high-precision` feature flag is **disabled**, values use the specification: | Type | Raw backing | Max precision | Min value | Max value | | :--------- | :---------- | :------------ | :------------- | :------------- | | `Price` | `i64` | 9 | -9,223,372,036 | 9,223,372,036 | | `Money` | `i64` | 9 | -9,223,372,036 | 9,223,372,036 | | `Quantity` | `u64` | 9 | 0 | 18,446,744,073 | # Portfolio Source: https://nautilustrader.io/docs/latest/concepts/portfolio/ The Portfolio maintains account and position-derived state for a trading node or backtest. Strategies use it to query accounts, PnL, exposure, margin, equity, and performance statistics. ## Currency conversion The Python `Portfolio` can convert PnL and exposure from native cost currencies to an account base currency or an explicit target currency. This supports instruments with different cost currencies and accounts with different base currencies. ### Supported conversions Currency conversion is available for the following portfolio queries: - `realized_pnl()` and `realized_pnls()` convert realized PnL. - `unrealized_pnl()` and `unrealized_pnls()` convert unrealized PnL. - `total_pnl()` and `total_pnls()` convert total PnL. - `net_exposure()` and `net_exposures()` convert net exposure. All eight methods accept an optional `target_currency`. A successful targeted query contains only that currency. The Portfolio converts each native value directly to the target, even when an account has a different base currency. ### Single account behavior With `PortfolioConfig.convert_to_account_base_currency=true` (the default), a query for one account without `target_currency` converts values to the account's base currency when it has one. Otherwise, the result remains in its native cost currency. ```python # Returns exposure in the account's base currency (e.g., USD) exposure = portfolio.net_exposures(venue=BINANCE, account_id=account_id) ``` ### Multi-account behavior When querying multiple accounts without `target_currency`, the output depends on whether the method returns a currency map or one `Money` value: - Collection methods can return a dictionary with one entry per output currency, using account base currencies where configured and native cost currencies otherwise. Provide `target_currency` to aggregate the complete result into one currency. - Single-value methods return `None` if values from different accounts cannot resolve to one output currency. `net_exposures()` also returns `None` if one instrument spans accounts with different base currencies because its per-instrument exposure cannot resolve to one output currency. ```python # Multiple accounts with the same base currency exposures = portfolio.net_exposures(venue=BINANCE) # Returns {USD: Money(...)} # Accounts with different base currencies and separately resolvable instruments exposures = portfolio.net_exposures(venue=BINANCE) # Returns {USD: Money(...), EUR: Money(...)} # Force single currency across accounts exposures = portfolio.net_exposures(venue=BINANCE, target_currency=USD) # Returns {USD: Money(...)} ``` ### Calculation failures PnL and exposure queries fail closed when any required price, xrate, or exact arithmetic operation is unavailable. Their Python behavior depends on the method type: - Single-value methods (`realized_pnl`, `unrealized_pnl`, `total_pnl`, and `net_exposure`) return `None`. - `realized_pnls`, `unrealized_pnls`, and `total_pnls` raise `RuntimeError`. - `net_exposures` returns `None`. Collection queries **fail as one unit**. They never return a partial result or combine target and source currencies. For example, one unpriced instrument invalidates the whole `unrealized_pnls` or `total_pnls` result. A valid all-scope `net_exposures()` query returns `{}` when the portfolio is flat. :::warning Exchange rate data must be available when using `target_currency` for cross-currency aggregation. ::: ### Conversion price types Position valuation prefers a current `MARK` price when `use_mark_prices` is enabled. Otherwise, it uses `BID` for a long position and `ASK` for a short position before trying the remaining price fallbacks. Currency conversion uses a current `MID` xrate from the cache. If `use_mark_xrates` is enabled, a current `MARK` xrate takes precedence and `MID` remains the fallback. Explicit target-currency queries do not reuse a carried stale xrate. ### Exposure aggregation `net_exposure()` values each open position for one instrument before adding long notional and subtracting short notional. It returns the magnitude of that net valued notional, so the result does not retain direction. A caller-supplied `price` values every selected position at the same price. Without an override or common mark price, side-specific bid and ask prices can leave a valuation residual for equal opposing quantities. `net_exposures()` groups and sums nonzero per-instrument magnitudes by output currency. It does not net directional exposure between different instruments. ### Price overrides The Python methods `unrealized_pnl`, `total_pnl`, and `net_exposure` accept an optional `price`. When supplied, the Portfolio values the selected instrument at that price instead of reading a cached market price. The calculation is fresh: it does not replace the cached PnL, exposure, or market price used by later queries. ## Equity and mark-to-market The Portfolio exposes pull-style queries for continuous portfolio valuation and recorded snapshots. Per-currency results use the relevant account base currency or native cost currency. | Method | Returns | | ---------------------------------------------- | ------------------------------------------------------ | | `mark_values(venue, account_id)` | Signed MTM totals for open positions. | | `equity(venue, account_id)` | Total equity combining balance and position valuation. | | `build_snapshot(account_id)` | Account-wide MTM totals and valuation metadata. | | `snapshots(account_id)` | Recorded account snapshots in emission order. | | `missing_price_instruments(venue, account_id)` | Instruments currently flagged as unpriceable. | Longs contribute positive notional, shorts contribute negative notional. Flat positions are skipped. An account-scoped `equity()` query returns `{}` for an unknown account. For a known account, it raises `RuntimeError` if exact snapshot valuation fails instead of presenting the failure as empty equity. ### Equity formula Equity combines the account balance with open-position valuation, using a different second term depending on account type: - **Cash accounts without a base currency and Wallet accounts**: Start with `balances_total`. For positions owned by that account, do not add a base-asset mark value when the balance already holds that asset and the instrument's cost currency differs from its base currency. Add mark values for inverse instruments and positions not represented by a credited balance asset. - **Cash accounts with a base currency and betting accounts**: `balances_total + Σ mark_value(open positions)`. - **Margin accounts**: `balances_total + Σ unrealized_pnl(open positions)`. `mark_values()` always returns gross open-position values, including assets already present in a multi-currency Cash or Wallet balance. The value-once rule means `equity()` and equity snapshots count each non-inverse base asset either as a balance or a mark value, not both. The margin path uses the same cached unrealized PnL pipeline that powers `unrealized_pnls()`. ### Price fallback Valuation asks `Cache` for a price in this order, stopping at the first match: 1. Mark price, if `use_mark_prices=true` (the default) in `PortfolioConfig` and a mark price is cached. 2. Side-appropriate quote: `BID` for longs, `ASK` for shorts. 3. Last trade price. 4. Most recent cached bar close (populated when `bar_updates=true`). Set `use_mark_prices=false` to skip the mark tier and begin with the side-appropriate quote. If none of the four yield a current price, the Portfolio carries the last valid price for that instrument and position side. The next snapshot lists the instrument in `stale_instruments`. If the position has never had a valid price, it goes into the missing-price tracker, is listed in `unpriced_instruments`, and is excluded from the sum. ### Base currency conversion When `convert_to_account_base_currency=true` (the default) and the account has a `base_currency` set, cost-currency values are converted to the base currency using `MID` xrates from `Cache.get_xrate()`. With `use_mark_xrates=true`, the cached mark xrate from `Cache.get_mark_xrate()` is used first and falls back to `MID` if unavailable. The output dictionary then has a single key matching the base currency. When `convert_to_account_base_currency=false`, or the account has no `base_currency`, results are keyed by each position's native cost currency and no xrate conversion is applied. If no current xrate is available for a required conversion, the Portfolio carries the last valid rate and lists its source currency in `stale_currencies`. If no valid rate has ever been available, the position is treated as unpriceable and flagged through the missing-price tracker rather than silently valued at a 1.0 rate. ### Snapshot valuation metadata `PortfolioSnapshot.total_equity` always provides the per-currency MTM breakdown. When base-currency conversion is enabled and the account has a base currency, `base_currency_equity` provides the headline scalar in that currency. It is `None` when conversion is disabled or the account has no base currency. `is_stale` is true when the snapshot uses a carried price or xrate, or excludes a position that has never had all required valuation inputs. The related fields identify the cause: - `stale_instruments`: Instruments valued with carried prices. - `stale_currencies`: Source currencies converted with carried xrates. - `unpriced_instruments`: Instruments excluded because no complete valid valuation has ever been available. Call `build_snapshot(account_id)` for an on-demand sample. Call `snapshots(account_id)` to read the bounded recorded sequence. The methods are available from the Rust Portfolio and Strategy API and from the Python Portfolio binding. Building a snapshot does not add it to the recorded sequence; only the configured lifecycle emission records snapshots. ### Automatic equity curve `PortfolioConfig.equity_curve=true` (the default) records and publishes a mark-to-market snapshot when each account registers, at every UTC midnight even while the account is flat, and when the backtest or live node shuts down. Set `equity_curve=false` for workloads such as optimizer runs that do not consume an equity curve. On-demand `equity()` and `build_snapshot()` calculations remain available. The separate `snapshot_interval_ms` setting remains opt-in. When set, it adds fine-grained snapshots only while the account has an open position. ### Missing-price tracking The tracker keeps the latest missing set for each account-filtered query scope and the unfiltered venue scope. `missing_price_instruments(venue)` returns their venue-wide union. Pass `account_id` to return only that account's current set. Each observation remains authoritative until the same scope runs again; a filtered result does not declare an earlier unfiltered result resolved. It has two observable behaviors: - A warning log fires once per instrument on the transition from no scope reporting it to at least one scope reporting it, not on every subsequent call. Once every reporting scope observes recovery, a future drop re-warns. - When a venue goes flat (no open positions), its tracker entry is cleared so stale instruments do not remain flagged. Call `missing_price_instruments(venue)` to inspect the current set. :::tip If `equity()` understates what you expect, check `missing_price_instruments(venue)` before investigating the math. An instrument without a usable mark, quote, trade, or bar price is excluded from the total and appears in the missing-price tracker. ::: ### Venue and account scope Python collection queries accept optional `venue` and `account_id` scopes. If both are provided, they must resolve to the same account or the query raises `ValueError`. With `account_id=None`, a venue query aggregates across every account on that venue. An account-filtered valuation reconciles only that account's observation, so flags raised by other accounts on the same venue survive. ### Python query boundary The Python Portfolio is a read-only query facade for portfolio state. It does not expose initialization, reset, or update commands; the Rust engine remains responsible for authoritative mutation. Statistic registration is the exception, because it configures analysis rather than portfolio state. The facade also does not expose the internal recorded realized-PnL cache. `account()` returns a detached, point-in-time copy. The copy **does not reflect later account updates**, and changing it does not affect the Portfolio. Call `account()` again to obtain the latest account state. ## Portfolio statistics `Portfolio.statistics()` computes a new `PortfolioStatistics` value from all accounts, cached positions, position snapshots, recorded close-time PnLs, and portfolio snapshots. It recomputes the statistics on every call, so invoke it sparingly on hot paths. The result contains: - PnL statistics for each currency. - Return statistics from the preferred return series described below. - General statistics derived from positions. The default set includes `WinRate`, `ProfitFactor`, `SharpeRatio`, and `LongRatio`. See the [Analysis API Reference](/docs/python-api-latest/analysis.html) for all built-in statistic types. Pass another built-in type, such as `MaxDrawdown`, to `Portfolio.register_statistic()` to add it to `Portfolio.statistics()`, backtest results, and post-run logs. A standalone `PortfolioAnalyzer` keeps its own registrations, which do not reach the Portfolio. After a backtest, `engine.get_result()` exposes these categories through `stats_pnls`, `stats_returns`, and `stats_general`, plus the selected `returns_series`. When `run_analysis=true`, the engine also logs the three statistic categories under `PORTFOLIO PERFORMANCE` after the run. ### Custom statistics Subclass `PortfolioStatistic` and override the calculation methods for the input categories the statistic supports. Each category receives the same data the built-in statistics use, and a statistic contributes a value only where it overrides the matching method. `Portfolio.statistics()` feeds the first three categories; benchmark-relative results come from `PortfolioAnalyzer.get_performance_stats_returns_vs_benchmark()`, which takes the benchmark series from the caller. | Method | Input | Result category | | --------------------------------------- | -------------------------------------------- | ------------------------ | | `calculate_from_returns` | Returns keyed by UNIX nanoseconds | `returns` | | `calculate_from_realized_pnls` | Realized PnLs for one currency, oldest first | `pnls` | | `calculate_from_positions` | Positions and position snapshots | `general` | | `calculate_from_returns_with_benchmark` | Strategy returns and benchmark returns | `PortfolioAnalyzer` only | ```python from nautilus_trader.analysis import PortfolioStatistic class TradeCount(PortfolioStatistic): def calculate_from_realized_pnls(self, realized_pnls: list[float]) -> float | None: return float(len(realized_pnls)) engine.portfolio.register_statistic(TradeCount()) ``` The name defaults to the class name split on word boundaries, so `TradeCount` registers as "Trade Count". Override the `name` property to choose the name directly. Registering a statistic whose name matches an existing one replaces it, and `Portfolio.deregister_statistic()` removes one by name. A registration persists across `Portfolio.statistics()` calls and analyzer state resets, so it reaches backtest results and post-run logs for the whole run. Register before the run to cover every query. Return a `float`, or `None` when the metric is undefined for the given data. Define the result for empty or insufficient data: return `None` when the metric is unknown, or use a domain-appropriate default such as `0.0`. A statistic that raises, or that returns a non-numeric value, logs an error and contributes no value for that category; the remaining statistics still calculate. Every category is called for a registered statistic, including on a run that closed no trades, where the PnL category receives an empty list. :::warning A calculation method runs while the Portfolio holds its internal state borrowed, so calling a Portfolio method that mutates state from inside one panics. Keep a statistic a pure function of the data it is given. ::: ## Returns: position vs portfolio The analyzer tracks two distinct return series: - **Position returns** (`analyzer.position_returns()`) measure realized return per position as a side-aware price return relative to the average open price. This reflects the instrument's price movement between entry and exit, independent of account size or leverage. - **Portfolio returns** (`analyzer.portfolio_returns()`) measure daily percentage change in mark-to-market account equity. A $900 gain on a $100,000 account reports roughly 0.9% for that day. When complete portfolio snapshots span at least two distinct UTC dates, the analyzer uses the final snapshot from each date and computes portfolio returns automatically. It uses them as the primary series for statistics, tearsheets, and the monthly returns heatmap. A snapshot emitted exactly at UTC midnight closes the preceding date, keeping the daily tier consistent with fine-grained samples. The first valid registration sample anchors the opening value for a partial first date. Missing or unpriced account dates are forward-filled after every account has an initial valid sample. Multiple snapshots on the same date count as one date, so intra-day trading alone does not produce portfolio returns. When portfolio returns are unavailable, the analyzer falls back to position returns; Python tearsheets can also fall back to account reports. The convenience accessor `analyzer.returns()` resolves this preference: portfolio returns when present, position returns otherwise. ### Multi-currency accounts Portfolio returns require every account's snapshot equity to resolve to one common currency. Base-currency conversion normally provides that scalar. When snapshots expose multiple currencies or accounts resolve to different currencies, the analyzer falls back to position returns. An explicit tearsheet currency can select matching per-currency equity where available. If you need portfolio-level returns for a multi-currency account, compute them externally by converting balances to a common currency before calculating percentage changes. ### Multi-account calculation Backtest analysis aggregates all cached accounts after resolving them to a common currency. The tearsheet follows the same account-wide aggregation rule for multi-venue backtests. ## Related guides - [Positions](positions.md): Position tracking within portfolios. - [Reports](reports.md): Generate portfolio analysis reports. - [Visualization](visualization.md): Visualize portfolio performance. # Positions Source: https://nautilustrader.io/docs/latest/concepts/positions/ This guide explains how NautilusTrader creates and updates positions from order fills, calculates profit and loss (PnL), and preserves closed cycles under `NETTING` order management system (OMS) configurations. ## Overview A position records exposure to an instrument during an open-close cycle. It aggregates the fills assigned to one position ID and tracks its quantity, average prices, realized PnL, commissions, and related identifiers. Use a market price with the position's methods to calculate unrealized PnL and notional value. The execution engine creates positions when orders fill and tracks them from open to close. OMS configuration determines whether fills share a net position or remain in separate hedged positions. ## Position lifecycle ### Creation The system opens a position on the first fill: - **NETTING OMS**: Opens on the first fill for an instrument and strategy. The position uses the deterministic ID `{instrument_id}-{strategy_id}`. - **HEDGING OMS**: Opens on first fill for a new `position_id` (multiple positions per instrument). A position tracks: - Opening order and fill details. - Entry side (`BUY` or `SELL`). - Quantity and average entry price after applying the opening fill. - Timestamps for initialization and opening. :::tip You can access positions through the Cache using `self.cache.position(position_id)` or `self.cache.positions(instrument_id=instrument_id)` from within your actors/strategies. ::: ### Updates As additional fills occur, the position: - Aggregates quantities from buy and sell fills. - Recalculates average entry and exit prices. - Updates peak quantity for the current cycle. - Tracks the current cycle's order IDs and trade IDs. - Accumulates commissions by currency. ### Closure A position closes when the **net quantity becomes zero** (`FLAT`). At closure: - The closing order ID is recorded. - Duration is calculated from open to close. - Final realized PnL is computed. - In either OMS type, when the position later reopens under the same ID, the engine snapshots the closed state to preserve historical PnL (see [Position snapshotting](#position-snapshotting)). ## Order fill aggregation Positions aggregate order fills to maintain an accurate view of market exposure. The aggregation process handles both sides of trading activity: ### Buy fills When a BUY order fills: - Increases long exposure or reduces short exposure. - Updates average entry price for opening trades. - Updates average exit price for closing trades. - Calculates realized PnL for any closed portion. ### Sell fills When a SELL order fills: - Increases short exposure or reduces long exposure. - Updates average entry price for opening trades. - Updates average exit price for closing trades. - Calculates realized PnL for any closed portion. ### Net position calculation The position maintains a `signed_qty` field representing the net exposure: - Positive values indicate `LONG` positions. - Negative values indicate `SHORT` positions. - Zero indicates a `FLAT` (closed) position. ```python # Example: Position aggregation # Initial BUY 100 units at $50 signed_qty = +100 # LONG position # Subsequent SELL 150 units at $55 signed_qty = -50 # Closes the LONG cycle and opens a SHORT cycle # Final BUY 50 units at $52 signed_qty = 0 # Position FLAT (closed) ``` ### Reversal accounting An opposite-side fill larger than the open quantity closes the existing exposure and opens the residual in the other direction. The execution engine splits this fill into a close and a new opening, with closed-state retention governed by [Position snapshotting](#position-snapshotting). When an unsplit reversal fill is applied directly to one `Position`, including during fill-void replay, the position starts a new accounting episode for the residual exposure: - `avg_px_open` becomes the reversal fill price. - `avg_px_close` becomes `None`, and `realized_return` becomes zero until a subsequent closing fill. - `buy_qty` and `sell_qty` restart with only the opening residual on the new entry side and zero on the other side. Later close averages therefore exclude volume from the previous direction. **Only the closing portion** realizes PnL against the previous entry price. The object's `realized_pnl` and commission totals remain cumulative across this reversal, with the fill's commission counted once. The reset does not clear fill history, opening timestamps, or peak quantity. If the position instead reaches `FLAT` and a later fill reopens it, the full cycle resets, including realized PnL and commissions. These episode resets apply to fill-driven reversals; a quantity adjustment that changes the position's side does not perform the same reset. ## Position adjustments Position adjustments record quantity or PnL changes that occur outside normal order fills. The system represents these changes as `PositionAdjusted` events. ### Base-currency commissions When trading spot currency pairs (for example, BTC/USDT) or FX spot, commissions paid in the base currency directly affect the net quantity received or delivered: - **Opening fills**: Commission is deducted from the traded quantity. A buy of 1.0 BTC with 0.001 BTC commission results in a net long position of 0.999 BTC. - **Closing fills**: Commission is applied to `signed_qty` because it affects actual inventory. Selling a 0.999 BTC LONG position with 0.000999 BTC commission leaves you SHORT 0.000999 BTC, not FLAT, because you gave up 0.999999 BTC total. - **Flips**: Commission affects the final position size on both sides of the flip. :::note Base-currency commissions only apply to spot currency pairs and FX spot instruments where the commission currency matches `instrument.base_currency`. For other instruments, commissions are tracked separately and do not affect position quantity. ::: ### Funding payments Funding adjustments track periodic payments for perpetual futures without affecting position quantity. They use `quantity_change = None` and can include a PnL change. ### Adjustment tracking The position exposes its retained adjustments: - `position.adjustments()` returns the list of all `PositionAdjusted` events. - Each adjustment includes its type (`COMMISSION` or `FUNDING`), quantity or PnL change, reason, event ID, and timestamps. - The current adjustment history is cleared when a closed position reopens. - If fills remain after `purge_events_for_order()`, the position regenerates commission adjustments from the surviving fills and reapplies non-commission adjustments. If no fills remain, the position becomes an empty `FLAT` shell and clears its adjustment history. ## OMS types and position management NautilusTrader supports two position management modes. A strategy configured with `OmsType.UNSPECIFIED` uses the venue's OMS type. For configuration details and position ID rules, see the [Execution guide](execution/index.md#order-management-system-oms). ### `NETTING` In `NETTING` mode, fills for each instrument and strategy are aggregated into a single position: - One position per instrument and strategy. - All fills contribute to the same position. - A fill that crosses zero closes the current cycle and opens a new cycle on the opposite side. - Historical snapshots preserve closed position states. ### `HEDGING` In `HEDGING` mode, multiple positions can exist for the same instrument: - Multiple simultaneous `LONG` and `SHORT` positions. - Each position has a unique position ID. - Positions are tracked independently. - No automatic netting across positions. - A fill with a new position ID creates a separate position. If a later fill reuses a closed position ID, the engine archives the closed cycle before replacing the cached state. - A virtual position flip creates a new ID and keeps the original closed position in the cache, so that path does not need a closed-cycle snapshot. :::warning `HEDGING` can increase margin requirements when a venue maintains long and short positions independently. A venue with a `NETTING` OMS exposes only its net position, even when NautilusTrader tracks multiple virtual positions. Check the venue's position mode and margin rules. ::: ### Strategy vs venue OMS Strategy and venue OMS types can differ: | Strategy OMS | Venue OMS | Result | | ------------ | --------- | ------------------------------------------------------------------- | | `NETTING` | `NETTING` | One position per instrument and strategy. | | `HEDGING` | `HEDGING` | Multiple positions per instrument and strategy. | | `NETTING` | `HEDGING` | One virtual position across the venue positions. | | `HEDGING` | `NETTING` | Multiple virtual positions against the venue's single net position. | :::tip Align the strategy and venue OMS types unless the strategy requires virtual positions. See the integration guide for the venue's position-mode configuration. ::: ## Position snapshotting Position snapshotting preserves closed cycles for PnL tracking and reporting when a later fill reopens a closed position. ### Why snapshotting matters When a position closes (becomes `FLAT`) and then reopens under the same ID with a new trade, the position object is reset to track the new exposure. Without snapshotting, the historical realized PnL from the previous position cycle would be lost. ### How it works When a fill reopens a closed position under the same ID, the execution engine archives the closed state before opening the next cycle. This applies to both `NETTING` and `HEDGING` OMS. A `HEDGING` flip using a non-virtual ID follows a separate path: it reuses the ID without archiving the closed cycle. The snapshot preserves: - Final quantities and prices. - Realized PnL. - All fill events. - Commission totals. The cache stores snapshots by position ID. The active cache entry then represents the new cycle, while previous snapshots remain accessible. The Portfolio includes their realized PnL in instrument totals. A fill void that corrects a fill from an earlier cycle is the one exception. The correction moves the cycle boundaries the stored snapshots describe, so the engine replaces them with the cycles the corrected history actually closes, keeping each counted once. See [Position replay across NETTING cycles](execution/index.md#position-replay-across-netting-cycles). :::note This closed-cycle archive differs from optional position state snapshots. Setting `snapshot_positions=True` publishes state when a position opens, changes, or closes, while `snapshot_positions_interval_secs` periodically publishes all open positions. A cache with a Redis or Postgres backing also persists these snapshots. Without cache backing, both paths publish snapshots on the in-process message bus without persisting them. See [`LiveExecutionEngineConfig`](/docs/python-api-latest/live.html#nautilus_trader.live.LiveExecutionEngineConfig) for these settings. ::: ### Example scenario ```text # NETTING OMS Example # Cycle 1: Open LONG position BUY 100 units at $50 # Position opens SELL 100 units at $55 # Position closes, PnL = $500 # Snapshot taken preserving $500 realized PnL # Cycle 2: Open SHORT position SELL 50 units at $54 # Position reopens (SHORT) BUY 50 units at $52 # Position closes, PnL = $100 # Snapshot taken preserving $100 realized PnL # Total realized PnL = $500 + $100 = $600 (from snapshots) ``` ## PnL calculations Position PnL calculations account for instrument specifications and market conventions. ### Realized PnL The price component of realized PnL is calculated when fills partially or fully close a position. Commissions in the position's cost currency affect realized PnL as each fill arrives. ```python # For standard instruments # LONG: realized_pnl = (exit_price - entry_price) * closed_quantity * multiplier # SHORT: realized_pnl = (entry_price - exit_price) * closed_quantity * multiplier # For inverse instruments (side-aware) # LONG: realized_pnl = closed_quantity * multiplier * (1/entry_price - 1/exit_price) # SHORT: realized_pnl = closed_quantity * multiplier * (1/exit_price - 1/entry_price) ``` The position side selects the formula. ### Unrealized PnL `unrealized_pnl()` calculates PnL for an open position from the supplied `price`. You can pass a bid, ask, mid, last, or mark price: ```python position.unrealized_pnl(last_price) # Using last traded price position.unrealized_pnl(bid_price) # Conservative for LONG positions position.unrealized_pnl(ask_price) # Conservative for SHORT positions ``` For a `FLAT` position, it returns `Money(0, cost_currency)` regardless of the supplied price. ### Total PnL `total_pnl()` combines the realized and unrealized components: ```python total_pnl = position.total_pnl(current_price) # Returns realized_pnl + unrealized_pnl ``` ### Currency considerations - PnL is calculated in the instrument's cost currency: quote for linear contracts, base for inverse contracts, and settlement for quanto contracts. - For Forex, the cost currency is typically the quote currency. - Portfolio aggregates realized PnL per instrument in cost currency. - Multi-currency totals require conversion outside the Position class. ## Commissions and costs Positions track fill commissions: - Commissions are accumulated by currency. - Each fill's commission is added to the running total. - Multiple commission currencies are supported. - Realized PnL includes commissions only when denominated in the position's cost currency. - Other commissions are tracked separately and may require conversion. ```python commissions = position.commissions() # Returns list[Money] with aggregated commission totals per currency notional = position.notional_value(current_price) # Returns Money in quote (linear), base (inverse), or settlement currency (quanto) ``` :::warning In Python, `notional_value()` raises `ValueError` if an inverse position lacks a base currency, the supplied inverse price is not positive, or the result cannot be represented as `Money`. Rust callers can use `try_notional_value()` to handle these calculation errors; `notional_value()` panics if the calculation fails. ::: ## Position properties and state ### Identifiers - `id`: Unique position identifier. - `instrument_id`: The traded instrument. - `account_id`: Account where position is held. - `trader_id`: The trader who owns the position. - `strategy_id`: The strategy managing the position. - `opening_order_id`: Client order ID that opened the position. - `closing_order_id`: Client order ID that closed the position, if closed. ### Position state - `side`: Current position side (`LONG`, `SHORT`, or `FLAT`). - `entry`: Opening side for the current cycle (`BUY` for `LONG`, `SELL` for `SHORT`). Updates when the position reverses direction. - `quantity`: Current absolute position size. - `signed_qty`: Signed position size (positive for `LONG`, negative for `SHORT`). - `peak_qty`: Maximum quantity reached during the current open-close cycle. - `is_open`: Whether position is currently open. - `is_closed`: Whether position is closed (`FLAT`). - `is_long`: Whether position side is `LONG`. - `is_short`: Whether position side is `SHORT`. ### Pricing and valuation - `avg_px_open`: Average entry price. - `avg_px_close`: Average exit price when closing. - `realized_pnl`: Realized profit/loss. - `realized_return`: Realized return as decimal (e.g., 0.05 for 5%). - `quote_currency`: Quote currency of the instrument. - `base_currency`: Base currency if applicable. - `settlement_currency`: Currency for PnL settlement. See [Reversal accounting](#reversal-accounting) for how price averages and returns reset while realized PnL remains cumulative when an unsplit fill reverses one position. ### Instrument specifications - `multiplier`: Contract multiplier. - `price_precision`: Decimal precision for prices. - `size_precision`: Decimal precision for quantities. - `is_inverse`: Whether instrument is inverse. ### Timestamps - `ts_init`: When position was initialized. - `ts_opened`: When position was opened. - `ts_last`: Last update timestamp. - `ts_closed`: When the position was closed, if closed. - `duration_ns`: Duration from open to close in nanoseconds, or zero while open. ### Associated data - `symbol`: The instrument's ticker symbol. - `venue`: The trading venue. - `client_order_ids`: Unique client order IDs for retained fills in the current cycle. - `venue_order_ids`: Unique venue order IDs for retained fills in the current cycle. - `trade_ids`: Unique trade IDs for retained fills in the current cycle. - `events`: Retained order fill events in the current cycle. - `adjustments`: Retained position adjustments in the current cycle. - `event_count`: Number of retained fill events in the current cycle. - `last_event`: Most recently retained fill event. - `last_trade_id`: Trade ID of the most recently retained fill. :::info For complete type information and detailed property documentation, see the Position [API Reference](/docs/python-api-latest/model/position.html#nautilus_trader.model.Position). ::: ## Events and tracking Each `Position` object records the fills and adjustments for its current open-close cycle: - Fill events remain in application order. - Client order, venue order, and trade ID accessors return sorted, unique values. - `event_count` reports the number of retained fill events. - Closed `NETTING` cycles retain their event history in the cache snapshots described above. This data supports: - Detailed position analysis. - Trade reconciliation. - Performance attribution. - Audit trails. :::tip Use `position.events()` to access the current cycle's retained fills for reconciliation. The `position.trade_ids()` result helps match against broker statements. See the [Execution guide](execution/) for reconciliation best practices. ::: ## Numerical precision `Position` uses `f64` for signed quantity, average prices, realized returns, and PnL intermediates. `Price`, `Quantity`, and `Money` retain their fixed-point representations at the API boundary, but conversions between these types and `f64` can introduce rounding. `f64` represents every integer exactly only through `2^53`; above that boundary, conversion can lose low-order bits. It provides roughly 15 to 16 significant decimal digits rather than a fixed number of exact decimal places. The design avoids the higher computational cost of arbitrary-precision arithmetic. The average-price calculation also avoids multiplying raw fixed-point values because those products can overflow their integer representation. Average prices reuse the prior `f64` average, and realized PnL is converted between `Money` and `f64` as fills accumulate. The resulting precision depends on the values, settlement-currency precision, and sequence of fills. `quantity` is derived from `signed_qty` at the instrument's `size_precision`. If that conversion rounds a residual quantity to zero, the position becomes `FLAT` and normalizes `signed_qty` to zero. Inverse PnL calculations reject nonpositive open or close prices and positive prices below `1e-15`. With the `defi` feature, converting a `Price` or `Quantity` with more than 16 decimal places to `f64` panics, so `Position` does not support 17- or 18-decimal fill values. Tests in `crates/model/src/position.rs` cover a `0.01` USD commission, nine-decimal price inputs, 100 sequential fills, prices from `0.00001` to `99999.99999`, and same-price round trips. These cases do not establish a universal precision bound. :::warning If a workflow requires exact decimal arithmetic for regulatory reporting or audit records, perform and retain a separate decimal calculation from the original fills and adjustments. Converting `Position` float outputs back to decimal, including through `signed_decimal_qty()`, cannot restore discarded precision. `Position` does not provide an exact-decimal guarantee. Validate the instruments, currencies, amount ranges, and fill sequences used by the application. ::: ## Integration with other components Positions interact with several key components: - **Portfolio**: Aggregates position exposure and PnL across instruments and strategies. - **ExecutionEngine**: Creates and updates positions from fills. - **Cache**: Stores current position state and closed-cycle snapshots. - **RiskEngine**: Reads open positions when it checks whether an order reduces exposure. :::info Positions are not created for spread instruments. Contingent orders can still trigger for spreads, but they operate without position linkage. The engine handles spread instruments separately from regular positions. ::: ## Related guides - [Events](events/): How fills produce position events. - [Orders](orders/): Orders that create and modify positions. - [Execution](execution/): Fill handling that updates positions. - [Portfolio](portfolio.md): Portfolio-level position aggregation. # Python Source: https://nautilustrader.io/docs/latest/concepts/python/ NautilusTrader provides a Python control surface over the Rust core through PyO3. Use this guide to understand which runtime owns each part of the system, where Python code executes, and which Python interfaces form the supported public contract. For native Rust applications, see [Rust](rust.md). For installation and supported Python versions, see [Installation](../getting_started/installation.md). ## Runtime model The Python package combines Python facades under `nautilus_trader` with the compiled `nautilus_trader._libnautilus` extension. Prebuilt wheels contain the extension and do not require a Rust toolchain at runtime. | Layer | Responsibility | | ------------------ | ---------------------------------------------------------------------------------------------- | | Python application | Configuration, composition, user components, analysis, and integration with Python services. | | PyO3 bindings | Type conversion, argument validation, exceptions, and ownership-safe wrappers over Rust state. | | Rust core | Domain types, engines, nodes, cache, portfolio, message bus, adapters, and persistence. | Python objects such as `Cache` and `Portfolio` are wrappers over Rust-owned state. Nodes and engines keep their internal runtime objects private and expose bounded inspection and control methods. This preserves one source of state while allowing Python code to configure the system and inspect its results. ## User components Python user components subclass the public PyO3 base classes and override their documented callbacks: | Component | Use | | -------------------- | ----------------------------------------------------------------------------- | | `DataActor` | Subscribe to data, handle events, and run non-trading workflows. | | `Strategy` | Implement trading decisions and submit orders. | | `ExecutionAlgorithm` | Split or schedule routed orders through the execution engine. | | `Controller` | Create and manage actors and strategies through `ImportableControllerConfig`. | Application code constructs configs, registers official adapter factories, and adds components to `BacktestNode`, `BacktestEngine`, or `LiveNode`. Rust remains responsible for routing, engine state, order management, accounting, and venue clients. Callbacks execute **synchronously** on the event-processing thread and must return promptly. Blocking I/O, model inference, or long calculations delay market-data handling and order execution. Offload that work to an executor or another process. See [Configure a live trading node](../how_to/configure_live_trading.md) for the live-trading rule. ## Async execution Rust adapter networking runs on Tokio. Python async libraries run on an asyncio event loop; PyO3 does not turn Python coroutines into Tokio tasks. `LiveNode` supports two execution modes: | Method | Execution context | Signal owner | Completion | | ------------- | ---------------------- | ---------------- | ----------------------------------------------------------- | | `run()` | Calling thread; blocks | `LiveNode` | Returns after coordinated shutdown finishes. | | `run_async()` | Python host loop | Host application | Resolves after the same coordinated shutdown path finishes. | `run_async()` lets an asyncio or ASGI application host the node on its existing loop. It drives the same Rust lifecycle as `run()` and leaves `SIGINT` and `SIGTERM` handling to the host. Compatibility is tested with the default asyncio loop, uvloop, and an ASGI lifespan managed by Uvicorn. The Python wheel does not install uvloop or Uvicorn; applications supply their chosen loop and server. An ASGI application whose lifespan constructs a node must run with one worker and without hot reload. Capture `node.cache`, `node.portfolio`, and `node.handle()` before starting `run_async()`. The coroutine owns the node until it finishes, while the captured objects remain usable. Stop a hosted run through `LiveNodeHandle.stop()`, then await the run task for complete shutdown. Cancellation requests the same graceful shutdown before it propagates. Call `node.dispose()` after the task finishes to release the node's resources. A host must wait for the handle to report `Running` before reporting startup complete, supervise the run task for its lifetime, and fail the service if the task completes unexpectedly. See [Hosted event loops](live.md#hosted-event-loops) for the lifecycle, cancellation, fairness, and cache-backing contract. ## Public API contract The generated type stubs under `python/nautilus_trader/` define the supported Rust-bound Python surface. They record public classes, methods, properties, parameters, and return types from the Rust binding sources. The [Python API reference](../api_reference/index.md) renders the same public modules and their documentation. A runtime attribute on a Rust-bound class absent from the generated stubs is not part of the supported contract. The documented Python client, provider, and importable-config classes also form a public interface. PyO3 validates bound arguments before Rust code runs and maps fallible operations to Python exceptions. Code should handle the documented exception type instead of depending on an internal Rust error representation. Generated stubs are source-derived artifacts. Binding changes update the Rust source and regenerate the stubs; the checked-in `.pyi` files are not independent API definitions. :::warning[Side enum compatibility aliases] `OrderSide.NO_ORDER_SIDE` and `PositionSide.NO_POSITION_SIDE` remain available as compatibility aliases for `None`. They are not enum members and may be removed in a future version. Use `None` for optional side values. ::: ## Ownership and lifecycle Rust ownership remains visible at node boundaries: - `BacktestNode` keeps its engines internal. Preserve an engine after a run with `dispose_on_completion=False`, then inspect it through the node's cache, portfolio, statistics, and report methods. - `LiveNode.run_async()` lends the node to its coroutine. State access through the node raises during the run, while `is_running` and `handle()` remain available. A `dispose()` call during the run is a no-op, not a deferred request. Call it again after the run finishes; objects captured before the run remain available until then. - Concurrent `LiveNode` or `BacktestNode` instances in one process are not supported because their runtime state is not isolated. Dispose one node before starting the next, or use separate processes for parallel execution. These boundaries prevent Python references from exposing mutable engine internals or creating multiple owners for the same runtime state. ## Support boundaries Official adapters are implemented in Rust and exposed through Python configs, factories, clients, and data types under `nautilus_trader.adapters`. Their integration guides define the supported venue capabilities. Custom live adapters subclass the Python client bases and register through `LiveNodeBuilder` or `LiveNode.build`. Their async work runs on the node's bound Python event loop. They receive a read-only cache view and emit typed data, events, and reports through queued output. Independent Rust/PyO3 packages can use the same Python protocol with model objects from the installed wheel. See the [Python adapter interface](../developer_guide/python_adapters.md) for the supported hooks, factory/config contract, lifecycle rules, and v1 migration limits. ## Choosing Python or Rust Use Python when application composition, rapid strategy development, analysis tools, or integration with the Python ecosystem matters. Use Rust when the application must run without a Python runtime or needs native traits and direct crate-level control. Both paths use the same Rust domain model and engines. The [Rust capability matrix](rust.md#capability-matrix) shows which components and official adapters are exposed through each path. ## Related guides - [Architecture](architecture.md) - Core components, threading, and dependency flow. - [Rust](rust.md) - Native Rust APIs and runtime use. - [Live trading](live.md) - LiveNode lifecycle and hosted event loops. - [Backtesting](backtesting/) - Backtest engines, nodes, data, and venues. - [Adapters](adapters.md) - Official adapter configuration and routing. - [Migration from v1](../../MIGRATION_V2.md) - Python API changes and migration boundaries. # Reports Source: https://nautilustrader.io/docs/latest/concepts/reports/ This guide explains the portfolio analysis and reporting capabilities provided by the `ReportProvider` class, and how these reports are used for PnL accounting and backtest post-run analysis. ## Overview `ReportProvider` turns cached orders, fills, positions, and account states into pandas DataFrames for analysis and visualization. These reports help you evaluate strategy performance, analyze execution quality, and verify PnL accounting. The same reports are available in backtesting and live trading, which keeps performance evaluation and strategy comparison consistent across both. Reports can be generated using two approaches: - **Backtest methods**: `BacktestEngine.generate_orders_report()` and its siblings read the engine's own cache. `BacktestNode` exposes the same methods, taking the run config ID as the first argument. - **`ReportProvider` directly**: pass any collection of orders or positions, such as a live node's cache or a filtered cache query. Every method returns an empty DataFrame when no matching data exists. Report generation requires pandas, which `nautilus_trader.analysis` imports lazily: the module imports without pandas installed, and the `ImportError` surfaces when you generate a report. The `visualization` extra installs it. ## Available reports The `ReportProvider` class offers several static methods to generate reports from trading data. Each report returns a pandas DataFrame with specific columns and indexing for easy analysis. ### Orders report Generates a full view of all orders: ```python from nautilus_trader.analysis import ReportProvider # From a completed backtest run orders_report = engine.generate_orders_report() # Or from any cache, such as a live node's orders_report = ReportProvider.generate_orders_report(cache.orders()) ``` **Returns `pd.DataFrame`. Columns include:** | Column | Description | | ----------------- | -------------------------------------------------- | | `client_order_id` | Index - unique order identifier. | | `instrument_id` | Trading instrument. | | `strategy_id` | Strategy that created the order. | | `trader_id` | Trader identifier. | | `account_id` | Account identifier (if assigned). | | `venue_order_id` | Venue-assigned order ID (if accepted). | | `side` | BUY or SELL. | | `type` | MARKET, LIMIT, etc. | | `status` | Current order status. | | `quantity` | Original order quantity (string). | | `filled_qty` | Amount filled (string). | | `price` | Limit price (string, order-type dependent). | | `avg_px` | Average fill price (string, if filled). | | `time_in_force` | Time-in-force instruction. | | `ts_init` | Order initialization timestamp (Unix nanoseconds). | | `ts_last` | Last update timestamp (Unix nanoseconds). | Additional columns vary by order type, such as `trigger_price` for stop orders and `expire_time_ns` for GTD orders. See `Order.to_dict()` for the complete field list. ### Order fills report Provides a summary of filled orders (**one row per order**): ```python # From a completed backtest run fills_report = engine.generate_order_fills_report() # Or from any cache fills_report = ReportProvider.generate_order_fills_report(cache.orders()) ``` This report includes only orders with `filled_qty > 0` and contains the same columns as the orders report, but filtered to executed orders only. Note that `ts_init` and `ts_last` are converted to datetime objects in this report for easier analysis. ### Fills report Details individual fill events (**one row per fill**): ```python # From a completed backtest run fills_report = engine.generate_fills_report() # Or from any cache fills_report = ReportProvider.generate_fills_report(cache.orders()) ``` **Returns `pd.DataFrame`. Columns include:** | Column | Description | | ----------------- | ---------------------------------------- | | `client_order_id` | Index - order identifier. | | `trade_id` | Unique trade/fill identifier. | | `venue_order_id` | Venue-assigned order ID. | | `instrument_id` | Trading instrument. | | `strategy_id` | Strategy that created the order. | | `account_id` | Account identifier. | | `position_id` | Associated position ID (if applicable). | | `order_side` | BUY or SELL. | | `order_type` | Order type (MARKET, LIMIT, etc.). | | `last_px` | Fill execution price (string). | | `last_qty` | Fill execution quantity (string). | | `currency` | Currency of the fill. | | `liquidity_side` | MAKER or TAKER. | | `commission` | Commission amount and currency (string). | | `ts_event` | Fill timestamp (datetime). | | `ts_init` | Initialization timestamp (datetime). | See `OrderFilled.to_dict()` for the complete field list; the report drops its `type` column. ### Positions report Position analysis including snapshots: ```python # From a completed backtest run, which includes snapshots automatically positions_report = engine.generate_positions_report() # Or from any cache positions_report = ReportProvider.generate_positions_report( positions=cache.positions(), snapshots=cache.position_snapshots(), # Needed for NETTING OMS totals ) ``` **Returns `pd.DataFrame`. Columns include:** | Column | Description | | ------------------ | ----------------------------------------------------- | | `position_id` | Index - unique position identifier. | | `instrument_id` | Trading instrument. | | `strategy_id` | Strategy that managed the position. | | `trader_id` | Trader identifier. | | `account_id` | Account identifier. | | `opening_order_id` | Order ID that opened the position. | | `closing_order_id` | Order ID that closed the position. | | `entry` | Entry side (BUY or SELL). | | `side` | Position side (LONG, SHORT, or FLAT). | | `quantity` | Current position size (string). | | `peak_qty` | Maximum size reached (string). | | `avg_px_open` | Average entry price (float). | | `avg_px_close` | Average exit price (float, if closed). | | `commissions` | Commissions paid, one entry per currency (list). | | `realized_pnl` | Realized profit/loss in the cost currency (string). | | `realized_return` | Realized return as a ratio (float), so `0.05` is 5%. | | `ts_init` | Position initialization timestamp (Unix nanoseconds). | | `ts_opened` | Opening timestamp (datetime). | | `ts_last` | Last update timestamp (Unix nanoseconds). | | `ts_closed` | Closing timestamp (datetime or NA). | | `duration_ns` | Position duration in nanoseconds. | | `is_snapshot` | Whether this is a historical snapshot. | Snapshot rows are indexed by a generated ID derived from the original position ID, so use `is_snapshot` rather than the index to separate archived cycles from live positions. See `Position.to_dict()` for the complete field list; the report drops `signed_qty`, `base_currency`, `quote_currency`, and `settlement_currency`. ### Account report Tracks account balance and margin changes over time: ```python from nautilus_trader.model import Venue venue = Venue("BINANCE") # From a completed backtest run account_report = engine.generate_account_report(venue=venue) # Or from any cache account_report = ReportProvider.generate_account_report(cache.account_for_venue(venue)) ``` `BacktestEngine.generate_account_report()` requires `venue` or `account_id` and raises `ValueError` when both are omitted. `account_id` takes precedence when both are supplied, and an unknown account yields an empty DataFrame. **Returns `pd.DataFrame`. Columns include:** | Column | Description | | --------------- | ------------------------------------------ | | `ts_event` | Index - timestamp of account state change. | | `account_id` | Account identifier. | | `account_type` | Type of account (e.g., CASH, MARGIN). | | `base_currency` | Base currency for the account. | | `total` | Total balance amount (string). | | `free` | Available balance (string). | | `locked` | Balance locked in orders (string). | | `currency` | Currency of the balance. | | `reported` | Whether balance was reported by venue. | | `margins` | Margin information (list, if applicable). | | `info` | Additional venue-specific information. | Each row represents a balance entry; accounts with multiple currencies produce multiple rows per account state event. ## PnL accounting considerations Accurate PnL accounting requires careful consideration of several factors: ### Position-based PnL - **Realized PnL**: Calculated when positions are partially or fully closed. - **Unrealized PnL**: Marked-to-market using current prices. `Position.unrealized_pnl(last)` marks an open position at a given `Price`. - **Commission impact**: Only included when in the position's cost currency. See [Positions](positions.md) for how base-currency commissions on spot pairs adjust position size instead. :::warning Position snapshots preserve historical PnL when a closed position reopens under the same ID, in either `NETTING` or `HEDGING` OMS. **Include snapshots in reports** for accurate total PnL calculation. See [Position snapshotting](positions.md#position-snapshotting) for reopening and flip behavior. ::: ### Multi-currency accounting When dealing with multiple currencies: - Each position tracks PnL in its cost currency: quote for linear contracts, base for inverse contracts, and settlement for quanto contracts. - Portfolio aggregation requires currency conversion. `Portfolio.realized_pnls(target_currency=...)` does this with cached exchange rates; see [Supported conversions](portfolio.md#supported-conversions). - Commission currencies may differ from the position's cost currency. ```python from decimal import Decimal # Accessing PnL across positions for position in cache.positions_closed(): realized = position.realized_pnl # Money in the position's cost currency, or None if realized is None or realized.currency == base_currency: continue # Converting by hand: cache.get_xrate() returns a float, so wrap rates as Decimal rate = Decimal(str(my_fx_rates[(realized.currency, base_currency)])) converted = realized.as_decimal() * rate ``` ### Snapshot considerations For `NETTING` OMS, an accurate instrument total adds the realized PnL of every archived cycle to the live position. See [Position snapshotting](positions.md#position-snapshotting) for how the execution engine archives a closed cycle. ```python from decimal import Decimal from nautilus_trader.model import Money pnl_by_currency = {} for position in cache.positions(instrument_id=instrument_id): # Archived cycles are stored under the live position's ID snapshots = cache.position_snapshots(position_id=position.id) for pnl in (position.realized_pnl, *(s.realized_pnl for s in snapshots)): if pnl is None: continue running = pnl_by_currency.get(pnl.currency, Decimal(0)) pnl_by_currency[pnl.currency] = running + pnl.as_decimal() # Create Money objects for each currency total_pnls = [Money.from_decimal(amount, currency) for currency, amount in pnl_by_currency.items()] ``` ## Backtest post-run analysis After a backtest completes, analysis is available through result statistics and generated reports. ### Accessing backtest results ```python # After backtest run engine.run() # Access result statistics result = engine.get_result() # Generate reports from the backtest engine fills_report = engine.generate_fills_report() venue = engine.list_venues()[0] account_report = engine.generate_account_report(venue=venue) # Or access data directly for custom analysis orders = engine.cache.orders() positions = engine.cache.positions() snapshots = engine.cache.position_snapshots() ``` ### Portfolio statistics The backtest result provides performance metrics: ```python # Access backtest result statistics result = engine.get_result() # Get different categories of statistics stats_pnls = result.stats_pnls # Keyed by currency code, then statistic name stats_returns = result.stats_returns # Keyed by statistic name stats_general = result.stats_general # Keyed by statistic name ``` Each statistic contributes to the categories for which it implements a calculation: realized PnLs, returns, or positions. A custom statistic can contribute to more than one category. :::info See the [Portfolio guide](portfolio.md#portfolio-statistics) for the default statistic set, how each category is derived, and the difference between position returns and portfolio returns. ::: ### Visualization NautilusTrader provides interactive tearsheets and plots via Plotly: ```python from nautilus_trader.analysis import create_tearsheet # After backtest run engine.run() # Generate interactive HTML tearsheet create_tearsheet(engine, output_path="tearsheet.html") ``` This creates an interactive HTML report with: - Equity curve - Drawdown analysis - Monthly returns heatmap - Performance statistics table - Returns distribution For more control, generate individual plots: ```python import pandas as pd from nautilus_trader.analysis import create_equity_curve returns = pd.Series( [0.01, -0.005, 0.002], index=pd.date_range("2024-01-01", periods=3, tz="UTC"), ) fig = create_equity_curve(returns, title="My Strategy Equity") fig.show() # Display in browser fig.write_image("equity.png") # Export to PNG (requires kaleido) ``` Install visualization dependencies: ```bash uv pip install --pre "nautilus_trader[visualization]" ``` ## Report generation patterns ### Live trading During live trading, generate reports periodically: ```python from datetime import timedelta from nautilus_trader.analysis import ReportProvider from nautilus_trader.common import DataActor from nautilus_trader.common import TimeEvent class ReportingActor(DataActor): def on_start(self) -> None: # Schedule periodic reporting self.clock.set_timer( name="generate_reports", interval=timedelta(minutes=30), callback=self.generate_reports, ) def generate_reports(self, event: TimeEvent) -> None: # Generate and log reports positions_report = ReportProvider.generate_positions_report( positions=self.cache.positions(), snapshots=self.cache.position_snapshots(), ) # Save or transmit report positions_report.to_csv(f"positions_{event.ts_event}.csv") ``` ### Performance analysis For backtest analysis: ```python import pandas as pd # Run the backtest engine.run() # Collect results positions_closed = engine.cache.positions_closed() result = engine.get_result() stats_pnls = result.stats_pnls stats_returns = result.stats_returns stats_general = result.stats_general # Create summary dictionary results = { "total_positions": len(positions_closed), "pnl_total": stats_pnls.get("USD", {}).get("PnL (total)"), "win_rate": stats_pnls.get("USD", {}).get("Win Rate"), "sharpe_ratio": stats_returns.get("Sharpe Ratio (252 days)"), "profit_factor": stats_returns.get("Profit Factor"), "long_ratio": stats_general.get("Long Ratio"), } # Display results results_df = pd.DataFrame([results]) print(results_df.T) # Transpose for vertical display ``` :::info Reports are generated from in-memory data structures. For large-scale analysis or long-running systems, consider persisting reports to a database for efficient querying. See the [Cache guide](cache.md) for persistence options. ::: ## Integration with other components The `ReportProvider` works with several system components: - **Cache**: Source of all trading data (orders, positions, accounts) for reports. - **Portfolio**: Computes its statistics from the same cache data independently, not from these reports. - **BacktestEngine**: Exposes the report methods used for post-run analysis and visualization. - **Position snapshots**: Required for accurate PnL reporting in `NETTING` OMS. ## Related guides - [Visualization](visualization.md) - Interactive tearsheets and charts from backtest results. - [Portfolio](portfolio.md) - Portfolio statistics and performance metrics. - [Backtesting](backtesting/) - Running backtests that generate reports. - [Cache](cache.md) - Cache system that stores data for reports. # Rust Source: https://nautilustrader.io/docs/latest/concepts/rust/ Nautilus has a complete Rust implementation under the `crates/` directory. You can write actors, strategies, run backtests, and trade live without Python. The domain model is shared with the Python package, which runs user components on the same Rust engine through PyO3. :::warning The Rust API is under active development. Method signatures and trait requirements may change between releases. ::: ## System implementations Nautilus has two paths. Choose the one that matches how you want to author and deploy the system. - **Rust**: Pure Rust under `crates/`. Runs without Python. - **Python**: [Python user components](python.md) running on the Rust core through PyO3 bindings under `python/nautilus_trader/`. ### Capability matrix | Component | Rust | Python | | --------------- | ---- | ------ | | Strategy | ✓ | ✓ | | Actor | ✓ | ✓ | | DataEngine | ✓ | ✓ | | ExecutionEngine | ✓ | ✓ | | RiskEngine | ✓ | ✓ | | BacktestEngine | ✓ | ✓ | | BacktestNode | ✓ | ✓ | | LiveNode | ✓ | ✓ | | OrderEmulator | ✓ | ✓ | | Matching engine | ✓ | ✓ | | Portfolio | ✓ | ✓ | | Accounts | ✓ | ✓ | | Cache | ✓ | ✓ | | MessageBus | ✓ | ✓ | | Data catalog | ✓ | ✓ | | Indicators | ✓ | ✓ | | Exec algorithms | TWAP | TWAP | | Controller | - | ✓ | | Tearsheets | - | ✓ | :::note The Controller runtime is implemented in Rust and powers the Python `Controller` base class. The matrix marks it absent for Rust because the supported registration path (importable controller configs) is Python-only. ::: ### Adapters | Adapter | Rust | Python | | ------------------- | ---- | ------ | | Architect AX | ✓ | ✓ | | Betfair | ✓ | ✓ | | Binance | ✓ | ✓ | | BitMEX | ✓ | ✓ | | Blockchain | ✓ | ✓ | | Bybit | ✓ | ✓ | | Coinbase | ✓ | ✓ | | Databento | ✓ | ✓ | | Deribit | ✓ | ✓ | | Derive | ✓ | ✓ | | dYdX | ✓ | ✓ | | Hyperliquid | ✓ | ✓ | | Interactive Brokers | ✓ | ✓ | | Kraken | ✓ | ✓ | | Lighter | ✓ | ✓ | | OKX | ✓ | ✓ | | Polymarket | ✓ | ✓ | | Sandbox | ✓ | ✓ | | Tardis | ✓ | ✓ | ### Choosing a path - **Rust** gives native performance without a Python runtime. All core trading functionality is available. Use it for latency-sensitive deployments or teams that prefer a compiled language. - **Python** keeps the Python authoring experience. User components (actors, strategies) run on the Rust core for data processing and execution. ## Project setup The Nautilus crates are published to [crates.io](https://crates.io/crates/nautilus-backtest). Add them to your `Cargo.toml`: ```toml [dependencies] nautilus-backtest = "0.63" nautilus-common = "0.63" nautilus-execution = "0.63" nautilus-model = { version = "0.63", features = ["test-support"] } nautilus-trading = { version = "0.63", features = ["examples"] } anyhow = "1" log = "0.4" ``` For live trading, add the live crate and the adapter for your venue: ```toml [dependencies] nautilus-live = "0.63" nautilus-okx = "0.63" ``` To track the latest development branch, point all Nautilus dependencies at the same git source to avoid type mismatches between crates.io and git versions: ```toml [dependencies] nautilus-backtest = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop" } nautilus-common = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop" } nautilus-execution = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop" } nautilus-model = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop", features = ["test-support"] } nautilus-trading = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop", features = ["examples"] } ``` The minimum supported Rust version (MSRV) is **1.98.1**. ### Feature flags | Flag | Crate | Effect | | ---------------- | ------------------- | --------------------------------------------------- | | `high-precision` | `nautilus-model` | 16-digit fixed precision (default is 9). | | `test-support` | `nautilus-model` | Test fixtures, builders, specs, and defaults. | | `examples` | `nautilus-trading` | Example strategies (`EmaCross`, `GridMarketMaker`). | | `streaming` | `nautilus-backtest` | Catalog-based data streaming via `BacktestNode`. | | `defi` | `nautilus-model` | DeFi data types. Implies `high-precision`. | :::tip Standard 9-digit precision handles most traditional finance instruments. Enable `high-precision` for crypto venues where prices or quantities need more than nine decimal places. ::: ### Memory allocator The `nautilus` CLI and Python wheels use [mimalloc](https://crates.io/crates/mimalloc) for Rust allocations. A Rust binary chooses its own allocator, so add mimalloc to yours to match: ```toml [dependencies] mimalloc = "0.1" ``` ```rust use mimalloc::MiMalloc; use nautilus_common::logging::headers::register_allocator_mimalloc; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; fn main() { register_allocator_mimalloc(); } ``` Declaring `GLOBAL` selects mimalloc. Call `register_allocator_mimalloc` at the start of `main`, before constructing a Nautilus node, so the version header reports `allocator: mimalloc `. Registration **only updates the header metadata**; it does not select the allocator. The default system allocator also works. Measure throughput and resident memory on your workload and platform when comparing allocator choices. See the [architecture guide](architecture.md#memory-allocation) for background. ## Actors An actor receives market data, custom data/signals, and system events but does not manage orders. Implement the `DataActor` trait and use `nautilus_actor!` to wire your `DataActorCore` field into the runtime contract. Your type implements or derives `Debug`; the macro supplies the native runtime wiring. User code normally uses the `DataActor` facade methods for subscriptions, cache access, and clock access. ### Handler methods Override any handler on the `DataActor` trait to receive the corresponding data or event. All handlers have default no-op implementations, so you only override what you need. | Handler | Receives | | ---------------------- | ------------------------- | | `on_start` | Actor started. | | `on_stop` | Actor stopped. | | `on_quote` | `QuoteTick` | | `on_trade` | `TradeTick` | | `on_bar` | `Bar` | | `on_book_deltas` | `OrderBookDeltas` | | `on_book` | `OrderBook` (at interval) | | `on_instrument` | `InstrumentAny` | | `on_mark_price` | `MarkPriceUpdate` | | `on_index_price` | `IndexPriceUpdate` | | `on_funding_rate` | `FundingRateUpdate` | | `on_option_greeks` | `OptionGreeks` | | `on_option_chain` | `OptionChainSlice` | | `on_instrument_status` | `InstrumentStatus` | | `on_time_event` | `TimeEvent` | For a step-by-step walkthrough, see the [Write an Actor (Rust)](../how_to/write_rust_actor.md) how-to guide. For a complete example, see [`BookImbalanceActor`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/actors/imbalance). ## Strategies A strategy extends an actor with order management. Implement `DataActor` for data handling and use `nautilus_strategy!` to wire your `StrategyCore` field into the strategy runtime contract. `StrategyCore` stores the runtime strategy state; normal strategy logic reaches it through facade methods on `self`. Runtime registration requires the native wiring generated by the macro, but normal strategy logic uses `Strategy` methods and the facade methods on `self`. Strategies also override order event handlers on the `Strategy` trait, such as `on_order_filled` (`OrderFilled`) and `on_order_canceled` (`OrderCanceled`). ### Order management The `Strategy` trait provides order methods through the facade: | Method | Action | | --------------------- | ----------------------------------------- | | `submit_order` | Submit a new order to the venue. | | `submit_order_list` | Submit a list of contingent orders. | | `modify_order` | Modify price, quantity, or trigger price. | | `cancel_order` | Cancel a specific order. | | `cancel_orders` | Cancel a filtered set of orders. | | `cancel_all_orders` | Cancel all orders for an instrument. | | `close_position` | Close a position with a market order. | | `close_all_positions` | Close all open positions. | The `OrderApi` (accessed via `self.order()`) builds orders and order lists: - `generate_client_order_id` - `generate_order_list_id` - `market` - `limit` - `stop_market` - `stop_limit` - `market_to_limit` - `market_if_touched` - `limit_if_touched` - `trailing_stop_market` - `trailing_stop_limit` - `bracket` - `create_list` ### Core wiring macros Rust actors, strategies, and execution algorithms keep their runtime core as a struct field. The macros tell the traits where that field lives. | Macro | Core field | Generates | | ---------------------------------------------- | ------------------------ | ------------------------------ | | `nautilus_actor!(Type)` | `DataActorCore` | Runtime wiring. | | `nautilus_strategy!(Type)` | `StrategyCore` | Runtime wiring and `Strategy`. | | `nautilus_execution_algorithm!(Type, { ... })` | `ExecutionAlgorithmCore` | Runtime wiring and algorithm. | The macros expect a field named `core`; pass a field name as the second argument when needed. They do not make the actor, strategy, or `StrategyCore` deref to runtime internals. The execution algorithm macro takes an `on_order()` implementation block because that method defines the algorithm's required order handling. ### Native traits Use facade methods by default: - `actor_id()` - `trader_id()` - `is_registered()` - `config()` - `strategy_id()` - `clock()` - `cache()` - `order()` - `portfolio()` `DataActorNative`, `StrategyNative`, and `ExecutionAlgorithmNative` are for native-only access below that facade. This section documents engine, runtime, and explicit latency-sensitive native Rust code, not the portable authoring path. | Authoring path | Native traits? | Normal API | | ------------------------- | ---------------- | ----------------------------------- | | Native Rust binary | Only when needed | `Strategy` and `DataActor` facades. | | Rust launched from Python | Only when needed | Same as native Rust. | | Python-authored component | No | Facades only. | Native traits expose borrowed core state, `Rc>`, and runtime references. Use them when native Rust code intentionally accepts those borrow rules for an explicit latency-sensitive path. Engine, runtime, registration, PyO3, and testkit code can import `DataActorNative`, `StrategyNative`, or `ExecutionAlgorithmNative` when they need actor-core, strategy-core, or execution-algorithm-core access. Do not use them in ordinary portable actor, strategy, or execution algorithm logic or Python-authored components, because those types do not cross the Python boundary. `ExecutionAlgorithmCore` owns a `DataActorCore`, but it does not deref to one. Normal execution algorithm logic should use `id()`, `actor_id()`, `trader_id()`, `clock()`, and `cache()`. Reach for `ExecutionAlgorithmNative` only when the code needs native execution-algorithm state. Choose the smallest native handle and keep each borrow scoped. Use `order()` for normal strategy order construction. Reach for `order_factory()` only when native code needs the raw mutable factory borrow. #### `DataActorNative` methods | Native method | Return shape | Use when | | ------------- | ------------------------ | ------------------------------- | | `core()` | `&DataActorCore` | Read actor internals. | | `core_mut()` | `&mut DataActorCore` | Mutate actor internals. | | `clock_mut()` | `RefMut<'_, dyn Clock>` | Need a mutable clock borrow. | | `clock_rc()` | `Rc>` | Store or pass the shared clock. | | `cache_ref()` | `Ref<'_, Cache>` | Need short live-cache reads. | | `cache_rc()` | `Rc>` | Mutate, store, or pass cache. | #### `StrategyNative` methods | Native method | Return shape | Use when | | --------------------- | --------------------------- | -------------------------------- | | `strategy_core()` | `&StrategyCore` | Read strategy internals. | | `strategy_core_mut()` | `&mut StrategyCore` | Mutate strategy internals. | | `order_factory()` | `RefMut<'_, OrderFactory>` | Need raw mutable factory borrow. | | `order_factory_rc()` | `Rc>` | Store or pass the factory. | | `portfolio_rc()` | `Rc>` | Store or pass the portfolio. | #### `ExecutionAlgorithmNative` methods | Native method | Return shape | Use when | | --------------------------- | ----------------------------- | ------------------------------------- | | `exec_algorithm_core()` | `&ExecutionAlgorithmCore` | Read execution algorithm internals. | | `exec_algorithm_core_mut()` | `&mut ExecutionAlgorithmCore` | Mutate execution algorithm internals. | For a step-by-step walkthrough, see the [Write a Strategy (Rust)](../how_to/write_rust_strategy.md) how-to guide. For complete examples, see [`EmaCross`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/strategies/ema_cross) and [`GridMarketMaker`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/strategies/grid_mm). ### Running Rust components Rust strategies and actors can run through two paths. The examples below use strategies, but the same pattern applies to bundled actors via `add_actor` (pure Rust) and `add_builtin_actor` (from Python). #### Pure Rust Write your strategy and `main` function in Rust, then build a standalone binary with `cargo build`. This path requires no Python runtime. ```rust let strategy = GridMarketMaker::new(config); node.add_strategy(strategy)?; node.run().await?; ``` See [Run Live Trading (Rust)](../how_to/run_rust_live_trading.md) for a full walkthrough. #### Built-in examples from Python Pass a type name and config to `add_builtin_strategy` to register a built-in example strategy from Python. This path exists to single-source the bundled example strategy code across Rust and Python docs, examples, and tests. It is not a first-class extension path for adding native strategies. For custom native components, use pure Rust. ```python from nautilus_trader.trading import GridMarketMakerConfig config = GridMarketMakerConfig( instrument_id=InstrumentId.from_str("BTC-USDT-SWAP.OKX"), max_position=Quantity.from_str("10.0"), trade_size=Quantity.from_str("0.1"), num_levels=5, grid_step_bps=15, ) node.add_builtin_strategy("GridMarketMaker", config) ``` Built-in strategy configs: | Config | Strategy | | ---------------------------- | ---------------------- | | `CompositeMarketMakerConfig` | `CompositeMarketMaker` | | `DeltaNeutralVolConfig` | `DeltaNeutralVol` | | `EmaCrossConfig` | `EmaCross` | | `ExecTesterConfig` | `ExecTester` | | `GridMarketMakerConfig` | `GridMarketMaker` | | `HurstVpinDirectionalConfig` | `HurstVpinDirectional` | `add_builtin_actor` follows the same bundled-only rule for actors used by examples and tests. Built-in actor configs (via `add_builtin_actor`): | Config | Actor | | -------------------------- | -------------------- | | `BookImbalanceActorConfig` | `BookImbalanceActor` | | `DataTesterConfig` | `DataTester` | ## Backtesting For annotated walkthroughs of both APIs, see the [Run a Backtest (Rust)](../how_to/run_rust_backtest.md) how-to guide. ### `BacktestEngine` (low-level API) Construct the engine, add venues and instruments, load data, register strategies, and run. See the full working example: ```bash cargo run -p nautilus-backtest --features examples --example engine-ema-cross ``` Source: [`crates/backtest/examples/engine_ema_cross.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/backtest/examples/engine_ema_cross.rs) ### `BacktestNode` (high-level API) Loads data from a `ParquetDataCatalog` and supports streaming in configurable chunk sizes. Requires the `streaming` feature on `nautilus-backtest`. See the full working example: ```bash cargo run -p nautilus-backtest --features examples,streaming --example node-ema-cross ``` Source: [`crates/backtest/examples/node_ema_cross.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/backtest/examples/node_ema_cross.rs) ## Live trading For an annotated walkthrough, see the [Run Live Trading (Rust)](../how_to/run_rust_live_trading.md) how-to guide. The `LiveNode` connects to real venues and data sources through adapter clients. The builder pattern configures data and execution clients, then `run()` starts the async event loop. Each adapter provides its own factory and config types. | Adapter | Example | | ------------------- | ----------------------------------------------- | | Architect AX | `crates/adapters/architect_ax/examples/` | | Betfair | `crates/adapters/betfair/examples/` | | Binance | `crates/adapters/binance/examples/` | | BitMEX | `crates/adapters/bitmex/examples/` | | Blockchain | `crates/adapters/blockchain/examples/` | | Bybit | `crates/adapters/bybit/examples/` | | Coinbase | `crates/adapters/coinbase/examples/` | | Databento | `crates/adapters/databento/examples/` | | Deribit | `crates/adapters/deribit/examples/` | | Derive | `crates/adapters/derive/examples/` | | dYdX | `crates/adapters/dydx/examples/` | | Hyperliquid | `crates/adapters/hyperliquid/examples/` | | Interactive Brokers | `crates/adapters/interactive_brokers/examples/` | | Kraken | `crates/adapters/kraken/examples/` | | Lighter | `crates/adapters/lighter/examples/` | | OKX | `crates/adapters/okx/examples/` | | Polymarket | `crates/adapters/polymarket/examples/` | | Sandbox | `crates/adapters/sandbox/examples/` | | Tardis | `crates/adapters/tardis/examples/` | Most adapters include `node_data_tester.rs` and `node_exec_tester.rs` examples. These test data requests, streaming, and order execution against live venues. ## Related guides - [Python](python.md) - Python ownership, runtime, and public API boundaries. - [Write an Actor (Rust)](../how_to/write_rust_actor.md) - Step-by-step actor walkthrough. - [Write a Strategy (Rust)](../how_to/write_rust_strategy.md) - Step-by-step strategy walkthrough. - [Run a Backtest (Rust)](../how_to/run_rust_backtest.md) - BacktestEngine and BacktestNode usage. - [Run Live Trading (Rust)](../how_to/run_rust_live_trading.md) - LiveNode setup and venue connection. - [Architecture](architecture.md) - System design and data/execution flow. - [Actors](actors.md) - Actor concepts (applies to both Python and Rust). - [Strategies](strategies.md) - Strategy concepts and handler reference. - [Events](events/) - Event types and handler dispatch. - [Backtesting](backtesting/) - Backtest concepts and matching engine behavior. # Strategies Source: https://nautilustrader.io/docs/latest/concepts/strategies/ A strategy inherits the `Strategy` class and implements the methods its logic requires. `Strategy` builds on `DataActor` and adds order management. **Capabilities**: - Historical data requests. - Live data feed subscriptions. - Setting time alerts or timers. - Cache access. - Portfolio access. - Creating and managing orders and positions. :::tip Review the [Actors](actors.md) guide before developing a strategy. It covers the subscription, request, and callback behavior a strategy inherits. ::: Add strategies to a Nautilus system in any [environment context](architecture.md#environment-contexts). They start sending commands and receiving events based on their logic as soon as the system starts. These building blocks of data ingest, event handling, and order management (discussed below) support any strategy type, including directional, momentum, re-balancing, pairs, and market making. There are two main parts of a Nautilus trading strategy: - The strategy implementation itself, defined by inheriting the `Strategy` class. - The *optional* strategy configuration, defined by inheriting the `StrategyConfig` class. :::note The same strategy source can run in backtest and live environments. Live execution still introduces venue, transport, timing, persistence, external-activity, and reconciliation behavior that a simulation may not reproduce. See [Backtest and live differences](live.md#backtest-and-live-differences). ::: See the [`Strategy` API Reference](/docs/python-api-latest/trading.html) for all available methods. :::info Rust implementation Rust strategy authors implement the `DataActor` callbacks they need and use `nautilus_strategy!` to generate the `Strategy` implementation, then call facade methods such as `clock()`, `cache()`, `order()`, and `portfolio()` on `self`. `DataActorNative` is native-only access to runtime wiring and actor-core state; `StrategyNative` exposes borrowed strategy state such as order factory, order manager, and portfolio access. Import them only for same-binary performance paths or internal runtime wiring. ::: ## Strategy implementation A trading strategy inherits from `Strategy`, so you must define a constructor. At minimum, initialize the base class: ```python from nautilus_trader.trading import Strategy class MyStrategy(Strategy): def __init__(self) -> None: super().__init__() # <-- the superclass must be called to initialize the strategy ``` From here, you can implement handlers as necessary to perform actions based on state transitions and events. :::warning `clock`, `cache`, `portfolio`, and `order_factory` raise a `RuntimeError` until the strategy is registered with a trader, which happens after `__init__` returns. Initialize plain state in the constructor and do system work in `on_start()`. ::: ### Handlers Handlers are methods on the `Strategy` class that perform actions based on events or state changes. These methods use the `on_*` prefix. Implement any or all of them as your strategy requires. Multiple handlers exist for similar event types to give you control over granularity. Respond to a specific event with a dedicated handler, or use a generic handler for a range of related events (using typical switch statement logic). The system calls handlers in sequence from **most specific to most general**. Subscribed data, order, and position handlers dispatch only while the strategy is `RUNNING`. Messages that arrive in any other state are logged but not passed to your handlers. Request responses are not gated this way: an `on_historical_*` handler still runs if its response lands after the strategy stops. #### Stateful actions Lifecycle state changes trigger these handlers. Recommendations: - Use the `on_start` method to initialize your strategy (e.g., fetch instruments, subscribe to data). - Use the `on_stop` method for cleanup tasks (e.g., cancel open orders, close open positions, unsubscribe from data). ```python def on_start(self) -> None: def on_stop(self) -> None: def on_resume(self) -> None: def on_reset(self) -> None: def on_dispose(self) -> None: def on_degrade(self) -> None: def on_fault(self) -> None: def on_save(self) -> dict[str, bytes]: # Returns user-defined dictionary of state to be saved def on_load(self, state: dict[str, bytes]) -> None: ``` #### Data handling These handlers receive data updates, including built-in market data and custom user-defined data. ```python from collections.abc import Sequence from typing import Any from nautilus_trader.common import Signal from nautilus_trader.model import Bar from nautilus_trader.model import CustomData from nautilus_trader.model import FundingRateUpdate from nautilus_trader.model import IndexPriceUpdate from nautilus_trader.model import InstrumentClose from nautilus_trader.model import InstrumentStatus from nautilus_trader.model import MarkPriceUpdate from nautilus_trader.model import OptionChainSlice from nautilus_trader.model import OptionGreeks from nautilus_trader.model import OrderBook from nautilus_trader.model import OrderBookDelta from nautilus_trader.model import OrderBookDeltas from nautilus_trader.model import OrderBookDepth10 from nautilus_trader.model import QuoteTick from nautilus_trader.model import TradeTick def on_book_deltas(self, deltas: OrderBookDeltas) -> None: def on_book_depth(self, depth: OrderBookDepth10) -> None: def on_book(self, order_book: OrderBook) -> None: def on_quote(self, tick: QuoteTick) -> None: def on_trade(self, tick: TradeTick) -> None: def on_bar(self, bar: Bar) -> None: def on_mark_price(self, mark_price: MarkPriceUpdate) -> None: def on_index_price(self, index_price: IndexPriceUpdate) -> None: def on_funding_rate(self, funding_rate: FundingRateUpdate) -> None: def on_instrument(self, instrument: Any) -> None: def on_instrument_status(self, data: InstrumentStatus) -> None: def on_instrument_close(self, data: InstrumentClose) -> None: def on_option_greeks(self, greeks: OptionGreeks) -> None: def on_option_chain(self, chain: OptionChainSlice) -> None: def on_historical_data(self, data: CustomData | Sequence[CustomData]) -> None: def on_historical_book_deltas(self, deltas: Sequence[OrderBookDelta]) -> None: def on_historical_book_depth(self, depths: Sequence[OrderBookDepth10]) -> None: def on_historical_quotes(self, quotes: Sequence[QuoteTick]) -> None: def on_historical_trades(self, trades: Sequence[TradeTick]) -> None: def on_historical_bars(self, bars: Sequence[Bar]) -> None: def on_historical_mark_prices(self, mark_prices: Sequence[MarkPriceUpdate]) -> None: def on_historical_index_prices(self, index_prices: Sequence[IndexPriceUpdate]) -> None: def on_historical_funding_rates(self, rates: Sequence[FundingRateUpdate]) -> None: def on_data(self, data: CustomData) -> None: def on_signal(self, signal: Signal) -> None: ``` Subscribed updates and request responses reach different handlers. See [Actors: callback handlers](actors.md#callback-handlers) for the operation-to-handler mapping. #### Order management These handlers receive events related to orders. `OrderEvent` type messages are passed to handlers in the following sequence: 1. Specific handler (e.g., `on_order_accepted`, `on_order_rejected`, etc.) 2. `on_order_event(...)` ```python from typing import Any from nautilus_trader.model import OrderAccepted from nautilus_trader.model import OrderCanceled from nautilus_trader.model import OrderCancelRejected from nautilus_trader.model import OrderDenied from nautilus_trader.model import OrderEmulated from nautilus_trader.model import OrderExpired from nautilus_trader.model import OrderFilled from nautilus_trader.model import OrderFillVoided from nautilus_trader.model import OrderInitialized from nautilus_trader.model import OrderModifyRejected from nautilus_trader.model import OrderPendingCancel from nautilus_trader.model import OrderPendingUpdate from nautilus_trader.model import OrderRejected from nautilus_trader.model import OrderReleased from nautilus_trader.model import OrderSubmitted from nautilus_trader.model import OrderTriggered from nautilus_trader.model import OrderUpdated def on_order_initialized(self, event: OrderInitialized) -> None: def on_order_denied(self, event: OrderDenied) -> None: def on_order_emulated(self, event: OrderEmulated) -> None: def on_order_released(self, event: OrderReleased) -> None: def on_order_submitted(self, event: OrderSubmitted) -> None: def on_order_rejected(self, event: OrderRejected) -> None: def on_order_accepted(self, event: OrderAccepted) -> None: def on_order_canceled(self, event: OrderCanceled) -> None: def on_order_expired(self, event: OrderExpired) -> None: def on_order_triggered(self, event: OrderTriggered) -> None: def on_order_pending_update(self, event: OrderPendingUpdate) -> None: def on_order_pending_cancel(self, event: OrderPendingCancel) -> None: def on_order_modify_rejected(self, event: OrderModifyRejected) -> None: def on_order_cancel_rejected(self, event: OrderCancelRejected) -> None: def on_order_updated(self, event: OrderUpdated) -> None: def on_order_filled(self, event: OrderFilled) -> None: def on_order_fill_voided(self, event: OrderFillVoided) -> None: def on_order_event(self, event: Any) -> None: # All order event messages are eventually passed to this handler ``` :::note The Python API does not export an `OrderEvent` base type. `on_order_event(...)` receives the same concrete event object the specific handler received, such as an `OrderAccepted`. ::: #### Position management These handlers receive events related to positions. `PositionEvent` type messages are passed to handlers in the following sequence: 1. Specific handler (e.g., `on_position_opened`, `on_position_changed`, etc.) 2. `on_position_event(...)` ```python from typing import Any from nautilus_trader.model import PositionChanged from nautilus_trader.model import PositionClosed from nautilus_trader.model import PositionOpened def on_position_opened(self, event: PositionOpened) -> None: def on_position_changed(self, event: PositionChanged) -> None: def on_position_closed(self, event: PositionClosed) -> None: def on_position_event(self, event: Any) -> None: # All position event messages are eventually passed to this handler ``` As with order events, the Python API does not export a `PositionEvent` base type, so `on_position_event(...)` receives the concrete event object. Use `on_time_event()` for timer events, `on_order_event()` for aggregate order events, and `on_position_event()` for aggregate position events. The Python API does not expose a generic `on_event()` hook. #### Handler example The following example shows a typical `on_start` handler method implementation (taken from the example EMA cross strategy). Here we can see the following: - Indicators being registered to receive bar updates. - Historical data being requested (to hydrate the indicators). - Live data being subscribed to. The cache check matters in live trading. Direct subscriptions assume the instrument was loaded by the instrument provider config or by an earlier instrument request. ```python def on_start(self) -> None: """ Actions to be performed on strategy start. """ self.instrument = self.cache.instrument(self.instrument_id) if self.instrument is None: self.log.error(f"Could not find instrument for {self.instrument_id}") self.stop() # Transitions strategy to STOPPED state return # Register the indicators for updating self.register_indicator_for_bars(self.bar_type, self.fast_ema) self.register_indicator_for_bars(self.bar_type, self.slow_ema) # Get historical data and subscribe to live data self.request_bars(self.bar_type) self.subscribe_bars(self.bar_type) self.subscribe_quotes(self.instrument_id) ``` Registered indicators receive the bars from a request response before `on_historical_bars()` runs, so a request hydrates them. Check `indicators_initialized()` before acting on indicator values. ### Clock and timers Strategies have access to a `Clock` which provides a number of methods for creating different timestamps, as well as setting time alerts or timers to trigger `TimeEvent`s. See the [`Clock` API Reference](/docs/python-api-latest/common.html) for all available methods. #### Current timestamps While there are multiple ways to obtain current timestamps, here are two commonly used methods as examples: To get the current UTC timestamp as a tz-aware `datetime`: ```python from datetime import datetime now: datetime = self.clock.utc_now() ``` To get the current UTC timestamp as nanoseconds since the UNIX epoch: ```python unix_nanos: int = self.clock.timestamp_ns() ``` #### Time alerts Time alerts can be set which will result in a `TimeEvent` being dispatched to the `on_time_event` handler at the specified alert time. In live trading, scheduling and queued work can delay delivery; the alert time is not a latency guarantee. This example sets a time alert to trigger one minute from the current time: ```python from datetime import timedelta # Fire a TimeEvent one minute from now self.clock.set_time_alert( name="MyTimeAlert1", alert_time=self.clock.utc_now() + timedelta(minutes=1), ) ``` #### Timers Continuous timers can be set up which will generate a `TimeEvent` at regular intervals until the timer expires or is canceled. This example sets a timer to fire once per minute. The timer starts immediately and its first event fires one interval later; pass `fire_immediately=True` to fire at the start time instead: ```python from datetime import timedelta # Fire a TimeEvent every minute self.clock.set_timer( name="MyTimer1", interval=timedelta(minutes=1), ) ``` Pass a `callback` to route `TimeEvent` objects to your own method rather than `on_time_event`. Timer names share the clock namespace, so use names unique to the component. See [Actors: timers and alerts](actors.md#timers-and-alerts). ### Cache access The trader's central `Cache` stores data and execution objects (orders, positions, etc). Many methods are available with filtering. Here are some basic use cases. #### Fetching data The following example fetches data from the cache (assuming some instrument ID attribute is assigned). These methods return `None` if the requested data is not available. ```python last_quote = self.cache.quote(self.instrument_id) last_trade = self.cache.trade(self.instrument_id) last_bar = self.cache.bar(bar_type) ``` #### Fetching execution objects The following example shows how individual order and position objects can be fetched from the cache: ```python order = self.cache.order(client_order_id) position = self.cache.position(position_id) ``` See the [`Cache` API Reference](/docs/python-api-latest/cache.html) for all available methods. ### Portfolio access The trader's central `Portfolio` provides account and positional information. The following shows a general outline of available methods. #### Account and positional information ```python import decimal import typing from nautilus_trader.model import AccountId from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Money from nautilus_trader.model import Price from nautilus_trader.model import Venue def account( self, venue: Venue | None = None, account_id: AccountId | None = None, ) -> typing.Any | None def balances_locked( self, venue: Venue | None = None, account_id: AccountId | None = None, ) -> dict[Currency, Money] | None def instrument_initial_margins( self, venue: Venue | None = None, account_id: AccountId | None = None, ) -> dict[InstrumentId, Money] | None def instrument_maintenance_margins( self, venue: Venue | None = None, account_id: AccountId | None = None, ) -> dict[InstrumentId, Money] | None def unrealized_pnls( self, venue: Venue | None = None, account_id: AccountId | None = None, target_currency: Currency | None = None, ) -> dict[Currency, Money] def realized_pnls( self, venue: Venue | None = None, account_id: AccountId | None = None, target_currency: Currency | None = None, ) -> dict[Currency, Money] def total_pnls( self, venue: Venue | None = None, account_id: AccountId | None = None, target_currency: Currency | None = None, ) -> dict[Currency, Money] def net_exposures( self, venue: Venue | None = None, account_id: AccountId | None = None, target_currency: Currency | None = None, ) -> dict[Currency, Money] | None def unrealized_pnl( self, instrument_id: InstrumentId, price: Price | None = None, account_id: AccountId | None = None, target_currency: Currency | None = None, ) -> Money | None def realized_pnl( self, instrument_id: InstrumentId, account_id: AccountId | None = None, target_currency: Currency | None = None, ) -> Money | None def total_pnl( self, instrument_id: InstrumentId, price: Price | None = None, account_id: AccountId | None = None, target_currency: Currency | None = None, ) -> Money | None def net_exposure( self, instrument_id: InstrumentId, price: Price | None = None, account_id: AccountId | None = None, target_currency: Currency | None = None, ) -> Money | None def net_position( self, instrument_id: InstrumentId, account_id: AccountId | None = None, ) -> decimal.Decimal def is_net_long(self, instrument_id: InstrumentId, account_id: AccountId | None = None) -> bool def is_net_short(self, instrument_id: InstrumentId, account_id: AccountId | None = None) -> bool def is_net_flat(self, instrument_id: InstrumentId, account_id: AccountId | None = None) -> bool def is_completely_net_flat(self, account_id: AccountId | None = None) -> bool ``` If both `venue` and `account_id` are supplied, they must resolve to the same account, otherwise the query raises `ValueError`. The Portfolio exposes queries to strategies; engine commands remain internal to the Rust runtime. See the [`Portfolio` API Reference](/docs/python-api-latest/portfolio.html) for all available methods. #### Reports and analysis Use `Portfolio.statistics()` and `Portfolio.snapshots(account_id)` for performance analysis. See the [Analysis API Reference](/docs/python-api-latest/analysis.html) and [Portfolio statistics](portfolio.md#portfolio-statistics) guide. The [Portfolio](portfolio.md) guide also covers equity, mark-to-market valuation, and multi-account query scope. ### Trading commands The following trading commands are available for order management. See also the [Execution](execution/) guide for the full flow through the system. #### Submitting orders An `OrderFactory` is provided on the base class for every `Strategy` as a convenience, reducing the amount of boilerplate required to create different `Order` objects (although these objects can still be initialized directly with the `Order.__init__(...)` constructor if the trader prefers). The component a `SubmitOrder` or `SubmitOrderList` command will flow to for execution depends on the following: - If an `emulation_trigger` is specified, the command will *firstly* be sent to the `OrderEmulator`. - If an `exec_algorithm_id` is specified (with no `emulation_trigger`), the command will *firstly* be sent to the relevant `ExecutionAlgorithm`. - Otherwise, the command will *firstly* be sent to the `RiskEngine`. This example submits a `LIMIT` BUY order for emulation (see [Emulated Orders](orders/emulated.md)): ```python from nautilus_trader.model import LimitOrder from nautilus_trader.model import OrderSide from nautilus_trader.model import TriggerType def buy(self) -> None: """ Users simple buy method (example). """ order: LimitOrder = self.order_factory.limit( instrument_id=self.instrument_id, order_side=OrderSide.BUY, quantity=self.instrument.make_qty(self.trade_size), price=self.instrument.make_price(5000.00), emulation_trigger=TriggerType.LAST_PRICE, ) self.submit_order(order) ``` :::info You can specify both order emulation and an execution algorithm. In this case, the order is first sent to the `OrderEmulator`, and upon release is then routed to the `ExecutionAlgorithm`. ::: This example submits a `MARKET` BUY order to a TWAP execution algorithm: ```python from nautilus_trader.model import ExecAlgorithmId from nautilus_trader.model import MarketOrder from nautilus_trader.model import OrderSide from nautilus_trader.model import TimeInForce def buy(self) -> None: """ Users simple buy method (example). """ order: MarketOrder = self.order_factory.market( instrument_id=self.instrument_id, order_side=OrderSide.BUY, quantity=self.instrument.make_qty(self.trade_size), time_in_force=TimeInForce.FOK, exec_algorithm_id=ExecAlgorithmId("TWAP"), exec_algorithm_params={"horizon_secs": "20", "interval_secs": "2.5"}, ) self.submit_order(order) ``` See [Execution algorithms](execution/algorithms.md) for TWAP parameter rules and spawned-order behavior. #### Canceling orders Orders can be canceled individually, as a batch, or all orders for an instrument (with an optional side filter). If the order is already *closed* or already pending cancel, then a warning will be logged. If the order is currently *open* then the status will become `PENDING_CANCEL`. Routing depends on the command and on the state of each order: - `cancel_order(...)` goes *firstly* to the `OrderEmulator` when the order is emulated, to the relevant `ExecutionAlgorithm` when the order has an `exec_algorithm_id` and is still active within the local system, and to the `ExecutionEngine` otherwise. - `cancel_all_orders(...)` cancels each matching order associated with the strategy by default. Each order follows the same routing as `cancel_order(...)`. - `cancel_orders(...)` always goes to the `ExecutionEngine` as a single `BatchCancelOrders` command. :::info Any managed GTD timer will also be canceled after the command has left the strategy. ::: The following shows how to cancel an individual order: ```python self.cancel_order(order.client_order_id) ``` The following shows how to cancel a batch of orders. Every order in the batch must be for the same instrument, and the batch must not include emulated or local orders: ```python from nautilus_trader.model import ClientOrderId client_order_ids: list[ClientOrderId] = [ order1.client_order_id, order2.client_order_id, order3.client_order_id, ] self.cancel_orders(client_order_ids) ``` The following shows how to cancel all orders: ```python self.cancel_all_orders(self.instrument_id) ``` :::warning Pass `strategy_only=False` to use the broad cancellation path. The strategy sends one `CancelAllOrders` command even when its cache has no matching order. The execution engine resolves one execution client and account, then routes venue, emulated, and execution-algorithm cancellation within that boundary. Matching orders associated with other strategies may be canceled, but orders assigned to other execution clients remain untouched. Use broad mode only when that cross-strategy scope is intended. See [Cancel-all routing](execution/index.md#cancel-all-routing) for the complete flow and client-selection rules. ::: #### Modifying orders Orders can be modified individually when emulated, or *open* on a venue (if supported). If the order is already *closed* or already pending cancel, then a warning will be logged. If the order is currently *open* then the status will become `PENDING_UPDATE`. :::warning At least one value must differ from the original order for the command to be valid. ::: The component a `ModifyOrder` command will flow to for execution depends on the following: - If the order is currently emulated, the command will *firstly* be sent to the `OrderEmulator`. - Otherwise, if the order has an `exec_algorithm_id` and is still active within the local system, the command will be sent to the relevant `ExecutionAlgorithm`. - Otherwise, the order will *firstly* be sent to the `RiskEngine`. :::info An `ExecutionAlgorithm` refuses a `ModifyOrder` for a primary order that is still active locally, and logs a warning without emitting an event. The algorithm's schedule is keyed to the quantity it computed, so applying the modification in place would break that state; the primary order is left unchanged. ::: The following shows how to modify the size of `LIMIT` BUY order currently *open* on a venue: ```python from nautilus_trader.model import Quantity new_quantity: Quantity = Quantity.from_int(5) self.modify_order(order.client_order_id, quantity=new_quantity) ``` :::info The price and trigger price can also be modified (when emulated or supported by a venue). ::: Use `modify_orders(...)` to send several modifications as a single `BatchModifyOrders` command to the `RiskEngine`. As with a batch cancel, every order must be for the same instrument, and the batch must not include emulated or local orders. #### Market exit The `market_exit()` method provides a graceful way to exit all positions and cancel all orders for a strategy. The strategy remains running after the exit completes, allowing you to re-enter positions later if desired. ```python self.market_exit() ``` The call logs a warning and returns without effect if the strategy is not `RUNNING`, or if an exit is already in progress. The market exit process: 1. Calls `on_market_exit()`. 2. Cancels all open and in-flight orders for the strategy. 3. Closes all open positions with market orders tagged `MARKET_EXIT`. 4. Periodically checks (at `market_exit_interval_ms`) until all orders resolve and positions close, re-submitting a closing order for any position still open once no orders remain working. 5. Calls `post_market_exit()` once flat, or after `market_exit_max_attempts` is reached, logging the orders and positions still outstanding. Two hooks are available for custom logic: - `on_market_exit()`: called when the exit process begins. - `post_market_exit()`: called when the exit process completes. ```python class MyStrategy(Strategy): def on_market_exit(self) -> None: self.log.info("Beginning market exit...") def post_market_exit(self) -> None: self.log.info("Market exit complete") ``` During a market exit, non-reduce-only orders are automatically denied with the reason `MARKET_EXIT_IN_PROGRESS`. The exit's own closing orders pass through because they carry the `MARKET_EXIT` tag. For order lists, if any order in the list is non-reduce-only, the entire list is denied to preserve list semantics (e.g., bracket orders with interdependencies). To check if an exit is in progress (e.g., to skip order submission logic), use `is_exiting()`: ```python def on_quote(self, tick: QuoteTick) -> None: if self.is_exiting(): return # Skip order logic during exit # ... normal order logic ``` To automatically perform a market exit when the strategy is stopped, set `manage_stop=True`: ```python config = StrategyConfig(manage_stop=True) ``` With this option, calling `stop()` first performs a market exit, then stops the strategy once flat or once `market_exit_max_attempts` is reached. Reaching the attempt limit can leave orders or positions outstanding. :::warning In a backtest, a managed stop requested at the end of the run cannot complete. `market_exit()` schedules its first completion check `market_exit_interval_ms` after the current time, which falls beyond the requested end, and the engine has already performed its final timer flush by then. No callback runs to observe that the exit is done, so the strategy ends the run still `RUNNING` and is reported at `ERROR` even when it is already flat. ::: Configuration options in `StrategyConfig`: - `manage_stop` (default: `False`): if `True`, `stop()` performs a market exit before stopping. - `market_exit_interval_ms` (default: `100`): interval between exit completion checks. - `market_exit_max_attempts` (default: `100`): maximum checks before completing the exit. - `market_exit_time_in_force` (default: `GTC`): time in force for closing market orders. - `market_exit_reduce_only` (default: `True`): if closing market orders should be reduce only. #### Closing positions Use `close_position(...)` and `close_all_positions(...)` to flatten without running the full market exit process. Both submit closing market orders and leave the strategy free to submit new orders. See the [Execution](execution/) guide. ## Strategy configuration A separate configuration class gives full flexibility over where and how a strategy is instantiated. Configurations serialize over the wire, enabling distributed backtesting and remote live trading. This is opt-in. You can skip configuration and pass parameters directly to your strategy constructor. If you want distributed backtests or remote live trading, define a configuration. `StrategyConfig` is implemented in Rust. Its `__new__` reads the base fields (`strategy_id`, `order_id_tag`, `oms_type`, and the rest) out of the constructor call and validates them, ignoring any keyword it does not recognize. There is no base `__init__`. A subclass therefore needs only three things: - Declare its own fields as keyword-only arguments, so no positional argument is matched against a base field. - Accept `**_kwargs` so the base fields pass through the subclass signature. - Call `super().__init__()` with no arguments, then assign its own fields. Do not give a custom field the same name as a base field: both constructors would read it, and `__new__` raises a `TypeError` when the value does not match the base field's type. Here is an example configuration: ```python from decimal import Decimal from nautilus_trader.config import StrategyConfig from nautilus_trader.model import Bar from nautilus_trader.model import BarType from nautilus_trader.model import InstrumentId from nautilus_trader.model import StrategyId from nautilus_trader.trading import Strategy # Configuration definition class MyStrategyConfig(StrategyConfig): def __init__( self, *, instrument_id: InstrumentId, bar_type: BarType, trade_size: Decimal, fast_ema_period: int = 10, slow_ema_period: int = 20, **_kwargs, ) -> None: super().__init__() self.instrument_id = instrument_id self.bar_type = bar_type self.trade_size = trade_size self.fast_ema_period = fast_ema_period self.slow_ema_period = slow_ema_period # Strategy definition class MyStrategy(Strategy): def __init__(self, config: MyStrategyConfig) -> None: # Always initialize the parent Strategy class # After this, configuration is stored and available via `self.config` super().__init__(config) # Custom state variables self.time_started = None self.count_of_processed_bars: int = 0 def on_start(self) -> None: self.time_started = self.clock.utc_now() # Remember time, when strategy started self.subscribe_bars( self.config.bar_type ) # See how configuration data are exposed via `self.config` def on_bar(self, bar: Bar): self.count_of_processed_bars += 1 # Update count of processed bars # Instantiate configuration with specific values. By setting: # - InstrumentId - we parameterize the instrument the strategy will trade. # - BarType - we parameterize bar-data, that strategy will trade. # - StrategyId - we name this instance, which also fixes its order ID tag. config = MyStrategyConfig( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), bar_type=BarType.from_str("ETHUSDT-PERP.BINANCE-15-MINUTE-LAST-EXTERNAL"), trade_size=Decimal("1"), strategy_id=StrategyId("MyStrategy-001"), ) # Pass configuration to our trading strategy. strategy = MyStrategy(config=config) ``` Access configuration values through `self.config`. This provides clear separation between: - Configuration data (accessed via `self.config`): - Contains initial settings, that define how the strategy works. - Example: `self.config.trade_size`, `self.config.instrument_id` - Strategy state variables (as direct attributes): - Track any custom state of the strategy. - Example: `self.time_started`, `self.count_of_processed_bars` This separation makes code easier to understand and maintain. :::note Even though it often makes sense to define a strategy which will trade a single instrument. The number of instruments a single strategy can work with is only limited by machine resources. ::: ### Claiming external orders Live adapters can report venue orders that a strategy did not submit or that NautilusTrader has not yet observed locally. NautilusTrader creates these as external orders and assigns them to the `EXTERNAL` strategy unless an active claim identifies another strategy as their owner. Set `external_order_instrument_ids` to list the instruments a strategy intends to claim for external orders, fills, and materialized reconciliation activity: ```python from nautilus_trader.config import StrategyConfig from nautilus_trader.model import InstrumentId config = StrategyConfig( external_order_instrument_ids=[ InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), ], ) ``` `external_order_instrument_ids` is serializable configuration intent. When a live node registers the strategy, the runtime materializes that intent as active external order claims in the shared cache. Each claim assigns one `InstrumentId` to one `StrategyId`. Registration fails without changing the active claims if the shared cache is already borrowed, the list repeats an instrument, or any listed instrument already has a claim, including a claim for the same strategy. After registration, call `set_external_order_instrument_ids(...)` to replace the strategy's complete active claim set. The method retains listed claims already owned by the strategy, releases omitted claims, and acquires listed instruments that have no owner. Passing an empty list releases every claim owned by the strategy. If an instrument appears more than once or belongs to another strategy, the call fails and leaves the active claim set unchanged. A change affects ownership decisions made after the update; it does not reassign external orders already held in the cache. The strategy must be registered before it can update active claims, and the update fails if the shared cache is already borrowed. For Python strategies, this method changes active routing state; the original `strategy.config` object continues to show the construction intent. Stopping a strategy retains its active claims so routing remains stable if it restarts. Removing the strategy from its trader releases the claims. A cache reset also preserves them. :::warning Transferring an instrument between strategies requires two calls: the existing owner releases the instrument before the new owner claims it. The transfer is not atomic across strategies. An external report processed between those calls has no active claim and is assigned to the `EXTERNAL` strategy. ::: See [External order creation](execution/reconciliation.md#external-order-creation) for how execution and reconciliation use active claims. ### Managed GTD expiry It's possible for the strategy to manage expiry for orders with a time in force of GTD (*Good 'till Date*). This may be desirable if the exchange/broker does not support this time in force option, or for any reason you prefer the strategy to manage this. To use this option, pass `manage_gtd_expiry=True` to your `StrategyConfig`. When an order is submitted with a time in force of GTD, the strategy will automatically start an internal time alert. Once the internal GTD time alert is reached, the order will be canceled (if not already *closed*). On start, the strategy also reinstates alerts for its open GTD orders held in the cache, and cancels any whose expiry has already passed. When a cancel request is rejected, a running strategy restores a missing expiry alert for an open or inflight GTD order. If expiry has already passed, it immediately retries the cancel instead. Existing alerts are preserved. This runs before `on_order_cancel_rejected`, so an immediate retry can return the order to `PENDING_CANCEL` before the callback runs. A stopped strategy does not restore alerts or retry cancels on rejection. Some venues (such as Binance Futures) support the GTD time in force, so to avoid conflicts when using `manage_gtd_expiry` you should set `use_gtd=False` for your execution client config. ### Multiple strategies If you intend running multiple instances of the same strategy, with different configurations (such as trading different instruments), then each instance needs a **unique strategy ID and order ID tag**. The system must be able to identify which strategy various commands and events belong to. The order ID tag also keeps generated client order IDs unique across strategies for the same trader. #### Order ID tag Set `strategy_id` on each config. The runtime takes the order ID tag from the final hyphen-separated part of the strategy ID, so `MyStrategy-001` and `MyStrategy-002` produce the tags `001` and `002`. Supplying `order_id_tag` as well appends the tag to the runtime strategy ID, unless the ID already ends with that tag. For example, `strategy_id=StrategyId("MyStrategy-PRIMARY")` with `order_id_tag="ABC"` registers as `MyStrategy-PRIMARY-ABC`. A strategy registered without `strategy_id` takes its base ID from the strategy type name. An `order_id_tag` becomes the suffix, so `MyStrategy` with `order_id_tag="ABC"` registers as `MyStrategy-ABC`; without a tag, registration assigns the next numeric tag, starting with `000`. Because the runtime reads the tag back from the final hyphen-separated part of the strategy ID, an `order_id_tag` cannot contain a hyphen. `StrategyConfig(order_id_tag="A-B")` raises a `ValueError`, and so does a subclass constructed with that keyword. A config class that does not inherit from `StrategyConfig` carries the tag to registration instead, which raises a `RuntimeError`. The trader ID carries a tag in the same way, and its final hyphen-separated part reaches the same generated IDs. See [Configure a live trading node](../how_to/configure_live_trading.md) for the requirement that it stays unique across nodes. :::note The platform has built-in safety measures. Registering a duplicated strategy ID raises a `RuntimeError` indicating the strategy ID is already registered, and two different strategy IDs that share an order ID tag raise a `RuntimeError` reporting the tag conflict. ::: :::info Rust implementation Rust treats `StrategyConfig` as immutable construction input. The runtime `StrategyId` carries the order ID tag, matching the Python behavior. This keeps actor registration, client order ID generation, order list ID generation, and position ID generation aligned through `strategy_id.get_tag()`. ::: See the [`StrategyId` API Reference](/docs/python-api-latest/model/identifiers.html) for further details. ## Related guides - [Actors](actors.md) - Base class that strategies extend. - [Events](events/) - Event types and handler dispatch. - [Orders](orders/) - Order types and management from strategies. - [Backtesting](backtesting/) - Test strategies with historical data. # Synthetics Source: https://nautilustrader.io/docs/latest/concepts/synthetics/ **Synthetic instruments** are locally defined instruments whose prices derive from other instruments. They can combine components from one venue or many venues and expose the result as a standard Nautilus instrument with the synthetic venue code `SYNTH`. Synthetic instruments are useful for: - Enabling `DataActor` and `Strategy` components to subscribe to quote or trade feeds. - Triggering emulated orders from derived prices. - Constructing bars from synthetic quotes or trades. :::info Synthetic instruments cannot be traded directly. They exist locally within the platform and serve as analytical tools. In the future, Nautilus may support trading component instruments based on synthetic instrument behavior. ::: ## Formula language Each synthetic instrument defines a derivation formula. Nautilus evaluates this formula with its built-in numeric expression engine and converts the final numeric result to the synthetic `Price`. ### Supported syntax Formulas can reference component `InstrumentId` values directly, including IDs that contain `/` and `-`. | Construct | Example | Notes | | ------------------- | ---------------------------------------------- | --------------------------------------------------------------------- | | Component reference | `BTCUSDT.BINANCE` | Use the raw `InstrumentId` text. | | Component reference | `AUD/USD.SIM` | IDs containing `/` are valid. | | Component reference | `ETH-USDT-SWAP.OKX` | IDs containing `-` are valid. | | Numeric literal | `1`, `0.5`, `1.2e-3` | Evaluated with `f64` semantics. | | Boolean literal | `true`, `false` | Used in conditions and logical expressions. | | Parentheses | `(a + b) / 2` | Use parentheses to override precedence. | | Unary operators | `-x`, `!flag` | Unary `-` negates numbers. Unary `!` negates booleans. | | Binary operators | `+ - * / % ^`, `== !=`, `< <= > >=`, `&& \|\|` | Arithmetic is numeric. Logical operators are boolean. | | Local assignment | `spread = a - b; spread / 2` | Statements run from left to right. The formula must end with a value. | | Comments | `// line`, `/* block */` | Comments are ignored. | :::note New formulas should use raw `InstrumentId` values. For backward compatibility, formulas that replace `-` with `_` in component IDs remain accepted. ::: ### Operator precedence The expression engine evaluates operators in the following order, from highest precedence to lowest precedence: | Level | Operators | Notes | | ------- | -------------------- | ------------------------------------------------------------ | | Highest | `^` | Exponentiation. Right associative. | | | Unary `-`, unary `!` | `-2 ^ 2` evaluates as `-(2 ^ 2)`. | | | `*`, `/`, `%` | Multiplication, division, and modulo. | | | `+`, `-` | Addition and subtraction. | | | `<`, `<=`, `>`, `>=` | Numeric comparisons. | | | `==`, `!=` | Equality and inequality. Both sides must have the same type. | | Lowest | `&&`, `\|\|` | Boolean operators. | Assignments are not expression operators. Separate statements with `;`, and make the last statement the value you want the synthetic to produce. ### Built-in functions | Function | Signature | Notes | | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `abs` | `abs(x)` | Absolute value. | | `ceil` | `ceil(x)` | Ceiling. | | `floor` | `floor(x)` | Floor. | | `round` | `round(x)` | Round to the nearest integer using Rust `f64` rules. | | `min` | `min(x1, x2, ...)` | Accepts one or more numeric arguments. | | `max` | `max(x1, x2, ...)` | Accepts one or more numeric arguments. | | `if` | `if(condition, when_true, when_false)` | The condition must be boolean. Both branches must have the same type. Only the selected branch evaluates. | ### Type rules - Component inputs are numeric. - Arithmetic operators require numeric operands and return numeric results. - `<`, `<=`, `>`, `>=` require numeric operands and return boolean results. - `==` and `!=` accept any matching type (both numeric or both boolean) and return boolean results. - `&&`, `||`, and unary `!` require boolean operands. - `&&` and `||` short-circuit. The right-hand side evaluates only when needed. - Local variables must be assigned before use. - Local variable names must start with an ASCII letter or `_` and then use ASCII letters, digits, or `_`. - The final formula result must be numeric. A formula that ends with an assignment or produces a boolean result is invalid for a synthetic instrument. ### Limits The expression engine enforces the following compile-time limits. Formulas that exceed them produce a clear error at construction time. | Limit | Value | Description | | --------------- | ----- | ----------------------------------------------------------------------------------------------- | | Stack depth | 32 | Maximum number of intermediate values on the evaluation stack. | | Local variables | 16 | Maximum number of distinct local variable names. | | Nesting depth | 128 | Maximum syntax nesting and expression tree depth; the top-level expression counts as one level. | A weighted sum of 8 components uses a peak stack depth of 3 and zero locals. A weighted sum of N components builds a tree of depth N + 1, so the nesting depth limit caps a weighted sum at 127 components. ### Examples ```python # Simple spread formula = "BTCUSDT.BINANCE - ETHUSDT.BINANCE" # Average of two FX pairs formula = "(AUD/USD.SIM + NZD/USD.SIM) / 2" # Reuse an intermediate value formula = "spread = BTCUSDT.BINANCE - ETHUSDT.BINANCE; spread / 2" # Conditional output formula = "if(BTCUSDT.BINANCE > ETHUSDT.BINANCE, BTCUSDT.BINANCE, ETHUSDT.BINANCE)" ``` ## Creating a synthetic instrument Make sure all component instruments already exist in the cache, and subscribe to each component's quote or trade feed as well as the synthetic's. Synthetic quotes derive only from component quotes, and synthetic trades only from component trades. When a component tick arrives, the engine combines it with the latest cached prices of the other components to calculate the synthetic price. Until **every component has produced at least one tick**, the synthetic publishes nothing. The following example creates a synthetic instrument with an actor or strategy. This synthetic represents a simple spread between Bitcoin and Ethereum spot prices on Binance. It assumes that `BTCUSDT.BINANCE` and `ETHUSDT.BINANCE` already exist in the cache. ```python from nautilus_trader.model import SyntheticInstrument btcusdt_binance_id = InstrumentId.from_str("BTCUSDT.BINANCE") ethusdt_binance_id = InstrumentId.from_str("ETHUSDT.BINANCE") synthetic = SyntheticInstrument( symbol=Symbol("BTC-ETH:BINANCE"), price_precision=8, components=[ btcusdt_binance_id, ethusdt_binance_id, ], formula=f"{btcusdt_binance_id} - {ethusdt_binance_id}", ts_event=self.clock.timestamp_ns(), ts_init=self.clock.timestamp_ns(), ) self._synthetic_id = synthetic.id self.add_synthetic(synthetic) self.subscribe_quotes(self._synthetic_id) ``` :::note The synthetic `instrument_id` in the example above is `{symbol}.SYNTH`, which produces `BTC-ETH:BINANCE.SYNTH`. ::: ## Updating formulas You can update a synthetic formula at any time. ```python synthetic = self.cache.synthetic(self._synthetic_id) new_formula = "(BTCUSDT.BINANCE + ETHUSDT.BINANCE) / 2" synthetic.change_formula(new_formula) self.update_synthetic(synthetic) ``` ## Trigger instrument IDs You can trigger emulated orders from synthetic prices. In the following example, a synthetic instrument releases an emulated order once the synthetic price reaches the trigger condition. ```python order = self.order_factory.limit( instrument_id=InstrumentId.from_str("ETHUSDT.BINANCE"), order_side=OrderSide.BUY, quantity=Quantity.from_str("1.5"), price=Price.from_str("30000.00000000"), emulation_trigger=TriggerType.DEFAULT, trigger_instrument_id=self._synthetic_id, ) self.submit_order(order) ``` ## Performance Formulas compile once at construction time and evaluate on every incoming component price tick. The expression engine uses a compile-once/eval-many architecture with a zero-allocation f64 stack, so evaluation adds negligible overhead to the tick-processing path. Measured on Apple M4 Pro, rustc 1.94.1, release profile (opt-level 3): ### Evaluation (hot path) | Formula pattern | Time | | --------------------------------------- | ----- | | `(A + B) / 2.0` | 12 ns | | `A * 0.4 + B * 0.3 + C * 0.2 + D * 0.1` | 18 ns | | `if(A > B, A - B, B - A)` | 12 ns | | `spread = A - B; mid = ...; mid + ...` | 19 ns | | `max(min(A, B * 20), abs(A - B))` | 15 ns | ### Evaluation scaling (weighted sum) | Components | Time | | ---------- | ----- | | 2 | 14 ns | | 4 | 18 ns | | 8 | 28 ns | ### Compilation (cold path) | Formula pattern | Time | | ---------------- | ------ | | Simple average | 675 ns | | 4-input weighted | 1.4 us | | Conditional | 1.0 us | | With locals | 1.3 us | | Hyphenated IDs | 755 ns | ## Error handling Nautilus validates synthetic instruments at every boundary. Formula compilation rejects unknown symbols, type errors, and capacity overflows. Evaluation rejects wrong input counts and non-finite prices (NaN, Infinity) before they reach the formula. See the [`SyntheticInstrument` API Reference](/docs/python-api-latest/model/instruments.html#nautilus_trader.model.SyntheticInstrument) for input requirements and exceptions. ## Related guides - [Instruments](instruments/) - Instrument definitions and venue-specific instrument types. - [Data](data/) - Market data types that reference instruments. - [Orders](orders/) - Orders can use synthetic instrument IDs for emulation triggers. # Value Types Source: https://nautilustrader.io/docs/latest/concepts/value_types/ NautilusTrader provides specialized value types for representing core trading concepts: `Price`, `Quantity`, and `Money`. These types use fixed-point arithmetic internally for performant, deterministic calculations across different platforms and environments. ## Overview | Type | Purpose | Signed | Currency | | ---------- | ---------------------------------------- | ------ | -------- | | `Quantity` | Trade sizes, order amounts, positions. | No | - | | `Price` | Market prices, quotes, price levels. | Yes | - | | `Money` | Monetary amounts, P&L, account balances. | Yes | Yes | ## Immutability In Python, all value types are **immutable**. Once a value is constructed, it cannot be changed. Operations do not mutate the original object. ```python from nautilus_trader.model import Quantity qty1 = Quantity(100, precision=0) qty2 = Quantity(50, precision=0) # This creates a NEW Quantity; qty1 and qty2 are unchanged result = qty1 + qty2 print(qty1) # 100 print(qty2) # 50 print(result) # 150 ``` This design provides several benefits: - **Thread safety**: Immutable values can be safely shared across threads without synchronization. - **Predictability**: Values never change unexpectedly, making debugging easier. - **Hashability**: Immutable types can be used as dictionary keys and in sets. ## Arithmetic operations Value types support standard arithmetic operators (`+`, `-`, `*`, `/`, `%`, `//`) and unary operators (`-`, `+`, `abs`). The return type depends on the operator and the operand types. ### Same-type binary operations Addition and subtraction of the same value type return that type, preserving domain meaning (a price plus a price is still a price): | Operation | Result | | --------------------- | ---------- | | `Quantity + Quantity` | `Quantity` | | `Quantity - Quantity` | `Quantity` | | `Price + Price` | `Price` | | `Price - Price` | `Price` | | `Money + Money` | `Money` | | `Money - Money` | `Money` | ```python from nautilus_trader.model import Price price1 = Price(100.50, precision=2) price2 = Price(0.25, precision=2) result = price1 + price2 # Returns Price(100.75, precision=2) print(type(result)) # ``` Multiplication, division, floor division, and modulo between two values of the same type return `Decimal`: | Operation | Result | | ---------------- | --------- | | `Price * Price` | `Decimal` | | `Price / Price` | `Decimal` | | `Price // Price` | `Decimal` | | `Price % Price` | `Decimal` | The same pattern applies to `Quantity` and `Money`. These operations do not return the original type because the result has different dimensional meaning. Multiplying a price by a price produces "price squared", not a price. Dividing a quantity by a quantity produces a dimensionless ratio, not a quantity. Returning `Decimal` makes the unit change explicit and prevents misinterpretation of the result as a value with the original unit. ### Unary operations Unary operators preserve the value type where the result is valid for that type: | Operation | `Price` | `Quantity` | `Money` | | ---------- | --------- | ---------- | --------- | | `-x` (neg) | `Price` | `Decimal` | `Money` | | `+x` (pos) | `Price` | `Quantity` | `Money` | | `abs(x)` | `Price` | `Quantity` | `Money` | | `int(x)` | `int` | `int` | `int` | | `float(x)` | `float` | `float` | `float` | | `round(x)` | `Decimal` | `Decimal` | `Decimal` | `Quantity.__neg__` returns `Decimal` rather than `Quantity` because `Quantity` is unsigned and cannot represent a negative value. ```python from nautilus_trader.model import Currency, Money, Price, Quantity USD = Currency.from_str("USD") price = Price(100.50, precision=2) print(-price) # -100.50 print(type(-price)) # money = Money(-50.00, USD) print(abs(money)) # 50.00 USD print(type(abs(money))) # qty = Quantity(10, precision=0) print(+qty) # 10 print(type(+qty)) # ``` ### Mixed-type operations When operating with other numeric types, the result type follows Python's [numeric tower](https://docs.python.org/3/library/numbers.html) conventions. The general principle is that operations widen to the more general type: `float` operations return `float`, while `int` and `Decimal` operations return `Decimal` for precision preservation. This applies to all six binary operators (`+`, `-`, `*`, `/`, `//`, `%`) and works in both directions (`value op scalar` and `scalar op value`): | Left operand | Right operand | Result type | | ------------ | ------------- | ----------- | | Value type | `int` | `Decimal` | | Value type | `float` | `float` | | Value type | `Decimal` | `Decimal` | | `int` | Value type | `Decimal` | | `float` | Value type | `float` | | `Decimal` | Value type | `Decimal` | ```python from decimal import Decimal from nautilus_trader.model import Quantity qty = Quantity(100, precision=0) # Quantity + int -> Decimal result1 = qty + 50 print(type(result1)) # # Quantity + float -> float result2 = qty + 50.5 print(type(result2)) # # Quantity + Decimal -> Decimal result3 = qty + Decimal("50") print(type(result3)) # ``` ## Precision handling Each value type carries a precision indicating the number of decimal places: `Price` and `Quantity` store an explicit `precision` field, while `Money` uses its currency's precision. Precision is set at construction and is immutable. There is no "unspecified" precision. ### Fixed-point representation Value types are stored internally as integers scaled to a global fixed precision (e.g., 10^16 in high-precision mode), not floating-point numbers. The precision tracks the number of decimal places used at construction, controlling display formatting and serialization, but the underlying raw value always uses the global scale. ```python from nautilus_trader.model import Price p1 = Price(1.23, precision=2) # displays as "1.23" p2 = Price(1.230, precision=3) # displays as "1.230" p1 == p2 # True: same underlying value str(p1) # "1.23" str(p2) # "1.230" ``` **Precision controls display, not identity.** Two prices with the same decimal value but different precisions are equal. The `precision` field determines string formatting and how many decimal places are shown, but equality is based on the underlying numeric value. **Market data serialization uses precision metadata.** When market data types (quotes, trades, order book deltas) are written to Parquet or Arrow format, precision is stored in the file metadata so that values can be correctly decoded. All market data values within a single file must share the same precision. :::warning If a venue changes an instrument's tick size (and thus its precision), data files written before and after the change will have different precision metadata and should not be consolidated into a single file. ::: For how instrument-level precision constrains valid prices and quantities, see the [Precision](instruments/index.md#precision) section of the Instruments guide. ### Arithmetic precision When performing arithmetic between values with different precisions, the result uses the maximum precision of the operands. ```python from nautilus_trader.model import Price price1 = Price(100.5, precision=1) # 1 decimal place price2 = Price(0.125, precision=3) # 3 decimal places result = price1 + price2 print(result) # 100.625 print(result.precision) # 3 (max of 1 and 3) ``` ## Type-specific constraints ### Quantity `Quantity` represents non-negative amounts. Attempting to create a negative quantity or subtract a larger quantity from a smaller one raises an error: ```python from nautilus_trader.model import Quantity # This raises ValueError: Quantity cannot be negative qty = Quantity(-100, precision=0) # This also raises ValueError qty1 = Quantity(50, precision=0) qty2 = Quantity(100, precision=0) result = qty1 - qty2 # Would be -50, which is invalid ``` ### Money `Money` values include a currency. Addition and subtraction between `Money` values require **matching currencies**: ```python from nautilus_trader.model import Currency, Money USD = Currency.from_str("USD") EUR = Currency.from_str("EUR") usd_amount = Money(100.00, USD) eur_amount = Money(50.00, EUR) # This works - same currency result = usd_amount + Money(25.00, USD) # This raises ValueError - currency mismatch result = usd_amount + eur_amount ``` ## Common patterns ### Accumulating values Since value types are immutable, accumulate by reassigning: ```python from nautilus_trader.model import Currency, Money USD = Currency.from_str("USD") total = Money(0.00, USD) amounts = [Money(100.00, USD), Money(50.00, USD), Money(25.00, USD)] for amount in amounts: total = total + amount # Reassign to new Money instance print(total) # 175.00 USD ``` ### Converting to other types Value types provide conversion methods: ```python from nautilus_trader.model import Price price = Price(123.456, precision=3) # Convert to Decimal (preserves precision) decimal_value = price.as_decimal() # Convert to float float_value = price.as_double() # Convert to string string_value = str(price) # "123.456" ``` ### Creating from strings Parse value types from string representations: ```python from nautilus_trader.model import Money, Price, Quantity qty = Quantity.from_str("100.5") price = Price.from_str("99.95") money = Money.from_str("1000.00 USD") ``` # Visualization Source: https://nautilustrader.io/docs/latest/concepts/visualization/ NautilusTrader provides interactive HTML tearsheets for analyzing backtest results through an extensible visualization system built on Plotly. You can generate reports with minimal code and add custom charts and themes. ## Overview The visualization system has three parts: 1. **Chart Registry** - Decoupled chart definitions that can be extended with custom visualizations. 2. **Theme System** - Consistent styling with built-in and custom themes. 3. **Configuration** - Declarative specification of what to render and how to display it. Tearsheets are written as **self-contained HTML files** that can be viewed in any modern browser, shared with stakeholders, or archived for future reference. Passing a static image extension (such as `.png` or `.pdf`) as the output path exports a static image via Kaleido instead. :::note The visualization system requires the `visualization` extra. It installs Pandas for DataFrame handling, Plotly for interactive figures, and Kaleido for static image export: ```bash uv pip install --pre "nautilus_trader[visualization]" ``` ::: ## Tearsheets A tearsheet is a performance report that combines multiple charts and statistics into a single interactive visualization. Tearsheets are generated after completing a backtest run and provide immediate visual feedback on strategy performance. ### Quick start Generate a tearsheet with default settings: ```python from nautilus_trader.analysis import create_tearsheet from nautilus_trader.backtest import BacktestEngine # After running your backtest engine.run() # Generate tearsheet create_tearsheet( engine=engine, output_path="backtest_results.html", ) ``` This produces an HTML file with all default charts, using the light theme and automatic layout. Open `backtest_results.html` in your browser to view the interactive tearsheet. ### Backtest result input Let `result` be a `BacktestResult` returned by a completed backtest. Pass it without its node for a result-only tearsheet: ```python create_tearsheet( engine=result, output_path="backtest_results.html", ) ``` To include starting account balances from node reports, retain the node state. The node is also required when the configured tearsheet includes a cache-backed chart such as `bars_with_fills`. Follow the complete [`BacktestNode` setup](backtesting/apis-and-runs.md#high-level-api), setting `dispose_on_completion=False` on its `BacktestRunConfig`. Then pass the completed result and retained node: ```python create_tearsheet( engine=result, node=node, output_path="backtest_results.html", ) ``` :::warning Passing a node whose matching run configuration enables disposal raises `ValueError` because its cache and reports are no longer available. ::: ### Customization Control which charts appear and how they're styled: ```python from nautilus_trader.config import TearsheetConfig from nautilus_trader.analysis import TearsheetDrawdownChart from nautilus_trader.analysis import TearsheetEquityChart from nautilus_trader.analysis import TearsheetRunInfoChart from nautilus_trader.analysis import TearsheetStatsTableChart config = TearsheetConfig( charts=[ TearsheetRunInfoChart(), TearsheetStatsTableChart(), TearsheetEquityChart(), TearsheetDrawdownChart(), ], theme="nautilus_dark", height=2000, ) create_tearsheet( engine=engine, output_path="custom_tearsheet.html", config=config, ) ``` ### Currency filtering For multi-currency backtests, filter statistics to a specific currency: ```python from nautilus_trader.model import Currency create_tearsheet( engine=engine, output_path="usd_only.html", currency=Currency.from_str("USD"), # Shows only USD statistics ) ``` When `currency` is `None` (default), statistics for all currencies are displayed separately in the tearsheet. For `BacktestEngine` input, return-based charts require a single currency: they are derived from portfolio equity snapshots, falling back to account reports, and cannot be built for mixed-currency accounts without a filter; pass `currency` for multi-currency backtests so return charts use the selected currency. For `BacktestResult` input, `currency` filters PnL statistics and account balances. The result's **stored return series remains unchanged**. ## Available charts The tearsheet can include any combination of the following built-in charts: | Chart Name | Type | Description | | ----------------- | ----------- | ------------------------------------------------------- | | `run_info` | Table | Run metadata and account balances. | | `stats_table` | Table | Performance statistics (PnL, returns, general metrics). | | `equity` | Line | Cumulative returns over time with optional benchmark. | | `drawdown` | Area | Drawdown percentage from peak equity. | | `monthly_returns` | Heatmap | Monthly portfolio return percentages organized by year. | | `distribution` | Histogram | Distribution of individual return values. | | `rolling_sharpe` | Line | 60-day rolling Sharpe ratio. | | `yearly_returns` | Bar | Annual return percentages. | | `bars_with_fills` | Candlestick | Price bars (OHLC) with order fills overlaid as markers. | All charts are registered in the chart registry and are configured via chart objects in `TearsheetConfig.charts` (each chart object maps to a built-in chart name). ### Run information table The `run_info` chart displays key metadata about the backtest run: - Run ID, start time, finish time - Backtest period (start/end dates) - Total iterations processed - Event, order, and position counts - Account starting and ending balances (per currency) This table appears in the top-left position by default. ### Performance statistics table The `stats_table` chart displays performance metrics organized into sections: - **PnL Statistics** (per currency): Total PnL, win rate, profit factor, etc. - **Returns Statistics**: Sharpe ratio, Sortino ratio, max drawdown, etc. - **General Statistics**: Total trades, average trade duration, etc. This table appears in the top-right position by default. ### Equity curve The `equity` chart plots cumulative returns over the backtest period. When `benchmark_returns` is provided to `create_tearsheet()`, the benchmark is overlaid for comparison. ```python import pandas as pd # Load benchmark returns (e.g., from a market index) # Index should be datetime, aligned with strategy returns timeframe benchmark_returns = pd.read_csv("sp500_returns.csv", index_col=0, parse_dates=True)["return"] create_tearsheet( engine=engine, output_path="with_benchmark.html", benchmark_returns=benchmark_returns, benchmark_name="S&P 500", ) ``` The benchmark series is plotted as-is; ensure the index aligns with your strategy's return dates for accurate comparison. ### Monthly and yearly returns The `monthly_returns` and `yearly_returns` charts default to compounded (time-weighted) returns: each cell measures the period's gain against the running start-of-period balance, and the periods compound to the total return. Set `compounding=False` to report simple, non-compounding returns measured against fixed initial capital. Each cell then measures the period's gain as a percentage of the starting capital, so the periods sum to the total return instead of compounding to it. This is the nominal rate of return, the convention used for constant-capital strategies that trade fixed size and withdraw profits. ```python config = TearsheetConfig( charts=[ TearsheetMonthlyReturnsChart(compounding=False), TearsheetYearlyReturnsChart(compounding=False), ], ) create_tearsheet(engine=engine, config=config) ``` The standalone `create_monthly_returns_heatmap()` and `create_yearly_returns()` functions accept the same `compounding` argument. For the non-compounding figures to faithfully represent constant capital, size positions at a fixed quantity rather than as a fraction of current equity; otherwise later periods inflate as the running balance grows. ## Themes Themes control the visual styling of charts including colors, fonts, and backgrounds. NautilusTrader provides four built-in themes: | Theme Name | Description | Use Case | | --------------- | --------------------------------------------- | ------------------------------ | | `plotly_white` | Clean light theme with dark gray headers. | Default, professional reports. | | `plotly_dark` | Dark background with standard Plotly colors. | Low-light environments. | | `nautilus` | Light theme with NautilusTrader brand colors. | Official light mode. | | `nautilus_dark` | Dark theme with teal/cyan signature colors. | Official dark mode. | ### Selecting a theme Specify the theme in `TearsheetConfig`: ```python config = TearsheetConfig(theme="nautilus_dark") create_tearsheet(engine=engine, config=config) ``` ### Custom themes Register a custom theme for consistent branding across all visualizations: ```python from nautilus_trader.analysis import register_theme register_theme( name="corporate", template="plotly_white", # Base Plotly template colors={ "primary": "#003366", # Navy blue "positive": "#2e8b57", # Sea green "negative": "#c41e3a", # Cardinal red "neutral": "#808080", # Gray "background": "#ffffff", # White "grid": "#e5e5e5", # Light gray # Optional table colors (defaults will be provided if omitted) "table_section": "#e5e5e5", "table_row_odd": "#f8f8f8", "table_row_even": "#ffffff", "table_text": "#000000", }, ) # Use the custom theme config = TearsheetConfig(theme="corporate") ``` The theme system automatically provides sensible defaults for `table_*` colors based on the `background` and `grid` colors, ensuring backward compatibility with themes registered before table-specific colors were introduced. ## Configuration The `TearsheetConfig` class provides declarative control over tearsheet generation: ```python from nautilus_trader.analysis import GridLayout from nautilus_trader.config import TearsheetConfig from nautilus_trader.analysis import TearsheetDrawdownChart from nautilus_trader.analysis import TearsheetEquityChart from nautilus_trader.analysis import TearsheetStatsTableChart config = TearsheetConfig( charts=[ TearsheetEquityChart(), TearsheetDrawdownChart(), TearsheetStatsTableChart(), ], theme="nautilus_dark", title="Q4 2024 Strategy Performance", height=1800, include_benchmark=True, benchmark_name="SPY", layout=GridLayout( rows=2, cols=2, heights=[0.60, 0.40], vertical_spacing=0.08, horizontal_spacing=0.12, ), ) ``` ### Configuration parameters | Parameter | Type | Default | Description | | ------------------- | ---------------------- | ---------------- | ----------------------------------- | | `charts` | `list[TearsheetChart]` | Built-ins | Charts to include, in order. | | `theme` | `str` | `"plotly_white"` | Theme name for styling. | | `layout` | `GridLayout` | `None` | Custom subplot grid layout. | | `title` | `str` | Auto-generated | Tearsheet title. | | `include_benchmark` | `bool` | `True` | Show benchmark when provided. | | `benchmark_name` | `str` | `"Benchmark"` | Display name for benchmark. | | `height` | `int` | `1500` | Total height in pixels. | | `show_logo` | `bool` | `True` | Reserved for future logo rendering. | When `layout` is `None`, the grid dimensions and row heights are automatically calculated based on the number of charts. For 8 charts (the default), a 4x2 grid is used with heights `[0.50, 0.22, 0.16, 0.12]` to give more space to the top row tables. ## Custom charts The registry pattern lets you add custom charts. Charts are functions that render traces onto a Plotly figure object. ### Registering a custom chart ```python from nautilus_trader.analysis import register_chart import plotly.graph_objects as go def my_custom_chart(returns, output_path=None, title="Custom Chart", theme="plotly_white"): """ Create a custom visualization. This function signature matches the built-in chart functions for consistency. """ from nautilus_trader.analysis import get_theme theme_config = get_theme(theme) # Create your visualization fig = go.Figure() fig.add_trace( go.Scatter( x=returns.index, y=returns.cumsum(), mode="lines", name="Custom Metric", line={"color": theme_config["colors"]["primary"]}, ) ) fig.update_layout( title=title, template=theme_config["template"], xaxis_title="Date", yaxis_title="Value", ) if output_path: fig.write_html(output_path) return fig # Register the chart for standalone use (via `get_chart()` / `list_charts()`) register_chart("my_custom", my_custom_chart) ``` ### Tearsheet integration For tearsheet integration with proper grid placement, use `register_tearsheet_chart`. Unlike `register_chart` (which registers a standalone function that returns its own figure), a tearsheet renderer draws traces directly onto a shared subplot grid cell, so its signature takes the target `fig` plus the `row` and `col` to render into. ```python from nautilus_trader.config import TearsheetConfig from nautilus_trader.analysis import TearsheetCustomChart from nautilus_trader.analysis import TearsheetEquityChart from nautilus_trader.analysis import TearsheetStatsTableChart from nautilus_trader.analysis import register_tearsheet_chart def _render_my_metric(fig, row, col, returns, theme_config, **kwargs): """ Render custom metric directly onto a subplot. Parameters ---------- fig : go.Figure The figure to add traces to. row : int Subplot row position. col : int Subplot column position. returns : pd.Series Strategy returns series supplied to the renderer. theme_config : dict Theme configuration dictionary. **kwargs : dict Additional parameters (stats_pnls, stats_returns, benchmark_returns, etc.). """ metric_values = returns.rolling(30).std() * 100 # Example metric fig.add_trace( go.Scatter( x=returns.index, y=metric_values, mode="lines", name="30-Day Volatility", line={"color": theme_config["colors"]["neutral"]}, ), row=row, col=col, ) fig.update_xaxes(title_text="Date", row=row, col=col) fig.update_yaxes(title_text="Volatility (%)", row=row, col=col) # Register for tearsheet use register_tearsheet_chart( name="volatility", subplot_type="scatter", title="Rolling Volatility (30-day)", renderer=_render_my_metric, ) # Now "volatility" can be used in TearsheetConfig.charts: config = TearsheetConfig( charts=[ TearsheetStatsTableChart(), TearsheetEquityChart(), TearsheetCustomChart(chart="volatility"), ], ) ``` The renderer function receives all necessary data (returns, statistics, theme configuration) and renders directly onto the specified subplot position. ## Offline analysis For situations where you have precomputed statistics but not a `BacktestEngine` instance, use the lower-level API: ```python import pandas as pd from nautilus_trader.analysis import create_tearsheet_from_stats # Load precomputed data. The structure matches BacktestResult stats fields. stats_pnls = {"USD": {"PnL (total)": 1500.0, "Win Rate": 0.55, ...}} # Per-currency stats_returns = {"Sharpe Ratio (252 days)": 1.2, "Max Drawdown": -0.15, ...} stats_general = {"Avg Winner": 100.0, "Avg Loser": -50.0, ...} returns = pd.Series(...) # Daily returns with datetime index create_tearsheet_from_stats( stats_pnls=stats_pnls, stats_returns=stats_returns, stats_general=stats_general, returns=returns, output_path="offline_analysis.html", ) ``` The dictionary keys should match those returned by `engine.get_result().stats_pnls`, `engine.get_result().stats_returns`, and `engine.get_result().stats_general`. This approach is useful for: - Analyzing results from multiple backtest runs stored separately. - Comparing strategies using precomputed metrics. - Integrating with external analysis pipelines. ## Best practices ### Chart selection - Use default charts for exploratory analysis to see all available metrics. - Customize charts when you know which metrics matter for your strategy. - Remove irrelevant charts to reduce visual clutter and file size. ### Theme usage - Use `plotly_white` for professional reports and presentations. - Use `nautilus_dark` for official materials or low-light viewing. - Create custom themes to match internal guidelines or personal preferences. ### Performance considerations - Tearsheet HTML files contain all data inline and can be several megabytes for long backtests. - Consider generating separate tearsheets for different analysis timeframes. - For very large datasets, use the individual chart functions instead of full tearsheets. ### Custom statistics integration Custom charts work best when paired with statistics supplied through the same `stats_pnls`, `stats_returns`, and `stats_general` dictionaries used by the built-in tearsheet charts. For `BacktestEngine` input these values come from `engine.get_result()`, so a statistic registered with `Portfolio.register_statistic()` reaches the tearsheet without any extra wiring; see [Custom statistics](portfolio.md#custom-statistics). For offline analysis, pass compatible dictionaries directly to `create_tearsheet_from_stats()`: ```python stats_returns = { "Sharpe Ratio (252 days)": 1.2, "Custom Volatility Score": 0.42, } ``` ## API levels The visualization system provides two API levels, plus standalone chart functions: ### High-level API Recommended for most use cases: ```python create_tearsheet(engine=engine, config=config) ``` Automatically extracts data from a `BacktestEngine` or `BacktestResult`, generates all configured charts, and produces a complete HTML tearsheet. ### Low-level API For advanced customization or offline analysis: ```python create_tearsheet_from_stats( stats_pnls=stats_pnls, stats_returns=stats_returns, stats_general=stats_general, returns=returns, run_info=run_info, account_info=account_info, config=config, ) ``` Provides fine-grained control over data inputs and allows analysis of precomputed statistics. ### Standalone chart functions Individual chart functions can be used independently to generate single-purpose HTML visualizations or Plotly figures for custom analysis workflows. #### Price bars with fills The `create_bars_with_fills` function generates a candlestick chart with order fills overlaid, useful for visually analyzing strategy execution within price action. It can be used standalone or included in tearsheets: ```python from nautilus_trader.analysis import create_bars_with_fills from nautilus_trader.analysis import create_tearsheet from nautilus_trader.analysis import TearsheetBarsWithFillsChart from nautilus_trader.config import TearsheetConfig from nautilus_trader.analysis import TearsheetEquityChart from nautilus_trader.analysis import TearsheetStatsTableChart from nautilus_trader.model import BarType # Standalone usage bar_type = BarType.from_str("ESM4.XCME-1-MINUTE-LAST-EXTERNAL") fig = create_bars_with_fills( engine=engine, bar_type=bar_type, title="ES Futures - Entry/Exit Analysis", ) fig.show() # Display in Jupyter fig.write_html("bars_with_fills.html") # Or save to file # Include in tearsheet config = TearsheetConfig( charts=[ TearsheetStatsTableChart(), TearsheetEquityChart(), TearsheetBarsWithFillsChart( bar_type="ESM4.XCME-1-MINUTE-LAST-EXTERNAL", title="Bars with Fills", ), ], ) create_tearsheet(engine=engine, config=config) # Multiple bars-with-fills charts in one tearsheet config = TearsheetConfig( charts=[ TearsheetStatsTableChart(), TearsheetEquityChart(), TearsheetBarsWithFillsChart( bar_type=f"{instrument.id}-5-MINUTE-MID-INTERNAL", title=f"Bars with Order Fills - {instrument.id}", ), TearsheetBarsWithFillsChart( bar_type=f"{other_instrument.id}-5-MINUTE-MID-INTERNAL", title=f"Bars with Order Fills - {other_instrument.id}", ), ], ) create_tearsheet(engine=engine, config=config) ``` The visualization shows candlesticks for OHLC price action with triangle markers representing order fills (up-triangles for buys and down-triangles for sells, colored with the theme's positive and negative colors). Charts that need extra configuration (like `bar_type`) take those parameters directly on the chart object (e.g. `TearsheetBarsWithFillsChart(bar_type=...)`). Other individual chart functions include `create_equity_curve`, `create_drawdown_chart`, `create_monthly_returns_heatmap`, and more. See the API reference for the complete list. ## Related guides - [Backtesting](backtesting/) - Learn how to run backtests that generate tearsheets. - [Reports](reports.md) - Understand the underlying statistics displayed in tearsheets. - [Portfolio](portfolio.md) - Explore portfolio tracking and performance metrics. # Adapters Source: https://nautilustrader.io/docs/latest/developer_guide/adapters/ ## Introduction Use this guide to build or extend a Rust-native adapter for NautilusTrader. Adapters connect the platform to venues and data providers, preserve venue semantics, produce valid Nautilus domain events, and make uncertain outcomes explicit. They implement the platform data and execution client traits in Rust, then expose configs, factories, and selected low-level APIs to Python through PyO3. :::note For out-of-tree adapters implemented in Python or an independent Rust/PyO3 package, use the [Python adapter interface](python_adapters.md). This guide covers in-tree Rust adapters. ::: Use reference adapters selectively. Their layouts reflect different venue protocols, product families, and implementation histories. | Adapter | Useful reference | | ------------------ | ------------------------------------------------------------------------------------------------------ | | [Bybit][bybit] | Multi-product HTTP and WebSocket clients, options data, and execution outcome handling. | | [OKX][okx] | Public, private, and business WebSocket endpoints with broad instrument coverage. | | [Binance][binance] | Spot and futures product splits, trading WebSockets, and SBE market data. | | [Kraken][kraken] | Spot and futures submodules with distinct HTTP, WebSocket, data, and execution paths. | | [Lighter][lighter] | Layer-2 signing, canonical benchmarks, coverage-guided fuzzing, and detailed execution state handling. | | [Derive][derive] | JSON-RPC data and execution, EIP-712 signing, canonical benchmarks, and invariant-based fuzzing. | This guide distinguishes four kinds of guidance: - **Shared rules** come from common traits, network abstractions, hooks, or CI. - **Common patterns** appear in several adapters but allow other designs. - **Examples** show one sound implementation without making it mandatory. - **Exceptions** are valid when venue semantics or protocol boundaries require them. ## Conformance An adapter conforms when it satisfies each rule below that applies to it, or documents an exception. Name the venue behavior that forces the exception, keep it inside the adapter, and cover it with a test that fails if the venue stops requiring it. [Phase 7](#phase-7-prove-conformance) sequences the work that proves conformance. ### Adapter foundations | Rule | Applies to | | ------------------------------------------------------------------------------------------ | ------------------ | | [Repository and Python wiring](#repository-and-python-wiring) | New adapter crates | | [Credentials and secret handling](#credentials-and-secret-handling) | Every adapter | | [Configurations](#configurations-configrs) | Every adapter | | [Symbols and instrument identity](#symbols-and-instrument-identity) | Every adapter | | [Venue payload modeling and precision](#modeling-venue-payloads) | Every adapter | | [Client traits and factories](#client-traits-and-factories-datars-executionrs-factoriesrs) | Every adapter | ### Runtime and client lifecycle | Rule | Applies to | | ----------------------------------------------------- | -------------------------- | | [Connection lifecycle](#connection-lifecycle-connect) | Data and execution clients | | [Data events and request freshness](#data-client) | Data clients | | [Backpressure](#backpressure) | Every adapter | | [Task management](#task-management) | Every adapter | ### Execution and reconciliation | Rule | Applies to | | --------------------------------------------------------------------------------------------- | ----------------- | | [Execution client boundaries](#execution-client) | Execution clients | | [Reconciliation reports](#reconciliation-reports) | Execution clients | | [Commission failure handling](#commission-failure-handling) | Execution clients | | [Bounded mass-status reports](#bounded-mass-status-reports) | Execution clients | | [Instrument resolution during reconciliation](#instrument-resolution-during-reconciliation) | Execution clients | | [Tracked and external execution updates](#tracked-and-external-execution-updates) | Execution clients | | [Event ordering and deduplication](#event-ordering-and-deduplication) | Execution clients | | [Order command outcome policy](#order-command-outcome-policy) | Execution clients | | [Naming the evidence classes](#naming-the-evidence-classes) | Execution clients | | [Diagnostics and strategy-facing reasons](#separate-diagnostics-from-strategy-facing-reasons) | Execution clients | ### Transport and streaming | Rule | Applies to | | ------------------------------------------------------------------------------- | -------------------------------- | | [Request flow](#request-flow) | HTTP clients | | [Request signing and authentication](#request-signing-and-authentication) | HTTP and WebSocket request paths | | [Error handling and retry logic](#error-handling-and-retry-logic) | HTTP and WebSocket request paths | | [Rate limiting](#rate-limiting) | HTTP and WebSocket clients | | [Handler initialization handshake](#handler-initialization-handshake-setclient) | WebSocket clients | | [Authentication](#authentication) | WebSocket clients | | [Subscription management](#subscription-management) | WebSocket clients | | [Message routing](#message-routing) | WebSocket clients | | [Reconnection and shutdown](#reconnection-and-shutdown) | WebSocket clients | The [data testing specification](spec_data_testing.md) and [execution testing specification](spec_exec_testing.md) hold the scenarios that prove these contracts against a venue. ### Shared baseline Use the shared implementation of each piece below, then use any state structure that satisfies the contract it implements. The shared type carries that contract with it and keeps behavior comparable across venues, so a local structure has to prove the same contract on its own terms. Two execution clients implement the same trait without trading through a venue API, so the baseline does not apply to them: [sandbox](../../crates/adapters/sandbox/src/execution.rs) simulates fills locally, and [blockchain](../../crates/adapters/blockchain/src/execution/client.rs) executes on-chain behind the `defi` feature. Deterministic simulation eligibility also sits outside the baseline, as an optional capability proven per adapter rather than a requirement. | Target | Shared piece | Contract | | -------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | | Command outcome evidence | [`CommandFailure`](../../crates/live/src/execution/failure.rs) | [Naming the evidence classes](#naming-the-evidence-classes) | | Order identity and context | [`OrderIdentity` and `OrderContext`](../../crates/live/src/execution/context.rs) | [Tracked and external updates](#tracked-and-external-execution-updates) | | Replay deduplication | [`FifoCache` and `FifoCacheMap`](../../crates/common/src/cache/fifo.rs) | [Event ordering and deduplication](#event-ordering-and-deduplication) | | Order denial reasons | [`OrderDeniedReason`](../../crates/model/src/events/order/denied_reason.rs) | [Diagnostics and reasons](#separate-diagnostics-from-strategy-facing-reasons) | | Task lifecycle | [`TaskGroup`](../../crates/live/src/task.rs) and [`TaskHandles`](../../crates/common/src/live/task.rs) | [Task management](#task-management) | | Ingestion precision | [Domain numeric types](rust.md#domain-numeric-types) | [Venue payload modeling](#modeling-venue-payloads) | | HTTP transport | [`HttpClient`](../../crates/network/src/http/client.rs) | [Request flow](#request-flow) | | Authentication state | [`AuthTracker`](../../crates/network/src/websocket/auth.rs) | [Authentication](#authentication) | | Subscription identity | [`SubscriptionState`](../../crates/network/src/websocket/subscription.rs) | [Subscription management](#subscription-management) | | Reconnect requests | [`request_reconnect`](../../crates/network/src/websocket/client.rs) | [Reconnection and shutdown](#reconnection-and-shutdown) | | Retry machinery | [`RetryManager`](../../crates/network/src/retry.rs) | [Error handling and retry logic](#error-handling-and-retry-logic) | | Inferred fill commission | [`ExecutionClient`](../../crates/common/src/clients/execution.rs) | [Commission failure handling](#commission-failure-handling) | Where a venue transmits a discrete value as an IEEE-754 field rather than a decimal string or JSON number, contain that at the parsing boundary as a documented exception instead of letting `f64` spread inward from it. Retry classification is the exception to this table: it stays adapter-owned because venue status codes and rate-limit semantics differ. The shared machinery around it is not. See [error handling and retry logic](#error-handling-and-retry-logic) for both halves. ## Structure of an adapter The Rust crate is the source of truth for protocol behavior. An adapter commonly separates these concerns: ```text crates/adapters// ├── Cargo.toml ├── src/ │ ├── common/ # Shared credentials, enums, models, parsing, symbols, and URLs │ ├── http/ # Typed requests, responses, signing hooks, and transport client │ │ ├── client.rs │ │ ├── error.rs │ │ ├── models.rs │ │ ├── parse.rs │ │ └── query.rs │ ├── websocket/ # Streaming transport, protocol messages, parsing, and routing │ │ ├── client.rs │ │ ├── handler.rs │ │ ├── messages.rs │ │ ├── parse.rs │ │ ├── subscription.rs # When subscription identity or replay needs a boundary │ │ └── dispatch.rs # When execution routing needs a boundary │ ├── config.rs │ ├── data.rs # Or data/ when product implementations need a split │ ├── execution.rs # Or execution/ when product implementations need a split │ ├── factories.rs │ ├── python/ # PyO3 projection │ ├── signing/ # When authentication or transaction signing is a subsystem │ └── lib.rs ├── tests/ # Public Rust boundary tests ├── test_data/ # Canonical venue payloads and protocol vectors ├── benches/ # When confirmed hot paths warrant benchmarks │ ├── common/ # Shared benchmark fixtures │ ├── data.rs │ ├── exec.rs │ └── micros.rs ├── fuzz/ # When untrusted codecs warrant coverage-guided fuzzing │ ├── fuzz_targets/ │ └── README.md ├── examples/ # Rust tester nodes and focused usage examples ├── bin/ # Optional protocol inspection or capture tools └── README.md ``` Python and documentation surfaces sit outside the crate: ```text python/nautilus_trader/adapters// # Public package and generated stubs examples/live// # Python data and execution testers python/tests/unit/adapters// # Public Python package tests docs/integrations/.md # User-facing integration guide ``` Only `Cargo.toml` and `src/lib.rs` are universal crate boundaries. Add the other modules when the adapter needs them: - Put symbols, credentials, URLs, shared enums, and shared parsing under `common/`. - Put transport models, typed requests, signing, and HTTP clients under `http/`. - Put frames, messages, subscription state, routing, and WebSocket clients under `websocket/`. - Implement live data and execution traits in `data.rs` and `execution.rs`, or in product submodules when the venue exposes materially different protocols. - Keep PyO3 projection code under `python/`. - Organize integration tests by public boundary or product. Do not force all adapters into the same filenames. Product-specific splits are legitimate when product families have different protocols. A shared client can also span distinct endpoints when request and state semantics remain common. Match the venue's real boundaries and keep shared behavior above those splits. An adapter's public Python package lives under `python/nautilus_trader/adapters//` and usually re-exports generated bindings. Change Rust binding metadata or other generator inputs, then run `make py-stubs`; do not edit generated `.pyi` files. ### Repository and Python wiring A new adapter crate must be discoverable by each build surface that owns it: | Surface | Required change | Enforcement or proof | | ----------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Root Rust workspace | Add the crate to the members and workspace dependencies in [`Cargo.toml`](../../Cargo.toml). | Workspace metadata and targeted Cargo checks discover the crate. | | Workspace test inventory | Add the crate to `ADAPTER_CRATES` in the [`Makefile`](../../Makefile). | The [workspace coverage check](../../scripts/ci/check-workspace-test-coverage.sh) requires one test inventory. | | PyO3 crate | Add the optional dependency and feature propagation in [`crates/pyo3/Cargo.toml`](../../crates/pyo3/Cargo.toml). | Building the matching PyO3 feature compiles the adapter projection. | | PyO3 root module | Register the adapter module in [`crates/pyo3/src/lib.rs`](../../crates/pyo3/src/lib.rs). | The conventions hook treats this module list as the public API allowlist. | | Adapter PyO3 registry | Register each applicable factory and config extractor with `get_global_pyo3_registry()`. | Factory boundary tests prove Python config objects reach the Rust factories. | | Python package and user guide | Add package projection, tests, examples, and an integration guide only for capabilities the adapter exposes. | Import, generated drift, example build, and documentation checks cover these surfaces. | The [Nautilus conventions hook](../../.pre-commit-hooks/check_nautilus_conventions.sh) treats the PyO3 module list as a public API allowlist. The [PyO3 conventions hook](../../.pre-commit-hooks/check_pyo3_conventions.sh) also enforces: - Stub metadata uses `nautilus_trader.adapters.`. - Runtime extension imports use `nautilus_trader._libnautilus.`. - A Rust function renamed with `#[pyo3(name = ...)]` has a `py_` Rust name. - Python exceptions use the project error conversion functions. ## Adapter implementation sequence Use these phases to organize the work. They describe dependencies, not release gates. A market-data-only adapter omits execution, and an adapter can complete one product before starting another. Keep the capability matrix current throughout the work rather than waiting for the final documentation phase. Omit phases and steps that do not apply to the adapter. ### Phase 0: Define scope | Step | Component | Work | | ---- | ------------------- | --------------------------------------------------------------------------------------------------------- | | 0.1 | Capability matrix | List the products, environments, account modes, data types, order types, and reports in scope. | | 0.2 | Venue constraints | Record venue restrictions, unsupported capabilities, and testnet differences. | | 0.3 | Protocol boundaries | Identify separate product APIs, public and private endpoints, and binary or JSON transports. | | 0.4 | Initial slice | Choose the smallest slice that proves an end-to-end path. | | 0.5 | Repository wiring | Add the crate to the Rust workspace and test inventory, then add only the projection surfaces it exposes. | **Exit:** The integration guide contains an initial capability matrix, known gaps, and a test plan. ### Phase 1: Build the protocol core | Step | Component | Work | | ---- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | 1.1 | HTTP error types | Model transport, HTTP status, venue, parsing, and validation failures; classify retryability when supported. | | 1.2 | HTTP client | Implement endpoint resolution and typed requests, plus credentials, signing, rate limits, retries, and pagination as needed. | | 1.3 | HTTP API models | Define typed requests and responses, commonly under `http/` or its product-specific modules. | | 1.4 | HTTP parsing | Convert venue responses to domain types at deterministic boundaries in `http/parse.rs` or `common/parse.rs`. | | 1.5 | WebSocket error types | Model connection, protocol, and parsing failures, plus authentication and command failures when applicable. | | 1.6 | WebSocket client | Implement lifecycle and shutdown, plus authentication, heartbeat, subscription state, and reconnection when applicable. | | 1.7 | WebSocket messages | Define frames and messages under `websocket/` or product-specific modules; include acknowledgements and venue errors as needed. | | 1.8 | WebSocket parsing | Decode each frame once, convert domain events, and route data or execution messages by typed identity. | | 1.9 | Protocol tests | Prove fixtures, canonical requests, applicable signing vectors, lifecycle, and raw exchanges with mock peers. | **Exit:** The crate compiles, protocol fixtures parse, applicable signing vectors pass, and mock or controlled requests complete any required authentication and exchange raw venue messages. ### Phase 2: Implement instruments | Step | Component | Work | | ---- | ------------------ | -------------------------------------------------------------------------------------------------------- | | 2.1 | Instrument parsing | Parse every supported family with complete identity, precision, currency, and contract fields. | | 2.2 | Instrument loading | Load, filter, cache, and emit definitions at each parsing boundary that needs context. | | 2.3 | Symbol mapping | Define bidirectional venue symbol and `InstrumentId` conversion without collapsing distinct instruments. | | 2.4 | Instrument updates | Implement fresh instrument requests and any supported definition or status updates. | **Exit:** Distinct fixtures cover every supported instrument family, invalid definitions fail clearly, and the data client emits or returns complete Nautilus instruments. ### Phase 3: Implement market data Start with one public stream and one instrument before adding product or endpoint fan-out. | Step | Component | Work | | ---- | ------------------------ | ----------------------------------------------------------------------------------------------------------- | | 3.1 | Public WebSocket streams | Subscribe and unsubscribe each advertised live data type while preserving subscription intent. | | 3.2 | Historical data requests | Request supported bars, trades, quotes, or order book snapshots with exact correlation and freshness rules. | | 3.3 | Data client | Implement `DataClient` requests, subscriptions, lifecycle, and complete `DataEvent` emission. | | 3.4 | Order book handling | Preserve snapshot, incremental update, sequence, clear, and batch boundaries. | | 3.5 | Stream recovery | Handle malformed input, gaps, unsubscribe, disconnect, reconnect, and subscription replay. | **Exit:** Unit and mock transport tests prove complete domain events for the supported request and subscription matrix. ### Phase 4: Implement execution Establish account state and reconciliation before enabling order flow. | Step | Component | Work | | ---- | ---------------------- | ------------------------------------------------------------------------------------------------------------ | | 4.1 | Account bootstrap | Establish account identity, initial account state, private subscriptions, and connected readiness. | | 4.2 | Reconciliation reports | Generate applicable order, fill, position, and mass-status reports at startup and on demand. | | 4.3 | Basic order submission | Implement supported market and limit order submission with deterministic local validation. | | 4.4 | Order modification | Implement supported modify and cancel commands, including cancel-replace venue semantics. | | 4.5 | Execution client | Implement `ExecutionClient` commands, lifecycle, tracked and external routing, and ordered event emission. | | 4.6 | Outcome recovery | Preserve unknown outcomes, deduplicate fills, and resolve state through streams, queries, or reconciliation. | **Exit:** Mock transport tests cover every supported command, definitive rejection, uncertain transmission, duplicate or out-of-order updates, and startup reconciliation. ### Phase 5: Add optional venue capabilities Add these only after the base lifecycle is stable. | Step | Component | Work | | ---- | -------------------------- | ------------------------------------------------------------------------------------------------- | | 5.1 | Advanced order types | Add applicable conditional, stop, take-profit, trailing-stop, or other advanced orders. | | 5.2 | Batch operations | Add batch submission, batch cancellation, and mass cancel with per-order result handling. | | 5.3 | Venue-specific data | Add funding, greeks, liquidations, or venue extensions as separate capability slices. | | 5.4 | Product or endpoint splits | Split ownership only when protocol, authentication, quota, or recovery boundaries require it. | | 5.5 | Capability proof | Add fixtures, functional tests, acceptance cases, and documented limitations for each capability. | **Exit:** Each optional capability is independently testable and does not weaken the established base paths. ### Phase 6: Complete factories and projection | Step | Component | Work | | ---- | --------------------- | ------------------------------------------------------------------------------------------------ | | 6.1 | Configuration structs | Finalize typed data and execution configs, defaults, environment fallback, and secret redaction. | | 6.2 | Client factories | Implement Rust factories with `CacheView` inputs and the data client clock. | | 6.3 | PyO3 registration | Register applicable factories and config extractors with the PyO3 registry. | | 6.4 | Python package | Add the public package and Python boundary tests for the capabilities exposed to Python. | | 6.5 | Generated stubs | Add Rust stub metadata and regenerate the `.pyi` output with `make py-stubs`. | **Exit:** Rust factory tests and PyO3 boundary tests pass, package imports resolve, and generated output matches its Rust inputs. ### Phase 7: Prove conformance | Step | Component | Work | | ---- | ---------------------- | ------------------------------------------------------------------------------------------------------ | | 7.1 | Rust unit tests | Prove parsers, serializers, symbols, signatures, state transitions, and malformed input. | | 7.2 | Rust integration tests | Exercise public HTTP, WebSocket, data, and execution boundaries against deterministic mock transports. | | 7.3 | Python boundary tests | Prove imports, config extraction, factories, type conversion, and representative async calls. | | 7.4 | Acceptance tests | Run every applicable `DataTester` and `ExecTester` case on testnet or a controlled account. | | 7.5 | Recovery tests | Exercise connection failure, reconnect, shutdown, rate limits, and state recovery. | | 7.6 | Specification gaps | Record every skipped specification case with a venue or capability reason. | **Exit:** The applicable data and execution testing specifications pass, and every advertised capability has deterministic and venue evidence. ### Phase 8: Measure performance and robustness | Step | Component | Work | | ---- | -------------------- | --------------------------------------------------------------------------------------------------------- | | 8.1 | Canonical benchmarks | Measure confirmed end-to-end data and execution hot paths with representative fixtures. | | 8.2 | Microbenchmarks | Isolate confirmed signing, hashing, authentication, codec, parsing, or serialization costs. | | 8.3 | Fuzz targets | Fuzz untrusted parsing, decoding, normalization, signing, and encoding boundaries with realistic corpora. | | 8.4 | Invariants | Assert domain and protocol properties stronger than panic freedom. | **Exit:** Canonical benchmark and fuzz suites run with representative fixtures, documented invariants, and no mandatory categories that the adapter does not use. ### Phase 9: Finish documentation and operations | Step | Component | Work | | ---- | ------------------- | ---------------------------------------------------------------------------------------------- | | 9.1 | Capability matrix | Reconcile every support claim and exception with the tested implementation. | | 9.2 | Integration guide | Document credentials, config, limits, reconciliation, environment differences, and known gaps. | | 9.3 | Tester entry points | Provide applicable Rust and Python data and execution testers with safe defaults. | | 9.4 | Operations | Document recovery, troubleshooting, and any venue behavior an operator must understand. | | 9.5 | Final verification | Verify links, generated output, examples, and the focused documentation checks. | **Exit:** A user can configure, test, operate, and diagnose the adapter without reading its source. ## Rust adapter patterns Repository-wide import policy applies to adapter code: import Nautilus types and use their short names instead of fully qualifying them at call sites. The [Nautilus conventions hook](../../.pre-commit-hooks/check_nautilus_conventions.sh) enforces this rule and documents its scoped exception marker. ### Configurations (`config.rs`) Follow the shared [configuration guide](../concepts/configuration.md). In particular, Rust configs use typed fields, strict Serde decoding, one source of truth for defaults, and `bon::Builder`. Adapter configs then add only venue semantics: - Use an enum for a closed set such as environment, product family, account mode, or endpoint. - Use `Option` only when absence has a distinct meaning, including runtime credential fallback. - Keep data and execution config separate when their capabilities or credentials differ. - Store fields that must not appear in `Debug` as `SecretString`. Derive `Debug` when every sensitive field uses a redacting type; write a custom implementation only when a field cannot use one or the type requires more restrictive output. - Keep Python config projection thin. It converts types and delegates to the Rust config. Centralize default HTTP and WebSocket endpoint resolution so one environment selection cannot mix live and test endpoints. Keep explicit URL overrides only where custom gateways, mock servers, or venue deployments require them. Test every supported environment and any precedence between an environment choice and an explicit override. ### Credentials and secret handling When HTTP and WebSocket clients use the same key material, centralize credential handling in a type, commonly under `common/credential.rs`. Keep configs as data transfer objects: resolve credentials when constructing the credential, factory, or client, not in Python wrappers or individual request methods. #### Classify sensitive values Classify a value before choosing its type and diagnostic output. Apply the more restrictive rule when a venue gives one value more than one role. | Value class | Examples | Diagnostic output | | ---------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | Secret material | Passwords, private keys, API secrets, passphrases, bearer and session tokens, refresh tokens, and signatures. | Always show ``. | | Credential identity | API keys, client IDs, usernames, and account identifiers used during authentication. | Redact by default. Use a masked API key only when operational correlation requires it. | | Secret-bearing location | Proxy URLs, RPC URLs, request paths, and query parameters that can contain credentials. | Redact the complete location from logs and errors. | | Deliberately public identity | Wallet addresses, vault addresses, and public account names that the venue exposes publicly. | Show only when the type and adapter contract deliberately classify the value as public. | Do not infer that an API key, username, or URL is safe to print because it is not sufficient to authenticate by itself. Configs often cross logging, exception, and Python representation boundaries where partial credential identity remains sensitive. #### Use the common secret types Use `nautilus_core::string::secret` and `zeroize` instead of defining adapter-local redaction or zeroization conventions. | Mechanism | Use | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `SecretString` | Own a string that must zeroize on drop and render as `` with `Debug`. | | `REDACTED` | Replace an unconditional secret field in a custom `Debug` or `Display` implementation. | | `redact_option` | Preserve `Some` versus `None` while redacting an optional field in a custom `Debug`. | | `mask_api_key` | Correlate an API key only through an explicit masked-identity method. Do not use it for secret material. | | `Zeroizing` | Bound the lifetime of an owned plaintext `String`, byte buffer, decoded key, canonical payload, or serialized authentication message. | | `Zeroize` and `ZeroizeOnDrop` | Clear secret-bearing fields in structs that cannot use `SecretString`, including byte arrays and signing types. | | `zeroize_json_value` | Clear owned strings in a mutable JSON value after serializing secret-bearing fields. | #### Use `SecretString` safely - Treat serialization as plaintext. `SecretString` uses the underlying string for wire-format compatibility, so never serialize a config, credential, or authentication model for diagnostics. - Do not use ordinary `SecretString` equality to verify attacker-controlled secrets; it is not constant-time. - Borrow plaintext through `expose_secret()` only at the signing, encoding, or transport boundary that needs it. - Consume with `into_inner()` only to transfer ownership. If the receiving API requires `String`, create that final copy at the call boundary and do not retain it in adapter code. - Take `&SecretString` when a function only reads the value. Take it by value when the function retains or consumes it. - Put secret-bearing fields in the authenticated wire model instead of creating a second model only to change `Debug`. Derive `Serialize` and derive `Debug` when every sensitive field redacts. - Write a custom `Debug` for credentials backed by byte arrays, signing keys, or other types that cannot store their secret fields as `SecretString`. - Avoid `Display` for secret-bearing types unless a caller requires it. Any implementation must redact secret material. #### Resolve and share credentials - Define environment variable names once and select them from typed environment and product values. - Document the established environment variable names in the adapter's integration guide. - Register every adapter environment variable in `scripts/strip-adapter-env.bash`. `make pre-flight` runs through that wrapper with all of them unset, so an unregistered variable can let a test pass locally while depending on ambient credentials. - Resolve all fields as one credential set. Public clients may remain unauthenticated, but an authenticated client rejects an incomplete or invalid set before sending a request. - Convert config and environment strings into zeroizing owners at the credential boundary. Do not retain a non-zeroizing plaintext copy in adapter state after conversion. - Share credential storage across transports only when they use the same key material. Keep HTTP, WebSocket, and transaction signing methods separate when their canonical payloads differ. #### Project credentials into Python - Convert credential strings accepted by a Python constructor to `SecretString` at the Rust boundary. - Apply the Rust `Debug` and `Display` redaction rules to Python `__repr__` and `__str__`. - Expose only a presence check for secret material and secret-bearing locations. - Return credential identity, such as a username, only when an existing public API or another explicit caller needs it. Document the choice and keep the value out of diagnostics. - Never expose passwords, private keys, API secrets, passphrases, tokens, or signatures through plaintext getters. #### Bound plaintext lifetime - Zeroize each owned plaintext allocation after its final use, including normalized and decoded keys, secret-bearing signing payloads, serialized authentication messages, encoded form values, and mutable request models. - Prefer borrowed slices and existing zeroizing owners over intermediate `String` and `Vec` copies. - Limit the guarantee to allocations the adapter owns. Serialization libraries, transports, TLS, and the operating system may make copies the adapter cannot reach. - Keep plaintext lifetimes short; do not promise process-wide or transport-wide erasure. #### Redact diagnostics and transport errors - Never include credentials, signatures, secret material, or secret-bearing URLs in errors or logs at any level. - Log request metadata such as the method, field count, and byte lengths instead of credentials or authentication payloads. Shared transports log metadata rather than payload contents. - Treat TRACE as developer-facing diagnostic output. Raw inbound payloads are allowed when their schema cannot contain credential material. - Treat raw private-stream TRACE output as sensitive because it can disclose orders, balances, positions, and account identity. Redact it before sharing. - Prefer metadata or a sanitized, bounded excerpt when either can diagnose the protocol. - Clear mutable source models after serialization when they own another plaintext copy. - Never log a raw authentication request or response, or any frame whose schema can contain secret material. | Surface | Required handling | Zeroization boundary | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | HTTP secret body | Use `HttpClient::request_with_secret_body`. | The client retains the zeroizing owner; lower layers may copy it. | | HTTP path or `HashMap` query | Use `HttpClient::request_with_url_redacted`. | The URL is removed from logs and transport errors. | | HTTP typed query | Use `HttpClient::request_with_params_url_redacted`. | The URL is removed from logs and transport errors. | | HTTP headers and proxy | Create credential-bearing strings at the client boundary, avoid clones, and do not retain them in adapter state. | The shared client or transport may retain copies. | | WebSocket authentication | Keep fields and serialized frames in `SecretString`; create the final `String` immediately before `send_text`. | The shared client has no secret-owner-preserving send method. | | Unsupported combination | Extend the common client instead of implementing adapter-local URL or error scrubbing. | The common API must define the resulting ownership and redaction rule. | #### Verify credential handling Test the secret-handling contract as well as successful authentication: - Cover explicit values, environment fallback, incomplete credentials, and invalid credentials. - Assert that config, credential, request, response, and client `Debug` output omits the exact input secrets. Test `Display` separately for every secret-bearing type that implements it. - Assert that Python `__repr__` and `__str__` omit credential identity and secret material. Test presence checks and every deliberately exposed identity getter. - Assert that serialization and transport preserve the exact wire value where the venue requires plaintext. - Force transport failures for credential-bearing URLs and assert that both `Display` and `Debug` error output omit the URL, path secret, and query secret. - Use compile-time trait assertions for `Zeroize` or `ZeroizeOnDrop`, and test explicit clearing for mutable request and response models. - Keep deterministic signature vectors so redaction and zeroization changes cannot alter signing bytes, field order, or encoding. ### Symbols and instrument identity Separate venue symbols from Nautilus `InstrumentId` values. A symbol module commonly owns: - Parsing and formatting venue symbols. - Product or contract suffixes required for a unique Nautilus symbol. - Validation of venue and product identity. - Round-trip tests for supported forms and rejection tests for ambiguous forms. Choose the mapping from the venue's identity scheme: | Venue identity | Nautilus representation | Example | | --------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------- | | Native symbol distinguishes the product | Preserve the symbol and add the venue. | `BTC-USDT-SWAP` -> `BTC-USDT-SWAP.OKX`. | | Raw symbol is reused across product families | Add and validate a stable product suffix. | Bybit linear `BTCUSDT` -> `BTCUSDT-LINEAR.BYBIT`. | | Nautilus and the venue use different contract marks | Implement both directions at one boundary. | Binance USD-M `BTCUSDT` -> `BTCUSDT-PERP.BINANCE`. | | Transport casing differs from canonical identity | Convert only when building the transport value. | Binance stream `BTCUSDT-PERP.BINANCE` -> `btcusdt`. | The [`BybitSymbol`](../../crates/adapters/bybit/src/common/symbol.rs) wrapper and [Binance symbol conversions](../../crates/adapters/binance/src/common/symbol.rs) show the suffix and bidirectional conversion patterns. Treat them as examples, not a shared suffix scheme. Do not normalize distinct venue instruments to the same `InstrumentId`. Give test fixtures distinct symbols, precisions, currencies, and contract fields so swaps and omissions fail visibly. For every supported product family, test venue symbol -> `InstrumentId` -> venue symbol. Normalize case once at the identity boundary and preserve venue-significant case elsewhere. When the mapping requires a product marker, reject a missing or ambiguous marker before caching the instrument. Construct instruments from current venue definitions. Validate required identity and precision before caching or emission. Keep parsing functions deterministic and independent of live client state where practical. ### Modeling venue payloads Model the wire format, not an imagined stable subset: - Use typed request and response structs for known fields. - Use Serde aliases or custom deserializers only when supported payloads require them. - Reject unknown values for closed sets whose meaning affects domain behavior. - Preserve or explicitly classify unknown values for open venue sets that may expand without a protocol version change. - Keep raw models separate from Nautilus domain objects. Convert at one auditable boundary. - Pass required parsing context explicitly, including instrument precision, currencies, account identity, and `ts_init`. Keep live client state outside parsers. - Treat missing, null, and empty values according to the venue schema. Do not collapse them into one fallback when they carry different meanings. - Use the venue timestamp for `ts_event` when the payload supplies one. Assign `ts_init` from the adapter clock when it receives or constructs the event. Use receipt time as event time only when the venue has no authoritative timestamp, and cover that fallback with a test. Avoid permissive fallbacks that silently turn a new venue value into an existing semantic value. Stable error handling is part of the parser contract. #### Numeric precision Deserialize prices, quantities, money, fees, and other discrete values as `Decimal`. Construct domain values with `Price::from_decimal`, `Price::from_decimal_dp`, `Quantity::from_decimal`, `Quantity::from_decimal_dp`, `Money::from_decimal`, or `Money::zero`; never route wire values through `f64`. See [domain numeric types](rust.md#domain-numeric-types). Choose domain precision from the field contract, not incidental payload formatting: | Field contract | `"25.000"` result | Conversion | | -------------------------------------------------- | ----------------------- | ------------------------------------------------------------------- | | Venue-declared scale is meaningful | `25.000` at precision 3 | Use `Price::from_decimal` or `Quantity::from_decimal`. | | Documented trailing zeros are non-semantic padding | `25` at precision 0 | Call `Decimal::normalize`, then use the scale-inferred constructor. | | Instrument or currency precision governs the value | `25.00` at precision 2 | Use `Price::from_decimal_dp` or `Quantity::from_decimal_dp`. | Use instrument or currency precision for event and report values when available. A venue may send the same value as `"25"`, `"25.0"`, or `"25.000"`, so do not infer precision per payload unless the adapter defines and tests an explicit compatibility fallback. The declared-precision constructors apply banker's rounding when a value has excess non-zero digits; validate round-trip equality when the field contract requires exact representation. During reconciliation, follow [instrument resolution](#instrument-resolution-during-reconciliation) when precision metadata is missing. #### Venue enum fallbacks Venues extend wire enums without notice: new order states, order types, and category codes appear in production before clients update. Give each extensible venue enum a forward-compatible fallback variant (`Unknown` for venue states, `Other` for open value sets such as types and categories) with `#[serde(other)]`, so one new value cannot fail deserialization of the message carrying it. Closed sets the adapter defines stay strict. The fallback changes where strictness lives, not whether it exists: - Never panic on an unknown wire variant; the fallback keeps the connection and the sibling records in the same payload alive. - Never map an unknown variant onto an existing domain value. Make the domain mapping fallible (`TryFrom`) so the fallback variant is rejected explicitly at the mapping boundary. - Preserve safety-critical payload data even when a sibling classification is unmapped. A fill must still be parsed and emitted when its order state or order type is unknown, because fill fields carry their own prices, quantities, and fees. - Skip only the unmappable classification and log a warning with the venue identifiers (order ID, instrument) needed to investigate. When the message carries no data worth preserving, fail the record explicitly instead of inventing a status. Reconciliation heals the gap once the order reaches a mapped state; an unmapped value fails the same way on the reconciliation path, so treat the warning as the signal to add the mapping. #### Separate authority from projections Use separate response models when one endpoint returns both evidence that establishes permission or authorizes state mutation and data needed for a narrower read. | Boundary | Purpose | Validation | Meaning of success | | -------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | **Authoritative response** | Establish permission or authorize state mutation. | Requires all authoritative fields; rejects legacy conflicts and semantic duplicates before mapping. | The response can support the authority decision it models. | | **Narrow projection** | Read a balance, health value, or metadata without using authority. | Decodes returned fields only; its type cannot expose, grant, or infer omitted authority. | Only the projected value; omitted permission or account evidence is unknown. | Use the projection when malformed authority fields must not block the narrower read. Keep the authoritative model strict. ### Client traits and factories (`data.rs`, `execution.rs`, `factories.rs`) The shared [`DataClient`](../../crates/common/src/clients/data.rs), [`ExecutionClient`](../../crates/common/src/clients/execution.rs), and [client factory](../../crates/common/src/factories/client.rs) traits define the adapter boundary. Implement the supported methods and leave unsupported capabilities explicit in the integration guide. The client traits use `#[async_trait(?Send)]`. Client objects are not intended to move across threads and may hold non-`Send` Python state. Move owned, `Send` inputs into explicit runtime tasks when asynchronous work must outlive a synchronous trait call. #### Client naming and registration Name each client family symmetrically: `DataClient`, `DataClientConfig`, and `DataClientFactory` for data; `ExecutionClient`, `ExecutionClientConfig`, and `ExecutionClientFactory` for execution. Each factory consumes its corresponding client config directly. Do not add a separate factory config wrapper. The live node passes its `LiveNodeConfig.trader_id` to execution factories, while venue-specific values such as `account_id` belong on the execution client config. Within a Python module, order client-family `add_class` registrations alphabetically by exported type name so the data and execution families remain grouped. Do not prefix the ordinary client family with `Live`: a connected client is the default, while names such as `SandboxExecutionClient` and `DatabentoHistoricalClient` state alternate behavior. Retain `Live` only when it distinguishes explicit runtime or protocol siblings. Runtime types such as `LiveNode`, `LiveClock`, and the `Live*EngineConfig` family retain the qualifier. Do not shorten `Execution` in public, project-owned PascalCase type names. Internal implementation types may retain established `Exec` names. Also keep `Exec` where the [general naming convention](coding_standards.md#naming-conventions) allows it, including venue protocol terms such as `ExecType`. Name protocol-specific wire models after the venue concept, such as `HyperliquidExchangeAction`. Preserve established public names, and apply this convention to new APIs. #### Factory inputs and cache ownership Factories receive a downcast `ClientConfig` and a read-only [`CacheView`](../../crates/common/src/cache/mod.rs). Data factories also receive the shared clock. Use the view to resolve instruments and existing state. Engine cache writes stay in the engines: emit domain events and reports instead of mutating the engine cache from an adapter. A private protocol cache is valid when parsing, subscription replay, or response correlation needs it. ### Adapter-owned state Choose collections from ownership and update behavior: - Use a plain `AHashMap` or `AHashSet` for state owned by one task. - Use `AtomicMap` or `AtomicSet` for read-heavy immutable snapshots with infrequent writes. Use `rcu` when writers can race; a separate load and store can lose another writer's update. - Use `DashMap` or `DashSet` for independent keys that receive concurrent entry updates. Adapters use these patterns in different combinations. Keep the collection behind the component that owns its invariant instead of sharing it merely to avoid passing a message. Use `Ustr` for repeated protocol strings when interning reduces allocation or comparison cost; keep unique request IDs and short-lived payload text in their natural types. ### Connection lifecycle (`connect`) Treat each lifecycle method as a contract: | Method | Responsibility | Successful postcondition | | ------------ | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | `start` | Install local event plumbing and start client-owned background work. | Local event paths exist before any task can publish. | | `connect` | Establish transports, authenticate, load required definitions or account state, and start stream processing. | Public commands can use the 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`](../../crates/live/src/execution/emitter.rs), resolve the execution event sender with `try_get_exec_event_sender` and install it in the factory's `create`, before the client is returned. `LiveNodeBuilder` binds the runner's senders to thread-local storage before it calls any registered factory, so `create` runs with the sender available. `None` from `try_get_exec_event_sender` means the calling thread has no bound senders, which is expected in a factory unit test or in a host that binds later; it is not a construction failure. Install the sender in `start` as well, from `get_exec_event_sender`, and do so unconditionally: `LiveNode` rebinds the runner's senders on the calling thread before it starts clients, so the `start` install is the authoritative one, and the emitter shares one sender slot across its clones, so it reaches every clone taken during construction, including those handed to client-owned tasks. A host that calls a factory outside `LiveNodeBuilder` binds the runner's senders on the client's thread before `start`: the `start` install resolves through `get_exec_event_sender`, which reads only the thread-local slot and panics when nothing has bound it, so a sender passed through the host's own factory or client constructor - which reaches the emitter's shared slot via `set_sender` but not the thread-local slot - does not satisfy that lookup on its own. Constructor injection stands alone only for a client whose `start` accepts an already-installed sender instead of performing the unconditional lookup; the emitter-backed execution clients in this repository all perform it. #### Bootstrap ordering Connection code varies, but its dependencies do not. A data client typically: 1. Resolves the environment and validates any credentials needed during bootstrap. 1. Fetches required instrument definitions and populates parsing context. 1. Publishes definitions that the engine must observe before data arrives. 1. Starts the transport and waits for the command path to become active. 1. Subscribes or replays intent only after handler initialization. 1. Reports connected after the required engine and protocol state is ready. An execution client typically: 1. Validates credentials, account identity, and required instrument context. 1. Establishes and authenticates the private transport. 1. Starts stream processing and subscriptions in an order that cannot lose acknowledgements or account updates. 1. Fetches and emits initial account state, or waits for an authoritative stream snapshot. 1. Waits for required account and instrument state to become observable to the engine. 1. Reports connected only after commands and reconciliation can run. 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. ### Data client Subscriptions express ongoing intent. Requests ask the provider for current or historical data. Keep their freshness semantics distinct: - An explicit instrument request fetches from the provider unless the API contract explicitly permits a cache result. - Private caches can provide parsing context, but must not turn a new request into a stale response. - Emit a response only for data that satisfies the request identity and filters. - Preserve the request correlation ID and original parameters in response events. The shared [`DataEvent`](../../crates/common/src/messages/mod.rs) 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](../concepts/data/index.md#delta-flags-and-event-boundaries). 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 client Execution clients translate commands, preserve order identity, publish account state, and generate reports for reconciliation. They must support these boundaries consistently: - Validate deterministic local constraints before submission. - Emit `OrderSubmitted` only when the command enters the adapter's submission path. - Correlate venue responses and stream updates to the correct client and venue order IDs. - Emit balances and margins with the account type and base currency used by the factory. - Generate order, fill, position, and mass-status reports from venue state for reconciliation. - Release shared clock, cache, or account borrows before publishing account state because subscribers may access the same state synchronously. Keep deterministic adapter-specific checks in one `validate_order` function that returns `OrderDeniedReason`. Call it before emitting `OrderSubmitted` from single-order and order-list submission paths. Do not infer support from a venue API alone. Implement and test the Nautilus command and event semantics, then advertise the capability. #### Reconciliation reports Reconciliation reads venue state through five [`ExecutionClient`](../../crates/common/src/clients/execution.rs) report methods. They return reports rather than emitting order events, leaving the execution engine to decide what a difference between cached state and venue state means. | Method | Produces | Driven by | | ---------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | | `generate_order_status_report` | One optional [`OrderStatusReport`](../../crates/model/src/reports/order.rs). | A targeted probe for one order the open-order check left unresolved. | | `generate_order_status_reports` | [`OrderStatusReport`](../../crates/model/src/reports/order.rs) values. | Mass status and the periodic open-order check. | | `generate_fill_reports` | [`FillReport`](../../crates/model/src/reports/fill.rs) values. | Mass status. | | `generate_position_status_reports` | [`PositionStatusReport`](../../crates/model/src/reports/position.rs) values. | Mass status and the periodic position check. | | `generate_mass_status` | One optional [`ExecutionMassStatus`](../../crates/model/src/reports/mass_status.rs). | Startup reconciliation, once per execution client. | [Execution reconciliation](../concepts/execution/reconciliation.md) documents what the engine does with these reports, including the startup procedure, the runtime checks that drive the periodic and targeted requests, and their retry and throttling rules. Cases TC-E84 to TC-E87 and TC-E101 in the [execution testing specification](spec_exec_testing.md) exercise startup reconciliation against a venue. Cases TC-E88 and TC-E89 use deterministic fixtures to exercise REST and private-stream commission failure. ##### Startup mass status `generate_mass_status` runs once per execution client before trading starts. Its default implementation composes the three bulk methods concurrently from one `ts_init`, derives each command's `start` from `lookback_mins`, and requests full order history with `open_only=false`. Implementing the bulk methods is therefore enough for startup. Override the composition when the client declares a history bound, as described in [bounded mass-status reports](#bounded-mass-status-reports), or when it does not use the realtime clock. Returning `Ok(None)` logs a warning and leaves that client unreconciled, while an error fails startup. ##### Bulk report filters The bulk methods take a filter command carrying `instrument_id`, `start`, and `end`, plus `open_only` for order reports and `venue_order_id` for fill reports. Apply every filter the venue endpoint supports and complete the rest locally: - `open_only` separates the currently open orders a periodic check needs from the history a mass status needs. Retain a report for `open_only` when its status is open **or** in-flight, not open alone: a venue holding an order it has not yet acknowledged reports it as `SUBMITTED`, which is in-flight rather than open. - Apply `start` and `end` only to closed reports, since an order working at the venue is authoritative however long it has rested without an update. - Test a report for a terminal status with `is_closed()`, never `!is_open()`, which classifies `SUBMITTED` as terminal. Log report counts at the command's `log_receipt_level` so periodic checks stay at debug while mass status logs at info. When a periodic check request fails, the engine marks that client failed for the cycle and stops inferring absence for the orders and positions it covers. Returning an error is therefore safer than returning an empty set. ##### Single-order probes `generate_order_status_report` resolves a single order. The engine issues it after the open-order check retries without confirming a cached order, which requires that check to run in full-history mode (`open_check_open_only=false`). The command carries the queried `instrument_id` and `client_order_id`, plus `venue_order_id` when the order has one, so support a lookup that has no venue identifier yet. The engine discards a report whose identity does not match the query. Distinguish absence from failure in that probe, because the engine acts on the difference: - `Ok(None)` states that the venue answered and has no such order. The engine treats that as proof and resolves an accepted, submitted, or partially filled order to a terminal state, while pending cancel and update states stay unresolved. - An error states that the lookup did not answer, so the engine defers the missing-order resolution to a later cycle. A failed lookup returned as `Ok(None)` can therefore reject or cancel an order that is live at the venue. The trait default returns `Ok(None)` after logging that the handler is not implemented, so implement this method before an open-order check runs in full-history mode. #### Commission failure handling Commission is part of a fill's economic record. Calculate it with exact decimal arithmetic, then construct the venue currency's [`Money`](../../crates/model/src/types/money.rs) value. The shared `ExecutionClient::calculate_commission` hook distinguishes these outcomes: | Result | Meaning | | ---------------------- | ---------------------------------------------------------------------------------------- | | `Ok(Some(commission))` | The venue formula applies and produces a representable commission. Use that exact value. | | `Ok(None)` | The adapter has no venue override. The caller may use the generic commission formula. | | `Err(error)` | The venue formula applies but cannot produce a representable value. Fail closed. | Never replace `Err(error)` with zero commission or the generic formula. That substitution records a confirmed trade with economics the venue did not report. ##### REST report construction Commission construction belongs to the REST report request. If it fails for any required fill, return an error from the direct fill report request, targeted recovery, or complete mass status. Never drop the fill, return a partial mass status, or mark a bounded report set incomplete for this failure. Otherwise, an order or position report can cause the engine to infer the same quantity without its venue commission. During startup, the error prevents the node from starting and leaves that client's mass status unapplied. Periodic and targeted reconciliation defer the affected work until a later cycle. ##### Inferred fills Call the hook for every adapter-backed inferred fill: external and cached orders, continuous reconciliation, and targeted order recovery. If commission calculation fails, the engine may apply valid explicit fills, but it leaves the residual inferred quantity and dependent terminal transition pending. For an external order, calculate commission before a cache or event transition could prevent a retry. If the responsible execution client is unavailable, defer the inferred fill instead of treating the missing client as an `Ok(None)` response. Pass the same quantity, price, and liquidity side as the inferred-fill event. For a cached order with prior fills, calculate commission from the back-solved price of the unbooked incremental quantity, not the venue report's cumulative average price. A position-only synthetic correction has no underlying trade evidence and may leave commission unspecified. Do not present an aggregate or generic value as the exact commission for that unknown fill; this case is distinct from a failed venue calculation. ##### WebSocket trade processing Process each WebSocket trade atomically. Construct every owned maker and taker fill report before emitting any report, mutating fill trackers, or consuming the trade's deduplication key. Consume the key only after all reports route successfully. On failure, log the error and leave the trade unprocessed. Do not confirm or terminalize the affected orders or mark them permanently unreconcilable. A duplicate or reconnect replay can retry the trade. Scheduled REST reconciliation remains the authoritative recovery path; the WebSocket handler does not start an immediate REST request. #### Bounded mass-status reports When an execution client applies a lower time bound to historical reconciliation reports, record the contract with `ExecutionMassStatus::set_report_window(Some(lookback_start), reports_complete)`. Capture one cutoff for the mass-status request and use it for every historical order and fill query. A moving cutoff can omit records at different boundaries and produce a report set that never existed at the venue. Set `reports_complete=true` only when every source needed to interpret the bounded history completed and all required records were parsed, mapped, and linked to their orders. A failed required source, required row that cannot be parsed or mapped, or historical fill without its required order report makes the set incomplete. Preserve successful legs and authoritative active orders, but do not represent a failed historical query as a successful empty result. Commission construction is an exception to partial bounded history. Follow [commission failure handling](#commission-failure-handling) and fail the report request instead of returning a set that omits the affected fill. When positions come from a cached stream, absence proves flat only when a complete snapshot from the current connection epoch positively covers that instrument. Invalidate snapshot coverage on reconnect, and keep a row uncovered when it could not be parsed or mapped. Emit an explicit flat report for an absent touched instrument only after that coverage is established. Preserve stable venue order and trade identities across live dispatch and mass status. Include client order linkage and `venue_position_id` where the venue supplies them so the execution engine can distinguish a coherent lifecycle from ambiguous history. See [Bounded history safety](../concepts/execution/reconciliation.md#bounded-history-safety) for the engine's economic application rules. #### Instrument resolution during reconciliation Report generation resolves each record's instrument to parse venue payloads at the correct price and size precision. Resolve it from the instruments the adapter loaded during connect, and classify a miss by whether the record was in scope. Do not request an instrument from the venue while generating reports: - Per-record requests multiply the bulk queries that startup reconciliation already issues against the venue's rate limits. - Hidden requests make reconciliation timing and results irreproducible. - A failed request cannot be distinguished from an instrument the venue does not have. Load what the adapter needs during connect instead. An in-scope record whose instrument is missing is never dropped silently. A discarded open order report is indistinguishable from an order the venue never had, which leads the engine to resolve a live order as missing at the venue. Scope decides whether a miss is expected, so evaluate it before classifying the record: | Record | Outcome | Report set | | ---------------------------------------------- | --------------------------------------- | ---------------------------------- | | Out of scope for `load_ids` | Log at debug and drop | Unaffected | | In scope, open order or position status report | Return an error from the report request | Not returned | | In scope, closed or historical record | Log a warning naming the instrument | Incomplete when history is bounded | `InstrumentProviderConfig.load_ids` defines that scope. When it names an explicit set, records for instruments outside it are expected absences rather than errors, so a node scoped to one instrument neither fails nor warns because the venue returned records for the rest. Historical queries reach past the loaded instrument set routinely, because expiries retire instruments that earlier fills still reference. Failing a bounded-history query for one expired instrument would withhold every other record it returned, so record the incompleteness through `set_report_window` and let the engine apply its bounded-history rules. The engine acts on that incompleteness only for a mass status that declares `lookback_start`; an adapter that declares no bound follows the compatibility fill-adjustment path instead. `reconciliation_instrument_ids` filters reports after the execution engine receives them, so it cannot prevent a resolution failure inside an adapter. Keep the adapter's scope in its instrument provider configuration. #### Tracked and external execution updates Route execution updates according to order ownership, independent of the dispatch module layout: - For an order submitted and tracked by this client, emit typed order events through the normal order state machine. - For an untracked or external order, emit `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](../concepts/execution/reconciliation.md#external-order-creation). The adapter may use any state structure that proves this routing decision. Model tracked ownership with two conceptual layers: - **Order identity** contains the stable fields that associate an update with the submitted order: client order ID, strategy, instrument, side, and order type. - **Order context** combines that identity with the submitted order shape needed to construct later events without accessing the engine cache, such as quantity, price and trigger details, time in force, and execution flags. Keep venue order bindings, request correlation, cumulative fills, and replace state in adapter-owned context around that common surface. [`OrderIdentity` and `OrderContext`](../../crates/live/src/execution/context.rs) provide that surface. Start from them, and keep an adapter-local structure only where it proves the same routing decision. Register the order context before sending or spawning work that can produce an inbound update. Restore context for active local orders before processing their live updates, and retain it while the order can still produce owned updates. Do not evict active context merely to bound replay state. Make every execution update take one explicit route: | Route | Evidence | Result | | ---------- | ------------------------------------------------------ | -------------------------------------------------------------- | | Tracked | Active or pending context owns the order. | Emit, deduplicate, or safely defer typed order events. | | External | No tracked, pending, or terminal ownership exists. | Forward reports for reconciliation or external order creation. | | Suppressed | The update is proven duplicated, stale, or superseded. | Emit neither an event nor a report. | Missing tracked metadata, a parse failure, or an unresolved venue binding does not prove that an update is external. A tracked status with no corresponding Nautilus lifecycle event is a tracked no-op or deferred update unless the adapter documents and tests a report exception. ##### Triggered parent and child orders Some venues replace a tracked parent venue order with a child after a trigger. Treat both venue IDs as one order context: - Keep the client identity stable. - Bind the child atomically with the trigger transition. The child ID becomes authoritative for later events and commands. - If the child arrives during the binding race, use venue linkage to complete the tracked transition or defer the update. Do not route it as external. - Once the child is authoritative, suppress stale parent acceptance and any superseded parent update that would regress child authority. ##### Incomplete and late updates Keep these cases distinct from normal tracked events: - An aggregate parent status without the trade identity or other fields required for a typed event remains a tracked no-op or deferred update. The authoritative child produces the live event, while report-based recovery remains in the reconciliation path. - A fill with a new trade identity that arrives after the tracked lifecycle reached a terminal state remains a `FillReport`. The active order context is no longer available for a typed event, so reconciliation applies the late venue evidence. Suppress terminal status replays and fills whose trade identity was already processed. #### Event ordering and deduplication 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: - Use the venue trade or match ID for fills. Include account, instrument, or product identity when the venue does not guarantee global uniqueness. - Share fill identity across live dispatch and reconciliation when those paths can overlap. - Do not consume a deduplication key before parsing and routing succeeds. If the implementation reserves first, release the key after a failure so a replay can recover the event. - Bound long-lived deduplication state, but retain enough history across reconnects to cover venue replay. Reset it only when the protocol proves old identifiers cannot return. - Reuse the shared [`FifoCache` and `FifoCacheMap`](../../crates/common/src/cache/fifo.rs) when first-in, first-out eviction matches the replay contract. Keep adapter-specific locking where several state changes must remain atomic. - Make repeated acknowledgements and order snapshots idempotent. They must not regress state or emit a second lifecycle event. Keep active order context, pending correlation, replay deduplication, and terminal tombstones as separate lifecycle concepts even when one state object owns them. Bound replay and tombstone state without letting eviction reclassify an update for an active order as external. 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 parent and child updates can arrive on different streams: - Serialize the venue order ID binding with the lifecycle events that publish it. - Emit any required `OrderAccepted`, `OrderUpdated`, and `OrderTriggered` events before routing a child fill or cancellation. - Route commands from the same authoritative binding. Do not infer it from an engine cache that may still be processing those 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. Focused tests distinguish tracked and external updates, fills that precede acknowledgement, duplicates from overlapping sources, submission or venue-binding races, and stale post-terminal updates. Test active-context retention separately from bounded replay eviction. #### Order command outcome policy Use [Execution policies](../concepts/execution/policies.md) as the cross-adapter contract for command delivery, event application, persistence, and recovery. Separate three evidence classes: - **Definitive local failure:** local evidence proves that the command was never transmitted. Deterministic validation before a submit is one example. Emit `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. - **Definitive venue result:** a structured venue response or status explicitly accepts, updates, or rejects one command. Emit the matching domain event. - **Unknown outcome:** the request may have reached the venue, but no definitive result is available. Keep the command in flight for stream updates, polling, queries, or reconciliation. ```mermaid flowchart TD command[Submit order] --> valid{Deterministic local validation passes?} valid -->|No| denied[OrderDenied] valid -->|Yes| submitted[OrderSubmitted] submitted --> unsent{Local evidence proves no transmission?} unsent -->|Yes| rejected[Emit OrderRejected] unsent -->|No| evidence{Definitive venue evidence?} evidence -->|Accepted or updated| event[Apply the venue event] evidence -->|Explicit rejection| rejected evidence -->|No| unknown[Keep the outcome unknown] unknown --> recovery[Resolve from stream, query, polling, or reconciliation] recovery --> event recovery --> rejected ``` If a submit failure occurs after `OrderSubmitted`, emit `OrderRejected` when local evidence proves that the command was never transmitted. Otherwise, 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. #### Naming the evidence classes Use consistent names so command failure classifications can be compared across adapters and the same wire condition is classified consistently. Classify every state-changing order command failure as one [`CommandFailure`](../../crates/live/src/execution/failure.rs) variant: | Evidence class | `CommandFailure` variant | Terminal event from this evidence | | -------------------------- | ------------------------ | --------------------------------- | | Definitive local failure | `NotSent` | Valid | | Definitive venue rejection | `VenueRejected` | Valid | | Unknown outcome | `Ambiguous` | Never | An `Ambiguous` classification never emits a terminal event by itself. Later definitive evidence from a stream update, query, poll, or reconciliation still resolves the command either way. A definitive venue acceptance or update is not a failure and carries no variant. Apply the venue event directly. Classify once at the execution boundary, using the evidence preserved by lower layers, rather than re-branching on the error enum at each emit site. Apply this to every state-changing order command, submit, modify, and cancel alike, including their batch and list forms. A classifier scoped to one command type leaves the others to drift. Queries produce no terminal command event and need no classification. Keep this axis separate from `is_retryable`. Retryability answers whether to send the request again; ambiguity answers whether the venue may already have acted on the first attempt. An error can be both, either, or neither, and collapsing them is what makes an unknown outcome look like a rejection. Two conditions are easy to misfile: - An HTTP 5xx without definitive command evidence is ambiguous. It proves only that the command was not confirmed, never that it was not applied, and a gateway 5xx does not prove the request failed to reach the venue. - A response parse failure is ambiguous, while a request encoding failure is `NotSent`. Both may surface as one serialization error variant, so classify by which side of the write boundary the failure occurred on. #### Separate diagnostics from strategy-facing reasons Preserve a structured diagnostic error through classification and logging. Derive a strategy-facing reason only at the execution event boundary, after the outcome and retry decisions. | Representation | Consumers | Required content | | ---------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | Diagnostic error | Classification, retry control, logs, and operators | Typed source plus available status, venue code, endpoint, backoff, transport, and decode context. | | Strategy-facing reason | Rejection events consumed by strategies | Bounded venue meaning without HTTP prefixes, response envelopes, markup, control characters, or secrets. | Format standardized local denial messages from [`OrderDeniedReason`](../../crates/model/src/events/order/denied_reason.rs) with the minimum suffix needed to identify the diagnostic context: - Emit `CODE` when the denial needs no diagnostic suffix. - Use `CODE: value` for one typed value or a free-text diagnostic. The code already identifies a single value, so do not repeat its name. - Use `CODE: key=value, key=value` only when multiple typed values need disambiguation. - Use `CODE: value; free text` when one typed value precedes a free-text diagnostic. Only the leading code is canonical. Do not parse the diagnostic suffix to recover classification, retryability, or command outcome. Apply these rules at the boundary: - Classify from typed or structured evidence. Never recover status, retryability, or command outcome from formatted display text. - Extracting a clean reason must not erase the diagnostic error or the evidence used to classify it. - Prefer documented venue error fields and codes. Sanitize and bound raw fallback text before logging, interning, or emitting it, and use a stable fallback when no useful text remains. - Map equivalent venue evidence through the same adapter-owned classification and reason functions whether it arrives through HTTP, WebSocket, polling, or reconciliation. Set `OrderRejected.due_post_only` from a structured venue code or flag when the protocol provides one. Otherwise, use one narrow adapter-owned, source-backed message classifier across every venue path. Test exact positive cases and close non-matching messages. Do not introduce a cross-adapter venue classifier. ## HTTP client patterns ### Client structure A common design separates three responsibilities: | Layer | Accepts | Produces | Owns | | ---------------- | --------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------ | | Raw client | Venue request and query types. | Venue response models and transport errors. | Transport, authentication, rate limits, and exact wire encoding. | | Domain client | Nautilus identifiers and domain values. | Domain objects, reports, or acknowledgements. | Operation semantics, parsing context, caching, and domain mapping. | | Execution client | Nautilus execution commands. | Lifecycle events and execution reports. | Command lifecycle, failure evidence classification, and terminal event policy. | The execution client is the shared command outcome boundary. Raw and domain clients preserve enough adapter-specific evidence to distinguish a failure before transmission from one after the venue may have received the request. They keep their natural venue and domain return types; do not make them return `CommandFailure` only to standardize command handling. Share the `CommandFailure` evidence classes and terminal event policy across adapters. Keep venue error codes, response statuses, protocol semantics, and their mapping to evidence classes inside the adapter. Do not introduce a cross-adapter venue classifier or shared classification trait. Use one HTTP client layer when the protocol is small and a raw/domain split would only add forwarding methods. Split by product when endpoints, signatures, or response models change for different product families. The execution boundary remains the same in either structure. Name low-level methods after the venue operation when practical, such as `get_instruments` or `place_order`. Name domain methods after Nautilus semantics, such as `request_instruments`, `submit_order`, or `cancel_order`. #### Request flow Whether one client or two own these responsibilities, keep their boundaries explicit: 1. At a domain boundary, validate Nautilus inputs and build typed venue parameters. 1. At the transport boundary, select the HTTP method and path, then serialize the exact query or body. 1. Allocate any required request identity, timestamp, or nonce. Sign the exact wire representation when needed, then send it through the shared `nautilus_network::http::HttpClient` with the applicable rate-limit keys. 1. Decode the response envelope and preserve transport, HTTP status, venue, and parse failures. 1. At a domain boundary, convert successful payloads to domain types with explicit instrument, account, and time context. 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. ### Request signing and authentication For each attempt, build the exact canonical bytes required by the venue, then sign that representation once. Test: - Field order and delimiters. - Timestamp and receive-window units. - Decimal and enum encoding. - Body or query hashing. - Environment and account identifiers. - Known venue vectors when available. 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. ### Error handling and retry logic Map transport, HTTP status, venue error, parse, and validation failures without erasing their source. Retry only when the failure is classified as transient and the operation is safe to repeat. Compose both decisions at the client boundary; a predicate on the error alone is not a complete retry policy. This contract applies to HTTP and WebSocket request paths. #### Classify transient failures Keep transient failure classification adapter-owned because venue status codes, error codes, and rate-limit semantics differ. Give each adapter transport one production classifier entry point. Define rules shared by HTTP and WebSocket once within the adapter, then call them from those entry points. Remove superseded classifier paths. Do not introduce a cross-adapter venue classifier or shared trait. #### Gate retries by operation safety At each call site, bypass retry for an unsafe operation or pass a `should_retry` predicate that combines transient failure classification with operation safety. Reads and other idempotent operations may retry classified transient failures. Retry a state-changing operation only when repeating the same request cannot apply the command twice or cause another state change. The protocol may guarantee this through duplicate detection for a stable request identity or idempotent semantics for the same target. Otherwise, send the command once and resolve an unknown outcome through stream updates, queries, polling, or reconciliation. #### Preserve identity and ambiguity Allocate the semantic request identity outside the retry closure and keep it stable across attempts. Regenerate authentication timestamps, signatures, or other transport fields only when changing them does not alter the venue's request identity or duplicate detection. Ambiguity is monotonic across attempts for one semantic command. Once any attempt may have reached the venue, a later failure remains ambiguous. A later venue rejection resolves it only when documented semantics make the response authoritative for the same request identity and prove that no attempt was applied. An acceptance resolves ambiguity only when it correlates to the same semantic command. Validate a returned venue identifier for syntax and expected scope before constructing a domain identifier or binding it to a local order; a non-empty string alone is not proof. A malformed or mismatched identifier leaves the outcome ambiguous unless separate authoritative evidence proves rejection. Treat a venue duplicate-identity response as evidence that the venue saw an earlier request with that wire identity, not as a rejection of the original command by default. Use it to resolve the current command only when the adapter proves the same semantic identity. If the identity may collide or its scope is uncertain, keep the outcome ambiguous and reconcile it against venue state. #### Handle backoff and termination Respect venue backoff and rate-limit signals, and stop retries on cancellation. Use the shared [`RetryManager`](../../crates/network/src/retry.rs) when its cancellation and backoff model fits. `RetryManager` passes a typed [`RetryError`](../../crates/network/src/retry.rs) to the caller's error callback: `Canceled`, `OperationTimeout`, `ElapsedBudgetExceeded`, or `InvalidConfiguration`. Match a variant when the adapter must distinguish its control reason; never branch on display text. The error returned for `OperationTimeout` is evaluated by `should_retry`, so map it to the adapter's transient timeout variant when timeouts should retry. Other synthesized reasons terminate without reclassification. `InvalidConfiguration` is created before the operation can run, so it is a definitive local failure. Preserve that evidence instead of mapping it to a transport or ambiguous outcome. `RetryManager` control errors do not record whether the operation ran. Track possible transmission at the adapter boundary for state-changing commands, treating entry into the send operation as possible transmission unless more precise evidence exists. Classify cancellation, per-attempt timeout, and retry exhaustion as `CommandFailure::NotSent` only when local evidence proves that no attempt was transmitted; otherwise, classify them as `CommandFailure::Ambiguous`. Map every elapsed-budget termination path by transmission evidence rather than the returned error shape. When an error provides a minimum delay and the effective retry delay cannot fit within the remaining budget, `RetryManager` returns the original operation error instead of a synthesized budget error. #### Test retry behavior Focused tests distinguish: - Transient failures from permanent failures. - HTTP 429 responses with and without a venue backoff hint when the protocol exposes one. - An idempotent operation that retries and a state-changing operation that must not retry. - A final failure after a possibly transmitted earlier attempt, including a later venue rejection. - A duplicate-identity response for the same semantic command and a wire-identity collision with a different command. - Stable semantic request identity across attempts, including retries with refreshed authentication fields when the protocol permits them. - Cancellation, per-attempt timeout, and every elapsed-budget termination path before and after possible transmission. ### Rate limiting The shared [`HttpClient`](../../crates/network/src/http/client.rs) supports one or more rate limiters. Scope limiter state to the venue quota, not to a convenient Rust object: - Share a bucket across clients and operations that consume the same allowance. - Separate buckets only when the venue publishes independent quotas. - Acquire all required quota before sending a request. - Keep pagination and retry loops inside the same policy. 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 client patterns WebSocket dispatch follows the shared ownership and routing contract while module layout and state containers remain adapter-specific. Keep new code aligned with the shared network abstractions, bounded cache primitives, and nearest protocol peers. Do not treat one adapter's dispatch modules or a union of venue state as the target architecture. ### Client structure A common pattern separates an outer client from a handler task: - The outer client owns lifecycle, authentication coordination, subscription intent, and the stream exposed to data or execution clients. - The handler owns the `WebSocketClient`, serializes commands, decodes frames, and emits typed messages. - Channels transfer owned commands and messages across the boundary. 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. ```mermaid 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. #### Handler initialization handshake (`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. ### Authentication Use [`AuthTracker`](../../crates/network/src/websocket/auth.rs) when authentication state must be shared across the client, handler, and reconnect path. The adapter still owns protocol details: - Begin one authentication attempt and correlate the response. - Mark positive success or failure from a definitive venue frame. - Invalidate authentication on reconnectable connection loss. - Fail waiters on terminal shutdown. - Gate private replay and commands until authentication succeeds. 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. ### Subscription management Use [`SubscriptionState`](../../crates/network/src/websocket/subscription.rs) 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: 1. Invalidate connection and authentication state. 1. Re-establish the transport. 1. Authenticate when required. 1. Replay active and pending subscribe intent. 1. Confirm subscriptions from explicit acknowledgements or authoritative data. 1. Notify downstream consumers when they must reset protocol state. 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. ### Message routing Keep the routing boundary auditable: - Decode a raw frame once. - Handle transport control, authentication, and subscription acknowledgements before domain data. - Validate the channel and product identity before choosing a parser. - Convert a venue payload to one typed adapter message or a bounded sequence of messages. - Dispatch domain events outside mutable protocol state where practical. - Preserve enough identity to correlate execution responses and deduplicate overlapping sources. The handler owns transport control, authentication, subscription acknowledgements, frame decoding, and protocol correlation. The consuming data or execution client owns domain routing and emission. Parsing may remain in the handler when it depends on handler-owned protocol state, but tracked versus external execution ownership remains a client decision. When reporting malformed frames, log the parse error separately from a sanitized, bounded payload excerpt. Never log a raw authentication frame or a frame whose schema can contain secret material. Log a peer close code and reason at the transport layer that receives it; the adapter should not duplicate the shared transport log. Dispatch module layout, intermediate enum names, context registries, venue bindings, and state containers remain adapter-specific. Prefer the smallest design that makes protocol ownership and state transitions testable; extract another component only when multiple adapters share its semantics and atomicity. ### Reconnection and shutdown Reconnection must restore protocol state, not only the socket: - Recreate or replace command paths before reporting the client active. - Reauthenticate private sessions. - Restore subscription intent and required instrument context. - Reset sequence, snapshot, or gap state when the venue requires a fresh bootstrap. - Preserve in-flight execution state needed to correlate late responses or reconciliation. 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. #### Reconnect ownership A handler-mode client requests a reconnect through the shared client rather than a private reconnect loop. Its `request_reconnect` returns `true` only when the call moves an active client into reconnecting. Take the reconnect handle's `request_reconnect` when the adapter must distinguish the `ReconnectRequestOutcome` variants, since an already reconnecting, disconnecting, closed, or unsupported transport each warrant a different response. Stream-mode clients own their reconnect loop, and their handles report `Unsupported`. #### Shutdown 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. ### Backpressure 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. ## Task management Classify every production task by its owner before choosing its storage and shutdown path. | Ownership | Use | Required behavior | | ------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | Session-scoped | Stream consumers, keepalives, health polls, refresh loops, and reconnect drivers. | One session group owns the task from successful admission through disconnect or failed startup. | | Command-scoped | Work spawned by synchronous data requests or execution commands. | A separate command group owns the task without tying its outcome to the transport session. | | Explicitly singular | One task whose handle or typed result must remain in owning state for direct joining. | Store one named handle and apply the same bounded join, forced abort, and failure reporting rules. | | Handler-local | Retry futures, send workers, and child work created and joined inside one handler. | The handler drains the work before it exits and exposes failure to its owner. | | Protocol exception | Typed fan-out results, keyed timeouts, or work whose local join preserves protocol meaning. | Keep the exception local, state why a shared group would lose meaning, and test its shutdown path. | Use separate session and command groups even when both groups have the same timeout policy. A disconnect ends the session, while an accepted command can still need reconciliation or an explicit ambiguous outcome. Do not let transport shutdown silently reclassify that command result. ### Task storage and observation [`TaskHandles`](../../crates/common/src/live/task.rs) stores unit task handles without setting spawn, cancellation, generation, or join policy. Use it inside a component that defines those rules. [`TaskGroup`](../../crates/live/src/task.rs) supplies the shared live-client policy for unit-output session and command tasks. Use `TaskGroup::spawn_named` when client state must observe a grouped task's logical name, instance identity, or terminal state. Its `TaskRef` is read-only: the group remains the sole owner of cancellation and joining. Read-only observation does not make the task explicitly singular. `TaskRef::is_active` and `TaskRef::is_finished` expose the same one-way lifecycle state. Active means the task was admitted and has not reached a terminal state. A task may finish before `spawn_named` returns; neither state proves that the user future received its first poll. The same task module's `finish_task` function applies the bounded policy to an explicitly singular handle without erasing a typed result. For another explicit ownership pattern, spawn through `nautilus_common::live::get_runtime().spawn()` as described in [Async code](rust.md#async-code), then retain or locally await the returned handle. ### Spawn through a task group Synchronous client trait methods must not block an active Tokio runtime. Clone owned inputs, spawn the asynchronous operation, and return the local validation result. Register client-owned work through its `TaskGroup`. The group stores the handle before opening the task's start gate, so a concurrent shutdown either owns the task or rejects its admission. The [Tokio usage hook](../../.pre-commit-hooks/check_tokio_usage.sh) rejects `tokio::spawn` in adapter production code and requires fully qualified Tokio spawn, time, and sync paths. Keep the synchronous boundary small: ```rust fn spawn_request(&self, description: &'static str, future: F) where F: Future> + Send + 'static, { let future = async move { if let Err(e) = future.await { log::warn!("{description} failed: {e:?}"); } }; if let Err(e) = self.command_tasks.spawn(future) { log::warn!("Skipping {description} after shutdown began: {e}"); } } ``` 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. When a long-lived task creates children, capture a `TaskSpawner` from the owning group. Use `TaskSpawner::spawn_named` when those child tasks also need identity in shutdown failures. A spawner from an older generation cannot register work in the replacement generation. Give each task: - One owner responsible for joining or aborting it. - A stable description for failure logs. - A cancellation path. - Owned inputs that do not retain `RefCell` or engine borrows. Keep typed task results local to the component that awaits them. Do not erase a `JoinHandle` to fit a unit-output group when `T` carries order, transport, or startup evidence. ### Shut down and reopen task generations Task shutdown has separate synchronous and asynchronous phases: 1. `stop`, `reset`, and `dispose` call `begin_shutdown`. This closes admission and cancels the current generation without blocking the runtime. Use `abort` instead only when the existing synchronous contract requires immediate task cancellation. 1. `disconnect`, or the next asynchronous `connect`, closes the generation's transports in their required protocol order and calls `finish_shutdown` with bounded graceful and forced intervals. 1. `finish_shutdown` repeatedly drains tasks registered by an allowed race, reports unexpected cancellations and join failures, aborts unfinished work after the graceful interval, and awaits the forced abort within its second bound. 1. `start_generation` reopens the group only after the prior generation drains. A timeout retains the remaining handles and keeps admission closed. When `connect` fails after creating a transport or admitting a task, apply the same sequence before returning the startup error. Close every transport created by that attempt, drain both session and command work that cannot survive the failure, and include teardown failures in the returned or logged evidence. Make repeated `begin_shutdown` and `finish_shutdown` calls safe. A synchronous lifecycle method may begin teardown more than once before an asynchronous boundary finishes it. ### Never use `block_on` in trait methods Live 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. ### Graceful shutdown with `CancellationToken` Use `CancellationToken` when several tasks share a lifecycle. Select cancellation alongside streams, timers, or response channels. For grouped tasks, obtain child tokens from the generation group or spawner and let `begin_shutdown` cancel their parent. Obtain the replacement token only after `finish_shutdown` drains every handle and `start_generation` reopens the group. Reusing a canceled token makes replacement tasks exit immediately, while replacing it before the drain lets old work cross the reconnect boundary. ## Testing 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/integration/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. | ### Rust testing 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](../../.pre-commit-hooks/check_testing_conventions.sh) enforces these repository-wide rules. #### Fixtures and parser assertions 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: - One realistic fixture for every supported message or instrument family. - Boundary values for decimal precision, quantities, timestamps, IDs, and enum codes. - Unknown, missing, null, and malformed fields according to the venue contract. - Round trips or canonical bytes where the protocol defines them. - Stable errors for rejected input. 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 synchronization 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. Prefer a notification owned by the test or mock when the operation exposes one. Subscribe before reading the authoritative state, then recheck it after every notification so a transition between the read and the await cannot be missed. When no suitable signal exists, use [`wait_until_async`](../../crates/common/src/testing.rs). A short sleep is valid when the time window itself is under test, but it should not mask a missing synchronization point. ### Functional and integration testing 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: - Instrument identity, precision, and freshness. - Snapshot and incremental order-book boundaries. - Multiple symbols or product families sharing a connection. - Acknowledgement, rejection, unsubscribe, reconnect, and resubscribe behavior. - Malformed or unknown messages without loss of subsequent valid data. Execution tests cover each advertised command and report, plus: - Local denial before submission. - Definitive venue rejection. - Diagnostic context and the exact clean strategy-facing reason for each changed rejection path. - Unknown transport outcomes that remain reconcilable. - An ambiguous attempt followed by a definitive-looking response. - Missing, malformed, and mismatched returned venue identifiers. - Structured error fields and bounded raw-body fallbacks: empty, plain text, malformed structured data, markup, invalid UTF-8, and oversized input. - Equivalent HTTP, WebSocket, polling, and reconciliation evidence producing the same reason and classification. - Structured post-only evidence or an exact text classifier, including close non-matching messages. - Partial and per-order batch results. - Duplicate or out-of-order stream updates. - Account state, open orders, fills, positions, and startup reconciliation. - One fixed cutoff across bounded order and fill queries, including records on the boundary. - Complete and incomplete mass statuses for each independently failing report source. - Position snapshot coverage after reconnect, skipped rows, explicit flats, and absent instruments. - Exact order recovery without position or portfolio effects when bounded history is incomplete or ambiguous. - Idempotent stop, reset, and disposal. Keep adapter tests focused on adapter behavior. The [data testing specification](spec_data_testing.md) and [execution testing specification](spec_exec_testing.md) define the shared scenario catalogs and skip rules; link to them instead of copying partial lists into an adapter README. ### Acceptance testing Run acceptance tests only after deterministic tests pass. Use testnet or a controlled account and record: - Venue environment and product. - Supported and skipped specification cases. - Order types and flags exercised. - Reconnect or recovery cases exercised. - Venue restrictions, rate limits, and known gaps. 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: - Rust: `crates/adapters//examples/node_data_tester.rs` and `node_exec_tester.rs`, with product subdirectories when protocols split by product. - Python: `examples/live//data_tester.py` and `exec_tester.py`, using `LiveNode` and the Rust config and factory classes. Python tester scripts run out of the box: settings live in module-level constants at the top of the file, and running the script connects and starts immediately without CLI flags. Execution testers place real orders by default, so state this plainly in a warning at the top of the module and set `dry_run=False` explicitly in the `ExecTesterConfig` to advertise the dry-run option. Rust tester controls vary; inspect them before running. ### Python boundary testing For Python-exposed adapters, test the Rust module before testing broad Python workflows. Verify: - The module imports at the runtime path. - Stub metadata points to the public adapter package. - Config conversion preserves optional values and rejects unknown fields. - Factories downcast config and construct the correct Rust client. - Representative async client methods convert success and error results. 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` after changing exported Rust types or signatures. The [generated drift check](../../scripts/ci/check-generated-drift.bash) verifies that generator inputs and committed `.pyi` output agree. ## Performance and robustness 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. ### Canonical benchmarks 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](../../crates/adapters/lighter/benches/data.rs), [Derive data](../../crates/adapters/derive/benches/data.rs) | | `benches/exec.rs` | Order command through serialization and signing; where applicable, inbound execution payload through event dispatch. | [Lighter execution](../../crates/adapters/lighter/benches/exec.rs), [Derive execution](../../crates/adapters/derive/benches/exec.rs) | | `benches/micros.rs` | Decode-only, parse-only, and focused component costs that localize a regression found at a pipeline boundary. | [Lighter micros](../../crates/adapters/lighter/benches/micros.rs), [Derive micros](../../crates/adapters/derive/benches/micros.rs) | 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. Recorded Lighter signing numbers and the official Go comparison live in the [Lighter adapter benchmarks](../../crates/adapters/lighter/benches/BENCHMARKS.md). Follow the repository [benchmarking guide](../../BENCHMARKING.md) for tool choice, baselines, noise control, and result reporting. Use the [Criterion practitioner guide](benchmarking.md#writing-criterion-benchmarks) for benchmark structure and local commands. ### Fuzz testing Coverage-guided fuzzing adds assurance where arbitrary venue bytes or values cross a trust boundary. Prioritize: - Raw WebSocket or binary frame decoding. - Decimal, timestamp, symbol, and enum normalization. - Signing payload and canonical encoding. - Hashes and binary codecs. - Nonce or sequence allocation. - Other venue-specific parsers and encoders that accept untrusted input. 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: - Encode/decode round trips. - Canonical encoding and idempotence. - Length, precision, range, and allocation bounds. - Deterministic hashes and signatures. - Monotonic nonce or sequence models. - Agreement with an independent implementation. - Stable rejection for invalid input. 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`](../../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](../../.pre-commit-hooks/check_cargo_conventions.sh) enforces the shared feature path. Use the focused [Lighter fuzz README][lighter-fuzz] and [Derive fuzz README][derive-fuzz] for setup, corpus, artifact, and target commands instead of copying every invocation here. ## Documentation Create or update `docs/integrations/.md` with: - Supported products, environments, data types, order types, and reports. - Authentication and environment variables. - Config examples and factory registration. - Venue limits, reconciliation behavior, and known gaps. - Testnet or sandbox differences. - Links to venue protocol documentation used by the implementation. Keep capability claims testable and name legitimate exceptions. Link to shared [configuration](../concepts/configuration.md), [benchmarking](../../BENCHMARKING.md), and testing guides instead of copying their policy. Follow the repository [documentation guide](docs.md) and [Markdown style guide](markdown_style.md). Change generator inputs and regenerate generated output. ## Testing spec references Use these shared specifications to plan and report adapter conformance: - [Data client testing specification](spec_data_testing.md). - [Execution client testing specification](spec_exec_testing.md). [binance]: ../../crates/adapters/binance/src/lib.rs [bybit]: ../../crates/adapters/bybit/src/lib.rs [derive]: ../../crates/adapters/derive/src/lib.rs [derive-fuzz]: ../../crates/adapters/derive/fuzz/README.md [kraken]: ../../crates/adapters/kraken/src/lib.rs [lighter]: ../../crates/adapters/lighter/src/lib.rs [lighter-fuzz]: ../../crates/adapters/lighter/fuzz/README.md [okx]: ../../crates/adapters/okx/src/lib.rs # Benchmarking Source: https://nautilustrader.io/docs/latest/developer_guide/benchmarking/ Use this guide to write, run, and profile NautilusTrader benchmarks. It contains benchmark layout, examples, local commands, and the measurement procedure for published results. For benchmark scope, evidence requirements, and CI policy, see [`/BENCHMARKING.md`](../../BENCHMARKING.md) at the repository root. --- ## Tooling overview Select a tool based on the work and result: | Tool | What it measures | Use it for | | --------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------------- | | [Criterion](https://docs.rs/criterion/latest/criterion/) | Wall-clock time with confidence intervals | Operations above roughly 100 ns and elapsed time | | [iai](https://docs.rs/iai/latest/iai/) | Retired CPU instructions under Cachegrind | Small, deterministic operations and change detection | | [CodSpeed](https://codspeed.io/docs/instruments/cpu) | Simulated CPU cost and cache behavior | Stable pull request comparisons of deterministic CPU work | | [flamegraph](https://github.com/flamegraph-rs/flamegraph) | Sampled call-stack profile | Locating work inside a representative slow path | Criterion reports user-visible elapsed time. iai produces stable counts for the same binary, toolchain, and inputs without requiring host noise controls. Compare iai results only under the same code generation assumptions, and use Criterion when elapsed time is the required result. --- ## Directory layout Each crate keeps its benchmarks in a local `benches/` folder: ```text crates// └── benches/ ├── foo_criterion.rs └── foo_iai.rs ``` Register each benchmark explicitly in the crate's `Cargo.toml` so `cargo bench` discovers it: ```toml [[bench]] name = "foo_criterion" path = "benches/foo_criterion.rs" harness = false [[bench]] name = "foo_iai" path = "benches/foo_iai.rs" harness = false ``` To opt into the nightly CI performance workflow, register the benchmark and add its crate to `CI_BENCH_CRATES` in the workspace `Makefile` when the list does not already include it. Add a deterministic Criterion target to `CODSPEED_BENCH_TARGETS` when CPU simulation preserves what the benchmark intends to measure. Do not add iai, Criterion's `iter_custom` or `with_filter` APIs, OS-dependent work, or concurrent wall-clock benchmarks to the CodSpeed subset. --- ## Writing Criterion benchmarks 1. **Set up outside the timing loop.** All work that doesn't change between iterations belongs in the surrounding code or in `iter_batched_ref`'s setup closure, not in the body passed to `iter`. 2. **Wrap inputs in `black_box`** so the optimizer doesn't fold them away. 3. **Use `iter_batched_ref` for mutating benches.** It excludes input `Drop` from the timed region, which otherwise dominates the measurement on benches that own large structures. 4. **Add `Throughput::Elements(n)`** to size-parameterized groups so Criterion reports per-element throughput. 5. **Comment intent.** State what the benchmark is measuring (the hot path, the worst case, the cache-cold case) so a future reader understands what regressing it would mean. ```rust use std::hint::black_box; use criterion::{BatchSize, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; const SIZES: &[usize] = &[10, 100, 1_000]; fn bench_my_op(c: &mut Criterion) { let mut group = c.benchmark_group("module/my_op"); for &n in SIZES { group.throughput(Throughput::Elements(n as u64)); group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, &n| { b.iter_batched_ref( || populate(n), |state| state.run(black_box(n)), BatchSize::SmallInput, ); }); } group.finish(); } criterion_group!(benches, bench_my_op); criterion_main!(benches); ``` --- ## Writing iai benchmarks `iai` requires functions that take no parameters. Use it for small, pure operations so the measured instruction count stays focused on the intended work. ```rust use std::hint::black_box; fn bench_add() -> i64 { let a = black_box(123); let b = black_box(456); a + b } iai::main!(bench_add); ``` Allocations, randomness, and system calls add their own instructions to the result. Keep variable setup outside the measured function and compare counts produced by the same toolchain and target. --- ## Running benches locally | Goal | Command | | ---------------------------------- | --------------------------------------------------------------------- | | All benches in one crate | `cargo bench -p nautilus-execution` | | One core bench module | `cargo bench -p nautilus-execution --bench matching_core` | | One engine bench module | `cargo bench -p nautilus-execution --bench matching_engine` | | One core benchmark name pattern | `cargo bench -p nautilus-execution --bench matching_core -- iterate` | | One engine benchmark name pattern | `cargo bench -p nautilus-execution --bench matching_engine -- submit` | | Quick smoke run (low sample count) | `cargo bench ... -- --quick` | | All nightly registered benches | `make cargo-ci-benches` | | Build the CodSpeed subset | `make cargo-codspeed-build` | | Check the built CodSpeed subset | `make cargo-codspeed-run` | Criterion writes HTML reports to `target/criterion/`. Open `target/criterion/report/index.html`. The report includes per-bench violin plots, confidence intervals, and comparisons against the previous run's saved baseline. `make install-tools` installs the pinned `cargo-codspeed` version. A local CodSpeed run checks that the selected benchmark targets build and register, but it does not upload measurements. The `codspeed-benchmarks` job in `.github/workflows/performance.yml` measures and uploads the results. ### Canonical backtest workloads The canonical backtest cases use the first 10,000 rows of the checked-in `test_data/btc-perp-20211231-20220201_1m.csv` file. A shared fixture drives replay-only, scheduled market-order, passive limit-order, and bar-EMA scenarios. The correctness test and both timed paths use the same fixture loader and exact result fingerprints. Run the semantic check first: ```bash CARGO_BUILD_JOBS=16 cargo test --locked -p nautilus-backtest \ --test integration canonical_backtest_workloads:: ``` Then run Criterion in test mode to confirm that every affected benchmark case executes without collecting samples: ```bash CARGO_BUILD_JOBS=16 cargo bench --locked -p nautilus-backtest \ --bench engine -- canonical --test ``` The `run_preloaded` cases load the CSV and build the engine outside the returned `iter_custom` duration. The `load_build_run` cases include CSV loading, engine setup, data registration, and `BacktestEngine::run`. Both exclude result projection and fingerprint verification from the reported duration, while still checking the result after every measured iteration. See [`crates/backtest/benches/BENCHMARKS.md`](../../crates/backtest/benches/BENCHMARKS.md) for the published baseline, measurement record, and current profile target. The [v2 migration guide](../../MIGRATION_V2.md#compare-backtest-performance) contains the cross-version backtest comparison procedure. --- ## Measure Criterion for publication Use the `bench-lto` profile for Criterion results that will be reported or published. The profile inherits from `release`, preserves full debug symbols, enables fat LTO, and uses one code generation unit. The default `bench` profile keeps full debug symbols without LTO and is better suited to local iteration. 1. Quiesce the machine. On Linux, set the CPU governor to `performance` when you administer the host and can restore its prior state: ```bash sudo cpupower frequency-set -g performance ``` 1. On Linux, disable ASLR for the benchmark process and run the selected benchmark with `bench-lto`: ```bash setarch "$(uname -m)" -R cargo bench --profile bench-lto -p --bench ``` 1. Run multiple full sessions and report whether each case uses its best or median result. 1. Record the CPU model, kernel or operating system, Rust toolchain, and build profile with the results: ```text Hardware: , Toolchain: Profile: bench-lto (release + lto = "fat" + codegen-units = 1, debug = full) ``` For deeper analysis, control hyper-threading and dynamic frequency scaling in firmware. Published results must record those controls when they differ from the normal host state. iai runs under Cachegrind's virtual CPU model, so host quiescence, frequency scaling, and ASLR do not affect its instruction counts. Run iai without the Criterion noise controls. --- ## Generating a flamegraph `cargo-flamegraph` produces a sampled call-stack profile for one bench. Use it when a benchmark regresses and the responsible inner call is unclear. 1. Install once per machine: ```bash cargo install flamegraph ``` 2. Run a specific bench with the `bench` profile: ```bash cargo flamegraph --bench matching -p nautilus-common --profile bench ``` 3. Open `flamegraph.svg` in a browser and zoom into hot paths. ### Linux `perf` must be available. On Debian/Ubuntu: ```bash sudo apt install linux-tools-common linux-tools-$(uname -r) ``` If `perf_event_paranoid` blocks the run: ```bash sudo sh -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid' ``` A value of `1` is usually enough. Set it back to `2` (default) afterwards or persist via `/etc/sysctl.conf`. ### macOS `DTrace` requires root, so `cargo flamegraph` must be run with `sudo`. :::warning Running with `sudo` creates files in `target/` owned by root, causing permission errors with subsequent `cargo` commands. You may need to remove root-owned files manually or run `sudo cargo clean`. ::: ```bash sudo cargo flamegraph --bench matching -p nautilus-common --profile bench ``` The `bench` profile keeps full debug symbols, so flamegraphs render with readable function names without bloating production binaries (which still use `panic = "abort"` and are built via `[profile.release]`). > **Note** Benchmark binaries are compiled with the custom `[profile.bench]` > defined in the workspace `Cargo.toml`. That profile inherits from > `release` and sets `debug = "full"`, preserving full optimization *and* > debug symbols so tools like `cargo flamegraph` or `perf` produce > human-readable stack traces. --- ## Templates Starter files live in [`docs/dev_templates/`](../dev_templates/): - **Criterion**: [`criterion_template.rs`](../dev_templates/criterion_template.rs) - **iai**: [`iai_template.rs`](../dev_templates/iai_template.rs) Copy the template into the target crate's `benches/`, adjust imports and group names, register in `Cargo.toml`, and start measuring. # Callback Dispatch Contract Source: https://nautilustrader.io/docs/latest/developer_guide/callback_dispatch/ This page defines the ownership, ordering, and progress requirements for queued actor and strategy callbacks. The [design principles](design_principles.md#queued-callback-dispatch-requirements) explain the policy. :::info These requirements are design constraints for queued actor and strategy callback delivery, not guarantees of the existing synchronous dispatch paths. Support for synchronous message-bus reentry does not activate queued actor or strategy callbacks. ::: ## Implementation limits | Area | Implemented behavior | Limit | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Actor delivery | Private primitives support ordered, owned callback delivery. | **Queued actor and strategy delivery is inactive.** Existing synchronous paths do not gain these guarantees. | | [Root propagation](#synchronous-commands) | Retained work and synchronous data and trading commands preserve causal roots. | Live channels and locally emitted data and execution events do not have complete root propagation. | | [Drain safety](#draining-and-progress) | Explicit drains respect slot budgets and checked access; a busy head blocks later delivery. | Callers must end enclosing mutable borrows. Automatic safe drains and detection of a head that cannot progress require runtime integration. | | [Progress budgets](#callback-roots-and-budgets) | Completed callbacks consume a per-root delivery budget. | Commands do not consume that budget. Command-only loops and the duration of an individual callback are not bounded. | | [Memory accounting](#storage-limits) | Private limits cover retained units and known callback storage. | Command payloads, command-queue capacity, and the listed opaque storage are excluded. This is not a total-process memory cap; limits have no user configuration. | | [Failure handling](#failure-cleanup) | Contexts restore on unwind; retained roots block premature teardown. | Fatal callback errors halt the dispatcher across roots. Command-handler panics propagate and discard pending children and the unprocessed collected batch; completed effects are not rolled back. | | [Access and backends](#backend-compatibility) | Private allocation guards reject overlapping checked access. | Unchecked access and enclosing engine/cache borrows remain outside those guards. Native, Python, and dynamic-backend parity is not established. | | [Observable state](#maintenance-and-observable-state) | Event payloads describe their event; cache mutations and facade effects stay synchronous. | Callbacks observe current cache state, not an event-time snapshot. Queued delivery does not defer or undo facade effects. | ## Ordering and reentrancy :::tip What is reentrancy? Reentrancy occurs when code is entered again before an earlier call to that code returns. For example, a message-bus subscriber publishes another message, causing nested delivery before the original publication finishes. This can happen on one thread; it does not require parallel execution. ::: Within one runtime thread, canonical actor and strategy callbacks must preserve **publication order** across components and topics. The rule applies equally to idle and active components. A nested publication must not overtake an earlier publication's pending deliveries, including all recipients of the earlier publication. Independent nodes have no shared global ordering guarantee. Callbacks require **exclusive access** to their component and a delivery boundary at which enclosing mutable runtime borrows have ended. Native and Python components must follow the same ordering contract. Raw Python [topic messaging](../concepts/message_bus.md) delivers the original object synchronously to subscribed callables, including during nested publication. It shares the runtime bus and topic space with canonical custom-data subscriptions. The queued ordering requirement applies to canonical callbacks; raw callables remain synchronous. A nested publication from a raw callable must still preserve the publication order of pending canonical deliveries. ## Maintenance and observable state Component maintenance runs with each queued event, before that event's author callbacks. This includes indicator updates, timer cleanup, and contingent-order handling. Maintenance retains its applicable lifecycle rules when author callbacks are suppressed. Events emitted by maintenance enter the same ordered dispatch mechanism. Engine [cache](../concepts/architecture.md#cache) mutations and direct facade effects remain synchronous. Callbacks observe **current cache state**; ordered delivery does not provide an event-time cache snapshot. The immutable event payload records the event, while the cache may already reflect later changes. Keeping indicator updates with event delivery preserves their ordering relative to the corresponding callbacks. Author callbacks require eligibility at **both event arrival and delivery**. Stop, reset, or retirement must not carry old callbacks into a new registration or [lifecycle](../concepts/actors.md#lifecycle) generation. ## Bounded progress A **drain** processes queued callbacks in order, attempting delivery when permitted. Pending callbacks participate in the runtime's drain condition. Live execution uses bounded drain batches and yields between them. Backtests finish pending work before advancing simulated time. Runaway callback chains produce an explicit fault. Queue overflow records a fatal error and halts execution at a safe boundary. It must neither silently discard callbacks nor interrupt an operation midway through its synchronous effects. Already completed effects are not rolled back by callback dispatch. ## Private dispatch primitives The actor module contains private access, admission, publication, invocation, and storage primitives. The synchronous data and trading command queues preserve [callback roots](#callback-roots-and-budgets). Production actor lookups, component access, and message-bus routes do not use queued callback delivery. Activating that delivery requires explicit native and Python runtime boundaries; the primitives alone do not establish runtime ownership safety or native, direct, and dynamic callback parity. ### Publication and admission A publication scope reserves an **ordinal** (a sequence number) before synchronous subscribers run. Callback reservations sort by publication ordinal and then admission ordinal, so nested publications follow every pending recipient of their enclosing publication. Scopes nest and drop in stack order. Dropping a scope restores publication state without delivering callbacks. A publication unwind latches a fatal error. The message bus does not install these scopes automatically. Admission reserves count, known storage, and a queue slot **before the caller constructs owned captures**. A successful reservation accepts its capture even if another operation subsequently latches a failure. An unfinished reservation blocks later deliveries and teardown; cancellation leaves a slot that an explicit drain or teardown releases. Actor reservations retain registration identity, so replacement or re-registration of the same allocation cancels stale delivery. ### Draining and progress A drain processes at most its supplied slot budget, including cancelled slots. The queue has a **busy head** when its first callback cannot acquire exclusive access to its component. This blocks later delivery. Drains do no work during publication, recursive draining, teardown, or checked allocation access. :::warning Guard destruction only releases access; it does not drain callbacks. The caller must also end enclosing engine, cache, and other untracked borrows before draining. ::: ### Callback roots and budgets A **root** owns the delivery budget for an incoming publication or reservation and all callback work it causes. Publication order does not depend on which root owns the work. - Nested publications and reservations inherit the active root. - A publication with no active root starts one on its first callback admission or command send. An empty scope allocates none. - Outside a publication scope, each admission starts a separate root unless a root is already active. Each completed delivery counts once against its root's budget. Busy attempts and cancelled slots do not count, though cancelled slots still consume the drain's slot budget. At the chain limit, dispatch latches a fatal runaway error before running another ready callback. The blocked callback stays queued for explicit teardown. **A fatal error halts the whole dispatcher**, even though roots have separate budgets. ### Retained work and cleanup Queued slots, active scopes, and retained invocation work keep their root alive. **An empty queue does not reset a surviving root's budget.** Retained storage captures the active root, or starts one when none is active. Growing that storage preserves its root. Resuming retained work through `with_chain` makes its root active for the call. Return or unwind restores the enclosing root. Invocation batches use this mechanism for each retained value; resuming work does not itself count as a delivery. A delivered capture's destructor runs with the delivery's root active, including during unwind. Uninvoked batch values also restore their root when destroyed. The root and its storage charge release when the last owner drops. Cancellation, failed batches, and explicit teardown use the same ownership cleanup as other retained storage. Retained work prevents teardown until released. ### Synchronous commands Commands capture any root active when they are sent, including during callback delivery and retained-work resumption. A send inside a publication or command-processing scope starts that scope's root if needed. A send with no active root outside those scopes carries no root; processing then creates an independent root when it first admits callback work or sends a nested command. Commands in the same drain batch do not share a root merely because they run together. Processing and destruction restore the enclosing root on return or unwind. A command drain processes the batch collected at entry, in order. Commands enqueued through a synchronous sender by its handlers remain queued for a subsequent drain. Trading handlers can also capture deferred children: these run depth-first, in capture order, before the next command in the collected batch. Each child captures the root active at capture time, including a temporary nested context, rather than using the parent's context after its handler returns. Direct endpoint routing stays unchanged. Queue and dispatch-frame borrows end before handlers run or abandoned commands are destroyed. Command processing does not count as callback delivery and does not automatically drain callbacks. If a handler panics, the panic propagates. Unprocessed commands in the collected batch and pending deferred children are destroyed under their own contexts; newly enqueued commands remain queued. Captured roots keep callback accounting alive and prevent explicit dispatcher teardown until those commands release them. Abandoned commands also release their captured ownership at thread teardown. Callback storage limits do not reject command sends. If a send needs a root and its allocation exceeds those limits, it latches callback overflow while the command is still queued. Command entries do not consume the callback retained-unit limit, and command payloads and queue capacity are excluded from known callback storage. Each root allocation is charged once. **This does not bound command-queue memory or loops that generate only commands.** ### Runtime integration The [root propagation tests](../../crates/common/src/actor/dispatch.rs) cover retained continuations and synchronous data and trading commands, including deferred trading children. Before queued callback activation, runtime integration must: - Extend root propagation to live channels and locally emitted data and execution events. - Preserve independent ingress boundaries when reusing long-lived storage, so unrelated events do not accumulate against one root's budget. - Provide safe drain boundaries. - Detect a busy head that cannot make progress. Public trading messages and their direct `dispatch()` path carry no callback context across threads. The synchronous queue owns its contexts privately; live-channel propagation remains outside this scope. These primitives do not establish runtime integration or backend parity. ### Storage limits The private limits are: - **Retained units**: At most 65,536. Invocation captures and batch-capacity reservations share the count with queued callbacks, so the count can conservatively exceed the number of callbacks. - **Known storage**: At most 64 MiB. - **Callback chain**: At most 1,048,576 completed deliveries per root. These limits are internal and expose no user configuration. Known storage includes callback values, queue slots and visible queue capacity, chain allocations, invocation vector capacity, and the caller-supplied heap charge. Payload measurements include accessible string and vector capacities, recursive JSON contents, metadata entries, order-event collections, visible book entries, option-chain entries, and, with the `defi` feature, directly owned blockchain strings. Shared payload storage may be charged more than once. Data-type names, topics, and identifiers expose string slices, so their charges cover lengths and exclude inaccessible spare capacity. Charges remain held while delivery is in flight and while batches retain captures. :::warning This accounting is not a bound on total process memory. It excludes allocator bookkeeping, collection internals whose capacity is not exposed by these measurements, intern-pool storage, registered actor allocations, opaque custom-data payloads, Python object graphs, and dynamic-module storage. ::: Callers must supply the known heap charge for their capture type; the generic reservation cannot inspect arbitrary owned fields. ### Failure cleanup Overflow latches the first fatal failure and rejects further reservations without constructing captures. Synchronous facade effects can finish before the runtime reports the failure at a safe boundary. Queued slots remain owned for explicit teardown. Failed invocation batches release their captures after preparation exits and its guards release. Teardown must follow reporting of the fatal error and must run after active publication, reservation, invocation, chain, drain, and access scopes end. It detaches storage before releasing captures and rejects destructor-driven admission while clearing. Invocation preparation keeps the batch owner outside the call that acquires guards. On rejection or unwind, previously transferred captures survive until those guards release. The caller reserves before constructing each capture and acquires preparation guards inside that call. The primitives cannot control arbitrary locals that author code constructs or explicitly destroys while holding a borrow. Destructors must not panic during an existing unwind. ### Backend compatibility Direct and dynamic backends must preserve the same callback ordering, exclusive component access, and lifecycle eligibility requirements. Facade effects remain synchronous. The [plug-in boundary rules](plugins.md#boundary-rules) apply to all values crossing a dynamic-library boundary; callback dispatch does not implicitly transfer allocation ownership across that boundary. Admission tickets and cleanup scopes remain framework machinery; author fields, callback signatures, and canonical facade APIs do not expose them. # Coding Standards Source: https://nautilustrader.io/docs/latest/developer_guide/coding_standards/ ## Code Style The current codebase can be used as a guide for formatting conventions. Additional guidelines are provided below. ### Universal formatting rules The following applies to **all** source files (Rust, Python, shell, etc.): - Use **spaces only**, never hard tab characters. - Lines should generally stay below **100 characters**; wrap thoughtfully when necessary. - Use American English spelling (`color`, `serialize`, `behavior`), enforced by `.typos.toml`. Preserve external API spellings through exact identifier exceptions; exclude verbatim data and generated files. Run `prek run typos --all-files` to check the repository. ### Shell scripts Bash is the default shell for repository scripts. Use POSIX `sh` only when a caller cannot rely on Bash being installed. See [Shell](shell.md) for script selection, extensions, portability, structure, testing, formatting, and linting requirements. ### Comment conventions 1. Generally leave **one blank line above** every comment block or docstring so it is visually separated from code. 2. Use *sentence case* - capitalize the first letter, keep the rest lowercase unless proper nouns or acronyms. 3. Do not use double spaces after periods. 4. **Single-line comments** *must not* end with a period *unless* the line ends with a URL or inline Markdown link - in those cases leave the punctuation exactly as the link requires. 5. **Multi-line comments** should separate sentences with commas (not period-per-line). The final line *should* end with a period. 6. Keep comments concise; favor clarity and only explain the non-obvious - *less is more*. 7. Avoid emoji symbols in text. ### Doc comment mood **Rust** doc comments should be written in the **indicative mood** - e.g. *"Returns a cached client."* This convention aligns with the prevailing style of the Rust ecosystem and makes generated documentation feel natural to end-users. ### Terminology and phrasing 1. **Error messages**: Avoid using ", got" in error messages. Use more descriptive alternatives like ", was", ", received", or ", found" depending on context. - Bad: `"Expected string, got {type(value)}"` - Good: `"Expected string, was {type(value)}"` 2. **Spelling**: Use "hardcoded" (single word) rather than "hard-coded" or "hard coded" - this is the more modern and accepted spelling. 3. **Error variable naming**: Use single-letter `e` for caught errors/exceptions: - Rust: `Err(e)` not `Err(err)` or `Err(error)`, and `|e|` not `|err|` in closures - Python: `except SomeError as e:` not `as err:` or `as error:` ### Naming conventions 1. **Internal fields**: Abbreviations are acceptable for private/internal fields (e.g., `_price_prec`, `_size_prec`) to keep hot-path code concise. 2. **User-facing API**: Use full, descriptive names for public properties, function parameters, return types, and metric names/labels (e.g., `price_precision`, `size_precision`). This prevents abbreviated terminology from leaking into dashboards or alerts. 3. **Error messages and logs**: Use full words for clarity (e.g., "price precision" not "price prec"). The user should never see abbreviated terminology. 4. **Execution terminology**: Use `Execution` in public, project-owned PascalCase type names, such as `BinanceExecutionClientConfig`. Internal implementation types may retain established `Exec` names. Also reserve `Exec` for the `ExecAlgorithmId` and `ExecTester` families, established `exec_*` names, and venue or protocol terms such as `BitmexExecType`. Name protocol-specific wire models after the venue concept, such as `HyperliquidExchangeAction`. Preserve established public names, historical release entries, and source names in migration tables. 5. **Runtime qualifiers**: Use `Live` when a type selects or configures real-time runtime semantics, such as `LiveNode` versus `BacktestNode`, `LiveClock` versus `TestClock`, and the `LiveDataEngineConfig`, `LiveRiskEngineConfig`, and `LiveExecutionEngineConfig` family versus reusable core engine configs. Omit `Live` from the ordinary adapter client family because a connected client is the default. Qualify alternate implementations by their behavior, such as `SandboxExecutionClient` or `DatabentoHistoricalClient`. An explicit live/historical protocol pair may retain `Live` to distinguish the two implementations. 6. **Adapter factory configs**: Name the data and execution inputs `DataClientConfig` and `ExecutionClientConfig`. Factories consume these client configs directly rather than a separate factory config wrapper. `LiveNodeConfig` owns `trader_id`; venue-specific `account_id` values belong on execution client configs. #### Data loading APIs Use free functions for stateless data ingestion. Use a class only when instances retain reusable configuration, caches, workers, iteration state, or an open resource across calls. Do not use a zero-state class solely to group static or class methods. Follow the semantic distinction in the [Polars I/O API](https://docs.pola.rs/api/python/stable/reference/io.html), adapted to the established Nautilus `load_*` vocabulary: - `load__` eagerly reads, normalizes, and materializes a complete result. - `scan__` creates a lazy query or deferred execution plan. - `stream__` incrementally yields records or batches. - `write_` eagerly writes an in-memory result. - `sink_` writes through a lazy or streaming execution path. For ingestion, order names from general to specific: verb, source, logical data, then an optional representation. Include the representation only when real sibling formats exist. For example, `load_binance_order_book_deltas` is preferable to a stateless `BinanceOrderBookDeltaDataLoader.load` class or a generic `load_binance_data` function. #### Adapter package facades Each package under `python/nautilus_trader/adapters/` is a thin facade over the private `_libnautilus` extension. Every adapter `__init__.py` declares a deterministic `__all__` that is the single source of truth for its public API; `python/generate_stubs.py` copies that list into the matching `.pyi` so runtime and stub exports agree exactly. A venue adapter exposes its canonical identity constants plus the supported public surface: - ``, `_CLIENT_ID`, `_VENUE`, registered from Rust via `m.add` in the adapter's `python/mod.rs` - data types, `*Config`, `*Factory`, user-facing enums (such as `*Environment` and `*ProductType`) - stateless loaders (`load_*`, `stream_*`, `convert_*`) and intentional utilities (`decode_*`, `get_*_arrow_schema_map`) Keep the facade thin. Never add raw HTTP or WebSocket clients, wire models, endpoint URL resolvers (`get_*_url`, `*_HTTP_URL`), caches, or other internals to `__all__` merely for structural parity. Data providers (such as `databento` and `tardis`), the `blockchain` data client, the `sandbox` execution client, and the multi-venue `interactive_brokers` broker omit venue constants because the constants would be meaningless for them. Order `__all__` entries so the `RUF022` pre-commit gate owns sort order; do not hand-order the list. ### Formatting 1. For longer lines of code, and when passing more than a couple of arguments, you should take a new line which aligns at the next logical indent (rather than attempting a hanging 'vanity' alignment off an opening parenthesis). This practice conserves space to the right, keeps important code more central in view, and survives function/method name changes. 2. The closing parenthesis should be located on a new line, aligned at the logical indent. 3. Multiple hanging parameters or arguments should end with a trailing comma: ```python long_method_with_many_params( some_arg1, some_arg2, some_arg3, # <-- trailing comma ) ``` ## Commit messages Commit messages use a capitalized, imperative subject naming the affected surface, optionally followed by a body explaining the change. ### Subject line - Open with a capitalized imperative verb, so the subject describes what the commit does when applied. `Add`, `Fix`, `Improve`, `Refine`, `Update`, `Remove`, `Refactor`, and `Standardize` cover most of the history. - Name the affected surface (crate, adapter, subsystem, or type) so the log stays scannable. - Keep the subject at 10 characters or more so it can name the affected surface clearly. - Aim for 60 characters or fewer for clear GitHub rendering and concise text. The commit-message hook warns without failing when the subject exceeds this target. The project plans to enforce this limit in the future. - Do not end the subject with a period. - Do not put an issue or pull request number in the subject. GitHub appends the pull request number on squash merge, and any other reference belongs in the body. The commit-message hook rejects a subject containing `#` in any position. ```text Add Decimal constructors to Instrument trait Fix non-atomic order event application Refine cross-platform wheel validation Remove stale security audit exceptions ``` Avoid these shapes: ```text feat(bybit): add due_post_only flag # Conventional Commits type and scope fix: bug # lowercase, unspecific, too short Fixed the Bybit post-only rejection flag. # past tense, trailing period Update stuff # says nothing about the surface Fix the post-only flag (#4544) # pull request number added by hand Fix PR #4544 review feedback # issue or pull request number in the subject ``` ### Conventional Commits Do NOT use [Conventional Commits](https://www.conventionalcommits.org/) syntax for commit messages or pull request titles. Many editors and AI assistants emit that format by default, but no commit in this repository's history uses it, and the type and scope ceremony duplicates what the subject already carries. Pull request titles matter here too, because a squash merge turns the PR title into the commit subject. ### Body The body is optional, but anything beyond a trivial change should say why the change was made rather than restate the diff. - Separate the body from the subject with a blank line. - Keep body lines to 79 characters or fewer to align with PEP 8 and traditional Git tooling. - Use prose paragraphs or bullet points, whichever suits the change. Bullets may keep the same imperative voice as the subject, and do not need terminating periods. - Include informative hyperlinks where they help a future reader. ### Issue references - Reference issues from the body, typically on a final line: `Resolves #4534` when the commit closes the issue, or `Related to #4547` when it is partial work. - GitHub appends the pull request number to the subject on squash merge, producing subjects such as `Fix TWAP child-order sizing and interval validation (#4544)`. Do not add that suffix by hand, and do not reference a pull request or issue anywhere else in the subject either. The subject has no room for detail the body carries better, and a hand-written number duplicates or contradicts the appended one. - Aim to keep the pull request title short enough for the appended suffix to leave the squash-merged subject at 60 characters or fewer. # Design Principles Source: https://nautilustrader.io/docs/latest/developer_guide/design_principles/ This page defines the principles, policies, and trade-offs that guide NautilusTrader design. [Architecture](../concepts/architecture.md) describes the components and runtime structure. These policies guide implementation and review; they do not establish that every existing path already conforms. Specific guides describe current behavior and limits. [Runtime conformance contract](runtime_conformance.md) maps selected requirements to source, representative checks, and implementation limits. ## Design priorities Design decisions weigh these quality attributes in roughly this order: - Reliability - Performance - Modularity - Testability - Maintainability - Deployability Performance improvements must preserve critical invariants and their required verification. Testability and maintainability sustain reliability; deployability affects safe rollout, configuration, and recovery. The priority order does not make these qualities optional. ## Data integrity and failure NautilusTrader prioritizes data integrity over availability for trading operations. Arithmetic and data-handling boundaries return errors or panic rather than silently accepting invalid values that could affect trading decisions. ### Invalid operations The system fails fast, either by returning an error or panicking according to the API contract, for: - Arithmetic overflow or underflow in operations on timestamps, prices, or quantities that exceed valid ranges. - Invalid data during deserialization, including NaN, infinity, or out-of-range values in market data or configuration. - Type conversion failures such as negative values where only positive values are valid (timestamps, quantities). - Malformed input parsing for prices, timestamps, or precision values. In a trading system, one incorrect price, timestamp, or quantity can propagate into: - Incorrect position sizing or risk calculations. - Orders placed at incorrect prices. - Backtests producing misleading results. - Silent financial losses. Failing at the invalid operation provides: - **No silent corruption**: Checked inputs fail before the invalid value propagates. - **Immediate feedback**: The caller receives an error, or the process terminates, at the point of the violated contract. - **Diagnostic context**: Errors and panic messages identify the rejected operation or value. - **Deterministic behavior**: With deterministic ordering and configuration, the same invalid input produces the same failure; nondeterministic inputs can still vary the outcome. Expected network, storage, business-validation, and user-input failures have explicit error surfaces. Unrecoverable invariant violations stop the operation or process before invalid state propagates. The [Rust error contracts](rust.md#failure-contract-examples) define panic and fallible API behavior. ### Failure containment Stop the smallest scope whose integrity can no longer be established, provided the remaining system can continue safely. Rejected input or a failed operation need not stop unrelated components when isolation is established. Untrustworthy shared state may require stopping the node. Containment must follow the API's failure contract and proven isolation boundaries; it must not assume a component can recover from an arbitrary panic. Stopping a process does not cancel working venue orders or remove exposure. Recovery must establish venue state before deciding which further actions are safe. ## Executable invariants NautilusTrader incrementally applies high-assurance practices to critical paths. Executable invariants verify that behavior matches the business requirements: - Identify high-impact components, including core domain types and risk and execution flows, and state their invariants in plain language. - Codify those invariants as executable checks (unit tests, property tests, fuzzers, and static assertions) that run in CI. - Enforce ownership and state invariants through types and explicit failure contracts. Add formal tools where their assurance benefit justifies their cost. - Require integrations to preserve existing critical-path invariants, and add executable coverage for invariants they introduce or alter. This approach gives high-stakes flows additional scrutiny without applying the same assurance cost to every path. Further reading: [High Assurance Rust](https://highassurance.rs/). ## Message immutability Messages (requests, responses, events, and commands) are immutable after creation. Their fields remain unchanged for the rest of the message lifetime. See [Message Bus: message integrity](../concepts/message_bus.md#message-integrity) for the ownership rules that follow from this invariant. The invariant protects several properties the system depends on: - **Stable inputs**: Every consumer sees the same message payload. Replaying a sequence preserves the original logical inputs for backtesting, incident reconstruction, and regression testing. - **Temporal integrity**: A message preserves what its producer reported, observed, requested, or inferred at creation time. Preserve the available provenance; immutability does not establish that the producer's information is true. Corrections require an explicit new record. - **Safer concurrency**: Readers do not need coordination to protect message payloads from later rewrites. This removes a common source of races around shared state. - **Debugging and auditability**: Logs, traces, replay tools, and dead-letter inspection retain the original payload, allowing investigation of what the system received or created, when, and what it did with that information. - **Clear ownership boundaries**: Components treat incoming messages as input. If a component needs a different representation, it derives new local state or a new message explicitly. - **More robust distribution**: Serialized messages already cross process and service boundaries as copies. The same ownership rule keeps the in-memory model aligned with that reality. ## Evidence and authority Distinguish external reports, local observations, inferences, and policy decisions. Preserve their source and uncertainty when deriving state. A valid numeric value or state transition does not by itself establish a venue fact. Missing evidence must not become a confirmed outcome merely because a timeout or retry limit expires. The [execution policies](../concepts/execution/policies.md) apply this rule to command outcomes, reconciliation, and the limits of retained history. ## Controlled nondeterminism Make time, randomness, input ordering, and external effects explicit at the boundaries that consume them. Reproducible tests must control the sources that affect their assertions and retain the inputs, seeds, and configuration needed to investigate a failure. State the binary, platform, and input conditions of any determinism guarantee. The [DST contract](../concepts/dst.md) defines the supported scope of seed-controlled execution. Live venue behavior and independent external inputs remain outside that guarantee. ## Bounded resource use Design queues, retries, retained history, and callback chains with explicit resource budgets and exhaustion behavior. Account for payload size as well as item count where memory use varies, and bound work as well as storage so a replenishing queue cannot monopolize execution. Choose backpressure, rejection, or safe termination according to the affected contract. Overload must not silently lose required state transitions or leave partially applied operations presented as complete. These are design requirements; existing paths can still be unbounded, as documented in [live dispatch and overload behavior](../concepts/live.md#dispatch-priority-and-overload-behavior). ## Recovery after failure Unrecoverable faults must not leave the system operating on potentially invalid state. Normal startup includes configured state recovery, so restart uses the same initialization path as ordinary startup. Recovery depends on retained state and backing-store durability. An external supervisor owns restart after process failure. Recovery aims to minimize downtime; its duration depends on the state to restore and the backing store. Execution recovery must reconcile venue state rather than blindly retry venue commands. Normal operation retains graceful shutdown; an unrecoverable fault may make cleanup unsafe. [Runtime failure and recovery](../concepts/architecture.md#crash-only-design) describes the process and persistence boundaries. ## Domain and integration boundaries Domain types and contracts define trading behavior. Components communicate through explicit interfaces and immutable messages. Ports and adapters keep venue transport and backing-store implementations outside the shared trading core, so custom integrations preserve the same domain contracts. ## Backtest and live behavior Backtest, sandbox, and live environments share core trading components and behavioral contracts. The same strategy and execution-algorithm code can run across these environments. Live execution also introduces venue, transport, timing, persistence, external-activity, and reconciliation behavior that a simulation may not reproduce. Shared code does not imply identical outcomes from different inputs or operating conditions. The [common core](../concepts/architecture.md#common-core) supplies the engines and interfaces; [behavioral models](../concepts/behavioral_models.md) define how model implementations enter the runtime. ## Queued callback dispatch requirements Canonical actor and strategy delivery must preserve publication order, exclusive component access, and lifecycle eligibility, with bounded progress. Reentrancy must not change the ordering rule. Ordered delivery does not imply an event-time cache snapshot. These are requirements for queued dispatch, not guarantees of existing synchronous paths. The [callback dispatch contract](callback_dispatch.md) specifies maintenance timing, ownership boundaries, lifecycle invalidation, and draining behavior. # Docs Style Source: https://nautilustrader.io/docs/latest/developer_guide/docs/ This guide outlines the style conventions and best practices for writing documentation for NautilusTrader. The [Markdown Style](markdown_style.md) guide is the shared baseline for Markdown syntax and formatting, and `.markdownlint.jsonc` enforces its mechanical subset. This guide covers what is specific to NautilusTrader documentation rather than repeating that baseline. ## General principles - We favor simplicity over complexity, less is more. - We favor concise yet readable prose and documentation. - We value standardization in conventions, style, patterns, etc. - Documentation should be accessible to users of varying technical backgrounds. ## Documentation types Most pages should fit one of four types ([Divio documentation system](https://docs.divio.com/documentation-system/)). Mixing types in a single page makes it harder to read and harder to maintain. | Type | Purpose | Section | | ---------------- | ------------------------------- | ---------------- | | **Tutorial** | Teach by walking through a task | `tutorials/` | | **How-to guide** | Solve a specific problem | `how_to/` | | **Explanation** | Clarify design and architecture | `concepts/` | | **Reference** | Describe the machinery | `api_reference/` | Two sections are exceptions: `getting_started/` is an onboarding path that combines tutorial-style walkthroughs with setup instructions, and `integrations/` pages mix reference (capabilities, symbology) with how-to content (setup, configuration) so each venue page is self-contained. Standalone how-to content that is not venue-specific belongs in `how_to/`. ### Choosing the right type - **Does your page walk a newcomer through a learning experience?** Tutorial. - **Does it answer "How do I...?" for someone who already knows the system?** How-to guide. - **Does it explain why something works the way it does?** Explanation. - **Does it list classes, config fields, enums, or capabilities?** Reference. A tutorial says "do this, then this, then this." The author picks the path. A how-to guide says "here is how to achieve X." The reader already knows they want X. Keep these distinct: - Tutorials should not assume prior knowledge. - How-to guides should not teach background concepts. When one type needs to reference another, link to it instead of inlining. For example, a how-to guide that configures `LiveNodeConfig` should link to the API reference for field definitions rather than listing them again. ## Language and tone - Use active voice when possible ("Configure the adapter" vs "The adapter should be configured"). - Write in present tense for describing current functionality. - Use future tense only for planned features. - Avoid unnecessary jargon; define technical terms on first use. - Be direct and concise; avoid filler words like "basically", "simply", "just". - Use parallel structure in lists; keep grammatical patterns consistent across items. ## Markdown tables Table syntax, pipe alignment, and delimiter padding follow the [Markdown Style](markdown_style.md#tables) guide. ### Notes and descriptions - All notes and descriptions should have terminating periods. - Keep notes concise but informative. - Use sentence case (capitalize only the first letter and proper nouns). ### Example ```markdown | Order Type | Spot | Margin | USDT Futures | Coin Futures | Notes | | ---------------------- | ---- | ------ | ------------ | ------------ | ----------------------- | | `MARKET` | ✓ | ✓ | ✓ | ✓ | | | `STOP_MARKET` | - | ✓ | ✓ | ✓ | Not supported for Spot. | | `MARKET_IF_TOUCHED` | - | - | ✓ | ✓ | Futures only. | ``` ### Support indicators - Use `✓` for supported features. - Use `-` for unsupported features (not `✗` or other symbols). - When adding notes for unsupported features, emphasize with italics: `*Not supported*`. - Make unsupported notes specific when the reason matters: use `*Not supported by *` for venue gaps, or `*Not currently implemented*` for adapter gaps. - Leave cells empty when no content is needed. ## Code references Inline code and fenced code blocks follow the [Markdown Style](markdown_style.md#code) guide. When referencing code locations, use `file_path::function_name` or `file_path::ClassName` rather than line numbers, which become stale as code changes. ## Headings Heading style, case, and hierarchy follow the [Markdown Style](markdown_style.md#headings) guide: title case for the page heading, sentence case below it. NautilusTrader makes one exception for `index.md` pages: headings and navigation links that name individual documents use the target document's exact H1 title, including its capitalization. General section headings retain sentence case. Always capitalize proper nouns regardless of heading level (product names, technologies, companies, acronyms). ## Lists List markers, ordering, and indentation follow the [Markdown Style](markdown_style.md#lists) guide. End list items with periods when they are complete sentences. ## Links and references Link text, link style, and images follow the [Markdown Style](markdown_style.md#links-and-images) guide. Reference external documentation when appropriate. ## Technical terminology - Base capability matrices on the Nautilus domain model, not exchange-specific terminology. - Mention exchange-specific terms in parentheses or notes when necessary for clarity. - Use consistent terminology throughout the documentation. ## Examples and code samples - Provide practical, working examples. - Include necessary imports and context. - Use realistic variable names and values. - Add comments to explain non-obvious parts of examples. ## Admonitions Use admonition blocks to highlight important information: | Admonition | Purpose | | ------------ | ------------------------------------------------------------- | | `:::note` | Supplementary context that clarifies but isn't essential. | | `:::info` | Important information the reader should be aware of. | | `:::tip` | Helpful suggestions or best practices. | | `:::warning` | Potential pitfalls or important caveats. | | `:::danger` | Critical issues that could cause data loss or system failure. | Avoid overusing admonitions; too many diminish their impact. ## MDX components The docs site (fumadocs) provides built-in MDX components available in all `.md` files. No imports are needed. ### Tabs Use tabs for language-specific or variant content. List Rust before Python so Rust is the default (left-most) tab. For code examples, add `tab="..."` to consecutive fenced code blocks: ```markdown \`\`\`rust tab="Rust" let params = Params::from([("close_position", true.into())]); \`\`\` \`\`\`python tab="Python" strategy.submit_order(order, params={"close_position": True}) \`\`\` ``` For tables or other content, wrap each variant in `` and ``. The instrument Fields tables use this so each language shows a single type column instead of side-by-side Rust and Python columns. Leave a blank line above and below the inner content so the Markdown renders. ```markdown | Field | Type | Required/default | Notes | | --------------- | -------------- | ---------------- | ----------------------- | | `instrument_id` | `InstrumentId` | Required | Stored as `id` in Rust. | | Field | Type | Required/default | Notes | | --------------- | -------------- | ---------------- | ----- | | `instrument_id` | `InstrumentId` | Required | | ``` ### Steps Use `Steps` and `Step` for sequential procedures. ```markdown Configure the adapter. Start the trading node. ``` ### Accordions Use `Accordions` and `Accordion` for collapsible content. ```markdown Content here. ``` ### Files Use `Files`, `Folder`, and `File` for directory tree visualizations. ```markdown ``` ### Cards Use `Cards` and `Card` for linked content grids. ```markdown ``` ### TypeTable Use `TypeTable` for parameter or type documentation tables. ## API documentation - Document parameters and return types clearly. - Include usage examples for complex APIs. - Explain any side effects or important behavior. - Keep parameter descriptions concise but complete. # Environment Setup Source: https://nautilustrader.io/docs/latest/developer_guide/environment_setup/ Use an editor with current Rust and Python language support, such as PyCharm or Visual Studio Code. [uv](https://docs.astral.sh/uv) is the preferred tool for handling all Python virtual environments and dependencies. [prek](https://github.com/j178/prek) is used to automatically run various pre-commit checks, auto-formatters, and linting tools at commit. Source builds and the Rust crates require [Rust](https://www.rust-lang.org) ([installation guide](https://www.rust-lang.org/tools/install)). [Cap'n Proto](https://capnproto.org) is required for serialization schema compilation. The required version is specified in `.nautilus-engineering/tools.toml`. Ubuntu's default package is typically too old, so you may need to install from source (see below). :::info NautilusTrader *must* compile and run on **Linux, macOS, and Windows**. Please keep portability in mind: use `std::path::Path` in code and follow the [shell portability policy](shell.md#define-the-portability-target) for scripts. ::: ## Setup The following steps are for UNIX-like systems, and only need to be completed once. ### Quick setup Use this as a compact setup path for a new Linux or macOS development machine. The detailed sections below explain each step and cover alternatives. Install platform tools first: ```bash tab="Ubuntu" sudo apt-get update sudo apt-get install -y build-essential clang lld curl git make pkg-config ``` ```bash tab="macOS" xcode-select --install ``` Then clone the repository and install the pinned project tools: ```bash git clone --branch develop https://github.com/nautechsystems/nautilus_trader cd nautilus_trader curl https://sh.rustup.rs -sSf | sh source "$HOME/.cargo/env" curl -LsSf https://astral.sh/uv/install.sh | sh export PATH="$HOME/.local/bin:$PATH" cargo install cargo-binstall --locked make install-tools ./scripts/install-capnp.sh make sync source python/.venv/bin/activate export PYO3_PYTHON="$PWD/python/.venv/bin/python" if [ "$(uname -s)" = "Linux" ]; then PYTHON_LIB_DIR="$("$PYO3_PYTHON" -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))')" export LD_LIBRARY_PATH="$PYTHON_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" fi export PYTHONHOME="$("$PYO3_PYTHON" -c 'import sys; print(sys.base_prefix)')" prek install make build-debug ``` Windows users should follow the source installation steps in the [installation guide](../getting_started/installation.md#from-source), then use the relevant commands from this guide. ### 1. Install dependencies Follow the [installation guide](../getting_started/installation.md), then sync the development and test dependencies from the repository root: ```bash make sync ``` For frequent development, install a debug build of the package into `python/.venv`: ```bash make install-debug ``` ### 2. Install development tools NautilusTrader pins every development tool so that all contributors and CI run identical versions. A single Makefile target installs the full set: ```bash make install-tools ``` This installs: - **Shared Cargo CLIs** pinned in `.nautilus-engineering/tools.toml`: `cargo-audit`, `cargo-deny`, `cargo-edit`, `cargo-llvm-cov`, `cargo-nextest`, and `cargo-vet`. - **NautilusTrader Cargo CLIs** pinned in `Cargo.toml` under `[workspace.metadata.tools]`: `cargo-codspeed`, `cargo-fuzz`, `cargo-hawk`, `cargo-machete`, `cbindgen`, `flamegraph`, and `lychee`. - **Prebuilt binaries** pinned in `.nautilus-engineering/tools.toml`: `prek` (pre-commit runner) and `osv-scanner` (vulnerability scanner). - **uv**, installed at the shared pinned version. The supported local uv minor series is defined in `python/pyproject.toml`. Cap'n Proto is also pinned in `.nautilus-engineering/tools.toml` but installs separately; see the [Cap'n Proto](#capn-proto) section below. Fuzz targets also require a Rust nightly toolchain at runtime because `cargo-fuzz` uses `libfuzzer-sys` and unstable compiler flags: ```bash rustup toolchain install nightly ``` The docs.rs compatibility check uses the dated nightly pinned in `tools.toml`: ```bash rustup toolchain install "$(bash scripts/tool-version.sh nightly)" --profile minimal ``` #### One-off prerequisite: cargo-binstall `make install-tools` uses [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall) to fetch `prek` as a prebuilt binary instead of compiling it from source. Install `cargo-binstall` once per machine: ```bash cargo install cargo-binstall --locked ``` This is a one-time step. Subsequent runs of `make install-tools` reuse the installed `cargo-binstall`. #### Single source of truth for versions The repository manifests are the canonical source for dependency and tool versions. Do not copy current version numbers into docs, runner images, or scripts unless there is no manifest-backed way to read them. | Source file or section | Defines | | ----------------------------------------- | -------------------------------------------------------- | | `rust-toolchain.toml` | Rust toolchain. | | `Cargo.toml` and `Cargo.lock` | Rust workspace dependencies and exact resolution. | | `Cargo.toml` `[workspace.metadata.tools]` | NautilusTrader-specific Cargo tools. | | `python/pyproject.toml` | Python dependencies and supported Python and uv ranges. | | `python/uv.lock` | Exact Python dependency resolution. | | `.nautilus-engineering/tools.toml` | Shared engineering tools. | | `tools.toml` | NautilusTrader-specific tools without a native manifest. | The shared catalog includes uv, `prek`, `pip-audit`, `osv-scanner`, Cap'n Proto, and common Cargo tools. The local catalog retains the docs.rs nightly, Miri toolchain, and `pypi-attestations` pins. The Makefile reads these via `scripts/cargo-tool-version.sh`, `scripts/tool-version.sh`, and `scripts/uv-version.sh`, so bumping a version in the source file is the only required version change. To check the pinned cargo tool versions against crates.io, run: ```bash make outdated ``` ### 3. Set up Git hooks Set up the file and commit-message hooks, which run automatically when committing: ```bash prek install ``` Rerun `prek install` after pulling a change to the configured hook types. Before opening a pull-request run the formatting and lint suite locally so that CI passes on the first attempt: ```bash make format make pre-commit ``` Make sure the Rust compiler reports **zero errors** -- broken builds slow everyone down. ### 4. Configure environment variables NautilusTrader keeps its uv-managed environment at `python/.venv`, beside `python/pyproject.toml`. This follows [uv's default project environment layout](https://docs.astral.sh/uv/concepts/projects/layout/#the-project-environment), which keeps the environment where uv and Python editors expect to discover it. Run direct uv project commands from `python/` or pass `--project python` from the repository root. Make targets and CI select the project themselves. :::warning If this checkout previously used the root `.venv`, remove any `UV_PROJECT_ENVIRONMENT` export from your shell startup files and the current shell before running Make or uv. This override takes precedence over uv's project discovery. Also replace any `PYO3_PYTHON` export that points to the root `.venv/bin/python` with this checkout's `python/.venv/bin/python`. Editing a startup file does not update existing shells or running applications: repeat the exports in each shell and restart applications that inherited the old environment. ::: **Required for Rust/PyO3 (Linux and macOS)**: When using Python installed via `uv` on Linux or macOS, set the following environment variables from the repository root after `make sync`: Use the commands for your shell. Bash and Zsh use `export` and `activate`; Fish uses `set -gx` and `activate.fish`. Source Fish scripts only from Fish. ```bash tab="Bash / Zsh" # Set the Python executable path for PyO3 export PYO3_PYTHON="$PWD/python/.venv/bin/python" # Linux only: Set the library path for the uv-managed Python runtime PYTHON_LIB_DIR="$("$PYO3_PYTHON" -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))')" export LD_LIBRARY_PATH="$PYTHON_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" # Set the Python home path (required for Rust tests) export PYTHONHOME="$("$PYO3_PYTHON" -c 'import sys; print(sys.base_prefix)')" ``` ```fish tab="Fish" set -gx PYO3_PYTHON "$PWD/python/.venv/bin/python" if test (uname -s) = Linux set -l python_lib_dir ("$PYO3_PYTHON" -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))') set -gx LD_LIBRARY_PATH "$python_lib_dir" (string match -v "" -- $LD_LIBRARY_PATH) end set -gx PYTHONHOME ("$PYO3_PYTHON" -c 'import sys; print(sys.base_prefix)') ``` :::note The `LD_LIBRARY_PATH` export is Linux-specific and not needed on macOS or Windows. - `PYO3_PYTHON` tells PyO3 which Python interpreter to use, reducing unnecessary recompilation. - `PYTHONHOME` is required when running `make cargo-test` with a `uv`-installed Python. Without it, tests that depend on PyO3 may fail to locate the Python runtime. ::: To verify your environment is configured correctly: ```bash python -c "import sys; print('Python:', sys.executable, sys.version)" echo "PYO3_PYTHON: $PYO3_PYTHON" echo "PYTHONHOME: $PYTHONHOME" ``` ## Dependency management Python dependencies are managed by [uv](https://docs.astral.sh/uv). The `[tool.uv]` section in `python/pyproject.toml` enforces three supply chain safety settings: - **`required-version`**: local uv commands accept any patch release in the supported minor series. If your local uv is outside that range, `uv lock` and `uv sync` fail with a version mismatch. `.nautilus-engineering/tools.toml` separately pins the exact version used by CI, Docker, pre-commit, and `make update-uv`. The stub targets run through `make sync`, so they enforce the same supported range; see [Generated Python artifacts](rust.md#generated-python-artifacts). - **`exclude-newer = "7 days"`**: `uv lock` ignores package versions published within the last 7 days. This gives the community time to detect and quarantine compromised releases before they enter the lockfile. The value accepts an RFC 3339 timestamp (`"2026-03-30T00:00:00Z"`), a friendly duration (`"7 days"`, `"1 week"`, `"24 hours"`), or an ISO 8601 duration (`"P7D"`, `"P1W"`, `"PT24H"`). uv 0.11.8+ stores the friendly/ISO form as `exclude-newer-span` inside `python/uv.lock` and emits a sentinel `exclude-newer` timestamp alongside it for backwards compatibility. `python/uv.lock` uses that format. - **`no-build-package`**: explicit list of every third-party package locked in `python/uv.lock`. `uv` refuses to build any of them from source. In normal operation uv prefers wheels, so the setting is a no-op; it triggers only if a listed package stops publishing wheels for the target platform, in which case `uv lock` fails rather than silently building from an sdist. The local workspace package is intentionally not in the list because it must be built by the workspace's own build backend. The list is kept in sync with `python/uv.lock` by `scripts/check-no-build-packages.sh`, which also runs as a pre-commit hook on changes to the lockfile or manifest. ### Bypassing the cooldown When a security patch or critical bug fix must be pulled in immediately, review the release and override `exclude-newer` for that lock operation. Prefer a package-scoped override so unrelated packages remain subject to the 7-day default. Do not add persistent package overrides to `python/pyproject.toml`. All forms accept a timestamp, friendly duration, or ISO duration; package overrides additionally accept `false` to exempt a package from the cooldown entirely. ```bash # Shorten the cooldown for a single package (friendly duration) uv lock --project python --exclude-newer-package "somepackage=1 day" # Pin a single package to an absolute cutoff uv lock --project python --exclude-newer-package "somepackage=2026-03-30T00:00:00Z" # Exempt a single package from the cooldown entirely uv lock --project python --exclude-newer-package "somepackage=false" # Disable the cooldown for the whole resolution after reviewing every newly eligible package uv lock --project python --exclude-newer "0 seconds" ``` The CLI flag overrides the `python/pyproject.toml` value for that invocation only. The config remains unchanged for subsequent runs. ### Updating uv To support a new uv minor series, change `required-version` in `python/pyproject.toml`. To update the exact project version within that range, update Nautilus Engineering's `[uv].version`, sync the shared catalog, then update the `rev` in `.pre-commit-config.yaml` and each digest-pinned uv Docker image. Run `make update-uv` to install the project version locally. ### Rust dependency cooldown before compilation Repository builds must check every resolved registry dependency before Cargo can execute dependency build scripts or procedural macros. `make check-cargo-cooldown` checks all tracked `Cargo.lock` files against `[workspace.metadata.cooldown]` in `Cargo.toml`, including versions already committed or pulled from another branch. It does not need a Git comparison base or full checkout history. The Rust build, stub, check, Clippy, test, coverage, documentation, benchmark, and local CLI install targets require this check. Stub generation counts as compilation because it runs the Rust `python-stub-gen` binary through Cargo. Each compilation target waits for the gate, including under parallel Make. Compilation uses the checked lockfile without resolving replacements. Pre-flight also checks early, and CI common setup checks before repository compilation begins. A version inside the cooldown window requires both an exact entry in `[workspace.metadata.cooldown.allow]` and a matching cargo-vet audit. Unsupported registries fail the check. Publication dates come from the committed database at `.supply-chain/crate-dates.json`. Recorded dates are trusted offline; versions missing from the database are looked up on crates.io and fail closed when the registry is unreachable. The pre-commit hook checks all resolved versions using these recorded dates, including entries added since the comparison base. A clean Git diff does not establish that dependencies are old enough. `make cargo-update` records dates for every change it accepts. After a manual lockfile edit, run `bash scripts/check-cargo-cooldown.sh --update-db` to reconcile the database, which also prunes entries no tracked lock resolves. The dependency-update command separately re-verifies newly added dates against crates.io; routine pre-commit and full checks use the committed database offline. Successful full checks are cached as `.cargo-cooldown.json` in `CARGO_TARGET_DIR`, or the Make `TARGET_DIR` when no Cargo target directory is set. CI uses its configured Cargo target directory so persistent runners retain the cache between jobs. Changes to any checked lockfile, the policy, audits, database, or the check script invalidate the cache. Failed checks are not cached. Treat this file as local verification state; do not restore it from an untrusted source. This gate reduces exposure to newly published malicious registry releases. It does not establish that older releases are safe, sandbox build scripts, or vet Git and local path dependencies. Development-tool bootstrap commands such as `make install-tools` install external packages with separate dependency resolutions and are outside this repository-lockfile gate. Direct Cargo and Maturin invocations also bypass Make: run the full check first and pass `--locked` when building repository code. Keep manifests and lockfiles unchanged between the check and compilation. ## Builds The Python package and the standalone Nautilus CLI are separate build artifacts. `make build-debug` and `make build` install the Python package into `python/.venv`; neither command updates the `nautilus` binary in Cargo's binary directory. See the [Nautilus CLI developer guide](#nautilus-cli-developer-guide) when changing or using the CLI. After changing Rust bindings or Python package code, use a debug build for normal development. It skips release optimization and LTO, which reduces build time and peak memory use: ```bash make build-debug ``` Use `make build` when you need an optimized build. The release profile uses fat LTO and one code generation unit, which increases peak memory use. Fat LTO can complete on a 16 GB machine when the build has access to the full memory allocation and sufficient swap. Check VM or container memory limits when applicable. If the linker runs out of memory, use ThinLTO for an optimized local build: ```bash CARGO_PROFILE_RELEASE_LTO=thin make build ``` This override applies only to that command. Use the default fat LTO profile for performance measurements. ### Refresh after pulling changes Use the command that updates the affected artifact. The build targets call their prerequisites, so `make build-debug` also syncs Python dependencies and regenerates Python type stubs. | Changed input | Command | Updated artifact | | --------------------------------------------------- | ---------------------------- | ------------------------------------------------- | | `python/pyproject.toml` or `python/uv.lock` | `make sync` | Dependencies in `python/.venv`. | | Rust bindings, Python package code, or stub sources | `make build-debug` | Debug Python package and generated type stubs. | | CLI code, SQL initialization code, or `schema/sql` | `make install-cli` | Standalone `nautilus` binary in Cargo's bin path. | | Cargo, uv, `prek`, or OSV Scanner tool pins | `make install-tools` | Pinned development tools. | | Cap'n Proto version in the shared catalog | `./scripts/install-capnp.sh` | Cap'n Proto compiler. | The environment variables in [Configure environment variables](#4-configure-environment-variables) contain checkout-specific paths. After switching checkouts, changing the selected Python version, or recreating `python/.venv`, activate that checkout's environment and export the variables again. Verify that the shell resolves Python from the expected checkout: ```bash source python/.venv/bin/activate command -v python python --version ``` In Fish, use `source python/.venv/bin/activate.fish` for activation. Activation alone does not refresh `PYO3_PYTHON`; repeat the environment variable commands above for the selected checkout. ## Cap'n Proto [Cap'n Proto](https://capnproto.org) is required for serialization schema compilation. The required version is defined in `.nautilus-engineering/tools.toml`. Install the correct version for your platform: ```bash tab="Script (Linux/macOS)" ./scripts/install-capnp.sh ``` ```bash tab="macOS (Homebrew)" brew install capnp ``` ```bash tab="Linux (source)" CAPNP_VERSION=$(bash scripts/tool-version.sh capnp) cd ~ wget https://capnproto.org/capnproto-c++-${CAPNP_VERSION}.tar.gz tar xzf capnproto-c++-${CAPNP_VERSION}.tar.gz cd capnproto-c++-${CAPNP_VERSION} ./configure make -j$(nproc) sudo make install sudo ldconfig ``` ```bash tab="Windows (Chocolatey)" choco install capnproto ``` Verify the installed version matches the shared catalog: ```bash capnp --version ``` The install script ensures the pinned version is installed. If Homebrew or Chocolatey provides an older version, install from source or see the [Cap'n Proto installation guide](https://capnproto.org/install.html). ## Faster builds The Cranelift code generation backend can reduce local build time for development, tests, and IDE checks. It requires the nightly Rust toolchain and local changes to `Cargo.toml`: ```bash rustup toolchain install nightly --component rust-analyzer ``` Save the patch below, then apply it with `git apply `. Remove it with `git apply -R ` before pushing changes. :::warning Do not commit these changes. The cranelift patch is for local development only and will break CI if pushed. ::: ```diff diff --git a/Cargo.toml b/Cargo.toml --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["codegen-backend"] + [workspace] resolver = "2" members = [ @@ -424,6 +426,7 @@ lto = false panic = "unwind" incremental = true +codegen-backend = "cranelift" # Compile third-party deps at opt-level=1 in dev/test profiles. Workspace # members keep opt-level=0 (fast iteration); deps recompile rarely so the @@ -444,6 +447,7 @@ strip = false lto = false incremental = true +codegen-backend = "cranelift" [profile.test.package."*"] opt-level = 1 @@ -452,6 +456,7 @@ inherits = "test" debug = false # Improves compile times strip = "debuginfo" # Improves compile times +codegen-backend = "cranelift" [profile.ci-pr] inherits = "test" ``` Run local build commands with `RUSTUP_TOOLCHAIN=nightly`, for example: ```bash RUSTUP_TOOLCHAIN=nightly make build-debug ``` Set the same toolchain in your [rust-analyzer settings](#rust-analyzer-settings) when using this local patch. ## Services Initialize PostgreSQL, Redis, and pgAdmin from the repository root: ```bash make init-services ``` This starts the containers and initializes the NautilusTrader database schema. To start the containers without reinitializing the schema, run `make start-services`. To start one service, use the Compose file directly: ```bash docker compose -f .docker/docker-compose.yml up -d postgres ``` The development services are: - `postgres`: PostgreSQL with `POSTGRES_USER=nautilus`, `POSTGRES_PASSWORD=pass`, and `POSTGRES_DB=nautilus` by default. - `redis`: Redis server. - `pgadmin`: pgAdmin 4 for database management and administration. :::info Please use this as development environment only. For production, use a proper and more secure setup. ::: Use `make stop-services` to stop the containers without removing their data. Use `make purge-services` only when you intend to delete the development volumes. PostgreSQL-backed tests can each maintain several connections. On a high-core workstation, the local nextest concurrency can exceed the development container's connection limit. Use the CI profile to match CI's lower concurrency: ```bash NEXTEST_PROFILE=ci make cargo-test-extras ``` To retain more local parallelism, set an explicit bounded worker count, for example: ```bash NEXTEST_TEST_THREADS=8 make cargo-test-extras ``` ## Nautilus CLI developer guide The Nautilus CLI is a standalone Rust binary for PostgreSQL administration and other repository operations. It is independent from the Python package installed by `make build-debug` or `make build`. ### Build and select the CLI Install the CLI from the current checkout with: ```bash make install-cli ``` This target runs `cargo install --locked --force` and places `nautilus` in Cargo's binary directory, normally `~/.cargo/bin`. Reinstall it after pulling changes to `crates/cli`, SQL initialization code, or `schema/sql`. An installed CLI can otherwise remain older than the checkout while reading newer schema files from it. Before running repository-dependent commands, check which binary the shell resolves and its version: ```bash command -v nautilus nautilus --version ``` To build and run the CLI directly from the checkout without replacing the installed binary, use: ```bash cargo run --locked --package nautilus-cli --bin nautilus -- --help ``` :::warning On Linux systems with GNOME, `/usr/bin/nautilus` is normally the GNOME file manager. Select the NautilusTrader CLI with one of these methods: - Put `~/.cargo/bin` before `/usr/bin` in `PATH`. - Run `~/.cargo/bin/nautilus` explicitly. - Add `alias nautilus="$HOME/.cargo/bin/nautilus"` to the shell configuration. ::: Windows source installs require GNU Make through MSYS2 or WSL. The nightly workflow also publishes a Windows x86-64 CLI archive. Run `nautilus --help` to view the available command groups. ### Database commands The database commands accept connection settings as command-line arguments or through a `.env` file in the current working directory or one of its parents. The CLI also accepts the corresponding environment variables. | Flag | Environment variable | Purpose | | ------------ | -------------------- | ------------------------------------------------------------- | | `--host` | `POSTGRES_HOST` | Database host. | | `--port` | `POSTGRES_PORT` | Database port. | | `--username` | `POSTGRES_USERNAME` | Connecting administrator, normally the `postgres` role. | | `--password` | `POSTGRES_PASSWORD` | Administrator password and password for the application role. | | `--database` | `POSTGRES_DATABASE` | Database name and application role created during `init`. | | `--schema` | `SCHEMA_DIR` | Directory containing the SQL schema files. | For example: ```dotenv POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_USERNAME=postgres POSTGRES_PASSWORD=pass POSTGRES_DATABASE=nautilus ``` `nautilus database init` creates or updates the roles and schema from the SQL files. Pass the schema directory explicitly so renamed clones and worktrees do not depend on checkout path detection: ```bash nautilus database init --schema "$PWD/schema/sql" ``` Use a CLI built from the same checkout as these schema files. The initialization is designed to be re-run, including after an earlier run stopped partway through. :::danger `nautilus database drop` removes the target schema, privileges, role, and stored data. Use it only for a disposable database or after confirming that the data can be deleted. ::: Run `nautilus database --help` for the complete command syntax. ## Rust analyzer settings Rust analyzer is a popular language server for Rust and integrates with many IDEs. Configure its `VIRTUAL_ENV` to use `python/.venv`. If PyO3 analysis cannot locate Python, also provide the `PYO3_PYTHON` and `PYTHONHOME` values from [Configure environment variables](#4-configure-environment-variables). The examples below cover VS Code and AstroNvim. For other settings, see the [rust-analyzer configuration](https://rust-analyzer.github.io/book/configuration.html). ```json tab="VSCode" { "rust-analyzer.restartServerOnConfigChange": true, "rust-analyzer.linkedProjects": [ "Cargo.toml" ], "rust-analyzer.cargo.features": "all", "rust-analyzer.check.workspace": false, "rust-analyzer.check.extraEnv": { "VIRTUAL_ENV": "/python/.venv", "CC": "clang", "CXX": "clang++" }, "rust-analyzer.cargo.extraEnv": { "VIRTUAL_ENV": "/python/.venv", "CC": "clang", "CXX": "clang++" }, "rust-analyzer.runnables.extraEnv": { "VIRTUAL_ENV": "/python/.venv", "CC": "clang", "CXX": "clang++" }, "rust-analyzer.check.features": "all", "rust-analyzer.testExplorer": true } ``` ```lua tab="Neovim (AstroLSP)" config = { rust_analyzer = { settings = { ["rust-analyzer"] = { restartServerOnConfigChange = true, linkedProjects = { "Cargo.toml" }, cargo = { features = "all", extraEnv = { VIRTUAL_ENV = "/python/.venv", CC = "clang", CXX = "clang++", }, }, check = { workspace = false, command = "check", features = "all", extraEnv = { VIRTUAL_ENV = "/python/.venv", CC = "clang", CXX = "clang++", }, }, runnables = { extraEnv = { VIRTUAL_ENV = "/python/.venv", CC = "clang", CXX = "clang++", }, }, testExplorer = true, }, }, }, } ``` # FFI Memory Contract Source: https://nautilustrader.io/docs/latest/developer_guide/ffi/ NautilusTrader exposes a C foreign function interface (FFI) only from `nautilus-core` and `nautilus-model`. Both crates gate the interface behind their `ffi` Cargo feature and keep the exported modules under `crates/core/src/ffi/` and `crates/model/src/ffi/`. Other workspace crates use Rust APIs or PyO3 bindings. The separate `nautilus-plugin` crate defines the public guest plug-in ABI and does not share this memory contract. The rules below are strict. Violating them can cause undefined behavior, including double frees, memory leaks, and invalid pointer access. ## Panic handling Rust panics must never unwind across an `extern "C"` function. Exported functions that can panic must route their implementation through `nautilus_core::ffi::abort_on_panic`, which logs the panic and aborts the process before unwinding crosses the C boundary. ## `CVec` ownership `CVec` is a C-compatible representation of Rust vector allocation metadata. A `CVec` created from `Vec` transfers unique ownership of the allocation to the foreign caller. It is intentionally neither `Copy` nor `Clone` in Rust, but C can still copy its fields, so callers must enforce the same exactly-once ownership rule. | Step | Owner | Action | | ---- | ------- | --------------------------------------------------------------------------------------------- | | 1 | Rust | Convert `Vec` into `CVec`, transferring the allocation to the caller. | | 2 | Foreign | Read the elements without changing `ptr`, `len`, or `cap`. | | 3 | Foreign | Call the matching type-specific `vec_drop_*` function exactly once to release the allocation. | Forgetting the drop leaks the allocation. Dropping the same allocation more than once can corrupt the allocator and crash the process. Empty `CVec` values have `len == 0` and `cap == 0`. Their pointer is an opaque sentinel and must not be dereferenced. Rust consumers must use `CVec::into_vec`, which handles the empty case before it inspects the pointer. Borrowing consumers must use `CVec::as_slice` for the same reason. Both methods are unsafe because the public metadata cannot prove allocation provenance, alignment, initialization, or exclusive ownership. Any exported function that accepts a caller-provided `CVec` and invokes either method must: - Be an `unsafe extern "C" fn`. - Document the caller obligations in a `# Safety` section. - Validate `len`, `cap`, and null-pointer invariants before reconstructing or borrowing data. - Use a concrete element type that matches the original `Vec` allocation. ## Type-specific drop functions There is no generic `cvec_drop`. Reconstructing every allocation as `Vec` gives the allocator the wrong element layout for other types. Each owned vector crossing the boundary requires a drop function for its exact element type, such as `vec_drop_book_levels`, `vec_drop_book_orders`, or `vec_drop_fills`. Add the drop function beside the producer so reviews can verify the pair together. Tests must cover the empty sentinel and any metadata checks implemented by the consumer. ## Borrowed foreign buffers Memory allocated outside Rust must not be reconstructed as `Vec`. Borrow it with `CVec::as_slice`, and copy it with `to_vec()` when Rust needs owned storage. The foreign caller keeps ownership and must release the original buffer with the allocator that created it. ## Opaque pointers Model objects whose layout is not `repr(C)` cross the ABI as opaque owning pointers. The generated header forward-declares the type, so foreign callers handle it only through exported functions. Constructors return an owning `*mut T` from `Box::into_raw`, pure accessors take `&T` or `&mut T` references, and every constructor must have a matching drop function: ```rust #[unsafe(no_mangle)] pub extern "C" fn orderbook_new(id: InstrumentId, book_type: BookType) -> *mut OrderBook { Box::into_raw(Box::new(OrderBook::new(id, book_type))) } /// # Safety /// /// `book` must be a live owning pointer returned by [`orderbook_new`], and must not /// be used after this call. /// /// # Panics /// /// Panics if `book` is null. #[unsafe(no_mangle)] pub unsafe extern "C" fn orderbook_drop(book: *mut OrderBook) { abort_on_panic(|| { assert!(!book.is_null(), "`book` was NULL"); // SAFETY: Caller guarantees `book` was allocated by `orderbook_new` drop(unsafe { Box::from_raw(book) }); }); } ``` The foreign owner must call the drop function exactly once. It must not copy the pointer, consume both copies, or use the pointer after the drop. ## Review checklist For each new or changed FFI export: - Keep the implementation in `nautilus-core` or `nautilus-model`. - Use `repr(C)` for every type whose layout crosses the boundary. - Prevent panics from unwinding across the boundary. - Pair each owned allocation with one type-specific release path. - State complete pointer and ownership obligations in `# Safety` documentation. - Add focused tests for the ownership and validation rules. # Developer Guide Source: https://nautilustrader.io/docs/latest/developer_guide/ Guidance on developing and extending NautilusTrader to meet your trading needs or to contribute improvements back to the project. The core is written in Rust. Python serves as the control plane for strategy logic, configuration, and orchestration. PyO3 bridges the two, exposing Rust functionality to Python with minimal overhead. } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> # Markdown Style Source: https://nautilustrader.io/docs/latest/developer_guide/markdown_style/ Standard revision: 1 This document defines a shared Markdown baseline for repositories that adopt a local copy. Keep copies byte-for-byte identical to the maintained source and put repository-specific additions in a separate local guide. ## Requirement levels Each rule has one of three levels: - **Required:** Applies to every in-scope file. Rules stated as unqualified imperatives such as "Use" and "Do not", or with "must", are Required. - **Preferred:** Identifies the default when more than one valid form exists. Preferred rules use "Prefer" and do not affect conformance. - **Transitional:** Applies to the named construct when it is added or substantially edited, and is labeled **Transitional** or described as transitional. Existing instances may remain until a separate migration. Statements with "may" or "allowed" grant bounded permissions rather than obligations. Any condition limiting that permission is Required. A repository may document narrower local exceptions where its renderer, generated content, or imported material requires them. ## Language and extensions - Use [CommonMark](https://spec.commonmark.org/current/) as the base specification. - Use [GitHub Flavored Markdown](https://github.github.com/gfm/) for tables, task lists, strikethrough, and autolinks. - Use additional front matter, Markdown extensions, or renderer components only when the repository documents and supports them. - Do not introduce syntax that depends on an undocumented renderer extension. The linked specifications are references, not routine prerequisites. Open them only to resolve a concrete parser or renderer ambiguity that the local guide, surrounding content, and markdownlint do not answer. ## Enforcement - Follow the repository's `.markdownlint.jsonc` or equivalent local configuration. - Treat this standard as the intended style and markdownlint as automated enforcement of its mechanical subset. - Treat an undocumented conflict between this standard and the local configuration as drift to resolve, not an implicit exception. - Ensure all authored Markdown in the repository's lint scope passes its configured Markdown check. - Do not hand-edit generated Markdown. Change its source and run the generator. Generated files, imported third-party documents, and renderer fixtures may use documented repository exclusions. ## Headings - Use ATX headings (`#`, `##`, `###`, and so on). - Use one H1 as the first Markdown heading. - Use title case for H1. - Use sentence case for H2 and below. - Maintain a logical hierarchy without skipping heading levels. - Leave one blank line above and below each heading. ## Paragraphs and wrapping - Separate paragraphs with one blank line. - Do not add consecutive blank lines. - Prefer a line-length target of 100-120 characters for prose. - Prefer natural breaks and a longer line over leaving one to three words on the next line. - Allow code blocks, tables, and long link destinations to exceed the target when needed. ## Lists - Use `-` for unordered list items. - Use ordered lists only when sequence matters. - **Transitional:** Use `1.` for every source item in an ordered list so the renderer supplies the displayed numbers. - Keep list indentation and spacing consistent with the local markdownlint configuration. - Leave one blank line before and after a list. Example: ```markdown - First item - Second item 1. First step 1. Second step ``` ## Admonitions - Use only the admonition syntax that the repository documents and renders. - When the repository documents and renders GitHub alerts without a stricter local syntax, use the GitHub blockquote alert form with one of `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, or `CAUTION`. - When support for that extension is absent or unconfirmed, use a portable blockquote with a bold text label. - For a multi-paragraph admonition, prefix every content line and blank continuation line with `>`. A bare blank line ends the admonition. - Keep the type label in the source and rendered output. Do not convey meaning through color or an icon alone. - Preserve established, supported admonition syntax during a narrow edit. Do not convert it only to impose this fallback. GitHub alert: ```markdown > [!WARNING] > Back up the database before running the migration. ``` Portable fallback: ```markdown > **Warning:** Back up the database before running the migration. ``` ## Tables - Use GFM pipe tables for tabular content. - Include leading and trailing pipes. - Align pipe characters vertically. - Pad each column to its widest cell plus one space either side, which matches Prettier's default table output. MD060 checks that pipes line up but ignores cell content, so it accepts a column padded far wider than anything in it. - Pad delimiter cells with spaces (`| ----- |`, not `|-----|`); MD060 does not check this either. - The `normalize markdown table padding` pre-commit hook rewrites tables to this form, so there is no need to count characters by hand. - Prefer left-aligned text columns and right-aligned numeric columns where appropriate. - Keep the delimiter row consistent with the intended rendered alignment. - Avoid HTML tables unless Markdown cannot express the required structure or the repository documents the need. Example: ```markdown | Name | Value | | ----- | ----: | | Alpha | 42 | | Beta | 17 | ``` ## Code - Use backtick-fenced code blocks instead of indented code blocks. - **Transitional:** Specify a language for every opening fence. Use `text` for plain text or output without a more specific grammar. - Use a longer outer fence when documenting fenced Markdown. - Use inline code for commands, file names, functions, types, environment variables, configuration keys, and identifiers. Example: ````markdown ```rust fn main() { println!("Hello"); } ``` ```` Run `cargo test` and edit `.markdownlint.jsonc`. ## Emphasis - Use `*italic*` for emphasis. - Use `**bold**` for strong emphasis. - Avoid emphasis that does not add meaning. ## Thematic breaks Use three hyphens: ```markdown --- ``` ## Links and images - Use descriptive link text. - Prefer inline links. Prefer reference-style links when the same destination appears more than once in a document. - Avoid bare URLs. - Keep internal links relative when the repository's renderer supports them. - Give images useful alternative text. Use empty alternative text only for deliberately decorative images. ## HTML - Prefer portable Markdown syntax over raw HTML. - Use raw HTML only when Markdown cannot express the required result or the repository documents the element or component. - Preserve supported front matter, MDX components, fence attributes, and other repository extensions when editing surrounding content. ## Files - Use UTF-8 encoding. - Use LF line endings. - End each file with one newline. - Do not leave trailing whitespace or use trailing spaces to create Markdown hard line breaks. ## Editing guidance When creating or modifying Markdown: - For narrow edits, preserve surrounding style and use markdownlint without loading the full guide unless both leave a concrete question unanswered. - Before creating or substantially restructuring documentation, consult only the relevant sections of the repository-local Markdown guide. - Preserve the existing document structure unless the task requires a change. - Keep formatting consistent with surrounding content and supported renderer extensions. - Do not reformat unrelated sections. - Run the repository's focused Markdown check when available. ## Transitional adoption ATX headings are enforced through `MD003`. Repeated `1.` ordered-list markers and languages on opening code fences are transitional rules, but `MD029` and `MD040` remain disabled until existing documents receive separate mechanical migrations. Do not broaden a narrow documentation change solely to migrate those existing constructs. # Plugins Source: https://nautilustrader.io/docs/latest/developer_guide/plugins/ The `nautilus-plugin` crate defines the artifact contract for NautilusTrader plug-ins: an independently compiled Rust `cdylib` that identifies itself with a versioned manifest and exchanges values across a C-ABI boundary. The crate covers artifact identity and the boundary primitives only. It does not load, register, or run plug-ins; the loading host is an internal Nautilus deployment detail and is not part of this repository. :::warning The plug-in ABI is early alpha and the contract is unstable. Pin plug-in builds to the matching `nautilus-plugin` version. ::: ## Artifact contract A plug-in is a Rust `cdylib` that exports a single entry symbol, `nautilus_plugin_init`. The `nautilus_plugin!` macro generates that symbol together with the static manifest carrying the build identity: ```rust nautilus_plugin::nautilus_plugin! { name: "example-plugin", vendor: "Nautech", version: env!("CARGO_PKG_VERSION"), } ``` `name` and `version` are required, `vendor` defaults to an empty string. Invoke the macro once per artifact at module scope, set `crate-type = ["cdylib"]` in the artifact's `Cargo.toml`, and depend on the matching `nautilus-plugin` version. ## Manifest compatibility `nautilus_plugin_init` takes an opaque host pointer and returns a `PluginManifest` held in process-lifetime storage, or null when the host pointer is null or the call panics. `PluginManifest::validate` checks the invariants a host relies on before registration. It reports every structural problem it finds and fails when: - `abi_version` does not equal `NAUTILUS_PLUGIN_ABI_VERSION`, or `build_id.schema_version` does not equal `PLUGIN_BUILD_ID_VERSION`. - `plugin_name` or `plugin_version` is empty. - Any manifest string is malformed: a null pointer with non-zero length, or bytes that are not valid UTF-8. - `build_id.precision_mode` or `build_id.fixed_precision` differs from the host build. Precision is validated because it changes model type layout across the boundary. The remaining build identity fields (`nautilus-plugin` version, `rustc` version, target triple, and build profile) are diagnostic. ## Boundary rules Only the `#[repr(C)]` types in `nautilus_plugin::boundary`, and `#[repr(C)]` types built from them, may appear in a signature that crosses the boundary. `String`, `Vec`, and `Box` rely on Rust's unstable ABI and must never cross it. Panics must not unwind across the boundary either; the generated entry symbol catches them and returns null. Under the current ABI, plug-ins are not unloaded, so manifest storage stays valid for the life of the process. See [the crate docs](https://docs.rs/nautilus-plugin) for the boundary and manifest types. # Python Source: https://nautilustrader.io/docs/latest/developer_guide/python/ The [Python](https://www.python.org/) programming language is used for the majority of user-facing code in NautilusTrader. Python provides a rich ecosystem of libraries and frameworks, making it ideal for strategy development, data analysis, and system integration. ## Code style ### PEP-8 The codebase generally follows the PEP-8 style guide. One notable departure is that Python truthiness is not always taken advantage of to check if an argument is `None` for everything other than collections. As per the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html), it's discouraged to use truthiness to check if an argument is/is not `None`, when there is a chance an unexpected object could be passed into the function or method which will yield an unexpected truthiness evaluation (which could result in a logical error type bug). *"Always use if foo is None: (or is not None) to check for a None value. E.g., when testing whether a variable or argument that defaults to None was set to some other value. The other value might be a value that's false in a boolean context!"* :::note Use truthiness to check for empty collections (e.g., `if not my_list:`) rather than comparing explicitly to `None` or empty. ::: We welcome all feedback on where the codebase departs from PEP-8 for no apparent reason. ### Type hints All function and method signatures *must* include type annotations: ```python def __init__(self, config: EMACrossConfig) -> None: def on_bar(self, bar: Bar) -> None: def on_save(self) -> dict[str, bytes]: def on_load(self, state: dict[str, bytes]) -> None: ``` **Union syntax**: Use PEP 604 union syntax for optional types: ```python # Preferred def get_instrument(self, id: InstrumentId) -> Instrument | None: # Avoid def get_instrument(self, id: InstrumentId) -> Optional[Instrument]: ``` **Generic types**: Use Python 3.12 type parameter syntax for reusable functions and classes: ```python def first[T](values: list[T]) -> T: return values[0] ``` ### Docstrings The [NumPy docstring spec](https://numpydoc.readthedocs.io/en/latest/format.html) is used throughout the codebase. This needs to be followed consistently so the docs build correctly. **Python** docstrings should be written in the **imperative mood** - e.g. *"Return a cached client."* This convention aligns with the prevailing style of the Python ecosystem and makes generated documentation feel natural to end-users. #### Private methods Do not add docstrings to private methods (prefixed with `_`): - Docstrings generate public-facing API documentation. - Docstrings on private methods incorrectly imply they are part of the public API. - Private methods are implementation details not intended for end-users. Exceptions where docstrings are acceptable: - Very complex methods with non-trivial logic, multiple steps, or important edge cases. - Methods requiring detailed parameter or return value documentation due to complexity. When a private method needs context (such as a tricky precondition or side effect), prefer a short inline comment (`#`) near the relevant logic rather than a docstring. ### Properties vs methods (PyO3 bindings) When exposing Rust types to Python via PyO3, use `#[getter]` (property) or a plain method based on what the call site communicates, not whether the value can change: - **Property (`#[getter]`):** cheap, side-effect-free, attribute-like view of current state. Scalar fields, predicates, and lightweight derived values belong here even if they change over the object's lifetime. Examples: `status`, `side`, `quantity`, `price`, `is_open`, `has_inputs`, `realized_pnl`, `venue_order_id`. - **Method (no `#[getter]`):** actions, mutations, nontrivial work, allocations/copies, I/O, or anything that takes arguments. Examples: `apply(fill)`, `unrealized_pnl(price)`, `calculate_pnl(...)`. - **Gray area (prefer method):** getters that clone or allocate a collection each call. Using a method signals the cost to the caller. Examples: `events()`, `adjustments()`, `client_order_ids()`, `trade_ids()`. ## Python live callback routing Python live nodes keep one runtime invariant: Tokio worker threads do not run Python code during live trading. `LiveNode::py_run` releases the GIL while the Rust async runtime runs. Worker-side work that must trigger Python uses existing live runner event channels instead of calling `Python::attach` on the worker. Timer callbacks use the time-event channel. The runner drains that channel during startup buffering and the main select loop, then executes callbacks on the live event loop thread. This path is a boundary for unavoidable user Python callback work. It is not a place to move adapter, provider, data, or execution logic into Python. Python adapter modules configure Rust adapters and register factories; Rust owns adapter operations. If worker-side Rust work needs a Python callback, route it through a specific event type that belongs in the live runner. When adding Python-aware live code: - Prefer an existing runner event channel. - Keep callback bodies short because they run synchronously on the live event loop. - Do not call `Python::attach` from Tokio worker tasks in Python live trading. - Do not add adapter business logic in Python to fit callback routing. ### Test naming Use descriptive names that explain the scenario. Keep tests as annotated pytest free functions: ```python def test_write_and_query_option_greeks_round_trip() -> None: ... def test_catalog_loaded_greeks_reach_on_option_greeks() -> None: ... def test_backend_session_rejects_zero_chunk_size() -> None: ... ``` ### Ruff [Ruff](https://astral.sh/ruff) is used to lint the codebase. Its rules are configured in `python/pyproject.toml`, with ignore justifications typically commented. # Python Adapter Interface Source: https://nautilustrader.io/docs/latest/developer_guide/python_adapters/ Use this guide to implement a custom live adapter in a separate Python package, optionally with its own Rust/PyO3 extension. It covers the client interface, registration, and runtime contracts. For the platform overview, see [Adapters](../concepts/adapters.md). For in-tree Rust adapters, see [the Rust adapter guide](adapters.md). Custom clients run on the node's Python event loop and exchange typed objects with the Rust engines through PyO3. Subclass a client from `nautilus_trader.live.clients`: | Base class | Responsibility | | ------------------ | -------------------------------------------------------------------- | | `DataClient` | Data subscriptions and historical requests. | | `MarketDataClient` | Naming specialization of `DataClient`; identical hooks and behavior. | | `ExecutionClient` | Order commands, account updates, and reconciliation reports. | Start with the [deterministic adapter template](../../examples/live/_template/README.md). The [independent Rust/PyO3 package section](#independent-rustpyo3-packages) shows how an extension uses the same interface. ## Registration and configuration ### Define a factory Subclass `DataClientFactory` or `ExecutionClientFactory` from `nautilus_trader.live.clients`. Register either the subclass with a static `create` method or an instance of it. Data factories implement `create(*, name, config, cache, clock)`. Execution factories also receive `trader_id`. The factory returns the appropriate client subclass **synchronously**. The client retains the supplied config and cache objects. Before accepting it, the node validates its registration name and execution identity. Run this example from the repository root to register the template's data client: ```python from nautilus_trader.config import DataClientConfig from nautilus_trader.live import LiveNodeBuilder from nautilus_trader.live.clients import DataClientFactory from examples.live._template.constants import VENUE from examples.live._template.data import TemplateDataClient from examples.live._template.providers import TemplateInstrumentProvider class ExampleFactory(DataClientFactory): @staticmethod def create(*, name, config, cache, clock): return TemplateDataClient( name=name, config=config, cache=cache, clock=clock, venue=VENUE, instrument_provider=TemplateInstrumentProvider(config.instrument_provider), ) builder = LiveNodeBuilder.from_config("example") builder = builder.add_data_client("TEMPLATE", ExampleFactory, DataClientConfig()) ``` The template also supplies an execution client and its factory. Keep node configuration and strategies outside the adapter package. ### Register clients with the node For **direct registration**, use `LiveNodeBuilder.add_data_client(name, factory, config)` or `add_exec_client(...)`. An explicit fourth `routing` argument overrides `config.routing`. For **configuration-based registration**: 1. Put name-to-config mappings in `LiveNodeConfig(data_clients=..., exec_clients=...)`. 1. Supply `data_factories` and `exec_factories` to `LiveNode.build(name, config, ...)` or `LiveNodeBuilder.from_config(name, config, ...)`. Factory lookup checks the complete client name, then its prefix before the first hyphen. A supplied factory takes precedence over an importable config's factory descriptor. Venue and default routing use each config's `RoutingConfig` through the same builder path as direct registration. Both registration paths preserve these rules: - **Native and custom clients can share a node.** Registration distinguishes native factories from Python factories; custom configs do not pass through a native config downcast. - **Duplicate names fail.** Each registered client needs a distinct name. - **Failed builds preserve Python client registrations.** The node disposes clients from the failed attempt, then a retry creates fresh client instances. ### Define adapter configuration Subclass `DataClientConfig` or `ExecutionClientConfig` for adapter fields. The inherited native constructor handles common fields. Declare adapter fields as keyword arguments in a Python `__init__` and accept `**kwargs` for common fields. The factory receives the original subclass instance. Import `ImportableConfig` and `ImportableFactoryConfig` from `nautilus_trader.config` when configs or factories need to be loaded by import path. | Operation | Result | | ---------------------------------------- | ------------------------------------------------------- | | `config.dict()` | Config fields with their Python values retained. | | `config.json()` | JSON bytes. | | `config.to_importable(factory)` | `ImportableConfig` with a `module:qualified_name` path. | | `ImportableFactoryConfig(path).create()` | Factory imported and constructed without arguments. | `ImportableConfig.json()`, `parse()`, and `create()` provide an importable round trip. Classes defined inside functions are not importable. ### Serialize configuration Keep runtime resources out of config objects. Instance fields, including underscore-prefixed fields, participate in serialization; `ClassVar` metadata does not. JSON serialization supports: - Native JSON values. - Common routing and instrument-provider configs. - `Decimal` values encoded as strings. - Domain values with `from_str`. Annotate adapter fields to restore domain values and decimals, including values inside lists, dictionaries, and optional types. `Any` retains the decoded JSON value. Unsupported values raise `TypeError`. :::warning[Serialized credentials] Serializing a config also serializes any credentials it contains. Protect the serialized result. ::: ## Startup, scheduling, and shutdown ### Bind resources at startup Constructors and factories **must not start tasks, capture an event loop, or open network resources**. `client.loop` is `None` during construction. A v1-style factory that declares a `loop` parameter receives `None`. Startup binds every custom client to the actual running loop before `_connect`. Create loop-dependent sessions, locks, and network clients inside that async hook. ### Choose a launch mode | Launch mode | Event loop | Signal ownership | | -------------------------------- | ------------------------------------------------- | ------------------------------------------ | | `node.run()` with custom clients | Creates and drives asyncio on the calling thread. | Restores signal handlers after completion. | | `await node.run_async()` | Uses the host's running loop. | Host application owns its signals. | Both modes preserve the Rust node's startup, reconciliation, maintenance, and shutdown path. Native-only `run()` retains its native execution path. Calling blocking `run()` from an active event loop raises before consuming the node. Use `run_async()` inside an existing loop. :::warning[Database cache backing] Redis/PostgreSQL cache backing is unsupported with custom Python clients in either launch mode. Both modes drive asyncio, where the backing's blocking worker calls would stall the event loop. Native-only nodes can use database backing with `run()`. ::: ### Schedule background work Use `self.create_task(coroutine, name)` for adapter background work. The runtime: - Retains the client and every task until terminal result retrieval. - Defers the first coroutine poll, including with an eager task factory. - Logs unawaited operation failures with the client and operation name. Tasks created directly through asyncio or an external library remain that code's responsibility. Join them during `_disconnect`. ### Process commands in order Commands and subscription changes share a **FIFO queue** with capacity for **1,024 waiting operations per client**. A full queue rejects further admission. An operation failure is logged before the next queued operation runs. Historical requests and awaited reconciliation operations run separately, so network waits do not block command admission. Keep each command hook bounded: it delays later commands on that client. ### Disconnect and drain tasks The node's post-stop grace period precedes disconnection, allowing strategy shutdown cancellations to enter the normal command path. Disconnection then: 1. Closes admission and discards queued commands with diagnostics. 1. Cancels the active command. 1. Calls `_disconnect`, where the adapter closes network resources and joins adapter-owned work. The node bounds disconnection by its configured timeout. The runtime requests cancellation **at most once per task** and shields tasks while draining them, so repeated supervisor cancellation does not interrupt asynchronous cleanup. A deadline reports incomplete cleanup rather than issuing another cancellation. Independently cancelled commands allow the next queued operation to run while admission remains open. :::warning[Cancellation is not completion] Resistant tasks remain supervised, and incomplete cleanup is reported, including when a host closes the loop too early. Finish awaiting the node before closing a host loop. ::: Disposal invalidates output and cache views, so late work cannot emit into a subsequent node. A client instance and its bound provider belong to **one node run**. ## Read-only cache and typed output ### Inspect core state Factories receive `ClientCache`, a **read-only view** of the owning core cache. Instruments, orders, accounts, positions, books, and query collections are owned snapshots. Mutating a returned object does not change core state. No mutable cache, engine, or raw dispatch handle is passed to an adapter. Cache access raises a Python exception when it occurs: - On another thread. - After disposal. - During an incompatible core borrow. The read methods include: | State | Methods | | ---------------------- | --------------------------------------------------------------------- | | Instruments | `instrument`, `instruments`, `instrument_ids` | | Quotes and books | `quote`, `order_book` | | Accounts and positions | `account`, `position`, `positions_open` | | Orders | `order`, `order_list`, `orders`, `orders_open`, `orders_inflight` | | Order queries | `orders_open_count`, `client_order_ids_open`, order identity mappings | | Stored bytes | `get` | Use `quote(instrument_id, index=0)` for quote history. `get` returns a list of byte values, consistent with the v2 cache binding. ### Send typed output Client output methods **enqueue typed values for core processing**. The core remains the sole writer of trading state. | Output | Client methods | | ---------------------- | ------------------------------------------------------------- | | Instrument definitions | `_handle_instrument` | | Streaming data | `_handle_data` | | Historical responses | `_handle_response` | | Execution events | `generate_account_state`, `generate_order_*`, `_handle_event` | | Reconciliation reports | `_handle_report` | `_handle_data` accepts these types from `nautilus_trader.model`: - **Quotes, trades, and bars**: `QuoteTick`, `TradeTick`, `Bar`. - **Order books**: `OrderBookDelta`, `OrderBookDeltas`, `OrderBookDepth10`. - **Prices and funding**: `MarkPriceUpdate`, `IndexPriceUpdate`, `FundingRateUpdate`. - **Options and instrument events**: `OptionGreeks`, `InstrumentStatus`, `InstrumentClose`. - **Custom data**: `CustomData`. `_handle_report` accepts `OrderStatusReport`, `FillReport`, `PositionStatusReport`, or `ExecutionMassStatus` from `nautilus_trader.model`. Its optional `fills` list accompanies an `OrderStatusReport` only. Report account identity, mass-report client/venue identity, and nested report ownership must match the registered client. ## Data client hooks ### Subscriptions and requests Each hook is async and receives an owned, frozen command or request from `nautilus_trader.live`. Nested params are copied. Override the hooks for the venue's supported capabilities; unimplemented hooks raise `NotImplementedError`. Track venue subscription acknowledgments and reconnect replay in adapter-local state. | Family | Subscribe/unsubscribe suffix | Historical request suffix | | ----------------- | --------------------------------------- | -------------------------------------------- | | Custom data | No suffix | `data` | | Instruments | `instruments`, `instrument` | `instruments`, `instrument` | | Order books | `book_deltas`, `book_depth10` | `book_snapshot`, `book_deltas`, `book_depth` | | Quotes and trades | `quotes`, `trades` | `quotes`, `trades` | | Reference prices | `mark_prices`, `index_prices` | *Not supported* | | Funding | `funding_rates` | `funding_rates` | | Bars | `bars` | `bars` | | Instrument events | `instrument_status`, `instrument_close` | *Not supported* | | Options | `option_greeks` | `option_chain_reference_price` | For example, quote subscriptions call `_subscribe_quotes(command)` and `_unsubscribe_quotes(command)`; quote history calls `_request_quotes(request)`. ### Historical responses Preserve request IDs, params, time bounds, and payload identity when constructing the matching typed response. Response classes in `nautilus_trader.live` are: - **Custom data and instruments**: `CustomDataResponse`, `InstrumentResponse`, `InstrumentsResponse`. - **Order books**: `BookResponse`, `BookDeltasResponse`, `BookDepthResponse`. - **Quotes and trades**: `QuotesResponse`, `TradesResponse`. - **Funding and bars**: `FundingRatesResponse`, `BarsResponse`. - **Options**: `OptionChainReferencePriceResponse`. An **empty response still completes the adapter's response path**. Windowed requests expose UTC `start`/`end` datetimes and exact `start_ns`/`end_ns`; response bounds use integer nanoseconds. `RequestBookSnapshot` and `RequestOptionChainReferencePrice` have no time-window fields. ### Instrument providers `InstrumentProvider` stores adapter-local instruments and currencies. Override `load_all_async`, `load_ids_async`, or `load_async`. `initialize(reload=False)` uses `InstrumentProviderConfig` and retries after a failed load. The synchronous loading methods depend on the provider's binding: - **Bound to a client**: schedule supervised work. - **Unbound, outside an active event loop**: run the load synchronously. - **Unbound, inside an active event loop**: await the async method instead. Send loaded instruments through the client output to populate the core cache. ## Execution client hooks ### Construct the client Pass the factory's `name`, `config`, `cache`, `clock`, and `trader_id` to `ExecutionClient`. Execution clients also require these non-optional values from `nautilus_trader.model`: | Argument | Type | | -------------- | ------------- | | `venue` | `Venue` | | `account_id` | `AccountId` | | `account_type` | `AccountType` | | `oms_type` | `OmsType` | `base_currency` and `instrument_provider` are optional. Data clients can use `venue=None`. ### Generate reconciliation reports Implement the reconciliation hooks: - `_generate_order_status_report` - `_generate_order_status_reports` - `_generate_fill_reports` - `_generate_position_status_reports` Return typed reports, lists, or the optional result prescribed by the method. `_generate_mass_status(lookback_mins)` defaults to native composition of the bulk methods using the owning node's clock; an override returns an `ExecutionMassStatus`. **Propagated report failures fail startup reconciliation.** ### Handle order commands Override the hooks supported by the venue: - **Submit**: `_submit_order`, `_submit_order_list`. - **Modify**: `_modify_order`. - **Cancel**: `_cancel_order`, `_cancel_all_orders`. - **Query**: `_query_account`, `_query_order`. `_batch_modify_orders` and `_batch_cancel_orders` default to ordered calls of the corresponding single-order hook. Override them for a venue batch endpoint. Commands preserve native identifiers and params, correlation/causation IDs, timestamps, and nested orders or batch members. ### Make synchronous decisions and receive notifications `_handles_order_venue`, `_provides_bulk_position_coverage`, and `_calculate_commission` are synchronous decision hooks. They **must return promptly without I/O**. Commission returns `Money` or `None` to use the core fallback. `position_reconciliation_tolerance` accepts a nonnegative `Decimal` at construction. `_on_instrument` and `_register_external_order` are async notifications admitted to the command queue. Registration timestamps come from reconciliation metadata, not necessarily the order's initialization event. ## Migration from v1 The interface restores live adapter capabilities using v2 names and ownership rules. It does not make an unchanged Cython adapter source-compatible. | V1 surface | V2 replacement | Migration detail | | ----------------------------------- | ----------------------------------- | ------------------------------------------- | | `LiveDataClient` | `DataClient` | Import from `nautilus_trader.live.clients`. | | `LiveMarketDataClient` | `MarketDataClient` | Same module. | | `LiveExecutionClient` | `ExecutionClient` | Same module. | | `quote_ticks` / `trade_ticks` hooks | `quotes` / `trades` hooks | Typed v2 commands. | | `order_book_*` hooks | `book_*` hooks | Depth subscription uses `book_depth10`. | | `_request` | `_request_data` | Custom-data request. | | `_handle_*` history methods | `_handle_response(typed_response)` | Preserve correlation and bounds. | | `_send_*` execution methods | `_handle_event` / `_handle_report` | Validates owner identity. | | Cache writes | Queued instruments and events | Core applies changes. | | Subscription tracking methods | Adapter-local subscription state | Update from venue acknowledgments. | | Constructor event loop | `client.loop` after startup binding | Create resources in `_connect`. | | `cancel_pending_tasks` | Supervised node disconnection | Cancellation is not terminal completion. | | `run_after_delay` | Coroutine with `asyncio.sleep` | Schedule through `create_task`. | ### Remaining differences and limitations - **Forward prices**: use `RequestOptionChainReferencePrice` and its matching response for a v2 option series. - **DeFi**: native block/pool subscriptions and pool snapshots are outside this Python interface. - **Historical requests**: the [known migration limitations](../../MIGRATION_V2.md#known-limitations) for request joining/completion still apply. - **Custom publication and subscription**: the same migration limitations apply. Clients emit `CustomData` through typed data output; they do not expose component `publish_message` or topic subscription methods. - **Database cache backing**: unsupported with custom Python clients in either launch mode. See [startup](#startup-scheduling-and-shutdown) and [hosted event loops](../concepts/live.md#hosted-event-loops). - **Revised bars**: config retains `handle_revised_bars`, but the v2 core lacks the v1 bar revision marker and revision overwrite behavior. - **Networking**: this interface does not restore removed HTTP/WebSocket bindings. ## Independent Rust/PyO3 packages An adapter can ship its venue implementation in a separate Rust/PyO3 extension. Its Python facade subclasses the same client bases and registers through the same factories as a pure Python adapter. Compile the extension against PyO3 and import NautilusTrader model classes from the installed wheel. :::warning[Use the installed wheel's model classes] All exchanged domain values must be instances of those classes. Do not link a second set of Nautilus model pyclasses or exchange native Rust trait objects across extension modules. The integration boundary is the Python protocol, not a shared Rust ABI. ::: ### Package layout and dependencies Keep the adapter in its own project, with this layout: ```text external-adapter/ Cargo.toml pyproject.toml src/lib.rs external_adapter/__init__.py ``` Configure the library as a Python extension. For example, `Cargo.toml` can contain: ```toml [package] name = "external-adapter" version = "0.1.0" edition = "2024" publish = false [lib] name = "_backend" crate-type = ["cdylib"] [dependencies] pyo3 = { version = "=0.29.2", features = ["extension-module"] } [workspace] ``` The empty workspace keeps Cargo resolution independent if the project sits beneath another Cargo workspace. Keep the adapter's dependency lockfile and audit policy in its own project. Configure the Python package in `pyproject.toml`: ```toml [build-system] requires = ["maturin==1.15.0"] build-backend = "maturin" [project] name = "external-adapter" version = "0.1.0" requires-python = ">=3.12,<3.15" dependencies = ["nautilus-trader"] [tool.maturin] module-name = "external_adapter._backend" python-source = "." ``` These build versions match the demonstrated PyO3 boundary. Set the NautilusTrader dependency range to the releases covered by your adapter's tests. ### Exchange wheel-owned objects Use `Bound<'_, PyAny>` for incoming commands and `Py` for objects returned to Python. Import classes while attached to Python, read typed command attributes, and construct output with those imported classes. This method illustrates the conversion inside a `#[pymethods]` implementation: ```rust fn quote(&self, py: Python<'_>, command: &Bound<'_, PyAny>) -> PyResult> { let model = py.import("nautilus_trader.model")?; let values = PyDict::new(py); values.set_item("instrument_id", command.getattr("instrument_id")?)?; for (name, value) in [("bid_price", "1.12345"), ("ask_price", "1.12349")] { values.set_item( name, model.getattr("Price")?.call_method1("from_str", (value,))?, )?; } for (name, value) in [("bid_size", "17000"), ("ask_size", "23000")] { values.set_item( name, model.getattr("Quantity")?.call_method1("from_str", (value,))?, )?; } values.set_item("ts_event", 19)?; values.set_item("ts_init", 29)?; Ok(model.getattr("QuoteTick")?.call((), Some(&values))?.unbind()) } ``` Import `pyo3::{prelude::*, types::PyDict}` and expose the containing class from a `#[pymodule]` function named `_backend`. The example values are deterministic; a venue implementation supplies its received prices, sizes, and nanosecond timestamps. Construct prices, quantities, and money from exact decimal values or strings without a floating-point round trip. ### Delegate from Python client hooks The Python facade imports the backend and delegates from its client hooks. For a backend class named `Backend` with the method above, the quote hook is: ```python from external_adapter._backend import Backend async def _subscribe_quotes(self, command): self._handle_data(Backend().quote(command)) ``` Place that hook on a `MarketDataClient` subclass. Publish the instrument definition before its market data. Implement connection and disconnection hooks, and supply a `DataClientFactory` that constructs the subclass using the node-provided name, config, cache, and clock. The [Python template](../../examples/live/_template/README.md) demonstrates those hooks and factory registration. A backend with persistent venue state should be retained by its client. Execution adapters follow the same pattern with `ExecutionClient` and `ExecutionClientFactory`. Read `command.order` and emit submission, acceptance, rejection, and fill events through the client's `generate_*` methods. Return the documented report types from reconciliation hooks. Preserve client/account identity, request correlation, exact commission values, and event order; calling from Rust does not change these contracts. ### Lifecycle and cache ownership Keep Python calls and cache access on the node's owner thread and event loop. The GIL permits Python object access; it does not grant another thread permission to use the node's cache or output capability. The supplied cache is read-only, and returned mutable snapshots do not mutate the core. Send typed output through the client so the synchronous core applies state changes. The extension follows the same lifecycle as a Python adapter: - **Construction**: retain configuration without starting network work. - **Connection**: create loop-bound resources in `_connect`. - **Background work**: schedule Python tasks through `self.create_task`. - **Disconnection**: close resources in `_disconnect`. Stop any Rust workers or network tasks owned by the extension and retrieve their terminal results. Do not retain usable cache/output capabilities beyond node teardown. Propagate failures as Python exceptions so the client runtime can supervise them. ### Build and verify installed wheels Install maturin into a compatible CPython build interpreter first. If Cargo would discover a different interpreter, select the build interpreter with `PYO3_PYTHON`. From the adapter's own project, build a wheel with bounded Cargo concurrency: ```bash CARGO_BUILD_JOBS=4 python -m maturin build --out dist ``` Install the adapter wheel and a standard, non-editable NautilusTrader wheel into a fresh environment. Use absolute wheel paths when running these commands from outside both source trees: ```bash python -m venv /tmp/adapter-check /tmp/adapter-check/bin/python -m pip install /absolute/path/nautilus_trader.whl /absolute/path/adapter.whl /tmp/adapter-check/bin/python -I /absolute/path/verify_adapter.py ``` Replace the wheel placeholders with the complete generated wheel filenames. On Windows, use the virtual environment's `Scripts/python.exe` path. Your verifier should check: - **Import isolation**: `nautilus_trader`, the adapter package, and its backend resolve beneath `sys.prefix`. Editable imports can hide packaging or extension-identity defects. - **Launch modes**: both `node.run()` and hosted `run_async()` work. - **Data delivery**: a quote reaches the strategy and core cache. - **Reconciliation**: reconciliation completes. - **Execution**: a submitted order produces the expected fill quantity, price, commission, and final cached order state. - **Shutdown**: resources close and further adapter output is prevented. Run this installed-wheel check when changing the adapter protocol or the extension's supported NautilusTrader versions. # Releases Source: https://nautilustrader.io/docs/latest/developer_guide/releases/ This guide covers the release process and the standards for writing release notes. ## Overview NautilusTrader uses a three-branch model: - **`develop`**: active development; publishes dev wheels to Cloudflare R2 on every push. - **`nightly`**: pre-release testing; publishes all supported pre-release wheels and CLI binaries. - **`master`**: stable releases; triggers the full release pipeline. Merging a release commit to `master` automatically tags the version from `python/pyproject.toml`, creates a draft GitHub release, uploads release assets, publishes Cargo crates to crates.io, publishes wheels and sdist to PyPI, publishes the GitHub release, builds Docker images, and triggers a docs rebuild. ## Stable release workflow The `build` workflow treats the GitHub release as the anchor for stable releases. It creates the release as a draft first, uploads the wheel and sdist assets to that draft release, and only then publishes those packages to package indexes. The workflow publishes the GitHub release only after the registry verification and final integrity assets are complete. ```mermaid flowchart TD push["Push to master"] wheels["Build wheel artifacts
Linux x86/ARM, macOS, Windows"] audits["Release gates
Rust suite + cargo-deny + cargo-vet
Cargo publish + docs/features preflights"] security["security-audit
Zizmor + supply chain"] tag["tag-release
Create tag and draft GitHub release"] wheel_assets["publish-wheels-master
Upload wheels to GitHub release and R2
release env"] build_sdist["build-sdist
Build sdist workflow artifact"] sdist_asset["upload-sdist-release
Upload sdist to GitHub release"] crates["publish-cargo-crates
crates.io Trusted Publishing
release env"] wheel_pypi["publish-wheels-pypi
Attest and publish wheels to PyPI
release env"] sdist_pypi["publish-sdist-pypi
Attest and publish sdist to PyPI
release env"] integrity["publish-release-integrity
Checksums and registry verification
Attestation siblings and cleanup"] publish_release["publish-github-release
Publish draft release
Verify release attestation"] push --> wheels push --> audits push --> security wheels --> tag audits --> tag security --> tag tag --> build_sdist build_sdist --> sdist_asset tag --> sdist_asset tag --> wheel_assets wheels --> wheel_assets sdist_asset --> wheel_assets wheel_assets --> wheel_pypi wheel_assets --> crates wheel_pypi --> sdist_pypi sdist_asset --> sdist_pypi crates --> integrity wheel_pypi --> integrity sdist_pypi --> integrity tag --> integrity integrity --> publish_release tag --> publish_release ``` Keep these sequencing rules intact when editing `.github/workflows/build.yml`: - The draft GitHub release must exist before any release asset upload or package registry publish. - `tag-release` must depend on `security-audit` so stable release tagging cannot proceed after an audit failure. - Wheel and sdist assets must be attached to the GitHub release before package index publishing starts (`packages.nautechsystems.io`, PyPI, crates.io). - PyPI and crates.io Trusted Publishing jobs must keep `environment: release` and `id-token: write`; those registrations depend on the `release` environment. - Non-OIDC integrity and asset-upload jobs should avoid `environment: release` unless they need release environment secrets or approvals. - `publish-release-integrity` must run after PyPI and crates.io publishing. It generates the release manifest first, verifies registries against that manifest, then attaches final integrity assets only after verification passes. - `publish-github-release` must be the final stable release job. GitHub recommends creating a draft release, attaching all assets, then publishing the draft before enabling release immutability. Once GitHub release immutability is enabled for the repo, published release assets and the release tag cannot be changed; only the title and release notes remain editable. The job verifies the final draft asset set before publishing and verifies GitHub's release attestation after publishing the draft. ## Versioning The project maintains two version numbers: | File | Scope | | ------------------------ | -------------- | | `python/pyproject.toml` | Python package | | `Cargo.toml` (workspace) | Rust crates | These are bumped independently. The Python version drives the `v` release tag. Versions ending in `aN`, `bN`, or `rcN` create a GitHub pre-release; final versions create a normal release. ## Crates.io publishing The `build` workflow publishes Cargo crates from the `publish-cargo-crates` job. The job uses crates.io Trusted Publishing through GitHub Actions OIDC, so it does not use a persistent cargo token. Configure each crate on crates.io with: | Field | Value | | ----------- | ----------------- | | Owner | `nautechsystems` | | Repository | `nautilus_trader` | | Workflow | `build.yml` | | Environment | `release` | Enable Trusted Publishing Only for crates after their trusted publisher is configured. Crates that have never been published still need an initial manual publish before crates.io allows the trusted publisher configuration. Do not use `cargo publish --workspace` for CI releases. The release job runs `scripts/ci/publish-cargo-crates.sh`, which publishes crates one at a time in dependency order, skips versions already present on crates.io, and waits for each new version to appear in the crates.io API and sparse index before publishing dependents. The script fails before uploading if a publishable crate depends on a local `publish = false` crate that is absent from crates.io. Optional local dependencies count as blockers because publishing a public feature that resolves to an absent crate would leave that feature unusable. Post-publish verification treats an existing crate version as `previously_published` only when crates.io shows it was trusted-published by this repository. It still fails for user-published crate versions unless `CRATES_IO_MANUAL_PUBLISH_EXCEPTIONS` names each recovered `crate@version` entry for emergency token-publish recovery. Accepted manual entries are recorded in `crates-manifest.json` with `release_status: "manual_token_publish"`, and malformed or unused exception entries fail the job. Wrong trusted-publishing repositories and checksum or sparse-index mismatches also fail. ## Release checklist ### Pre-release (on `develop`) - [ ] Finalize `RELEASES.md`: review all items, remove empty sections - [ ] Ensure versions are set in `python/pyproject.toml` and the `Cargo.toml` workspace - [ ] Ensure crates.io Trusted Publishing is configured for every crate that CI publishes: `bash scripts/ci/check-crates-io-trusted-publishing.sh` - [ ] Ensure all CI checks pass on `develop` ### Release - [ ] Merge `develop` into `nightly`, verify nightly CI passes - [ ] Merge `nightly` into `master` - [ ] Verify the `build` workflow completes: - Wheels built for Linux x86/ARM, macOS, Windows - `cargo-deny` and `cargo-vet` pass - `security-audit` passes its Zizmor and supply-chain checks - Release docs/features and Cargo publish preflights pass before tagging - Tag and draft GitHub release created - Wheels and sdist attached to the GitHub release before package registry publishing - Cargo crates published to crates.io or skipped because the version already exists - Wheels and sdist published to PyPI - Registry verification passes before release checksums, crates manifest, and attestation siblings are attached - GitHub release published after all release assets and integrity assets are attached - [ ] Verify the `docker` workflow completes (images built and pushed) - [ ] Verify the `build-docs` workflow completes (docs rebuild triggered) ### Post-release (on `develop`) - [ ] Update the release date in `RELEASES.md` for the published version - [ ] Add horizontal separator `---` below the completed release - [ ] Add the next version template at the top of `RELEASES.md` (see below) - [ ] Bump `python/pyproject.toml` version to the next release number - [ ] Bump crate versions in tutorial and how-to `Cargo.toml` snippets (`docs/concepts/rust.md`, `docs/how_to/run_rust_backtest.md`, `docs/how_to/run_rust_live_trading.md`) ## Release notes This section documents the standards for writing release notes in `RELEASES.md`. ### Sections Use the following sections in this order: 1. Enhancements 2. Breaking Changes 3. Security 4. Fixes 5. Internal Improvements 6. Documentation Updates 7. Deprecations Omit sections that have no items for a given release. ### Enhancements New features and user-visible improvements. **Format**: ```markdown - Added `subscribe_order_fills(...)` and `unsubscribe_order_fills(...)` for `Actor` - Added BitMEX conditional orders support - Added support for `OrderBookDepth10` requests (#2955), thanks @faysou ``` **Guidelines**: - Start with "Added". - Use backticks for code elements. - Be specific about what was added, not how. ### Breaking Changes Changes that may break existing code. **Format**: ```markdown - Removed `nautilus_trader.analysis.statistics` subpackage - must import from `nautilus_trader.analysis` - Renamed `BinanceAccountType.USDT_FUTURE` to `USDT_FUTURES` - Changed `start` parameter to required for `Actor` data request methods ``` **Guidelines**: - Start with "Removed", "Renamed", or "Changed". - Explain migration path briefly. ### Security Security hardening and fixes that prevent crashes, undefined behavior, or data corruption. Includes significant hardening improvements elevated from Internal Improvements. **Format**: ```markdown - Fixed divide-by-zero and overflow bugs in model crate that could cause crashes - Fixed core arithmetic operations to reject NaN/Infinity values and improve overflow handling ``` **Guidelines**: - Include overflow/underflow fixes, memory safety improvements, FFI guards, data integrity fixes. - Focus on user impact: what could have happened. - Exclude routine dependency updates, minor hardening, or test-only fixes. - Omit this section entirely if there are no security items for the release. ### Fixes Bug fixes that improve correctness but don't qualify as security issues. **Format**: ```markdown - Fixed reduce-only order panic when quantity exceeds position - Fixed Binance order status parsing for external orders (#3006), thanks for reporting @bmlquant ``` **Guidelines**: - Start with "Fixed". ### Internal Improvements Implementation details and infrastructure changes. **Format**: ```markdown - Added ARM64 support to Docker builds - Ported `PortfolioAnalyzer` to Rust - Improved clock and timer thread safety - Upgraded Rust (MSRV) to 1.90.0 - Upgraded `pyo3` crates to v0.26.0 ``` **Guidelines**: - Use "Added", "Implemented", "Improved", "Optimized", "Upgraded", "Refined", "Standardized". - Include version numbers for dependency upgrades. ### Documentation Updates Changes to guides and examples. **Format**: ```markdown - Added rate limit tables with links to official docs - Improved dark and light themes for readability - Fixed broken links ``` ### Deprecations Features marked for removal. **Format**: ```markdown - Deprecated `some_config_option`; disable (`False`) to maintain consistent behavior. Will be removed in future version ``` **Guidelines**: - Explain migration path and provide alternatives. ## Attribution - Credit external contributors: `thanks @username` or `thanks for reporting @username`. - Include issue/PR numbers for community contributions and complex features: `(#1234)`. ## Style - Use sentence case (capitalize first word only). - Do not end with periods. - Use backticks for code elements. - Focus on **what** changed, not how. **Be specific**: ```markdown Bad: Improved Binance adapter Good: Improved Binance fill handling when instrument not cached ``` ## Security classification Include in Security if the change addresses: - Memory safety (overflow, underflow, divide-by-zero that threatens stability). - Undefined behavior or crashes that could corrupt state. - Data integrity (NaN/Infinity propagation, race conditions leading to corruption). - Input validation preventing injection or exploitation (SQL injection, command injection, path traversal). - Build hardening (non-exec stack, FFI guards). - Significant hardening that users should know about. Otherwise use Fixes (for logic bugs and panics) or Internal Improvements (for minor hardening). Note: Plain logic panics belong in Fixes unless they threaten system stability or data corruption. ## Examples **Security** (could cause crashes/corruption): ```markdown - Fixed divide-by-zero in margin calculations that could crash the engine - Fixed integer overflow in model arithmetic that could crash the process ``` **Fixes** (incorrect but safe): ```markdown - Fixed Binance order status parsing for external orders - Fixed position purge logic to prevent purging re-opened position ``` **Enhancements** (user-facing): ```markdown - Added BitMEX conditional orders support ``` **Internal** (implementation): ```markdown - Implemented BitMEX ping/pong handling ``` ## Release notes template ```markdown # NautilusTrader Beta Released on TBD (UTC). ### Enhancements ### Breaking Changes ### Security ### Fixes ### Internal Improvements ### Documentation Updates ### Deprecations --- ``` # Runtime Conformance Contract Source: https://nautilustrader.io/docs/latest/developer_guide/runtime_conformance/ Use this reference to locate implementation boundaries and representative checks for selected [design principles](design_principles.md). The source baseline is commit `46f87cd1b7af576495418761bbf11db23e89124c`. Source links are relative to this document's revision; use that baseline when reproducing this snapshot. The entries describe Rust source and test coverage. They do not certify every adapter, Python entry point, configuration, or failure mode. The named tests are source references, not a record of a test run. ## Evidence and outcome provenance The [execution policies](../concepts/execution/policies.md#terminal-reconciliation-provenance) distinguish venue evidence from local policy resolution. In the Rust live execution manager, `check_inflight_orders` generates a rejection with reason `INFLIGHT_TIMEOUT` for a submitted order when the configured retry limit expires. Pending updates and cancellations instead generate `OrderCanceled`. These events carry `reconciliation=true`. - **Implementation**: [Execution manager](../../crates/live/src/execution/manager.rs), `check_inflight_orders`. - **Representative checks**: [Manager integration tests](../../crates/live/tests/integration/manager.rs), `test_inflight_order_generates_rejection_after_max_retries`, `test_inflight_pending_update_generates_canceled`, and `test_inflight_pending_cancel_generates_canceled`. - **Limit**: Retry exhaustion does not establish a venue outcome. The reconciliation flag alone does not distinguish a venue report from a local policy resolution, and `OrderCanceled` has no reason field. Consumers need the associated inputs and logs to retain that distinction. ## Callback ordering and ownership The [callback dispatch contract](callback_dispatch.md) requires publication order across recipients and exclusive component access. Private Rust primitives reserve publication order and reject overlapping checked access to an allocation. Production dispatch does not use these primitives. - **Implementation**: [Dispatch](../../crates/common/src/actor/dispatch.rs), `PublicationScope` and `drain`; [allocation access](../../crates/common/src/actor/access.rs), `AllocationGuard`. - **Representative checks**: `nested_publication_reserves_all_outer_recipients` in the dispatch module checks outer-recipient ordering across a nested publication. `test_actor_and_component_views_share_access` in the access module checks exclusion across views. - **Limit**: These checks do not establish production callback ordering or ownership safety. Runtime integration must end enclosing mutable borrows before draining and preserve native, Python, and dynamic-backend lifecycle eligibility. Unchecked access remains outside the private allocation guards. ## Recovery For a Rust live node with execution reconciliation enabled, startup performs reconciliation before starting trader components. A reconciliation error aborts startup. The startup integration test below supplies terminal order and fill reports through a test execution client and checks the recovered quantity, price, trade identity, commission, position quantity, and terminal status. - **Implementation**: [Live node](../../crates/live/src/node/mod.rs), `perform_startup_reconciliation` and its callers. - **Representative check**: [Node integration tests](../../crates/live/tests/integration/node.rs), `test_live_node_startup_recovers_terminal_fill_exactly`. - **Limit**: This check covers report-based startup recovery, not backing-store durability, supervisor restart, or arbitrary panic recovery. Reconciliation can be disabled; event-store replay also skips live client connection and reconciliation. Venue completeness remains subject to the [reconciliation policies](../concepts/execution/policies.md#reconciliation-authority). ## Overload handling The [live runner](../concepts/live.md#dispatch-priority-and-overload-behavior) uses unbounded message channels. Polling priority does not impose producer backpressure or a queue-depth limit. The private callback dispatcher separately enforces retained-count, known-storage, and callback-chain limits. - **Implementation**: [Runner](../../crates/live/src/runner.rs), `AsyncRunner::new` and `recv`; [dispatch](../../crates/common/src/actor/dispatch.rs), admission accounting and `drain`. - **Representative checks**: `test_recv_processes_system_event_before_command` in the runner module checks priority for that channel pair. `event_count_limit_latches_after_exact_capacity` and `progress_limit_persists_between_bounded_drains` in the dispatch module check private limits. - **Limit**: A polling-order test does not prove bounded latency or progress under sustained load. Private callback limits do not bound live runner queues or total process memory. Production overflow handling and safe drain boundaries remain integration requirements; queue monitoring supplies operational signals without automatically throttling feeds or stopping trading. # Rust Source: https://nautilustrader.io/docs/latest/developer_guide/rust/ This page defines NautilusTrader conventions for Rust source, Cargo manifests, PyO3 bindings, and tests. NautilusTrader uses Rust for its mission-critical core because the language combines a strong type system, an ownership model, and predictable performance. Safe Rust prevents data races and many memory errors at compile time. `unsafe` code must make explicit the invariants that the compiler cannot check. `rustfmt` and the workspace lints own general Rust style; the rules below supplement them. ## Sources of truth | Concern | Source | | -------------------------- | -------------------------------------------------- | | Formatting and imports. | `rustfmt.toml`. | | Workspace lints. | `Cargo.toml` and `clippy.toml`. | | Cargo layout. | `.pre-commit-hooks/check_cargo_conventions.sh`. | | Rust layout. | `.pre-commit-hooks/check_formatting_rs.sh`. | | Nautilus type conventions. | `.pre-commit-hooks/check_nautilus_conventions.sh`. | | Async boundaries. | `.pre-commit-hooks/check_tokio_usage.sh`. | | DST boundaries. | `.pre-commit-hooks/check_dst_conventions.sh`. | | PyO3 bindings. | `.pre-commit-hooks/check_pyo3_conventions.sh`. | | Anyhow. | `.pre-commit-hooks/check_anyhow_usage.sh`. | | Error names. | `.pre-commit-hooks/check_error_conventions.sh`. | | Logging. | `.pre-commit-hooks/check_logging_conventions.sh`. | | Rustdoc contracts. | `.pre-commit-hooks/check_docs_conventions.sh`. | | Test style. | `.pre-commit-hooks/check_testing_conventions.sh`. | | Workspace test selection. | `scripts/ci/check-workspace-test-coverage.sh`. | Match nearby code when the tools do not settle a choice. Change generator inputs and regenerate outputs instead of editing generated files directly. ## Module layout Arrange each hand-written module so its primary behavior appears before its supporting implementation details. ### Module roots and declarations Preserve the surrounding module-root style when adding child modules. In each contiguous declaration block in a `mod.rs` file, order out-of-line module declarations by these sections: 1. `#[macro_use]` modules. 1. Public modules. 1. Restricted modules such as `pub(crate)`. 1. Non-test `#[cfg(...)]`-gated modules. 1. Private modules. 1. Test-only modules. Alphabetize declarations within each section and leave one blank line between sections. The formatting hook enforces this order in `mod.rs` files. Use the narrowest visibility that serves the caller. The workspace denies unreachable `pub` items. ### Imports Keep imports at the top of the file or module. Use a local import only when its narrow scope materially improves clarity. ### Constants and global state Group module-wide constants and global state near the top of the module. Put `const` items before `static` and `thread_local!` declarations. Keep a narrowly used constant or static next to its consumers instead. ### Primary types and implementations Keep the primary type and its inherent implementation near the top of a module. ### Enums Place enums by role rather than collecting them in one module-wide block: - Keep a primary or public enum with the module's primary types. - Keep a small state enum next to the global whose state it represents. - Place a private supporting enum below its first caller. ### Supporting definitions Place private functions and types below their callers. In adapters, place private route types, decision enums, and parsing functions below the main client implementations. ### Box-style banner comments Do not add box-style banner or separator comments to divide module contents. Use modules and implementation blocks to express structure. The standard copyright and license header is the exception. ## Cargo manifests ### Dependencies and sections - Use workspace inheritance for shared dependencies, for example `serde = { workspace = true }`. Pin a version in a crate only when the dependency is not workspace-managed. - Separate dependency groups with a blank line and alphabetize each group. Manifests normally group internal `nautilus-*` crates, required external crates, and optional external crates, but preserve a manifest's meaningful local groups. - Keep optional crates in their own blank-line group, except when every crate in the group is a `nautilus-*` crate. - Keep the standard section order: package, lints, library, features, `cargo-machete` metadata, docs.rs metadata, dependencies, development dependencies, build dependencies, benches, binaries, examples, and tests. - Add `[lints] workspace = true` to every workspace crate with a library or binary target. - Keep adapter dependencies in the `# Adapter dependencies` section of the workspace `Cargo.toml`. Core crates must not depend on entries from that section. - Keep related dependency families compatible. The Cargo convention hook checks the known constraints, including `capnp` with `capnpc`, `arrow` with `parquet`, and `dydx-proto` with `prost` and `tonic`. - List only declared dependencies under `[package.metadata.cargo-machete] ignored`. - Remove a root `[workspace.dependencies]` entry when no crate uses it. Cargo tools kept only for CI are exempt from this check. - Remove a root `[workspace.package]` field when no crate inherits it. - List each `[workspace] members` entry as a literal path. The convention hook resolves member manifests directly and does not expand Cargo glob members. - Obtain `libfuzzer-sys` in adapter crates through `nautilus-live`; do not add it directly to an adapter manifest. When `crates/pyo3/Cargo.toml` groups adapters separately, keep its `# Adapters` block below the core internal crates. ### Package fields Crate `[package]` sections use this canonical prefix. Cargo infers `README.md` next to the manifest, so omit `readme`. The other fields shown are required. ```toml [package] name = "nautilus-example" version.workspace = true edition.workspace = true rust-version.workspace = true authors.workspace = true license.workspace = true description = "Example crate for NautilusTrader" categories.workspace = true keywords.workspace = true documentation.workspace = true repository.workspace = true homepage.workspace = true ``` Place the optional `publish`, `build`, and `include` fields after `homepage.workspace`. ### Features - Preserve the crate's existing default feature contract. Most core crates have empty defaults, while many adapter crates enable `high-precision` by default. - Include `"python"` in every `extension-module` feature that builds a Python artifact. Keep it next to `"pyo3/extension-module"`. - Propagate `high-precision` to dependent Nautilus crates that store or construct fixed-point domain values. - Document each public non-default feature once, in alphabetical order, under `## Feature flags` in the crate README and `# Feature Flags` in the crate-level library docs. ### Targets - Use snake_case filenames for `bin/` sources and kebab-case executable names, for example `path = "bin/ws_data.rs"` and `name = "hyperliquid-ws-data"`. - Set `doc = false` on binary and example targets. Also set `test = false` on binary targets. - Order library crate types as `rlib`, `staticlib`, then `cdylib` when a crate produces more than one type. ## File header requirements Copy the standard copyright and license header from a neighboring hand-written Rust file. Generated files retain their generator header instead. The copyright hook checks the year. Change a generator input and rerun the generator instead of editing generated Rust, C headers, Python stubs, or wrapper doc comments. ## Formatting and attributes `rustfmt` groups standard library, external crate, and local imports, then alphabetizes each group. Run the formatter instead of ordering imports by hand. Leave one blank line: - Between functions, including tests. - Above each `///` or `//!` doc comment. - Above standalone `if`, `match`, `for`, `while`, and `loop` expressions. - Above task spawn calls, including `spawn_local` and `spawn_blocking`. - Before a `let` that contains a multiline braced expression, unless it starts a block. - Before a multiline struct literal or a direct qualified `Type::new(...)` call, unless it starts a block. - After a statement that contains a multiline braced expression, before the next statement in the same block. The control-flow and spawn rules do not apply when the expression starts a block, continues the previous operation, or has an attached comment or attribute. Wrapped calls and method chains alone do not need a separator. Construction spacing also applies to assignments, explicit returns, and calls followed by `?` or `.await`. Construction must span multiple lines after rustfmt layout; one-line initializers stay together. Keep comments and attributes attached to their statement when inserting a separator. Skip macro bodies and `rustfmt::skip` regions. Apply the multiline statement rules around changed code; leave unrelated code alone. Add only blank lines that `rustfmt` preserves. The formatting hook: - Checks existing control-flow, spawn, and module-ordering rules at changed boundaries. - Compares staged and unstaged changes against `HEAD`, or against the merge base with `CHANGED_BASE_SHA` when set. - Falls back to checking all tracked Rust files when a CI base is unavailable. - Reads complete changed files for context and includes lines used by exemption checks when selecting diagnostics. - Does not modify files. These remain review conventions: - Multiline statement spacing. - Construction spacing. - `spawn_local` and `spawn_blocking` spacing. Keep changed code readable for a human reader, and add further blank lines when those separators still leave a dense block hard to follow. Use inline format arguments for existing variables: ```rust anyhow::bail!("Failed to subtract {n} months from {datetime}"); ``` Add `#[must_use]` to constructors, accessors, pure conversions, and consuming `with_*` methods when discarding the returned value is almost certainly a mistake. A return type such as `Result` already carries its own `must_use` annotation. When suppressing `missing_panics_doc` or `missing_errors_doc`, include a reason that explains why the lint does not apply: ```rust #[allow( clippy::missing_errors_doc, reason = "result type is retained for API compatibility but no error is returned", )] ``` ## Type qualification | Item | Convention | | ----------------------------- | --------------------------------------------------------------------------- | | `anyhow`. | Import only `anyhow::Context`; fully qualify macros and `Result`. | | Nautilus domain types. | Import the type, then use its short name. | | Tokio time, sync, and tasks. | Fully qualify the path; use `std::time::Duration`. | | `Debug` and `Display`. | Import the trait. | | `Formatter` and `fmt::Result` | Use `std::fmt::Formatter` and `std::fmt::Result` at the implementation. | | Nautilus macros. | Import `nautilus_actor!` or `nautilus_strategy!`, then call it unqualified. | Use `debug_struct(stringify!(TypeName))` in manual `Debug` implementations. Use `// nautilus-import-ok` only where a macro, generated path, or conditional import requires a fully qualified Nautilus type. Put the marker on the affected line or directly above the narrow block. ## Error handling and contracts ### Design by contract Use the narrowest mechanism that expresses the contract: | Situation | Mechanism | | -------------------------------------------------- | -------------------------------------------- | | Compile-time state or ownership rule. | Types, lifetimes, newtypes, and visibility. | | Public input precondition. | `check_*` from `nautilus_core::correctness`. | | Validated value construction. | `new_checked()` with a `new()` wrapper. | | Recoverable parse, I/O, or network failure. | `Result`. | | Internal invariant safe to omit in release builds. | `debug_assert!`, covered by a targeted test. | | Soundness or always-on invariant. | `assert!` or a checked error path. | Place an assertion where the code first relies on the invariant. Never use `debug_assert!` for public input validation or a soundness condition because release builds remove it. Prefix debug assertion messages with `Invariant:` and state the positive rule. The shared `Condition failed: ...` prefix marks a caller input violation; `Invariant: ...` marks an internal contract bug. ### Error boundaries Choose the error type at the API boundary: | Boundary | Return type | | ------------------------------------- | ----------------------------------- | | Reusable library or domain API. | A typed `Result`. | | Application or adapter orchestration. | `anyhow::Result`. | | Public input validation. | `CorrectnessResult` when suited. | - Define a typed error with `thiserror` when callers inspect or recover from the failure. - Bind error patterns and closures as `e`, not `err` or `error`. - Use `anyhow::bail!` for an early return from an `anyhow::Result` function. Use `anyhow::anyhow!` when an error value is required, such as inside `ok_or_else`. - Start `.context()` messages with lowercase text so chained errors read naturally. Preserve the capitalization of a leading proper noun or acronym. - Do not use `", got"` in an error or assertion. Use `", was"`, `", received"`, or `", found"` according to the context. ```rust parse_timestamp(value).context("failed to parse timestamp")?; connect().context("BitMEX websocket did not become active")?; ``` ### Panic policy Panics are valid for internal invariant violations that callers cannot reasonably recover from. Keep an API infallible when returning `Result` would only require callers to handle a programming defect. Use an always-on assertion for an invariant that protects soundness or prevents unsafe code from continuing with invalid state. Return `Result` at boundaries that accept untrusted input or can fail because of configuration, I/O, external data, task cancellation, or resource lifecycle. These are operational failures even when a specific call site expects them never to occur. For validated values, follow the [`new()` and `new_checked()` constructor pattern](#constructor-patterns): expose a fallible path for untrusted values and use the convenience wrapper where its caller contract permits a panic. Document reachable panics in public Rust APIs, and make each panic message name the violated invariant. Treat findings for these lints from `make clippy-strict-audit` as review prompts: - `clippy::panic` - `clippy::unwrap_used` - `clippy::expect_used` The audit also reports `clippy::panic_in_result_fn`, which the required workspace Clippy gate already enforces. Remove a panic when the failure is recoverable; retain a justified invariant panic, with a scoped lint reason when needed. The audit uses forced warnings, so its totals include deliberate sites with local lint allowances and diagnostics from macro expansions. ### Failure contract examples APIs with a documented panic contract use panics for: - Programmer errors (logic bugs, incorrect API usage). - Data that violates fundamental invariants (negative timestamps, NaN prices). - Arithmetic that would silently produce incorrect results. APIs return `Result` or `Option` when callers, including downstream crates, can handle a failure or absence, including: - Expected runtime failures (network errors, file I/O). - Business logic validation (order constraints, risk limits). - User input validation. The API determines how an invalid operation fails: ```rust let later = timestamp + duration_ns; // Panics on overflow. let price = Price::new_checked(f64::NAN, precision); // Returns Err. let later = timestamp.checked_add(duration_ns); // Returns None on overflow. ``` This policy is implemented throughout the core types (`UnixNanos`, `Price`, `Quantity`, etc.) and helps NautilusTrader maintain strong data correctness for production trading. The repository release profile sets `panic = "abort"`, so a panic terminates the process for a supervisor or orchestration system to handle. Downstream Rust binaries control their own release profile. ## Logging - Fully qualify log macros, for example `log::debug!` and `log::info!`. - Start messages with a capitalized word and omit terminal periods. - Use `// log-period-ok` on the call or within three lines above it when a terminal period is part of the logged value rather than sentence punctuation. - Keep connection and client lifecycle, reconnection, reconciliation, and mass-status summaries at `INFO`. - Keep subscription detail, per-order confirmations, instrument counts, authentication, and WebSocket internals at `DEBUG`. - Log a warning or error when an unexpected, user-actionable, or data-loss condition is handled locally. Do not log an error that continues to propagate through `?` or `anyhow::bail!`. - Leave a blank line above a log call unless it is the first line of the function. - Do not write to stdout or stderr or terminate the process from production library code. Binaries, examples, benches, tests, adapters, the CLI, and testkit may control their process when that is part of their role. ## Async code Synchronous core crates do not take Tokio as a regular dependency. The `common` crate keeps Tokio optional. The Tokio convention hook owns the exact crate list. Adapter production code uses the shared runtime because calls may arrive from Python threads without a thread-local Tokio context: ```rust use nautilus_common::live::get_runtime; get_runtime().spawn(async move { run_client().await; }); ``` - Import `get_runtime` through `nautilus_common::live`, not `live::runtime`. - Use `get_runtime().block_on()` when synchronous adapter code must call an async function. - Use the runtime supplied by `#[tokio::test]` in tests; `tokio::spawn()` is valid there. - Put `// tokio-import-ok` on an import or spawn line only when the fully qualified form or shared runtime cannot serve that site. - Install a custom runtime before `LiveNode::build()` or any adapter use. Build a multi-threaded runtime with all drivers enabled. - `set_runtime()` bypasses the default initializer, including Python initialization. With the `python` feature enabled, initialize Python before installing a custom runtime or keep the default runtime. Code on the deterministic simulation path follows the [DST determinism contract](../concepts/dst.md#determinism-contract). Route clocks, random values, task spawning, and network access through the project seams. Use `biased;` in `tokio::select!` blocks on that path. ## Runtime ownership and access ### Cache order access `Cache` hides its `SharedCell` order storage behind lifetime-scoped accessors. Use `order_ref()` or `try_order_ref()` for scoped reads, `order_mut()` for an exclusive write borrow, and `order_owned()` or `try_order_owned()` when a snapshot must cross a boundary. The `try_*` forms return `OrderLookupError` when the order is absent. `order_mut()` requires `&mut Cache`, so adapter-facing `CacheView` code cannot mutate orders. Drop an `OrderRef` or `OrderRefMut` before dispatching events or taking a borrow that can re-enter the same order, then look up the order again for post-event state. ### Runtime invariants The actor registry, component registry, and message bus use `thread_local!` storage. Objects registered on one thread are not visible from another. `LiveNodeHandle` is the intended cross-thread control surface. Keep an `ActorRef` within one synchronous scope. Do not store it in a struct, hold it across `.await`, or send it to another thread. Capture the actor ID in long-lived callbacks and look up the actor each time the callback runs. The component registry rejects aliased mutable access with a scoped borrow guard. Do not make component lifecycle operations re-entrant. ## Construction and conversion ### Constructor patterns Validated value types pair a fallible `new_checked()` with a convenience `new()`: ```rust pub fn new_checked>(value: T) -> CorrectnessResult { let value = value.as_ref(); check_valid_string_ascii(value, stringify!(value))?; Ok(Self(Ustr::from(value))) } pub fn new>(value: T) -> Self { Self::new_checked(value).expect_display(FAILED) } ``` Use the shared `FAILED` constant with `CorrectnessResultExt::expect_display` so panic messages use the standard `Condition failed: ...` prefix. Document the error on `new_checked()` and the panic on `new()`. For types with long constructors dominated by optional fields, use a `bon` builder instead of public positional `new()` and `new_checked()` methods. Put `#[bon::bon]` on the inherent implementation and make the builder's finish method delegate to a private validation function or perform the validation directly. Keep one validation and defaulting path. ```rust #[builder(start_fn = builder, finish_fn = build)] pub fn build_checked(/* same inputs as new_checked */) -> CorrectnessResult { Self::new_checked(/* forward inputs */) } ``` Required fields remain required in `bon` typestate. Optional fields remain omittable, and `build()` returns the same `CorrectnessResult` as the internal validation path. ### Conversion patterns - Implement `From` only for infallible, complete conversions. - Implement `TryFrom` when the source can contain an invalid or unrepresentable value. - Implement `FromStr` for string parsing. - Treat generic `From>` implementations that panic as compatibility surfaces. Do not copy that pattern into new APIs. - Keep venue wire enums distinct from Nautilus domain enums. Use idiomatic Rust variant names, express the wire spelling with `serde` or `strum` attributes, and convert explicitly at the boundary. - Deserialize adapter payloads into wire models before constructing domain objects. Keep parsing and validation in conversion functions instead of embedding venue wire details in domain types. ### Domain numeric types Preserve discrete financial values as decimals from ingestion: | Value | Type and construction | | ---------------------------------------- | -------------------------------------------------------- | | Price or quantity. | `Price::from_decimal_dp` or `Quantity::from_decimal_dp`. | | Money, fee, margin, or balance. | `Decimal`, then `Money::from_decimal` or `Money::zero`. | | Continuous signal ratio or timing curve. | `f64` when decimal precision has no domain meaning. | Do not route wire values through `f64` constructors. In tests, compare `.as_decimal()` with `dec!(value)`. ## Identifier storage ### How string interning works String interning stores one shared copy of each distinct string in a central cache. Repeated values refer to the same cached bytes instead of allocating another copy. Small handles make the values cheap to copy and compare, while a cached hash avoids reading the full string again during hashing. NautilusTrader uses `Ustr` for its interned identifier components. Each `Ustr` is a pointer-sized `Copy` handle with a precomputed hash and stable direct string access. Composite types such as `InstrumentId` preserve the same cheap copy semantics by storing these handles. ### Reclamation boundary The string cache retains every unique value for the process lifetime. This retention keeps copied handles and returned string slices valid without reference counting, access guards, or explicit lifetime parameters on identifier types. Process teardown is the normal reclamation boundary. These guarantees rule out safe reclamation of individual entries. Rust can copy a `Copy` value without executing code, so an atomic reference count cannot observe every copy. Designs that add reclamation change the identifier contract: - Reference counting requires `Clone` and `Drop`, which removes `Copy` from identifiers and types that contain them. - Borrowed or epoch-protected storage requires lifetimes or access guards at string access points. - Generational handles permit reclamation but make lookup fallible and invalidate stale handles. - A global cache reset is safe only at a proven quiescent point after all handles, references, and foreign pointers have been destroyed and no task or thread can retain one. ### Storage boundaries Interning is best suited to identifiers drawn from a bounded process-scoped universe and values that repeat enough to benefit from deduplication. Identifiers whose distinct values can grow with every order, trade, or message increase the cache for the process lifetime. Fixed-capacity inline storage retains `Copy` when the external protocol supplies a suitable maximum. `TradeId`, for example, uses a 36-character `StackStr`. Owned or reference-counted storage provides dynamic capacity when reclamation matters more than `Copy`. The domain model also contains compatibility exceptions. `ClientOrderId`, `VenueOrderId`, `PositionId`, and `OrderListId` remain `Ustr`-backed and therefore retain every distinct value. Identifier storage participates in the supported by-value C ABI, so a broader redesign depends on conversion-based bindings replacing raw layout sharing. The storage boundary includes an up-front estimate of every unique value and all intermediate strings interned during parsing. The cache is shared by every `Ustr` use in the process, so its memory cost is the aggregate set rather than a separate budget for each identifier type. ### Polymarket scale example A Polymarket instrument symbol combines a 66-byte condition ID with a 77- or 78-byte token ID, for a 144- or 145-byte interned symbol. With the 64-bit `ustr` 1.1.0 layout, 600,000 unique `InstrumentId` values require roughly 150 MiB for the retained identifier values, cache lookup table, and reserved string storage. The Polymarket parsing path also interns each raw token ID and each condition ID. For 600,000 instruments from about 300,000 markets, these entries raise the estimate to roughly 300 MiB before instrument objects, descriptions, maps, and other metadata. The estimate assumes unique instrument and token IDs and includes capacity reserved by the cache's geometric allocator, so it is not an exact resident-set measurement. NautilusTrader accepts this bounded cost to preserve `Copy`, stable direct access, and global deduplication across the instrument universe. Unbounded streams of unique external IDs remain outside this storage model. ## Collections Choose a hash collection by iteration semantics and trust boundary: | Requirement | Collection | | ---------------------------------------------- | ------------------------- | | Observable insertion-order iteration. | `IndexMap` or `IndexSet`. | | Hot lookup with no observable iteration order. | `AHashMap` or `AHashSet`. | | Keys chosen by an untrusted third party. | `HashMap` or `HashSet`. | | External API requires a standard collection. | `HashMap` or `HashSet`. | Default `AHashMap` and `AHashSet` instances randomize their hasher state, so their iteration order is not stable. When iteration order affects emitted events, commands and traffic sent to a venue, persisted output, or random number consumption, make the order deterministic. An iteration whose order no caller observes does not need stabilization. Use `IndexMap` or `IndexSet` when insertion order is both deterministic and the required sequence. Otherwise, keep the hash collection and sort at the point of use. Prefer sorting when ordered traversal is infrequent or order-preserving removal is hot. `shift_remove` preserves relative order in O(n) average time; `swap_remove` runs in O(1) average time but can move the last entry. `AHash` is not cryptographically secure. Use `HashMap` or `HashSet` where untrusted keys make hash-flooding resistance part of the security boundary. ## Documentation ### Coverage and tone - Add doc comments to public items. Document private behavior with a normal comment only when non-obvious context prevents a likely misreading. - Add module documentation to public modules and modules with a non-obvious contract. Do not add boilerplate to a private leaf module. - Use the indicative mood: "Returns the account ID", not "Return the account ID". - End public field and enum variant documentation with a period. - Match documentation density to neighboring items. Do not add filler comments to make a bare block look uniform. - Put important context for private fields in the type-level documentation instead of documenting each field. Document public functions when their contract is not fully clear from the type and name. Cover fallible conditions, panic conditions, safety obligations, and non-obvious input semantics. ### Rustdoc sections Use Title Case for Rustdoc section headings: - `# Examples` - `# Errors` - `# Panics` - `# Safety` - `# Notes` - `# Thread Safety` - `# Feature Flags` Use one sentence for a single error or panic condition: ```rust /// # Errors /// /// Returns an error if the currency conversion fails. ``` Use bullets with terminating periods for multiple conditions: ```rust /// # Errors /// /// Returns an error if: /// - The market price cannot be found. /// - The conversion rate calculation fails. ``` Use `# Errors` only on a function that returns `Result`, `PyResult`, or `Option`. Use `# Panics` only when the function can panic, and remove the section instead of saying that the function does not panic. The documentation hook recognizes direct panic sites in a `Result`-returning function. Put `// panics-doc-ok` immediately above the doc block when a called function supplies the documented panic. Use `// errors-doc-ok` in the same position only for a special error contract that the signature check cannot recognize. ### Doc examples `make cargo-test-doc` compiles and runs doc examples. Annotate every fence: | Fence | Behavior | Use for | | -------------- | ------------------------- | ------------------------------------------------------ | | `rust` | Compiled and run. | Self-contained examples with no external dependencies. | | `rust,no_run` | Compiled, not run. | Examples needing a catalog, network, or venue. | | `ignore` | Neither compiled nor run. | Pseudocode; state why it cannot compile. | | `compile_fail` | Must fail to compile. | Demonstrating a rejected usage. | | `text` | Not code. | Directory trees, output, or diagrams. | | `bash`, `json` | Not Rust. | Commands or payloads. | Prefer `no_run` to `ignore` when only the runtime dependency is unavailable. Prefix setup lines with `#` when they must compile but would obscure the rendered example. Put examples on public items. Rustdoc also collects fences from private item docs, but those examples cannot import the item they document. ## Python bindings ### PyO3 names and errors - Prefix a Rust function renamed with `#[pyo3(name = "...")]` with `py_`. - When a binding needs a Rust-only wrapper type, prefix it with `Py` and expose the Python name without that prefix. - Use `nautilus_trader.adapters.` for public adapter stub metadata. Runtime module paths use `nautilus_trader._libnautilus.`. - Convert standard Python exceptions with `to_pyvalue_err`, `to_pytype_err`, `to_pyruntime_err`, `to_pykey_err`, `to_pyexception`, or `to_pynotimplemented_err` from `nautilus_core::python`. The submodules registered in `crates/pyo3/src/lib.rs` are public API. Do not add an internal crate as a new Python submodule as a refactor side effect. Register an intentional class in an existing submodule when that preserves the public package structure. A deliberate submodule change also updates the allowlist in `check_nautilus_conventions.sh`. Keep each submodule registration as `let n = ""` followed by one `pyo3::wrap_pymodule!()` call. The name must match the final component of the target path. ### PyO3 enums Nautilus domain integer enums use `frozen`, `eq`, `eq_int`, `from_py_object`, and `rename_all = "SCREAMING_SNAKE_CASE"`. Do not add PyO3's `hash` attribute to an `eq_int` enum. Its generated hash differs from Python's hash for the equal integer discriminant and breaks the rule that equal values have equal hashes. Return the discriminant directly instead: ```rust #[pymethods] impl MyEnum { const fn __hash__(&self) -> isize { *self as isize } } ``` ### Type stub annotations Every Python-exposed type and function needs the matching `pyo3-stub-gen` annotation: | PyO3 construct | Stub annotation | | ----------------- | ----------------------------------------------- | | `#[pyclass]` | `pyo3_stub_gen::derive::gen_stub_pyclass`. | | Enum `#[pyclass]` | `pyo3_stub_gen::derive::gen_stub_pyclass_enum`. | | `#[pymethods]` | `pyo3_stub_gen::derive::gen_stub_pymethods`. | | `#[pyfunction]` | `pyo3_stub_gen::derive::gen_stub_pyfunction`. | - Put class and enum stub annotations in `#[cfg_attr(feature = "python", ...)]` directly below the runtime `pyo3::pyclass` attribute. - Put `gen_stub_pymethods` directly below `#[pymethods]`. - Put `gen_stub_pyfunction` after doc comments and directly above `#[pyfunction]`. - Set the stub `module` to the package from which Python imports the object. - Add `pyo3-stub-gen` as an optional dependency and include it in the `python` feature. ### Generated Python artifacts The Python surface commits generated `.pyi` files under `python/nautilus_trader/` and generated wrapper doc comments under `crates/**/src/python/`. Regenerate both with: ```bash make py-stubs ``` Run the target after changing a Python-exposed Rust item, its stub annotation, its core doc comments, or adapter feature wiring. Commit every changed generated artifact with the source change. The stub generator removes `extension-module` before invoking Cargo. Add a feature that is enabled only through `extension-module` explicitly to `cargo_features` in `python/generate_stubs.py`; otherwise its exported types disappear from the generated stubs. The Interactive Brokers `gateway` feature is the model. The Python targets accept the uv minor series defined by `required-version` in `python/pyproject.toml`. `make sync`, `make py-stubs`, and `make build-debug` stop when the installed version falls outside that range. Run `make update-uv` to install the exact project version from `.nautilus-engineering/tools.toml`. Do not edit wrapper `///` comments in `crates/**/src/python/`. Edit the core Rust item docs and regenerate. The doc sync: - Preserves `# Errors` and `# Safety`. - Drops `# Panics` so a panic contract does not cross the Python API boundary. - Removes Rust intra-doc links. - Converts Rust `::` paths to Python `.` paths. ### Rust-Python object ownership `Py` owns a reference to a Python object. Clone it with `Py::clone_ref` while attached to the interpreter or with `nautilus_core::python::clone_py_object`. An extra `Arc>` is normally unnecessary because `Py` already provides shared ownership. Cloning a Python reference does not break a cycle. Use a Python weak reference when a back-reference must not keep its target alive. ## Testing conventions - Use `mod tests` for ordinary inline tests. - Use `#[rstest]` instead of `#[test]`, including for a non-parameterized test. - Use `#[tokio::test]` for a non-parameterized async test. - Keep `#[cfg(test)]` on test modules and test-only files. Do not add test behavior or interfaces to production code. - Store JSON fixtures under the crate's `test_data/` directory and load them with `include_str!`. - Use distinct, non-default inputs and exact expected values. Assert every stable field. - Group assertions after setup and actions unless the test checks stepwise state changes. - Do not use Arrange, Act, Assert separator comments. Every workspace member appears exactly once in `CORE_CRATES`, `ADAPTER_CRATES`, or `NO_TEST_CRATES` in the `Makefile`. Add a crate to `NO_TEST_CRATES` only when it defines no Rust test target and contains no `#[test]`, `#[rstest]`, or `#[test_case]` function. Parametrize cases when the same behavior applies to several inputs: ```rust #[rstest] #[case("AUDUSD", false)] #[case("CL.FUT", true)] fn test_symbol_is_composite(#[case] input: &str, #[case] expected: bool) { assert_eq!(Symbol::new(input).is_composite(), expected); } ``` ### Test specs Events with many constructor arguments use a fluent `bon` spec next to the event under `events/order/spec/`. Gate the module with `#[cfg(any(test, feature = "test-support"))]` so downstream tests can opt in without adding the spec to production builds. - Derive `bon::Builder` with `finish_fn = into_spec`. - Give required fields deterministic valid defaults. Leave optional fields as `Option`. - Default event IDs with `test_uuid()` from `crate::stubs`. - Implement `build()` by forwarding through the production constructor. - Return the event directly because spec defaults are valid by construction. - Pin every default in one test in the spec module. Override only the fields relevant to the caller: ```rust let fill = OrderFilledSpec::builder() .last_qty(Quantity::from(50_000)) .trade_id(TradeId::from("TRADE-1")) .build(); ``` Under plain `cargo test`, call `reset_test_uuid_rng()` before a test that compares UUID sequences. `cargo nextest` starts each test in a fresh process, so the sequence resets automatically. ### Property-based tests Use `proptest` when an invariant spans an input class that examples cannot cover. Keep strategies near the property suite and combine ranges with explicit edge cases. Prefix property names with `prop_` and retain `#[rstest]` on tests inside `proptest!`. A large suite may use a `property_tests` module or a dedicated file, but the repository also keeps focused properties beside their unit tests. ## Unsafe Rust Unsafe code must make its proof obligations reviewable: - Give each unsafe function a `# Safety` section that states the caller's complete obligations. - Put a `SAFETY:` comment directly above each unsafe operation and explain why its preconditions hold at that site. - Wrap each unsafe operation in its own `unsafe { ... }` block. Workspace crates deny `unsafe_op_in_unsafe_fn`. - Use always-on checks for null, alignment, provenance, and other soundness conditions. - Add targeted tests for observable behavior around unsafe code. Tests support the proof but do not establish soundness. - Treat an unsafe `Send` or `Sync` implementation as a proof over all reachable state, aliases, callbacks, generic parameters, safe methods, cloning, and destruction. For raw vectors crossing FFI, follow the [FFI memory contract](ffi.md). Foreign code owns the allocation after transfer and must call the matching `vec_drop_*` function exactly once. ## Other generated artifacts ### FFI bindings and precision Only `nautilus-core` and `nautilus-model` expose an `ffi` feature. Check both crates directly when changing their C ABI: ```bash cargo check -q -p nautilus-core --features ffi cargo check -q -p nautilus-model --features ffi,python,high-precision ``` The crate-local `cbindgen.toml` files define the header layout for native consumers. Do not add an `ffi` feature or `src/ffi` module to another workspace crate. Run `make check-cbindgen-abi` to generate both headers, verify their public names and compatibility enum values, and compile a C11 consumer against them. The scheduled nightly tests run this check. ### Cap'n Proto schemas Schema files live under `crates/serialization/schemas/capnp/`, and generated Rust lives under `crates/serialization/generated/capnp/`. - Add fields at the end. - Do not remove fields or reuse field numbers. Mark obsolete fields as deprecated in comments. - Regenerate with `make regen-capnp`. - Review `git diff crates/serialization/generated/capnp`. - Run `make check-capnp-schemas` to verify the checked-in output. - Test the serialization crate with `make cargo-test-crate-nautilus-serialization`. Install the pinned compiler as described in [Environment setup](environment_setup.md#capn-proto). Generated bindings stay checked in so docs.rs can build without the compiler. # Security Architecture Source: https://nautilustrader.io/docs/latest/developer_guide/security/ This page describes the security model for the NautilusTrader release pipeline. It explains how release artifacts are built, published, attested, and verified. Use this page with: - [Supply chain policy](https://nautilustrader.io/security/supply-chain/), which states the public dependency and release integrity commitments. - [Releases](releases.md), which documents the release workflow and checklist. - [Security Policy](../../SECURITY.md), which gives consumer-facing verification commands. - [GitHub Actions overview](../../.github/OVERVIEW.md#security), which documents CI/CD controls. ## Security goals The release pipeline has four goals: - Build every official artifact from a reviewed repository commit. - Publish Python and Rust packages without long-lived package registry tokens. - Attach checksums, manifests, and provenance before publishing the GitHub release. - Give users enough public data to verify that downloaded artifacts match the release. The GitHub release anchors package integrity. Stable releases attach wheel and sdist assets to a draft GitHub release before any package index publish starts. The pipeline publishes package indexes, verifies those indexes against the GitHub release assets, attaches final integrity assets, then publishes the GitHub release. ## Threat model The pipeline defends against: - Compromised or mutable third-party GitHub Actions, by pinning actions to commit SHAs. - Accidental release from the wrong workflow, branch, or environment, by binding OIDC publishers to `nautechsystems/nautilus_trader`, `build.yml`, and the `release` environment. - Long-lived package registry token theft, by using PyPI and crates.io Trusted Publishing. - Registry propagation lag or partial re-runs, by making publish and verify scripts idempotent and retry-tolerant. - Registry substitution or upload drift, by comparing PyPI and crates.io artifacts against release manifests and registry metadata. - Silent manual crate recovery, by requiring explicit `CRATES_IO_MANUAL_PUBLISH_EXCEPTIONS` entries and recording those exceptions in `crates-manifest.json`. The pipeline does not defend against: - A malicious maintainer with permission to change release workflows and approve releases. - A compromise of GitHub, PyPI, crates.io, or Sigstore that can forge the trust roots users rely on. - A compromised end-user machine before verification runs. - Runtime compromise of an exchange, broker, data provider, or user trading strategy. - Bit-identical rebuild drift for wheels and sdists. The current guarantee is provenance and digest verification, not reproducible builds. ## Trust roots - GitHub repository rules protect reviewed source, release branches, and release tags. Protected `master` and immutable `v*` release tags are the relevant records. - GitHub Actions OIDC issuer provides short-lived workflow identities from `https://token.actions.githubusercontent.com`. - GitHub `release` environment gates package publishing and release approvals. The environment restricts deployment to `master` and requires reviewer approval. - PyPI Trusted Publishing publishes wheels and sdist without a persistent token. It binds to repository `nautechsystems/nautilus_trader`, workflow `build.yml`, and environment `release`. - crates.io Trusted Publishing publishes Rust crates without a persistent token. It binds to owner `nautechsystems`, repository `nautilus_trader`, workflow `build.yml`, and environment `release`. - Sigstore Fulcio, Rekor, and TUF bind artifacts to OIDC identities and the transparency log. GitHub artifact attestations, PyPI publish attestations, and Docker cosign signatures rely on this root. - GitHub release immutability prevents post-publish asset and tag replacement. Published release assets and the release tag become immutable. ## Release flow ```mermaid flowchart TD source["Reviewed commit on master"] gates["Release gates
Rust suite + cargo-deny + cargo-vet
Cargo publish + docs/features preflights"] wheels["Build wheels"] draft["Create tag and draft GitHub release"] sdist["Build sdist"] assets["Attach wheels and sdist to draft release"] registries["Publish PyPI and crates.io
Trusted Publishing"] verify["Verify registries against release assets"] integrity["Attach SHA256SUMS, manifests,
Sigstore bundles, DSSE envelopes"] publish["Publish GitHub release"] release_attest["Verify GitHub release attestation"] docker["Build, sign, and attest Docker images"] source --> gates source --> wheels gates --> draft wheels --> draft draft --> sdist sdist --> assets wheels --> assets assets --> registries registries --> verify verify --> integrity integrity --> publish publish --> release_attest source --> docker ``` The Docker workflow is separate from the package release workflow, but it follows the same identity model: image signatures and SBOM attestations bind the image digest to the expected GitHub Actions workflow identity. ## Artifact records - Python wheels are published to GitHub Releases, PyPI, and the Nautech Systems package index (`packages.nautechsystems.io`). `SHA256SUMS`, per-asset `.sha256` files, and `dist-manifest.json` record integrity. GitHub artifact attestations, PyPI publish attestations, `.sigstore` bundles, and `.intoto.jsonl` envelopes record provenance. - Python sdists are published to GitHub Releases and PyPI. They use the same integrity and provenance records as wheels but are not published to the wheel-only package index. - Rust crates are published to crates.io. The crates.io checksum and `crates-manifest.json` record integrity. crates.io `trustpub_data` records provenance unless an explicit manual exception is present. - Docker images are published to GitHub Container Registry. The image digest is the integrity record. Sigstore cosign signatures and SPDX SBOM attestations record provenance. - The GitHub release record is published through GitHub Releases. Published release assets and the immutable tag record integrity. The GitHub release attestation records provenance. ## Consumer verification map Detailed commands live in [Verifying releases](https://github.com/nautechsystems/nautilus_trader/blob/master/SECURITY.md#verifying-releases). The checks below show the public data each consumer should verify. ### Python wheels and sdist Verify: - The artifact digest matches `SHA256SUMS`, the per-asset `.sha256` file, or `dist-manifest.json`. - The GitHub artifact attestation identity matches `nautechsystems/nautilus_trader/.github/workflows/build.yml` on `master` or `nightly`. - The PyPI publish attestation reports repository `nautechsystems/nautilus_trader`, workflow `build.yml`, and environment `release`. Example: ```bash : "${VERSION:?Set VERSION to the Python package version}" : "${ARTIFACT:?Set ARTIFACT to the release asset filename}" TAG="v$VERSION" REPO=nautechsystems/nautilus_trader ISSUER=https://token.actions.githubusercontent.com IDENTITY='^https://github\.com/nautechsystems/nautilus_trader/\.github/workflows/build\.yml@refs/heads/(master|nightly)$' gh release download "$TAG" --repo "$REPO" --pattern "$ARTIFACT" --pattern "$ARTIFACT.sha256" sha256sum -c "$ARTIFACT.sha256" gh attestation verify "$ARTIFACT" \ --repo "$REPO" \ --cert-identity-regex "$IDENTITY" \ --cert-oidc-issuer "$ISSUER" ``` ### PyPI publish provenance Verify: - PyPI file hashes match `dist-manifest.json`. - PyPI provenance exposes the expected GitHub publisher identity. - `pypi-attestations verify` accepts the downloaded file URL. Example: ```bash : "${VERSION:?Set VERSION to the Python package version}" : "${ARTIFACT:?Set ARTIFACT to the release asset filename}" PYPI_URL=$(curl -sS "https://pypi.org/pypi/nautilus_trader/$VERSION/json" | \ jq -r --arg artifact "$ARTIFACT" '.urls[] | select(.filename == $artifact) | .url') uv run --no-project --no-build --with pypi-attestations -- \ pypi-attestations verify pypi \ --repository https://github.com/nautechsystems/nautilus_trader \ "$PYPI_URL" ``` ### Rust crates Verify: - The crates.io version checksum matches the downloaded `.crate` file. - `trustpub_data.provider` is `github`. - `trustpub_data.repository` is `nautechsystems/nautilus_trader`. - `published_by` is `null`, unless `crates-manifest.json` records an explicit `manual_token_publish` exception. Example: ```bash CRATE=${CRATE:-nautilus-core} : "${VERSION:?Set VERSION to the crate version}" REPO=nautechsystems/nautilus_trader VERSION_JSON=$(curl -sS "https://crates.io/api/v1/crates/$CRATE/versions" | \ jq -c --arg version "$VERSION" '.versions[] | select(.num == $version)') CRATE_SHA256=$(printf '%s\n' "$VERSION_JSON" | jq -r '.checksum') printf '%s\n' "$VERSION_JSON" | jq -e --arg repo "$REPO" \ '.trustpub_data.provider == "github" and .trustpub_data.repository == $repo and .published_by == null' curl -sSL "https://static.crates.io/crates/$CRATE/$CRATE-$VERSION.crate" -o "$CRATE-$VERSION.crate" test "$(sha256sum "$CRATE-$VERSION.crate" | cut -d ' ' -f 1)" = "$CRATE_SHA256" ``` ### Docker images Verify: - The mutable tag resolves to the digest you intend to run. - The cosign signature identity matches the Docker workflow. - The SPDX SBOM attestation is bound to the same image digest. Example: ```bash export IMAGE_BASE=ghcr.io/nautechsystems/nautilus_trader export DIGEST=$(crane digest "$IMAGE_BASE:latest") export IMAGE=$IMAGE_BASE@$DIGEST export ISSUER=https://token.actions.githubusercontent.com export IDENTITY='^https://github\.com/nautechsystems/nautilus_trader/\.github/workflows/docker\.yml@refs/heads/(master|nightly)$' cosign verify "$IMAGE" --certificate-identity-regexp "$IDENTITY" --certificate-oidc-issuer "$ISSUER" cosign verify-attestation \ --type https://spdx.dev/Document/v2.3 \ "$IMAGE" \ --certificate-identity-regexp "$IDENTITY" \ --certificate-oidc-issuer "$ISSUER" ``` ## Manual recovery posture Normal releases use Trusted Publishing only. Manual package publishing is a last-resort recovery path after a partial release. Rules for manual recovery: - Prefer re-running the failed job or workflow when a registry or Sigstore verifier fails. - Do not replace a release tag or GitHub release assets after publication. - Do not silently accept manually published crates. - If a crate must be recovered with a token, list each `crate@version` in `CRATES_IO_MANUAL_PUBLISH_EXCEPTIONS`. - Record the exception in release notes and in `crates-manifest.json` with `release_status: "manual_token_publish"`. No routine release path depends on a long-lived PyPI or crates.io token. ## Incident response posture - PyPI publisher drift is detected by the PyPI provenance verifier. Stop publishing, fix the PyPI Trusted Publisher, and rerun verification. - crates.io publisher drift is detected by the trusted-publishing check or registry verifier. Fix crate publisher settings and rerun. Use a manual exception only for partial recovery. - GitHub release asset mismatch is detected by checksum or manifest verification. Stop the release before publication, or publish an advisory if assets already shipped. - Sigstore, Rekor, or TUF lag is detected by retryable transparency errors. Retry with bounded backoff, then pause release sealing if lag persists. - Sigstore trust root concern appears when attestation verification becomes ambiguous. Pause releases, verify against registry records, and rotate trust roots when supported. - Workflow identity mismatch is detected by GitHub, PyPI, or cosign identity checks. Treat it as configuration drift or compromise until reviewed. - Manual crate publish exceptions are detected when crates.io shows `published_by` instead of `trustpub_data`. Record the explicit exception, document affected crates, and preserve the audit trail. ## SLSA posture Python release artifacts carry build provenance through GitHub artifact attestations and PyPI publish attestations. Docker images carry Sigstore signatures and SPDX SBOM attestations. Rust crates rely on crates.io Trusted Publishing metadata and the release `crates-manifest.json`. This page does not assert a named SLSA level for all artifact classes. Any future SLSA level claim must cite this architecture, name the artifact classes it covers, and include CI validation that the published provenance parses as the claimed predicate type. # Shell Scripts Source: https://nautilustrader.io/docs/latest/developer_guide/shell/ This page defines when to create a shell script and how to name, write, invoke, and test it. Bash is the default. Use POSIX `sh` only when a supported caller cannot rely on Bash being installed. The policy applies to shell files throughout the repository, including `scripts/`, `scripts/ci/`, `.pre-commit-hooks/`, and component directories. ## When to write a script Before adding a script, search for an existing script, Make target, or installed tool that already provides the behavior. Add a script when it gives procedural logic one testable, linted source of truth. | Location | Owns | Delegates | | -------------- | -------------------------------------------------------------------- | ------------------------------------------------------ | | GitHub Actions | Events, permissions, matrices, runners, secrets, and hosted actions | Multi-step shell behavior | | `Makefile` | Discoverable tasks, dependencies, variables, and concurrency limits | Non-trivial control flow | | Shell script | Validation, reusable command sequences, retries, and transformations | Workflow orchestration and build dependency management | Prefer a script when: - A command sequence is used by more than one workflow, Make target, or developer task. - An inline GitHub Actions step contains branches, loops, retries, or failure handling worth testing outside the workflow. - A Make recipe needs enough procedural logic that quoting, error propagation, or platform behavior becomes hard to review. - A repeated maintenance or release task benefits from ShellCheck, `shfmt`, and focused tests. Keep a command inline when it is short, used once, and clearer in its caller. Do not wrap one stable command only to add another file. When extracting GitHub Actions logic, keep expressions such as `${{ github.ref }}`, permissions, secret selection, and runner selection in the workflow. Pass ordinary values to the script through arguments or environment variables. The script's exit status must remain the step's exit status. ## Choose the shell and extension The extension identifies the shell language, not whether the file is executable or sourceable. | Extension | Interpreter | Use | | --------- | ----------- | ------------------------------------------------------------------------ | | `.bash` | Bash | Default for new scripts and sourceable Bash files. | | `.sh` | POSIX `sh` | Only when a supported caller has a real requirement to run without Bash. | Use `#!/usr/bin/env bash` for `.bash` files and `#!/usr/bin/env sh` for `.sh` files. Keep the shebang even when Make or GitHub Actions invokes the file through `bash` or `sh`, because tools and direct callers use it to identify the interpreter. Bash is preferred for normal development, Make, and CI scripts because the repository already depends on it and its features make non-trivial shell code clearer. These features include `pipefail`, `[[ ... ]]`, arrays, process substitution, and function-local variables. Use POSIX `sh` for a small bootstrap or wrapper only when avoiding a Bash dependency is part of its supported interface. Test the script under the target `/bin/sh`; simple syntax alone does not prove POSIX compatibility. Existing filenames predate this extension policy, so some `.sh` files contain Bash. Treat their shebangs as the source of truth. Do not rename an existing script only to change its extension. Apply the policy to new files and to scoped renames that already update every call site and document. ## Define the portability target A script must support every platform on which its callers run. Unless its purpose states a narrower target, write it for Linux, macOS, and Windows through Git Bash, MSYS2, or WSL. Use Bash 3.2 as the default language floor because it is available on supported macOS systems. Avoid Bash 4+ features unless every caller provisions a newer version. Common Bash 4+ features and portable alternatives include: | Feature | Bash version | Portable alternative | | --------------------------------- | ------------ | ---------------------------------- | | Associative arrays (`declare -A`) | 4.0+ | Files, simple arrays, or functions | | `readarray` / `mapfile` | 4.0+ | `while read` loops | | `${var,,}` / `${var^^}` | 4.0+ | `tr` for case conversion | A CI-only script may use a newer Bash version or platform-specific tool when every workflow caller guarantees that environment. Document the constraint near the code that depends on it. A path under `scripts/ci/` does not by itself make a script Linux-only because CI also uses macOS and Windows runners. ### System utilities Prefer options supported by both GNU and BSD utilities. When no common form exists, detect the capability or operating system and implement both forms. | Operation | GNU form | BSD or macOS form | Portable approach | | ----------------- | -------------- | ----------------- | --------------------------------------------------------- | | In-place `sed` | `sed -i` | `sed -i ''` | Use a backup suffix such as `sed -i.bak`, then remove it. | | File size | `stat -c '%s'` | `stat -f '%z'` | Try or select the supported form. | | SHA-256 | `sha256sum` | `shasum -a 256` | Detect the command and keep output handling equal. | | Canonical path | `readlink -f` | No common form | Avoid it or resolve from a known directory with `pwd`. | | Extended matching | `grep -P` | No common form | Use `grep -E` when it expresses the same pattern. | | Nanosecond time | `date +%N` | No common form | Use an existing run ID or `$RANDOM` for cache busting. | Quote paths and expansions so spaces do not change argument boundaries. Do not assume filesystem paths are case-sensitive. Use repository-relative paths only after resolving the repository root from the script location, not from the caller's working directory. Use only commands installed by the documented development or runner setup. If an optional command is necessary, check for it with `command -v` and report how to install or replace it. The repository stores text with LF endings through `.gitattributes`; do not add platform-specific line endings. ## Place and name scripts - Put general development and maintenance commands under `scripts/`. - Put workflow-specific build, test, publication, and verification commands under `scripts/ci/`. - Put repository checks invoked by pre-commit under `.pre-commit-hooks/`. - Keep component-specific scripts beside the component when moving them to `scripts/` would hide their ownership. - Under `scripts/` and in component directories, use lowercase kebab-case. Name a regression script `test-.bash` or `test-.sh` to keep the tested pair together. - Under `.pre-commit-hooks/`, use lowercase snake_case and match the existing `check_*` and `test_check_*` name families. Keep each script focused on one task, and extend an existing script when new logic shares the same responsibility. ## Structure scripts for reliable execution Standalone Bash scripts should start with: ```bash #!/usr/bin/env bash set -euo pipefail ``` Use `set -eu` in a standalone POSIX `sh` script. POSIX does not define `pipefail`, so check pipeline behavior explicitly when a failure must propagate. If a script cannot use these options, explain the specific control flow that makes an option unsafe. Follow these requirements: - Validate required arguments and environment variables before changing state. Print concise usage text and exit nonzero for invalid input. - Quote parameter expansions. Use Bash arrays for argument lists instead of building a command string, and do not use `eval`. - Keep machine-readable output on standard output and diagnostics on standard error when callers capture the result. - Do not end routine status output with a terminating period. Keep punctuation when the output is a complete explanatory or diagnostic sentence. - Create temporary files with `mktemp`, register cleanup with `trap`, and constrain cleanup to the exact paths created by the script. - Bound retries, report the final failure, and return a nonzero status when the requested operation does not complete. - Do not print secrets or enable command tracing around credentials. Pass secrets through the environment or the tool's supported secret input. - Use comments only for constraints or behavior that the commands do not make clear. Give a standalone script executable permissions when users or tools call it as `./path`. A sourceable file does not need executable permissions. Prefer executing a script in a child process; source a file only when the caller must share its functions or shell state. A sourceable Bash file must not call `exit` or change the caller's shell options. Return errors from functions and let the caller choose its error policy. Use `${BASH_SOURCE[0]}` instead of `$0` to resolve the source file's location. When a script needs several functions, define `main` first, place called functions below their callers, and invoke `main "$@"` after all definitions. This keeps the task visible at the top while ensuring every function exists before execution starts. ## Integrate with Make and GitHub Actions Make targets should provide the stable, discoverable command that developers run. Keep target dependencies, build variables, and concurrency limits in the Makefile, then invoke the script with an explicit interpreter. For example, the `check-generated-drift` target delegates its procedural work to `scripts/ci/check-generated-drift.bash`. GitHub Actions should provide workflow context through named environment variables and call the same script used locally where practical. The build workflow follows this boundary: it owns the Python matrix condition and `TARGET_DIR`, then invokes `scripts/ci/check-generated-drift.bash`. Keep GitHub-specific output files such as `$GITHUB_OUTPUT` and `$GITHUB_ENV` at the workflow boundary when the script is also a local command. A script dedicated to GitHub Actions may write them when that integration is its stated purpose. ## Format and lint The pre-commit configuration formats shell files with `shfmt` using two-space indentation, indented case branches, consistent redirect spacing, and the Bash parser. ShellCheck then checks quoting, expansion, control flow, portability, and common command errors. The pinned hooks in `.pre-commit-config.yaml` are the source of truth for tool versions and options. Run both hooks against the exact changed scripts: ```bash prek run shfmt --files scripts/ci/test-wheel.bash prek run shellcheck --files scripts/ci/test-wheel.bash ``` `shfmt` updates files in place. Review its changes before running ShellCheck. ShellCheck selects the language from the shebang, so a `.sh` file with `#!/usr/bin/env sh` receives POSIX checks even though the formatter uses the common Bash parser. Keep ShellCheck suppressions on the narrowest applicable line. Add a nearby reason when the constraint is not clear from the code, and do not disable a check for the whole repository to silence one script. The repository also rejects executable files without shebangs, mixed line endings, trailing whitespace, and unresolved merge markers. These checks complement ShellCheck; they do not replace a runtime test. ## Test behavior Run the smallest test that exercises the changed branches and failure paths. For logic that can regress independently of a workflow, add a companion shell test. This includes parsing, multi-branch decisions, retries and cleanup, policy checks, and material external side effects. A domain-level suite may cover cooperating scripts, and a thin wrapper does not need a one-to-one test when that suite invokes it and proves its behavior. Each companion test: - Creates isolated state under `mktemp -d` and removes it on exit. - Supplies fake external commands through a temporary `PATH` instead of changing production code. - Uses distinct inputs and exact output, exit status, and side-effect assertions. - Covers success, invalid input, dependency failure, and cleanup when those paths exist. - Fails when a required test command is unavailable; a passing skip does not validate behavior. - Runs from `make test-scripts`, which is the script test inventory used by CI. When a script has callers on multiple operating systems, exercise platform-sensitive changes on each caller's relevant CI matrix. A Linux test plus clean ShellCheck output does not prove macOS or Windows behavior. ## Review checklist - The behavior is not already available from a script, Make target, installed tool, or simple native workflow feature. - GitHub Actions, Make, and the script own the right parts of the workflow. - The extension, shebang, executable mode, and documented portability target agree. - The script runs independently of the caller's working directory. - Arguments, failures, temporary files, retries, output, and secrets have explicit handling. - Focused behavior tests, `shfmt`, and ShellCheck pass for the final changed files. - Every call site, workflow path filter, and document uses the final filename. # Data Testing Spec Source: https://nautilustrader.io/docs/latest/developer_guide/spec_data_testing/ This section defines a rigorous test matrix for validating adapter data functionality using the Rust `DataTester` actor. Python exposes it as a built-in actor configured through `nautilus_trader.testkit.DataTesterConfig`; Rust code imports it from `nautilus_testkit::testers`. Each test case is identified by a prefixed ID (e.g. TC-D01) and grouped by functionality. **Each adapter must pass the subset of tests matching its supported data types.** Test groups are ordered from least derived to most derived data: instruments and raw book data first, then quotes, trades, bars, and derivatives data. An adapter that passes groups 1-4 is considered baseline data compliant. Document adapter-specific data behavior (custom channels, throttling, snapshot semantics, etc.) in the adapter's own guide, not here. ## Prerequisites Before running data tests: - Target instrument available and loadable via the instrument provider. - API credentials set via environment variables (`{VENUE}_API_KEY`, `{VENUE}_API_SECRET`) when the venue requires authentication for the data being tested. - If the venue offers a demo/testnet mode, use credentials created for that environment. Demo and production API keys are typically separate and not interchangeable; using the wrong credentials produces authentication errors (e.g. HTTP 401). **Python node setup**: Use `nautilus_trader.live.LiveNode`. Call `LiveNode.builder(...)` when you need to register adapter client factories before the node is built. ```python from nautilus_trader.common import Environment from nautilus_trader.config import LiveDataEngineConfig from nautilus_trader.live import LiveNode from nautilus_trader.model import TraderId from nautilus_trader.testkit import DataTesterConfig node = ( LiveNode.builder("TESTER-001", TraderId("TESTER-001"), Environment.SANDBOX) .with_data_engine_config(LiveDataEngineConfig(time_bars_build_with_no_updates=False)) .add_data_client(None, adapter_data_client_factory, data_client_config) .build() ) tester_config = DataTesterConfig( client_id=client_id, instrument_ids=[instrument_id], subscribe_quotes=True, ) node.add_builtin_actor("DataTester", tester_config) # Register remaining components, then start or run ``` **Rust node setup** (reference: `crates/adapters/{adapter}/examples/node_data_tester.rs`): ```rust use nautilus_testkit::testers::{DataTester, DataTesterConfig}; let tester_config = DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_quotes(true) .build()?; let tester = DataTester::new(tester_config); node.add_actor(tester)?; node.run().await?; ``` ## Timestamp scale Nautilus stores `ts_event` and `ts_init` as Unix nanoseconds (`UnixNanos`). Every data message that carries those fields must use that scale, not leftover seconds, milliseconds, or microseconds. - A value below `10^16` is not a plausible Unix-nanosecond timestamp (`10^16` ns is about 116 days after 1970-01-01) and usually means the adapter left the venue scale unconverted. - Second-precision venue times that were converted correctly end in `000000000` and still pass: that is coarse precision, not a scale error. - Live stream `ts_event` should be near wall-clock time for the session. Historical request results may be older and still valid if the scale is nanoseconds. - `ts_init` is the local clock when Nautilus created the object. Small `ts_event` > `ts_init` skew is possible when the venue clock is ahead. `DataTester` warns when `ts_event` or `ts_init` fails the scale check on instruments, quotes, trades, bars, book deltas, book depth, mark and index prices, funding rates, instrument status and close, option greeks, and historical batches of those types. It does not check reconstructed books in `on_book`. Treat a warning as a failure for the case that produced the message. --- Each group below begins with a summary table, followed by detailed test cards. Test IDs use spaced numbering to allow insertion without renumbering. --- ## Group 1: Instruments Verify instrument loading and subscription before testing market data streams. | TC | Name | Description | Skip when | | ------ | ------------------------ | --------------------------------- | ------------------ | | TC-D01 | Request instruments | Load all instruments for a venue. | Never. | | TC-D02 | Subscribe instrument | Subscribe to instrument updates. | No instrument sub. | | TC-D03 | Load specific instrument | Load a single instrument by ID. | Never. | ### TC-D01: Request instruments | Field | Value | | ------------------ | --------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected. | | **Action** | DataTester requests all instruments for the venue on start. | | **Event sequence** | `on_instruments` callback receives instrument list. | | **Pass criteria** | At least one instrument received; each has valid symbol, price precision, and size increment. | | **Skip when** | Never. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], request_instruments=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .request_instruments(true) .build()? ``` ### TC-D02: Subscribe instrument | Field | Value | | ------------------ | --------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester subscribes to instrument updates. | | **Event sequence** | `on_instrument` callback receives instrument. | | **Pass criteria** | Instrument received with correct `instrument_id`, valid fields. | | **Skip when** | Adapter does not support instrument subscriptions. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_instrument=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_instrument(true) .build()? ``` ### TC-D03: Load specific instrument | Field | Value | | ------------------ | -------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected. | | **Action** | Load a specific instrument by `InstrumentId` via the instrument provider. | | **Event sequence** | Instrument available in cache after load. | | **Pass criteria** | Instrument loaded with correct ID, price precision, size increment, and trading rules. | | **Skip when** | Never. | **Considerations:** - This tests the instrument provider's `load` / `load_async` method directly. - Verify the instrument is cached and available via `self.cache.instrument(instrument_id)`. --- ## Group 2: Order book Test order book subscription modes and snapshot requests. | TC | Name | Description | Skip when | | ------ | -------------------------- | ----------------------------------- | ----------------- | | TC-D10 | Subscribe book deltas | Stream `OrderBookDeltas` updates. | No book support. | | TC-D11 | Subscribe book at interval | Periodic `OrderBook` snapshots. | No book support. | | TC-D12 | Subscribe book depth | `OrderBookDepth10` snapshots. | No book depth. | | TC-D13 | Request book snapshot | One-time book snapshot request. | No book snapshot. | | TC-D14 | Managed book from deltas | Build local book from delta stream. | No book support. | Python uses `BookType.L2_MBP` for these scenarios. The Rust builder can override `book_type` when an adapter requires a different book representation. ### TC-D10: Subscribe book deltas | Field | Value | | ------------------ | -------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester subscribes to order book deltas. | | **Event sequence** | `OrderBookDeltas` events received in `on_book_deltas`. | | **Pass criteria** | Deltas received with valid instrument ID; at least one delta contains bid/ask updates. | | **Skip when** | Adapter does not support order book data. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_book_deltas=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_book_deltas(true) .book_type(BookType::L2_MBP) .build()? ``` ### TC-D11: Subscribe book at interval | Field | Value | | ------------------ | ----------------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester subscribes to periodic order book snapshots. | | **Event sequence** | `OrderBook` events received in `on_book` at configured interval. | | **Pass criteria** | Book snapshots received with bid/ask levels; updates arrive at approximately the configured interval. | | **Skip when** | Adapter does not support order book data. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_book_at_interval=True, book_depth=10, book_interval_ms=1000, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_book_at_interval(true) .book_type(BookType::L2_MBP) .book_depth(10) .book_interval_ms(1000) .build()? ``` ### TC-D12: Subscribe book depth | Field | Value | | ------------------ | ------------------------------------------------------------------------------------ | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester subscribes to `OrderBookDepth10` snapshots. | | **Event sequence** | `OrderBookDepth10` events received in `on_book_depth`. | | **Pass criteria** | Depth snapshots received with up to 10 bid/ask levels; prices are correctly ordered. | | **Skip when** | Adapter does not support book depth subscriptions. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_book_depth=True, book_depth=10, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_book_depth(true) .book_type(BookType::L2_MBP) .book_depth(10) .build()? ``` ### TC-D13: Request book snapshot | Field | Value | | ------------------ | ------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester requests a one-time order book snapshot. | | **Event sequence** | Book snapshot received via historical data callback. | | **Pass criteria** | Snapshot contains bid/ask levels with valid prices and sizes. | | **Skip when** | Adapter does not support book snapshot requests. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], request_book_snapshot=True, book_depth=10, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .request_book_snapshot(true) .book_depth(10) .build()? ``` ### TC-D14: Managed book from deltas | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, book deltas streaming. | | **Action** | DataTester subscribes to deltas with `manage_book=True`; builds local order book from the delta stream. | | **Event sequence** | `OrderBookDeltas` applied to local `OrderBook`; book logged with configured depth. | | **Pass criteria** | Local book builds correctly from deltas; bid levels descend, ask levels ascend; book is not empty after initial snapshot. | | **Skip when** | Adapter does not support order book data. | **Considerations:** - The managed book applies each delta to an `OrderBook` instance maintained by the actor. - Use `book_levels_to_print` to control logging verbosity. **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_book_deltas=True, manage_book=True, book_levels_to_print=10, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_book_deltas(true) .manage_book(true) .book_type(BookType::L2_MBP) .build()? ``` `DataTesterConfig` exposes `request_book_deltas`, but `DataTester` does not issue that historical request. Test an adapter's historical book delta support through a custom actor until the tester implements the request path. --- ## Group 3: Quotes Test quote tick subscriptions and historical requests. | TC | Name | Description | Skip when | | ------ | ------------------------- | ------------------------------------------- | --------------------- | | TC-D20 | Subscribe quotes | Verify `QuoteTick` events flow after start. | Never. | | TC-D21 | Request historical quotes | Request historical quote ticks. | No historical quotes. | ### TC-D20: Subscribe quotes | Field | Value | | ------------------ | --------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester subscribes to quotes on start. | | **Event sequence** | `QuoteTick` events received in `on_quote`. | | **Pass criteria** | At least one `QuoteTick` received with valid bid/ask prices and sizes; bid < ask. | | **Skip when** | Never. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_quotes=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_quotes(true) .build()? ``` ### TC-D21: Request historical quotes | Field | Value | | ------------------ | ----------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester requests historical quote ticks. | | **Event sequence** | Historical quote batches received via `on_historical_quotes`. | | **Pass criteria** | Quotes received with valid timestamps, bid/ask prices, and sizes. | | **Skip when** | Adapter does not support historical quote requests. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], request_quotes=True, ) ``` --- ## Group 4: Trades Test trade tick subscriptions and historical requests. | TC | Name | Description | Skip when | | ------ | ------------------------- | ------------------------------------------- | --------------------- | | TC-D30 | Subscribe trades | Verify `TradeTick` events flow after start. | Never. | | TC-D31 | Request historical trades | Request historical trade ticks. | No historical trades. | ### TC-D30: Subscribe trades | Field | Value | | ------------------ | ----------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester subscribes to trades on start. | | **Event sequence** | `TradeTick` events received in `on_trade`. | | **Pass criteria** | At least one `TradeTick` received with valid price, size, and aggressor side. | | **Skip when** | Never. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_trades=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_trades(true) .build()? ``` ### TC-D31: Request historical trades | Field | Value | | ------------------ | -------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester requests historical trade ticks. | | **Event sequence** | Historical trade batches received via `on_historical_trades`. | | **Pass criteria** | Trades received with valid timestamps, prices, sizes, and trade IDs. | | **Skip when** | Adapter does not support historical trade requests. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], request_trades=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .request_trades(true) .build()? ``` --- ## Group 5: Bars Test bar subscriptions and historical requests. | TC | Name | Description | Skip when | | ------ | ----------------------- | ------------------------------------- | ------------------- | | TC-D40 | Subscribe bars | Verify `Bar` events flow after start. | No bar support. | | TC-D41 | Request historical bars | Request historical OHLCV bars. | No historical bars. | ### TC-D40: Subscribe bars | Field | Value | | ------------------ | ---------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, bar type configured. | | **Action** | DataTester subscribes to bars for a configured `BarType`. | | **Event sequence** | `Bar` events received in `on_bar`. | | **Pass criteria** | At least one `Bar` received with valid OHLCV values; high >= low, high >= open, high >= close. | | **Skip when** | Adapter does not support bar subscriptions. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], bar_types=[BarType.from_str("BTCUSDT-PERP.VENUE-1-MINUTE-LAST-EXTERNAL")], subscribe_bars=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .bar_types(vec![bar_type]) .subscribe_bars(true) .build()? ``` ### TC-D41: Request historical bars | Field | Value | | ------------------ | --------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, bar type configured. | | **Action** | DataTester requests historical bars for a configured `BarType`. | | **Event sequence** | Historical bars received via callback. | | **Pass criteria** | Bars received with valid OHLCV values and ascending timestamps. | | **Skip when** | Adapter does not support historical bar requests. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], bar_types=[BarType.from_str("BTCUSDT-PERP.VENUE-1-MINUTE-LAST-EXTERNAL")], request_bars=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .bar_types(vec![bar_type]) .request_bars(true) .build()? ``` --- ## Group 6: Derivatives data Test derivatives-specific data streams: mark prices, index prices, and funding rates. | TC | Name | Description | Skip when | | ------ | -------------------------------- | ----------------------------- | ----------------- | | TC-D50 | Subscribe mark prices | `MarkPriceUpdate` events. | Not a derivative. | | TC-D51 | Subscribe index prices | `IndexPriceUpdate` events. | Not a derivative. | | TC-D52 | Subscribe funding rates | `FundingRateUpdate` events. | Not a perpetual. | | TC-D53 | Request historical funding rates | Historical funding rate data. | Not a perpetual. | ### TC-D50: Subscribe mark prices | Field | Value | | ------------------ | -------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, derivative instrument loaded. | | **Action** | DataTester subscribes to mark price updates. | | **Event sequence** | `MarkPriceUpdate` events received in `on_mark_price`. | | **Pass criteria** | At least one `MarkPriceUpdate` received with valid instrument ID and mark price. | | **Skip when** | Instrument is not a derivative, or adapter does not provide mark prices. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_mark_prices=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_mark_prices(true) .build()? ``` ### TC-D51: Subscribe index prices | Field | Value | | ------------------ | ---------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, derivative instrument loaded. | | **Action** | DataTester subscribes to index price updates. | | **Event sequence** | `IndexPriceUpdate` events received in `on_index_price`. | | **Pass criteria** | At least one `IndexPriceUpdate` received with valid instrument ID and index price. | | **Skip when** | Instrument is not a derivative, or adapter does not provide index prices. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_index_prices=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_index_prices(true) .build()? ``` ### TC-D52: Subscribe funding rates | Field | Value | | ------------------ | ---------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, perpetual instrument loaded. | | **Action** | DataTester subscribes to funding rate updates. | | **Event sequence** | `FundingRateUpdate` events received in `on_funding_rate`. | | **Pass criteria** | At least one `FundingRateUpdate` received with valid instrument ID and rate. | | **Skip when** | Instrument is not a perpetual, or adapter does not provide funding rates. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_funding_rates=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_funding_rates(true) .build()? ``` ### TC-D53: Request historical funding rates | Field | Value | | ------------------ | -------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, perpetual instrument loaded. | | **Action** | DataTester requests historical funding rates (default 7-day lookback). | | **Event sequence** | Historical funding rates received via callback. | | **Pass criteria** | Funding rates received with valid timestamps and rate values. | | **Skip when** | Instrument is not a perpetual, or adapter does not support historical funding rate requests. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], request_funding_rates=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .request_funding_rates(true) .build()? ``` --- ## Group 7: Instrument status Test instrument status and close event subscriptions. | TC | Name | Description | Skip when | | ------ | --------------------------- | -------------------------- | ------------------ | | TC-D60 | Subscribe instrument status | `InstrumentStatus` events. | No status support. | | TC-D61 | Subscribe instrument close | `InstrumentClose` events. | No close support. | ### TC-D60: Subscribe instrument status | Field | Value | | ------------------ | ------------------------------------------------------------------------ | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester subscribes to instrument status updates. | | **Event sequence** | `InstrumentStatus` events received in `on_instrument_status`. | | **Pass criteria** | Status events received with valid `MarketStatusAction` (e.g. `Trading`). | | **Skip when** | Adapter does not support instrument status subscriptions. | **Considerations:** - Status events may only fire on state changes (e.g. trading halt -> resume). - During normal trading hours, a `Trading` status may be received on subscribe. **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_instrument_status=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_instrument_status(true) .build()? ``` ### TC-D61: Subscribe instrument close | Field | Value | | ------------------ | ----------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded. | | **Action** | DataTester subscribes to instrument close events. | | **Event sequence** | `InstrumentClose` events received in `on_instrument_close`. | | **Pass criteria** | Close event received with valid close price and close type. | | **Skip when** | Adapter does not support instrument close subscriptions. | **Considerations:** - Close events typically fire at end-of-session for traditional markets. - May not fire for 24/7 crypto venues unless the adapter synthesizes a daily close. **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_instrument_close=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_instrument_close(true) .build()? ``` --- ## Group 8: Option greeks Test option greeks and option chain subscriptions. | TC | Name | Description | Skip when | | ------ | ----------------------- | -------------------------------------------- | ------------------ | | TC-D62 | Subscribe option greeks | `OptionGreeks` data for a single instrument. | No greeks support. | | TC-D63 | Subscribe option chain | `OptionChainSlice` snapshots for a series. | No chain support. | ### TC-D62: Subscribe option greeks | Field | Value | | ------------------ | ------------------------------------------------------------ | | **Prerequisite** | Adapter connected, option instrument loaded. | | **Action** | DataTester subscribes to option greeks updates. | | **Event sequence** | `OptionGreeks` events received in `on_option_greeks`. | | **Pass criteria** | Greeks received with valid delta, gamma, vega, theta values. | | **Skip when** | Adapter does not support option greeks subscriptions. | **Considerations:** - Greeks are only available for option instruments. - Values depend on the venue's pricing model and may update on every quote change. - Some venues (Bybit, Deribit) subscribe per instrument; OKX subscribes per instrument family and filters to the requested instruments. - `rho` may be zero when the venue does not provide it (Bybit, OKX). - `underlying_price` and `open_interest` may be `None` depending on the venue channel. **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_option_greeks=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_option_greeks(true) .build()? ``` ### TC-D63: Subscribe option chain | Field | Value | | ------------------ | ------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, option instruments loaded for the series. | | **Action** | DataTester subscribes to option chain snapshots for a series. | | **Event sequence** | `OptionChainSlice` snapshots received in `on_option_chain`. | | **Pass criteria** | Chain snapshot contains greeks for instruments matching the series. | | **Skip when** | Adapter does not support option chain subscriptions. | **Considerations:** - Option chain subscriptions are managed by the DataEngine, which creates per-instrument quote and greeks subscriptions internally. - Dynamic strike ranges require an ATM price before instrument subscriptions begin. The DataEngine requests an initial reference price and otherwise waits for live option Greeks. - Not yet configurable via `DataTesterConfig`; requires manual actor setup with `subscribe_option_chain` and an `OptionSeriesId`. --- ## Group 9: Lifecycle Test actor lifecycle behavior: unsubscribe handling, retirement cleanup, and custom parameters. | TC | Name | Description | Skip when | | ------ | ----------------------- | ----------------------------------------------- | ----------------- | | TC-D70 | Unsubscribe on stop | Unsubscribe from data feeds on actor stop. | No unsub support. | | TC-D71 | Custom subscribe params | Adapter-specific subscription parameters. | N/A. | | TC-D72 | Custom request params | Adapter-specific request parameters. | N/A. | | TC-D73 | Retirement cleanup | Release an actor's retained data subscriptions. | N/A. | ### TC-D70: Unsubscribe on stop | Field | Value | | ------------------ | ---------------------------------------------------------------- | | **Prerequisite** | Active data subscriptions (quotes, trades, book). | | **Action** | Stop the actor with `can_unsubscribe=True` (default). | | **Event sequence** | Data subscriptions removed; no further data events received. | | **Pass criteria** | Clean unsubscribe; no errors in logs; no data events after stop. | | **Skip when** | Adapter does not support unsubscribe. | **Python config:** ```python DataTesterConfig( instrument_ids=[instrument_id], subscribe_quotes=True, subscribe_trades=True, can_unsubscribe=True, ) ``` **Rust config:** ```rust DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_quotes(true) .subscribe_trades(true) .can_unsubscribe(true) .build()? ``` ### TC-D71: Custom subscribe params | Field | Value | | ------------------ | ---------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, adapter accepts additional subscription parameters. | | **Action** | Subscribe with adapter-specific `subscribe_params`. | | **Event sequence** | Subscription established with custom parameters applied. | | **Pass criteria** | Data flows with adapter-specific parameters in effect. | | **Skip when** | N/A (adapter-specific). | **Rust config:** ```rust use nautilus_core::Params; use serde_json::json; let mut subscribe_params = Params::new(); subscribe_params.insert("key".to_string(), json!("value")); DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .subscribe_quotes(true) .subscribe_params(subscribe_params) .build()? ``` **Considerations:** - `subscribe_params` is opaque to the DataTester and passed through to the adapter. - The Python `DataTesterConfig` constructor does not expose this Rust-only field. - Consult the adapter's guide for supported parameters. ### TC-D72: Custom request params | Field | Value | | ------------------ | -------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, adapter accepts additional request parameters. | | **Action** | Request data with adapter-specific `request_params`. | | **Event sequence** | Request fulfilled with custom parameters applied. | | **Pass criteria** | Historical data received with adapter-specific parameters in effect. | | **Skip when** | N/A (adapter-specific). | **Rust config:** ```rust use nautilus_core::Params; use serde_json::json; let mut request_params = Params::new(); request_params.insert("key".to_string(), json!("value")); DataTesterConfig::builder() .client_id(client_id) .instrument_ids(vec![instrument_id]) .request_quotes(true) .request_params(request_params) .build()? ``` **Considerations:** - `request_params` is opaque to the DataTester and passed through to the adapter. - The Python `DataTesterConfig` constructor does not expose this Rust-only field. - Consult the adapter's guide for supported parameters. ### TC-D73: Retirement cleanup | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | An actor has venue-backed subscriptions; two actors share an internally aggregated bar. | | **Action** | Retire the first actor, then retire the second actor through the trader. | | **Event sequence** | `on_dispose` completes; unsubscribe commands are sent; the actor is deregistered. | | **Pass criteria** | The first retirement keeps shared data active; the final retirement releases the retained route and leaves no retired actor handlers. | | **Skip when** | N/A. | The shared bar must remain active after the first actor retires and stop after the final actor retires. **Considerations:** - `DataTesterConfig` does not cover multi-actor retirement. Create two actors manually, then remove them through Python `Controller.remove_actor` or Rust `Trader::remove_actor`. - If `on_dispose` fails, the actor must remain registered with its subscriptions intact so a later retirement can release them without invoking the failed hook again. - A failed `on_stop` or `on_fault` must not block retirement: disposal and deregistration must still complete from the corresponding transitional state. --- ## DataTester configuration reference The Python constructor accepts the parameters below. Defaults are resolved values after construction. Historical quote, trade, and bar requests use a one-hour lookback; funding rate requests use seven days. The lookback is not configurable through `DataTesterConfig`. | Parameter | Type | Default | Affects groups | | ----------------------------- | -------------------- | ------- | --------------- | | `actor_id` | `ActorId?` | `None` | All | | `client_id` | `ClientId?` | `None` | All | | `instrument_ids` | `list[InstrumentId]` | `[]` | All | | `bar_types` | `list[BarType]?` | `None` | 5 | | `subscribe_book_deltas` | `bool` | `False` | 2 | | `subscribe_book_depth` | `bool` | `False` | 2 | | `subscribe_book_at_interval` | `bool` | `False` | 2 | | `subscribe_quotes` | `bool` | `False` | 3 | | `subscribe_trades` | `bool` | `False` | 4 | | `subscribe_mark_prices` | `bool` | `False` | 6 | | `subscribe_index_prices` | `bool` | `False` | 6 | | `subscribe_funding_rates` | `bool` | `False` | 6 | | `subscribe_bars` | `bool` | `False` | 5 | | `subscribe_instrument` | `bool` | `False` | 1 | | `subscribe_instrument_status` | `bool` | `False` | 7 | | `subscribe_instrument_close` | `bool` | `False` | 7 | | `subscribe_option_greeks` | `bool` | `False` | 8 | | `can_unsubscribe` | `bool` | `True` | 9 | | `request_instruments` | `bool` | `False` | 1 | | `request_book_snapshot` | `bool` | `False` | 2 | | `request_book_deltas` | `bool` | `False` | Not implemented | | `request_quotes` | `bool` | `False` | 3 | | `request_trades` | `bool` | `False` | 4 | | `request_bars` | `bool` | `False` | 5 | | `request_funding_rates` | `bool` | `False` | 6 | | `book_depth` | `PositiveInt?` | `None` | 2 | | `book_interval_ms` | `PositiveInt` | `1000` | 2 | | `book_levels_to_print` | `PositiveInt` | `10` | 2 | | `manage_book` | `bool` | `True` | 2 | | `log_data` | `bool` | `True` | All | | `stats_interval_secs` | `int` | `5` | All | | `log_events` | `bool` | `True` | All | | `log_commands` | `bool` | `True` | All | The Rust builder also exposes these parameters: | Parameter | Type | Default | Affects groups | | ------------------ | ---------- | -------- | -------------- | | `book_type` | `BookType` | `L2_MBP` | 2 | | `subscribe_params` | `Params?` | `None` | 9 | | `request_params` | `Params?` | `None` | 9 | --- # Execution Testing Spec Source: https://nautilustrader.io/docs/latest/developer_guide/spec_exec_testing/ This section defines a rigorous test matrix for validating adapter execution functionality using the Rust `ExecTester` strategy. Python exposes it as a built-in strategy configured through `nautilus_trader.testkit.ExecTesterConfig`; Rust code imports it from `nautilus_testkit::testers`. Each test case is identified by a prefixed ID (e.g. TC-E01) and grouped by functionality. **Each adapter must pass the subset of tests matching its supported capabilities.** Tests progress from simple (single market order) to complex (brackets, modification chains, rejection handling). An adapter that passes groups 1-5 is considered baseline compliant. Data connectivity should be verified first using the [Data Testing Spec](spec_data_testing.md). Document adapter-specific behavior (how a venue simulates market orders, handles TIF options, etc.) in the adapter's own guide, not here. Each adapter guide should include a capability matrix showing which order types, time-in-force options, actions, and flags it supports. ## Prerequisites Before running execution tests: - Demo/testnet account with valid API credentials (preferred, not required). - Account funded with sufficient margin for the test instrument and quantities. - Target instrument available and loadable via the instrument provider. - Environment variables set: `{VENUE}_API_KEY`, `{VENUE}_API_SECRET` (or sandbox variants). - If the venue offers a demo/testnet mode, use credentials created for that environment. Demo and production API keys are typically separate and not interchangeable; using the wrong credentials produces authentication errors (e.g. HTTP 401). - Risk engine bypassed (`LiveRiskEngineConfig(bypass=True)`) to avoid interference. - Reconciliation enabled to verify state consistency. **Python node setup**: Use `nautilus_trader.live.LiveNode`. Call `LiveNode.builder(...)` when you need to register adapter client factories before the node is built. ```python from nautilus_trader.common import Environment from nautilus_trader.config import LiveExecutionEngineConfig from nautilus_trader.config import LiveRiskEngineConfig from nautilus_trader.live import LiveNode from nautilus_trader.model import TraderId from nautilus_trader.testkit import ExecTesterConfig node = ( LiveNode.builder("TESTER-001", TraderId("TESTER-001"), Environment.SANDBOX) .with_risk_engine_config(LiveRiskEngineConfig(bypass=True)) .with_exec_engine_config(LiveExecutionEngineConfig(reconciliation=True)) .add_exec_client(None, adapter_exec_client_factory, exec_client_config) .build() ) tester_config = ExecTesterConfig( instrument_id=instrument_id, client_id=client_id, order_qty=order_qty, ) node.add_builtin_strategy("ExecTester", tester_config) # Register remaining components, then start or run ``` **Rust node setup** (reference: `crates/adapters/{adapter}/examples/node_exec_tester.rs`): ```rust use nautilus_testkit::testers::{ExecTester, ExecTesterConfig}; use nautilus_trading::strategy::StrategyConfig; let tester_config = ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(order_qty) .build()?; let tester = ExecTester::new(tester_config); node.add_strategy(tester)?; node.run().await?; ``` ## Timestamp scale Nautilus stores `ts_event` and `ts_init` as Unix nanoseconds (`UnixNanos`). Every execution message must use that scale: order events, fills, account state, and reconciliation reports (`OrderStatusReport`, `FillReport`, `PositionStatusReport`, `ExecutionMassStatus`). - A value below `10^16` is not a plausible Unix-nanosecond timestamp (`10^16` ns is about 116 days after 1970-01-01) and usually means the adapter left the venue scale unconverted. - Second-precision venue times that were converted correctly end in `000000000` and still pass: that is coarse precision, not a scale error. - Live stream `ts_event` should be near wall-clock time for the session. Reconciliation reports may be older and still valid if the scale is nanoseconds. - `ts_init` is the local clock when Nautilus created the object. Small `ts_event` > `ts_init` skew is possible when the venue clock is ahead. `ExecTester` warns when received market-data or order-event timestamps fail the scale check. Inspect report timestamps on the reports themselves. Treat a scale warning or leftover seconds, milliseconds, or microseconds on any execution message as a failure. ## Basic smoke test A quick sanity check that can run at any time, for example after adapter changes or between development iterations. The tester opens a position with a market order on start, places a buy and sell post-only limit order, waits 30 seconds, then stops (cancelling open orders and closing the position). **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.001"), open_position_on_start_qty=Decimal("0.001"), enable_limit_buys=True, enable_limit_sells=True, use_post_only=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.001")) .open_position_on_start_qty(dec!(0.001)) .enable_limit_buys(true) .enable_limit_sells(true) .use_post_only(true) .build()? ``` **Expected behavior:** 1. On start: market order fills, opening a position. 2. Two limit orders placed at `tob_offset_ticks` away from best bid/ask (default 500 ticks). 3. Strategy idles for 30 seconds. Check logs for errors, rejected orders, or disconnections. 4. On stop: open limit orders cancelled, position closed with a market order. **Pass criteria:** No errors in logs, position opened and closed cleanly, and limit orders acknowledged by the venue. --- Each group below begins with a summary table, followed by detailed test cards. Test IDs use spaced numbering to allow insertion without renumbering. --- ## Group 1: Market orders Test market order submission and fills. Market orders should execute immediately. | TC | Name | Description | Skip when | | ------ | ----------------------------- | --------------------------------------------------- | ------------------ | | TC-E01 | Market BUY - submit and fill | Open long position via market buy. | No market orders. | | TC-E02 | Market SELL - submit and fill | Open short position via market sell. | No market orders. | | TC-E03 | Market order with IOC TIF | Market order explicitly using IOC time in force. | No IOC. | | TC-E04 | Market order with FOK TIF | Market order explicitly using FOK time in force. | No FOK. | | TC-E05 | Market order with quote qty | Market order using quote currency quantity. | No quote quantity. | | TC-E06 | Close position via market | Close an open position with a market order on stop. | No market orders. | ### TC-E01: Market BUY - submit and fill | Field | Value | | ------------------ | ---------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, market data flowing, no open position. | | **Action** | ExecTester opens a long position via `open_position_on_start_qty`. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled`. | | **Pass criteria** | Position opened with side=LONG, quantity matches config, fill price within market range, `AccountState` updated. | | **Skip when** | Adapter does not support market orders. | **Considerations:** - Some adapters simulate market orders as aggressive limit IOC orders (check adapter guide). - The event sequence from the strategy's perspective should be identical regardless of the venue mechanism. - Fill price should be within the recent bid/ask spread. - Partial fills are valid; verify the cumulative filled quantity matches the order quantity. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), open_position_on_start_qty=Decimal("0.01"), enable_limit_buys=False, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .open_position_on_start_qty(Decimal::new(1, 2)) .enable_limit_buys(false) .enable_limit_sells(false) .build()? ``` ### TC-E02: Market SELL - submit and fill | Field | Value | | ------------------ | ----------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, market data flowing, no open position. | | **Action** | ExecTester opens a short position via negative `open_position_on_start_qty`. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled`. | | **Pass criteria** | Position opened with side=SHORT, quantity matches config, fill price within market range. | | **Skip when** | Adapter does not support market orders or short selling. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), open_position_on_start_qty=Decimal("-0.01"), enable_limit_buys=False, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .open_position_on_start_qty(Decimal::new(-1, 2)) .enable_limit_buys(false) .enable_limit_sells(false) .build()? ``` ### TC-E03: Market order with IOC TIF | Field | Value | | ------------------ | --------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, market data flowing. | | **Action** | Open position with `open_position_time_in_force=IOC`. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled`. | | **Pass criteria** | Same as TC-E01; the IOC TIF is explicitly set on the order. | | **Skip when** | No IOC support. | **Considerations:** - Some adapters simulate market orders as aggressive limit IOC. An unfilled simulated market maps to `OrderCanceled`, not `OrderRejected`. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), open_position_on_start_qty=Decimal("0.01"), open_position_time_in_force=TimeInForce.IOC, enable_limit_buys=False, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .open_position_on_start_qty(Decimal::new(1, 2)) .enable_limit_buys(false) .enable_limit_sells(false) .open_position_time_in_force(TimeInForce::Ioc) .build()? ``` ### TC-E04: Market order with FOK TIF | Field | Value | | ------------------ | --------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, market data flowing. | | **Action** | Open position with `open_position_time_in_force=FOK`. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled`. | | **Pass criteria** | Same as TC-E01; the FOK TIF is explicitly set on the order. | | **Skip when** | No FOK support. | **Considerations:** - FOK requires the entire quantity to be fillable immediately or the order is canceled. - Use small test quantities so book depth is sufficient for a complete fill. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), open_position_on_start_qty=Decimal("0.01"), open_position_time_in_force=TimeInForce.FOK, enable_limit_buys=False, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .open_position_on_start_qty(Decimal::new(1, 2)) .enable_limit_buys(false) .enable_limit_sells(false) .open_position_time_in_force(TimeInForce::Fok) .build()? ``` ### TC-E05: Market order with quote quantity | Field | Value | | ------------------ | -------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, adapter supports quote quantity. | | **Action** | Open position with `use_quote_quantity=True`, quantity in quote currency. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled`. | | **Pass criteria** | Order submitted with quote currency quantity; fill quantity is in base currency. | | **Skip when** | Adapter does not support quote quantity orders. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("100.0"), # Quote currency amount open_position_on_start_qty=Decimal("100.0"), use_quote_quantity=True, enable_limit_buys=False, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("100")) .open_position_on_start_qty(Decimal::from(100)) .use_quote_quantity(true) .enable_limit_buys(false) .enable_limit_sells(false) .build()? ``` ### TC-E06: Close position via market order on stop | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------- | | **Prerequisite** | Open position from TC-E01 or TC-E02. | | **Action** | Stop the strategy; ExecTester closes position via market order. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled` (closing order). | | **Pass criteria** | Position closed, or only the exact sub-precision residual remains; no open orders remain. | | **Skip when** | Adapter does not support market orders. | **Considerations:** - This test naturally follows TC-E01 or TC-E02 as part of the same session. - `close_positions_on_stop=True` is the default. - The closing order should be on the opposite side of the position. - Set `close_positions_qty_precision` when the venue accepts fewer size decimals than the instrument. The tester closes only that venue-fillable quantity and logs any exact residual. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), open_position_on_start_qty=Decimal("0.01"), close_positions_on_stop=True, enable_limit_buys=False, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .open_position_on_start_qty(Decimal::new(1, 2)) .close_positions_on_stop(true) .enable_limit_buys(false) .enable_limit_sells(false) .build()? ``` --- ## Group 2: Limit orders Test limit order submission, acceptance, and behavior across time-in-force options. | TC | Name | Description | Skip when | | ------ | ------------------------- | ------------------------------------------------ | --------- | | TC-E10 | Limit BUY GTC | Place GTC limit buy below TOB, verify accepted. | Never. | | TC-E11 | Limit SELL GTC | Place GTC limit sell above TOB, verify accepted. | Never. | | TC-E12 | Limit BUY and SELL pair | Both sides simultaneously, verify both accepted. | Never. | | TC-E13 | Limit IOC aggressive fill | Limit IOC at aggressive price, expect fill. | No IOC. | | TC-E14 | Limit IOC passive no fill | Limit IOC away from market, expect cancel. | No IOC. | | TC-E15 | Limit FOK fill | Limit FOK at aggressive price, expect fill. | No FOK. | | TC-E16 | Limit FOK no fill | Limit FOK away from market, expect cancel. | No FOK. | | TC-E17 | Limit GTD | Limit with expiry time, verify accepted. | No GTD. | | TC-E18 | Limit GTD expiry | Verify documented terminal event at expiry. | No GTD. | | TC-E19 | Limit DAY | Limit with DAY TIF, verify accepted. | No DAY. | ### TC-E10: Limit BUY GTC - submit and accept | Field | Value | | ------------------ | --------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester places a limit buy at `best_bid - tob_offset_ticks`. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order is open on the venue with correct price, quantity, side=BUY, TIF=GTC. | | **Skip when** | Never. | **Considerations:** - The `tob_offset_ticks` (default 500) places the order well away from the market to avoid accidental fills. - Verify the order appears in the cache with `OrderStatus.ACCEPTED`. - The order should remain open until explicitly canceled. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(false) .build()? ``` ### TC-E11: Limit SELL GTC - submit and accept | Field | Value | | ------------------ | ---------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester places a limit sell at `best_ask + tob_offset_ticks`. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order is open on the venue with correct price, quantity, side=SELL, TIF=GTC. | | **Skip when** | Never. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=False, enable_limit_sells=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(false) .enable_limit_sells(true) .build()? ``` ### TC-E12: Limit BUY and SELL pair | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------ | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester places both a limit buy and limit sell. | | **Event sequence** | Two independent sequences: each `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Both orders open on venue, buy below bid, sell above ask. | | **Skip when** | Never. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(true) .build()? ``` ### TC-E13: Limit IOC aggressive fill | Field | Value | | ------------------ | --------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | Submit a limit buy IOC at or above the best ask (aggressive price). | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled`. | | **Pass criteria** | Order fills immediately; position opened. | | **Skip when** | Adapter does not support IOC TIF. | **Considerations:** - This test requires manual order creation or adapter-specific configuration, as the ExecTester's default limit order placement uses GTC TIF. - IOC orders that don't fill immediately are canceled by the venue. ### TC-E14: Limit IOC passive - no fill | Field | Value | | ------------------ | ----------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | Submit a limit buy IOC well below the market (passive price). | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderCanceled`. | | **Pass criteria** | Order is immediately canceled by venue with no fill. | | **Skip when** | Adapter does not support IOC TIF. | **Considerations:** - The venue should cancel the unfilled IOC order; verify `OrderCanceled` (not `OrderExpired` or `OrderRejected`). ### TC-E15: Limit FOK fill | Field | Value | | ------------------ | ---------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing, sufficient book depth. | | **Action** | Submit a limit buy FOK at aggressive price with quantity within top-of-book depth. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled`. | | **Pass criteria** | Order fills completely in a single fill event. | | **Skip when** | Adapter does not support FOK TIF. | **Considerations:** - FOK requires the entire quantity to be fillable; use small quantities so book depth is sufficient. ### TC-E16: Limit FOK no fill | Field | Value | | ------------------ | ----------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | Submit a limit buy FOK at passive price (well below market). | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderCanceled`. | | **Pass criteria** | Order is immediately canceled by venue with no fill. | | **Skip when** | Adapter does not support FOK TIF. | ### TC-E17: Limit GTD - submit and accept | Field | Value | | ------------------ | --------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | Place limit buy with `order_expire_time_delta_mins` set (e.g., 60 minutes). | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order accepted with GTD TIF and correct expiry timestamp. | | **Skip when** | Adapter does not support GTD TIF. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), order_expire_time_delta_mins=60, enable_limit_buys=True, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(false) .order_expire_time_delta_mins(60) .build()? ``` ### TC-E18: Limit GTD expiry | Field | Value | | ------------------ | --------------------------------------------------------------------------- | | **Prerequisite** | Open GTD limit order from TC-E17 using the shortest venue-supported expiry. | | **Action** | Wait for the GTD expiry time to elapse. | | **Event sequence** | `OrderExpired` by default, or the terminal event documented by the adapter. | | **Pass criteria** | Order reaches the adapter-documented terminal status at venue expiry. | | **Skip when** | Adapter does not support GTD TIF. | **Considerations:** - Use the shortest expiry accepted by the venue; do not assume one or two minutes is valid. - Some venues report GTD expiry as a cancel. Preserve and verify the documented adapter mapping instead of normalizing every venue to `OrderExpired`. ### TC-E19: Limit DAY - submit and accept | Field | Value | | ------------------ | ---------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, market is in trading hours. | | **Action** | Submit limit buy with DAY TIF. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order accepted with DAY TIF; will be automatically canceled at end of trading day. | | **Skip when** | Adapter does not support DAY TIF. | **Considerations:** - DAY orders may behave differently on 24/7 crypto venues vs traditional markets. - Verify behavior when submitted outside trading hours (if applicable). --- ## Group 3: Stop and conditional orders Test stop and conditional order types. These orders rest on the venue until a trigger condition is met. Adapters that support venue-native conditional orders should also verify that open trigger orders appear in restart reconciliation, not only in the normal open-order endpoint. | TC | Name | Description | Skip when | | ------ | -------------------- | ------------------------------------------- | ----------------- | | TC-E20 | StopMarket BUY | Stop buy above ask, verify accepted. | No `STOP_MARKET`. | | TC-E21 | StopMarket SELL | Stop sell below bid, verify accepted. | No `STOP_MARKET`. | | TC-E22 | StopLimit BUY | Stop-limit buy with trigger + limit price. | No `STOP_LIMIT`. | | TC-E23 | StopLimit SELL | Stop-limit sell with trigger + limit price. | No `STOP_LIMIT`. | | TC-E24 | MarketIfTouched BUY | MIT buy below bid. | No `MIT`. | | TC-E25 | MarketIfTouched SELL | MIT sell above ask. | No `MIT`. | | TC-E26 | LimitIfTouched BUY | LIT buy with trigger + limit price. | No `LIT`. | | TC-E27 | LimitIfTouched SELL | LIT sell with trigger + limit price. | No `LIT`. | ### TC-E20: StopMarket BUY | Field | Value | | ------------------ | --------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester places a stop-market buy above the current ask. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Stop order accepted on venue with correct trigger price and side=BUY. | | **Skip when** | Adapter does not support `StopMarket` orders. | **Considerations:** - The trigger price should be above the current ask by `stop_offset_ticks`. - The order should NOT trigger immediately (trigger price is above market). - For venues with long-lived trigger signatures, verify the trigger-order signing expiry uses the venue's trigger-order window rather than the normal order expiry. - Verifying trigger and fill requires the market to move, which may not happen during the test. - After acceptance, restart or force reconciliation and verify the order still appears as an open order report when the venue keeps trigger orders in a separate endpoint. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=False, enable_limit_sells=False, enable_stop_buys=True, enable_stop_sells=False, stop_order_type=OrderType.STOP_MARKET, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(false) .enable_limit_sells(false) .enable_stop_buys(true) .enable_stop_sells(false) .stop_order_type(OrderType::StopMarket) .build()? ``` ### TC-E21: StopMarket SELL | Field | Value | | ------------------ | ---------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester places a stop-market sell below the current bid. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Stop order accepted on venue with correct trigger price and side=SELL. | | **Skip when** | Adapter does not support `StopMarket` orders. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=False, enable_limit_sells=False, enable_stop_buys=False, enable_stop_sells=True, stop_order_type=OrderType.STOP_MARKET, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(false) .enable_limit_sells(false) .enable_stop_buys(false) .enable_stop_sells(true) .stop_order_type(OrderType::StopMarket) .build()? ``` ### TC-E22: StopLimit BUY | Field | Value | | ------------------ | --------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester places a stop-limit buy with trigger price above ask and limit offset. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Stop-limit order accepted with correct trigger price, limit price, and side=BUY. | | **Skip when** | Adapter does not support `StopLimit` orders. | **Considerations:** - Requires `stop_limit_offset_ticks` to be set for the limit price offset from the trigger price. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=False, enable_limit_sells=False, enable_stop_buys=True, enable_stop_sells=False, stop_order_type=OrderType.STOP_LIMIT, stop_limit_offset_ticks=50, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(false) .enable_limit_sells(false) .enable_stop_buys(true) .enable_stop_sells(false) .stop_order_type(OrderType::StopLimit) .stop_limit_offset_ticks(50) .build()? ``` ### TC-E23: StopLimit SELL | Field | Value | | ------------------ | --------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester places a stop-limit sell with trigger price below bid. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Stop-limit order accepted with correct trigger price, limit price, and side=SELL. | | **Skip when** | Adapter does not support `StopLimit` orders. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=False, enable_limit_sells=False, enable_stop_buys=False, enable_stop_sells=True, stop_order_type=OrderType.STOP_LIMIT, stop_limit_offset_ticks=50, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(false) .enable_limit_sells(false) .enable_stop_buys(false) .enable_stop_sells(true) .stop_order_type(OrderType::StopLimit) .stop_limit_offset_ticks(50) .build()? ``` ### TC-E24: MarketIfTouched BUY | Field | Value | | ------------------ | ---------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | Place MIT buy with trigger below current bid (buy on dip). | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | MIT order accepted on venue with correct trigger price. | | **Skip when** | Adapter does not support `MarketIfTouched` orders. | ### TC-E25: MarketIfTouched SELL | Field | Value | | ------------------ | -------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | Place MIT sell with trigger above current ask (sell on rally). | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | MIT order accepted on venue with correct trigger price. | | **Skip when** | Adapter does not support `MarketIfTouched` orders. | ### TC-E26: LimitIfTouched BUY | Field | Value | | ------------------ | -------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | Place LIT buy with trigger below bid and limit price offset. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | LIT order accepted with correct trigger price and limit price. | | **Skip when** | Adapter does not support `LimitIfTouched` orders. | ### TC-E27: LimitIfTouched SELL | Field | Value | | ------------------ | -------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | Place LIT sell with trigger above ask and limit price offset. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | LIT order accepted with correct trigger price and limit price. | | **Skip when** | Adapter does not support `LimitIfTouched` orders. | --- ## Group 4: Order modification Test order modification (amend) and cancel-replace workflows. | TC | Name | Description | Skip when | | ------ | ------------------------- | ---------------------------------------------- | ------------------------ | | TC-E30 | Modify limit BUY price | Amend open limit buy to new price. | No modify support. | | TC-E31 | Modify limit SELL price | Amend open limit sell to new price. | No modify support. | | TC-E32 | Cancel-replace limit BUY | Cancel and resubmit limit buy at new price. | Never. | | TC-E33 | Cancel-replace limit SELL | Cancel and resubmit limit sell at new price. | Never. | | TC-E34 | Modify stop trigger price | Amend stop order trigger price. | No modify or no stop. | | TC-E35 | Cancel-replace stop order | Cancel and resubmit stop at new trigger price. | No stop orders. | | TC-E36 | Modify rejected | Modify on unsupported adapter. | Adapter supports modify. | `trigger_limit_order_maintenance_once=True` requires exactly one of the limit-order modify or cancel-replace modes and at least one enabled limit side. It is incompatible with bracket entries, `test_modify_rejected`, and two-sided batch limit submission. Configuration validation rejects these combinations. ### TC-E30: Modify limit BUY price | Field | Value | | ------------------ | ---------------------------------------------------------------------------- | | **Prerequisite** | Open GTC limit buy from TC-E10. | | **Action** | ExecTester modifies the open limit buy to a new price. | | **Event sequence** | `OrderPendingUpdate` -> `OrderUpdated`. | | **Pass criteria** | `OrderUpdated` event logged with the new price; order exits `PendingUpdate`. | | **Skip when** | Adapter does not support order modification. | **Considerations:** - `trigger_limit_order_maintenance_once=True` amends to a different valid price directly from the first `OrderAccepted`, preferring one tick more passive. A quiet book is not inconclusive. - The one-shot workflow stops further limit maintenance for that side after it starts, so the tester does not immediately amend the order back to the TOB target. - Without that flag, the modify waits for the order price to drift from the target TOB offset. No movement and no `OrderUpdated` is inconclusive, not a failure. - Verify the `OrderUpdated` log shows the expected price. If the event never arrives after a forced or drift-triggered amend, the order stays in `PendingUpdate` and the tester stops modifying it. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=False, modify_orders_to_maintain_tob_offset=True, trigger_limit_order_maintenance_once=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(false) .modify_orders_to_maintain_tob_offset(true) .trigger_limit_order_maintenance_once(true) .build()? ``` ### TC-E31: Modify limit SELL price | Field | Value | | ------------------ | ---------------------------------------------------------------------------- | | **Prerequisite** | Open GTC limit sell from TC-E11. | | **Action** | ExecTester modifies the open limit sell to a new price. | | **Event sequence** | `OrderPendingUpdate` -> `OrderUpdated`. | | **Pass criteria** | `OrderUpdated` event logged with the new price; order exits `PendingUpdate`. | | **Skip when** | Adapter does not support order modification. | **Considerations:** - Same one-shot trigger and quiet-book guidance as TC-E30. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=False, enable_limit_sells=True, modify_orders_to_maintain_tob_offset=True, trigger_limit_order_maintenance_once=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(false) .enable_limit_sells(true) .modify_orders_to_maintain_tob_offset(true) .trigger_limit_order_maintenance_once(true) .build()? ``` ### TC-E32: Cancel-replace limit BUY | Field | Value | | ------------------ | ----------------------------------------------------------------------------------------------------- | | **Prerequisite** | Open GTC limit buy. | | **Action** | ExecTester cancels and resubmits the limit buy at a new price. | | **Event sequence** | `OrderPendingCancel` -> `OrderCanceled` -> `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Original order canceled, new order accepted at updated price. | | **Skip when** | Never (cancel-replace is always available). | **Considerations:** - This is the universal alternative when the adapter does not support native modify. - Two distinct orders in the cache: the canceled original and the new replacement. - `trigger_limit_order_maintenance_once=True` requests one cancel directly from the first `OrderAccepted`, then submits the replacement only after `OrderCanceled`. - Without the one-shot trigger, cancel-replace waits for TOB drift. No movement and no replacement is inconclusive, not a failure. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=False, cancel_replace_orders_to_maintain_tob_offset=True, trigger_limit_order_maintenance_once=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(false) .cancel_replace_orders_to_maintain_tob_offset(true) .trigger_limit_order_maintenance_once(true) .build()? ``` ### TC-E33: Cancel-replace limit SELL | Field | Value | | ------------------ | ----------------------------------------------------------------------------------------------------- | | **Prerequisite** | Open GTC limit sell. | | **Action** | ExecTester cancels and resubmits the limit sell at a new price. | | **Event sequence** | `OrderPendingCancel` -> `OrderCanceled` -> `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Original order canceled, new order accepted at updated price. | | **Skip when** | Never. | **Considerations:** - Same one-shot trigger and quiet-book guidance as TC-E32. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=False, enable_limit_sells=True, cancel_replace_orders_to_maintain_tob_offset=True, trigger_limit_order_maintenance_once=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(false) .enable_limit_sells(true) .cancel_replace_orders_to_maintain_tob_offset(true) .trigger_limit_order_maintenance_once(true) .build()? ``` ### TC-E34: Modify stop trigger price | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------------------ | | **Prerequisite** | Open stop order from TC-E20 or TC-E22. | | **Action** | ExecTester modifies stop trigger price as market moves (`modify_stop_orders_to_maintain_offset=True`). | | **Event sequence** | `OrderPendingUpdate` -> `OrderUpdated`. | | **Pass criteria** | `OrderUpdated` event logged with the new trigger price; order exits `PendingUpdate`. | | **Skip when** | Adapter does not support native stop modify, or no stop order support. | **Considerations:** - Some venues allow limit-order modify but reject trigger-order replace. For those adapters, skip TC-E34 and run TC-E35 instead. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_stop_buys=True, modify_stop_orders_to_maintain_offset=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_stop_buys(true) .modify_stop_orders_to_maintain_offset(true) .build()? ``` ### TC-E35: Cancel-replace stop order | Field | Value | | ------------------ | ----------------------------------------------------------------------------------------------------- | | **Prerequisite** | Open stop order. | | **Action** | ExecTester cancels and resubmits stop at new trigger price. | | **Event sequence** | `OrderPendingCancel` -> `OrderCanceled` -> `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Original stop canceled, new stop accepted at updated trigger price. | | **Skip when** | No stop order support. | **Considerations:** - This is the required path for venues that do not support native trigger-order replace. - After the new stop is accepted, restart or force reconciliation and verify exactly one current trigger order remains. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_stop_buys=True, cancel_replace_stop_orders_to_maintain_offset=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_stop_buys(true) .cancel_replace_stop_orders_to_maintain_offset(true) .build()? ``` ### TC-E36: Modify rejected | Field | Value | | ------------------ | ---------------------------------------------------------------------------------------------------- | | **Prerequisite** | Open limit order, adapter does NOT support modify. | | **Action** | Attempt to modify the order (programmatically, not via ExecTester auto-maintain). | | **Event sequence** | `OrderModifyRejected`. | | **Pass criteria** | Modify attempt results in `OrderModifyRejected` event with reason; original order remains unchanged. | | **Skip when** | Adapter supports order modification. | **Considerations:** - This tests the adapter's rejection path, not the ExecTester's cancel-replace logic. - The rejection reason should indicate that modification is not supported. --- ## Group 5: Order cancellation Test order cancellation workflows. | TC | Name | Description | Skip when | | ------ | -------------------------- | ------------------------------------------------- | ---------------- | | TC-E40 | Cancel single limit order | Cancel an open limit order. | Never. | | TC-E41 | Cancel all on stop | Strategy stop cancels all open orders (default). | Never. | | TC-E42 | Individual cancels on stop | Cancel orders one-by-one on stop. | Never. | | TC-E43 | Batch cancel on stop | Cancel orders via batch API on stop. | No batch cancel. | | TC-E44 | Cancel already-canceled | Verify documented rejection or idempotent result. | Never. | ### TC-E40: Cancel single limit order | Field | Value | | ------------------ | --------------------------------------------------------------- | | **Prerequisite** | Open GTC limit order from TC-E10 or TC-E11. | | **Action** | Stop the strategy; ExecTester cancels the open limit order. | | **Event sequence** | `OrderPendingCancel` -> `OrderCanceled`. | | **Pass criteria** | Order status transitions to CANCELED; no open orders remaining. | | **Skip when** | Never. | **Considerations:** - `cancel_orders_on_stop=True` (default) triggers cancellation when the strategy stops. - Verify the `OrderCanceled` event contains the correct `venue_order_id`. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=False, cancel_orders_on_stop=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(false) .cancel_orders_on_stop(true) .build()? ``` ### TC-E41: Cancel all on stop | Field | Value | | ------------------ | -------------------------------------------------------------- | | **Prerequisite** | Multiple open orders (limit buy + limit sell from TC-E12). | | **Action** | Stop the strategy with `cancel_orders_on_stop=True` (default). | | **Event sequence** | For each order: `OrderPendingCancel` -> `OrderCanceled`. | | **Pass criteria** | All open orders canceled; no open orders remaining. | | **Skip when** | Never. | **Considerations:** - Default stop uses `cancel_all`. Some venues implement that as account-wide. Confirm the adapter's `CancelAllOrders` scope before running beside other open orders. Use `use_individual_cancels_on_stop` or `use_batch_cancel_on_stop` to isolate. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=True, cancel_orders_on_stop=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(true) .cancel_orders_on_stop(true) .build()? ``` ### TC-E42: Individual cancels on stop | Field | Value | | ------------------ | ------------------------------------------------------------------- | | **Prerequisite** | Multiple open orders. | | **Action** | Stop with `use_individual_cancels_on_stop=True`. | | **Event sequence** | Individual `OrderPendingCancel` -> `OrderCanceled` for each order. | | **Pass criteria** | Each order canceled individually; all orders reach CANCELED status. | | **Skip when** | Never. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=True, use_individual_cancels_on_stop=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(true) .use_individual_cancels_on_stop(true) .build()? ``` ### TC-E43: Batch cancel on stop | Field | Value | | ------------------ | ------------------------------------------------------------------------ | | **Prerequisite** | Multiple open orders, adapter supports batch cancel. | | **Action** | Stop with `use_batch_cancel_on_stop=True`. | | **Event sequence** | Batch `OrderPendingCancel` -> `OrderCanceled` for all orders. | | **Pass criteria** | All orders canceled via single batch request; all reach CANCELED status. | | **Skip when** | Adapter does not support batch cancel. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=True, use_batch_cancel_on_stop=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(true) .use_batch_cancel_on_stop(true) .build()? ``` ### TC-E44: Cancel already-canceled order | Field | Value | | ------------------ | ------------------------------------------------------------------------------------- | | **Prerequisite** | A previously canceled order (from TC-E40). | | **Action** | Attempt to cancel the same order again. | | **Event sequence** | `OrderCancelRejected` by default, or no new event for a documented idempotent result. | | **Pass criteria** | Result matches the adapter contract without a duplicate terminal event. | | **Skip when** | Never. | **Considerations:** - The default contract tests the adapter's error handling for invalid cancel requests. The rejection reason should indicate the order is not in a cancelable state. - `Strategy.cancel_order` and the order manager drop cancels for locally closed orders, so `ExecTester` cannot reach the adapter after TC-E40. A second `cancel_order` that never leaves the process is inconclusive. - A venue may treat an already-terminal cancel as idempotent. Document that disposition and use an adapter-focused test that submits `CancelOrder` with the venue order ID after the order is terminal. --- ## Group 6: Bracket orders Test bracket order submission (entry + take-profit + stop-loss). | TC | Name | Description | Skip when | | ------ | ---------------------------- | ----------------------------------------------- | ------------------- | | TC-E50 | Bracket BUY | Entry limit buy + TP limit sell + SL stop sell. | No bracket support. | | TC-E51 | Bracket SELL | Entry limit sell + TP limit buy + SL stop buy. | No bracket support. | | TC-E52 | Bracket entry fill activates | Verify TP/SL become active after entry fill. | No bracket support. | | TC-E53 | Bracket with post-only entry | Entry order uses post-only flag. | No bracket or PO. | ### TC-E50: Bracket BUY | Field | Value | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester submits a bracket order: limit buy entry + take-profit sell + stop-loss sell. | | **Event sequence** | Entry: `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`; TP and SL: `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Three orders created and accepted: entry below bid, TP above ask, SL below entry. | | **Skip when** | Adapter does not support bracket orders. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_brackets=True, bracket_entry_order_type=OrderType.LIMIT, bracket_offset_ticks=500, enable_limit_buys=True, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_brackets(true) .bracket_entry_order_type(OrderType::Limit) .bracket_offset_ticks(500) .enable_limit_buys(true) .enable_limit_sells(false) .build()? ``` ### TC-E51: Bracket SELL | Field | Value | | ------------------ | --------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester submits bracket: limit sell entry + TP buy + SL buy. | | **Event sequence** | Same pattern as TC-E50 but for sell side. | | **Pass criteria** | Three orders created and accepted on sell side. | | **Skip when** | Adapter does not support bracket orders. | ### TC-E52: Bracket entry fill activates TP/SL | Field | Value | | ------------------ | --------------------------------------------------------------------- | | **Prerequisite** | Bracket order from TC-E50 where entry order fills. | | **Action** | Entry order fills; verify contingent TP and SL orders activate. | | **Event sequence** | Entry: `OrderFilled`; TP and SL transition from contingent to active. | | **Pass criteria** | After entry fill, TP and SL orders are live on the venue. | | **Skip when** | Adapter does not support bracket orders. | **Considerations:** - This requires the entry order to actually fill, which may need aggressive pricing. - The TP/SL activation mechanism varies by venue (some activate immediately, some are OCA groups). ### TC-E53: Bracket with post-only entry | Field | Value | | ------------------ | ----------------------------------------------------------------------- | | **Prerequisite** | Adapter supports brackets and post-only. | | **Action** | Submit bracket with `use_post_only=True` (applied to entry and TP). | | **Event sequence** | Same as TC-E50 with post-only flag on entry. | | **Pass criteria** | Entry and TP orders accepted as post-only (maker); SL is not post-only. | | **Skip when** | No bracket support or no post-only support. | --- ## Group 7: Order flags Test order-level flags and special parameters. | TC | Name | Description | Skip when | | ------ | ------------------- | -------------------------------------------- | -------------------- | | TC-E60 | PostOnly accepted | Limit with post-only, placed away from TOB. | No post-only. | | TC-E61 | ReduceOnly on close | Close position with reduce-only flag. | No reduce-only. | | TC-E62 | Display quantity | Iceberg order with visible quantity < total. | No display quantity. | | TC-E63 | Custom order params | Adapter-specific params via `order_params`. | N/A. | ### TC-E60: PostOnly accepted | Field | Value | | ------------------ | ----------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester places limit buy with `use_post_only=True` at passive price. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order accepted as a maker order; post-only flag acknowledged by venue. | | **Skip when** | Adapter does not support post-only flag. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=False, use_post_only=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(false) .use_post_only(true) .build()? ``` ### TC-E61: ReduceOnly on close | Field | Value | | ------------------ | ---------------------------------------------------------------------------------------------- | | **Prerequisite** | Open position (from TC-E01). | | **Action** | Stop strategy with `reduce_only_on_stop=True`; closing order uses reduce-only flag. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled` (with reduce-only). | | **Pass criteria** | Closing order has reduce-only flag; position fully closed. | | **Skip when** | Adapter does not support reduce-only flag. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), open_position_on_start_qty=Decimal("0.01"), reduce_only_on_stop=True, close_positions_on_stop=True, enable_limit_buys=False, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .open_position_on_start_qty(Decimal::new(1, 2)) .reduce_only_on_stop(true) .close_positions_on_stop(true) .enable_limit_buys(false) .enable_limit_sells(false) .build()? ``` ### TC-E62: Display quantity (iceberg) | Field | Value | | ------------------ | ------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, adapter supports display quantity. | | **Action** | Place limit order with `order_display_qty` < `order_qty`. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order accepted with display quantity set; only display qty visible on the book. | | **Skip when** | Adapter does not support display quantity / iceberg orders. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("1.0"), order_display_qty=Quantity.from_str("0.1"), enable_limit_buys=True, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("1.0")) .enable_limit_buys(true) .enable_limit_sells(false) .order_display_qty(Quantity::from("0.1")) .build()? ``` ### TC-E63: Custom order params | Field | Value | | ------------------ | ---------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, adapter accepts additional parameters. | | **Action** | Place order with `order_params` dict containing adapter-specific parameters. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order accepted; adapter-specific parameters passed through to venue. | | **Skip when** | N/A (adapter-specific). | **Considerations:** - The `order_params` dict is opaque to the ExecTester and passed through to the adapter. - Consult the adapter's guide for supported parameters. --- ## Group 8: Rejection handling Test that the adapter correctly handles and reports order rejections. | TC | Name | Description | Skip when | | ------ | ---------------------- | ------------------------------------------------ | --------------- | | TC-E70 | PostOnly rejection | Post-only order that would cross the spread. | No post-only. | | TC-E71 | ReduceOnly rejection | Reduce-only order with no position to reduce. | No reduce-only. | | TC-E72 | Unsupported order type | Submit order type not supported by adapter. | Never. | | TC-E73 | Unsupported TIF | Submit order with unsupported time in force. | Never. | | TC-E74 | Ambiguous submit fail | Transport, timeout, or send failure on submit. | No mock path. | | TC-E75 | Ambiguous cancel fail | Transport, timeout, or send failure on cancel. | No cancel. | | TC-E76 | Ambiguous modify fail | Transport, timeout, or send failure on modify. | No modify. | | TC-E77 | Ambiguous batch fail | Whole-batch failure without per-order result. | No batch. | | TC-E78 | Per-order batch reject | Batch response has explicit per-order rejection. | No batch. | TC-E74 through TC-E78 are specified collectively below because they usually require a mock HTTP or WebSocket boundary rather than a live venue. ### Ambiguous outcome failures These cases prove that adapter request failures do not turn into terminal rejection events when the venue outcome is unknown. The pass criteria also define the local prepare-failure carve-out: when a command is known not to have been sent and is attributable to one cancel or modify command, the adapter may emit the matching rejection event. **Pass criteria:** - Submit failures from transport errors, timeouts, WebSocket send failures, retry exhaustion, or response parse failures do not emit `OrderRejected`. - Cancel failures from transport errors, timeouts, WebSocket send failures, retry exhaustion, or whole-request server failures do not emit `OrderCancelRejected`. - Modify failures from transport errors, timeouts, WebSocket send failures, retry exhaustion, or whole-request server failures do not emit `OrderModifyRejected`. - Local cancel prepare failures that prove the command cannot be sent may emit `OrderCancelRejected` when the adapter can attribute the failure to one cancel command. - Local modify prepare failures that prove the command cannot be sent may emit `OrderModifyRejected` when the adapter can attribute the failure to one modify command. - Whole-batch request failures do not emit one rejection per order when the venue did not return per-order results. - Explicit per-order venue rejections still emit the matching rejection event with the venue reason. The order remains in the appropriate in-flight state until a venue update, query result, or reconciliation pass resolves it. ### TC-E70: PostOnly rejection | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, quotes flowing. | | **Action** | ExecTester places post-only order on the wrong side of the book (`test_reject_post_only=True`), causing it to cross the spread. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderRejected`. | | **Pass criteria** | Venue rejects order; `OrderRejected.due_post_only=true`; reason names post-only violation. | | **Skip when** | Adapter does not support post-only flag. | **Considerations:** - The ExecTester's `test_reject_post_only` mode intentionally prices the order to cross. - Some venues may partially fill instead of rejecting; behavior is venue-specific. - Adapters that emit `OrderRejected` for a post-only crossing reject should set `due_post_only=true` so strategies can distinguish this from other venue rejections. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), enable_limit_buys=True, enable_limit_sells=False, use_post_only=True, test_reject_post_only=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .enable_limit_buys(true) .enable_limit_sells(false) .use_post_only(true) .test_reject_post_only(true) .build()? ``` ### TC-E71: ReduceOnly rejection | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Prerequisite** | Adapter connected, no open position for the instrument. | | **Action** | ExecTester opens a market position with `reduce_only=True` via `test_reject_reduce_only=True` and `open_position_on_start_qty`, when no position exists to reduce. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderRejected`. | | **Pass criteria** | Order rejected; `OrderRejected` event with reason indicating reduce-only violation. | | **Skip when** | Adapter does not support reduce-only flag. | **Considerations:** - The `test_reject_reduce_only` flag only applies to the opening market order submitted via `open_position_on_start_qty`. - Verify no prior position exists for the instrument before running this test. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), open_position_on_start_qty=Decimal("0.01"), test_reject_reduce_only=True, enable_limit_buys=False, enable_limit_sells=False, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .open_position_on_start_qty(Decimal::new(1, 2)) .test_reject_reduce_only(true) .enable_limit_buys(false) .enable_limit_sells(false) .build()? ``` ### TC-E72: Unsupported order type | Field | Value | | ------------------ | -------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, order type not in adapter's supported set. | | **Action** | Submit an order type the adapter does not support. | | **Event sequence** | `OrderDenied` (pre-submission rejection by adapter). | | **Pass criteria** | Order denied before reaching venue; `OrderDenied` event with reason. | | **Skip when** | Never (every adapter has unsupported order types to test). | **Considerations:** - `OrderDenied` occurs at the adapter level before the order reaches the venue. - This differs from the normal `OrderRejected` path, which follows a venue rejection. Reconciliation can also synthesize `OrderRejected`; see [Terminal reconciliation provenance](../concepts/execution/policies.md#terminal-reconciliation-provenance). - Test by configuring a stop order type that the adapter does not support. ### TC-E73: Unsupported TIF | Field | Value | | ------------------ | -------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, TIF not in adapter's supported set. | | **Action** | Submit an order with a TIF the adapter does not support. | | **Event sequence** | `OrderDenied` (pre-submission rejection by adapter). | | **Pass criteria** | Order denied before reaching venue; `OrderDenied` event with reason. | | **Skip when** | Never (every adapter has unsupported TIF options to test). | **Considerations:** - Similar to TC-E72 but for time-in-force options. - Test with TIF values from the Nautilus enum that the adapter does not map. --- ## Group 9: Lifecycle and reconciliation Test strategy lifecycle behavior and execution-state recovery on start, stop, and report failure. TC-E88 and TC-E89 run offline as deterministic adapter unit or integration tests rather than through `ExecTester` against a venue. | TC | Name | Description | Skip when | | ------ | --------------------------------- | -------------------------------------------------------- | ------------------------------------------- | | TC-E80 | Open position on start | Open a position immediately when strategy starts. | No market orders. | | TC-E81 | Cancel orders on stop | Cancel all open orders when strategy stops. | Never. | | TC-E82 | Close positions on stop | Close open positions when strategy stops. | No market orders. | | TC-E83 | Unsubscribe on stop | Unsubscribe from data feeds on strategy stop. | No unsub support. | | TC-E84 | Reconcile open orders | Reconcile existing open orders from a prior session. | Never. | | TC-E85 | Reconcile filled orders | Reconcile previously filled orders from a prior session. | Never. | | TC-E86 | Reconcile open long | Reconcile existing open long position. | Never. | | TC-E87 | Reconcile open short | Reconcile existing open short position. | Never. | | TC-E88 | Reconciliation commission failure | A required fill commission cannot be represented. | No fill commission logic. | | TC-E89 | WebSocket commission failure | A private fill commission cannot be represented. | No private fill stream or commission logic. | ### TC-E80: Open position on start | Field | Value | | ------------------ | -------------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, instrument loaded, no existing position. | | **Action** | Strategy starts with `open_position_on_start_qty` set. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled`. | | **Pass criteria** | Position opened on start; market order submitted and filled before limit order maintenance begins. | | **Skip when** | Adapter does not support market orders. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), open_position_on_start_qty=Decimal("0.01"), ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .open_position_on_start_qty(Decimal::new(1, 2)) .build()? ``` ### TC-E81: Cancel orders on stop | Field | Value | | ------------------ | -------------------------------------------------------------- | | **Prerequisite** | Open limit orders from the strategy session. | | **Action** | Stop the strategy with `cancel_orders_on_stop=True` (default). | | **Event sequence** | For each open order: `OrderPendingCancel` -> `OrderCanceled`. | | **Pass criteria** | All strategy-owned open orders canceled on stop. | | **Skip when** | Never. | ### TC-E82: Close positions on stop | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------ | | **Prerequisite** | Open position from the strategy session. | | **Action** | Stop the strategy with `close_positions_on_stop=True` (default). | | **Event sequence** | Closing order: `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled`. | | **Pass criteria** | Positions closed, or only exact sub-precision residuals remain; no open orders remain. | | **Skip when** | Adapter does not support market orders. | ### TC-E83: Unsubscribe on stop | Field | Value | | ------------------ | ---------------------------------------------------------------- | | **Prerequisite** | Active data subscriptions (quotes, trades, book). | | **Action** | Stop the strategy with `can_unsubscribe=True` (default). | | **Event sequence** | Data subscriptions removed. | | **Pass criteria** | No further data events received after stop; clean disconnection. | | **Skip when** | Adapter does not support unsubscribe. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, order_qty=Quantity.from_str("0.01"), can_unsubscribe=True, ) ``` **Rust config:** ```rust ExecTesterConfig::builder() .base(StrategyConfig { strategy_id: Some(strategy_id), ..Default::default() }) .instrument_id(instrument_id) .client_id(client_id) .order_qty(Quantity::from("0.01")) .can_unsubscribe(true) .build()? ``` ### TC-E84: Reconcile open orders | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | One or more open limit orders on the venue from a prior session. | | **Action** | Start the node with `reconciliation=True`. | | **Event sequence** | `OrderStatusReport` generated for each open order. | | **Pass criteria** | Each open order is loaded into the cache with correct `venue_order_id`, status=ACCEPTED, price, quantity, side, and order type. | | **Skip when** | Never. | **Considerations:** - Leave limit orders open from a prior test session (do not cancel on stop). - Configure `external_order_instrument_ids` so strategy registration creates the active claim used to assign reconciled orders. - Verify that the reconciled order count matches the venue-reported count. - Mass-status may include historical terminal orders. Unclaimed ones appear as EXTERNAL. Compare open-order counts against the venue open-order endpoint, not the full mass-status length. - Report `ts_event` values must be Unix nanoseconds even when the venue only has second precision. ### TC-E85: Reconcile filled orders | Field | Value | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | One or more filled orders on the venue from a prior session. | | **Action** | Start the node with `reconciliation=True`. | | **Event sequence** | `FillReport` generated for each historical fill. | | **Pass criteria** | Each filled order is loaded into the cache with correct `venue_order_id`, status=FILLED, fill price, fill quantity, and commission. | | **Skip when** | Never. | **Considerations:** - Requires orders that filled in a prior session. - Verify fill price, quantity, and commission match the venue's reported values. - Some adapters may only report fills within a lookback window. - Historical fill replay can change a live zero commission to the REST fee. That flip is expected when the stream omits fees and reconciliation supplies them. - Report `ts_event` values must be Unix nanoseconds even when the venue only has second precision. ### TC-E86: Reconcile open long position | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------------------------ | | **Prerequisite** | An open long position on the venue from a prior session. | | **Action** | Start the node with `reconciliation=True`. | | **Event sequence** | `PositionStatusReport` generated for the long position. | | **Pass criteria** | Position loaded into cache with correct instrument, side=LONG, quantity, and entry price matching the venue. | | **Skip when** | Never. | **Considerations:** - Open a long position in a prior session and stop the strategy without closing it (`close_positions_on_stop=False`). - Verify the reconciled position quantity and average entry price match the venue. - After reconciliation, the strategy should be able to manage or close this position. ### TC-E87: Reconcile open short position | Field | Value | | ------------------ | ------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | An open short position on the venue from a prior session. | | **Action** | Start the node with `reconciliation=True`. | | **Event sequence** | `PositionStatusReport` generated for the short position. | | **Pass criteria** | Position loaded into cache with correct instrument, side=SHORT, quantity, and entry price matching the venue. | | **Skip when** | Never. | **Considerations:** - Open a short position in a prior session and stop the strategy without closing it (`close_positions_on_stop=False`). - Verify the reconciled position quantity and average entry price match the venue. - After reconciliation, the strategy should be able to manage or close this position. ### TC-E88: Reconciliation commission failure | Field | Value | | ------------------ | --------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | A deterministic fixture produces an out-of-range commission for an owned, confirmed fill. | | **Action** | Exercise direct fill, mass-status, targeted report, and inferred-fill commission paths with the fixture. | | **Event sequence** | Report requests return an error; engine reconciliation logs a hook error and emits no fallback inferred fill. | | **Pass criteria** | The error is observable; startup fails or inferred work defers; a later valid response reconciles exactly once. | | **Skip when** | The adapter does not calculate commission for fill reports. | **Considerations:** - Assert that no zero or generic commission replaces the failed venue calculation. - Exercise the inferred-fill hook as well as direct `FillReport` construction. - Skip the inferred-fill portion when the adapter does not override the shared commission hook. - Assert that hook quantity, price, and liquidity inputs match the emitted inferred fill, including the back-solved price of an incremental residual after a prior fill. - Valid explicit fills may apply, but the residual quantity and dependent terminal transition stay pending. - A position-only synthetic correction without trade evidence is not a commission calculation failure. ### TC-E89: WebSocket commission failure | Field | Value | | ------------------ | -------------------------------------------------------------------------------------------------------------------- | | **Prerequisite** | A private fill fixture produces an out-of-range commission and has a stable venue trade ID. | | **Action** | Deliver the invalid trade, replace its commission with a valid value, then redeliver the same trade ID. | | **Event sequence** | The first delivery emits no fill and changes no fill, terminal, or deduplication state; the replay emits one fill. | | **Pass criteria** | No panic or fallback occurs; the valid replay applies exactly once; REST reconciliation can recover a missed replay. | | **Skip when** | The adapter has no private fill stream or does not calculate commission for private fills. | **Considerations:** - For a venue trade that fills several owned orders, make one report fail and assert that the first delivery emits none of them. This prevents a replay from duplicating reports built before the failure. - Assert that the adapter consumes the deduplication key only after all reports construct and route successfully. - Assert that a failed trade does not confirm or terminalize its order. --- ## Group 10: Options trading Test options-specific execution behavior. Options instruments typically have different constraints from linear derivatives: venues may restrict order types, support alternative pricing modes, or disallow conditional orders. Exact restrictions vary by venue; consult the adapter guide. These tests require a `CryptoOption` instrument. Use an OTM option with reasonable liquidity for fills. | TC | Name | Description | Skip when | | ------- | ----------------------------- | --------------------------------------------------------------------- | ------------------- | | TC-E90 | Limit BUY option | Place a limit buy on an option instrument. | No options support. | | TC-E91 | Limit SELL option | Place a limit sell on an option instrument. | No options support. | | TC-E92 | Limit with alt pricing | Place a limit order with adapter-specific pricing via `order_params`. | No alt pricing. | | TC-E94 | Unsupported order type denied | Submit an order type the adapter rejects for options. | No options support. | | TC-E96 | Conditional order rejected | Submit a stop/conditional order on an option; expect rejection. | No options support. | | TC-E99 | FOK limit option | Place a FOK limit order on an option instrument. | No FOK options. | | TC-E100 | Cancel option order | Cancel an open limit order on an option instrument. | No options support. | | TC-E101 | Reconcile option position | Reconcile an open option position from a prior session. | No options support. | ### TC-E90: Limit BUY option | Field | Value | | ------------------ | --------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, option instrument loaded, quotes flowing. | | **Action** | ExecTester places a limit buy on the option at a passive price. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order accepted by venue with correct instrument, side, price, and quantity. | | **Skip when** | Adapter does not support options trading. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, # CryptoOption instrument order_qty=Quantity.from_str("1"), enable_limit_buys=True, enable_limit_sells=False, tob_offset_ticks=500, ) ``` ### TC-E91: Limit SELL option | Field | Value | | ------------------ | --------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, option instrument loaded, quotes flowing. | | **Action** | ExecTester places a limit sell on the option at a passive price. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order accepted by venue with correct instrument, side, price, and quantity. | | **Skip when** | Adapter does not support options trading. | **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, # CryptoOption instrument order_qty=Quantity.from_str("1"), enable_limit_buys=False, enable_limit_sells=True, tob_offset_ticks=500, ) ``` ### TC-E92: Limit with alternative pricing | Field | Value | | ------------------ | ------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, option instrument loaded. | | **Action** | Place limit order with adapter-specific pricing via `order_params`. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted`. | | **Pass criteria** | Order accepted; venue acknowledges the alternative pricing mode. | | **Skip when** | Adapter does not support alternative pricing modes for options. | **Considerations:** - The `price` field on the order object may be a placeholder when alternative pricing is active. Consult the adapter guide for supported parameter keys. - Example: OKX supports `px_usd` (USD price) and `px_vol` (implied volatility). - Verify in venue responses that the pricing mode is reflected correctly. **Python config:** ```python ExecTesterConfig( instrument_id=instrument_id, # CryptoOption instrument order_qty=Quantity.from_str("1"), enable_limit_buys=True, enable_limit_sells=False, order_params={"px_usd": "100.5"}, # Adapter-specific pricing key ) ``` ### TC-E94: Unsupported order type denied for options | Field | Value | | ------------------ | ----------------------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, option instrument loaded. | | **Action** | Submit an order type the venue does not support for options (e.g. market order). | | **Event sequence** | Adapter-dependent: `OrderDenied` (pre-submission) or `OrderSubmitted` -> `OrderRejected` (post-submission). | | **Pass criteria** | Order does not fill. Denial or rejection reason references the unsupported order type. | | **Skip when** | Adapter does not support options. | **Considerations:** - The exact rejection point varies by adapter. Some adapters deny locally before submitting; others submit and relay the venue rejection. - ExecTester can trigger a market order via `open_position_on_start_qty` on an option instrument. Some unsupported types (e.g. `MarketToLimit`) require manual or programmatic submission. - Test each unsupported type the adapter documents. ### TC-E96: Conditional order rejected for options | Field | Value | | ------------------ | ----------------------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, option instrument loaded. | | **Action** | Submit a conditional order on an option instrument. | | **Event sequence** | Adapter-dependent: `OrderDenied` (pre-submission) or `OrderSubmitted` -> `OrderRejected` (post-submission). | | **Pass criteria** | Order does not fill. Reason references unsupported conditional order type. | | **Skip when** | Adapter does not support options, or adapter supports conditionals for options. | **Considerations:** - Test each conditional type the adapter documents as unsupported for options (e.g. `STOP_MARKET`, `STOP_LIMIT`, `MARKET_IF_TOUCHED`, `LIMIT_IF_TOUCHED`, `TRAILING_STOP_MARKET`). - ExecTester can trigger conditional orders via `enable_stop_buys`/`enable_stop_sells` with `stop_order_type` on an option instrument. ### TC-E99: FOK limit option | Field | Value | | ------------------ | ---------------------------------------------------------------------------------------------- | | **Prerequisite** | Adapter connected, option instrument loaded, sufficient book depth. | | **Action** | Place a limit order with `TimeInForce::Fok` on an option instrument. | | **Event sequence** | `OrderInitialized` -> `OrderSubmitted` -> `OrderAccepted` -> `OrderFilled` or `OrderCanceled`. | | **Pass criteria** | Order fills completely or is canceled. No partial fills. | | **Skip when** | Adapter does not support FOK for options. | **Considerations:** - Some venues use a dedicated order type for options FOK orders (e.g. OKX uses `op_fok`). The adapter handles this mapping transparently. - Use small quantities and aggressive pricing to get a fill for the positive case. ### TC-E100: Cancel option order | Field | Value | | ------------------ | -------------------------------------------------------------- | | **Prerequisite** | Open limit order from TC-E90 or TC-E91. | | **Action** | Cancel the open limit order. | | **Event sequence** | `OrderPendingCancel` -> `OrderCanceled`. | | **Pass criteria** | Order canceled; no longer appears in open orders on the venue. | | **Skip when** | Adapter does not support options. | ### TC-E101: Reconcile option position | Field | Value | | ------------------ | ------------------------------------------------------------------------------------ | | **Prerequisite** | Open option position from a prior session. | | **Action** | Start the node with `reconciliation=True`. | | **Event sequence** | `PositionStatusReport` generated for the option position. | | **Pass criteria** | Position loaded into cache with correct instrument, side, quantity, and entry price. | | **Skip when** | Adapter does not support options. | **Considerations:** - Open an option position in a prior session and stop without closing (`close_positions_on_stop=False`). - Verify the reconciled position matches the venue-reported state. --- ## ExecTester configuration reference Quick reference for every Python `ExecTesterConfig` parameter. Defaults are resolved values after construction; the Rust builder uses equivalent defaults. | Parameter | Type | Default | Affects groups | | ----------------------------------------------- | --------------------- | ---------------------- | -------------- | | `strategy_id` | `StrategyId?` | `None` | All | | `order_id_tag` | `str?` | `None` | All | | `use_hyphens_in_client_order_ids` | `bool` | `True` | All | | `use_uuid_client_order_ids` | `bool` | `False` | All | | `external_order_instrument_ids` | `list[InstrumentId]?` | `None` | 9 | | `instrument_id` | `InstrumentId` | `BTCUSDT-PERP.BINANCE` | All | | `client_id` | `ClientId?` | `None` | All | | `order_qty` | `Quantity` | `0.001` | All | | `order_display_qty` | `Quantity?` | `None` | 2, 7 | | `order_expire_time_delta_mins` | `PositiveInt?` | `None` | 2 | | `order_params` | `dict?` | `None` | 7, 10 | | `subscribe_book` | `bool` | `False` | | | `subscribe_quotes` | `bool` | `True` | | | `subscribe_trades` | `bool` | `True` | | | `book_type` | `BookType` | `L2_MBP` | | | `book_depth` | `PositiveInt?` | `None` | | | `book_interval_ms` | `PositiveInt` | `1000` | | | `book_levels_to_print` | `PositiveInt` | `10` | | | `open_position_on_start_qty` | `Decimal?` | `None` | 1, 9 | | `open_position_on_first_quote` | `bool` | `False` | 1 | | `open_position_time_in_force` | `TimeInForce` | `GTC` | 1 | | `enable_limit_buys` | `bool` | `True` | 2, 4, 5, 6 | | `enable_limit_sells` | `bool` | `True` | 2, 4, 5, 6 | | `enable_stop_buys` | `bool` | `False` | 3, 4 | | `enable_stop_sells` | `bool` | `False` | 3, 4 | | `tob_offset_ticks` | `PositiveInt` | `500` | 2, 4 | | `limit_time_in_force` | `TimeInForce?` | `None` | 2, 6 | | `stop_order_type` | `OrderType` | `STOP_MARKET` | 3 | | `stop_offset_ticks` | `PositiveInt` | `100` | 3 | | `stop_limit_offset_ticks` | `PositiveInt?` | `None` | 3 | | `stop_trigger_type` | `TriggerType` | `DEFAULT` | 3 | | `stop_time_in_force` | `TimeInForce?` | `None` | 3 | | `trailing_offset` | `Decimal?` | `None` | 3 | | `trailing_offset_type` | `TrailingOffsetType` | `BASIS_POINTS` | 3 | | `enable_brackets` | `bool` | `False` | 6 | | `batch_submit_limit_pair` | `bool` | `False` | 2, 5 | | `bracket_entry_order_type` | `OrderType` | `LIMIT` | 6 | | `bracket_offset_ticks` | `PositiveInt` | `500` | 6 | | `modify_orders_to_maintain_tob_offset` | `bool` | `False` | 4 | | `modify_stop_orders_to_maintain_offset` | `bool` | `False` | 4 | | `cancel_replace_orders_to_maintain_tob_offset` | `bool` | `False` | 4 | | `cancel_replace_stop_orders_to_maintain_offset` | `bool` | `False` | 4 | | `trigger_limit_order_maintenance_once` | `bool` | `False` | 4 | | `use_post_only` | `bool` | `False` | 2, 6, 7, 8 | | `limit_aggressive` | `bool` | `False` | 2 | | `use_quote_quantity` | `bool` | `False` | 1, 7 | | `emulation_trigger` | `TriggerType?` | `None` | 2, 3 | | `use_individual_cancels_on_stop` | `bool` | `False` | 5 | | `cancel_orders_on_stop` | `bool` | `True` | 5, 9 | | `close_positions_on_stop` | `bool` | `True` | 9 | | `close_positions_qty_precision` | `int?` | `None` | 9 | | `close_positions_time_in_force` | `TimeInForce?` | `None` | 9 | | `reduce_only_on_stop` | `bool` | `True` | 7, 9 | | `use_batch_cancel_on_stop` | `bool` | `False` | 5 | | `dry_run` | `bool` | `False` | | | `log_data` | `bool` | `True` | | | `test_reject_post_only` | `bool` | `False` | 8 | | `test_reject_reduce_only` | `bool` | `False` | 8 | | `test_modify_rejected` | `bool` | `False` | 4 | | `can_unsubscribe` | `bool` | `True` | 9 | | `clamp_to_instrument_price_range` | `bool` | `False` | 1-8, 10 | | `log_events` | `bool` | `True` | All | | `log_commands` | `bool` | `True` | All | # Test Datasets Source: https://nautilustrader.io/docs/latest/developer_guide/test_datasets/ Target standards for curating, storing, and consuming external datasets used as test fixtures. New datasets should follow these standards. Existing datasets that predate this policy are documented under [legacy datasets](#legacy-datasets). ## Dataset categories **Small data** (< 1 MB) is checked directly into `test_data//` alongside a `metadata.json` file. These files are always available without network access. **Large data** (> 1 MB) is hosted as Parquet in the R2 test-data bucket. A SHA-256 checksum is recorded in `test_data/large/checksums.json`. Before running tests that use large data, prepare the fixtures from the repository root: ```bash cargo run --locked -p nautilus-testkit --bin prepare-test-data ``` This command downloads missing files and verifies every fixture in the tracked checksum manifest. It replaces cached files whose checksums differ, leaves the manifest unchanged, and rejects and removes downloads with mismatched checksums. CI runs this setup after restoring the test-data cache. The `ensure_test_data_exists()` function only checks for a local file. A test that needs a missing fixture fails with a message naming the setup command, without downloading data. Setup and tests both honor `TEST_DATA_ROOT_PATH`. **User-fetched data** is used when a vendor license, entitlement model, or access control does not allow NautilusTrader to redistribute the data through the public repo or the public R2 bucket. In this model, the repo stores only a manifest, fetch instructions, and the transform code. Each user downloads the source data with their own vendor account and converts it locally. Use the user-fetched model when any of the following apply: - The vendor requires each user to hold their own account, API key, or historical-data license. - The license allows internal use but does not clearly allow redistribution of derived fixtures. - The dataset is suitable for examples or opt-in integration tests, but not for default CI. ## Required metadata Every curated dataset that stores or redistributes a concrete artifact must include a `metadata.json` with at minimum: | Field | Description | | -------------- | ------------------------------------------------- | | `file` | Filename of the dataset. | | `sha256` | SHA-256 hash of the file. | | `size_bytes` | File size in bytes. | | `original_url` | Download URL of the original source data. | | `licence` | License terms and any redistribution constraints. | | `added_at` | ISO 8601 timestamp when the dataset was curated. | These fields match the output of `scripts/curate-dataset.sh`. Additional recommended fields for richer provenance: | Field | Description | | --------------- | --------------------------------------------------------- | | `instrument` | Instrument symbol(s) covered. | | `date` | Trading date(s) covered. | | `format` | Storage format (e.g., "Nautilus OrderBookDelta Parquet"). | | `original_file` | Original vendor filename before transformation. | | `parser` | Parser used for transformation (e.g., "itchy 0.3.4"). | User-fetched datasets use the same metadata fields where they apply. They should also include: | Field | Description | | ------------------- | -------------------------------------------------------------------- | | `distribution` | Must be `"user-fetch"`. | | `fetch_method` | How the user acquires the source data (API, web portal, CLI, etc.). | | `fetch_reference` | URL or document reference for the user-facing download flow. | | `auth` | Required credentials or entitlements, if any. | | `transform_version` | Version of the local transform pipeline that builds the final files. | | `redistribution` | Short note describing redistribution limits for the dataset. | | `public_mirror` | Must be `false` for restricted vendor datasets. | For user-fetched datasets without a single committed or mirrored artifact, `file`, `sha256`, and `size_bytes` may be omitted from `metadata.json`. In that case, `target_files` in `manifest.json` is authoritative for the local output files. For user-fetched datasets, `original_url` may point to the vendor download entry point rather than the exact file URL when the exact file is generated per user account or per request. Other metadata fields remain recommended where they apply. In particular, `licence` and `added_at` should still be recorded for user-fetched datasets. ## Storage format New datasets should be stored as **Nautilus Parquet** (not raw vendor formats). This ensures: - Consistent data types across all test datasets. - No vendor format parsing at test time. - Clear derivative-work status for licensing. Use ZSTD compression (level 3) with 1M row groups. User-fetched datasets should also end up as Nautilus Parquet after the local transform step. Raw vendor files should stay outside the repo and outside the public R2 bucket. ## Naming convention ``` ___.parquet ``` Examples: - `itch_AAPL_2019-01-30_deltas.parquet` - `tardis_BTCUSDT_2020-09-01_depth10.parquet` - `histdata_EURUSD.SIM_2020-01_quotes.parquet` ## Curation workflow ### Simple files (single download) Use `scripts/curate-dataset.sh`: ```bash scripts/curate-dataset.sh ``` This creates a versioned directory (`v1//`) with the file, `LICENSE.txt`, and `metadata.json` containing the required fields above. ### Complex pipelines (parse + transform) For datasets requiring format conversion (e.g., binary ITCH to Parquet): 1. Write a curation function in `crates/testkit/src//` gated behind `#[cfg(test)]` or an `#[ignore]` test. 2. The function should: download, parse, filter, convert to NautilusTrader types, write Parquet. 3. Output the Parquet file and `metadata.json` to a local directory. 4. Upload to R2 manually, then add the checksum to `checksums.json`. ### User-fetched pipelines (restricted redistribution) For datasets that NautilusTrader cannot redistribute: 1. Commit a manifest and `metadata.json`, but do not commit the real vendor data or derived Parquet output. 2. Provide a local fetch command or script that uses the user's own vendor credentials, entitlements, or purchased historical files. 3. Convert the vendor data locally into Nautilus Parquet. 4. Store the resulting files in a local cache path that is ignored by git. 5. Make tests and examples opt in. They should skip cleanly when the dataset is missing. The default distribution order for new datasets is: 1. Checked in small data. 2. Public R2 large data. 3. User-fetched data. Choose user-fetched only when the first two options are not acceptable under the vendor's terms. Do not: - Upload restricted vendor datasets to the public R2 bucket. - Commit real vendor-derived Parquet files to the repo when redistribution rights are unclear. - Make default CI depend on vendor credentials or paid historical-data access. You may maintain a private mirror for internal CI or employees when the license permits internal sharing. Treat this as a separate operational path, not as part of the public test-data standard. ## Adding a new dataset 1. Curate the data following the workflow above. 2. Write `metadata.json` with all required fields. 3. For small data: commit to `test_data//`. 4. For large data: upload Parquet to R2, add checksum to `test_data/large/checksums.json`. 5. For user-fetched data: commit the manifest and fetch instructions only. Keep the source and derived data out of the repo and out of the public R2 bucket. 6. Add shared test-data path functions to `crates/testkit/src/common.rs` when needed. 7. Write tests that consume the dataset. For user-fetched data, prefer this layout: ```text test_data/// metadata.json manifest.json README.md ``` Use `test_data/local///` as the standard local cache path for generated artifacts. Keep raw vendor downloads in a sibling `vendor/` directory under the same cache path when local retention is needed. The manifest should be machine-readable and stable. It should capture the minimum information needed to reproduce the fetch and transform steps on another machine. `metadata.json` is authoritative for provenance, licensing, and redistribution rules. `manifest.json` is authoritative for fetch inputs, commands, cache locations, and output files. Recommended manifest fields: | Field | Description | | ------------------- | ----------------------------------------------------------- | | `slug` | Stable dataset identifier. | | `vendor` | Vendor or venue name. | | `source_type` | `api`, `portal-download`, `purchased-archive`, etc. | | `source_filters` | Symbols, event IDs, market IDs, date ranges, or file names. | | `target_files` | Output Nautilus Parquet files expected after conversion. | | `cache_dir` | Local output location relative to `test_data/local/`. | | `fetch_command` | Suggested command or script entry point. | | `transform_command` | Suggested local conversion command. | | `env` | Required environment variables. | | `notes` | Short operational notes for users. | Tests that rely on user-fetched data should: - Be marked or grouped separately from default CI tests. - Skip with a clear message when the local dataset is absent. - Avoid network access unless the user explicitly opts in. - Reuse a stable local cache path so the fetch happens once per machine. For pytest-based tests, prefer a guard like: ```python if not filepath.exists(): pytest.skip(f"User-fetched test data not found: {filepath}") ``` For Rust tests that require manual dataset preparation, prefer `#[ignore]` when the test is not expected to run in default CI. ## Regenerating datasets When a schema change invalidates a large Parquet file, regenerate it from the original source data using the curation tests below. After regenerating: 1. `sha256sum /tmp/.parquet` 1. Update `test_data/large/checksums.json` with the new hash. 1. Update the corresponding `metadata.json` (sha256, size_bytes). 1. Upload the Parquet file to R2. 1. Replace the cached file in `test_data/large/` with the regenerated file, then run the preparation command to verify it. Use the corresponding cache under `TEST_DATA_ROOT_PATH` when set. 1. Commit `checksums.json` and `metadata.json` (this also busts the CI cache). ### ITCH AAPL L3 deltas Source: `01302019.NASDAQ_ITCH50.gz` (~4.4 GB) from NASDAQ EMI. ```bash # Download source (keep a local copy, this is a large file) wget -O ~/Downloads/01302019.NASDAQ_ITCH50.gz \ "https://emi.nasdaq.com/ITCH/Nasdaq%20ITCH/01302019.NASDAQ_ITCH50.gz" # Curation test expects source at /tmp ln -sf ~/Downloads/01302019.NASDAQ_ITCH50.gz /tmp/01302019.NASDAQ_ITCH50.gz # Regenerate parquet (output: /tmp/itch_AAPL.XNAS_2019-01-30_deltas.parquet) cargo test -p nautilus-testkit --lib test_curate_aapl_itch -- --ignored --nocapture ``` ### Tardis Deribit BTC-PERPETUAL L2 deltas Source: `tardis_deribit_incremental_book_L2_2020-04-01_BTC-PERPETUAL.csv.gz` from [Tardis](https://tardis.dev/). First-of-month data is available as free samples (no API key required). ```bash # Download source (free sample, no API key needed) wget -O test_data/large/tardis_deribit_incremental_book_L2_2020-04-01_BTC-PERPETUAL.csv.gz \ "https://datasets.tardis.dev/v1/deribit/incremental_book_L2/2020/04/01/BTC-PERPETUAL.csv.gz" # Regenerate parquet (output: /tmp/tardis_BTC-PERPETUAL.DERIBIT_2020-04-01_deltas.parquet) cargo test -p nautilus-tardis test_curate_deribit_deltas -- --ignored --nocapture ``` ## Tutorial test data Several tutorials and guides load user-provided market data. The `NAUTILUS_DATA_DIR` environment variable overrides their base data path. Use `test_data/local/` as an ignored repository-local location for these files. ### Directory layout ```text test_data/local/ Binance/ BTCUSDT_T_DEPTH_2022-11-01_depth_snap.csv BTCUSDT_T_DEPTH_2022-11-01_depth_update.csv Bybit/ 2024-12-01_XRPUSDT_ob500.data.zip HISTDATA/ DAT_ASCII_EURUSD_T_202001.csv.gz ``` The `test_data/local/` directory is gitignored. The tutorial scripts stop with a missing-data message when the expected files are absent. ### Obtaining the data **Binance depth snapshots** are available from the [Binance public data portal](https://data.binance.vision/). Download the BTCUSDT T_DEPTH files for 2022-11-01 and place the snap and update CSVs under `test_data/local/Binance/`. For testing, a subset of rows (e.g. first 10,000) is sufficient. **Bybit ob500 orderbook data** is available from the Bybit CDN: ```bash curl -L "https://quote-saver.bycsi.com/orderbook/linear/XRPUSDT/2024-12-01_XRPUSDT_ob500.data.zip" \ -o test_data/local/Bybit/2024-12-01_XRPUSDT_ob500.data.zip ``` The full file is ~360 MB. For testing, extract the first few hundred lines and repackage as a smaller zip. **HISTDATA tick data** is available from [histdata.com](https://www.histdata.com/). Download EUR/USD ASCII tick data for any month and place the CSV (or `.csv.gz`) under `test_data/local/HISTDATA/`. ### Running the tutorials Build the Python package, then run the source tutorials from the repository root: ```bash make build-debug NAUTILUS_DATA_DIR="$PWD/test_data/local" \ uv run --project python --no-sync python docs/tutorials/backtest_orderbook_binance.py NAUTILUS_DATA_DIR="$PWD/test_data/local" \ uv run --project python --no-sync python docs/tutorials/backtest_orderbook_bybit.py ``` ## Legacy datasets These datasets predate this policy and use raw vendor formats (CSV/CSV.gz) without `metadata.json`. They remain valid for existing tests. New datasets should follow the Parquet standard above. | Dataset | Source | Format | Location | Status | | -------------------------- | -------- | ---------------- | ------------------- | -------- | | Tardis Deribit L2 deltas | Tardis | Parquet (large) | `test_data/large/` | Curated | | ITCH AAPL L3 deltas | NASDAQ | Parquet (large) | `test_data/large/` | Curated | | HISTDATA EURUSD.SIM quotes | HISTDATA | Parquet (large) | `test_data/large/` | Migrated | | Tardis Deribit L2 | Tardis | CSV (checked in) | `test_data/tardis/` | Legacy | | Tardis Binance snapshots | Tardis | CSV.gz (large) | `test_data/large/` | Legacy | | Tardis Bitmex trades | Tardis | CSV.gz (large) | `test_data/large/` | Legacy | The former `nautechsystems/nautilus_data` catalog maps to the HISTDATA EURUSD.SIM Parquet files above. Raw HISTDATA CSV files remain user-fetched. # Testing Source: https://nautilustrader.io/docs/latest/developer_guide/testing/ Our automated tests serve as executable specifications for the trading platform. A healthy suite documents intended behavior, gives contributors confidence to refactor, and catches regressions before they reach production. Tests also double as living examples that clarify complex flows and provide rapid CI feedback so issues surface early. The suite covers these categories: - Unit tests - Integration tests - Acceptance tests - Performance tests - Property-based tests - Fuzzing - Memory leak tests ## Testing policy Tests and runtime contracts form one design system. The [Design by contract](rust.md#design-by-contract) ladder pushes invariants into the type system where possible; the testing ladder below escalates the remaining unknowns through larger input spaces and richer execution models. Each layer extends coverage to inputs or execution states the layer below cannot reach. Not every module requires every technique. Use this section to decide which layers apply before adding tests or `debug_assert!` statements. ### Mechanism ladder Runtime contracts are covered in the [Rust guide](rust.md#design-by-contract): prefer the type system first, then `check_*` from `nautilus_core::correctness` at API boundaries, then `debug_assert!` for internal invariants, then `assert!` for soundness-critical or always-on checks. Test layers follow a parallel escalation. Start at the lowest layer that proves what matters; climb only when the layer below stops detecting regressions or when the input space grows beyond hand-picked cases. | Layer | Trigger condition | | ------------------------ | ------------------------------------------------------------------------------- | | Unit test | A single function or transition has a small, enumerable set of cases. | | Parametrized test | The same shape repeats across discrete inputs (order side, status, instrument). | | Property-based test | An invariant must hold for a whole class of inputs the mind cannot enumerate. | | Integration test | Multiple modules interact through a real (non-mocked) engine or runtime. | | Fuzz test | Untrusted or adversarial bytes cross a parser, decoder, or wire-format handler. | | Spec acceptance test | Behavior depends on a live venue contract (see `spec_exec_testing.md`). | | Deterministic simulation | Correctness depends on task scheduling, timeouts, or wall-clock ordering. | | Formal verification | A pure function has crisp invariants and a bounded input space worth a proof. | The formal verification rung is aspirational: no Kani or Prusti harness has landed in the workspace. The row records the escalation condition for when a verifier is adopted, not a current obligation. ### Projection rule Module shape determines which layers pay off. Not every module warrants the full ladder. Apply the rule at module granularity, not crate granularity: an adapter crate contains pure parsers and I/O-bound client loops, and each row applies to a different part. | Module shape | Layers that apply | Example | | ----------------------------------- | --------------------------------------------- | -------------------------------------- | | Pure function, crisp invariants | Unit, parametrized, property, fuzz | Reconciliation kernels, portfolio math | | Pure function, no stated invariants | Unit, parametrized, property, fuzz | Codecs, adapter parsers, formatters | | Stateful, synchronous | Unit, parametrized, property over transitions | Cache, order book | | Stateful, async | Unit, integration, deterministic simulation | Live engine, execution manager | | I/O bound, venue contract | Integration, spec acceptance, boundary fuzz | Adapter client loops | ### When not to add coverage - Add `debug_assert!` only where a test can reach it. Release builds strip the check, so an unexercised assertion has no signal. A targeted unit test counts as a harness; a proptest or fuzz harness amplifies the signal. - Prefer a proptest over a hand-written edge-case test when the invariant spans a whole class of inputs. Targeted unit tests remain valid for known venue pathologies and as regression reproducers for shrunk counterexamples. - Do not duplicate a live spec acceptance card as an integration test. Link to it instead. - Do not pad coverage with tests that assert language or framework guarantees (`Option::is_some` after `Some(..)`, `Vec::len` after `push`). ### DST readiness Deterministic simulation testing (DST) requires the runtime to be free of ambient non-determinism. Before promoting a module to run under DST, verify the following: - Time, task, runtime, and signal primitives route through `nautilus_common::live::dst` rather than `tokio` directly. Wall-clock reads go through the seam in `nautilus_core::time` rather than `SystemTime::now()` at call sites. - State maps with ordering-dependent iteration use `IndexMap` or `IndexSet`, not the default hash collections. - Every `tokio::select!` on a control-plane path sets `biased` so poll order is fixed. - No calls to `Instant::now()`, `SystemTime::now()`, `tokio::signal::ctrl_c`, `std::thread::spawn`, or `tokio::task::spawn_blocking` escape the seam. Blocking-thread and OS-thread primitives break madsim determinism the same way an ambient clock read does. - Replay-sensitive IDs (`trade_id`, `venue_order_id`) are pure functions of their inputs; see `crates/execution/src/reconciliation/ids.rs`. Ephemeral event UUIDs on other reconciliation paths do not need to be deterministic. The `surface` probe in `crates/common/src/live/dst.rs` only pins the re-export shape; it does not check that callers actually use the seam. Enforcement is by review. Run the audit whenever a new async module enters the workspace or an existing module gains new control-plane scheduling. ## Property-based testing Property testing verifies that logic holds for *all* valid inputs, not just hand-picked examples. We use [`proptest`](https://altsysrq.github.io/proptest-book/intro.html) in Rust to enforce invariants. - **Use cases:** Core domain types (`Price`, `Quantity`, `UnixNanos`), accounting engines, matching engines, and state machines. - **Example invariants:** - Round-trip serialization: `parse(to_string(value)) == value` - Inverse operations: `(A + B) - B == A` - Transitivity: `If A < B and B < C, then A < C` ## Fuzzing Fuzzing introduces unstructured or malicious data to the system to verify it fails gracefully. - **Use cases:** Network boundaries, exchange data parsers (JSON, FIX, WebSocket feeds), and complex state machines. - **Goal:** The system returns a `Result::Err` and never panics, hangs, or leaks memory when encountering malformed data. Adapter fuzz binaries are registered in each adapter package behind its `fuzz` feature. Run all registered targets for one adapter from the repository root: ```bash scripts/fuzz-adapter.sh derive ``` The workspace pins `libfuzzer-sys`, and `nautilus-live` owns the shared libFuzzer integration. A separate `publish = false` package is reserved for fuzz targets that require dependencies which must not enter a published adapter graph, such as Lighter's git-pinned Pornin differential oracle. When building or modifying core types, write property tests to cover the mathematical boundaries. Performance tests help evolve performance-critical components. Run tests with [pytest](https://docs.pytest.org), our primary test runner. Use parametrized tests and fixtures (e.g., `@pytest.mark.parametrize`) to avoid repetitive code and improve clarity. ## Running tests CI runs `scripts/ci/check_test_network.py` through `make test-scripts` to flag direct network calls with non-local literal addresses, explicit live-test switches, and fork-RPC options. It checks test directories and Rust files from their named test module onward. Loopback addresses and reserved fixture domains are allowed. This is a heuristic regression check: it does not resolve variable destinations, follow calls into other code, or enforce network isolation. ### Python tests The Python test suite lives under `python/tests/` and tests the Rust-backed PyO3 package. It requires a built extension module and uses the Python project under `python/`. From the repository root, run: ```bash make pytest ``` The Makefile target isolates certain test modules in separate pytest processes to avoid global Rust state conflicts. Use `make pytest` rather than invoking pytest directly. Local `make pytest` runs use the debug extension from `make build-debug`. CI tests a release wheel. Do not write `python/tests/` cases that probe Rust panic paths in process with `pytest.raises(BaseException)` or similar broad catches. Those tests can appear to pass against the debug build and abort the interpreter against the release wheel. For abort-prone PyO3 or FFI methods, verify the Python signature and parameter names, or isolate the call in a subprocess. For the registered Rust benchmark set: ```bash make cargo-ci-benches ``` No canonical Python performance suite is wired into CI. See the [Benchmarking guide](benchmarking.md) for focused Criterion and iai commands, profiling, and measurement policy. Run benchmarks separately from unit tests to avoid interference. ### Rust tests Before running the full suite, [prepare the large test fixtures](test_datasets.md#dataset-categories). ```bash make cargo-test # or cargo nextest run --workspace --features "$(bash scripts/cargo-features.bash)" --cargo-profile nextest --lib --tests ``` :::info `cargo nextest` is the supported runner for the full Rust unit and integration suite. The suite relies on nextest's per-test process isolation for process-global and thread-local state, including logging, the message bus, and deterministic test state. Plain `cargo test --workspace` runs a test binary's cases in a shared process, so it is not a supported full-suite gate and is not guaranteed to pass. Plain `cargo test` remains appropriate for doctests and focused tests that are known to work with the libtest runner. ::: #### Rust doctests `cargo nextest` cannot execute doctests, so they run through a separate target: ```bash make cargo-test-doc # or cargo test --doc --workspace --features "$(bash scripts/cargo-features.bash)" --profile nextest ``` Doc examples are a maintained test surface. The scheduled `nightly-tests` workflow runs this target with Python 3.13 and 3.14. See the [Rust guide](rust.md#doc-examples) for how to annotate a fence so it compiles. #### Testing with optional features Use `EXTRA_FEATURES` to include optional features like `capnp` or `hypersync`: ```bash # Test with capnp feature make cargo-test EXTRA_FEATURES="capnp" # Test with multiple features make cargo-test EXTRA_FEATURES="capnp hypersync" # Legacy shorthand for hypersync make cargo-test HYPERSYNC=true # Test specific crate with features make cargo-test-crate-nautilus-serialization FEATURES="capnp" ``` ### IDE integration - **PyCharm**: Right-click the tests folder or file -> "Run pytest". - **VS Code**: Use the Python Test Explorer extension. ## Test style ### General - Name test functions after what they exercise; you do not need to encode the expected assertions in the name. - Add docstrings when they clarify setup, scenarios, or expectations. - **Group assertions** when possible: perform all setup/act steps first, then assert together to avoid the act-assert-act smell. - Use `unwrap`, `expect`, or direct `panic!`/`assert` calls inside tests; clarity and conciseness matter more than defensive error handling here. - Do not capture log output to assert on log messages. Log capture in tests is fragile because loggers are global state, test execution order is non-deterministic, and the assertions break when log wording changes. Instead, verify the observable behavior (return values, state changes, side effects) that the log message reflects. ### Python tests (`python/tests/`) Use **pytest-style free functions and fixtures**. Do not use test classes. - Write each test as a standalone `def test_*()` function. - Use `@pytest.fixture` for shared setup (instruments, engine instances, data). Prefer `yield` fixtures when teardown is needed (e.g., `engine.dispose()`). - Use `@pytest.mark.parametrize` to cover multiple inputs without duplicating test bodies. - Import model types from `nautilus_trader.model`, not from `nautilus_trader._libnautilus`. - Test providers live in `python/tests/providers.py`. Use `TestInstrumentProvider` and `TestDataProvider` for common instruments and data. - Mark tests that depend on unfinished features with `@pytest.mark.skip(reason="WIP: ")` rather than deleting them. ### Rust For Rust-specific test conventions (module structure, `#[rstest]`, parameterization), see the [Rust guide](rust.md#testing-conventions). ## Waiting for asynchronous effects In Rust tests, prefer a notification channel or another event owned by the test over repeated condition evaluation. Subscribe before reading the authoritative state, then recheck it after every notification so a transition between the read and the await cannot be missed. When no suitable signal exists, use `wait_until_async(...)` from `nautilus_common::testing`; it stops as soon as the condition succeeds and applies a bounded timeout. Use a fixed sleep only when the time window itself is under test. ## Mocks Prefer hand-written stubs that return fixed values over mocking frameworks. Use `MagicMock` only when you need to assert call counts/arguments or simulate complex state changes. Avoid mocking the objects you're actually testing. ## Code coverage Aim for high coverage without sacrificing appropriate error handling or causing "test induced damage" to the architecture. Some branches remain untestable without modifying production behavior. For example, a final condition in a defensive if-else block may only trigger for unexpected values; leave these checks in place so future changes can exercise them if needed. Design-time exceptions can also be impractical to test, so 100% coverage is not the target. ## Excluded code coverage We use `pragma: no cover` comments to [exclude code from coverage](https://coverage.readthedocs.io/en/coverage-4.3.3/excluding.html) when tests would be redundant. Typical examples include: - Asserting an abstract method raises `NotImplementedError` when called. - Asserting the final condition check of an if-else block when impossible to test (as above). Such tests are expensive to maintain because they must track refactors while providing little value. Keep concrete implementations of abstract methods fully covered. Remove `pragma: no cover` when it no longer applies and restrict its use to the cases above. ## Debugging Rust tests Use the default test configuration to debug Rust tests. To run the full suite with debug symbols for later, run `make cargo-test-debug` instead of `make cargo-test`. In IntelliJ IDEA, adjust the run configuration for parametrised `#[rstest]` cases so it reads `test --package nautilus-model --lib data::bar::tests::test_get_time_bar_start::case_1` (remove `-- --exact` and append `::case_n` where `n` starts at 1). This workaround matches the behavior explained in [rust-analyzer issue 8964](https://github.com/rust-lang/rust-analyzer/issues/8964#issuecomment-871592851). In VS Code you can pick the specific test case to debug directly. ## Debugging Python and Rust Build the PyO3 extension with the workspace's `debug-pyo3` Cargo profile when a native debugger needs Rust symbols: ```bash make sync ( cd python CARGO_TARGET_DIR=../target \ uv run --no-sync maturin develop --profile debug-pyo3 ) ``` Start the Python program or notebook with the Python debugger, then attach LLDB or GDB to that Python process for Rust breakpoints. The repository does not generate editor launch configurations, so configure both debugger sessions in the editor you use. ## Data type testing Each data type flows through multiple layers of the platform. The table below shows where existing types are tested, so new types can follow the same pattern. ### Test layer matrix | Layer | Location | What it covers | | ---------------------- | ------------------------------------------- | ---------------------------------------------------------- | | DataEngine subscribe | `crates/data/tests/integration/engine.rs` | Engine processes subscribe/unsubscribe commands correctly. | | DataEngine publish | `crates/data/tests/integration/engine.rs` | Engine routes published data to the message bus. | | DataActor subscribe | `crates/common/src/actor/tests.rs` | Actor subscribes and receives data via typed publish. | | DataActor unsubscribe | `crates/common/src/actor/tests.rs` | Actor stops receiving data after unsubscribe. | | PyO3 actor dispatch | `crates/common/src/python/actor.rs` | Rust handler dispatches to Python `on_*` method. | | Python Actor subscribe | `python/tests/unit/common/test_actor.py` | Python actor subscribes; command count increments. | | Python Actor unsub | `python/tests/unit/common/test_actor.py` | Python actor unsubscribes; subscription list clears. | | Adapter live tests | `docs/developer_guide/spec_data_testing.md` | Live data acceptance tests (DataTester). | ### Coverage per data type The following table shows which layers have test coverage for each data type. Use this as a checklist when adding a new type. | Data type | Engine | Actor (Rust) | PyO3 dispatch | Actor (Python) | Adapter spec | | ------------------- | ------ | ------------ | ------------- | -------------- | ------------ | | `InstrumentAny` | ✓ | ✓ | ✓ | ✓ | ✓ | | `OrderBookDeltas` | ✓ | ✓ | ✓ | ✓ | ✓ | | `OrderBook` | ✓ | ✓ | ✓ | ✓ | ✓ | | `QuoteTick` | ✓ | ✓ | ✓ | ✓ | ✓ | | `TradeTick` | ✓ | ✓ | ✓ | ✓ | ✓ | | `Bar` | ✓ | ✓ | ✓ | ✓ | ✓ | | `MarkPriceUpdate` | ✓ | ✓ | ✓ | ✓ | ✓ | | `IndexPriceUpdate` | ✓ | ✓ | ✓ | ✓ | ✓ | | `FundingRateUpdate` | ✓ | ✓ | ✓ | ✓ | ✓ | | `InstrumentStatus` | ✓ | ✓ | ✓ | ✓ | ✓ | | `InstrumentClose` | ✓ | ✓ | ✓ | ✓ | ✓ | | `OptionGreeks` | ✓ | ✓ | ✓ | ✓ | ✓ | | `OptionChainSlice` | - | ✓ | ✓ | ✓ | ✓ | | `CustomData` | ✓ | ✓ | ✓ | ✓ | - | `OptionChainSlice` is assembled by the DataEngine's `OptionChainManager` from per-instrument greeks and quote subscriptions. It does not have its own engine subscribe command. ### Adding a new data type When introducing a new data type, add tests at each layer: 1. **DataEngine** (`crates/data/tests/integration/engine.rs`): Add `test_execute_subscribe_` and `test_execute_unsubscribe_` tests. Follow the pattern in existing subscribe tests: register client, build command, call `engine.execute`, assert subscription list. 1. **DataActor Rust** (`crates/common/src/actor/tests.rs`): - Add `received_: Vec` field to `TestDataActor`. - Implement the `on_` handler in the `DataActor` trait impl. - Add `test_subscribe_and_receive_` and `test_unsubscribe_` tests. - Use the typed publish function (`msgbus::publish_`), not `publish_any`, for types that use `TypedHandler` routing. 1. **PyO3 actor dispatch** (`crates/common/src/python/actor.rs`): - Add `dispatch_on_` method that calls `py_self.call_method1("on_", ...)`. - Add `on_` in the `DataActor` trait impl that calls the dispatch method. - Add `#[pyo3(name = "on_")]` method in the `#[pymethods]` block. - Add `on_` to `RustTestDataActor` wrapper and the inline Python test class. - Add handler test and dispatch test. 1. **Python Actor** (`python/tests/unit/common/test_actor.py`): - Add `test_subscribe_` and `test_unsubscribe_` tests. - Assert `actor.subscribed_()` returns expected entries after subscribe and is empty after unsubscribe. 1. **Documentation**: Add entries to `actors.md` callback table, `strategies.md` handler signatures, `adapters.md` subscribe method stubs, and `spec_data_testing.md` test cards. :::tip Search for an existing type like `instrument_close` or `funding_rate` across all five layers to find concrete examples of the patterns described above. ::: # Backtest (High-Level API) Source: https://nautilustrader.io/docs/latest/getting_started/backtest_high_level/ Use `BacktestNode` for config-driven backtesting with the Parquet data catalog. This is the recommended path for production workflows because the strategies, actors, and execution algorithms you build here carry forward to live trading with `LiveNode`. This tutorial loads FX quote tick data, writes it to a catalog, and backtests an EMA cross strategy on a simulated FX ECN venue. [View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/master/docs/getting_started/backtest_high_level.py). ## Prerequisites - Python 3.12+ - [NautilusTrader](https://pypi.org/project/nautilus_trader/) 2.x installed (`pip install -U --pre nautilus_trader`). The `--pre` flag is required while 2.x ships as `2.0.0rcN`. - pandas (`pip install pandas`). The wheel declares no runtime dependencies. ```python import os import shutil from pathlib import Path import pandas as pd from nautilus_trader.backtest import BacktestNode from nautilus_trader.config import BacktestDataConfig from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.config import BacktestRunConfig from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.core.datetime import dt_to_unix_nanos from nautilus_trader.model import AccountType from nautilus_trader.model import BookType from nautilus_trader.model import Currency from nautilus_trader.model import OmsType from nautilus_trader.model import Quantity from nautilus_trader.persistence import ParquetDataCatalog from nautilus_trader.testkit.providers import TestDataProvider from nautilus_trader.testkit.providers import TestInstrumentProvider from nautilus_trader.trading import EmaCrossConfig ``` ## Load the sample data The tutorial runs with no download: `TestDataProvider` ships AUD/USD quote ticks, read from the local `test_data/` directory in a source checkout and from GitHub otherwise. We take the first 20,000 to keep the run short. To replay a longer history, download FX tick data from [histdata.com](https://www.histdata.com/download-free-forex-historical-data/?/ascii/tick-data-quotes/) and extract the CSV files into `~/Downloads/Data/HISTDATA/` (or set the `NAUTILUS_DATA_DIR` environment variable to the parent directory containing a `HISTDATA` subfolder). Downloaded files look like `DAT_ASCII_EURUSD_T_202410.csv` (EUR/USD for October 2024). The cell below picks them up automatically. A full month of tick data runs to millions of rows, so expect the catalog write to take several minutes. ```python DATA_DIR = Path(os.environ.get("NAUTILUS_DATA_DIR", "~/Downloads/Data")).expanduser() / "HISTDATA" raw_files = ( sorted( f for f in DATA_DIR.iterdir() if f.is_file() and (f.suffix == ".csv" or f.name.endswith(".csv.gz")) ) if DATA_DIR.is_dir() else [] ) raw_files ``` ## Load data into the catalog Both loaders parse vendor rows into Nautilus `QuoteTick` objects with a default notional size. Histdata CSV files contain `timestamp, bid_price, ask_price` fields; the bundled TrueFX sample contains `timestamp, bid, ask`. ```python if raw_files: instrument = TestInstrumentProvider.default_fx_ccy("EUR/USD") ticks = TestDataProvider.quotes_from_histdata_csv(instrument, raw_files[0]) else: instrument = TestInstrumentProvider.default_fx_ccy("AUD/USD") ticks = TestDataProvider.quotes_from_truefx_csv( instrument, "truefx/audusd-ticks.csv", max_rows=20_000, ) # Vendor exports are not always monotonic; the catalog requires ascending timestamps ticks.sort(key=lambda tick: tick.ts_init) print(f"Loaded {len(ticks)} quote ticks for {instrument.id}") # Preview: see first 2 ticks ticks[0:2] ``` See the [Loading data](../concepts/data/) guide for more details. Instantiate a `ParquetDataCatalog` with a storage directory (here we use the current directory). Write the instrument and tick data to the catalog. ```python CATALOG_PATH = Path.cwd() / "catalog" # Clear if it already exists, then create fresh if CATALOG_PATH.exists(): shutil.rmtree(CATALOG_PATH) CATALOG_PATH.mkdir(parents=True) # Create a catalog instance catalog = ParquetDataCatalog(str(CATALOG_PATH)) # Write instrument to the catalog catalog.write_instruments([instrument]) # Write ticks to the catalog catalog.write_quote_ticks(ticks) ``` ## Query the catalog The catalog provides methods like `.instruments()` and `.query_quote_ticks()` to query stored data and determine the available time range. ```python # Get list of all instruments in catalog catalog.instruments() ``` ```python # See 1st instrument from catalog catalog.instruments()[0] ``` ```python # Query quote ticks from catalog to determine the data range all_ticks = catalog.query_quote_ticks(identifiers=[instrument.id.value]) print(f"Total ticks in catalog: {len(all_ticks)}") if all_ticks: # Get timestamps from the data first_tick_time = pd.Timestamp(all_ticks[0].ts_init, unit="ns", tz="UTC") last_tick_time = pd.Timestamp(all_ticks[-1].ts_init, unit="ns", tz="UTC") print(f"Data range: {first_tick_time} to {last_tick_time}") # Set backtest range to first 2 weeks of data (as UNIX nanoseconds) start_ns = all_ticks[0].ts_init end_ns = dt_to_unix_nanos(first_tick_time + pd.Timedelta(days=14)) print(f"Backtest range: {first_tick_time} to {first_tick_time + pd.Timedelta(days=14)}") # Preview selected data selected_quote_ticks = catalog.query_quote_ticks( identifiers=[instrument.id.value], start=start_ns, end=end_ns, ) print(f"Selected ticks for backtest: {len(selected_quote_ticks)}") selected_quote_ticks[:2] else: raise ValueError("No ticks found in catalog") ``` ## Add venues ```python venue_configs = [ BacktestVenueConfig( name="SIM", oms_type=OmsType.HEDGING, account_type=AccountType.MARGIN, book_type=BookType.L1_MBP, base_currency=Currency.from_str("USD"), starting_balances=["1_000_000 USD"], ), ] ``` ## Add data ```python str(CATALOG_PATH) ``` ```python data_configs = [ BacktestDataConfig( data_type="QuoteTick", catalog_path=str(CATALOG_PATH), instrument_id=instrument.id, start_time=start_ns, end_time=end_ns, ), ] ``` ## Configure the backtest `BacktestRunConfig` centralizes venue and data configuration in one object. ```python config = BacktestRunConfig( venues=venue_configs, data=data_configs, engine=BacktestEngineConfig(), ) ``` ## Add the strategy Build the node, then attach a strategy to the run configuration. Here we add the built-in `EmaCross` example strategy, which subscribes to the quote ticks and trades the crossover of a fast and slow EMA on the mid price. To run your own strategy, make it importable and use `node.add_strategy_from_config()` with an `ImportableStrategyConfig` instead. ```python node = BacktestNode(configs=[config]) node.build() node.add_builtin_strategy( config.id, "EmaCross", EmaCrossConfig( instrument_id=instrument.id, trade_size=Quantity.from_int(1_000_000), fast_period=10, slow_period=20, ), ) ``` ## Run the backtest `BacktestNode` processes all data in timestamp order with deterministic execution semantics. The architectural patterns (strategies, actors, execution algorithms) carry forward to live trading with `LiveNode`. ```python results = node.run() results ``` # Backtest (Low-Level API) Source: https://nautilustrader.io/docs/latest/getting_started/backtest_low_level/ Use `BacktestEngine` for direct component access: load market data, wire up strategies and execution algorithms, and run backtests with full control over every step. This tutorial backtests an EMA cross strategy with a TWAP execution algorithm on a simulated Binance Spot exchange using historical trade tick data. [View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/master/docs/getting_started/backtest_low_level.py). ## Prerequisites - Python 3.12+ - [NautilusTrader](https://pypi.org/project/nautilus_trader/) 2.x installed (`pip install -U --pre nautilus_trader`). The `--pre` flag is required while 2.x ships as `2.0.0rcN`. - pandas (`pip install pandas`), used by the reports at the end. The wheel declares no runtime dependencies. ```python from decimal import Decimal from nautilus_trader.backtest import BacktestEngine from nautilus_trader.common import LogLevel from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.config import ExecutionAlgorithmConfig from nautilus_trader.config import LoggerConfig from nautilus_trader.config import StrategyConfig from nautilus_trader.indicators import ExponentialMovingAverage from nautilus_trader.model import AccountType from nautilus_trader.model import Bar from nautilus_trader.model import BarType from nautilus_trader.model import Currency from nautilus_trader.model import ExecAlgorithmId from nautilus_trader.model import InstrumentId from nautilus_trader.model import Money from nautilus_trader.model import OmsType from nautilus_trader.model import OrderSide from nautilus_trader.model import TraderId from nautilus_trader.model import Venue from nautilus_trader.testkit.providers import TestDataProvider from nautilus_trader.testkit.providers import TestInstrumentProvider from nautilus_trader.trading import Strategy ``` ## Load data Load bundled test data (ETHUSDT trades from Binance), initialize the matching instrument, and build Nautilus `TradeTick` objects from the CSV. ```python # Initialize the instrument which matches the data ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance() # Build Nautilus trade ticks from the bundled Binance CSV ticks = TestDataProvider.trades_from_binance_csv( ETHUSDT_BINANCE, "binance/ethusdt-trades.csv", ) ``` See the [Data](../concepts/data/) concept guide for details on the data processing pipeline. ## Initialize the engine Pass a `BacktestEngineConfig` to configure the engine. Here we set a custom `trader_id` to show the pattern. ```python # Configure backtest engine config = BacktestEngineConfig( trader_id=TraderId("BACKTESTER-001"), logging=LoggerConfig(stdout_level=LogLevel.ERROR), ) # Build the backtest engine engine = BacktestEngine(config=config) ``` ## Add a venue Set up a simulated venue that matches the market data. Here we configure a Binance Spot exchange with a cash account. ```python # Add a trading venue (multiple venues possible) BINANCE = Venue("BINANCE") engine.add_venue( venue=BINANCE, oms_type=OmsType.NETTING, account_type=AccountType.CASH, # Spot CASH account (not for perpetuals or futures) base_currency=None, # Multi-currency account starting_balances=[ Money(1_000_000.0, Currency.from_str("USDT")), Money(10.0, Currency.from_str("ETH")), ], ) ``` ## Add data Add the instrument and trade ticks to the engine. ```python # Add instrument(s) engine.add_instrument(ETHUSDT_BINANCE) # Add data engine.add_data(ticks) ``` :::note You can add multiple data types (including custom types) and backtest across multiple venues. ::: ## Add strategies The strategy extends `Strategy` and trades an EMA crossover on 250-tick bars, which the engine aggregates internally from the trade ticks. Entries are submitted with an `exec_algorithm_id` so the engine routes them to the TWAP execution algorithm for slicing. ```python class EMACrossTWAPConfig(StrategyConfig): def __init__( self, *, instrument_id: InstrumentId, bar_type: BarType, trade_size: Decimal, fast_ema_period: int = 10, slow_ema_period: int = 20, twap_horizon_secs: float = 10.0, twap_interval_secs: float = 2.5, **_kwargs: object, ) -> None: super().__init__() self.instrument_id = instrument_id self.bar_type = bar_type self.trade_size = trade_size self.fast_ema_period = fast_ema_period self.slow_ema_period = slow_ema_period self.twap_horizon_secs = twap_horizon_secs self.twap_interval_secs = twap_interval_secs class EMACrossTWAP(Strategy): def __init__(self, config: EMACrossTWAPConfig) -> None: super().__init__(config) self.fast_ema = ExponentialMovingAverage(config.fast_ema_period) self.slow_ema = ExponentialMovingAverage(config.slow_ema_period) self.exec_algorithm_id = ExecAlgorithmId("TWAP") self.exec_algorithm_params = { "horizon_secs": str(config.twap_horizon_secs), "interval_secs": str(config.twap_interval_secs), } def on_start(self) -> None: self.register_indicator_for_bars(self.config.bar_type, self.fast_ema) self.register_indicator_for_bars(self.config.bar_type, self.slow_ema) self.subscribe_bars(self.config.bar_type) def on_bar(self, _bar: Bar) -> None: if not self.indicators_initialized(): return if self.fast_ema.value >= self.slow_ema.value: if self.portfolio.is_net_flat(self.config.instrument_id): self.buy() elif self.portfolio.is_net_short(self.config.instrument_id): self.close_all_positions(self.config.instrument_id) self.buy() elif self.fast_ema.value < self.slow_ema.value: if self.portfolio.is_net_flat(self.config.instrument_id): self.sell() elif self.portfolio.is_net_long(self.config.instrument_id): self.close_all_positions(self.config.instrument_id) self.sell() def buy(self) -> None: self.submit_twap_order(OrderSide.BUY) def sell(self) -> None: self.submit_twap_order(OrderSide.SELL) def submit_twap_order(self, side: OrderSide) -> None: instrument = self.cache.instrument(self.config.instrument_id) order = self.order_factory.market( self.config.instrument_id, side, instrument.make_qty(self.config.trade_size), exec_algorithm_id=self.exec_algorithm_id, exec_algorithm_params=self.exec_algorithm_params, ) self.submit_order(order) def on_stop(self) -> None: self.close_all_positions(self.config.instrument_id) ``` ```python # Configure and add the strategy strategy_config = EMACrossTWAPConfig( instrument_id=ETHUSDT_BINANCE.id, bar_type=BarType.from_str("ETHUSDT.BINANCE-250-TICK-LAST-INTERNAL"), trade_size=Decimal("0.10"), fast_ema_period=10, slow_ema_period=20, twap_horizon_secs=10.0, twap_interval_secs=2.5, ) strategy = EMACrossTWAP(config=strategy_config) engine.add_strategy(strategy=strategy) ``` The strategy config carries the TWAP parameters, but the execution algorithm itself is a separate component. ## Add execution algorithms Register the built-in TWAP execution algorithm under the `TWAP` identifier the strategy references. ```python # Add the native TWAP execution algorithm engine.add_native_exec_algorithm( "TwapAlgorithm", ExecutionAlgorithmConfig(exec_algorithm_id=ExecAlgorithmId("TWAP")), ) ``` ## Run the backtest Call `.run()` to process all available data. The engine replays events in timestamp order with deterministic execution semantics. ```python # Run the engine (from start to end of data) engine.run() ``` ## Post-run analysis The engine retains data and execution objects in memory for generating reports. It also logs a tearsheet with default statistics; see the [Portfolio statistics](../concepts/portfolio.md#portfolio-statistics) guide for custom statistics. ```python engine.generate_account_report(BINANCE) ``` ```python engine.generate_order_fills_report() ``` ```python engine.generate_positions_report() ``` ## Repeated runs Reset the engine for repeated runs. Instruments, data, and loaded components persist across resets; loaded components have their internal state reset. ```python # For repeated backtest runs, reset the engine engine.reset() # Clear or remove loaded components before adding replacements. ``` Remove and add individual components (actors, strategies, execution algorithms) as required. See the [BacktestEngine](../api_reference/backtest.md) API reference for the add and clear methods. ```python # Once done, good practice to dispose of the object if the script continues engine.dispose() ``` # Getting Started Source: https://nautilustrader.io/docs/latest/getting_started/ Set up a Python 3.12-3.14 environment and install the package: ```bash pip install -U --pre nautilus_trader ``` PyPI publishes NautilusTrader 2.x as `2.0.0rcN` pre-releases. Include `--pre` until `2.0.0` is released. Confirm the install with: ```bash python -c "import nautilus_trader; print(nautilus_trader.__version__)" ``` ## Choose your path - **Backtesting**: learn the two API levels below, then work through the [tutorials](../tutorials/) for strategy pattern walkthroughs. - **Live trading**: see the [Configure a live trading node](../how_to/configure_live_trading.md) how-to and [Integrations](../integrations/) for supported venues. - **Data workflows**: see the [how-to guides](../how_to/) for loading external data and setting up the Parquet data catalog. - **Building adapters**: see the [Developer guide](../developer_guide/). ## Backtesting API levels NautilusTrader provides two API levels for backtesting: | API level | Entry point | Best for | | ---------- | ---------------- | --------------------------------------------------------------------- | | Low-level | `BacktestEngine` | Direct component access, library development | | High-level | `BacktestNode` | Production workflows, easier transition to live trading (recommended) | The high-level API requires a Parquet-based data catalog. The low-level API works with in-memory data but has no live-trading path. :::warning[One node per process] Running multiple `BacktestNode` or `LiveNode` instances concurrently in the same process is not supported due to global singleton state. Sequential execution with proper disposal between runs is supported. A replacement `LiveNode` on the same thread also requires dropping the previous node, or releasing all references to it in Python, before construction. See [Processes and threads](../concepts/architecture.md#processes-and-threads). ::: :::info See the [Backtesting](../concepts/backtesting.md) guide for help choosing an API level. ::: ## Examples The docs cover a subset of examples. For the full set, see the [nautilus_trader repository](https://github.com/nautechsystems/nautilus_trader). | Directory | Description | | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | | [examples/](https://github.com/nautechsystems/nautilus_trader/tree/master/examples) | Runnable Python examples organized by environment | | [docs/tutorials/](../tutorials/) | Python and Rust tutorials for common workflows | | [docs/concepts/](../concepts/) | Concept guides with code snippets | | [python/tests/unit/](https://github.com/nautechsystems/nautilus_trader/tree/master/python/tests/unit) | Unit tests covering core functionality and edge cases | ## Running in Docker A self-contained Jupyter notebook server is available as a Docker image, with no local setup required. ```bash docker pull ghcr.io/nautechsystems/jupyterlab:latest --platform linux/amd64 docker run -p 8888:8888 ghcr.io/nautechsystems/jupyterlab:latest ``` Then open `http://localhost:8888` in your browser. :::note Container data is ephemeral; deleting the container removes all data. ::: :::info NautilusTrader log output can exceed Jupyter's default rate limit, causing notebooks to hang. Set `LoggerConfig(stdout_level=LogLevel.ERROR)` to avoid this. ::: # Installation Source: https://nautilustrader.io/docs/latest/getting_started/installation/ NautilusTrader is officially supported for Python 3.12-3.14 on the following 64-bit platforms: | Operating System | Supported Versions | CPU Architecture | | ---------------- | ------------------ | ---------------- | | Linux (Ubuntu) | 22.04 and later | x86_64 | | Linux (Ubuntu) | 22.04 and later | ARM64 | | macOS | 15.0 and later | ARM64 | | Windows Server | 2022 and later | x86_64 | :::note NautilusTrader may work on other platforms, but only those listed above are regularly used by developers and tested in CI. ::: NautilusTrader follows the [Python support window in Scientific Python SPEC 0](https://scientific-python.org/specs/spec-0000/). Each Python minor version is supported for three years after its initial release. Support normally ends in the first NautilusTrader release after that window and after the replacement Python version passes compatibility checks. Continuous CI coverage comes from the GitHub Actions runners we build on: - `Linux (Ubuntu)` builds currently pin to `ubuntu-22.04` to keep glibc 2.35 compatibility even as `ubuntu-latest` moves ahead. - `macOS (ARM64)` builds run on `macos-latest`, so support tracks that runner image as it moves ahead. - `Windows (x86_64)` builds currently pin to `windows-2022` to keep the toolchain stable. On Linux, confirm your glibc version with `ldd --version` and ensure it reports 2.35 or newer before proceeding. We recommend using the latest supported version of Python and installing [nautilus_trader](https://pypi.org/project/nautilus_trader/) inside a virtual environment to isolate dependencies. **There are two supported ways to install**: 1. Pre-built binary wheel from PyPI *or* the Nautech Systems package index. 2. Build from source. :::tip We highly recommend installing using the [uv](https://docs.astral.sh/uv) package manager with a "vanilla" CPython. Conda and other Python distributions *may* work but aren't officially supported. ::: ## From PyPI :::warning[Install the 2.x wheel for these docs] This documentation covers NautilusTrader 2.x. PyPI still resolves a plain `uv pip install nautilus_trader` to the 1.x line, whose Python API differs, so the code on these pages fails with `ImportError` and `TypeError` against a 1.x install. Pass `--pre` until `2.0.0` is released, and confirm that `python -c "import nautilus_trader; print(nautilus_trader.__version__)"` reports a `2.` version. ::: NautilusTrader publishes 2.x release-candidate wheels to PyPI using `2.0.0rcN` versions while final validation is in progress. To install the latest [nautilus_trader](https://pypi.org/project/nautilus_trader/) binary wheel (or sdist package): ```bash uv pip install --pre nautilus_trader ``` The `--pre` flag is required because these wheels are pre-release builds. The installed import name is still `nautilus_trader`. :::warning We do not recommend release candidates for production environments, such as live trading controlling real capital. ::: Run this command outside a NautilusTrader source checkout. The repository root uses an `exclude-newer` uv policy for reproducible development, which can filter out newly published wheels. Inside a source checkout, use [Build Python from source](#8-build-python-from-source) instead. Current wheels target Python 3.12-3.14. Build from source when you need local Rust changes, a debug build, or a platform wheel that is not available. ### Stable 1.x wheels Omitting `--pre` installs the latest stable 1.x release: ```bash uv pip install nautilus_trader ``` A 1.x install cannot run the examples on these pages. See [Migrate from v1 to v2](https://github.com/nautechsystems/nautilus_trader/blob/master/MIGRATION_V2.md) for the API differences. ## Extras Install the optional dependencies for Plotly-based interactive tearsheets and charts with the `visualization` extra: ```bash uv pip install --pre "nautilus_trader[visualization]" ``` ## From the Nautech Systems package index The Nautech Systems package index (`packages.nautechsystems.io`) complies with [PEP-503](https://peps.python.org/pep-0503/) and hosts both stable and development binary wheels for `nautilus_trader`. This enables users to install either the latest stable release or pre-release versions for testing. ### Stable wheels Stable wheels correspond to official releases of `nautilus_trader` on PyPI, and use standard versioning. As on PyPI, the latest stable release is still on the 1.x line, so add `--pre` for a 2.x wheel. To install the latest stable release: ```bash uv pip install nautilus_trader --index-url=https://packages.nautechsystems.io/simple ``` :::tip Use `--extra-index-url` instead of `--index-url` if you want uv to fall back to PyPI automatically. ::: ### Development wheels The main package index publishes development wheels from both the `nightly` and `develop` branches, allowing users to test features and fixes ahead of stable releases. This process also helps preserve compute resources and provides easy access to the exact binaries tested in CI pipelines, while adhering to [PEP-440](https://peps.python.org/pep-0440/) versioning standards: - `develop` wheels use the version suffix `.devYYYYMMDD+run`. - `nightly` wheels use `.devYYYYMMDD` when the base version is already a pre-release, and `aYYYYMMDD` otherwise. | Platform | Develop | Nightly | | :----------------- | :------ | :------ | | `Linux (x86_64)` | ✓ | ✓ | | `Linux (ARM64)` | - | ✓ | | `macOS (ARM64)` | - | ✓ | | `Windows (x86_64)` | - | ✓ | :::warning We do not recommend using development wheels in production environments, such as live trading controlling real capital. ::: By default, uv will install the latest stable release. Adding the `--pre` flag ensures that pre-release versions, including development wheels, are considered. To install the latest available pre-release (including development wheels): ```bash uv pip install nautilus_trader --pre --index-url=https://packages.nautechsystems.io/simple ``` The installed import name is still `nautilus_trader`. Run this command outside a NautilusTrader source checkout so the repository's `exclude-newer` uv policy does not filter out newly published wheels. Build from source when you need local Rust changes, a debug build, or a platform wheel that is not available. ### Available versions You can view all available versions of `nautilus_trader` on the [package index](https://packages.nautechsystems.io/simple/nautilus-trader/index.html). To programmatically request and list available versions: ```bash curl -s https://packages.nautechsystems.io/simple/nautilus-trader/index.html | grep -oP '(?<= C++ Clang tools for Windows (latest) > Modify # 2. Add to PATH: [System.Environment]::SetEnvironmentVariable('path', "C:\Program Files\Microsoft Visual Studio\2022\BuildTools\VC\Tools\Llvm\x64\bin\;" + $env:Path,"User") ``` Verify: `clang --version` ### 4. Install uv Install [uv](https://docs.astral.sh/uv/getting-started/installation): ```bash tab="Linux/macOS" curl -LsSf https://astral.sh/uv/install.sh | sh ``` ```powershell tab="Windows" irm https://astral.sh/uv/install.ps1 | iex ``` ### 5. Clone and sync dependencies Clone the source with `git`, then sync its dependencies from the project root: ```bash git clone --branch master --depth 1 https://github.com/nautechsystems/nautilus_trader cd nautilus_trader make sync ``` For development hosts and CI runner images, see the [single source of truth for versions](../developer_guide/environment_setup.md#single-source-of-truth-for-versions) before installing pinned tools. :::note The `--depth 1` flag fetches just the latest commit for a faster, lightweight clone. ::: ### 6. Install Cap'n Proto for development Install [Cap'n Proto](https://capnproto.org) if you plan to enable the `capnp` Rust feature, regenerate serialization schemas, or work on serialization code. Use the repository script on Linux or macOS to install the pinned version from `.nautilus-engineering/tools.toml`: ```bash ./scripts/install-capnp.sh ``` Verify: `capnp --version` :::note Cap'n Proto is a development dependency. It is not required when installing pre-built wheels. ::: ### 7. Set environment variables The uv project environment lives at `python/.venv`, beside `python/pyproject.toml`. Run direct uv project commands from `python/` or pass `--project python` from the repository root. Set environment variables for PyO3 compilation (Linux and macOS only). Run these commands from the repository root after `make sync` in Bash or Zsh. For Fish commands, see the developer guide's [environment setup](../developer_guide/environment_setup.md#4-configure-environment-variables). Set `PYO3_PYTHON` in each shell to this checkout's `python/.venv/bin/python`; replace any saved export that still points to the root `.venv/bin/python`. ```bash # Set the Python executable path for PyO3 export PYO3_PYTHON="$PWD/python/.venv/bin/python" # Linux only: Set the library path for the uv-managed Python runtime PYTHON_LIB_DIR="$("$PYO3_PYTHON" -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))')" export LD_LIBRARY_PATH="$PYTHON_LIB_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" # Required for Rust tests when using uv-installed Python export PYTHONHOME="$("$PYO3_PYTHON" -c 'import sys; print(sys.base_prefix)')" ``` :::note The `LD_LIBRARY_PATH` export is Linux-specific and not needed on macOS. The `PYTHONHOME` variable is required when running `make cargo-test` with a `uv`-installed Python. Without it, tests that depend on PyO3 may fail to locate the Python runtime. ::: ### 8. Build Python from source This path builds the PyO3 package from the `python/` directory and installs it into `python/.venv`. Use it from a NautilusTrader source checkout when a development wheel is not available for your platform or when you need local Rust changes. From the repository root: ```bash make build-debug ``` This target syncs `python/.venv`, builds the Rust extension with maturin, and regenerates Python type stubs. It uses `target/` for Cargo artifacts. Run a Python example with the project environment: ```bash uv run --project python --no-sync python examples/live/lighter/data_tester.py ``` The script connects to Lighter Testnet and starts streaming market data; stop it with Ctrl+C. For direct commands and test targets, see the [Python package README][python-readme]. [python-readme]: https://github.com/nautechsystems/nautilus_trader/blob/master/python/README.md ## From GitHub release To install a binary wheel from GitHub, first navigate to the [latest release](https://github.com/nautechsystems/nautilus_trader/releases/latest). Download the appropriate `.whl` for your operating system and Python version, then run: ```bash uv pip install .whl ``` ## Troubleshooting ### Documentation examples fail to import ```text ImportError: cannot import name 'OrderSide' from 'nautilus_trader.model' ImportError: cannot import name 'BacktestEngine' from 'nautilus_trader.backtest' TypeError: Struct types cannot define __init__ ``` These come from running 2.x documentation against a 1.x install. Check what you have: ```bash python -c "import nautilus_trader; print(nautilus_trader.__version__)" ``` A `1.` version means the resolver picked the stable line. Reinstall with `--pre`: ```bash uv pip install -U --pre nautilus_trader ``` The 1.x and 2.x Python APIs are not interchangeable. See [Migrate from v1 to v2](https://github.com/nautechsystems/nautilus_trader/blob/master/MIGRATION_V2.md) when porting a 1.x application. ### uv resolves an older version inside the repository The repository root sets an `exclude-newer` policy for reproducible development, which hides recently published wheels. Run install commands from another directory, or [build from source](#from-source). ### Wheel not found for your platform Check your Python version is 3.12-3.14 and your platform is listed at the top of this page. On Linux, `ldd --version` must report glibc 2.35 or newer. Otherwise [build from source](#from-source). ## Versioning and releases NautilusTrader is still under active development. Some features may be incomplete, and while the API is becoming more stable, breaking changes can occur between releases. We strive to document these changes in the release notes on a **best-effort basis**. We aim to follow a **bi-weekly release schedule**, though experimental or larger features may cause delays. Use NautilusTrader only if you are prepared to adapt to these changes. ## Redis Using [Redis](https://redis.io) with NautilusTrader is **optional** and only required if configured as the backend for a cache database or [message bus](../concepts/message_bus.md). :::info The minimum supported Redis version is 6.2 (required for [streams](https://redis.io/docs/latest/develop/data-types/streams/) functionality). ::: For a quick setup, we recommend using a [Redis Docker container](https://hub.docker.com/_/redis/). You can find an example setup in the `.docker` directory, or run the following command to start a container: ```bash docker run -d --name redis -p 6379:6379 redis:latest ``` This command will: - Pull the latest version of Redis from Docker Hub if it's not already downloaded. - Run the container in detached mode (`-d`). - Name the container `redis` for easy reference. - Expose Redis on the default port 6379, making it accessible to NautilusTrader on your machine. To manage the Redis container: - Start it with `docker start redis` - Stop it with `docker stop redis` :::tip We recommend using [Redis Insight](https://redis.io/insight/) as a GUI to visualize and debug Redis data efficiently. ::: ## Precision mode NautilusTrader supports two precision modes for its core value types (`Price`, `Quantity`, `Money`), which differ in their internal bit-width and maximum decimal precision. - **High-precision**: 128-bit integers with up to 16 decimals of precision, and a larger value range. - **Standard-precision**: 64-bit integers with up to 9 decimals of precision, and a smaller value range. :::note By default, the official Python wheels ship in high-precision (128-bit) mode on all supported platforms. For pure Rust crates, high-precision works on all platforms (including Windows) since Rust handles `i128`/`u128` via software emulation. The default is standard-precision unless you explicitly enable the `high-precision` feature flag. ::: The performance tradeoff is that standard-precision is ~3-5% faster in typical backtests, but has lower decimal precision and a smaller representable value range. :::note Performance benchmarks comparing the modes are pending. ::: ### Build configuration The precision mode is selected at compile time through the `high-precision` Rust feature flag. The Python package enables this flag in the maturin build features (see `python/pyproject.toml`), so source builds default to high-precision. For a standard-precision (64-bit) Python build, remove `high-precision` from the maturin feature list, then build as usual: ```bash make build-debug ``` ### Rust feature flag To enable high-precision (128-bit) mode in Rust, add the `high-precision` feature to your `Cargo.toml`: ```toml [dependencies] nautilus-core = { version = "*", features = ["high-precision"] } ``` :::info See the [Value Types](../concepts/overview.md#value-types) specifications for more details. ::: # Quickstart Source: https://nautilustrader.io/docs/latest/getting_started/quickstart/ Run your first backtest in under five minutes. [View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/master/docs/getting_started/quickstart.py). ## Prerequisites - Python 3.12+ - NautilusTrader 2.x installed (`pip install -U --pre nautilus_trader`). The `--pre` flag is required while 2.x ships as `2.0.0rcN`; without it pip installs the 1.x line, whose Python API differs and cannot run this page. - NumPy and pandas (`pip install numpy pandas`). The wheel declares no runtime dependencies, so it does not pull them in. ## Write a strategy A strategy extends the `Strategy` base class and overrides event handlers to react to market data. This one trades an EMA crossover: buy when a fast exponential moving average crosses above a slow one, sell when it crosses below. Its parameters live on a `StrategyConfig` subclass. Declare your own fields as keyword-only arguments and absorb the rest in `**_kwargs`: the base config reads its own fields (`strategy_id`, `oms_type`, and so on) from the same call and ignores the ones it does not recognize. ```python from decimal import Decimal from nautilus_trader.config import StrategyConfig from nautilus_trader.indicators import ExponentialMovingAverage from nautilus_trader.model import Bar from nautilus_trader.model import BarType from nautilus_trader.model import InstrumentId from nautilus_trader.model import OrderSide from nautilus_trader.trading import Strategy class EMACrossConfig(StrategyConfig): def __init__( self, *, instrument_id: InstrumentId, bar_type: BarType, trade_size: Decimal, fast_ema_period: int = 10, slow_ema_period: int = 20, **_kwargs: object, ) -> None: super().__init__() self.instrument_id = instrument_id self.bar_type = bar_type self.trade_size = trade_size self.fast_ema_period = fast_ema_period self.slow_ema_period = slow_ema_period class EMACross(Strategy): def __init__(self, config: EMACrossConfig) -> None: super().__init__(config) self.fast_ema = ExponentialMovingAverage(config.fast_ema_period) self.slow_ema = ExponentialMovingAverage(config.slow_ema_period) def on_start(self) -> None: self.register_indicator_for_bars(self.config.bar_type, self.fast_ema) self.register_indicator_for_bars(self.config.bar_type, self.slow_ema) self.subscribe_bars(self.config.bar_type) def on_bar(self, _bar: Bar) -> None: if not self.indicators_initialized(): return if self.fast_ema.value >= self.slow_ema.value: if self.portfolio.is_net_flat(self.config.instrument_id): self.buy() elif self.portfolio.is_net_short(self.config.instrument_id): self.close_all_positions(self.config.instrument_id) self.buy() elif self.fast_ema.value < self.slow_ema.value: if self.portfolio.is_net_flat(self.config.instrument_id): self.sell() elif self.portfolio.is_net_long(self.config.instrument_id): self.close_all_positions(self.config.instrument_id) self.sell() def buy(self) -> None: instrument = self.cache.instrument(self.config.instrument_id) order = self.order_factory.market( self.config.instrument_id, OrderSide.BUY, instrument.make_qty(self.config.trade_size), ) self.submit_order(order) def sell(self) -> None: instrument = self.cache.instrument(self.config.instrument_id) order = self.order_factory.market( self.config.instrument_id, OrderSide.SELL, instrument.make_qty(self.config.trade_size), ) self.submit_order(order) def on_stop(self) -> None: self.close_all_positions(self.config.instrument_id) ``` `on_start` registers the two EMA indicators so the engine updates them automatically with each new bar. `on_bar` waits for the indicators to warm up, then enters or reverses a position based on the crossover signal. ## Generate synthetic data To keep the quickstart self-contained, we generate 10,000 synthetic EUR/USD 1-minute bars using a random walk. In practice you would load real market data from a vendor or the Parquet data catalog. ```python import numpy as np import pandas as pd from nautilus_trader.backtest import BacktestEngine from nautilus_trader.common import LogLevel from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.config import LoggerConfig from nautilus_trader.model import AccountType from nautilus_trader.model import Currency from nautilus_trader.model import CurrencyPair from nautilus_trader.model import Money from nautilus_trader.model import OmsType from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol from nautilus_trader.model import Venue # Create a EUR/USD instrument on the SIM venue EUR = Currency.from_str("EUR") USD = Currency.from_str("USD") EURUSD = CurrencyPair( instrument_id=InstrumentId.from_str("EUR/USD.SIM"), raw_symbol=Symbol("EUR/USD"), base_currency=EUR, quote_currency=USD, price_precision=5, size_precision=0, price_increment=Price.from_str("0.00001"), size_increment=Quantity.from_int(1), ts_event=0, ts_init=0, lot_size=Quantity.from_int(1_000), margin_init=Decimal("0.03"), margin_maint=Decimal("0.03"), ) # Generate synthetic 1-minute bars (random walk around 1.10) rng = np.random.default_rng(42) n = 10_000 price = 1.10 + np.cumsum(rng.normal(0, 0.0002, n)) spread = np.abs(rng.normal(0, 0.0003, n)) bars_df = pd.DataFrame( { "open": price, "high": price + spread, "low": price - spread, "close": price + rng.normal(0, 0.00005, n), }, index=pd.date_range("2024-01-01", periods=n, freq="1min", tz="UTC"), ) bars_df["high"] = bars_df[["open", "high", "close"]].max(axis=1) bars_df["low"] = bars_df[["open", "low", "close"]].min(axis=1) bar_type = BarType.from_str("EUR/USD.SIM-1-MINUTE-LAST-EXTERNAL") bars = [ Bar( bar_type=bar_type, open=Price(row.open, precision=EURUSD.price_precision), high=Price(row.high, precision=EURUSD.price_precision), low=Price(row.low, precision=EURUSD.price_precision), close=Price(row.close, precision=EURUSD.price_precision), volume=Quantity.from_int(1_000_000), ts_event=int(timestamp.value), ts_init=int(timestamp.value), ) for timestamp, row in bars_df.iterrows() ] ``` Each row becomes a `Bar` with the instrument's price precision. The bar type string encodes the instrument, aggregation period, price source, and data origin. ## Configure and run the engine Create a `BacktestEngine`, add a simulated FX venue with a margin account, wire up the instrument, data, and strategy, then run. The engine processes all bars in timestamp order with deterministic execution semantics. ```python engine = BacktestEngine( config=BacktestEngineConfig( logging=LoggerConfig(stdout_level=LogLevel.ERROR), ), ) # Add a simulated FX venue SIM = Venue("SIM") engine.add_venue( venue=SIM, oms_type=OmsType.NETTING, account_type=AccountType.MARGIN, starting_balances=[Money(1_000_000, USD)], base_currency=USD, default_leverage=Decimal(1), ) # Add instrument, data, and strategy engine.add_instrument(EURUSD) engine.add_data(bars) strategy = EMACross( EMACrossConfig( instrument_id=EURUSD.id, bar_type=bar_type, trade_size=Decimal(100000), ), ) engine.add_strategy(strategy) # Run the backtest engine.run() ``` The engine processes all 10,000 bars in timestamp order. Each bar updates the registered indicators, then triggers `on_bar`. The simulated exchange fills market orders at the current price. ## Review results The engine generates reports from the completed backtest. The account report shows balance changes over time, the positions report lists each round-trip trade with its realized PnL, and the order fills report shows every execution. ```python engine.generate_account_report(venue=SIM) ``` ```python engine.generate_positions_report() ``` ```python engine.generate_order_fills_report() ``` ## Next steps - [Backtest (low-level API)](backtest_low_level) for direct `BacktestEngine` usage with real market data and execution algorithms. - [Backtest (high-level API)](backtest_high_level) for config-driven backtesting with `BacktestNode` and the Parquet data catalog. - [Tutorials](../tutorials/) for strategy pattern walkthroughs covering market making, mean reversion, order book imbalance, and more. ```python engine.dispose() ``` # Configure a Live Trading Node Source: https://nautilustrader.io/docs/latest/how_to/configure_live_trading/ Set up a `LiveNode` for live market connectivity. For the node lifecycle, see [Live trading](../concepts/live.md). For command outcomes, see [Execution policies](../concepts/execution/policies.md#command-outcomes). For state recovery, see [Execution reconciliation](../concepts/execution/reconciliation.md). :::danger[Jupyter notebooks not recommended for live trading] Do not run live trading nodes in Jupyter notebooks. The node owns a long-running loop on the calling thread, and notebook lifecycle controls make production operation unsafe: - Cells can run out of order, kernels can crash, and state can disappear. - Notebooks lack the logging, monitoring, and graceful shutdown needed for production trading. Use Jupyter for backtesting, analysis, and experimentation. For live trading, run nodes as standalone Python scripts or services. ::: :::warning[One LiveNode per process] Running multiple `LiveNode` instances concurrently in the same process is not supported because runtime state is not isolated. `run_async()` also rejects a second hosted node on the same event loop. Add multiple strategies to a single node, or run additional nodes in separate processes. See [Processes and threads](../concepts/architecture.md#processes-and-threads) for details. ::: :::warning[Do not block the event loop] User code on the event loop thread (strategy callbacks, actor handlers, and time event callbacks) must return quickly. This applies to both Python and Rust. Blocking operations like model inference, heavy calculations, or synchronous I/O cause missed fills, stale data, and delayed order submissions. Offload long-running work to an executor or a separate thread/process. ::: :::info[Platform differences] Windows signal handling differs from Unix-like systems. If you are running on Windows, please read the note on [Windows signal handling](#windows-signal-handling) for guidance on graceful shutdown behavior and Ctrl+C (SIGINT) support. ::: ## LiveNodeConfig `LiveNodeConfig` owns the node's core component settings. Register data and execution clients with `LiveNode.builder(...)`, not through client dictionaries on this config. For background on config defaults and `Option` semantics, see the [Configuration](../concepts/configuration.md) concept guide. ```python from nautilus_trader.common import Environment from nautilus_trader.common import LogLevel from nautilus_trader.config import CacheConfig from nautilus_trader.config import LiveDataEngineConfig from nautilus_trader.config import LiveExecutionEngineConfig from nautilus_trader.config import LiveNodeConfig from nautilus_trader.config import LiveRiskEngineConfig from nautilus_trader.config import LoggerConfig from nautilus_trader.config import MessageBusConfig from nautilus_trader.config import PortfolioConfig from nautilus_trader.model import TraderId config = LiveNodeConfig( environment=Environment.LIVE, trader_id=TraderId.from_str("MY-TRADER-001"), logging=LoggerConfig(stdout_level=LogLevel.INFO), cache=CacheConfig(), msgbus=MessageBusConfig(), data_engine=LiveDataEngineConfig(), risk_engine=LiveRiskEngineConfig(), exec_engine=LiveExecutionEngineConfig(), portfolio=PortfolioConfig(), ) ``` ### Core configuration parameters | Setting | Default | Description | | ----------------------------- | ------------ | -------------------------------------------------------------------------------- | | `trader_id` | "TRADER-001" | Unique trader identifier (name-tag format); the tag must be unique across nodes. | | `instance_id` | `None` | Optional unique instance identifier. | | `timeout_connection_secs` | 60.0 | Connection timeout in seconds. | | `timeout_reconciliation_secs` | 30.0 | Reconciliation timeout in seconds. | | `timeout_portfolio_secs` | 10.0 | Portfolio initialization timeout. | | `timeout_disconnection_secs` | 10.0 | Disconnection timeout. | | `delay_post_stop_secs` | 10.0 | Delay for residual events after stopping. | | `timeout_shutdown_secs` | 5.0 | Pending-task shutdown timeout in seconds. | :::warning[Trader ID tag uniqueness] The tag after the final hyphen is what reaches generated client order IDs, order list IDs, and position IDs; the name before it does not. Two nodes trading the same venue account must therefore use different tags, because `MY-TRADER-001` and `OTHER-TRADER-001` share the tag `001` and can produce identical IDs. Setting `use_uuid_client_order_ids` on the strategy removes the exposure for client order IDs only: order list IDs and position IDs keep the tag either way, so unique tags remain required. ::: ### Cache database configuration Rust-native live systems keep cache behavior in `CacheConfig` and Redis connection settings in `RedisCacheConfig`. ```rust use nautilus_common::{ cache::{CacheConfig, database::CacheDatabaseFactory}, enums::SerializationEncoding, }; use nautilus_infrastructure::redis::cache::RedisCacheConfig; let config = CacheConfig { encoding: SerializationEncoding::MsgPack, timestamps_as_iso8601: true, buffer_interval_ms: Some(100), flush_on_start: false, ..Default::default() }; let database = RedisCacheConfig { host: Some("localhost".to_string()), port: Some(6379), username: Some("nautilus".to_string()), password: Some("pass".to_string()), connection_timeout: 2, response_timeout: 2, ..Default::default() }; let cache_database = database .create(trader_id, instance_id, config.clone()) .await?; ``` Attach the adapter after building the Rust-native node and before starting it. The node restores the database before reconciliation when `exec_engine.load_cache` is enabled, which is the default. ```rust let node_config = LiveNodeConfig { trader_id, ..Default::default() }; let mut node = LiveNode::build("LiveNode".to_string(), Some(node_config))?; node.set_cache_database(cache_database)?; node.run().await?; ``` Set `CacheConfig.flush_on_start = true` to clear the attached backing instead of restoring it. Python injects the same database config through `LiveNodeBuilder`. The node constructs and owns the adapter when it starts: ```python from nautilus_trader.common import Environment from nautilus_trader.infrastructure import RedisCacheConfig from nautilus_trader.config import LiveExecutionEngineConfig from nautilus_trader.live import LiveNode from nautilus_trader.model import TraderId node = ( LiveNode.builder("LiveNode", TraderId("TRADER-001"), Environment.LIVE) .with_cache_database_factory(RedisCacheConfig(host="localhost", port=6379)) .with_exec_engine_config( LiveExecutionEngineConfig( snapshot_orders=True, snapshot_positions=True, ), ) .with_load_state(True) .with_save_state(True) .build() ) try: node.run() finally: node.dispose() ``` Pass `PostgresCacheConfig` instead to back the cache with Postgres. Any other object raises `NotImplementedError` from `with_cache_database_factory`, and a failed database connection fails `run()`. Database-backed nodes must use `run()` because `run_async()` rejects cache database backings that would block the host event loop. With `snapshot_orders=True`, the execution engine persists an order snapshot during submission processing and after each state change. Order snapshots require a Redis or Postgres cache backing. With `snapshot_positions=True`, the execution engine publishes a snapshot when a position opens, changes, or closes. Set `snapshot_positions_interval_secs` independently to add periodic snapshots of every open position. Redis and Postgres cache backings persist both paths. Without backing, the snapshots remain available on the in-process message bus but are not persisted. `with_load_state` and `with_save_state` control actor and strategy state persistence, which requires a Redis backing. The Postgres adapter backs cache state only: with registered actors or strategies, `with_load_state(True)` fails when the trader starts, while `with_save_state(True)` fails when the node stops or is disposed. On startup the kernel passes non-empty persisted state to `on_load`; when stopping or disposing the node it persists whatever `on_save` returns. :::warning State persistence is not continuous checkpointing. The kernel saves state at most once per run, so a `SIGKILL` or a crash loses every change since the last save. Dispose the node so `dispose()` closes the backing and flushes buffered writes; returning straight from `run()` can drop the final save. ::: ### MessageBus configuration Message bus behavior stays in `MessageBusConfig`. Redis connection settings live in `RedisMessageBusConfig`, which implements `MessageBusBackingFactory` and constructs the backing from those settings. ```rust use nautilus_common::{ enums::SerializationEncoding, msgbus::{MessageBusBackingFactory, MessageBusConfig}, }; use nautilus_infrastructure::redis::msgbus::RedisMessageBusConfig; let config = MessageBusConfig { encoding: SerializationEncoding::Json, timestamps_as_iso8601: true, use_instance_id: false, types_filter: Some(vec!["QuoteTick".to_string(), "TradeTick".to_string()]), stream_per_topic: false, autotrim_mins: Some(30), heartbeat_interval_secs: Some(1), ..Default::default() }; let redis_config = RedisMessageBusConfig { connection_timeout: 2, response_timeout: 2, ..Default::default() }; let backing = redis_config.create(trader_id, instance_id, config.clone())?; ``` Python injects the Redis config through `LiveNodeBuilder`: ```python from nautilus_trader.common import Environment from nautilus_trader.common import MessageBusConfig from nautilus_trader.infrastructure import RedisMessageBusConfig from nautilus_trader.live import LiveNode from nautilus_trader.model import TraderId trader_id = TraderId("TRADER-001") message_bus = MessageBusConfig( external_streams=["external-stream"], stream_per_topic=False, ) redis_config = RedisMessageBusConfig( host="localhost", port=6379, ) node = ( LiveNode.builder("LiveNode", trader_id, Environment.LIVE) .with_msgbus_config(message_bus) .with_external_msgbus_factory(redis_config) .build() ) node.run() ``` Existing code can continue passing `RedisMessageBusFactory(redis_config)` to `with_external_msgbus_factory`. `MessageBusConfig` alone does not install a backing. Pair it with a factory as shown above. The factory always installs external egress, and calling `run()` also consumes the configured external streams. Entries already in a stream before the node starts are not replayed. `run_async()` runs the same lifecycle as `run()`, so a node hosted on a caller's event loop services external message-bus ingress too. See [message bus backing configuration](../concepts/message_bus.md#backing-config) for lifecycle and ingress details. External producers that write directly to Redis must supply the required `type` field. See [external egress and ingress](../concepts/message_bus.md#external-egress-and-ingress) for the wire fields and Python custom-data registration. ## Multi-venue configuration A node can connect to multiple clients. This example registers Binance spot and USD-M futures data clients before building the node: ```python from nautilus_trader.adapters.binance import BinanceDataClientConfig from nautilus_trader.adapters.binance import BinanceDataClientFactory from nautilus_trader.adapters.binance import BinanceEnvironment from nautilus_trader.adapters.binance import BinanceProductType from nautilus_trader.common import Environment from nautilus_trader.live import LiveNode from nautilus_trader.model import TraderId node = ( LiveNode.builder( "BINANCE-MULTI-CLIENT-001", TraderId.from_str("MULTI-VENUE-001"), Environment.LIVE, ) .add_data_client( "BINANCE_SPOT", BinanceDataClientFactory(), BinanceDataClientConfig( product_type=BinanceProductType.SPOT, environment=BinanceEnvironment.LIVE, ), ) .add_data_client( "BINANCE_FUTURES", BinanceDataClientFactory(), BinanceDataClientConfig( product_type=BinanceProductType.USD_M, environment=BinanceEnvironment.LIVE, ), ) .build() ) ``` ## ExecutionEngine configuration `LiveExecutionEngineConfig` controls order processing, execution events, and venue reconciliation. For full details see the [API Reference](/docs/python-api-latest/live.html#nautilus_trader.live.LiveExecutionEngineConfig). ### Reconciliation Recovers missed order and position events to keep system state consistent with the venue. | Setting | Default | Description | | ------------------------------- | ------- | ----------------------------------------------------------------------------- | | `reconciliation` | True | Activate reconciliation at startup to align internal state with the venue. | | `reconciliation_lookback_mins` | None | How far back (minutes) to request past events for reconciling uncached state. | | `reconciliation_instrument_ids` | None | Include list of instrument IDs to reconcile. | | `filtered_client_order_ids` | None | Client order IDs to skip during reconciliation (for venue-side duplicates). | See [Execution reconciliation](../concepts/execution/reconciliation.md) for details. ### Order filtering Controls which order events and reports the system processes, preventing conflicts across trading nodes. | Setting | Default | Description | | ---------------------------------- | ------- | --------------------------------------------------------------------------- | | `filter_unclaimed_external_orders` | False | Drop unclaimed external orders so they do not affect the strategy. | | `filter_position_reports` | False | Drop position status reports. Useful when multiple nodes trade one account. | :::note[Order tagging behavior] Reconciliation tags orders by origin: - **`VENUE` tag**: external orders discovered at the venue (placed outside this system). - **`RECONCILIATION` tag**: synthetic orders generated to align position discrepancies. When `filter_unclaimed_external_orders` is enabled, only `VENUE`-tagged orders are filtered. `RECONCILIATION`-tagged orders are never filtered, so position alignment always succeeds. ::: ### Continuous reconciliation Continuous reconciliation keeps runtime execution state aligned after startup by checking in-flight orders, polling open orders, checking position status, and auditing own order books. Configure the loop with these settings. For runtime state-transition rules, retry coordination, and caveats, see [Runtime checks](../concepts/execution/reconciliation.md#runtime-checks). | Setting | Default | Description | | ------------------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `inflight_check_interval_ms` | 2,000 ms | How often to check in-flight order status. Set to 0 to disable. | | `inflight_check_threshold_ms` | 5,000 ms | Time before an in-flight order triggers a venue status check. Lower if colocated. | | `inflight_check_retries` | 5 retries | Retry attempts to verify an in-flight order with the venue. | | `open_check_interval_secs` | None | How often (seconds) to check open orders at the venue. None or 0.0 disables. Recommended: 5-10s. | | `open_check_open_only` | True | When true, query only open orders; when false, fetch full history (resource-intensive). | | `open_check_lookback_mins` | 60 min | Lookback window (minutes) for order status polling. Only orders modified within this window. | | `open_check_threshold_ms` | 5,000 ms | Minimum time since last cached event before acting on venue discrepancies. | | `open_check_missing_retries` | 5 retries | Max retries before targeted not-found resolution for eligible orders. | | `max_single_order_queries_per_cycle` | 10 | Cap on single-order queries per cycle. Prevents rate-limit exhaustion. | | `single_order_query_delay_ms` | 100 ms | Delay (ms) between single-order queries to avoid rate limits. | | `reconciliation_startup_delay_secs` | 10.0 s | Delay (seconds) *after* startup reconciliation before continuous checks begin. | | `own_books_audit_interval_secs` | None | Interval (seconds) between auditing own order books against public books. | | `position_check_interval_secs` | None | Interval (seconds) between position consistency checks. On discrepancy, queries for missing fills. None disables. Recommended: 30-60s. | | `position_check_lookback_mins` | 60 min | Lookback window (minutes) for querying fill reports on position discrepancy. | | `position_check_threshold_ms` | 5,000 ms | Minimum time since last local activity before acting on position discrepancies. | | `position_check_retries` | 3 retries | Max attempts per instrument/account before the engine stops retrying that discrepancy. Once exceeded, an error is logged and the discrepancy is no longer actively reconciled until it clears. | :::warning - **`open_check_lookback_mins`**: do not reduce below 60 minutes. A short window triggers false "missing order" resolutions because orders fall outside the query range. - **`open_check_threshold_ms`**: increase if venue timestamps lag the local clock, so recently updated orders are not marked missing prematurely. - **`reconciliation_startup_delay_secs`**: do not reduce below 10 seconds in production. The delay lets the system stabilize after startup reconciliation before continuous checks begin. ::: ### Additional options | Setting | Default | Description | | ---------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ | | `allow_overfills` | False | Allow fills exceeding order quantity (logs warning). Useful when reconciliation races fills. | | `generate_missing_orders` | True | Generate reconciliation orders from unmatched fill and position reports. | | `snapshot_orders` | False | Persist order snapshots during submission processing and after each state change when cache backing is configured. | | `snapshot_positions` | False | Publish position snapshots on open, change, and close, and persist them when cache backing is configured. | | `snapshot_positions_interval_secs` | None | Interval (seconds) between position snapshots. | | `debug` | False | Enable debug logging for execution. | ### Memory management Periodically purges closed orders, closed positions, and account events from the in-memory cache, keeping memory bounded during long-running or HFT sessions. | Setting | Default | Description | | -------------------------------------- | ------- | ---------------------------------------------------------------------------------- | | `purge_closed_orders_interval_mins` | None | How often (minutes) to purge closed orders from memory. Recommended: 10-15 min. | | `purge_closed_orders_buffer_mins` | None | How long (minutes) an order must be closed before purging. Recommended: 60 min. | | `purge_closed_positions_interval_mins` | None | How often (minutes) to purge closed positions from memory. Recommended: 10-15 min. | | `purge_closed_positions_buffer_mins` | None | How long (minutes) a position must be closed before purging. Recommended: 60 min. | | `purge_account_events_interval_mins` | None | How often (minutes) to purge account events from memory. Recommended: 10-15 min. | | `purge_account_events_lookback_mins` | None | How old (minutes) an account event must be before purging. Recommended: 60 min. | Setting an interval enables the purge loop; leaving it unset disables scheduling and deletion. Each loop delegates to the cache APIs described in [Cache](../concepts/cache.md). ## Strategy configuration For a complete parameter list see the `StrategyConfig` [API Reference](/docs/python-api-latest/trading.html#nautilus_trader.trading.StrategyConfig). ### Identification | Setting | Default | Description | | -------------- | ------- | -------------------------------------------------------------------------- | | `strategy_id` | None | Unique strategy identifier. | | `order_id_tag` | None | Unique tag appended to this strategy's order IDs; cannot contain a hyphen. | ### Order management | Setting | Default | Description | | ------------------------------- | ------- | -------------------------------------------------------------------------------------------------- | | `oms_type` | None | [OMS type](../concepts/execution/index.md#oms-configuration) for position ID and order processing. | | `use_uuid_client_order_ids` | False | Use UUID4 values for client order IDs. | | `external_order_instrument_ids` | None | Serializable intent to claim external orders and reconciliation activity by instrument. | | `manage_contingent_orders` | False | Manage open, non-active-local OTO, OCO, and OUO relationships. | | `manage_gtd_expiry` | False | Manage GTD expirations for orders. | See [Claiming external orders](../concepts/strategies.md#claiming-external-orders) for active claim lifecycle and runtime updates. The `OrderEmulator` retains active-local contingent orders. See [Advanced orders](../concepts/orders/advanced.md#strategy-managed-contingencies) for ownership and venue-support boundaries. Read these runtime settings through `strategy.config`; the strategy itself does not duplicate them as direct properties. ## Windows signal handling `LiveNode` handles Ctrl+C (SIGINT) and, on Unix, SIGTERM in its Rust run loop. The Python bridge also routes SIGINT into the same shutdown path, so runner and tasks shut down cleanly. # Data Catalog with Databento Source: https://nautilustrader.io/docs/latest/how_to/data_catalog_databento/ Set up a Nautilus Parquet data catalog with market data from Databento. The catalog provides efficient storage and querying for backtests and research. [View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/master/docs/how_to/data_catalog_databento.py). ## Prerequisites - Python 3.12+ - [NautilusTrader](https://pypi.org/project/nautilus_trader/) 2.x installed (`pip install -U --pre nautilus_trader`) - [databento](https://pypi.org/project/databento/) Python client library (`pip install databento`) - [Databento](https://databento.com) account with API key set as `DATABENTO_API_KEY` ## Request data Initialize a Databento historical client. The client reads your API key from the `DATABENTO_API_KEY` environment variable by default. ```python import databento as db client = db.Historical() # Uses the DATABENTO_API_KEY environment variable ``` **Every historical streaming request from `timeseries.get_range` incurs a cost (even for the same data), so**: - Check the cost before making a request - Avoid requesting the same data twice - Write responses to disk as zstd compressed DBN files Use the metadata [get_cost endpoint](https://databento.com/docs/api-reference-historical/metadata/metadata-get-cost?historical=python&live=python) to quote the cost before each request. Only request data that does not already exist on disk. The response is in USD, displayed as fractional cents. The following request is for a small amount of data (as used in this Medium article [Building high-frequency trading signals in Python with Databento and sklearn](https://databento.com/blog/hft-sklearn-python)) to demonstrate the workflow. ```python from pathlib import Path from databento import DBNStore ``` We'll prepare a directory for the raw Databento DBN format data, which we'll use for the rest of the tutorial. ```python DATABENTO_DATA_DIR = Path("databento") DATABENTO_DATA_DIR.mkdir(exist_ok=True) ``` ```python # Request cost quote (USD) - this endpoint is 'free' client.metadata.get_cost( dataset="GLBX.MDP3", symbols=["ES.n.0"], stype_in="continuous", schema="mbp-10", start="2023-12-06T14:30:00", end="2023-12-06T20:30:00", ) ``` Use the historical API to request the data used in the Medium article. ```python path = DATABENTO_DATA_DIR / "es-front-glbx-mbp10.dbn.zst" if not path.exists(): # Request data client.timeseries.get_range( dataset="GLBX.MDP3", symbols=["ES.n.0"], stype_in="continuous", schema="mbp-10", start="2023-12-06T14:30:00", end="2023-12-06T20:30:00", path=path, # <-- Passing a `path` writes the data to disk ) ``` Read the data from disk and convert to a pandas.DataFrame ```python data = DBNStore.from_file(path) df = data.to_df() df ``` ## Write to data catalog ```python import shutil from pathlib import Path from nautilus_trader.adapters.databento import DatabentoDataLoader from nautilus_trader.model import InstrumentId from nautilus_trader.persistence import ParquetDataCatalog ``` ```python CATALOG_PATH = Path.cwd() / "catalog" # Clear if it already exists if CATALOG_PATH.exists(): shutil.rmtree(CATALOG_PATH) CATALOG_PATH.mkdir() # Create a catalog instance catalog = ParquetDataCatalog(str(CATALOG_PATH)) ``` Use a `DatabentoDataLoader` to decode and load the data into Nautilus objects. ```python loader = DatabentoDataLoader() ``` Passing an `instrument_id` is optional but speeds up loading by skipping symbology mapping. If provided, use the Nautilus `symbol.venue` format (e.g., "ES.GLBX"). ```python path = DATABENTO_DATA_DIR / "es-front-glbx-mbp10.dbn.zst" # Option 1 (recommended): Let the loader infer the instrument ID from DBN metadata depth10 = loader.load_order_book_depth10(filepath=path) # Option 2: Explicitly specify a valid Nautilus instrument ID (symbol.venue format) # instrument_id = InstrumentId.from_str("ESZ3.GLBX") # E-mini S&P December 2023 futures on Globex # depth10 = loader.load_order_book_depth10( # filepath=path, # instrument_id=instrument_id, # ) ``` ```python # Write data to catalog (this takes ~20 seconds or ~250,000/second for writing MBP-10 at the moment) catalog.write_order_book_depths(depth10) ``` ```python # Test reading from catalog depths = catalog.query_order_book_depths() len(depths) ``` ## Preparing a month of AAPL trades Now we'll expand on this workflow by preparing a month of AAPL trades on the Nasdaq exchange using the Databento `trade` schema, which will translate to Nautilus `TradeTick` objects. ```python # Request cost quote (USD) - this endpoint is 'free' client.metadata.get_cost( dataset="XNAS.ITCH", symbols=["AAPL"], schema="trades", start="2024-01", ) ``` Pass a `path` parameter when requesting historical data to write it to disk. ```python path = DATABENTO_DATA_DIR / "aapl-xnas-202401.trades.dbn.zst" if not path.exists(): # Request data client.timeseries.get_range( dataset="XNAS.ITCH", symbols=["AAPL"], schema="trades", start="2024-01", path=path, # <-- Passing a `path` parameter ) ``` Read the data from disk and convert to a pandas.DataFrame ```python data = DBNStore.from_file(path) df = data.to_df() df ``` We'll use an `InstrumentId` of `"AAPL.XNAS"`, where XNAS is the ISO 10383 MIC (Market Identifier Code) for the Nasdaq venue. Passing an `instrument_id` speeds up loading by skipping symbology mapping. ```python instrument_id = InstrumentId.from_str("AAPL.XNAS") trades = loader.load_trades( filepath=path, instrument_id=instrument_id, ) ``` Here we organize data as one file per month. A file per day works equally well. ```python # Write data to catalog catalog.write_trade_ticks(trades) ``` ```python trades = catalog.query_trade_ticks([str(instrument_id)]) ``` ```python len(trades) ``` # Get Started with Lighter Source: https://nautilustrader.io/docs/latest/how_to/get_started_lighter/ Lighter is available through the Rust engine. You can use it from a pure Rust project, or from Python through PyO3 bindings that expose the same Rust data and execution clients to a Python `LiveNode`. The shortest path is to start with public data. Once data subscriptions work, add execution credentials and then add a strategy that can submit orders. ## Choose a setup path | Path | Use when | First step | | :---------- | :-------------------------------------------------- | :----------------------------------------- | | Pure Rust | You want a compiled app with no Python runtime. | Copy the Rust quickstart. | | Python | You want Python scripts on the Rust engine. | Run the Python data tester. | | RWA example | You want Databento signal data and Lighter trading. | Read the composite market making tutorial. | Start from these files: - Rust quickstart: `examples/quickstarts/lighter-rust-data-client/`. - Python data tester: `examples/live/lighter/data_tester.py`. - RWA tutorial: [Composite market making tutorial][lighter-rwa-composite-mm]. The Rust and Python paths both use these pieces: - `LighterDataClientConfig` selects the Lighter or Robinhood deployment, mainnet or testnet, an optional custom venue, and optional transport settings. - `LighterExecutionClientConfig` adds the account ID and resolves credentials. Its account issuer must match the resolved venue. - `LighterDataClientFactory` and `LighterExecutionClientFactory` register clients with `LiveNode`. - `DataTester` and `ExecTester` provide smoke-test actors before you write a custom strategy. ## Pure Rust starter Copy the quickstart into your own workspace: ```bash cp -R examples/quickstarts/lighter-rust-data-client ~/lighter-rust-data-client cd ~/lighter-rust-data-client cargo run ``` This builds a `LiveNode`, registers the Lighter data client, adds a `DataTester`, and connects to testnet public streams. Stop it with Ctrl+C. The core setup uses builders, which fill in optional defaults for you: ```rust let data_config = LighterDataClientConfig::builder() .environment(LighterEnvironment::Testnet) .build(); let mut node = LiveNode::builder(trader_id, Environment::Live)? .with_name("LIGHTER-DATA-STARTER-001".to_string()) .add_data_client( None, Box::new(LighterDataClientFactory::new()), Box::new(data_config), )? .build()?; ``` After the data path works, add an execution client to the builder before calling `.build()`: ```rust let exec_config = LighterExecutionClientConfig::builder() .account_id(account_id) .environment(LighterEnvironment::Testnet) .build(); let mut node = LiveNode::builder(trader_id, Environment::Live)? .with_name("LIGHTER-EXEC-STARTER-001".to_string()) .add_data_client( None, Box::new(LighterDataClientFactory::new()), Box::new(data_config), )? .add_exec_client( None, Box::new(LighterExecutionClientFactory::new()), Box::new(exec_config), )? .build()?; ``` For execution, follow the [account and API key setup](../integrations/lighter.md#account-and-api-key-setup), then set the matching environment variables before connecting: ```bash export LIGHTER_TESTNET_ACCOUNT_INDEX="123456" export LIGHTER_TESTNET_API_KEY_INDEX="4" export LIGHTER_TESTNET_API_SECRET="your-lighter-api-secret" ``` The deployment and environment select the credential namespace: | Deployment | Environment | Credential prefix | | ---------- | ----------- | ----------------------------- | | Lighter | Mainnet | `LIGHTER_*` | | Lighter | Testnet | `LIGHTER_TESTNET_*` | | Robinhood | Mainnet | `LIGHTER_ROBINHOOD_*` | | Robinhood | Testnet | `LIGHTER_ROBINHOOD_TESTNET_*` | Each namespace supplies `ACCOUNT_INDEX`, `API_KEY_INDEX`, and `API_SECRET`. ## Python starter Python uses the Rust engine through PyO3. Install a Python development wheel outside a source checkout, or build the package from source before running these examples. See [Python installation][python-install]. From the repository root with Python installed: ```bash uv run --project python --no-sync python examples/live/lighter/data_tester.py ``` The script connects to Lighter Testnet immediately and starts streaming. The deployment, environment, and instrument are module-level constants at the top of the file. The Python script mirrors the Rust setup: ```python builder = LiveNode.builder( "LIGHTER-DATA-TESTER-001", TraderId.from_str("TESTER-001"), Environment.LIVE, ).add_data_client( VENUE, LighterDataClientFactory(), LighterDataClientConfig( environment=LighterEnvironment.TESTNET, deployment=LighterDeployment.LIGHTER, ), ) ``` Use the execution tester only after the data tester works: ```bash uv run --project python --no-sync python examples/live/lighter/exec_tester.py ``` The execution tester also connects immediately, and it places real orders by default (`dry_run=False`, with a warning at the top of the module). The default environment is testnet; set `LIGHTER_DEPLOYMENT` and `LIGHTER_ENVIRONMENT` to select the target deployment and environment. ## Move to a strategy The starter paths prove client wiring, subscriptions, and credential lookup. The next step is to replace the tester with a strategy: - Use [Write a Strategy (Rust)](write_rust_strategy.md) for a pure Rust strategy. - Use `examples/live/lighter/nvda_composite_mm.py` for Python node wiring with the built-in Rust `CompositeMarketMaker` strategy. - Use [Composite market making on Lighter RWA][lighter-rwa-composite-mm] when you need the full Databento signal setup. :::warning The Rust execution example ships with `DRY_RUN = false` and can submit live orders as soon as you run it. Set `DRY_RUN` to `true` to connect without order submission. Python execution examples also submit live orders as soon as you run them. Start on testnet or use the smallest accepted size, and confirm the instrument, deployment, environment, account index, API key index, and private key before you run. ::: For emergency cleanup, `cargo run --bin lighter-flatten -p nautilus-lighter` cancels open orders and closes positions for the selected deployment account. Stop other account writers and review [Emergency account cleanup](../integrations/lighter.md#emergency-account-cleanup) before use. The tool submits one account-wide cancellation and one position-close pass. It does not reconcile until the account is flat. [lighter-rwa-composite-mm]: ../tutorials/lighter_rwa_composite_mm.md [python-install]: ../getting_started/installation.md#development-wheels # How-To Source: https://nautilustrader.io/docs/latest/how_to/ Goal-oriented recipes for common tasks. Each guide assumes familiarity with Nautilus concepts and focuses on achieving a specific outcome. New to Nautilus? Start with the [Getting Started](../getting_started/) path and [Tutorials](../tutorials/) first. } /> } /> Set up LiveNodeConfig, execution engine, and venues. } href="configure_live_trading" icon={} /> } /> } /> } /> Use BacktestEngine or BacktestNode with a catalog in Rust. } href="run_rust_backtest" icon={} /> Connect to a venue with LiveNode in Rust. } href="run_rust_live_trading" icon={} /> # Loading External Data Source: https://nautilustrader.io/docs/latest/how_to/loading_external_data/ Load CSV market data into the Parquet data catalog, then run a backtest with `BacktestNode`. This is a common workflow when you have historical data from an external vendor that is not directly supported by a NautilusTrader adapter. [View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/master/docs/how_to/loading_external_data.py). ## Prerequisites - Python 3.12+ - [NautilusTrader](https://pypi.org/project/nautilus_trader/) 2.x installed (`pip install -U --pre nautilus_trader`) - pandas (`pip install pandas`), needed only for the histdata path below ```python import os import shutil from pathlib import Path from nautilus_trader.backtest import BacktestNode from nautilus_trader.config import BacktestDataConfig from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.config import BacktestRunConfig from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.model import AccountType from nautilus_trader.model import Currency from nautilus_trader.model import OmsType from nautilus_trader.model import Quantity from nautilus_trader.persistence import ParquetDataCatalog from nautilus_trader.testkit.providers import TestDataProvider from nautilus_trader.testkit.providers import TestInstrumentProvider from nautilus_trader.trading import EmaCrossConfig ``` ## Load and wrangle the data Place CSV tick files (e.g. from [histdata.com](https://www.histdata.com/)) into `~/Downloads/Data/HISTDATA/`. Set the `NAUTILUS_DATA_DIR` environment variable to the parent directory if your data lives elsewhere. `TestDataProvider.quotes_from_histdata_csv` converts the rows into Nautilus `QuoteTick` objects. Without a download, the how-to falls back to 20,000 bundled AUD/USD quote ticks so it still runs end to end. ```python DATA_DIR = Path(os.environ.get("NAUTILUS_DATA_DIR", "~/Downloads/Data")).expanduser() / "HISTDATA" raw_files = ( sorted( f for f in DATA_DIR.iterdir() if f.is_file() and (f.suffix == ".csv" or f.name.endswith(".csv.gz")) ) if DATA_DIR.is_dir() else [] ) raw_files ``` ```python if raw_files: instrument = TestInstrumentProvider.default_fx_ccy("EUR/USD") ticks = TestDataProvider.quotes_from_histdata_csv(instrument, raw_files[0]) else: instrument = TestInstrumentProvider.default_fx_ccy("AUD/USD") ticks = TestDataProvider.quotes_from_truefx_csv( instrument, "truefx/audusd-ticks.csv", max_rows=20_000, ) # Vendor exports are not always monotonic; the catalog requires ascending timestamps ticks.sort(key=lambda tick: tick.ts_init) ``` ## Write to the data catalog Create a `ParquetDataCatalog` and write the instrument definition and tick data. The catalog stores data in Parquet format for efficient querying across backtest runs. ```python CATALOG_PATH = Path.cwd() / "catalog" # Clear if it already exists, then create fresh if CATALOG_PATH.exists(): shutil.rmtree(CATALOG_PATH) CATALOG_PATH.mkdir(parents=True) catalog = ParquetDataCatalog(str(CATALOG_PATH)) ``` ```python catalog.write_instruments([instrument]) catalog.write_quote_ticks(ticks) ``` ```python # Verify instruments written to catalog catalog.instruments() ``` ```python start = ticks[0].ts_event end = ticks[-1].ts_event + 1 ticks = catalog.query_quote_ticks(identifiers=[instrument.id.value], start=start, end=end) ticks[:10] ``` ## Configure and run the backtest Set up venue and data configs, build the node, then register the built-in `EmaCross` strategy. The same node and strategy pattern carries forward to live trading with `LiveNode`. ```python instrument = catalog.instruments()[0] venue_configs = [ BacktestVenueConfig( name="SIM", oms_type=OmsType.HEDGING, account_type=AccountType.MARGIN, base_currency=Currency.from_str("USD"), starting_balances=["1000000 USD"], ), ] data_configs = [ BacktestDataConfig( catalog_path=str(CATALOG_PATH), data_type="QuoteTick", instrument_id=instrument.id, start_time=start, end_time=end, ), ] config = BacktestRunConfig( engine=BacktestEngineConfig(), data=data_configs, venues=venue_configs, ) ``` ```python node = BacktestNode(configs=[config]) node.build() node.add_builtin_strategy( config.id, "EmaCross", EmaCrossConfig( instrument_id=instrument.id, trade_size=Quantity.from_int(1_000_000), fast_period=10, slow_period=20, ), ) [result] = node.run() ``` ```python result ``` # Run a Backtest (Rust) Source: https://nautilustrader.io/docs/latest/how_to/run_rust_backtest/ Nautilus provides two Rust APIs for backtesting: `BacktestEngine` (low-level) and `BacktestNode` (high-level with catalog streaming). This guide covers both. For background on backtesting concepts, fill models, and matching engine behavior, see the [Backtesting](../concepts/backtesting/) concept guide. For project setup and feature flags, see the [Rust](../concepts/rust.md#project-setup) concept guide. ## Dependencies Add the following to your `Cargo.toml`. The `streaming` and `nautilus-persistence` entries are only needed for the high-level `BacktestNode` API. ```toml [dependencies] nautilus-backtest = { version = "0.63", features = ["streaming"] } nautilus-execution = "0.63" nautilus-model = { version = "0.63", features = ["test-support"] } nautilus-persistence = "0.63" nautilus-trading = { version = "0.63", features = ["examples"] } ahash = "0.8" anyhow = "1" tempfile = "3" ustr = "1" ``` If you only need the low-level `BacktestEngine`, drop `streaming`, `nautilus-persistence`, `tempfile`, and `ustr`. ## BacktestEngine (low-level API) The low-level API gives direct control: you build the engine, add venues and instruments, load data in memory, register strategies, and run. ### 1. Create the engine ```rust use nautilus_backtest::{config::BacktestEngineConfig, engine::BacktestEngine}; let mut engine = BacktestEngine::new(BacktestEngineConfig::default())?; ``` ### 2. Add a venue `SimulatedVenueConfig` uses a `bon::Builder`: only required fields must be set, every other setting falls back to a documented default. `build()` validates the configuration and returns a `ConfigResult`, so propagate or unwrap it. ```rust use nautilus_backtest::config::SimulatedVenueConfig; use nautilus_model::{ enums::{AccountType, BookType, OmsType}, identifiers::Venue, types::Money, }; engine.add_venue( SimulatedVenueConfig::builder() .venue(Venue::from("SIM")) .oms_type(OmsType::Hedging) .account_type(AccountType::Margin) .book_type(BookType::L1_MBP) .starting_balances(vec![Money::from("1_000_000 USD")]) .build()?, )?; ``` Override any default by chaining setters, e.g. `.reject_stop_orders(false)` or `.allow_cash_borrowing(true)`. ### 3. Add instruments and data ```rust use nautilus_model::instruments::{ Instrument, InstrumentAny, stubs::audusd_sim, }; let instrument = InstrumentAny::CurrencyPair(audusd_sim()); let instrument_id = instrument.id(); engine.add_instrument(&instrument)?; let quotes = generate_quotes(instrument_id); // Your data loading function engine.add_data(quotes, None, true, true)?; ``` ### 4. Register a strategy and run ```rust use nautilus_model::types::Quantity; use nautilus_trading::examples::strategies::EmaCross; let strategy = EmaCross::new( instrument_id, Quantity::from("100000"), 10, // fast EMA period 20, // slow EMA period ); engine.add_strategy(strategy)?; engine.run(None, None, None, false)?; ``` ### Run the full example ```bash cargo run -p nautilus-backtest --features examples --example engine-ema-cross ``` Source: [`crates/backtest/examples/engine_ema_cross.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/backtest/examples/engine_ema_cross.rs) ## BacktestNode (high-level API) The high-level API loads data from a `ParquetDataCatalog` and streams in configurable chunk sizes. Requires the `streaming` feature on `nautilus-backtest`. ### 1. Write data to a catalog ```rust use nautilus_model::instruments::{ Instrument, InstrumentAny, stubs::audusd_sim, }; use nautilus_persistence::backend::catalog::ParquetDataCatalog; use tempfile::TempDir; let instrument = InstrumentAny::CurrencyPair(audusd_sim()); let instrument_id = instrument.id(); let quotes = generate_quotes(instrument_id); let temp_dir = TempDir::new()?; let catalog_path = temp_dir.path().to_str() .context("temp dir path is not valid UTF-8")? .to_string(); let catalog = ParquetDataCatalog::new( temp_dir.path(), None, None, None, None, ); catalog.write_instruments(vec![instrument])?; catalog.write_to_parquet("es, None, None, None)?; ``` ### 2. Configure the run ```rust use nautilus_backtest::config::{ BacktestDataConfig, BacktestRunConfig, BacktestVenueConfig, NautilusDataType, }; use nautilus_model::enums::{AccountType, BookType, OmsType}; let venue_config = BacktestVenueConfig::builder() .name("SIM") .oms_type(OmsType::Hedging) .account_type(AccountType::Margin) .book_type(BookType::L1_MBP) .starting_balances(vec!["1_000_000 USD".to_string()]) .build()?; let data_config = BacktestDataConfig::builder() .data_type(NautilusDataType::QuoteTick) .catalog_path(catalog_path) .instrument_id(instrument_id) .build()?; let run_config = BacktestRunConfig::builder() .id("ema-cross-run".to_string()) .venues(vec![venue_config]) .data(vec![data_config]) .chunk_size(100) .build()?; ``` ### 3. Build, add strategies, and run ```rust use nautilus_backtest::node::BacktestNode; use nautilus_model::types::Quantity; use nautilus_trading::examples::strategies::EmaCross; let mut node = BacktestNode::new(vec![run_config])?; node.build()?; let engine = node.get_engine_mut("ema-cross-run") .context("engine not found for run config ID")?; let strategy = EmaCross::new( instrument_id, Quantity::from("100000"), 10, 20, ); engine.add_strategy(strategy)?; node.run()?; ``` ### Run the full example ```bash cargo run -p nautilus-backtest --features examples,streaming --example node-ema-cross ``` Source: [`crates/backtest/examples/node_ema_cross.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/backtest/examples/node_ema_cross.rs) # Run Live Trading (Rust) Source: https://nautilustrader.io/docs/latest/how_to/run_rust_live_trading/ The `LiveNode` connects to real venues and data sources through adapter clients. This guide walks through a complete live trading setup using OKX as an example. For the node lifecycle, see [Live trading](../concepts/live.md). For command outcomes, see [Execution policies](../concepts/execution/policies.md#command-outcomes). For state recovery, see [Execution reconciliation](../concepts/execution/reconciliation.md). For project setup and feature flags, see the [Rust](../concepts/rust.md#project-setup) concept guide. ## Dependencies Add the live crate, your venue adapter, and supporting crates to `Cargo.toml`: ```toml [dependencies] nautilus-common = "0.63" nautilus-live = "0.63" nautilus-model = "0.63" nautilus-okx = "0.63" nautilus-trading = { version = "0.63", features = ["examples"] } anyhow = "1" dotenvy = "0.15" log = "0.4" tokio = { version = "1", features = ["full"] } ``` ## Build the node The `LiveNode` uses a builder pattern. Add data and execution client factories for your venue, configure logging, and build. ```rust use log::LevelFilter; use nautilus_common::{enums::Environment, logging::logger::LoggerConfig}; use nautilus_live::node::LiveNode; use nautilus_model::identifiers::{AccountId, TraderId}; use nautilus_okx::{ common::enums::OKXInstrumentType, config::{OKXDataClientConfig, OKXExecutionClientConfig}, factories::{OKXDataClientFactory, OKXExecutionClientFactory}, }; let trader_id = TraderId::from("TESTER-001"); let account_id = AccountId::from("OKX-001"); let data_config = OKXDataClientConfig::builder() .instrument_types(vec![OKXInstrumentType::Swap]) .build(); let exec_config = OKXExecutionClientConfig::builder() .account_id(account_id) .instrument_types(vec![OKXInstrumentType::Swap]) .build(); let log_config = LoggerConfig { stdout_level: LevelFilter::Info, ..Default::default() }; let mut node = LiveNode::builder(trader_id, Environment::Live)? .with_name("MY-NODE-001".to_string()) .with_logging(log_config) .add_data_client( None, Box::new(OKXDataClientFactory::new()), Box::new(data_config), )? .add_exec_client( None, Box::new(OKXExecutionClientFactory::new()), Box::new(exec_config), )? .with_reconciliation(false) // Simplified; enable in production .with_delay_post_stop_secs(5) .build()?; ``` :::warning This example disables reconciliation for simplicity. In production, remove `.with_reconciliation(false)` so the engine aligns cached state with the venue on startup. See [Execution reconciliation](../concepts/execution/reconciliation.md). ::: ## Add strategies and run ```rust use nautilus_model::{identifiers::InstrumentId, types::Quantity}; use nautilus_trading::examples::strategies::{ GridMarketMaker, GridMarketMakerConfig, }; let mut config = GridMarketMakerConfig::builder() .instrument_id(InstrumentId::from("ETH-USDT-SWAP.OKX")) .max_position(Quantity::from("0.10")) .num_levels(3) .grid_step_bps(100) .skew_factor(0.5) .requote_threshold_bps(10) .expire_time_secs(8) .on_cancel_resubmit(true) .build(); // OKX rejects hyphens in client order IDs config.base.use_hyphens_in_client_order_ids = false; let strategy = GridMarketMaker::new(config); node.add_strategy(strategy)?; node.run().await?; ``` The node runs until interrupted (Ctrl+C) or shut down programmatically. ## Environment variables OKX reads API credentials from environment variables. Use a `.env` file with `dotenvy` or set them in your shell: ```bash export OKX_API_KEY="your_api_key" export OKX_API_SECRET="your_api_secret" export OKX_API_PASSPHRASE="your_passphrase" ``` For demo trading, set `.environment(OKXEnvironment::Demo)` on both config builders and use demo API credentials from OKX. Each adapter documents its required variables in the [integration guide](../integrations/) for that venue. ## Async runtime `LiveNode::run()` is async and requires a Tokio runtime. Use `#[tokio::main]` on your main function: ```rust #[tokio::main] async fn main() -> Result<(), Box> { dotenvy::dotenv().ok(); // ... node setup ... node.run().await?; Ok(()) } ``` ## Adapter examples Most adapters include runnable examples with data testers and execution testers: | Adapter | Example directory | | ------------------- | ----------------------------------------------- | | Architect AX | `crates/adapters/architect_ax/examples/` | | Betfair | `crates/adapters/betfair/examples/` | | Binance | `crates/adapters/binance/examples/` | | BitMEX | `crates/adapters/bitmex/examples/` | | Blockchain | `crates/adapters/blockchain/examples/` | | Bybit | `crates/adapters/bybit/examples/` | | Coinbase | `crates/adapters/coinbase/examples/` | | Databento | `crates/adapters/databento/examples/` | | Deribit | `crates/adapters/deribit/examples/` | | Derive | `crates/adapters/derive/examples/` | | dYdX | `crates/adapters/dydx/examples/` | | Hyperliquid | `crates/adapters/hyperliquid/examples/` | | Interactive Brokers | `crates/adapters/interactive_brokers/examples/` | | Kraken | `crates/adapters/kraken/examples/` | | Lighter | `crates/adapters/lighter/examples/` | | OKX | `crates/adapters/okx/examples/` | | Polymarket | `crates/adapters/polymarket/examples/` | | Sandbox | `crates/adapters/sandbox/examples/` | | Tardis | `crates/adapters/tardis/examples/` | # Write an Actor (Rust) Source: https://nautilustrader.io/docs/latest/how_to/write_rust_actor/ An actor receives market data, custom data/signals, and system events but does not manage orders. This guide walks through building a `SpreadMonitor` that subscribes to quotes and logs the bid-ask spread. For background on actors, traits, and handler dispatch, see the [Actors](../concepts/actors.md) and [Rust](../concepts/rust.md) concept guides. ## Define the struct An actor owns a `DataActorCore` and any state it needs. The core stores runtime state for the actor. User code normally reaches that state through the `DataActor` facade methods such as: - `clock()` - `cache()` - `config()` - `actor_id()` - `trader_id()` - Subscription methods ```rust use nautilus_common::{nautilus_actor, actor::{DataActor, DataActorConfig, DataActorCore}}; use nautilus_model::{data::QuoteTick, identifiers::{ActorId, InstrumentId}}; pub struct SpreadMonitor { core: DataActorCore, instrument_id: InstrumentId, } ``` ## Implement the constructor Create a `DataActorConfig` with an actor ID, then pass it to `DataActorCore::new`. The config fields use `Option` with defaults, so `..Default::default()` covers everything except the actor ID. ```rust impl SpreadMonitor { pub fn new(instrument_id: InstrumentId) -> Self { let config = DataActorConfig { actor_id: Some(ActorId::from("SPREAD_MON-001")), ..Default::default() }; Self { core: DataActorCore::new(config), instrument_id, } } } ``` ## Wire up the core and implement Debug The `nautilus_actor!` macro connects the actor's `DataActorCore` field to the runtime contract. By default it delegates to a field named `core`; pass a second argument for a different field name. Normal callbacks do not call the generated native accessors; use the `DataActor` facade methods on `self`. Runtime registration uses blanket `Actor` and `Component` implementations. The macro supplies the native runtime wiring; implement `Debug` manually or derive it. ```rust nautilus_actor!(SpreadMonitor); impl std::fmt::Debug for SpreadMonitor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SpreadMonitor").finish() } } ``` ## Implement the DataActor trait Override handler methods to receive data. All handlers have default no-op implementations, so you only override what you need. Each handler returns `anyhow::Result<()>`. ```rust impl DataActor for SpreadMonitor { fn on_start(&mut self) -> anyhow::Result<()> { self.subscribe_quotes(self.instrument_id, None, None); Ok(()) } fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> { let spread = quote.ask_price.as_f64() - quote.bid_price.as_f64(); log::info!("Spread: {spread:.5}"); Ok(()) } } ``` `subscribe_quotes` is available directly on `self` through the `DataActor` trait. See the [handler table](../concepts/rust.md#handler-methods) for all available handlers. ## Native runtime access Use the public `DataActor` facade by default. Add `DataActorNative` only for an explicit native-only access path that the facade methods cannot serve. Read-only properties are available on the facade: - `config()` - `actor_id()` - `trader_id()` - `is_registered()` The [Rust native traits](../concepts/rust.md#native-traits) section covers the native-traits applicability matrix and this method table: - [`DataActorNative` methods](../concepts/rust.md#dataactornative-methods) Those types do not cross the Python boundary, so portable actors should use facade methods such as: - `clock()` - `cache()` ## Register the actor With a `BacktestEngine`: ```rust let actor = SpreadMonitor::new(instrument_id); engine.add_actor(actor)?; ``` With a `LiveNode`: ```rust let actor = SpreadMonitor::new(instrument_id); node.add_actor(actor)?; ``` ## Guard safety When the system dispatches messages to your actor, it obtains a short-lived `ActorRef` guard from the registry. You do not manage these guards directly. If you write code that accesses other actors in a callback, follow these rules: - Look up actors by ID each time; do not cache an `ActorRef`. - Drop the guard before the scope ends; never store it in a field. - Never hold a guard across an `.await` point. The subscription methods on `DataActorCore` handle this correctly by capturing the actor ID and performing the lookup inside the callback closure. See [Runtime invariants](../developer_guide/rust.md#runtime-invariants) for the full threading and registry model. ## Full example See [`BookImbalanceActor`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/actors/imbalance) for a more complete actor that tracks per-instrument state and prints a summary on stop. # Write a Strategy (Rust) Source: https://nautilustrader.io/docs/latest/how_to/write_rust_strategy/ A strategy extends an actor with order management. This guide walks through building a minimal strategy that subscribes to quotes and submits market orders. Read [Write an Actor (Rust)](write_rust_actor.md) first. For background on strategy concepts and order management, see the [Strategies](../concepts/strategies.md) and [Rust](../concepts/rust.md) concept guides. ## Define the struct A strategy stores a `StrategyCore` field for runtime wiring. Normal strategy logic does not use the field directly; use the facade methods on `self`. ```rust use nautilus_common::actor::DataActor; use nautilus_model::{ data::QuoteTick, enums::OrderSide, identifiers::{InstrumentId, StrategyId}, types::Quantity, }; use nautilus_trading::{nautilus_strategy, strategy::{Strategy, StrategyConfig, StrategyCore}}; pub struct MyStrategy { core: StrategyCore, instrument_id: InstrumentId, trade_size: Quantity, } ``` ## Implement the constructor `StrategyConfig` takes a `strategy_id` and an `order_id_tag`. The tag is appended to all client order IDs from this strategy, preventing collisions when multiple strategies trade the same instrument. The tag cannot contain a hyphen, because the runtime reads it back from the final hyphen-separated part of the strategy ID. `StrategyCore::new` panics on an invalid tag; use `StrategyCore::new_checked` to handle it as an error instead. ```rust impl MyStrategy { pub fn new(instrument_id: InstrumentId) -> Self { let config = StrategyConfig { strategy_id: Some(StrategyId::from("MY_STRAT-001")), order_id_tag: Some("001".to_string()), ..Default::default() }; Self { core: StrategyCore::new(config), instrument_id, trade_size: Quantity::from("1.0"), } } } ``` ## Wire up the core and implement Debug The `nautilus_strategy!` macro generates the native runtime wiring used by registration and the `Strategy` trait impl. By default it delegates to a field named `core`; pass a second argument for a different field name. The macro does not make your strategy or its `StrategyCore` deref to runtime internals. It also adds `config()`, which returns the `StrategyConfig` passed to `StrategyCore::new`. Runtime registration uses blanket `Actor` and `Component` implementations that require native wiring and `Debug`. The macro supplies the native wiring; implement `Debug` manually or derive it. ```rust nautilus_strategy!(MyStrategy); impl std::fmt::Debug for MyStrategy { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("MyStrategy").finish() } } ``` ## Implement the DataActor trait Data handling works the same as in an actor. Subscribe in `on_start`, respond in handlers. ```rust impl DataActor for MyStrategy { fn on_start(&mut self) -> anyhow::Result<()> { self.subscribe_quotes(self.instrument_id, None, None); Ok(()) } fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> { let order = self.order().market( self.instrument_id, OrderSide::Buy, self.trade_size, None, None, None, None, None, None, None, ); self.submit_order(order, None, None, None)?; Ok(()) } } ``` `self.order()` builds orders and order lists. Available methods: - `market` - `limit` - `stop_market` - `stop_limit` - `market_to_limit` - `market_if_touched` - `limit_if_touched` - `trailing_stop_market` - `trailing_stop_limit` - `bracket` - `create_list` - `generate_client_order_id` - `generate_order_list_id` `submit_order` is available on `self` through the `Strategy` trait impl generated by the macro. ## Native runtime access Use the public facade in strategy logic: - `clock()` - `cache()` - `order()` - `portfolio()` - `strategy_id()` - The order management methods on `Strategy` Normal strategy code does not import `DataActorNative` or `StrategyNative`, and does not call native handles such as: - `core()` - `core_mut()` - `strategy_core()` - `strategy_core_mut()` - `order_factory()` - `order_factory_rc()` - `portfolio_rc()` Those native handles expose borrowed runtime state and stay in engine, runtime, registration, PyO3, testkit, or explicit latency-sensitive native code. The [Rust native traits](../concepts/rust.md#native-traits) section covers the native-traits applicability matrix and these method tables: - [`DataActorNative` methods](../concepts/rust.md#dataactornative-methods) - [`StrategyNative` methods](../concepts/rust.md#strategynative-methods) ## Override Strategy hooks To override `Strategy` trait methods such as order or position event handlers, pass them in a block. The macro generates the internal plumbing automatically; keep `DataActor` handlers in the separate `impl DataActor` block. ```rust nautilus_strategy!(MyStrategy, { fn on_order_rejected(&mut self, event: OrderRejected) { log::warn!("Order rejected: {}", event.reason); } }); ``` ## Order management methods The `Strategy` trait provides these facade methods: | Method | Action | | --------------------- | ----------------------------------------------- | | `submit_order` | Submit a new order to the venue. | | `submit_order_list` | Submit a list of contingent orders. | | `modify_order` | Modify price, quantity, or trigger price. | | `modify_orders` | Modify multiple orders for the same instrument. | | `cancel_order` | Cancel a specific order. | | `cancel_orders` | Cancel a filtered set of orders. | | `cancel_all_orders` | Cancel all orders for an instrument. | | `close_position` | Close a position with a market order. | | `close_all_positions` | Close all open positions. | ## Full examples - [`EmaCross`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/strategies/ema_cross): Dual-EMA crossover with indicator integration. - [`GridMarketMaker`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/strategies/grid_mm): Grid market making with configurable levels and requoting. # AX Exchange Source: https://nautilustrader.io/docs/latest/integrations/architect_ax/ [AX Exchange](https://architect.exchange) is a centralized and regulated derivatives exchange for traditional underlying asset classes. Operated by Architect Bermuda Ltd. and licensed by the [Bermuda Monetary Authority (BMA)](https://www.bma.bm), AX lists perpetual contracts in production and also exposes dated futures in its sandbox catalog. This integration supports live market data ingest and order execution with AX Exchange. ## Overview This adapter is implemented in Rust and exposed to Python through PyO3 bindings. It does not require external AX client libraries. This guide assumes a trader is setting up for both live market data feeds, and trade execution. The AX Exchange adapter includes multiple components, which can be used together or separately depending on the use case. - `AxHttpClient`: Low-level HTTP API connectivity. - `AxMdWebSocketClient` and `AxOrdersWebSocketClient`: Low-level WebSocket connectivity for Rust callers. - `AxDataClient`: A market data feed manager. - `AxExecutionClient`: An account management and trade execution gateway. - `AxDataClientFactory`: Factory for AX data clients. - `AxExecutionClientFactory`: Factory for AX execution clients. :::note Most users will define a configuration for a live trading node (as below), and won't need to necessarily work with these lower level components directly. ::: ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/architect_ax/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/architect_ax/examples/) ## AX Exchange documentation AX Exchange provides documentation for users at the [Architect documentation site](https://docs.architect.exchange/). Refer to the AX Exchange documentation in conjunction with this NautilusTrader integration guide. ## Products The production catalog contains perpetual contracts across these venue categories: | Venue category | Examples | Nautilus asset class | | ---------------- | ---------------------------- | -------------------- | | Foreign exchange | `EURUSD-PERP`, `JPYUSD-PERP` | FX | | Equities | `AAPL-PERP`, `NVDA-PERP` | Equity | | Energy ETFs | `USO-PERP`, `UNG-PERP` | Equity | | Metals | `XAU-PERP`, `XAG-PERP` | Commodity | | Energy | `WTI-PERP` | Commodity | | Treasuries | `UST10Y-PERP` | Debt | | Compute | `OCPI-H100-PERP` | Alternative | The sandbox also lists dated gold contracts such as `XAU-2026-SEP` and `XAU-2026-DEC`. The adapter maps a `crypto` venue category to the `CRYPTOCURRENCY` asset class, and any category it does not recognize to `ALTERNATIVE`. ### Perpetual contracts A perpetual contract (perpetual swap) is a derivative that tracks the price of an underlying asset without expiring. Unlike standard futures, there is no settlement date, which eliminates rollover costs and simplifies position management. A funding rate mechanism keeps the contract price aligned with the underlying index price through periodic payments between long and short holders. See the [Architect documentation](https://docs.architect.exchange/) for details on funding rate mechanics and contract specifications. Characteristics of AX perpetual contracts: - **Cash-settled in USD**: No physical delivery. All profit and loss is settled in USD. - **Funding rates**: Periodic payments keep the contract price aligned with the underlying. - **Multiplier of 1**: Each contract represents one unit of exposure to the underlying. - **Whole contracts only**: Fractional quantities are not supported. - **Margin**: Initial margin is required to open a position; maintenance margin to keep it open. The adapter represents an AX instrument without an expiration as `PerpetualContract` and an instrument with an expiration as `FuturesContract`. The venue category determines the Nautilus asset class. The adapter uses `MARGIN` account type and `NETTING` order management. ## Symbology The adapter preserves each AX symbol and appends the Nautilus venue identifier `.AX`. Perpetual symbols use the `-PERP` suffix. Dated symbols include their year and contract month. | Contract | AX Symbol | Nautilus InstrumentId | | ------------ | -------------- | --------------------- | | EUR/USD perp | `EURUSD-PERP` | `EURUSD-PERP.AX` | | Gold perp | `XAU-PERP` | `XAU-PERP.AX` | | Dated gold | `XAU-2026-SEP` | `XAU-2026-SEP.AX` | The venue identifier is `AX`. To construct a Nautilus `InstrumentId`: ```python from nautilus_trader.model import InstrumentId instrument_id = InstrumentId.from_str("EURUSD-PERP.AX") ``` ## Environments AX Exchange provides two trading environments. Configure the appropriate environment using the `environment` parameter in your client configuration. | Environment | Config | Description | | -------------- | -------------------------------------- | -------------------------------------- | | **Sandbox** | `environment=AxEnvironment.SANDBOX` | Test environment with simulated funds. | | **Production** | `environment=AxEnvironment.PRODUCTION` | Live trading with real funds. | ### Sandbox The default environment for development and testing with simulated funds. All sandbox endpoints are resolved automatically when `environment=AxEnvironment.SANDBOX`. #### 1. Create a sandbox account Follow the [Architect documentation](https://docs.architect.exchange/) to create a sandbox account. An invite code is required during registration. #### 2. Create API keys and fund the account Use the AX sandbox UI to generate API keys and deposit simulated funds into your account. Store the `api_key` and `api_secret` securely. #### 3. Set environment variables ```bash export AX_API_KEY="your-sandbox-api-key" export AX_API_SECRET="your-sandbox-api-secret" ``` #### 4. Configure the live node Set `environment=AxEnvironment.SANDBOX` on the data and execution client configs. See the [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/architect_ax/) for complete `LiveNode` setup. ### Production For live trading with real funds. Requires a verified AX Exchange account. ```python config = AxExecutionClientConfig( environment=AxEnvironment.PRODUCTION, ) ``` :::warning Ensure you are using the correct environment before placing orders. Sandbox is the default to prevent accidental live trading. ::: ## Market data The adapter provides real-time market data via WebSocket subscriptions, with HTTP endpoints for historical data backfill. ### Data types | AX Data | Nautilus Data Type | Notes | | ----------------- | ------------------- | -------------------------------------------------------------- | | Order book (L1) | `QuoteTick` | Best bid/ask top-of-book from L1 book subscription. | | Order book (L2) | `OrderBookDelta` | Aggregated price levels. | | Order book (L3) | `OrderBookDelta` | Per-snapshot order quantities with synthetic IDs. | | Trades | `TradeTick` | Real-time trade events from trade-only WebSocket subscription. | | Mark price | `MarkPriceUpdate` | Extracted from L1 ticker subscription. | | Bars/candles | `Bar` | OHLCV data (total volume only, no buy/sell breakdown). | | Funding rates | `FundingRateUpdate` | Polled via HTTP; interval configurable. | | Instrument status | `InstrumentStatus` | State changes from L1 ticker subscription. | AX instrument states map to `MarketStatusAction` as follows: | AX state | `MarketStatusAction` | | ----------------------------------- | --------------------------- | | Pre-open | `PRE_OPEN` | | Open | `TRADING` | | Closed, closed-frozen | `CLOSE` | | Halted | `HALT` | | Match-and-close auction | `CROSS` | | Suspended | `SUSPEND` | | Delisted, or any unrecognized state | `NOT_AVAILABLE_FOR_TRADING` | :::note Historical quote tick requests are not supported by AX Exchange. Only real-time quote data is available via WebSocket L1 book subscriptions. AX also publishes no index prices and no instrument close events, so those subscriptions log a warning and yield no data. ::: :::note AX L3 snapshots contain per-order quantities but no venue order IDs. The adapter assigns synthetic IDs within each snapshot. It cannot track the same individual order across snapshots. ::: :::note AX publishes no trade identifier for market data, so the adapter derives `TradeTick.trade_id` from the trade's timestamp, price, size, and aggressor side. REST and WebSocket agree on the same trade whenever both report its aggressor side. Prints that AX reports identically share an ID; only consumers that deduplicate market data on `trade_id` are affected, since fills carry the venue's own trade IDs. ::: ### WebSocket subscription behavior AX market data WebSocket subscriptions use one active stream per symbol. The adapter selects the smallest stream that covers the active Nautilus subscriptions: - A trades-only subscription uses AX `level: "TRADES"`, which delivers trade prints only. - Book-only and quote-only subscriptions set AX `trades: false` and `ticker: false` to suppress unrequested trade and ticker events. - Mark price and instrument status subscriptions require AX ticker events, so the adapter enables ticker delivery on the active book stream, opening an L1 stream when no book subscription exists. - Book deltas subscribe at the AX level matching the Nautilus book type. `L1_MBP` has no delta-capable AX equivalent, so the adapter logs a warning and subscribes at L2 instead. - If multiple Nautilus data types are active for a symbol, the adapter resubscribes only when the required AX level or delivery flags change. AX documents estimated funding rates on ticker events and an estimated-funding request on the orders WebSocket. Nautilus exposes settled funding-rate updates through HTTP polling; the adapter does not parse or emit the venue's estimated funding fields as a separate Nautilus data type. ### HTTP API behavior - `GET /tickers` returns limit/offset page metadata and supports `limit`, `offset`, and `sort` query parameters. - `GET /ticker` returns the ticker under a top-level `ticker` response field. - `GET /open-orders` uses limit/offset pagination. Open-order reconciliation traverses all pages and validates totals, offsets, duplicates, and completeness so detected response drift fails the request. - `GET /fills` and `GET /funding-rates` use cursor pagination. The adapter traverses each cursor chain as a best-effort historical read; AX corrections during traversal are not an atomic snapshot. - `GET /orders` exposes cursor metadata and supports `order_id`, `order_ids`, `account_id`, and optional timestamp filters. Startup mass-status reconciliation traverses its cursor chain, accepts partial pages, and rejects repeated cursors or duplicate order IDs. - Open-order, historical-order, fill, and position report requests resolve an uncached symbol through `GET /instrument` and cache the result. An instrument request or parse failure fails that entire report request instead of dropping venue state. - `GET /transactions` requires `start_timestamp_ns` and `end_timestamp_ns` with a range no wider than 7 days. The low-level client exposes its cursor and account selectors. - `GET /order-status` can include `reject_reason` and `reject_message` for rejected orders. - When an account selector is omitted, AX uses the primary account. The high-level execution client owns one primary account; low-level request models expose documented account selectors. ### Bar intervals | Interval | Description | | -------- | ----------- | | `1s` | 1-second | | `5s` | 5-second | | `1m` | 1-minute | | `5m` | 5-minute | | `15m` | 15-minute | | `1h` | 1-hour | | `1d` | 1-day | ## Orders capability The AX order-entry API has no order-type selector. Its single native order shape requires a price, which the adapter maps to a Nautilus `LIMIT` order. The adapter simulates a Nautilus `MARKET` order by previewing an aggressive price and submitting that priced shape with IOC. The official [REST place-order](https://docs.architect.exchange/api-reference/order-management/place-order) and [orders WebSocket](https://docs.architect.exchange/api-reference/order-management/orders-ws) request schemas contain no `order_type` or `trigger_price` field, and sandbox stop-limit submissions with unbreached triggers executed immediately at the active limit price. With conditional execution unconfirmed, the adapter rejects venue-native stop-limit orders before sending them. Nautilus can still emulate a stop-limit order locally. The common order emulator waits for the configured trigger, then sends a plain limit order to this adapter. ### Order types | Order Type | Supported | Notes | | ---------------------- | --------- | ----------------------------------------------- | | `MARKET` | ✓ | Adapter-simulated with an aggressive IOC price. | | `LIMIT` | ✓ | Maps to the native AX priced order shape. | | `STOP_LIMIT` | - | *Not supported by AX Exchange*. | | `LIMIT_IF_TOUCHED` | - | *Not supported by AX Exchange*. | | `STOP_MARKET` | - | *Not supported by AX Exchange*. | | `MARKET_IF_TOUCHED` | - | *Not supported by AX Exchange*. | | `TRAILING_STOP_MARKET` | - | *Not supported by AX Exchange*. | ### Execution instructions | Instruction | Supported | Notes | | ---------------- | --------- | ------------------------------------------------------------- | | `post_only` | ✓ | Maker-only; rejected if the order would take. | | `reduce_only` | - | Rejected locally; AX exposes no reduce-only field. | | `quote_quantity` | - | Rejected locally; the adapter wire path encodes base only. | | `display_qty` | - | Rejected locally; the adapter wire path has no display field. | The reduce-only boundary matters because AX has no reduce-only field. In sandbox, an order whose reduce-only instruction was dropped from the wire payload was accepted and filled as an ordinary order, which can open or increase exposure instead of closing it; production behavior was not verified. The adapter therefore denies reduce-only orders before submission rather than sending an instruction the venue cannot honor. The adapter also rejects quote-quantity and display-quantity instructions because its AX wire path cannot encode those semantics. This is an adapter boundary, not a claim that AX Exchange rejects equivalent venue-native features. ### Time in force | Time in Force | Supported | Notes | | -------------- | --------- | -------------------------------- | | `GTC` | ✓ | Good Till Canceled. | | `GTD` | - | Rejected locally by the adapter. | | `DAY` | ✓ | Valid until end of trading day. | | `IOC` | ✓ | Immediate or Cancel. | | `FOK` | - | Rejected locally by the adapter. | | `AT_THE_OPEN` | - | Rejected locally by the adapter. | | `AT_THE_CLOSE` | - | Rejected locally by the adapter. | The venue deprecates `DAY` and recommends `GTC` instead. ### Advanced order features | Feature | Supported | Notes | | ------------------ | --------- | ------------------------------------------------------------------ | | Order modification | ✓ | Atomic replace; AX returns a new venue order ID. | | Cancel order | ✓ | Single order cancellation. | | Cancel all orders | ✓ | Cancel all open orders for an instrument. | | Batch cancel | - | The adapter sends individual cancels. | | Order lists | ✓ | Sequential submission (orders submitted individually, non-atomic). | ### Position management | Feature | Supported | Notes | | --------------- | --------- | ------------------------------------ | | Query positions | ✓ | Real-time position updates. | | Position mode | - | Netting mode only. | | Cross margin | ✓ | Cross-margin across all instruments. | ### Order querying | Feature | Supported | Notes | | -------------------- | --------- | ------------------------------------------------------- | | Query open orders | ✓ | List all active orders. | | Query single order | ✓ | By venue order ID or client order ID (any order state). | | Order status reports | ✓ | Open-order checks and historical startup mass status. | | Fill reports | ✓ | Execution and fill history. | :::note Bulk open-order checks use `/open-orders` when `open_check_open_only` is enabled, which is the default. Otherwise, they use `/orders`. Startup mass-status reconciliation uses `/orders`, so its snapshot includes historical terminal orders such as filled and canceled orders. Single-order queries via `query_order` use the dedicated `/order-status` endpoint, which works for any order state. AX open and historical order payloads do not expose a stop order type or trigger price. REST-derived reconciliation therefore reports every visible external order as a limit order. The adapter does not submit venue-native conditional orders. ::: ## Authentication AX Exchange uses bearer token authentication: 1. API key and secret obtain a session token via `/authenticate`. 2. The session token is used as a bearer token for subsequent REST and WebSocket requests. 3. The adapter requests one-hour session tokens and refreshes them every 30 minutes. 4. A refresh updates REST authentication and the token used by the next WebSocket reconnect without interrupting the active connection. ## Configuration ### Environments and endpoints | Environment | HTTP API | HTTP API (orders) | Market Data WS | Orders WS | | ----------- | ------------------------------------------------ | --------------------------------------------------- | ------------------------------------------------ | ---------------------------------------------------- | | Sandbox | `https://gateway.sandbox.architect.exchange/api` | `https://gateway.sandbox.architect.exchange/orders` | `wss://gateway.sandbox.architect.exchange/md/ws` | `wss://gateway.sandbox.architect.exchange/orders/ws` | | Production | `https://gateway.architect.exchange/api` | `https://gateway.architect.exchange/orders` | `wss://gateway.architect.exchange/md/ws` | `wss://gateway.architect.exchange/orders/ws` | :::info Order management endpoints (place, cancel, replace, cancel-all, order status, open orders, historical orders, and initial margin requirement) use the orders base URL. Every other REST endpoint, including authentication, account state, fills, transactions, and market data, uses the API base URL. The adapter resolves both from the configured environment. ::: ### Data client configuration options | Option | Default | Description | | ---------------------------------- | --------- | ------------------------------------------------------------------- | | `api_key` | `None` | API key; loaded from `AX_API_KEY` env var when omitted. | | `api_secret` | `None` | API secret; loaded from `AX_API_SECRET` env var when omitted. | | `environment` | `SANDBOX` | Trading environment (`SANDBOX` or `PRODUCTION`). | | `base_url_http` | `None` | Override for the REST base URL. | | `base_url_ws_public` | `None` | Override for the market data WebSocket URL. | | `base_url_ws_private` | `None` | Override for the private orders WebSocket URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `http_timeout_secs` | `60` | Timeout (seconds) for REST requests. | | `max_retries` | `3` | Maximum retry attempts for REST requests. | | `retry_delay_initial_ms` | `1,000` | Initial delay (milliseconds) between retries. | | `retry_delay_max_ms` | `10,000` | Maximum delay (milliseconds) between retries (exponential backoff). | | `heartbeat_interval_secs` | `20` | Heartbeat interval (seconds) for WebSocket connections. | | `recv_window_ms` | `5,000` | Reserved; AX uses bearer tokens and the adapter sends no window. | | `update_instruments_interval_mins` | `60` | Interval (minutes) between instrument catalog refreshes. | | `funding_rate_poll_interval_mins` | `15` | Interval (minutes) between funding rate poll requests. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | ### Execution client configuration options | Option | Default | Description | | ------------------------- | --------- | ------------------------------------------------------------------- | | `account_id` | `AX-001` | Account ID for the execution client. | | `api_key` | `None` | API key; loaded from `AX_API_KEY` env var when omitted. | | `api_secret` | `None` | API secret; loaded from `AX_API_SECRET` env var when omitted. | | `environment` | `SANDBOX` | Trading environment (`SANDBOX` or `PRODUCTION`). | | `base_url_http` | `None` | Override for the API REST base URL. | | `base_url_orders` | `None` | Override for the orders REST base URL. | | `base_url_ws_private` | `None` | Override for the orders WebSocket URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `http_timeout_secs` | `60` | Timeout (seconds) for REST requests. | | `max_retries` | `3` | Maximum retry attempts for REST requests. | | `retry_delay_initial_ms` | `1,000` | Initial delay (milliseconds) between retries. | | `retry_delay_max_ms` | `10,000` | Maximum delay (milliseconds) between retries (exponential backoff). | | `heartbeat_interval_secs` | `30` | Heartbeat interval (seconds) for WebSocket connections. | | `recv_window_ms` | `5,000` | Reserved; AX uses bearer tokens and the adapter sends no window. | | `cancel_on_disconnect` | `False` | Cancel this WebSocket session's open orders on disconnect. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | When `transport_backend=None`, the compiled Rust default selects Sockudo when the `transport-sockudo` Cargo feature is enabled and Tungstenite otherwise. Use `AxDataClientConfig` with `AxDataClientFactory` and `AxExecutionClientConfig` with `AxExecutionClientFactory`. The Python examples show the complete `LiveNode.builder(...)` configuration for data and execution clients. ### API credentials There are two options for supplying your credentials to the AX Exchange clients. Either pass the corresponding `api_key` and `api_secret` values to the configuration objects, or set the following environment variables: - `AX_API_KEY` - `AX_API_SECRET` :::tip We recommend using environment variables to manage your credentials. ::: When starting the trading node, you'll receive immediate confirmation of whether your credentials are valid and have trading permissions. ## Implementation notes - **Whole contracts only**: AX uses integer contract quantities. The adapter models a one-contract size increment and lot size, while enforcing each instrument's separate `minimum_order_size`. Fractional quantities generate `OrderDenied` locally. - **Dated futures activation**: AX publishes expiration but not activation timestamps. The adapter uses zero for the unknown activation time and preserves that limitation in instrument metadata. - **Rate limiting**: The adapter applies a conservative rate limit of 10 requests/second with automatic exponential backoff on rate limit responses. - **Market orders**: AX does not support native market orders. The adapter uses a preview endpoint to determine the take-through price and submits an aggressive IOC limit order. Because the book can move between the preview and the submission, a simulated market order may fill partially. - **Stop-limit orders**: The adapter rejects venue-native stop-limit submissions because sandbox testing did not confirm conditional semantics. Use local order emulation when a strategy requires a stop-limit order. - **Order modification**: AX supports atomic order replacement via `POST /replace-order`. The execution client maps `modify_order` to this endpoint and records the new venue order ID it returns. A modification is rejected locally when it carries a trigger price, which AX has no field for, or when the order has no venue order ID yet. - **Funding rate polling**: The data client polls `GET /funding-rates` per subscribed instrument on `funding_rate_poll_interval_mins`, requesting a seven-day lookback so a rate is still found across weekends and holidays, and emits the latest rate only when it differs from the last one emitted. - **Cancel on disconnect**: Set `cancel_on_disconnect=True` in the execution client config to have the exchange cancel all open orders if the orders WebSocket disconnects. - **Instrument fee rates**: AX reports maker and taker rates per account on `GET /whoami`, so the adapter resolves them after authenticating and applies them to every instrument. A client with credentials fails to connect if that lookup fails, rather than reporting zero fees for the process lifetime. A data client configured without credentials cannot read the rates and reports zero fees. - **Fill commissions**: Real-time fill events from the WebSocket do not include fee data. Commission is reported as zero for streaming fills. During reconciliation, the REST `/fills` endpoint provides accurate fee information. - **Fill reconciliation window**: The `/fills` endpoint requires a bounded time range and caps the span at seven days. Reconciliation requests the most recent seven days of fills; fills older than that are not reconciled. - **Fill order identity**: AX can omit `order_id` for block trades and final settlement fills. The adapter derives a deterministic reconciliation order ID from `trade_id` for those classified records. Classification fields are optional for regular fills with a valid `order_id`. The adapter rejects rows with neither an order ID nor explicit special-fill classification, and rejects inconsistent classification. - **Unfilled IOC/FOK**: AX reports an unfilled immediate order as an expiry; the adapter maps it to `OrderCanceled` to match NautilusTrader semantics. - **One-tick quotes**: Example testers place post-only limits one tick from top of book. Those quotes can still fill. Flatten leftovers with `cargo run --bin ax-flatten -p nautilus-architect-ax` (`AX_IS_SANDBOX` defaults to true). That binary cancels all open orders on the account, then closes every position. ## Contributing :::info For additional features or to contribute to the AX Exchange adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Betfair Source: https://nautilustrader.io/docs/latest/integrations/betfair/ Founded in 2000, Betfair operates the world's largest online betting exchange. This integration supports instrument discovery, live market data, account state, order management, and execution updates through the Betfair Betting, Accounts, and Exchange Streaming APIs. The adapter is implemented in Rust and exposed to Python at `nautilus_trader.adapters.betfair`, so data and execution have the same behavior from either language. ## Overview The adapter includes several components, which can be used separately or together: - `BetfairHttpClient`: Low-level Betting and Accounts API connectivity. - `BetfairStreamClient`: Low-level Exchange Streaming API connectivity for the market and order streams. - `BetfairRaceStreamClient`: Low-level connectivity for the race and cricket data streams. - `BetfairInstrumentProvider`: Loads Betfair markets and converts them into Nautilus instruments. - `BetfairDataClient`: Market data feed manager. - `BetfairExecutionClient`: Account management and bet execution gateway. - `BetfairDataClientFactory`: Factory for Betfair data clients. - `BetfairExecutionClientFactory`: Factory for Betfair execution clients. :::note Most users will define a configuration for a live trading node, and won't need to work directly with these lower-level components. The Python examples show a complete `LiveNode.builder(...)` configuration for data and execution clients. ::: ## Installation Install NautilusTrader using the [installation guide](../getting_started/installation.md). The Betfair adapter is included in the Python package; no adapter-specific extra is required. ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/betfair/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/betfair/examples/) - [Book imbalance backtest tutorial](../tutorials/backtest_book_imbalance_betfair.md) ## Betfair documentation - [Betfair Developer Program](https://developer.betfair.com/) - [Exchange API Guide](https://developer.betfair.com/exchange-api/) - [Application keys](https://betfair-developer-docs.atlassian.net/wiki/spaces/1smk3cen4v3lu3yomq5qye0ni/pages/2687105/Application+Keys) - [Interactive login](https://betfair-developer-docs.atlassian.net/wiki/spaces/1smk3cen4v3lu3yomq5qye0ni/pages/2687772/Interactive+Login+-+API+Endpoint) ## Credentials Betfair requires an application key to authenticate API requests. After registering and funding your account, obtain your key with the [API-NG Developer AppKeys Tool](https://apps.betfair.com/visualisers/api-ng-account-operations/). Betfair assigns two keys per account: a **Live** key, which requires a one-time activation fee, and a **Delayed** key for development and testing. Supply the account credentials through configuration or environment variables: ```bash export BETFAIR_USERNAME= export BETFAIR_PASSWORD= export BETFAIR_APP_KEY= ``` The adapter uses Betfair's interactive login endpoint. It does not use client certificates. ## Timestamp policy The adapter keeps venue event time separate from local initialization time: - `ts_event` records when Betfair says the event occurred. - `ts_init` records when the live adapter received the containing stream message. Each live stream callback reads the real-time atomic clock once, before decoding the message. Every output decoded from that message shares the same `ts_init`. | Input | `ts_event` source | `ts_init` source | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | Market change (`mcm`) | Message publish time (`pt`). | Local receipt time. | | Race change (`rcm`) | Runner or race feed time (`ft`), falling back to the message publish time (`pt`) when `ft` is absent. | Local receipt time. | | Cricket change (`ccm`) | Message publish time (`pt`). | Local receipt time. | | Order change (`ocm`) | The relevant order lifecycle time. Acceptance uses `pd`; fills use `md`, falling back to `pt`; status and cancel events use the latest of `md`, `cd`, or `ld`, falling back to `pt`. OCM-level custom data uses `pt`. | Local receipt time. | | Historical data loader | The same feed-time rules as live data. | Message publish time (`pt`), because recorded data has no local receipt time. | When an OCM arrives during post-reconnect reconciliation, the adapter buffers the message together with its captured `ts_init`. Draining the buffer preserves the original receipt time instead of using the later replay time. ## Orders capability Betfair is a betting exchange, so several concepts from traditional financial venues do not apply. ### Order types | Order Type | Supported | Notes | | ---------------------- | --------- | ----------------------------------------------------------------- | | `MARKET` | ✓* | Supports `AT_THE_CLOSE`, which maps to Betfair `MARKET_ON_CLOSE`. | | `LIMIT` | ✓ | Supports regular limit orders and BSP on-close limit orders. | | `STOP_MARKET` | - | Not supported. | | `STOP_LIMIT` | - | Not supported. | | `MARKET_IF_TOUCHED` | - | Not supported. | | `LIMIT_IF_TOUCHED` | - | Not supported. | | `TRAILING_STOP_MARKET` | - | Not supported. | Submitting a `MARKET` order with any time in force other than `AT_THE_CLOSE` is rejected, because Betfair has no immediate market order. :::warning BSP on-close instructions carry a **liability**, not a stake. For `MARKET_ON_CLOSE` and `LIMIT_ON_CLOSE` orders, the adapter sends the order quantity as the Betfair liability. Size a BSP order by the amount you are prepared to lose, not by the stake you want matched. ::: ### Time in force | Time in force | Supported | Notes | | -------------- | --------- | ------------------------------------------------------------ | | `GTC` | ✓ | Maps to Betfair `PERSIST`. | | `DAY` | ✓ | Maps to Betfair `LAPSE`. | | `FOK` | ✓ | Maps to Betfair `FILL_OR_KILL`. | | `IOC` | ✓ | Maps to `FILL_OR_KILL` with `min_fill_size=0`. | | `AT_THE_CLOSE` | ✓ | Used for Betfair BSP `LIMIT_ON_CLOSE` and `MARKET_ON_CLOSE`. | | `GTD` | - | Not supported; the expiry is ignored and maps to `LAPSE`. | A `LIMIT` order in `AT_THE_OPEN` mode also routes to `LIMIT_ON_CLOSE`, because Betfair has no at-the-open instruction. ### Execution instructions | Instruction | Supported | Notes | | ------------- | --------- | ------------------------------------- | | `post_only` | - | Not applicable to a betting exchange. | | `reduce_only` | - | Not applicable to a betting exchange. | ### Advanced order features | Feature | Supported | Notes | | ------------------ | --------- | --------------------------------- | | Order Modification | ✓ | Price and size change separately. | | Bracket/OCO Orders | - | Not supported. | | Iceberg Orders | - | Not supported. | ### Batch operations | Operation | Supported | Notes | | ------------ | --------- | ---------------------------------------- | | Batch Submit | ✓ | Implemented through `SubmitOrderList`. | | Batch Modify | - | Not supported. | | Batch Cancel | ✓ | Implemented through `BatchCancelOrders`. | ### Cancel all orders Without an order side, `CancelAllOrders` sends one market-wide request and cancels orders for every selection in that market. With an order side, the command selects open cached orders for the exact instrument and side that belong to the current execution client and account, regardless of strategy. This includes reconciled external orders assigned to that client and excludes orders with no client assignment. The command uses the venue order IDs already held in cache and does not refresh order state first. If any otherwise eligible order lacks a cached venue order ID, the command sends no requests. Otherwise, it sends per-bet cancel instructions in batches of at most 60. `CancelAllOrders` does not create per-order cancel commands, so request and instruction failures emit no order events. OCM and mass-status reconciliation provide the final order state. ### Position management | Feature | Supported | Notes | | ---------------- | --------- | ---------------------------------------------- | | Query positions | - | Exposure is tracked per bet, not per position. | | Position mode | - | Not applicable to a betting exchange. | | Leverage control | - | No leverage on a betting exchange. | | Margin mode | - | No margin on a betting exchange. | Set `position_check_interval_secs=None` on `LiveExecutionEngineConfig`, because Betfair reports no venue-side positions to check against. ### Order querying | Feature | Supported | Notes | | --------------------- | --------- | -------------------------------------------------- | | Query open orders | ✓ | Built from `listCurrentOrders`. | | Order status updates | ✓ | Real-time bet state changes from the order stream. | | Fill reports | ✓ | Matched sizes and prices from `listCurrentOrders`. | | Cleared order history | - | The adapter does not request settlement history. | ## Execution control flow Startup: 1. Connect the HTTP client and fetch initial account funds. 2. Seed OCM state from cached orders. 3. Connect the Betfair execution stream and subscribe to order updates. 4. Generate startup mass status from `listCurrentOrders`. 5. Reconcile order and fill reports into the execution engine. Cached open orders with venue identity are restored as already accepted. The adapter also restores retained identity for up to 10,000 recent closed cached orders. Neither path emits another `OrderAccepted`. On every stream reconnect, the adapter repeats the order-and-fill mass-status fetch over a recent window. It halts new-order submissions after transport loss or a server `connectionClosed` status until the latest recovery generation dispatches its mass status. For the full transition sequence, see [post-reconnect reconciliation](#post-reconnect-reconciliation). Reconciliation behavior: - `stream_market_ids_filter` filters live OCM updates. - Reconciliation uses `reconcile_market_ids` only when `reconcile_market_ids_only=True` and `reconcile_market_ids` is set. - In every other case, including `reconcile_market_ids_only=True` with no `reconcile_market_ids`, the adapter falls back to `stream_market_ids_filter` for reconciliation scope. - `ignore_external_orders=True` skips OCM updates with no `rfo`. ## Session management and reconnection Betfair expires session tokens, so the adapter renews them rather than waiting for a failure. It handles renewal and recovery through four mechanisms: | Mechanism | Trigger | Action | | -------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | Periodic keep-alive | Every 10 hours (36,000 seconds). | Renew the session token and update retained stream authentication without reconnecting. | | Keep-alive fallback | Keep-alive returns `LoginFailed`. | Re-login, update all active stream authentication, then request replacement stream transports. | | Stream reconnect | Current order image after transport recovery. | Try keep-alive. `LoginFailed` triggers full re-login; other failures retain the existing session. | | HTTP report recovery | A report query returns a session error. | Try keep-alive and retry once; any keep-alive failure falls back to full re-login before that retry. | The periodic keep-alive tasks and data stream reconnect handler log and skip transient keep-alive errors such as network timeouts and 5xx responses. The execution reconnect handler also preserves the existing session token, but continues report reconciliation. At the periodic or handler-level keep-alive step, only `LoginFailed` triggers full re-login. HTTP report recovery differs: after a session error, any keep-alive failure falls back to full re-login before the report-level retry. Both the data and execution clients use the same session-renewal policy. Each spawns: - A **keep-alive task** that periodically attempts renewal. An ordinary successful keep-alive updates retained authentication without replacing the transport. - A **reconnect handler** that waits for the replacement order subscription to become current, then attempts to refresh the session. After a full re-login, the adapter updates authentication for every affected active stream before it requests any reconnect. Each replacement connection sends the latest authentication before retained subscriptions or traffic buffered during the reconnect. Market and order streams retain their subscription IDs and `clk`/`initialClk` resume values. Correlated status responses keep socket availability, authentication, pending subscriptions, current subscriptions, rejected requests, and degraded streams distinct. The data client applies the same update to active market, race, and cricket streams. A periodic keep-alive fallback requests replacement transports immediately after updating authentication. An HTTP report recovery requests an execution stream replacement after the query finishes. When full re-login occurs inside the execution stream reconnect handler, that handler first fetches and dispatches mass status, then requests a replacement execution stream. The replacement stream's `Connection` message starts another handler iteration; a successful keep-alive updates retained authentication without requesting another replacement. This ordering prevents a reconnect loop. ## Post-reconnect reconciliation After the initial handshake, a Betfair execution transport loss immediately halts new-order submissions. This applies to automatic network reconnects and replacements requested after a full re-login. The adapter assumes the cache may have diverged while the previous transport was unavailable. In particular, fills can complete and roll off the unmatched book before the post-reconnect stream image arrives. The adapter therefore fetches and dispatches a mass status over a recent window before allowing new submissions. | Step | Trigger | Action | | ---- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | 1 | Transport loss or a server `connectionClosed` status. | Advances the reconciliation generation and halts new submissions immediately. | | 2 | Replacement `Connection` message. | Marks authentication and retained subscriptions pending and raises `pending_resync`. | | 3 | Complete `SUB_IMAGE` or `RESUB_DELTA`. | Queues the current generation once. OCMs remain buffered until recovery completes. | | 4 | Reconnect task receives the generation. | Refreshes the session, requests `getAccountFunds`, then queries orders and fills with up to four bounded attempts. | | 5 | Both `listCurrentOrders` queries succeed. | Dispatches the complete mass status, commits fill deduplication, and reopens submissions under one generation check. | The account-state refresh is best effort: a request or parse failure is logged but does not prevent mass-status dispatch or reopening the gate. A keep-alive failure other than `LoginFailed` continues with the retained session because the report queries retain their own retry and session-recovery logic. Read-only mass-status recovery retries four times with exponential backoff. Exhausted retries, a failed full re-login, or a failed report dispatch leave the gate halted until a later reconnect succeeds or the client disconnects. A newer transport loss, reconnect, disconnect, or shutdown cancels stale recovery work. This fail-closed behavior also covers an active socket whose authentication or order subscription is not current. Mass-status dispatch and fill-deduplication commit form the completion boundary for the handled generation. A failed or stale recovery does not advance fill deduplication. The gate does not wait for a separate acknowledgement that the execution engine has applied the report to its cache. While the execution stream is unavailable or reconciliation is in progress: - `submit_order` and `submit_order_list` emit `OrderDenied` with reason `STREAM_RECONCILING: execution stream unavailable or recovering, retry after recovery`. - `cancel_order`, `batch_cancel_orders`, and `modify_order` pass through unchanged. - `pending_resync` buffers OCMs received after the replacement `Connection` message. Connectivity polling and command or report entry points invoke `process_pending_resync` on the engine thread, which synchronizes OCM state from the cache and drains the buffer. If the client disconnects while a reconciliation is still in flight, `clear_resync_state` clears the active halt so a subsequent connect/submit cycle starts clean. The lookback window for the mass-status fetch is `stream_gap_recovery_lookback_mins` (default `10`). Fill recovery requests `OrderProjection::All`, orders results by match time, and bounds the request at the recovery timestamp. Betfair applies the date range to match time, so the result includes an order placed before the lookback when it matched during the gap, including execution-complete and settled orders still returned by `listCurrentOrders`. ## Tick scheme and pricing Betfair uses a tiered tick scheme with varying increments across price ranges: | Price range | Tick size | | ---------------- | --------- | | 1.01 - 2.00 | 0.01 | | 2.00 - 3.00 | 0.02 | | 3.00 - 4.00 | 0.05 | | 4.00 - 6.00 | 0.10 | | 6.00 - 10.00 | 0.20 | | 10.00 - 20.00 | 0.50 | | 20.00 - 30.00 | 1.00 | | 30.00 - 50.00 | 2.00 | | 50.00 - 100.00 | 5.00 | | 100.00 - 1000.00 | 10.00 | Minimum price is 1.01, maximum is 1000.00. ## Order modification - Price and size cannot change atomically; these require separate operations. - Price modification uses `ReplaceOrders` (cancel + new order at new price). - Size reduction uses `CancelOrders` with a `size_reduction` parameter. - Size increase is not supported; submit a new order instead. A successful price replacement remains the same logical Nautilus order. The adapter maps the old and new Bet IDs to the same `client_order_id`, suppresses the cancel for the old bet, and emits exactly one `OrderUpdated` carrying the new Bet ID. This holds whether the REST response or order change message (OCM) arrives first. If the replacement OCM already contains a fill, `OrderUpdated` precedes `OrderFilled`. Betfair can return `CANCELLED_NOT_PLACED` when the replace operation cancels the old bet but fails to place its replacement. The adapter then emits `OrderCanceled` for the logical order instead of `OrderModifyRejected`. A late fill for the canceled Bet ID is still applied once, after which the order remains `CANCELED`. The same terminal outcome applies when the old-bet cancel OCM arrives while a replacement is pending and the REST call later returns any definitive replace failure. ### Recovering an ambiguous modification When the REST response is lost or ambiguous, the adapter resolves the modification from the OCM stream or from a confirming `listCurrentOrders` result. Only a fully paginated reconciliation can prove that the original order remained unchanged or closed without a replacement: - A bet listed under the same `customerOrderRef` with a different Bet ID promotes the pending replace. Both active and closed listings emit `OrderUpdated` carrying the new Bet ID, its price, and the original size. An active listing is then withheld from the resolving report set, while a closed listing follows the update through its terminal order status report. - A bet whose active size (matched plus remaining) has fallen to at least the requested size but below the original confirms the reduction. An active listing emits `OrderUpdated` carrying the reduced size, while a closed listing carries the confirmed size in its terminal report without an `OrderUpdated`. A smaller active size is a lapse or void rather than the requested reduction, and an unchanged one means Betfair has not applied the reduction yet, so both leave the command in flight. Whichever channel resolves the modification first wins, and the others become no-ops, so a size reduction confirmed by the stream is not repeated when its REST response finally returns. A listing that still carries only the original bet proves nothing while the REST request may still be running, so the order stays `PENDING_UPDATE`. After the REST result becomes ambiguous, a fully paginated reconciliation that shows the original Bet ID still executable emits `OrderModifyRejected`, retains its active report, and clears the pending replacement. If `customerOrderRef` uniquely resolves to the pending order, the same reconciliation with a closed original Bet ID and no replacement clears the pending state and lets the terminal report carry the cancellation. If `customerOrderRef` does not resolve uniquely, the adapter cannot identify a possible new Bet ID, so the replacement remains pending. A definitive modification failure also clears the pending state, so a later lapse cannot be mistaken for the requested reduction. Reconciliation withholds order status reports that would duplicate or contradict the resolved state: - The superseded replace leg on the resolving pass, whether the replacement is active or terminal, because its `CANCELED` report would otherwise cancel the logical order. - The active report that produced `OrderUpdated`, because the order is still pending locally while reconciliation runs. Reports retained alongside `OrderModifyRejected` and terminal reports follow the normal report path. A terminal replacement report follows its `OrderUpdated` into the retained terminal lifecycle. A terminal reduction resolves without `OrderUpdated`; that report and later reports carry the confirmed size rather than Betfair's original stake. The resolving pass suppresses a historical Bet ID as described above. Once the logical replacement order is terminal, later explicit and mass-status queries retain order status reports for its historical Bet IDs. ## Order command failures and retries ### Request correlation Betfair provides separate values for logical order correlation and request deduplication: | Field | Scope | Adapter behavior | | ------------------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `customerOrderRef` | One logical order | Derived from `client_order_id`, returned as OCM `rfo`, and retained across replacement Bet IDs. | | `customerRef` | One REST command | Generated for each place, replace, or cancel request and reused unchanged for every retry, including batches and reductions. | :::warning Client order IDs longer than 32 characters use their last 32 characters as `customerOrderRef`. Keep those suffixes distinct across tracked orders. A new submission whose reference matches another tracked order emits `OrderDenied` before `OrderSubmitted` or HTTP dispatch with `VALIDATION_FAILED: customerOrderRef collides with another tracked order`; in an order list, only the colliding leg is denied. ::: When OCM state is synchronized from cached orders, the adapter also recognizes the legacy first-32-character format. If either truncation identifies more than one tracked order, OCM and reconciliation order status and fill reports omit `client_order_id` and retain the Bet ID so reconciliation can match by venue identity. ### Retry and ambiguity State-changing order calls use up to three retries by default within a 45-second total budget. The elapsed-time limit keeps every retry within Betfair's 60-second `customerRef` deduplication window. The adapter handles failures as follows: | Failure or response | Order command handling | | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Transport failure, client timeout, malformed success response, or HTTP 5xx | Mark the attempt ambiguous and retry with the same `customerRef`. | | HTTP 429, `TOO_MANY_REQUESTS`, or `SERVICE_BUSY` | Retry with the same `customerRef`. | | `UNEXPECTED_ERROR` | Mark the attempt ambiguous and retry with the same `customerRef`. | | `TIMEOUT_ERROR` or an adapter cancellation | Leave the command ambiguous without retrying it. | | `TIMEOUT` report or `BET_IN_PROGRESS` | Leave the command ambiguous for OCM or reconciliation. | | Incomplete or contradictory report | Leave the command ambiguous unless a definitive top-level error proves rejection. | | Known validation, authentication, permission, or other definitive venue error | Reject the affected command without retrying it. | | Missing, malformed, or unknown nested API error under a server error | Leave the command ambiguous without retrying it until its meaning is explicitly supported. | An ambiguous placement remains `SUBMITTED`, an ambiguous replacement remains `PENDING_UPDATE`, and an ambiguous cancellation remains `PENDING_CANCEL` until OCM or reconciliation resolves it. The adapter does not emit a rejection because Betfair may have applied the request. Once a dispatched attempt has an unknown outcome, a later failed attempt cannot make the overall result definitive. Definitive placement, cancellation, and modification failures normally emit `OrderRejected`, `OrderCancelRejected`, and `OrderModifyRejected`, respectively. A definitive price replacement failure instead emits `OrderCanceled` once the old-bet cancel has arrived because that bet is no longer executable. `BET_TAKEN_OR_LAPSED` completes a cancellation for the same terminal reason. ### JSON-RPC errors Betfair JSON-RPC errors contain an outer numeric `code` and `message` and can also contain a nested API `errorCode` and `errorDetails`. The outer values describe the JSON-RPC envelope; Betfair commonly uses `-32099` with an actionable API error stored in the object named by `data.exceptionname`, such as `APINGException` or `AccountAPINGException`. The adapter preserves the outer and nested fields and uses the nested API error when available. Unknown, missing, or malformed nested data remains visible through the outer code and message and receives the conservative order handling shown above. Read-only calls retain their broader retry policy and can retry `TIMEOUT_ERROR` or a generic retryable outer error. ## Order stream fill handling The execution client processes order updates from the Betfair Exchange Streaming API. Two configuration options control how updates are filtered: - `stream_market_ids_filter`: filters at the market level (early exit, silent skip). - `ignore_external_orders`: filters at the order level (skips OCM updates with no `rfo`). ```mermaid flowchart TD A[OCM update arrives] --> B{stream_market_ids_filter set
and market not listed?} B -->|Yes| C[Skip whole market, silently] B -->|No| D{ignore_external_orders set
and order has no rfo?} D -->|Yes| E[Skip order, silently] D -->|No| F[Process applicable order status,
fill, or void changes] ``` After both filters pass, the adapter emits only the outputs that apply to the update. Market-level filtering exits before any per-runner work, and neither filter logs a warning. :::warning If you set `stream_market_ids_filter`, ensure it includes every market you trade. Orders placed on markets excluded from the filter miss live fill and cancel updates from the stream. ::: ### Fill handling The adapter handles several edge cases when processing fills from the stream: - **Incremental fills**: Betfair reports cumulative matched sizes per Bet ID. The adapter tracks a separate fill cursor for every current or historical Bet ID and restores those cursors from cached events during reconciliation. - **Overfill protection**: fills that would exceed the order quantity are rejected. - **Race conditions**: when stream fills arrive before the HTTP order response, the adapter caches the venue order ID immediately to ensure correct order matching. - **Replacement fills**: a fill reported against an old Bet ID updates the same logical order once without replacing its current Bet ID. A partial fill received while an order is `PENDING_UPDATE` or `PENDING_CANCEL` updates its filled quantity while preserving the pending command state. - **Late terminal corrections**: the adapter retains correlation and per-Bet fill and void state for the 10,000 most recent terminal identities, including identities restored from closed cached orders. Locally owned identities and external terminal Bet IDs share this bound. Delayed fills and void corrections for an unambiguous retained order emit direct order events. After applying a delayed fill to a canceled order, the adapter emits `OrderCanceled` again to preserve the terminal state. If the same update carries void corrections, the cancel precedes those corrections. Correlation and deduplication state expire together, so an older replay can return through the report path. - **Gap-window fills**: a fill that completes and rolls off the unmatched book during a stream disconnect is recovered by the post-reconnect mass-status reconciliation; see [Post-reconnect reconciliation](#post-reconnect-reconciliation). ### Voided fills Betfair can void matched bets after reporting them, for example after an integrity ruling or a VAR decision. The order stream carries the running total in `sv` (size voided). Voids caused by runner removal settle instead of streaming, so they do not reach this path. The adapter allocates each `sv` increase to locally applied fill lots newest-first and emits one cumulative [`OrderFillVoided`](../concepts/events/order_fill_voided.md) per affected `trade_id`. A first-seen snapshot seeds its cumulative void state without reversing exposure Nautilus never applied, so a reconnect does not double-correct. Any `sv` increase also triggers an account refresh. An `EXECUTION_COMPLETE` update with no locally applied fill lots takes the terminal path instead: one correction under a synthetic `VOID-{bet_id}` trade ID that carries the order to `VOIDED`. That status resolves only when `sv` is positive and both cancelled and lapsed quantities are zero, so a mixed update carrying `sc` or `sl` alongside `sv` emits no correction. Betfair voids never set `is_reopened`, so `VOIDED` is final. The adapter also publishes the [`BetfairOrderVoided`](#custom-data-types) custom data type carrying the venue's raw void detail. ## Rate limiting The adapter uses separate rate limit buckets so that account state polling and reconciliation do not throttle order placement: | Bucket | Default | Endpoints | Configurable | | ------- | ------- | ----------------------------------------------- | -------------------------------- | | General | 5/s | Account state, reconciliation, keep-alive. | `request_rate_per_second`. | | Orders | 20/s | `placeOrders`, `replaceOrders`, `cancelOrders`. | `order_request_rate_per_second`. | Read-only Betting API calls use the general HTTP retry budget, with up to three retries by default. State-changing calls use the policy in [Order command failures and retries](#order-command-failures-and-retries). After a report query returns a session or rate-limit error, the order status and fill report paths make one additional report-level attempt. A session error first tries keep-alive and falls back to full re-login after any keep-alive failure. Full re-login updates execution stream authentication and requests a replacement after the query finishes. A `TOO_MANY_REQUESTS` error waits 5 seconds before the report-level retry. Betfair's own API limits are more nuanced than a single request rate: | Category | Limit | Notes | | ------------------------ | -------------------- | ------------------------------------------------------------------------------------------ | | Order operations | 1,000 transactions/s | Total instructions across `placeOrders`, `cancelOrders`, `replaceOrders`. | | Order projection queries | 3 concurrent | `listMarketBook` (with `OrderProjection`), `listCurrentOrders`, `listMarketProfitAndLoss`. | | Best practice | 5 requests/s | Recommended for `listMarketBook` per market. | See [Why am I receiving the TOO_MANY_REQUESTS error?](https://support.developer.betfair.com/hc/en-us/articles/360000406111) for how Betfair applies these limits. ## Market version price protection Betfair carries a `version` on the market definition. It changes when the market itself is redefined, for example when a runner is removed or the market status changes. It does not track ordinary price updates or matched volume. Attaching that version to an order asks Betfair to lapse the bet rather than match it into a market that has since been redefined. :::warning `use_market_version` provides no protection today. The adapter reads the market version from the instrument's `info` dictionary, but it constructs every Betfair instrument with `info` unset, so no version is ever attached to a `placeOrders` or `replaceOrders` request. Setting `use_market_version=True` currently changes nothing; do not rely on it for price protection. ::: ## Custom data types The adapter emits custom data through the market, order, race, and cricket streams. Market custom data flows automatically when subscribed to markets. | Type | Stream | Metadata key | Description | | -------------------------- | ------- | --------------- | -------------------------------------------------- | | `BetfairTicker` | Market | `instrument_id` | Last traded price, traded volume, BSP indicators. | | `BetfairStartingPrice` | Market | `instrument_id` | Realized BSP after market close. | | `BetfairBspBookDelta` | Market | `instrument_id` | BSP projected book updates. | | `BetfairSequenceCompleted` | Market | | Marks end of a market change sequence. | | `BetfairOrderVoided` | Order | `instrument_id` | Voided order details (size voided, price, side). | | `BetfairRaceRunnerData` | Race | `selection_id` | Live GPS tracking per runner (TPD). | | `BetfairRaceProgress` | Race | `race_id` | Sectional times, running order, jump data. | | `BetfairCricketMatch` | Cricket | `event_id` | Fixture, team, match statistic, and incident data. | Subscribe by type name from an actor or strategy. Every type in the table above carries its metadata key on the published topic, so the subscription must supply that key and the value it is scoped to. `BetfairSequenceCompleted` is the exception: it publishes without metadata, so it is subscribed by type name alone. For segmented updates, the adapter emits this marker on `SEG_END`, after that segment's updates have been published. It does not emit the marker on `SEG_START` or `SEG`. ```python from nautilus_trader.model import DataType # One runner's GPS data self.subscribe_data(DataType("BetfairRaceRunnerData", metadata={"selection_id": 49411491})) # One race's progress self.subscribe_data(DataType("BetfairRaceProgress", metadata={"race_id": "35278018.1617"})) # Sequence markers carry no metadata self.subscribe_data(DataType("BetfairSequenceCompleted")) ``` Race data requires Total Performance Data (TPD) coverage and a Betfair API key with TPD access. Enable with `subscribe_race_data=True`. Not every race has GPS tracking. Cricket data requires `subscribe_cricket_data=True`. ## Historical data `BetfairDataLoader` converts recorded Betfair stream files into instruments, order book deltas, trade ticks, and instrument status and close events, along with the market, race, and cricket custom data types above. Files hold newline-delimited JSON, either plain or compressed with gzip (`.gz`) or bzip2 (`.bz2`). The loader parses `mcm`, `rcm`, and `ccm` messages and skips the rest, so it produces no `BetfairOrderVoided` because that type comes from the order stream. Use `load_instruments` when only the instrument definitions are needed, because it skips all other parsing. Trade ticks are derived from cumulative traded volumes, so the loader keeps that state across lines within a file. Call `reset` before loading an unrelated file to clear cached volumes and instruments. See the [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/betfair/examples/) for loading a file and running it through a backtest. ## Multi-node deployment When multiple trading nodes share a single Betfair account across different markets: 1. Set `stream_market_ids_filter` to include only that node's markets. 2. Set `reconcile_market_ids_only=True` with `reconcile_market_ids` to limit reconciliation scope. 3. Set `ignore_external_orders=True` to drop bets placed outside NautilusTrader. Market isolation between nodes comes from `stream_market_ids_filter` and the reconciliation scope, not from `ignore_external_orders`. Every bet this adapter submits carries a customer order reference, so another node's bets pass that filter; only bets with no reference, such as those placed on the Betfair site, are dropped. Without the market filters, each node reconciles and reports the whole account. ## Configuration The adapter configures stream liveness and message size as follows: - Market and order subscriptions set `heartbeatMs` to `5,000`, so Betfair sends at least one message every 5 seconds. When no update is available, Betfair sends an empty heartbeat change message. These subscriptions also enable segmentation. Race and cricket subscriptions do not support these fields. - `stream_heartbeat_secs` controls separate client-initiated heartbeat requests on all stream connections. It defaults to `None`, which sends none. Betfair recommends leaving these requests off unless a firewall or proxy needs traffic to keep the connection open because the heartbeat response blocks the connection while it is served. See Betfair's [Exchange Stream API heartbeat guidance](https://betfair-developer-docs.atlassian.net/wiki/spaces/1smk3cen4v3lu3yomq5qye0ni/pages/2687396/Exchange+Stream+API#ExchangeStreamAPI-Heartbeat/HeartbeatMessage). Outbound heartbeats do not set the server subscription interval or determine market and order stream readiness. For race and cricket streams, an unset timeout uses two outbound heartbeat intervals for dead-peer detection. - `stream_heartbeat_timeout_secs` overrides dead-peer detection. When unset, the adapter uses two effective server heartbeat intervals, rounded up to a whole second, and follows a valid interval reported by Betfair. An explicit override must cover at least two requested intervals. Dead-peer detection starts after the first market or order subscription, which avoids reconnect loops before a data client subscribes. Race and cricket streams do not support subscription heartbeats. - A change message with status 503 marks its subscription degraded without replacing the socket. A later current message restores data readiness after a valid initial image has been received. A degraded initial image still requires a later valid `SUB_IMAGE`. For execution, the recovery message queues mass-status reconciliation, and submissions reopen only after the report publishes. Execution submissions remain closed whenever the order stream is pending, rejected, degraded, disconnected, or reconciling. ### Data client configuration | Option | Default | Notes | | ----------------------------------- | -------- | ---------------------------------------------------------- | | `account_currency` | `GBP` | Betfair account currency. | | `username` | `None` | Falls back to `BETFAIR_USERNAME`. | | `password` | `None` | Falls back to `BETFAIR_PASSWORD`. | | `app_key` | `None` | Falls back to `BETFAIR_APP_KEY`. | | `proxy_url` | `None` | Optional proxy URL for HTTP requests. | | `request_rate_per_second` | `5` | General HTTP rate limit. | | `default_min_notional` | `None` | Optional minimum notional override. | | `event_type_ids` | `None` | Optional navigation filter. | | `event_type_names` | `None` | Optional navigation filter. | | `event_ids` | `None` | Optional navigation filter. | | `country_codes` | `None` | Optional navigation filter. | | `market_types` | `None` | Optional navigation filter. | | `market_ids` | `None` | Optional navigation filter. | | `min_market_start_time` | `None` | Optional navigation filter. | | `max_market_start_time` | `None` | Optional navigation filter. | | `stream_host` | `None` | Optional stream host override. | | `stream_port` | `None` | Optional stream port override. | | `stream_heartbeat_secs` | `None` | Outbound heartbeat interval in seconds; `None` sends none. | | `stream_heartbeat_timeout_secs` | `None` | Dead-peer override; `None` uses two server intervals. | | `stream_reconnect_delay_initial_ms` | `2,000` | Initial reconnect delay. | | `stream_reconnect_delay_max_ms` | `30,000` | Maximum reconnect delay. | | `stream_use_tls` | `True` | Use TLS for the stream connection. | | `stream_conflate_ms` | `None` | Explicit conflation setting. | | `subscription_delay_secs` | `3` | Delay before the first market subscription. | | `subscribe_race_data` | `False` | Subscribe to RCM updates. | | `subscribe_cricket_data` | `False` | Subscribe to cricket CCM updates. | :::warning When `stream_conflate_ms` is `None`, the adapter omits `conflateMs` from the subscription and leaves the conflation rate to Betfair. Set `stream_conflate_ms=0` to request no conflation explicitly and receive every price update. ::: ### Execution client configuration | Option | Default | Notes | | ----------------------------------- | ------------- | ------------------------------------------------------------------ | | `account_id` | `BETFAIR-001` | Account ID for the client core. | | `account_currency` | `GBP` | Betfair account currency. | | `username` | `None` | Falls back to `BETFAIR_USERNAME`. | | `password` | `None` | Falls back to `BETFAIR_PASSWORD`. | | `app_key` | `None` | Falls back to `BETFAIR_APP_KEY`. | | `proxy_url` | `None` | Optional proxy URL for HTTP requests. | | `request_rate_per_second` | `5` | General HTTP rate limit. | | `order_request_rate_per_second` | `20` | Order endpoint rate limit. | | `stream_host` | `None` | Optional stream host override. | | `stream_port` | `None` | Optional stream port override. | | `stream_heartbeat_secs` | `None` | Outbound heartbeat interval in seconds; `None` sends none. | | `stream_heartbeat_timeout_secs` | `None` | Dead-peer override; `None` uses two server intervals. | | `stream_reconnect_delay_initial_ms` | `2,000` | Initial reconnect delay. | | `stream_reconnect_delay_max_ms` | `30,000` | Maximum reconnect delay. | | `stream_use_tls` | `True` | Use TLS for the stream connection. | | `stream_market_ids_filter` | `None` | Optional live OCM market filter. | | `ignore_external_orders` | `False` | Only skips OCM updates with no `rfo`. | | `calculate_account_state` | `True` | Enables periodic account state polling. | | `request_account_state_secs` | `300` | Poll interval for account funds (`0` disables). | | `reconcile_market_ids_only` | `False` | When `True`, use `reconcile_market_ids`. | | `reconcile_market_ids` | `None` | Explicit startup reconciliation market IDs. | | `use_market_version` | `False` | Attach market version to orders; currently has no effect. | | `stream_gap_recovery_lookback_mins` | `10` | Lookback window for the post-reconnect mass-status reconciliation. | ## Contributing :::info For additional features or to contribute to the Betfair adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Binance Source: https://nautilustrader.io/docs/latest/integrations/binance/ Founded in 2017, Binance is one of the largest cryptocurrency exchanges in terms of daily trading volume, and open interest of crypto assets and crypto derivative products. NautilusTrader provides Binance integration for live market data and execution. The adapter is implemented in Rust and exposed to Python through the same public configurations, factories, and data types. Supported products: - **Binance Spot** (including Binance US) - **Binance USDT-Margined Futures** (crypto and TradFi perpetuals; current and next monthly and quarterly delivery contracts) - **Binance Coin-Margined Futures** (perpetuals and current or next quarterly delivery contracts) ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/binance/) - [Rust spot examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/binance/examples/spot/) - [Rust futures examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/binance/examples/futures/) ## Overview The adapter exposes these public components: - `BinanceDataClientConfig` and `BinanceExecutionClientConfig`: Live client configuration. - `BinanceInstrumentProviderConfig`: Instrument selection, filtering, warning, and fee policy. - `BinanceDataClientFactory` and `BinanceExecutionClientFactory`: Trading node client factories. - `load_binance_instruments`: Standalone configured instrument discovery. - `load_binance_order_book_deltas`: Rust-backed Binance depth CSV loading for order book wrangling. - `BINANCE`, `BINANCE_CLIENT_ID`, `BINANCE_VENUE`, and the client-order-ID decoders: Public identifiers and decoding utilities. :::note Most users need only the configs and factories, wired into a live trading node as shown under [Live node configuration](#live-node-configuration). The remaining components serve standalone loading and offline decoding. ::: Low-level HTTP and WebSocket clients, their caches, and product-specific instrument provider objects are not exposed through the Python API. Use the live configs and factories, or the standalone instrument loader, instead of depending on those internals. For standalone discovery, pass the same data-client and provider configuration used by a live client: ```python import asyncio from nautilus_trader.adapters.binance import BinanceDataClientConfig from nautilus_trader.adapters.binance import BinanceInstrumentProviderConfig from nautilus_trader.adapters.binance import BinanceProductType from nautilus_trader.adapters.binance import load_binance_instruments config = BinanceDataClientConfig( product_type=BinanceProductType.USD_M, instrument_provider=BinanceInstrumentProviderConfig( load_all=False, load_ids=["BTCUSDT-PERP.BINANCE"], ), ) instruments = asyncio.run(load_binance_instruments(config)) ``` This function supports Spot, USD-M, and COIN-M. It uses the configured environment, URLs, proxy, receive window, Binance US mode, filters, warning policy, and commission policy. Margin is not a supported Binance product and is rejected. For Binance depth CSV data, call the stateless loader directly: ```python from nautilus_trader.adapters.binance import load_binance_order_book_deltas df = load_binance_order_book_deltas(path, nrows=1_000_000) ``` The loader preserves the source values and column order. File-open failures and invalid numeric or side values raise `RuntimeError`. ### Product support | Product Type | Supported | Notes | | --------------------------------------- | --------- | ----------------------------------------- | | Spot Markets (incl. Binance US) | ✓ | | | Margin Accounts (Cross & Isolated) | - | *Not implemented.* | | USDT-Margined Futures (PERP & Delivery) | ✓ | Monthly and quarterly delivery contracts. | | Coin-Margined Futures (PERP & Delivery) | ✓ | Quarterly delivery contracts. | :::note Margin account features such as borrow, repay, and isolated margin management are not implemented. ::: :::info Each Binance client instance handles one product type. The configs use a singular `product_type` field, and the live factories create one data or execution client from one config. To run Spot and Futures in the same node, configure separate clients with distinct IDs such as `BINANCE_SPOT` and `BINANCE_FUTURES`, then pass the matching `client_id` when a strategy subscribes or submits orders. See the current Python examples for complete client setup. ::: ## Data types The integration includes several custom data types: - `BinanceSpotTicker`: Spot 24-hour ticker data including prices, volumes, and trade statistics. - `BinanceFuturesTicker`: Futures 24-hour ticker data including price and statistics. - `BinanceBar`: Bar data with additional volume metrics for historical and real-time use. - `BinanceFuturesMarkPriceUpdate`: Futures mark data including the estimated settlement price. - `BinanceFuturesLiquidation`: Futures liquidation events from the `forceOrder` stream. - `BinanceFuturesOpenInterest`: Current Futures open interest snapshot (request only). - `BinanceFuturesOpenInterestHist`: Futures open interest history for a period (request only). See the Binance [API Reference](/docs/python-api-latest/adapters/binance.html) for full definitions. ## Symbology Native Binance symbols are used where possible for spot and futures contracts. Because NautilusTrader supports multi-venue trading, it must distinguish between `BTCUSDT` the spot pair and `BTCUSDT` the perpetual futures contract (Binance uses the same symbol for both). Nautilus appends `-PERP` to USD-M perpetual symbols. For example, the Binance USD-M `BTCUSDT` perpetual becomes `BTCUSDT-PERP`. USD-M `TRADIFI_PERPETUAL` listings use the same suffix, so `XAUUSDT` becomes `XAUUSDT-PERP`. The adapter maps `TRADIFI_PERPETUAL` listings to `PerpetualContract` and derives their asset class from Binance's `underlyingType`: | Binance `underlyingType` | Nautilus asset class | | ------------------------------------------------------------ | -------------------- | | `EQUITY`, `CN_EQUITY`, `KR_EQUITY`, `HK_EQUITY`, `PREMARKET` | Equity | | `COMMODITY` | Commodity | Listings with other or missing values are skipped with a warning. The adapter preserves Binance's native `_PERP` suffix for COIN-M perpetuals, so `BTCUSD_PERP` remains unchanged. Delivery symbols keep Binance's `_YYMMDD` suffix. For example, `BTCUSDT_260925` and `BTCUSD_260925` remain unchanged within Nautilus. USD-M supports the documented `CURRENT_MONTH`, `NEXT_MONTH`, `CURRENT_QUARTER`, and `NEXT_QUARTER` contract types. COIN-M supports `CURRENT_QUARTER` and `NEXT_QUARTER`. Contract availability varies by environment and listing cycle. USD-M delivery instruments are linear and settle in the margin asset. COIN-M delivery instruments are inverse, settle in the margin asset (the base currency), and use Binance's `contractSize` as the instrument multiplier. Both use `onboardDate` and `deliveryDate` for activation and expiration. See Binance's official [USD-M common definitions](https://developers.binance.com/en/docs/products/derivatives-trading-usds-futures/common-definition) and [COIN-M common definitions](https://developers.binance.com/en/docs/products/derivatives-trading-coin-futures/common-definition). The Rust Futures data tester accepts a delivery instrument without source edits: ```bash BINANCE_FUTURES_INSTRUMENT_ID=BTCUSDT_260925.BINANCE \ cargo run -p nautilus-binance --example binance-futures-data-tester --features examples ``` ## Spot notional constraints Spot instruments loaded through SBE or JSON populate the dedicated `min_notional` and `max_notional` fields from `MIN_NOTIONAL` and `NOTIONAL` filters. When both filters are present, the parser uses the strictest bounds. These fields hold quote-currency `Money` values at the currency's precision. The risk engine checks these instrument fields using its price estimates. Instrument `info` is metadata for downstream actors and strategies and does not affect risk decisions. Binance applies its market-order flags and reference-price rules when validating orders at the venue. PostgreSQL preserves instrument metadata when restoring instruments. After upgrading an existing database, run `nautilus database init --schema "$PWD/schema/sql"` from the repository root to add metadata storage. Previously discarded metadata requires reloading instrument definitions. ## Order capability The following tables detail order types, execution instructions, and time-in-force options across the supported Binance products. ### Order types | Order Type | Spot | USDT Futures | Coin Futures | Notes | | ---------------------- | ---- | ------------ | ------------ | --------------------------------------- | | `MARKET` | ✓ | ✓ | ✓ | Quote quantity support: Spot only. | | `LIMIT` | ✓ | ✓ | ✓ | | | `STOP_MARKET` | ✓ | ✓ | ✓ | Spot sends Binance `STOP_LOSS`. | | `STOP_LIMIT` | ✓ | ✓ | ✓ | Spot sends Binance `STOP_LOSS_LIMIT`. | | `MARKET_IF_TOUCHED` | ✓ | ✓ | ✓ | Spot sends Binance `TAKE_PROFIT`. | | `LIMIT_IF_TOUCHED` | ✓ | ✓ | ✓ | Spot sends Binance `TAKE_PROFIT_LIMIT`. | | `TRAILING_STOP_MARKET` | - | ✓ | ✓ | Futures only. | Binance Spot publishes a supported order-type set per symbol in `exchangeInfo`. The adapter does not filter on it, so a conditional Spot order for a type the symbol does not support is rejected by the venue rather than locally. ### Execution instructions | Instruction | Spot | USDT Futures | Coin Futures | Notes | | ------------- | ---- | ------------ | ------------ | --------------------------------------------------------- | | `post_only` | ✓ | ✓ | ✓ | See restrictions below. | | `reduce_only` | - | ✓ | ✓ | Futures only; translated to `positionSide` in Hedge Mode. | In One-way Mode, the adapter sends Binance's `reduceOnly` field. Binance does not accept that field in Hedge Mode, so the adapter instead selects the closing `positionSide`. This keeps the order on the identified leg and prevents it from opening the opposite leg. See Binance's [New Order API](https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Order) for the wire restrictions. #### Post-only restrictions Only *limit* order types support `post_only`. | Order Type | Spot | USDT Futures | Coin Futures | Notes | | ------------ | ---- | ------------ | ------------ | --------------------------------------------------- | | `LIMIT` | ✓ | ✓ | ✓ | Uses `LIMIT_MAKER` for Spot, `GTX` TIF for Futures. | | `STOP_LIMIT` | - | ✓ | ✓ | Futures only. | ### Time in force | Time in force | Spot | USDT Futures | Coin Futures | Notes | | ------------- | ---- | ------------ | ------------ | ----------------------------------------- | | `GTC` | ✓ | ✓ | ✓ | Good Till Canceled. | | `GTD` | ✓* | ✓ | ✓* | *Non-default local mapping through `GTC`. | | `FOK` | ✓ | ✓ | ✓ | Fill or Kill. | | `IOC` | ✓ | ✓ | ✓ | Immediate or Cancel. | #### GTD policy [Binance Spot time-in-force values](https://github.com/binance/binance-spot-api-docs/blob/master/enums.md) are `GTC`, `IOC`, and `FOK`; Spot has no native `GTD` or `goodTillDate`. USD-M supports native `GTD` for `LIMIT` and the limit forms of `STOP` and `TAKE_PROFIT`. The adapter routes regular orders through HTTP or WebSocket trading, independent batches through HTTP `batchOrders`, and conditional algo orders through HTTP `algoOrder`. The current Binance WebSocket algo schema includes `goodTillDate` but does not include `GTD` in its `timeInForce` enum, so the adapter does not route GTD algo orders through that endpoint. COIN-M has no native `GTD` value or `goodTillDate` parameter in its documented order APIs. See the official [USD-M trade API](https://developers.binance.com/en/docs/catalog/core-trading-derivatives-trading-usd-s-m-futures/api/rest-api/trade) and [COIN-M common definitions](https://developers.binance.com/en/docs/products/derivatives-trading-coin-futures/common-definition). USD-M `goodTillDate` is an epoch timestamp in milliseconds, but Binance ignores any sub-second part. Nautilus rejects an expiry that is not on a whole-second boundary rather than silently rounding it. The expiry must be strictly greater than the current time plus 600 seconds and strictly less than `253402300799000`. Native GTD also rejects market and post-only orders and any order without an expiry. `use_gtd=True` is the default. It uses native USD-M GTD and rejects native GTD on Spot and COIN-M. Set `use_gtd=False` only when the submitting strategy has `manage_gtd_expiry=True`. The adapter then warns and sends `GTC`, while Nautilus cancels the order at its local expiry. ### Advanced order features | Feature | Spot | USDT Futures | Coin Futures | Notes | | ------------------ | ---- | ------------ | ------------ | ------------------------------------------- | | Order Modification | ✓ | ✓ | ✓ | Price and quantity for `LIMIT` orders only. | | OCO Orders | ✓ | - | - | Spot OCO submitted via `orderList/oco`. | | Bracket Orders | - | - | - | *Planned*. Currently denied at submission. | | Iceberg Orders | ✓ | - | - | Spot `icebergQty` from `display_qty`. | ### Batch operations | Operation | Spot | USDT Futures | Coin Futures | Notes | | ------------ | ---- | ------------ | ------------ | --------------------------------------- | | Batch Submit | ✓ | ✓ | ✓ | Spot OCO or Futures `batchOrders`. | | Batch Modify | - | - | - | Not implemented. | | Batch Cancel | -* | ✓ | ✓ | *Spot falls back to individual cancels. | #### Cancel all orders behavior By default, `Strategy.cancel_all_orders()` sends individual cancels for orders associated with that strategy. When `strategy_only=False` is used, the strategy sends a broad `CancelAllOrders` command to the adapter. The adapter includes orders in both open and inflight (SUBMITTED) states so that it also cancels orders not yet acknowledged by Binance. **Multi-strategy safety**: When multiple strategies trade the same instrument, the adapter compares orders associated with the requesting strategy against all orders for that instrument. If all orders are associated with the strategy, a single cancel-all API call is used. Otherwise, per-strategy cancels are sent (batch for regular orders, individual for algo orders) to avoid affecting other strategies. **Futures algo orders**: Conditional order types (`STOP_MARKET`, `STOP_LIMIT`, `TAKE_PROFIT`, `TAKE_PROFIT_MARKET`, `TRAILING_STOP_MARKET`) require a different cancel endpoint. The adapter routes these through the correct endpoint automatically. Once an algo order triggers and becomes a regular order, it uses the standard cancel endpoint. **Endpoints used**: | Product | Regular Orders | Algo Orders (batch) | Algo Orders (individual) | | ------------ | ------------------------------- | -------------------------------- | --------------------------- | | Spot | `DELETE /api/v3/openOrders` | N/A | N/A | | USDT Futures | `DELETE /fapi/v1/allOpenOrders` | `DELETE /fapi/v1/algoOpenOrders` | `DELETE /fapi/v1/algoOrder` | | Coin Futures | `DELETE /dapi/v1/allOpenOrders` | `DELETE /dapi/v1/algoOpenOrders` | `DELETE /dapi/v1/algoOrder` | #### Submit, modify, and cancel retry policy The execution clients send each submit, modify, or cancel command once. They do not blindly retry a command after a timeout, network failure, or Binance unknown-status response because the first request may have reached the matching engine. Retrying could create a duplicate order or apply a second amendment. - Local submit validation emits `OrderDenied` before submission; local modify validation emits `OrderModifyRejected`. A definitive venue rejection emits the matching rejection event. - An ambiguous transport result remains inflight and is resolved by the private stream or REST reconciliation. The adapter does not emit a false rejection while the venue outcome is unknown. - A Futures algo cancel may fall back from the pre-trigger algo endpoint to the regular-order endpoint. This changes endpoint after the order triggers; it does not resend the same cancel to the same endpoint. - Strategy code must not resubmit a command while its result is ambiguous. Wait for reconciliation or query the order by its client order ID. `BinanceDataClientConfig` and `BinanceExecutionClientConfig` expose `max_retries`, `retry_delay_initial_ms`, and `retry_delay_max_ms` for HTTP GET requests. Transient read failures retry with bounded exponential backoff and fresh authentication fields. A venue `Retry-After` header sets the minimum delay, which can exceed `retry_delay_max_ms`. The fixed total retry budget is 180 seconds. When a required delay exceeds the remaining budget, the request returns the venue error without waiting. These settings do not retry order commands because resending an ambiguous command could duplicate an order or amendment. ### Position management | Feature | Spot | USDT Futures | Coin Futures | Notes | | ---------------- | ---- | ------------ | ------------ | --------------------------------------- | | Query positions | - | ✓ | ✓ | Real-time position updates. | | Position mode | - | ✓ | ✓ | One-Way vs Hedge mode (position IDs). | | Leverage control | - | ✓ | ✓ | Dynamic leverage adjustment per symbol. | | Margin mode | - | ✓ | ✓ | Cross vs Isolated margin per symbol. | Binance Futures logs out-of-scope position symbols at debug and drops them before parsing. For the remaining rows, position report generation warns when an amount cannot be parsed. After flat positions are removed, unresolved instruments and other conversion failures also warn. If any position fails, `generate_position_status_reports` returns an error with the failure count instead of an incomplete report set. See [instrument availability](../concepts/execution/reconciliation.md#instrument-availability). ### Risk events | Feature | Spot | USDT Futures | Coin Futures | Notes | | -------------------- | ---- | ------------ | ------------ | ---------------------------------- | | Liquidation handling | - | ✓ | ✓ | Exchange-forced position closures. | | ADL handling | - | ✓ | ✓ | Auto-Deleveraging events. | Binance Futures can trigger exchange-generated orders in response to risk events: - **Liquidations**: When insufficient margin exists to maintain a position, Binance forcibly closes it at the bankruptcy price. These orders have client IDs starting with `autoclose-`. - **ADL (Auto-Deleveraging)**: When the insurance fund is depleted, Binance closes profitable positions to cover losses. These orders use client ID prefix `adl_autoclose`. - **Settlements (USD-M)**: Funding and margin settlement orders use client IDs starting with `settlement_autoclose-`. - **Deliveries (COIN-M)**: Expiring delivery contracts auto-close with client IDs starting with `delivery_autoclose-`. - **Insurance fund**: Takeover by the insurance fund uses status `NEW_INSURANCE` (deprecated on the public changelog but still observed on the wire). The adapter detects these special order types via their client ID patterns (checked before the execution type), then: 1. Logs a warning with order details for monitoring. 2. Generates a `FillReport` with correct fill details and TAKER liquidity side. 3. Generates an `OrderStatusReport` for reconciliation. Upstream references: - [USDT-M `ORDER_TRADE_UPDATE`](https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update) - [COIN-M `ORDER_TRADE_UPDATE`](https://developers.binance.com/docs/derivatives/coin-margined-futures/user-data-streams/Event-Order-Update) The execution engine creates external orders from runtime status reports when the order is not already in cache. This covers first-seen exchange-generated orders (the typical case for a live liquidation or ADL event). The engine assigns the order through the instrument's active external order claim, configured initially with `external_order_instrument_ids`, or to the `EXTERNAL` strategy by default. :::note The status report and fill report are emitted bundled as a single `OrderWithFills` execution report. The engine creates the external order from the status report and then applies the real fill, preserving the venue's `trade_id` and `commission`. Any residual quantity not covered by the bundled fills is closed with an inferred fill from the status report's `avg_px`. ::: #### Commission estimation When Binance omits the commission fields (`N`/`n`) from the fill event, the adapter estimates commission as `default_taker_fee * qty * price` using the quote currency. This applies to USD-M linear contracts only. COIN-M inverse contracts use zero commission as a fallback because the linear formula does not account for contract size. Configure `default_taker_fee` on `BinanceExecutionClientConfig` to match your fee tier (default: 0.0004 / 0.04%). ### Order querying | Feature | Spot | USDT Futures | Coin Futures | Notes | | -------------------- | ---- | ------------ | ------------ | ------------------------------ | | Query open orders | ✓ | ✓ | ✓ | List all active orders. | | Query order history | ✓ | ✓ | ✓ | Historical order data. | | Order status updates | ✓ | ✓ | ✓ | Real-time order state changes. | | Trade history | ✓ | ✓ | ✓ | Execution and fill reports. | #### Futures trade-history retention Binance retains USD-M and COIN-M account trades for the past three months. Because Binance does not define whether this means calendar months or a fixed duration, the adapter treats the most recent 88 days as its complete Futures fill-history window. `generate_fill_reports` rejects an explicit start before that window. The boundary uses the command's `ts_init`, capped so it can trail the current client time by no more than 12 hours. An end time also requires a start time. For mass status, an unset `reconciliation_lookback_mins` or a value longer than the complete window applies that window. The returned `ExecutionMassStatus` sets `lookback_start` to the applied boundary and `reports_complete` to `false`; see the [mass-status history contract](../concepts/execution/reconciliation.md#mass-status-history-contract). Binance Spot is unaffected. See the Binance [Futures change log](https://developers.binance.com/en/docs/products/derivatives-trading-usds-futures/change-log). ### Contingent orders | Feature | Spot | USDT Futures | Coin Futures | Notes | | ------------------ | ---- | ------------ | ------------ | -------------------------------------------- | | Order lists | ✓ | ✓ | ✓ | Spot OCO lists; Futures independent batches. | | OCO orders | ✓ | - | - | Spot only, via `orderList/oco`. | | Bracket orders | - | - | - | *Planned*. Currently denied at submission. | | Conditional orders | ✓ | ✓ | ✓ | Stop and market-if-touched orders. | ### Order parameters Customize individual orders by supplying a `params` dictionary when calling `Strategy.submit_order` (Python) or setting `Params` on a `SubmitOrder` command (Rust). The Binance execution clients recognize: | Parameter | Type | Products | Purpose | Restrictions | | ---------------- | ------ | ----------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------- | | `price_match` | `str` | USDT/COIN Futures | Delegate price selection to Binance. | `LIMIT` only; not with `post_only`. | | `close_position` | `bool` | USDT/COIN Futures | Close the whole position when the trigger fires. | `StopMarket` and `MarketIfTouched` only; requires `reduce_only=true`; not in order lists. | | `rpi` | `bool` | USDT Futures | Submit a Retail Price Improvement order. | `LIMIT` only; requires `post_only=true`; individual orders only. | See [Price match](#price-match), [RPI](#rpi), and [Close position](#close-position) for the full behavior. ### Price match Binance Futures supports BBO (Best Bid/Offer) price matching via the `priceMatch` parameter, which delegates price selection to the exchange. Limit orders dynamically join the order book at optimal prices without specifying an exact price level. When using `price_match`, you submit a limit order with a reference price (for local risk checks), and Binance determines the actual working price based on the current market state and price match mode. #### Valid price match values | Value | Behavior | | ------------- | -------------------------------------------------------------- | | `OPPONENT` | Join the best price on the opposing side of the book. | | `OPPONENT_5` | Join the opposing side price but allow up to a 5-tick offset. | | `OPPONENT_10` | Join the opposing side price but allow up to a 10-tick offset. | | `OPPONENT_20` | Join the opposing side price but allow up to a 20-tick offset. | | `QUEUE` | Join the best price on the same side (stay maker). | | `QUEUE_5` | Join the same-side queue but offset up to 5 ticks. | | `QUEUE_10` | Join the same-side queue but offset up to 10 ticks. | | `QUEUE_20` | Join the same-side queue but offset up to 20 ticks. | :::info For more details, see the [official documentation](https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api). ::: #### Event sequence When an order is submitted with `price_match`: 1. Nautilus sends the order with the `priceMatch` parameter and omits the limit price from the API request. 2. Binance accepts the order and determines the actual working price. 3. Nautilus generates an `OrderAccepted` event. 4. If the Binance-accepted price differs from the reference price, Nautilus generates an `OrderUpdated` event with the actual working price. 5. The order price in the Nautilus cache now matches the Binance-accepted price. #### Example ```python order = strategy.order_factory.limit( instrument_id=InstrumentId.from_str("BTCUSDT-PERP.BINANCE"), order_side=OrderSide.BUY, quantity=Quantity.from_int(1), price=Price.from_str("65000"), # Reference price for local risk checks ) strategy.submit_order( order, params={"price_match": "QUEUE"}, ) ``` :::note If Binance accepts the order at a different price (e.g. 64,995.50), you receive an `OrderAccepted` event followed by an `OrderUpdated` event with the new price. ::: ### RPI Binance RPI (Retail Price Improvement) uses `timeInForce=RPI`. It is post-only and only matches eligible retail orders from the Binance App or Web. Nautilus exposes it through the Binance-specific `rpi` parameter; use it only with a USD-M LIMIT order whose `post_only=true`. It is supported only for individual `SubmitOrder` commands; `SubmitOrderList` is denied. Without `rpi`, regular post-only orders continue to use `GTX`. See Binance's [USD-M Futures API definitions](https://developers.binance.com/docs/derivatives/usds-margined-futures/common-definition) for venue details. RPI is available only for symbols whose `permissionSets` contains `RPI` in the `GET /fapi/v1/exchangeInfo` response. Check symbol eligibility before submitting an RPI order; see Binance's [RPI guide](https://www.binance.com/en/support/faq/detail/92c83c53173947c4a44f9a7277c3b9ce). The Rust example assumes `order` is a post-only LIMIT order for an eligible symbol. For Python, set `instrument_id` to an eligible instrument and choose `quantity` (`Quantity`) and `price` (`Price`) that meet its trading rules. ```rust tab="Rust" use nautilus_core::params::Params; let mut params = Params::new(); params.insert("rpi".to_string(), true.into()); self.submit_order(order, None, None, Some(params))?; ``` ```python tab="Python" order = strategy.order_factory.limit( instrument_id=instrument_id, order_side=OrderSide.BUY, quantity=quantity, price=price, post_only=True, ) strategy.submit_order(order, params={"rpi": True}) ``` ### Close position Binance Futures conditional orders support `closePosition`, which closes the entire position when the trigger fires. Binance resolves the quantity server-side from the current position size at trigger time. See the official [USD-M Algo Service API](https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/rest-api/New-Algo-Order) and [COIN-M Algo Service API](https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/rest-api/New-Algo-Order). Unlike `reduce_only`, `closePosition` adapts to position size changes, and Binance auto-cancels the order when the position is closed by other means. Set `reduce_only=true` on the Nautilus `StopMarket` or `MarketIfTouched` order, then pass `close_position=true` in its `params`. The reduce-only flag records the order's closing intent and is required for the order to pass while the trading state is `REDUCING`. The adapter translates this combination into Binance's close-all instruction and rejects `close_position` in order lists. Allow Binance whole-position exits in the risk engine configuration: ```python from nautilus_trader.adapters.binance import BINANCE_VENUE from nautilus_trader.config import LiveRiskEngineConfig risk_engine = LiveRiskEngineConfig( full_position_exit_venues=[BINANCE_VENUE], ) ``` The allowlist defaults to empty. Without this entry, the placeholder quantity receives the same minimum quantity, maximum quantity, and notional checks as an ordinary order. Pass the open position ID when submitting the order so the risk engine can verify that the exit reduces it. ```rust tab="Rust" let params = Params::from([("close_position", true.into())]); self.submit_order(order, Some(position.id), None, Some(params))?; ``` ```python tab="Python" strategy.submit_order( order, position_id=position.id, params={"close_position": True}, ) ``` :::info The Nautilus order must set `reduce_only=true`, but Binance does not permit its `reduceOnly` field with `closePosition=true`. The adapter therefore sends `closePosition=true` while omitting `quantity` and `reduceOnly` from the Binance request. In Hedge Mode, it also sends the closing `positionSide`. For an allowlisted venue, the risk engine still validates quantity precision and positivity, the trigger price, the order shape and side, and the linked open position. It does not apply minimum or maximum quantity and notional bounds to the placeholder quantity. ::: :::warning Only add a venue when its configured execution client enforces whole-position closing semantics. An execution client that does not interpret `close_position` may submit only the placeholder quantity through its standard reduce-only path instead of closing the whole position. ::: ### Trailing stops For trailing stop market orders on Binance: - Use `activation_price` (optional) to specify when the trailing mechanism activates. - When omitted, Binance uses the current market price at submission time. - Use `trailing_offset` for the callback rate, with `TrailingOffsetType.BASIS_POINTS`. The adapter rejects any other offset type, and rejects a callback rate outside Binance's 0.1% to 10% range. :::warning Do not use `trigger_price` for trailing stop orders: it will fail with an error. Use `activation_price` instead. ::: ## Link & Trade The NautilusTrader integration ID is automatically prefixed to all system-generated client order IDs for every order placed through the Binance adapter. This provides transparent order attribution through Binance's [Link and Trade](https://developers.binance.com/docs/binance_link/link-and-trade) program without requiring any user configuration. The adapter uses a deterministic two-way encoding to compress outgoing `ClientOrderId` values into a compact format that fits within Binance's 36-character `newClientOrderId` limit, and decodes incoming order events back to the original ID before they reach strategies. This transformation is fully transparent: strategies see only their original `ClientOrderId` values at all times. :::note The integration ID prefix applies to all order operations including submissions, modifications, cancellations, and status queries. Orders placed before this support was added are handled gracefully through passthrough decoding. ::: ### Decoding client order IDs When querying Binance directly (REST API, web UI, or your own HTTP code), the `clientOrderId` field contains the encoded form. Two utility functions recover the original Nautilus `ClientOrderId`: ```python from nautilus_trader.adapters.binance import ( decode_binance_futures_client_order_id, decode_binance_spot_client_order_id, ) # Encoded ID from a Binance REST response or the web UI encoded = "x-TD67BGP9-T0000000000000" original = decode_binance_spot_client_order_id(encoded) # Returns "O-20200101-000000-000-000-0" # Futures equivalent encoded_futures = "x-aHRE4BCj-T0000000000000" original_futures = decode_binance_futures_client_order_id(encoded_futures) # Returns "O-20200101-000000-000-000-0" ``` Strings without the broker prefix pass through unchanged, so these are safe to call on any `clientOrderId` value. :::note The adapter decodes automatically wherever it returns Nautilus types such as `OrderStatusReport`. Manual decoding is only needed when working outside the adapter: direct REST queries, the Binance web UI, or raw venue payloads. ::: ## Order books Order books can be maintained at full or partial depths. The diff-depth stream and its update rate differ by product and Spot transport: | Product / transport | Diff-depth stream | Update rate | | ------------------- | -------------------- | ---------------- | | Spot SBE | `@depth` | 25ms | | Spot JSON | `@depth` | 1000ms (default) | | Futures | `@depth@0ms` | Unthrottled | ### Futures L2 subscriptions Futures `L2_MBP` subscriptions with depth 5, 10, or 20 use the partial-depth stream `@depth@100ms`. Binance provides partial-depth streams only at these depths. Each message is a snapshot of both sides of the book, emitted as a `Clear` delta followed by the snapshot levels. This removes absent prices and keeps at most the requested number of levels per side. These subscriptions do not request a REST snapshot, including after reconnects. Futures subscriptions without a depth, or with depth 50, 100, 500, or 1000, use the diff-depth stream. The depth limits the initial and reconnect REST snapshots, not the maintained book; omitting it selects a 1000-level snapshot. Subsequent updates can add levels beyond that depth. The `OrderBook.bids(depth=...)` and `OrderBook.asks(depth=...)` accessors limit their returned results without removing stored levels. Other `L2_MBP` subscription depths are rejected. Unsubscribe before changing an instrument's subscription depth. ### Spot L2 subscriptions Spot partial-depth subscriptions deliver self-contained top-N snapshots. The supported depths depend on the market data mode: - **JSON**: Explicit depths 5, 10, or 20 use the `@depth` partial-depth stream. Other explicit depths, including 50, 100, 500, and 1000, are rejected before subscription with an error listing the valid depths. - **SBE**: Partial books require depth 20. Other partial depths are rejected before subscription; use JSON market data for depth 5 or 10. Omit depth to use the diff-depth stream in either mode, seeded by a 5000-level REST snapshot. Unsubscribe before changing an instrument's subscription depth; a new partial-depth subscription does not remove the previous stream. See [Spot market data mode](#spot-market-data-mode) for transport configuration. ### L1 top-of-book subscriptions `L1_MBP` subscriptions require depth 1 and use the Spot `bestBidAsk` or `bookTicker` stream and the Futures `bookTicker` stream. Each update emits the normal `QuoteTick` and a two-sided `OrderBookDeltas` batch with `F_MBP` flags so a managed L1 book receives the same top-of-book state. Quote and L1 subscriptions share the venue stream through reference counting. The client rejects concurrent L1 and L2 subscriptions for the same instrument. ### Snapshot requests Explicit order-book snapshot requests are supported separately from subscription synchronization: - **Spot**: Depths in [1, 5000]. - **Futures**: Depths 5, 10, 20, 50, 100, 500, or 1000. ### Snapshot synchronization Futures diff-depth subscriptions and Spot `BookDeltas` subscriptions without an explicit depth rebuild the order book on the initial subscription and on every data WebSocket reconnect. The rebuild runs in this order: 1. Buffering of incoming deltas starts. 1. The snapshot is requested and awaited. 1. The snapshot response is parsed to `OrderBookDeltas`. 1. The snapshot deltas are sent to the `DataEngine`. 1. Buffered deltas are iterated, dropping those whose sequence number is not greater than the last delta in the snapshot. 1. Buffering stops. 1. The remaining deltas are sent to the `DataEngine`. ## Quote timestamps The `ts_event` field on `QuoteTick` differs between transports. Spot SBE uses the microsecond event timestamp. Spot public JSON `bookTicker` messages can omit an event timestamp, in which case the adapter uses `ts_init`. Futures uses the transaction time. ## Bars and historical market data Spot supports one-second klines for subscriptions and historical requests. Real-time Spot kline subscriptions require `spot_market_data_mode=Json` because Binance does not publish kline or ticker streams over Spot SBE. Binance Futures rejects second-level klines because the Futures API does not offer them. Closed venue klines emit a core `Bar` and a `BinanceBar` custom-data event. `BinanceBar` retains quote volume, trade count, taker-buy base volume, and taker-buy quote volume. Historical core bar requests return `Bar`; request `BinanceBar` custom data with `bar_type` metadata to retain the extended fields in historical responses. Real-time trade subscriptions use the `@aggTrade` stream on Futures, because Binance only publishes aggregated trades on the Futures WebSocket, and the individual `@trade` stream on Spot. Historical trade requests without bounds use the recent-trades endpoint. A request with time bounds uses aggregate trades and accepts at most 1000 records, so the source follows the request rather than a config option. Spot passes the supplied bounds to `/api/v3/aggTrades`. Futures accepts either bound within the last 24 hours; when both are supplied, the range must be shorter than one hour. Historical core bar requests accept externally aggregated time bars and use the corresponding venue kline endpoint. Internally aggregated bars are built by the `DataEngine` from raw trade, quote, or source-bar responses through the `bar_types` request parameter; the Binance data client does not aggregate them. ## Binance specific data Bars, mark prices, index prices, and funding rates are subscribed to in the normal way. The custom data types below expose additional venue-specific fields that the core data types do not carry. Binance Futures mark-price payloads preserve the venue `P` estimated settlement price in `BinanceFuturesMarkPriceUpdate`. Nautilus also emits standard mark-price, index-price, and funding-rate updates from the same stream. The optional USD-M `ap` moving-average field is parsed at the transport boundary but is not exposed as domain or custom data. ### `BinanceSpotTicker` Spot 24-hour ticker custom data requires public JSON market-data mode and an `instrument_id` metadata value: ```python from nautilus_trader.adapters.binance import BinanceSpotTicker from nautilus_trader.model import ClientId from nautilus_trader.model import DataType self.subscribe_data( data_type=DataType( BinanceSpotTicker.__name__, metadata={"instrument_id": "BTCUSDT.BINANCE"}, ), client_id=ClientId.from_str("BINANCE"), ) ``` The adapter subscribes to the instrument `@ticker` stream. SBE mode rejects this subscription because Binance Spot SBE does not provide the stream. ### `BinanceFuturesTicker` Subscribe to 24-hour ticker statistics for a specific Futures instrument: ```python from nautilus_trader.adapters.binance import BinanceFuturesTicker from nautilus_trader.model import ClientId from nautilus_trader.model import DataType client_id = ClientId.from_str("BINANCE") self.subscribe_data( data_type=DataType( BinanceFuturesTicker.__name__, metadata={"instrument_id": "BTCUSDT-PERP.BINANCE"}, ), client_id=client_id, ) ``` The adapter subscribes to the instrument `@ticker` stream and emits `BinanceFuturesTicker` custom data with `metadata={"instrument_id": ""}`. Ticker custom data requires `instrument_id`; all-market ticker subscriptions are not supported. ### `BinanceFuturesMarkPriceUpdate` Subscribe to `BinanceFuturesMarkPriceUpdate` (including funding rate info) from your actor or strategy: ```python from nautilus_trader.adapters.binance import BinanceFuturesMarkPriceUpdate from nautilus_trader.model import DataType from nautilus_trader.model import ClientId # In your `on_start` method self.subscribe_data( data_type=DataType( BinanceFuturesMarkPriceUpdate.__name__, metadata={"instrument_id": self.instrument.id} ), client_id=ClientId("BINANCE"), ) ``` Received `BinanceFuturesMarkPriceUpdate` objects are passed to your `on_data` method. Check the type, as this method handles all custom/generic data. ```python def on_data(self, data): # First check the type of data if isinstance(data, BinanceFuturesMarkPriceUpdate): # Do something with the data ``` ### `BinanceFuturesLiquidation` Subscribe to liquidation updates for either: - a specific instrument (`@forceOrder`), or - all symbols (`!forceOrder@arr`) by omitting `instrument_id`. ```python from nautilus_trader.adapters.binance import BinanceFuturesLiquidation from nautilus_trader.model import ClientId from nautilus_trader.model import DataType client_id = ClientId.from_str("BINANCE") # Instrument-specific self.subscribe_data( data_type=DataType( BinanceFuturesLiquidation.__name__, metadata={"instrument_id": "BTCUSDT-PERP.BINANCE"}, ), client_id=client_id, ) # All-market (no instrument_id metadata) self.subscribe_data( data_type=DataType(BinanceFuturesLiquidation.__name__), client_id=client_id, ) ``` For instrument-specific subscriptions, `CustomData.data_type` includes `metadata={"instrument_id": ""}`. For all-market subscriptions, the data type has no metadata. When both modes are subscribed concurrently, all-market takes precedence. The adapter suspends per-symbol liquidation streams while all-market is active, and restores active per-symbol streams after all-market is unsubscribed. ### Futures open interest Open interest is request-only; the Futures data client has no open interest subscription. Both types require `instrument_id` metadata, and `BinanceFuturesOpenInterestHist` also requires a Binance `period` string such as `"5m"`: ```python from nautilus_trader.adapters.binance import BinanceFuturesOpenInterest from nautilus_trader.adapters.binance import BinanceFuturesOpenInterestHist from nautilus_trader.model import ClientId from nautilus_trader.model import DataType client_id = ClientId.from_str("BINANCE") # Current open interest snapshot self.request_data( data_type=DataType( BinanceFuturesOpenInterest.__name__, metadata={"instrument_id": "BTCUSDT-PERP.BINANCE"}, ), client_id=client_id, ) # Historical open interest series self.request_data( data_type=DataType( BinanceFuturesOpenInterestHist.__name__, metadata={"instrument_id": "BTCUSDT-PERP.BINANCE", "period": "5m"}, ), client_id=client_id, ) ``` `BinanceFuturesOpenInterestHist` returns a batch of points, each carrying the summed open interest and its notional value for one bucket. COIN-M history is keyed by pair and contract type, which the adapter derives from the symbol for perpetuals and from the cached instrument definition for delivery contracts. ## Funding rates The adapter emits `FundingRateUpdate` as a first-class data type through `subscribe_funding_rates`. The data comes from the [Mark Price Stream](https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Mark-Price-Stream) WebSocket endpoint, which provides the current funding rate and next funding time alongside mark and index prices. All three subscriptions (`subscribe_mark_prices`, `subscribe_index_prices`, `subscribe_funding_rates`) share a single `@markPrice@1s` stream with ref-counted subscription management. Historical funding rates are available through `request_funding_rates`, which queries the [Get Funding Rate History](https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Get-Funding-Rate-History) REST endpoint (`GET /fapi/v1/fundingRate` for USD-M, `GET /dapi/v1/fundingRate` for COIN-M). Each history row maps to a `FundingRateUpdate` with `ts_event` set to the funding time. The `next_funding_ns` field is `None` for historical rows because the endpoint does not provide it. The adapter also exposes the venue payload through `BinanceFuturesMarkPriceUpdate` custom data subscriptions (see [Binance specific data](#binance-specific-data)). The `interval` field on `FundingRateUpdate` is `None` for Binance because the Mark Price Stream and the funding rate history endpoint do not include a funding interval field. Binance exposes `fundingIntervalHours` through the [Get Funding Rate Info](https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Get-Funding-Rate-Info) REST endpoint, but the adapter does not consume it. ## Instrument status polling The data clients periodically poll Binance `exchangeInfo` to detect changes in instrument trading status. When a symbol transitions between states (e.g. Trading to Halt, or Trading to Delivering for a futures contract approaching expiry), the adapter emits an `InstrumentStatus` event. The polling interval defaults to 3,600 seconds (60 minutes) and is configurable via `instrument_status_poll_secs` in the data client config. Set to `0` to disable polling entirely. On initial connect, the adapter seeds its status cache from the exchange info response without emitting events. Only subsequent polls that detect a status change emit `InstrumentStatus` events. If a symbol disappears from exchange info (e.g. after delisting or contract expiry), the adapter emits `NotAvailableForTrading`. Status polling does not reload instrument definitions. The separate `instrument_refresh_interval_secs` task performs a complete filtered catalog load, atomically replaces the data-client and WebSocket lookup maps, sends the refreshed instruments to the data engine, and updates the status snapshot. It also refreshes the execution client precision cache. The default full refresh interval is 3,600 seconds; set it to `0` to disable it. Disconnect cancels the task, and reconnect starts one replacement task with a new cancellation token. ### Status mapping #### Spot | Binance status | MarketStatusAction | | ---------------- | ---------------------- | | Trading | Trading | | EndOfDay | Close | | Halt | Halt | | Break | Pause | | CancelOnly | Halt | | NonRepresentable | NotAvailableForTrading | Binance US polls the public JSON exchange info instead, which maps `TRADING` to `Trading`, `BREAK` to `Pause`, and every other value to `NotAvailableForTrading`. #### Futures (USD-M) | Binance status | MarketStatusAction | | ----------------- | ------------------ | | Trading | Trading | | PendingTrading | PreOpen | | PreTrading | PreOpen | | PostTrading | PostClose | | EndOfDay | Close | | Halt | Halt | | AuctionMatch | Cross | | Break | Pause | | PreDelivering | PreClose | | Delivering | Close | | Delivered | Close | | PreSettle | PreClose | | Settling | Close | | Close | Close | | TradingHalt | Halt | | TradingCancelOnly | Halt | #### Futures (COIN-M) | Binance status | MarketStatusAction | | ----------------- | ---------------------- | | Trading | Trading | | PendingTrading | PreOpen | | PreDelivering | PreClose | | Delivering | Close | | Delivered | Close | | PreSettle | PreClose | | Settling | Close | | Close | Close | | PreDelisting | PreClose | | Delisting | Suspend | | Down | NotAvailableForTrading | | TradingHalt | Halt | | TradingCancelOnly | Halt | Unknown or undocumented Futures status values map to `NotAvailableForTrading`. :::note Only instruments that are in a tradable state at connect time are tracked. Symbols that start in a non-trading state (e.g. halted at connect) do not appear in the instruments cache, so status transitions for them are not monitored. ::: ## Rate limiting Binance uses an interval-based rate limiting system where request weight is tracked per fixed time window (every minute, resetting at :00 seconds). Each API endpoint has an assigned weight cost, and total weight usage is tracked per IP address. ### Venue weight limits Binance's own per-IP weight allowance, which the endpoint costs below draw from: | Product | Weight limit | Interval | | ----------- | ------------ | -------- | | Spot/Margin | 6,000 | 1 minute | | Futures | 2,400 | 1 minute | ### Endpoint weight costs Binance charges these weights per request: | Endpoint | Weight | Notes | | ------------------------- | ------ | -------------------------------------- | | `/api/v3/order` | 1 | Spot order placement. | | `/api/v3/allOrders` | 20 | Spot historical orders (expensive). | | `/api/v3/klines` | 2+ | Scales with `limit` parameter. | | `/fapi/v1/order` | 1 | Futures order placement. | | `/fapi/v1/algoOrder` | 0 | Uses order-count limits. | | `/fapi/v1/allOrders` | 20 | Futures historical orders (expensive). | | `/fapi/v1/commissionRate` | 20 | Futures commission rate query. | | `/fapi/v1/klines` | 5+ | Scales with `limit` parameter. | USD-M Futures `POST /fapi/v1/algoOrder` consumes `1` from both `X-MBX-ORDER-COUNT-10S` and `X-MBX-ORDER-COUNT-1M`. Binance charges no IP request weight for this endpoint; the adapter still queues it through the global bucket as part of its local pacing model. ### WebSocket API limits The WebSocket API (used for order entry and user data streams) shares the same weight quota as the REST API: | Limit Type | Value | Notes | | ---------------- | ------ | ------------------------------------- | | Request weight | Shared | Counts against REST API weight quota. | | Handshake | 5 | Weight cost per connection attempt. | | Ping/pong frames | 5/sec | Maximum ping/pong rate. | ### Adapter pacing The adapter runs its own token bucket limiters ahead of the venue's accounting. Every HTTP request draws one token from a per-product request bucket, and every order operation draws an additional token from the order-count buckets: | Product | Requests | Order operations | | -------------------------- | ------------ | ---------------------------- | | Spot | 1,200/minute | 10/second, 100,000/day | | Futures (USD-M and COIN-M) | 2,400/minute | 300/10 seconds, 1,200/minute | The order-count buckets cover every order operation the product supports, not just placement: submit, OCO submit, batch submit, algo submit, modify (Spot modifies through cancel-replace), batch modify, cancel, cancel-all, batch cancel, and algo cancel. Cancel-heavy and modify-heavy strategies are throttled by these buckets as well. Non-order authenticated requests, such as leverage and margin-type changes and listen-key keepalives, draw from the request bucket alone. The request bucket counts calls rather than weight, so it does not mirror the venue's weight accounting. A run of high-weight or dynamic-weight endpoints (`/api/v3/allOrders` at weight 20, or `/klines` scaling with `limit`) spends venue weight faster than the local bucket accounts for. Large history requests may need manual pacing. Monitor the `X-MBX-USED-WEIGHT-*` response headers to track actual venue usage. :::warning Binance returns HTTP 429 when you exceed the allowed weight. Repeated violations trigger temporary IP bans (escalating from 2 minutes to 3 days for repeat offenders). ::: :::info For the latest rate limits, query `/api/v3/exchangeInfo` (Spot) or `/fapi/v1/exchangeInfo` (Futures), or see: - [Spot API Limits](https://developers.binance.com/docs/binance-spot-api-docs/rest-api/limits) - [Futures API Limits](https://developers.binance.com/docs/derivatives/usds-margined-futures/general-info) ::: ## Configuration ### Data client | Option | Default | Description | | ---------------------------------- | --------- | ------------------------------------------------------------------------------ | | `product_type` | `Spot` | One of `Spot`, `UsdM`, or `CoinM`. | | `environment` | `Live` | One of `Live`, `Testnet`, or `Demo`. | | `base_url_http` | `None` | Optional HTTP endpoint override. | | `base_url_ws` | `None` | Optional market WebSocket endpoint override. | | `api_key` / `api_secret` | `None` | Required for Spot SBE; optional for public JSON and Futures data. | | `spot_market_data_mode` | `Sbe` | `Json` keeps the credential-free Global Spot path. Binance US requires `Json`. | | `instrument_provider` | default | Loading, filters, parser-warning, and commission policy. | | `instrument_refresh_interval_secs` | `3,600` | Full catalog refresh interval; `0` disables it. | | `instrument_status_poll_secs` | `3,600` | Status-only exchange-info poll interval; `0` disables it. | | `proxy_url` | `None` | Proxy applied to HTTP and every market WebSocket connection. | | `recv_window_ms` | `5,000` | Signed HTTP receive window, inclusive range `1..=60000`. | | `max_retries` | `3` | Maximum retries for HTTP GET requests. | | `retry_delay_initial_ms` | `1,000` | Initial HTTP read retry delay in milliseconds. | | `retry_delay_max_ms` | `10,000` | Maximum exponential delay; a venue minimum can exceed it. | | `us` | `False` | Route a live Spot JSON client to Binance US. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | ### Execution client | Option | Default | Description | | ---------------------------------- | --------- | ----------------------------------------------------------------------- | | `account_id` | Required | Nautilus account identity. | | `product_type` | `Spot` | One of `Spot`, `UsdM`, or `CoinM`. | | `environment` | `Live` | One of `Live`, `Testnet`, or `Demo`. | | `base_url_http` | `None` | Optional HTTP endpoint override. | | `base_url_ws` | `None` | Optional private stream override. | | `base_url_ws_trading` | `None` | Optional Global Spot or USD-M WebSocket trading override. | | `use_ws_trading` | `True` | Use Global WebSocket order entry where supported; Binance US uses HTTP. | | `ws_trading_setup_timeout_ms` | `10,000` | WebSocket trading authentication and setup timeout. | | `instrument_provider` | default | Loading, filters, parser-warning, and commission policy. | | `instrument_refresh_interval_secs` | `3,600` | Execution precision-cache refresh interval; `0` disables it. | | `proxy_url` | `None` | Proxy applied to HTTP, private streams, and WebSocket trading. | | `recv_window_ms` | `5,000` | Signed HTTP and WebSocket receive window, inclusive range `1..=60000`. | | `max_retries` | `3` | Maximum retries for HTTP GET requests. | | `retry_delay_initial_ms` | `1,000` | Initial HTTP read retry delay in milliseconds. | | `retry_delay_max_ms` | `10,000` | Maximum exponential delay; a venue minimum can exceed it. | | `us` | `False` | Route a live Spot execution client to Binance US. | | `api_key` / `api_secret` | `None` | Global uses Ed25519 WebSocket auth; Binance US uses HMAC HTTP signing. | | `use_gtd` | `True` | Use native USD-M GTD; see [GTD policy](#gtd-policy). | | `use_position_ids` | `True` | Expose Futures IDs on order, fill, and hedge REST reports. | | `oms_type` | `None` | `None` selects Futures netting; use `Hedging` for dual-side mode. | | `default_taker_fee` | `0.0004` | Fallback for exchange-generated Futures fills. | | `futures_leverages` | `None` | Initial leverage by Futures symbol. | | `futures_margin_types` | `None` | Initial margin type by Futures symbol. | | `treat_expired_as_canceled` | `False` | Map `EXPIRED` execution events to canceled events. | | `use_trade_lite` | `False` | Use the lower-latency USD-M trade-lite fill stream. | | `bnfcr_currency` | `USDT` | Currency used to resolve `BNFCR` balances and fees. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | ### Live node configuration Use `BinanceDataClientConfig` with `BinanceDataClientFactory` and `BinanceExecutionClientConfig` with `BinanceExecutionClientFactory`. The current Python examples show the complete `LiveNode.builder(...)` configuration for data and execution clients. ### Futures Credits Trading Mode (BNFCR) Binance Futures Credits Trading Mode is an EU regulatory mode in which the USD-M futures wallet, margin, PnL, and fees are denominated in `BNFCR`: an internal credit unit pegged 1:1 to USD that replaces stablecoin balances. Because `BNFCR` is not a tradable asset, the adapter maps it to the `bnfcr_currency` execution config option (default `USDT`) so account balances and commissions reconcile against the stablecoin the traded contracts settle in. Set `bnfcr_currency` to `USDC` when trading USDC-margined perpetuals. Any other unrecognized futures asset is registered as a generic crypto currency rather than failing. ### Spot market data mode `spot_market_data_mode` on `BinanceDataClientConfig` selects the Spot data transport. It affects Spot only; Futures is unchanged. | Mode | Credentials | Quotes | | ------ | ------------------ | ------------ | | `Sbe` | Ed25519 (required) | `bestBidAsk` | | `Json` | None (public) | `bookTicker` | `Sbe` (default) uses Binance Simple Binary Encoding streams and requires Ed25519 keys (see [Key types](#key-types)); the client refuses to connect without them. `Json` uses public streams with no credentials. Full Spot `BookDeltas` subscriptions use the `@depth` diff-depth stream on the selected transport, with REST snapshot synchronization. Explicit depth subscriptions use partial-book snapshots (see [Order books](#order-books)). :::note Exposed to Python as `BinanceSpotMarketDataMode` on `nautilus_trader.adapters.binance`. ::: ### Key types The adapter signs with **Ed25519** or **HMAC-SHA256**, auto-detecting the key type from your API secret format, so no configuration is needed. A secret that parses as a PKCS#8 Ed25519 private key signs with Ed25519; anything else is treated as an HMAC secret. **Ed25519 is strongly recommended.** Binance recommends Ed25519 for its superior performance and security. A future version of NautilusTrader will require Ed25519 exclusively. | Key Type | Data Clients | Execution Clients | Status | | -------- | ------------ | ----------------- | ------------------------------------------------ | | Ed25519 | ✓ | ✓ | **Recommended** | | HMAC | ✓ | ✓ | Deprecated, will be removed in a future version. | | RSA | - | - | Not supported; register an Ed25519 key instead. | :::tip Switch to Ed25519 keys now. Generate an Ed25519 keypair and register it with Binance. See [Generating Ed25519 keys](#generating-ed25519-keys) below. ::: :::note Ed25519 keys must be provided in unencrypted PEM format (base64-encoded ASN.1/DER). The implementation automatically extracts the 32-byte seed from the DER structure. Encrypted (password-protected) PEM keys are not supported. If your key is encrypted, decrypt it first: `openssl pkey -in encrypted.pem -out decrypted.pem` ::: #### Generating Ed25519 keys **Option 1: OpenSSL (recommended)** ```bash # Generate private key (PKCS#8 PEM format) openssl genpkey -algorithm ed25519 -out binance_ed25519_private.pem # Extract public key openssl pkey -in binance_ed25519_private.pem -pubout -out binance_ed25519_public.pem ``` **Option 2: Binance Key Generator** Download the [Binance Asymmetric Key Generator](https://github.com/binance/asymmetric-key-generator) from the releases page and run it to generate a keypair. **Registering with Binance** 1. Log in to Binance and go to **Profile** -> **API Management**. 2. Click **Create API** and select **Self-generated**. 3. Paste the contents of your public key file, including the `-----BEGIN PUBLIC KEY-----` header and footer. 4. Configure permissions (Enable Spot & Margin Trading, etc.). **Using with NautilusTrader** Set the private key as your API secret: ```bash export BINANCE_API_KEY="your-api-key-from-binance" export BINANCE_API_SECRET="$(cat binance_ed25519_private.pem)" ``` Or pass the PEM content directly in your configuration. :::warning Keep your private key secure. Never share it or commit it to version control. ::: ### API credentials Pass credentials directly to the configuration objects, or set the appropriate environment variables (see [Environments](#environments) for per-environment variables). :::tip Use Ed25519 keys for all clients. HMAC keys still work for both data and execution clients, but Ed25519 offers better performance and will become the only supported key type in a future version. See [Key types](#key-types). ::: :::warning The `BINANCE_ED25519_*` and `BINANCE_*_ED25519_*` environment variables have been removed for Spot; a client that finds one logs an error and treats the credential as missing. For Futures they are deprecated, still honored with a warning, and will be removed in a future version. Rename them to `BINANCE_API_KEY` / `BINANCE_API_SECRET` (Ed25519 keys are now auto-detected). ::: When the trading node starts, you receive confirmation of whether your credentials are valid and have trading permissions. ### Product type Configs select one supported product with the `product_type` field and `BinanceProductType` enum: - `SPOT` - `USD_M` (USDT, USDC, or BNFCR collateral) - `COIN_M` (cryptocurrency collateral) :::note Margin trading is not implemented. Other enum variants are rejected by the live clients and the standalone instrument loader. See [Product support](#product-support). ::: ### Base URL overrides Override the default base URLs for both HTTP REST and WebSocket APIs. This is useful for configuring API clusters or when Binance has provided specialized endpoints. ### Binance US Set `us=True` on the config for first-class Binance US Spot routing. Binance US is not a custom-URL alias: the switch selects `api.binance.us`, the public JSON stream, HMAC-signed HTTP execution, and the port 443 listen-key private stream with periodic keepalive. See the official Binance US [REST API](https://github.com/binance-us/binance-us-api-docs/blob/master/rest-api.md), [market streams](https://github.com/binance-us/binance-us-api-docs/blob/master/web-socket-streams.md), and [user data stream](https://github.com/binance-us/binance-us-api-docs/blob/master/web-socket-api.md) documentation for the venue contracts behind this routing. The supported combinations are deliberate: - Data: `product_type=Spot`, `environment=Live`, `spot_market_data_mode=Json`. - Execution: `product_type=Spot`, `environment=Live`; order entry uses HTTP and private events use the listen-key stream. - Futures, Testnet, Demo, and Spot SBE configurations with `us=True` fail validation. Binance US public JSON covers live market data, depth snapshots, recent and aggregate trade history, and kline history. It uses account-wide maker and taker rates. Global Binance keeps its existing credential-free Spot JSON behavior with `us=False` and `spot_market_data_mode=Json`. ### Environments Binance provides three trading environments, each with separate API credentials and endpoints. The `environment` config option selects which to use. | Environment | Config value | Description | | ----------- | ---------------------------- | --------------------------------------------------- | | **Live** | `BinanceEnvironment.LIVE` | Production trading with real funds (default). | | **Demo** | `BinanceEnvironment.DEMO` | Demo Trading with simulated Spot and Futures funds. | | **Testnet** | `BinanceEnvironment.TESTNET` | Legacy Spot and Futures test network. | #### Live (production) The default environment for live trading with real funds. Uses your main Binance account credentials. ```python config = BinanceExecutionClientConfig( account_id=AccountId.from_str("BINANCE-001"), api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", product_type=BinanceProductType.SPOT, # environment=BinanceEnvironment.LIVE (default) ) ``` | Variable | Description | | -------------------- | ---------------- | | `BINANCE_API_KEY` | Live API key. | | `BINANCE_API_SECRET` | Live API secret. | #### Demo trading Practice trading with simulated funds on production infrastructure. Demo accounts use the same Binance login as your live account but trade with virtual balances. **How to get demo credentials:** 1. Log in at [binance.com/en/demo-trading](https://www.binance.com/en/demo-trading). 2. Go to **API Management** and create a demo API key. 3. Demo keys work for Spot and Futures demo endpoints. | Endpoint | URL | | ----------- | -------------------------- | | Spot HTTP | `demo-api.binance.com` | | Spot WS | `demo-stream.binance.com` | | USD-M HTTP | `demo-fapi.binance.com` | | USD-M WS | `demo-fstream.binance.com` | | COIN-M HTTP | `demo-dapi.binance.com` | | COIN-M WS | `demo-dstream.binance.com` | ```python config = BinanceExecutionClientConfig( account_id=AccountId.from_str("BINANCE-001"), api_key="YOUR_DEMO_API_KEY", api_secret="YOUR_DEMO_API_SECRET", product_type=BinanceProductType.SPOT, environment=BinanceEnvironment.DEMO, ) ``` | Variable | Description | | ------------------------- | ---------------- | | `BINANCE_DEMO_API_KEY` | Demo API key. | | `BINANCE_DEMO_API_SECRET` | Demo API secret. | #### Testnet A legacy test network with its own user accounts, balances, and order books. Prefer `environment=BinanceEnvironment.DEMO` for new simulated trading setups. Spot testnet remains at `testnet.binance.vision`; futures testnet endpoints may route through the Demo Trading infrastructure. **How to get Spot testnet credentials:** 1. Go to [testnet.binance.vision](https://testnet.binance.vision/). 2. Log in with GitHub. 3. Generate an Ed25519 or HMAC API key (the adapter does not support RSA keys). **Futures testnet:** Existing configs with `BinanceEnvironment.TESTNET` continue to work, but new Futures testing should use `BinanceEnvironment.DEMO`. ```python config = BinanceExecutionClientConfig( account_id=AccountId.from_str("BINANCE-001"), api_key="YOUR_TESTNET_API_KEY", api_secret="YOUR_TESTNET_API_SECRET", product_type=BinanceProductType.SPOT, environment=BinanceEnvironment.TESTNET, ) ``` | Variable | Description | | ------------------------------------ | --------------------------- | | `BINANCE_TESTNET_API_KEY` | Spot testnet API key. | | `BINANCE_TESTNET_API_SECRET` | Spot testnet API secret. | | `BINANCE_FUTURES_TESTNET_API_KEY` | Futures testnet API key. | | `BINANCE_FUTURES_TESTNET_API_SECRET` | Futures testnet API secret. | :::note Testnet credentials are completely separate from your live account. Market data and liquidity differ from production. ::: ### Commission rate queries The instrument provider controls both selection and fee policy: ```python from nautilus_trader.adapters.binance import BinanceInstrumentProviderConfig instrument_provider = BinanceInstrumentProviderConfig( load_all=False, load_ids=["BTCUSDT.BINANCE", "ETHUSDT.BINANCE"], filters={"quotes": ["USDT"], "bases": ["BTC", "ETH"]}, log_warnings=True, query_commission_rates=True, ) ``` `load_all=False` selects only `load_ids`; venue filters then apply as an intersection. Supported filters are `symbols`, `bases`, and `quotes`, plus `contract_types` for Futures. Values are a string or non-empty list of strings, and matching is case-insensitive. The adapter rejects `filter_callable`; use the supported declarative filters. Every parsed instrument receives maker and taker fees: - Spot uses the account-wide rate when credentials are present, otherwise 0.1% maker and taker. - Futures uses the account VIP tier when credentials are present, otherwise VIP 0. - `query_commission_rates=True` opts Global Spot and Futures into rate-limited exact per-symbol queries. A failed or invalid query falls back to the account or tier rate for that symbol. - Binance US uses its account-wide commission rates because it does not expose the Global `account/commission` endpoint. The exact-query behavior follows the Global Spot [commission FAQ](https://github.com/binance/binance-spot-api-docs/blob/master/faqs/commission_faq.md) and the USD-M [user commission rate](https://developers.binance.com/docs/derivatives/usds-margined-futures/account/rest-api/User-Commission-Rate) endpoint. Exact queries require credentials. Because they issue one private request per selected symbol, combine `load_ids` or filters with this option on large catalogs. ### Parser warnings Some Binance instruments cannot be parsed into Nautilus objects if they contain field values beyond what the platform handles. These instruments are skipped with a warning. To suppress these warnings: ```python from nautilus_trader.adapters.binance import BinanceInstrumentProviderConfig instrument_provider = BinanceInstrumentProviderConfig( load_all=True, log_warnings=False, ) ``` ### Futures hedge mode Binance Futures Hedge mode allows holding both long and short positions on the same instrument simultaneously. When `use_position_ids` is enabled (default), Futures order and fill reports include a `venue_position_id` derived from the instrument and Binance position side. Hedge-mode REST position reports use the same IDs, such as `ETHUSDT-PERP.BINANCE-LONG`. This identity is preserved through REST history, user stream updates, stream recovery, exchange-generated fills, and tracked `TRADE_LITE` fills. One-way `BOTH` positions, orders, and fills remain unkeyed and use netting reconciliation. Set `use_position_ids` to false only for virtual positions with `OmsType.HEDGING`, where the engine manages position identity. With `use_position_ids=True`, the adapter rejects a submitted custom position ID that differs from the canonical Binance hedge-leg ID before sending the order. To use hedge mode, configure it on Binance, set `oms_type=OmsType.HEDGING` on `BinanceExecutionClientConfig`, and keep `use_position_ids=True` to track both venue position sides: ```python from nautilus_trader.adapters.binance import BinanceExecutionClientConfig from nautilus_trader.adapters.binance import BinanceProductType from nautilus_trader.model import AccountId from nautilus_trader.model import OmsType config = BinanceExecutionClientConfig( account_id=AccountId.from_str("BINANCE-001"), product_type=BinanceProductType.USD_M, oms_type=OmsType.HEDGING, use_position_ids=True, ) ``` This configuration is required for startup reconciliation to retain the `LONG` and `SHORT` legs separately. If the cache contains an open Binance hedge position under a different locally generated ID, the adapter rejects that position row and reports both the cached and expected IDs. Reconcile the cached state before retrying startup. The adapter does not alias the old ID or create a duplicate venue position. ### COIN-M / USD-M architecture Binance COIN-M Futures (CM / DAPI) and USD-M Futures (UM / FAPI) share a unified architecture. This section covers the implications for the adapter. See the [Important CM-UM Integration Notice](https://developers.binance.com/docs/derivatives/coin-margined-futures/Important-CM-UM-Integration-Notice) for the full details. #### WebSocket streams Market-data stream payloads include `st` (symbol type: `1` = UM, `2` = CM) on `@aggTrade`, `@ticker`, `@bookTicker`, `@depth`, `@miniTicker`, and all `!*@arr` streams. UM-side single-symbol streams also include `ps` (pair symbol) on `@bookTicker`, `@depth`, `@miniTicker`, and `@rpiDepth`. The adapter decodes JSON with `serde`, which ignores unknown fields by default, so these fields are silently dropped. All-market array streams (`!ticker@arr`, `!miniTicker@arr`, `!bookTicker`, `!forceOrder@arr`, `!contractInfo`) deliver merged UM + CM content on both `fstream` and `dstream`. #### REST and WebSocket API - Order placement and modification acknowledgement responses do not include `avgPrice` / `cumQuote` / `cumBase`. The adapter sources fills from the user data stream. Query endpoints (`GET /{f,d}api/v1/order`, `userTrades`) still return these fields. - `PUT /dapi/v1/order` (COIN-M modify) requires both `price` and `quantity`. The adapter always sends both fields, falling back to the cached order's values for whichever the modify command omits. - COIN-M conditional orders (STOP, TAKE_PROFIT, etc.) use the `/dapi/v1/algoOrder` endpoint. The adapter routes all futures conditional orders through the algo order API. - `GET /dapi/v1/openOrders` with an invalid symbol returns error `-1121`. #### Rate-limit pools UM and CM share Binance rate-limit pools: 2400 weight/min per IP, plus 1200 orders/min and 300 orders/10s per account. Rust futures HTTP clients in the same process share request-weight state across UM and CM for the same environment or custom endpoint scope and configured egress path. They share order-count state across UM and CM when authenticated with the same API key, regardless of egress path. Live, testnet, demo, and unrelated custom endpoint scopes remain isolated. Different configured egress paths have separate request-weight state, while different API keys have separate order-count state. Separate processes and multiple API keys for one Binance account still require external coordination. #### dualSidePosition UM and CM share the same `dualSidePosition` setting. Changing it on either side affects both. Ensure both UM and CM have no open orders or positions before flipping the setting. ## Contributing :::info To contribute to the Binance adapter, see the [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # BitMEX Source: https://nautilustrader.io/docs/latest/integrations/bitmex/ :::warning BitMEX will close on 23 September 2026 at 04:00 UTC. This adapter is deprecated and receives only critical fixes during the wind-down. NautilusTrader plans to remove the live integration in the first release after the closure. See the [BitMEX decommissioning RFC](https://github.com/nautechsystems/nautilus_trader/issues/4552). ::: Founded in 2014, BitMEX (Bitcoin Mercantile Exchange) is a cryptocurrency derivatives trading platform offering spot, perpetual contracts, traditional futures, prediction markets, and other advanced trading products. This integration supports live market data ingest and order execution with BitMEX. ## Overview This adapter is implemented in Rust, with optional Python bindings for ease of use in Python-based workflows. It does not require external BitMEX client libraries; the core components are compiled as a static library and linked automatically during the build. ## Examples - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/bitmex/examples/) ## Components This guide assumes a trader is setting up for both live market data feeds, and trade execution. The BitMEX adapter includes multiple components, which can be used together or separately depending on the use case. - `BitmexHttpClient`: Low-level HTTP API connectivity. - `BitmexWebSocketClient`: Low-level WebSocket API connectivity for Rust callers. - `BitmexInstrumentProvider`: Instrument parsing and loading functionality. - `BitmexDataClient`: A market data feed manager. - `BitmexExecutionClient`: An account management and trade execution gateway. - `BitmexDataClientFactory`: Factory for BitMEX data clients (used by the trading node builder). - `BitmexExecutionClientFactory`: Factory for BitMEX execution clients (used by the trading node builder). :::note Most users will define a configuration for a live trading node (as below), and won't need to necessarily work with these lower level components directly. ::: ## BitMEX documentation BitMEX provides extensive documentation for users: - [BitMEX API Explorer](https://www.bitmex.com/app/restAPI) - Interactive API documentation. - [BitMEX API Documentation](https://www.bitmex.com/app/apiOverview) - Complete API reference. - [BitMEX Exchange Rules](https://www.bitmex.com/exchange-rules) - Official exchange rules and regulations. - [Contract Guides](https://www.bitmex.com/app/contract) - Detailed contract specifications. - [Spot Trading Guide](https://www.bitmex.com/app/spotGuide) - Spot trading overview. - [Perpetual Contracts Guide](https://www.bitmex.com/app/perpetualContractsGuide) - Perpetual swaps explained. - [Futures Contracts Guide](https://www.bitmex.com/app/futuresGuide) - Traditional futures information. It's recommended you refer to the BitMEX documentation in conjunction with this NautilusTrader integration guide. ## Product support | Product Type | Data Feed | Trading | Notes | | ------------------ | --------- | ------- | --------------------------------------------------- | | Spot | ✓ | ✓ | Limited pairs, unified wallet with derivatives. | | Perpetual Swaps | ✓ | ✓ | Inverse and linear contracts available. | | Stock Perpetuals | - | - | *Not yet supported*. Currently on testnet only. | | Futures | ✓ | ✓ | Traditional fixed expiration contracts. | | Quanto Futures | ✓ | ✓ | Settled in different currency than underlying. | | Prediction Markets | ✓ | ✓ | Event-based contracts, 0-100 pricing, USDT settled. | | Options | - | - | *Not provided by BitMEX*. | :::note BitMEX has discontinued their options products to focus on their core derivatives and spot offerings. ::: ### Spot trading - Direct token/coin trading with immediate settlement. - Major pairs including XBT/USDT, ETH/USDT, ETH/XBT. - Additional altcoin pairs (LINK, SOL, UNI, APE, AXS, BMEX against USDT). ### Derivatives - **Perpetual contracts**: Inverse (e.g., XBTUSD) and linear (e.g., ETHUSDT). - **Traditional futures**: Fixed expiration date contracts. - **Quanto futures**: Contracts settled in a different currency than the underlying. - **Prediction markets**: Event-based derivatives (e.g., P_FTXZ26, P_SBFJAILZ26) allowing traders to speculate on outcomes across crypto, finance, and other events. No leverage, priced 0-100, settled in USDT. - **Stock perpetuals**: Equity-based perpetuals (e.g., SPYUSDT, CRCLUSDT). *Currently on testnet only; not yet supported by this adapter.* ### Instrument type codes (CFI) BitMEX uses CFI (Classification of Financial Instruments) codes following the ISO 10962 standard. The adapter recognizes the following instrument type codes: | Code | Type | Status | Description | | -------- | ------------------ | ----------- | ---------------------------------------------- | | `FFWCSX` | Perpetual Contract | Supported | Crypto-based perpetual swaps (e.g., XBTUSD). | | `FFWCSF` | Perpetual FX | Supported | FX-based perpetual contracts. | | `FFCCSX` | Futures | Supported | Calendar futures with fixed expiration. | | `FFICSX` | Prediction Market | Supported | Event-based prediction contracts. | | `IFXXXP` | Spot | Supported | Spot trading pairs. | | `FFSCSX` | Stock Perpetual | Unsupported | Stock/equity-based perpetuals. Testnet only. | | `SRMCSX` | Swap Rate | Unsupported | Yield-based swap products (historical). | | `MR****` | Index | Reference | BitMEX indices (non-tradeable, for price ref). | See [BitMEX Typ Values](https://support.bitmex.com/hc/en-gb/articles/6299296145565-What-are-the-Typ-Values-for-Instrument-endpoint) for more details. ## Symbology BitMEX uses a specific naming convention for its trading symbols. Understanding this convention is crucial for correctly identifying and trading instruments. ### Symbol format BitMEX symbols typically follow these patterns: - **Spot pairs**: Base currency + Quote currency (e.g., `XBT/USDT`, `ETH/USDT`). - **Perpetual contracts**: Base currency + Quote currency (e.g., `XBTUSD`, `ETHUSD`). - **Futures contracts**: Base currency + Expiry code (e.g., `XBTM24`, `ETHH25`). - **Quanto contracts**: Special naming for non-USD settled contracts. - **Prediction markets**: `P_` prefix + Event identifier + Expiry code (e.g., `P_POWELLK26`, `P_FTXZ26`). :::info BitMEX uses `XBT` as the symbol for Bitcoin instead of `BTC`. This follows the ISO 4217 currency code standard where "X" denotes non-national currencies. XBT and BTC refer to the same asset - Bitcoin. ::: ### Expiry codes Futures contracts use standard futures month codes: - `F` = January - `G` = February - `H` = March - `J` = April - `K` = May - `M` = June - `N` = July - `Q` = August - `U` = September - `V` = October - `X` = November - `Z` = December Followed by the year (e.g., `24` for 2024, `25` for 2025). ### NautilusTrader instrument IDs Within NautilusTrader, BitMEX instruments are identified using the native BitMEX symbol directly, combined with the venue identifier: ```python from nautilus_trader.model import InstrumentId # Spot pairs (note: no slash in the symbol) spot_id = InstrumentId.from_str("XBTUSDT.BITMEX") # XBT/USDT spot eth_spot_id = InstrumentId.from_str("ETHUSDT.BITMEX") # ETH/USDT spot # Perpetual contracts perp_id = InstrumentId.from_str("XBTUSD.BITMEX") # Bitcoin perpetual (inverse) linear_perp_id = InstrumentId.from_str("ETHUSDT.BITMEX") # Ethereum perpetual (linear) # Futures contract (June 2024) futures_id = InstrumentId.from_str("XBTM24.BITMEX") # Bitcoin futures expiring June 2024 # Prediction market contracts prediction_id = InstrumentId.from_str( "P_XBTETFV23.BITMEX" ) # Bitcoin ETF SEC approval prediction expiring October 2023 ``` :::note BitMEX spot symbols in NautilusTrader don't include the slash (/) that appears in the BitMEX UI. Use `XBTUSDT` instead of `XBT/USDT`. ::: ### Quantity scaling BitMEX reports spot and derivative quantities in *contract* units. The actual asset size per contract is exchange-specific and published on the instrument definition: - `lotSize` - minimum number of contracts you can trade. - `underlyingToPositionMultiplier` - number of contracts per unit of the underlying asset. For example, the SOL/USDT spot instrument (`SOLUSDT`) exposes `lotSize = 1000` and `underlyingToPositionMultiplier = 10000`, meaning one contract represents `1 / 10000 = 0.0001` SOL, and the minimum order (`lotSize * contract_size`) is `0.1` SOL. The adapter now derives the contract size directly from these fields and scales both inbound market data and outbound orders accordingly, so quantities in Nautilus are always expressed in base units (SOL, ETH, etc.). See the BitMEX API documentation for details on these fields: . ## Orders capability The BitMEX integration supports the following order types and execution features. ### Order types | Order Type | Supported | Notes | | ---------------------- | --------- | --------------------------------------------------------------------------- | | `MARKET` | ✓ | Executed immediately at current market price. Quote quantity not supported. | | `LIMIT` | ✓ | Executed only at specified price or better. | | `STOP_MARKET` | ✓ | Supported (set `trigger_price`). | | `STOP_LIMIT` | ✓ | Supported (set `price` and `trigger_price`). | | `MARKET_IF_TOUCHED` | ✓ | Supported (set `trigger_price`). | | `LIMIT_IF_TOUCHED` | ✓ | Supported (set `price` and `trigger_price`). | | `TRAILING_STOP_MARKET` | ✓ | Supported (set `trailing_offset`). Price offset type only. | | `TRAILING_STOP_LIMIT` | ✓ | Supported (set `price` and `trailing_offset`). Price offset type only. | ### Execution instructions | Instruction | Supported | Notes | | ------------- | --------- | --------------------------------------------------------------------------------- | | `post_only` | ✓ | Supported via `ParticipateDoNotInitiate` execution instruction on `LIMIT` orders. | | `reduce_only` | ✓ | Supported via `ReduceOnly` execution instruction. | :::note Post-only orders that would cross the spread are canceled by BitMEX rather than rejected. The integration surfaces these as rejections with `due_post_only=True` so strategies can handle them consistently. ::: ### Trigger types BitMEX supports multiple reference prices to evaluate stop/conditional order triggers for: - `STOP_MARKET` - `STOP_LIMIT` - `MARKET_IF_TOUCHED` - `LIMIT_IF_TOUCHED` Choose the trigger type that matches your strategy and/or risk preferences. | Reference price | Nautilus `TriggerType` | BitMEX value | Notes | | --------------- | ---------------------- | ------------ | ------------------------------------------------------------------------------- | | Last trade | `LAST_PRICE` | `LastPrice` | BitMEX default; triggers on the last traded price. | | Mark price | `MARK_PRICE` | `MarkPrice` | Recommended for many stop-loss use cases to reduce stop-outs from price spikes. | | Index price | `INDEX_PRICE` | `IndexPrice` | Tracks the external index; useful for some contracts. | - If no `trigger_type` is provided, BitMEX uses its venue default (`LastPrice`). - These trigger references are exchange-evaluated; the order remains resting at the venue until triggered. **Example**: ```python from nautilus_trader.model import TriggerType order = self.order_factory.stop_market( instrument_id=instrument_id, order_side=order_side, quantity=qty, trigger_price=trigger, trigger_type=TriggerType.MARK_PRICE, # Use BitMEX Mark Price as reference ) ``` The [Rust execution tester](https://github.com/nautechsystems/nautilus_trader/blob/master/crates/adapters/bitmex/examples/node_exec_tester.rs) also configures `ExecTester` with `TriggerType::MarkPrice`. ### Trailing stops BitMEX supports trailing stop orders that automatically adjust the stop price as the market moves favorably. The adapter maps `TRAILING_STOP_MARKET` and `TRAILING_STOP_LIMIT` orders to BitMEX's pegged orders with the `TrailingStopPeg` price type; the limit variant additionally carries the limit `price`. **Limitations:** - Only `PRICE` trailing offset type is supported (absolute price offset, not basis points or ticks). - The offset sign is handled automatically: sell stops use negative offset, buy stops use positive. - Trigger type can be combined with trailing stops for additional control. **Example**: ```python from nautilus_trader.model import TrailingOffsetType order = self.order_factory.trailing_stop_market( instrument_id=instrument_id, order_side=OrderSide.SELL, quantity=qty, trailing_offset=Decimal("100"), # $100 trailing offset trailing_offset_type=TrailingOffsetType.PRICE, trigger_type=TriggerType.LAST_PRICE, # Optional ) ``` :::note BitMEX updates trailing stop prices periodically as the market moves. The stop price freezes when the market moves toward the trigger level. See the [BitMEX API documentation](https://www.bitmex.com/app/perpetualContractsGuide) for current update cadence details. ::: ### Pegged orders BitMEX supports pegged orders (BBO) that automatically track a reference price. The adapter supports pegged orders via the `params` dict on `submit_order`, which overrides the order type to `Pegged` on the exchange side. | Peg price type | Description | | -------------- | ---------------------------------------------------------------- | | `PrimaryPeg` | Pegs to the best bid (buy) or best ask (sell). | | `MarketPeg` | Pegs to the opposite side (best ask for buy, best bid for sell). | | `MidPricePeg` | Pegs to the mid-price between bid and ask. | | `LastPeg` | Pegs to the last traded price. | **Requirements**: - The underlying order must be a `LIMIT` order. Other order types are rejected. - `peg_price_type` is required; `peg_offset_value` is optional (defaults to 0). - `peg_offset_value` can be negative (e.g., sell-side offsets) or fractional. **Example**: ```python # Pegged to best bid with zero offset (BBO) order = self.order_factory.limit( instrument_id=instrument_id, order_side=OrderSide.BUY, quantity=qty, price=price, # Required for LIMIT order, but overridden by peg ) self.submit_order(order, params={"peg_price_type": "PrimaryPeg", "peg_offset_value": "0"}) # Pegged to mid-price with a -0.5 offset self.submit_order(order, params={"peg_price_type": "MidPricePeg", "peg_offset_value": "-0.5"}) ``` :::note The `price` field is still required when constructing the `LimitOrder`, but BitMEX ignores it for pegged orders and instead continuously tracks the reference price plus offset. ::: ### Time in force | Time in force | Supported | Notes | | ------------- | --------- | --------------------------------------------------- | | `GTC` | ✓ | Good Till Canceled (default). | | `GTD` | - | *Not supported by BitMEX*. | | `FOK` | ✓ | Fill or Kill - fills entire order or cancels. | | `IOC` | ✓ | Immediate or Cancel - partial fill allowed. | | `DAY` | ✓ | Expires at 00:00 UTC (BitMEX trading day boundary). | :::note `DAY` orders expire at 12:00am UTC, which marks the BitMEX trading day boundary (end of trading hours for that day). See the [BitMEX Exchange Rules](https://www.bitmex.com/exchange-rules) and [API documentation](https://www.bitmex.com/api/explorer/) for complete details. ::: ### Advanced order features | Feature | Supported | Notes | | ------------------ | --------- | ------------------------------------------------------------------------ | | Order Modification | ✓ | Modify price, quantity, and trigger price. | | Bracket Orders | ✓ | Use `contingency_type` and `linked_order_ids`. | | Iceberg Orders | ✓ | Use `display_qty`. | | Trailing Stops | ✓ | Use `trailing_offset`. Price offset type only. | | Pegged Orders | ✓ | Use `params` with `peg_price_type`. See [Pegged orders](#pegged-orders). | ### Batch operations | Operation | Supported | Notes | | ------------ | --------- | ------------------------------------------- | | Batch Submit | - | *Not supported by BitMEX*. | | Batch Modify | - | *Not supported by BitMEX*. | | Batch Cancel | ✓ | Cancel multiple orders in a single request. | ### Position management | Feature | Supported | Notes | | --------------- | --------- | -------------------------------------------------- | | Query positions | ✓ | REST and real-time position updates via WebSocket. | | Cross margin | ✓ | Default margin mode. | | Isolated margin | ✓ | | ### Order querying | Feature | Supported | Notes | | -------------------- | --------- | -------------------------------------------- | | Query open orders | ✓ | List all active orders. | | Query order history | ✓ | Historical order data. | | Order status updates | ✓ | Real-time order state changes via WebSocket. | | Trade history | ✓ | Execution and fill reports. | ### Liquidation and ADL handling BitMEX surfaces forced-close fills through the `execType` field on the `execution` channel: | `execType` | Meaning | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `Trade` | Normal execution (user or taker-initiated). | | `Liquidation` | Position was force-closed by the liquidation engine. BitMEX uses this code for both auto-deleveraging and counterparty liquidation fills. | | `Bankruptcy` | Account bankruptcy; position closed against the insurance fund. | | `Settlement` | Scheduled contract settlement. | | `Funding` | Funding settlement on open positions. | The adapter routes `Liquidation` and `Bankruptcy` through the standard `FillReport` path and logs a warning on bankruptcy executions. BitMEX's public API does **not** distinguish auto-deleveraging from counterparty liquidation in `execType`; both appear as `Liquidation`. An ADL-closed position can usually be identified by zero commission and the absence of a matching order in the local cache (the engine creates an external order for it). Upstream references: - [`/execution` field definitions](https://support.bitmex.com/hc/en-gb/articles/6205689858077--execution-field-definitions) - [Auto-Deleveraging overview](https://support.bitmex.com/hc/en-gb/articles/18589621443357-What-is-Auto-Deleveraging) - [Liquidation overview](https://support.bitmex.com/hc/en-gb/articles/360003188434-Liquidations) ## Market data - Order book deltas: `L2_MBP` only; `depth` 0 (full book) or 25. - Order book depth10 snapshots: fixed 10 levels via `orderBook10` channel. - Quotes, trades, and instrument updates are supported via WebSocket. - Funding rates, mark prices, and index prices are supported where applicable. - REST requests: - Current L2 order book snapshots with optional depth. - Trade ticks with optional `start`, `end`, and `limit` filters (up to 1,000 results per call). - Time bars (`1m`, `5m`, `1h`, `1d`) for externally aggregated LAST prices, including optional partial bins. - Funding rates with optional `start`, `end`, and `limit` filters. :::note BitMEX table REST page sizes vary by endpoint. Funding rate requests paginate in 500-row pages until the requested limit or time range is exhausted; trade tick and bar requests currently return one venue page per request and allow up to 1,000 rows. ::: ### Trade ID derivation Trade ticks and fills use the venue-provided `trdMatchID` (UUID) as the `TradeId`. When the venue omits `trdMatchID` (bucketed trades or certain execution types), the execution path falls back to the venue's `execID`; market data parsers fall back to a deterministic FNV-1a hash of the symbol, `ts_event`, price, size, and side. The same venue event yields the same trade ID across replays, keeping downstream dedup intact. ## Connection management ### HTTP Keep-Alive The BitMEX adapter uses HTTP keep-alive for optimal performance: - **Connection pooling**: Connections are automatically pooled and reused. - **Keep-alive timeout**: 90 seconds (matches BitMEX server-side timeout). - **Automatic reconnection**: Failed connections are automatically re-established. - **SSL session caching**: Reduces handshake overhead for subsequent requests. This configuration ensures low-latency communication with BitMEX servers by maintaining persistent connections and avoiding the overhead of establishing new connections for each request. ### Request authentication and expiration BitMEX uses an `api-expires` header for request authentication to prevent replay attacks: - Signed requests include an `api-expires` Unix timestamp set `recv_window_ms / 1000` seconds ahead (10 seconds by default). - BitMEX rejects any request once that timestamp has passed, so keep latency within your configured window. ## Funding rates The adapter receives funding rate data from the [Funding](https://www.bitmex.com/app/wsAPI#Funding) WebSocket stream. BitMEX returns a `fundingInterval` datetime field in each message, and the adapter reads the hours and minutes to compute the `interval` field on `FundingRateUpdate`. ## Rate limiting BitMEX implements a dual-layer rate limiting system: ### REST limits - **Burst limit**: 10 requests per second for authenticated users (applies to order placement, modification, and cancel endpoints). - **Rolling minute limit**: 120 requests per minute for authenticated users (30 requests per minute for unauthenticated users). - **Order caps**: 200 open orders and 10 stop orders per symbol; exceeding these caps triggers exchange-side rejections. The adapter enforces these quotas locally using the configured `max_requests_per_second` and `max_requests_per_minute` values. ### WebSocket limits - Connection requests: follow the exchange guidance (currently 3 connections per second per IP). - Private streams require authentication; the adapter reconnects automatically if a limit is exceeded. :::warning Exceeding BitMEX rate limits returns HTTP 429 and may trigger temporary IP bans; persistent 4xx/5xx errors can extend the lockout period. ::: ### Configurable rate limits The rate limits can be configured if your account has different limits than the defaults: | Parameter | Default (authenticated) | Default (unauthenticated) | Description | | ------------------------- | ----------------------- | ------------------------- | --------------------------------------------- | | `max_requests_per_second` | 10 | 10 | Maximum requests per second (burst limit). | | `max_requests_per_minute` | 120 | 30 | Maximum requests per minute (rolling window). | :::info For more details on rate limiting, see the [BitMEX API documentation on rate limits](https://www.bitmex.com/app/restAPI#Limits). ::: :::warning **Cancel Broadcaster Rate Limit Considerations** The cancel broadcaster (when `canceller_pool_size > 1`) fans out each cancel request to multiple independent HTTP clients in parallel. Each client maintains its own rate limiter, which means the effective request rate is multiplied by the pool size. **Example**: With `canceller_pool_size=3` and `max_requests_per_second=10`, a single cancel operation consumes **3 requests** (one per client), potentially reaching **30 requests/second** if canceling rapidly. Since BitMEX enforces rate limits **at the account level** (not per connection), the broadcaster can push you over the exchange's default limits of 10 req/s burst and 120 req/min rolling window. **Mitigations**: Reduce `max_requests_per_second` and `max_requests_per_minute` proportionally (divide by `canceller_pool_size`), or adjust the pool size itself (see [Cancel broadcaster configuration](#cancel-broadcaster)). Future versions may support shared rate limiters across the pool. ::: ### Rate-limit headers BitMEX exposes the current allowance via response headers: - `x-ratelimit-limit`: total requests permitted in the current window. - `x-ratelimit-remaining`: remaining requests before throttling occurs. - `x-ratelimit-reset`: UNIX timestamp when the allowance resets. - `retry-after`: seconds to wait after a 429 response. ## Submit broadcaster The BitMEX execution client includes a submit broadcaster that provides higher assurance of market and limit orders being accepted at target prices through parallel request fanout, trading lower minimum latency for the risk of duplicate submissions. ### Concepts Order submissions are time-critical operations - when a strategy decides to enter a position, any delay can result in missed opportunities or adverse pricing. The submit broadcaster addresses this by: - **Parallel fanout**: Submit requests are simultaneously broadcast to multiple independent HTTP client instances. - **First-success short-circuiting**: The first successful response wins, minimizing latency to acceptance. - **Shared client_order_id**: All transports use the same `client_order_id`. BitMEX rejects duplicate submissions with "duplicate clOrdID" (tracked as expected rejections). - **Latency vs. duplicates tradeoff**: Accepts the risk of potential duplicate fills (if multiple transports succeed before rejection) in exchange for lower minimum latency and higher assurance of acceptance. This architecture reduces the minimum latency to order acceptance by parallelizing across multiple network paths. ### Usage The submit broadcaster is opt-in and controlled via the `submit_tries` parameter when submitting orders. By default, orders are submitted through a single HTTP client. To enable broadcasting: ```python # Single submission (default behavior) self.submit_order(order) # Broadcast to 2 parallel HTTP clients for redundancy self.submit_order(order, params={"submit_tries": 2}) # Broadcast to 3 parallel HTTP clients (maximum recommended) self.submit_order(order, params={"submit_tries": 3}) ``` **Behavior**: - `submit_tries` must be a positive integer. - Broadcasting only occurs when `submit_tries > 1`. Default submits go through a single HTTP client. - If `submit_tries` exceeds `submitter_pool_size`, it will be capped at the pool size with a warning. - All transports use the same `client_order_id`; BitMEX rejects duplicates as expected rejections. ### Health monitoring Each HTTP client in the broadcaster pool maintains health metrics: - Successful submissions mark a client as healthy. - Failed requests increment error counters. - Background health checks periodically verify client connectivity. - Degraded clients are tracked but remain in the pool to maintain fault tolerance. The broadcaster exposes metrics including total submits, successful submits, failed submits, and expected rejects for operational monitoring and debugging. #### Tracked metrics | Metric | Type | Description | | -------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ | | `total_submits` | `u64` | Total number of submit operations initiated. | | `successful_submits` | `u64` | Number of submit operations that successfully received acknowledgement from BitMEX. | | `failed_submits` | `u64` | Number of submit operations where all HTTP clients in the pool failed (no healthy clients or all requests failed). | | `expected_rejects` | `u64` | Number of expected rejection patterns detected (e.g., duplicate clOrdID from parallel submissions). | | `healthy_clients` | `usize` | Current number of healthy HTTP clients in the pool (clients that passed recent health checks). | | `total_clients` | `usize` | Total number of HTTP clients configured in the pool (`submitter_pool_size`). | These metrics can be accessed programmatically via the `get_metrics()` method on the `SubmitBroadcaster` instance. ### Configuration The submit broadcaster is configured via the execution client configuration: | Option | Default | Description | | ---------------------- | ------- | ---------------------------------------------------------------------------------- | | `submitter_pool_size` | `None` | Size of the HTTP client pool. `None` resolves to 1 (single client, no redundancy). | | `submitter_proxy_urls` | `None` | Optional list of proxy URLs for submit broadcaster path diversity. | `submitter_pool_size` must be in `[1, 15]`. The submit and cancel pools always count together, and an unset pool counts as 1, so their combined size must be in `[2, 16]`. **Example configuration**: ```python from nautilus_trader.adapters.bitmex import BitmexExecutionClientConfig exec_config = BitmexExecutionClientConfig( api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", submitter_pool_size=3, # Recommended pool size for redundancy ) ``` :::tip Each pooled client has an independent local rate limiter, but BitMEX enforces limits at the account level; see [Rate limiting](#rate-limiting). The default `submitter_pool_size=None` selects one client and provides no redundant fan-out. The recommended setting of `submitter_pool_size=3` allows requests with `submit_tries > 1` to fan out to up to three healthy HTTP clients for fault tolerance. A broadcast can therefore send three HTTP requests for one submit. ::: The broadcaster is automatically started when the execution client connects and stopped when it disconnects. Submit operations are routed through the broadcaster only when `submit_tries > 1`; default submits use a single HTTP client directly. ## Cancel broadcaster The BitMEX execution client includes a cancel broadcaster that provides fault-tolerant order cancellation through parallel request fanout. ### Concepts Order cancellations are time-critical operations - when a strategy decides to cancel an order, any delay or failure can result in unintended fills, slippage, or unwanted position exposure. The cancel broadcaster addresses this by: - **Parallel fanout**: Cancel requests are simultaneously broadcast to multiple independent HTTP client instances. - **First-success short-circuiting**: The first successful response wins, and remaining in-flight requests are immediately aborted. - **Fault tolerance**: If one HTTP client experiences network issues, DNS failures, or connection timeouts, other clients in the pool continue processing. - **Idempotent success handling**: Responses indicating the order was already canceled (such as "orderID not found" or similar idempotent states) are treated as success rather than failure, preventing unnecessary error propagation. This architecture ensures that a single network path failure or slow connection doesn't block cancel operations, improving the reliability of risk management and position control in live trading. ### Health monitoring Each HTTP client in the broadcaster pool maintains health metrics: - Successful cancellations mark a client as healthy. - Failed requests increment error counters. - Background health checks periodically verify client connectivity. - Degraded clients are tracked but remain in the pool to maintain fault tolerance. The broadcaster exposes metrics including total cancels, successful cancels, failed cancels, expected rejects (already canceled orders), and idempotent successes for operational monitoring and debugging. #### Tracked metrics | Metric | Type | Description | | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ | | `total_cancels` | `u64` | Total number of cancel operations initiated (includes single, batch, and cancel-all requests). | | `successful_cancels` | `u64` | Number of cancel operations that successfully received acknowledgement from BitMEX. | | `failed_cancels` | `u64` | Number of cancel operations where all HTTP clients in the pool failed (no healthy clients or all requests failed). | | `expected_rejects` | `u64` | Number of expected rejection patterns detected (e.g., post-only order rejections). | | `idempotent_successes` | `u64` | Number of idempotent success responses (order already cancelled, order not found, unable to cancel due to state). | | `healthy_clients` | `usize` | Current number of healthy HTTP clients in the pool (clients that passed recent health checks). | | `total_clients` | `usize` | Total number of HTTP clients configured in the pool (`canceller_pool_size`). | These metrics can be accessed programmatically via the `get_metrics()` method on the `CancelBroadcaster` instance. ### Configuration The cancel broadcaster is configured via the execution client configuration: | Option | Default | Description | | ---------------------- | ------- | ---------------------------------------------------------------------------------- | | `canceller_pool_size` | `None` | Size of the HTTP client pool. `None` resolves to 1 (single client, no redundancy). | | `canceller_proxy_urls` | `None` | Optional list of proxy URLs for cancel broadcaster path diversity. | `canceller_pool_size` must be in `[1, 15]`. The submit and cancel pools always count together, and an unset pool counts as 1, so their combined size must be in `[2, 16]`. **Example configuration**: ```python from nautilus_trader.adapters.bitmex import BitmexExecutionClientConfig exec_config = BitmexExecutionClientConfig( api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", canceller_pool_size=3, # Recommended pool size for redundancy ) ``` :::tip Each pooled client has an independent local rate limiter, but BitMEX enforces limits at the account level; see [Rate limiting](#rate-limiting). The default `canceller_pool_size=None` selects one client and provides no redundant fan-out. The recommended setting of `canceller_pool_size=3` broadcasts each cancel request to up to three healthy HTTP clients for fault tolerance. A broadcast can therefore send three HTTP requests for one cancel. ::: The broadcaster is automatically started when the execution client connects and stopped when it disconnects. All cancel operations (`cancel_order`, `cancel_all_orders`, `batch_cancel_orders`) are automatically routed through the broadcaster without requiring any changes to strategy code. ## Dead man's switch The adapter supports BitMEX's [dead man's switch](https://www.bitmex.com/app/restAPI#OrdercancelAllAfter) (`cancelAllAfter`), which provides automatic order cancellation as a safety net against connectivity failures. ### How it works When enabled, a server-side timer is set on BitMEX. If the timer expires without being refreshed, BitMEX cancels **all** open orders on the account. The adapter keeps the timer alive by sending periodic heartbeat requests. If the adapter loses connectivity (network failure, process crash, etc.), the heartbeats stop and BitMEX cancels the orders after the configured timeout. The flow: 1. On **connect**, the adapter calls `POST /api/v1/order/cancelAllAfter` with the configured timeout (in milliseconds) to arm the server-side timer. 2. A background task sends the same request at a **refresh interval** of `timeout / 4` (minimum 1 second) to keep resetting the timer before it expires. 3. On **disconnect**, the adapter waits for the background heartbeat task to fully shut down, then calls `cancelAllAfter` with `timeout=0` to **disarm** the server-side timer. For example, with a 60-second timeout the adapter sends a heartbeat every 15 seconds. If four consecutive heartbeats fail (60 seconds of lost connectivity), BitMEX cancels all open orders. ### Disconnect ordering Disarming the dead man's switch during disconnect requires careful ordering. The disarm request (`timeout=0`) should be the last `cancelAllAfter` call to reach BitMEX. If an in-flight heartbeat were processed after the disarm, it would re-arm the server-side timer and orders could be unexpectedly cancelled after the timeout expires, even though the adapter disconnected gracefully. The adapter mitigates this in both implementations: - **Rust**: The heartbeat task is immediately stopped (abort + await) so disconnect does not stall waiting for a sleep or HTTP timeout to elapse. The disarm request is then sent after the task has exited. - **Python**: The heartbeat task is cancelled and awaited, ensuring the coroutine fully unwinds before the disarm request is sent. In a force-stop scenario (e.g., process shutdown via `stop()`), the heartbeat task is aborted without disarming. This is intentional, as the server-side timer provides the desired safety behavior when the process exits unexpectedly. :::note Each heartbeat consumes one REST rate limit token. A 60-second timeout uses approximately 4 requests per minute from the 120/min budget. ::: ### Configuration Enable the dead man's switch by setting `deadmans_switch_timeout_secs` on the execution client config: ```python from nautilus_trader.adapters.bitmex import BitmexExecutionClientConfig exec_config = BitmexExecutionClientConfig( api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", deadmans_switch_timeout_secs=60, # Cancel all orders after 60s of lost connectivity ) ``` When enabled, the adapter logs: ``` Starting dead man's switch: timeout=60s, refresh_interval=15s ``` on connect, and: ``` Disarming dead man's switch ``` on disconnect. :::tip A timeout of **60 seconds** is the recommended starting point. Shorter timeouts provide faster protection but are more sensitive to transient network blips. Longer timeouts are more tolerant of brief outages but leave orders exposed longer during a real failure. ::: :::warning The dead man's switch applies to **all** open orders on the account, not just orders placed by the adapter. If other systems place orders on the same account, enabling the dead man's switch will affect those orders too. ::: ## Configuration ### API credentials BitMEX API credentials can be provided either directly in the configuration or via environment variables: - `BITMEX_API_KEY`: Your BitMEX API key for production. - `BITMEX_API_SECRET`: Your BitMEX API secret for production. - `BITMEX_TESTNET_API_KEY`: Your BitMEX API key for testnet. - `BITMEX_TESTNET_API_SECRET`: Your BitMEX API secret for testnet. To generate API keys: 1. Log in to your BitMEX account. 2. Navigate to Account & Security -> API Keys. 3. Create a new API key with appropriate permissions. 4. For testnet, use [testnet.bitmex.com](https://testnet.bitmex.com). :::note **Testnet API endpoints**: - REST API: `https://testnet.bitmex.com/api/v1` - WebSocket: `wss://ws.testnet.bitmex.com/realtime` The adapter automatically routes requests to the correct endpoints when `environment=BitmexEnvironment.TESTNET` is configured. ::: ### Data client configuration options The BitMEX data client provides the following configuration options: | Option | Default | Description | | ---------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------- | | `api_key` | `None` | Optional API key; if `None`, loaded from the environment selected by `environment`. | | `api_secret` | `None` | Optional API secret; if `None`, loaded from the environment selected by `environment`. | | `environment` | `None` | Environment enum (`MAINNET` or `TESTNET`). | | `base_url_http` | `None` | Override for the REST base URL (defaults to production). | | `base_url_ws` | `None` | Override for the WebSocket base URL (defaults to production). | | `http_timeout_secs` | `60` | Request timeout applied to HTTP calls. | | `max_retries` | `3` | Maximum retry attempts for HTTP calls. | | `retry_delay_initial_ms` | `1,000` | Initial backoff delay (milliseconds) between retries. | | `retry_delay_max_ms` | `10,000` | Maximum backoff delay (milliseconds) between retries. | | `recv_window_ms` | `10,000` | Expiration window (milliseconds) for signed requests. See [Request authentication](#request-authentication-and-expiration). | | `update_instruments_interval_mins` | `None` | Interval (minutes) between instrument catalog refreshes. `None` disables periodic refresh. | | `max_requests_per_second` | `10` | Burst rate limit enforced by the adapter for REST calls. | | `max_requests_per_minute` | `120` | Rolling minute rate limit enforced by the adapter for REST calls. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | ### Execution client configuration options The BitMEX execution client provides the following configuration options: | Option | Default | Description | | ------------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------- | | `account_id` | `None` | Optional account ID; defaults to `BITMEX-001` when omitted. | | `api_key` | `None` | Optional API key; if `None`, loaded from the environment selected by `environment`. | | `api_secret` | `None` | Optional API secret; if `None`, loaded from the environment selected by `environment`. | | `environment` | `None` | Environment enum (`MAINNET` or `TESTNET`). | | `base_url_http` | `None` | Override for the REST base URL (defaults to production). | | `base_url_ws` | `None` | Override for the WebSocket base URL (defaults to production). | | `http_timeout_secs` | `60` | Request timeout applied to HTTP calls. | | `max_retries` | `3` | Maximum retry attempts for HTTP calls. | | `retry_delay_initial_ms` | `1,000` | Initial backoff delay (milliseconds) between retries. | | `retry_delay_max_ms` | `10,000` | Maximum backoff delay (milliseconds) between retries. | | `recv_window_ms` | `10,000` | Expiration window (milliseconds) for signed requests. See [Request authentication](#request-authentication-and-expiration). | | `max_requests_per_second` | `10` | Burst rate limit enforced by the adapter for REST calls. | | `max_requests_per_minute` | `120` | Rolling minute rate limit enforced by the adapter for REST calls. | | `deadmans_switch_timeout_secs` | `None` | Timeout in seconds for the dead man's switch. `None` disables. See [Dead man's switch](#dead-mans-switch). | | `canceller_pool_size` | `None` | Number of HTTP clients in the cancel broadcaster pool. `None` resolves to 1. See [Cancel broadcaster](#cancel-broadcaster). | | `submitter_pool_size` | `None` | Number of HTTP clients in the submit broadcaster pool. `None` resolves to 1. See [Submit broadcaster](#submit-broadcaster). | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `submitter_proxy_urls` | `None` | Optional list of proxy URLs for submit broadcaster path diversity. | | `canceller_proxy_urls` | `None` | Optional list of proxy URLs for cancel broadcaster path diversity. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | Each broadcaster pool size must be in `[1, 15]`. An unset pool counts as 1, and the submit and cancel pool sizes combined must be in `[2, 16]`. ### Configuration examples A typical BitMEX configuration for live trading includes both testnet and mainnet options: ```python from nautilus_trader.adapters.bitmex import BitmexDataClientConfig from nautilus_trader.adapters.bitmex import BitmexEnvironment from nautilus_trader.adapters.bitmex import BitmexExecutionClientConfig # Using environment variables (recommended) testnet_data_config = BitmexDataClientConfig( environment=BitmexEnvironment.TESTNET, ) # Using explicit credentials mainnet_data_config = BitmexDataClientConfig( api_key="YOUR_API_KEY", # Or use os.getenv("BITMEX_API_KEY") api_secret="YOUR_API_SECRET", # Or use os.getenv("BITMEX_API_SECRET") environment=BitmexEnvironment.MAINNET, ) mainnet_exec_config = BitmexExecutionClientConfig( api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", environment=BitmexEnvironment.MAINNET, ) ``` ## Trading considerations ### Contingent orders The BitMEX execution adapter maps Nautilus contingent order lists to the exchange's native `clOrdLinkID`/`contingencyType` mechanics. When the engine submits `ContingencyType::Oco` or `ContingencyType::Oto` orders, the adapter: - Creates/maintains the linked order group on BitMEX so child stops and targets inherit the parent order status. - Propagates order list updates and cancellations so that contingent peers stay aligned with the current position state. - Surfaces execution reports with the appropriate contingency metadata, enabling strategy-level tracking without additional manual wiring. The adapter does not map Nautilus `ContingencyType::Ouo` to BitMEX. For bracket flows with an entry, stop, and take-profit, BitMEX can natively link the OTO entry-to-contingent activation step, but the mutual cancel or update behavior between the stop-loss and take-profit legs requires strategy-level emulation. When defining strategies, continue to use Nautilus `OrderList`/`ContingencyType` abstractions, but do not rely on the adapter to provide OUO pairing for the contingent exit legs. ### Contract specifications - **Inverse contracts**: Settled in cryptocurrency (e.g., XBTUSD settled in XBT). - **Linear contracts**: Settled in stablecoin (e.g., ETHUSDT settled in USDT). - **Contract size**: Varies by instrument, check specifications carefully. - **Tick size**: Minimum price increment varies by contract. ### Margin requirements - Initial margin requirements vary by contract and market conditions. - Maintenance margin is typically lower than initial margin. - Liquidation occurs when maintenance margin requirement is not satisfied. - BitMEX supports both isolated margin and cross margin modes. - Risk limits can be adjusted based on position size per the [Exchange Rules](https://www.bitmex.com/exchange-rules). ### Fees - **Maker fees**: Typically negative (rebate) for providing liquidity. - **Taker fees**: Positive fee for taking liquidity. - **Funding rates**: Apply to perpetual contracts every 8 hours. - **Prediction market fees**: Maker 0.00%, Taker 0.25% (no leverage allowed). ## Contributing :::info For additional features or to contribute to the BitMEX adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Blockchain Source: https://nautilustrader.io/docs/latest/integrations/blockchain/ ## Overview The blockchain adapter ingests DeFi data from EVM chains and exposes it through the NautilusTrader data model. It also includes an execution client for locally signed Uniswap V3 market swaps. The execution client is not production-ready. The adapter uses three backends: - HyperSync: high-throughput historical blocks and contract logs. See the [Envio HyperSync docs](https://docs.envio.dev/docs/HyperSync/hypersync-usage) for query shape, pagination, and tuning. - HTTP RPC: contract calls, Multicall reads, and final on-chain state hydration. - Postgres: optional durable cache state, pool metadata, decoded events, and snapshots. ## Capability status | Capability | Scope | Readiness | | ------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------- | | Historical blocks | Any configured `Chain` with a reachable HyperSync endpoint. | Available through the Rust service and `sync-blocks`. | | Live blocks | HyperSync, or WSS RPC for chains with an RPC client. | Available through the Rust and Python data-client surfaces. | | DEX pool discovery | Chain and DEX combinations with a registered pool-creation parser. | Available through the data client and `sync-dex`. | | Pool snapshots and replay | Concentrated-liquidity integrations with the complete snapshot parser set. | Available with Postgres and the provider constraints below. | | Live pool events | Registered swap, liquidity, collect, flash, and fee-protocol parsers. | Available through the Rust and Python data-client surfaces. | | Transaction execution | Locally signed Uniswap V3 BUY and SELL market swaps. | Experimental; not production-ready or exposed for Python use. | Direct WSS RPC clients exist for Ethereum, Polygon, Base, Arbitrum, and BSC. Other configured chain values can use HyperSync block history when their endpoint is reachable, but they do not support WSS live mode. ### Chain and DEX command support Command support is derived from the parsers registered for each chain and DEX. The CLI help for `sync-dex` and `analyze-pool(s)` prints the same capability boundaries. | Tier | Meaning | DEXes | Chains | | --------------- | ------------------------------------------------------------ | --------------------------------------------------- | ------------------------------------------- | | Replay-ready | Discovery, snapshot, and fee-protocol replay parsers. | Uniswap V3 and PancakeSwap V3. | Ethereum, Base, Arbitrum, and BSC. | | Analysis only | Snapshot parsers without CLI pool discovery. | Aerodrome Slipstream. | Base. | | Discovery only | Pool discovery without the complete snapshot parser set. | Uniswap V2 and Uniswap V4. | Ethereum, Base, and Arbitrum. | | Discovery only | Pool discovery without the complete snapshot parser set. | Camelot V3 and Fluid DEX. | Arbitrum. | | Registered only | Metadata registration without command-capable event parsers. | Curve Finance and Fluid DEX. | Ethereum. | | Registered only | Metadata registration without command-capable event parsers. | Aerodrome V1, BaseSwap V2, BaseX, and SushiSwap V3. | Base. | | Registered only | Metadata registration without command-capable event parsers. | Curve Finance, SushiSwap V2, and SushiSwap V3. | Arbitrum. | | Blocks only | No DEX registration; `sync-blocks` remains available. | - | Other configured chains, including Polygon. | `sync-dex` requires a pool-creation parser. `analyze-pool(s)` requires Initialize, Swap, Mint, Burn, and Collect parsers. Replay-ready integrations also parse `SetFeeProtocol`; integrations with a `CollectProtocol` parser can replay protocol-fee withdrawals. Aerodrome Slipstream has no pool-creation parser, and the CLI has no separate pool-registration command. Analysis works only when its pool and token metadata already exist in the cache through another integration path. Its replay-derived snapshots cannot be validated against on-chain state. Registered-only DEXes are omitted from command help and fail the relevant capability check. ### Interface availability | Surface | Rust | Python | CLI | | ------------------------------------- | ----------------------------------------- | -------------------------- | ---------------------------------------- | | Data configuration and factory | Public config and factory. | Public config and factory. | - | | Live data subscriptions | Data-client subscription API. | LiveNode data-client API. | - | | Block sync, discovery, and analysis | Adapter services. | - | `sync-blocks`, `sync-dex`, and analysis. | | Stored snapshot loading | Cache API. | `load_pool_snapshot`. | - | | Execution configuration | Public config. | Configuration types only. | - | | Execution factory and order routing | Public factory and client. | - | - | | Preflight, wrap, approve, and storage | Direct `BlockchainExecutionClient` calls. | - | - | The Python module does not register or export `BlockchainExecutionClientFactory`, so Python LiveNode configuration cannot instantiate the execution client. ### Examples Runnable data-client examples are available for both public language surfaces: - [Rust LiveNode data tester](../../crates/adapters/blockchain/examples/node_data_tester.rs). - [Python data tester](../../examples/live/blockchain/data_tester.py). - [Python LiveNode example](../../examples/live/blockchain/node_test.py). The repository does not provide a maintained runnable execution setup example. Build execution integrations in Rust and apply the constraints in [Execution](#execution). ## Core primitives The DeFi domain model lives in `nautilus_model::defi`. ### Chain `Chain` defines the target blockchain and its default service endpoints. | Field | Type | Description | | -------------------------- | ---------------- | ------------------------------------------------------------------ | | `name` | `Blockchain` | Chain enum value, such as `Ethereum` or `Arbitrum`. | | `chain_id` | `u32` | EVM chain ID, such as `1` for Ethereum. | | `hypersync_url` | `String` | HyperSync endpoint, by default `https://{chain_id}.hypersync.xyz`. | | `rpc_url` | `Option` | Optional direct RPC endpoint stored on the chain model. | | `native_currency_decimals` | `u8` | Native gas token decimal precision, usually `18`. | Chains can be loaded by numeric ID with `Chain::from_chain_id` or by name with `Chain::from_chain_name`. | Chain family | Code | Name | Decimals | | ---------------- | ---- | ------------ | -------- | | Ethereum and L2s | ETH | Ethereum | 18 | | Polygon | POL | Polygon | 18 | | Avalanche | AVAX | Avalanche | 18 | | BSC | BNB | Binance Coin | 18 | ### DEX and pools DEX integrations register: - Factory addresses. - Event signatures and parser functions. - AMM type. Pool definitions bind the chain and DEX to a pool contract address or protocol pool ID to form a stable Nautilus instrument ID. The token pair, fee tier, tick spacing, and creation block remain pool metadata. When the data engine processes a pool definition, it caches and publishes a `CurrencyPair` under the same pool instrument ID. The instrument keeps the raw pool `token0`/`token1` order as base/quote, derives price and size precision from token decimals up to `FIXED_PRECISION`, and exposes the fee tier divided by 1,000,000 as `taker_fee`. Distinct pool identifiers let same-token pools coexist in the cache and on the message bus. Uniswap V3 and compatible concentrated-liquidity pools also use: - `Initialize(uint160,int24)` for initial price state. - `Mint` and `Burn` events for position and tick state replay. - `Swap` events for live pool price movement. - HTTP RPC final-state reads for `slot0`, liquidity, active ticks, and position data. ## Data client configuration | Option | Default | Description | | --------------------------------- | ----------------------------- | ------------------------------------------------------ | | `chain` | Required | Target `Chain`, such as Ethereum or Arbitrum. | | `dex_ids` | `[]` | DEX integrations to register and sync. | | `http_rpc_url` | Required | HTTP RPC endpoint for contract reads and Multicall. | | `wss_rpc_url` | `None` | WSS endpoint; required for RPC live streams. | | `rpc_requests_per_second` | `None` | Optional RPC request throttle. | | `multicall_calls_per_rpc_request` | `200` | Requested maximum Multicall targets per RPC request. | | `use_hypersync_for_live_data` | Rust: `false`; Python: `true` | When true, live block and event streams use HyperSync. | | `from_block` | `None` | Optional start block for historical sync. | | `pool_filters` | `DexPoolFilters()` | Pool universe filtering rules. | | `postgres_cache_database_config` | `None` | Optional Postgres cache configuration. | | `proxy_url` | `None` | Optional HTTP and WebSocket proxy URL. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | :::note Pool snapshot requests require a Postgres cache database. The in-memory cache can hold tokens and pools, but latest pool profiler bootstrap reads snapshot and event state through the cache database path. ::: ## Environment Set credentials outside the repository: ```bash export ENVIO_API_TOKEN="" export RPC_HTTP_URL="https://your-rpc.example" export RPC_WSS_URL="wss://your-rpc.example" ``` For local `.env` usage, keep the file out of version control: ```dotenv ENVIO_API_TOKEN= RPC_HTTP_URL=https://your-rpc.example RPC_WSS_URL=wss://your-rpc.example ``` - `ENVIO_API_TOKEN` is required by the Rust HyperSync client. Missing or malformed tokens fail client construction before any query is sent. - `RPC_HTTP_URL` or `--rpc-url` is required for contract reads and snapshot hydration. - `RPC_WSS_URL` is required when `use_hypersync_for_live_data = false`; that mode uses WSS RPC live streams. Execution adds further variables (see [Execution](#execution)): - The signer private key is read from the variable named by the `signer_private_key_env` configuration field, never from configuration directly. - Signed-payload protection reads the active and retired 32-byte keys from the variables named by `payload_key_env` and `payload_key_retired_env`. These configuration fields contain variable names, never key values. For token setup and quota details, see Envio's [HyperSync API token docs](https://docs.envio.dev/docs/HyperSync/api-tokens). ### RPC provider requirements `RPC_HTTP_URL` or `--rpc-url` must point at an EVM JSON-RPC endpoint for the target chain. The data client uses it for contract reads, and first-time pool syncs read on-chain state through it. The client reads the HyperSync endpoint from `Chain::hypersync_url`; built-in chains default to `https://{chain_id}.hypersync.xyz`. Choose an RPC provider that supports the intended target blocks and Multicall workload. Provider labels are not sufficient evidence of archive access: verify an `eth_getCode` or `eth_call` against the historical block the workflow will use. Returning the block header alone does not prove that historical contract state is available. Large pools may also require higher payload, gas, timeout, and request-rate limits. Archive support affects validation, not whether event sync runs: - On an archive node, a historical-block snapshot validates against on-chain state and is stored with `validation_state = on_chain`. - On a non-archive node, the historical read fails and the snapshot stays `validation_state = replay`, which is still usable as a replay start point. - A first-time sync on a non-archive node must use a recent `--to-block`, because bootstrap reads on-chain state at the target block and non-archive nodes serve only recent state. ## Local services The development Docker Compose file starts Postgres, Redis, and pgAdmin. To create the containers, wait for Postgres, and initialize the database schema, run: ```bash make init-services ``` Use `make start-services` to start an initialized stack. Run `make init-db` to initialize or reapply the Postgres schema. The local Postgres defaults are: | Field | Value | | -------- | ---------------- | | Host | `127.0.0.1:5432` | | Database | `nautilus` | | User | `nautilus` | | Password | `pass` | Check that the schema exists: ```bash docker exec nautilus-database psql -U nautilus -d nautilus -Atc \ "select count(*) from information_schema.tables where table_schema='public'" ``` Pool snapshot generation and snapshot requests require a schema-initialized Postgres cache. Pool discovery and snapshot generation write `token`, `pool`, `pool_*_event`, `pool_snapshot`, `pool_position`, and `pool_tick` rows. Use a dedicated database or resettable Docker volume for repeatable or destructive data workloads. ## Data flow ### Architecture `sync-dex` discovers and stores pools and tokens. `analyze-pool(s)` then generates `pool_snapshot` rows. The diagram shows the default replay path and the `--snapshot-from-rpc` path. ```mermaid flowchart TD HS["HyperSync (Envio): logs and events"] RPC["HTTP RPC + Multicall3: on-chain reads"] PG[("Postgres cache")] subgraph discovery["sync-dex (pool discovery)"] direction TB D1["Stream factory PoolCreated logs"] D2["Fetch ERC-20 token metadata"] D3["Write pool and token rows"] D1 --> D2 --> D3 end subgraph analyze["analyze-pool(s) (snapshot generation, one task per pool)"] direction TB AP0{"Mode"} AP1["Default: sync full pool events"] AP2["Bootstrap from cache snapshot, replay events"] AP3["extract_snapshot per --checkpoint-blocks"] AP4["Persist snapshot + ticks + positions"] AP5{"check_snapshot_validity"} RP1["--snapshot-from-rpc: stream state events"] RP2["Hydrate checkpoint from RPC"] RP3["Persist snapshot + ticks + positions"] AP0 --> AP1 --> AP2 --> AP3 --> AP4 --> AP5 AP0 --> RP1 --> RP2 --> RP3 AP5 -->|"matches chain"| V1["validation_state = on_chain"] AP5 -->|"RPC cannot reach block, or --skip-validation"| V2["validation_state = replay"] AP5 -->|"structural mismatch"| V3["validation_state = invalid"] RP3 -->|"validated from RPC"| V1 end R["Backtest replay: load latest usable snapshot (not invalid), replay forward"] HS --> D1 RPC --> D2 D3 --> PG HS --> AP1 HS --> RP1 PG --> AP2 AP4 --> PG RP3 --> PG RPC --> AP5 RPC --> RP2 PG --> R ``` `analyze-pools` runs one task per pool, bounded by `--concurrency`. Each task owns its data client. A snapshot is usable as a replay start point unless its `validation_state` is `invalid`. ### Data-client surface The public data client supports these DeFi subscriptions and requests: | Surface | Commands | Behavior | | ----------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | | Blocks | `SubscribeBlocks`, `UnsubscribeBlocks` | Starts or stops the shared HyperSync or WSS RPC block feed. | | Complete pool | `SubscribePool`, `UnsubscribePool` | Selects swaps, mints, burns, collects, flashes, and both fee-protocol event types. | | Pool swaps | `SubscribePoolSwaps`, `UnsubscribePoolSwaps` | Selects swap events for one pool instrument. | | Liquidity updates | `SubscribePoolLiquidityUpdates`, `UnsubscribePoolLiquidityUpdates` | Selects mint and burn events for one pool instrument. | | Fee collections | `SubscribePoolFeeCollects`, `UnsubscribePoolFeeCollects` | Selects collect events for one pool instrument. | | Flash events | `SubscribePoolFlashEvents`, `UnsubscribePoolFlashEvents` | Selects flash events for one pool instrument. | | Pool snapshot | `RequestPoolSnapshot` | Publishes the pool definition, then a usable snapshot when cache bootstrap and validation succeed. | Subscriptions share the underlying block and DEX event feeds. Removing one subscription does not stop a feed that another subscription still owns. Pool snapshot requests require Postgres because bootstrap reads stored pool and event state through the cache database. :::warning DeFi pool definitions and account-state updates publish on typed message-bus routers. A `subscribe_any` handler never receives them. Use `subscribe_defi_pools` and `subscribe_account_state`, or the matching actor subscription APIs. ::: ### Pool discovery Pool discovery: - Streams DEX factory events from HyperSync. - Fetches ERC-20 metadata through RPC. - Stores valid tokens and pools in the cache. - Skips invalid token metadata. `DexPoolFilters` can also exclude empty token metadata. ### Live data - `use_hypersync_for_live_data = true`: subscribe to blocks through HyperSync for live timestamps and hold one open-ended HyperSync DEX-event stream per subscribed DEX filter. - `use_hypersync_for_live_data = false`: use WSS RPC block and pool-log subscriptions for live swaps, liquidity updates, fee collections, flash events, and fee-protocol events. ### Snapshot bootstrap For Uniswap V3-compatible snapshots, the default bootstrap replays stored pool events to rebuild price, liquidity, ticks, positions, fees, and counters. Validation then reads on-chain state through HTTP RPC and Multicall. Bootstrap modes: - Default: store the full pool event history up to the target block, then bootstrap from the database. - `--snapshot-from-rpc`: skip full swap storage, stream Initialize, Mint, Burn, SetFeeProtocol, and CollectProtocol events from HyperSync to enumerate ticks and positions, then hydrate the exact checkpoint block from RPC. Use `--snapshot-from-rpc` for old high-volume pools when the required output is the final snapshot, not a stored swap history. It cannot be combined with `--from-block`, `--reset`, or `--require-existing-snapshot`. In `--snapshot-from-rpc` mode, final RPC hydration is the source of the checkpoint state. If it fails, the command fails instead of emitting a replayed snapshot with stale price state. ### Snapshot validation For a replay-derived snapshot, bootstrap compares the profiler against on-chain state before marking it valid. | Class | Fields | Mismatch result | | -------------- | --------------------------------------------------------------------------- | ---------------------------------------------- | | Structural | Current tick, active liquidity, per-tick liquidity, and position liquidity. | Store `invalid`; exclude from default loading. | | Non-structural | Sqrt price, fee protocol, and protocol-fee balances. | Warn and accept the snapshot as `on_chain`. | Non-structural differences can arise because event replay is transaction-scoped while an RPC snapshot is block-scoped, a fork or replay range omits a fee-protocol update, or replay rounding differs from the on-chain fee accumulator. Accepting those fields matches backtest replay behavior. ### Snapshot bootstrap guard Use `--require-existing-snapshot` when analysis should run only from the local snapshot cache: - Checks for the latest usable `pool_snapshot` at or before the target block. - Returns `needs_bootstrap` if no usable snapshot exists. - Treats an empty creation-block snapshot with no positions or ticks as unusable. - Skips the creation-to-target bootstrap for that pool. #### Analysis output `analyze-pool(s)` prints: - One JSON result per `--checkpoint-blocks` entry. - One JSON result at `--to-block` when no checkpoints are given. A pool that needs a first-time bootstrap has this shape: ```json { "chain": "Ethereum", "dex": "UniswapV3", "pool_address": "0x1111111111111111111111111111111111111111", "target_block": 25218797, "status": "needs_bootstrap" } ``` A successful result includes `validation_state`: - `on_chain`: hydrated and matched against chain. - `replay`: replay-derived or unchecked, still usable as a replay start point. - `invalid`: hydrated and mismatched, not usable. ```json { "chain": "Ethereum", "dex": "UniswapV3", "pool_address": "0x1111111111111111111111111111111111111111", "target_block": 25218797, "status": "success", "snapshot_block": 25218790, "positions": 2, "ticks": 7, "validation_state": "replay", "already_valid": false, "liquidity_utilization_rate": 0.25 } ``` ### Checkpoints and concurrency - `--checkpoint-blocks b1,b2,...`: produces snapshots in one bootstrap pass. Blocks are sorted, deduped, and clamped to `--to-block`. - `--concurrency`: controls `analyze-pools` parallelism. Default: `4`. - `--skip-validation`: skips the on-chain compare and keeps replay-derived snapshots as `replay`. - `--snapshot-from-rpc`: hydrates from chain at the checkpoint block and records snapshots as `on_chain`. Snapshot keys: - Default mode: keyed to the last pool event at or before the checkpoint. Checkpoints with no events between them can share one stored row. - `--snapshot-from-rpc`: keyed to the requested checkpoint block with a block-scoped sentinel transaction/log index. ### Backtest replay Backtest replay needs a snapshot in the input data. The adapter does not service live snapshot requests during backtests. `load_pool_snapshot` reads a full snapshot, including positions and ticks, from Postgres: ```python from nautilus_trader.adapters.blockchain import load_pool_snapshot snapshot = load_pool_snapshot( pg_config=postgres_config, chain_id=chain_id, pool_address=pool_address, before_block=replay_start_block, # latest snapshot at or before this block ) ``` Replay rules: - By default, snapshots marked `invalid` are excluded; both `on_chain` and `replay` snapshots can be returned. Pass `require_valid=False` only when the caller also accepts `invalid` snapshots. - Treat `None` as setup failure. Do not replay without profiler state. - Wrap the result as `DefiData.PoolSnapshot(snapshot)` and pass it to `BacktestEngine.add_defi_data` with the pool events. - Replay every pool event from the snapshot block forward. Starting after the snapshot block can leave the profiler stale. Cached block timestamps load into Nautilus data objects as UNIX nanoseconds. Cache rows written with second-resolution block timestamps are normalized to nanoseconds when snapshots and pool events are loaded, while nanosecond rows preserve their stored precision. ### Pool analysis constraints `analyze-pool(s)` validates its prerequisites and reports each pool independently. These boundaries also apply when the underlying analysis services are called from Rust. | Condition | Behavior | Constraint | | -------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | Missing pool metadata | Fails with `Pool
is not registered`. | Discover the pool first; analysis cannot infer or register metadata. | | Missing parser capability | Fails before sync. | Use a snapshot-capable combination from [Chain and DEX command support](#chain-and-dex-command-support). | | Non-checksummed address | Fails with `Blockchain address '
' has incorrect checksum`. | Supply an EIP-55 address; factory `getPool` results may need checksum conversion. | | Provider Multicall cap | An `out of gas`, payload, or timeout error aborts final-state hydration. | Lower `multicall_calls_per_rpc_request` or use a provider with higher limits. | | Missing historical state | A first bootstrap cannot read final state at an old `--to-block`. | Use a recent target or an archive-capable provider; see [RPC provider requirements](#rpc-provider-requirements). | | Restricted HyperSync quota | High-activity pools back off, and a full first sync can require thousands of requests. | Lower `--concurrency`, or use `--snapshot-from-rpc` when stored swap history is unnecessary. | | Mid-life `--from-block` | Omitting `Initialize` can leave the profiler without an initial price. | Sync from pool creation when generating a first snapshot. | | No liquidity events | `analyze-pool` errors; `analyze-pools` emits a failure result and continues other pools. | Select a pool with a Mint or Burn at or before the target block. | | Any per-pool failure | Emits `"status": "failure"` and makes `analyze-pool(s)` exit non-zero. | Use the exit code for the overall result and each JSON status for the per-pool result. | Final RPC hydration in `--snapshot-from-rpc` mode is authoritative for checkpoint state. A failed hydration aborts analysis rather than storing a snapshot with stale price state. ## Pool analysis operations ### Discover pools before analysis `analyze-pool(s)` reads pool metadata from the Postgres cache and fails with `Pool
is not registered` when the pool has not been discovered. Run `sync-dex` for the chain and DEX before analysis to populate the `pool` and `token` tables. ### Check command support Use [Chain and DEX command support](#chain-and-dex-command-support) or the command help before starting a sync. `sync-dex` and `analyze-pool(s)` reject unsupported chain and DEX combinations before querying events, rather than returning an empty result. ```bash ./target/debug/nautilus blockchain sync-dex --help ./target/debug/nautilus blockchain analyze-pools --help ``` ### Use checksummed pool addresses Pool addresses must use the EIP-55 checksum. A lowercase address fails with `Blockchain address '
' has incorrect checksum`. Convert discovered addresses to checksum form before passing `--address` or adding them to an addresses file. ### Reduce the Multicall request size An RPC provider can reject a large final-state Multicall with an out-of-gas, payload, or timeout error. Lower `--multicall-calls-per-rpc-request` from its default of `200` to keep each request within the provider's limits. ### Choose a target block the RPC provider can serve A first-time sync reads on-chain state at `--to-block`. Use a recent target with a non-archive RPC provider, or use a provider that serves contract state at the requested historical block. See [RPC provider requirements](#rpc-provider-requirements). ### Control HyperSync request volume A full first-time sync of a large or old pool can require thousands of requests. Lower `--concurrency` when the configured token has a restrictive quota. Use `--snapshot-from-rpc` when an exact checkpoint snapshot is sufficient and stored swap history is not required. ### Start an initial replay at pool creation Starting `--from-block` in the middle of a pool's history can omit its `Initialize` event. Without an initial price, snapshot bootstrap fails with `Pool is not initialized and it doesn't contain initial price, cannot bootstrap profiler`. Sync from pool creation when generating the first snapshot. ### Interpret pool failures A pool without processed Mint or Burn events at or before the target block can lack the state needed for a snapshot. `analyze-pool` returns the error. `analyze-pools` emits a JSON line with `"status": "failure"`, continues with the other pools, and exits non-zero after any per-pool failure. Use the process exit code for the overall result and each JSON status for individual results. ## Runbook: validate a live pool sync Use this procedure to check pool discovery, event parsing, and snapshot generation for one DEX on one chain. The example uses PancakeSwap V3 on Arbitrum. It performs read-only chain queries and writes only to the configured Postgres cache. ### Prerequisites - Docker is available for the local Postgres service. - `ENVIO_API_TOKEN` contains a valid HyperSync token. - `RPC_HTTP_URL` points to an Arbitrum RPC provider that can serve the target block. - `POOL_ADDRESS` contains an EIP-55 checksummed PancakeSwap V3 pool address. ### Start the local services and build the CLI ```bash make init-services cargo build -p nautilus-cli --features defi --bin nautilus ``` ### Discover pools Run discovery immediately before analysis when the local database has been reset: ```bash ./target/debug/nautilus blockchain sync-dex \ --chain arbitrum \ --dex PancakeSwapV3 \ --rpc-url "$RPC_HTTP_URL" \ --host 127.0.0.1 \ --port 5432 \ --username nautilus \ --password pass \ --database nautilus ``` ### Analyze the pool Keep concurrency at one for this validation run: ```bash ./target/debug/nautilus blockchain analyze-pools \ --chain arbitrum \ --dex PancakeSwapV3 \ --address "$POOL_ADDRESS" \ --rpc-url "$RPC_HTTP_URL" \ --host 127.0.0.1 \ --port 5432 \ --username nautilus \ --password pass \ --database nautilus \ --concurrency 1 ``` ### Check the stored data Count the rows written for the pool in: - `pool_swap_event` - `pool_liquidity_event` - `pool_collect_event` - `pool_flash_event` - `pool_fee_protocol_update_event` - `pool_fee_protocol_collect_event` - `pool_snapshot` - `pool_position` - `pool_tick` Fee-protocol tables remain empty when the synced range contains no `SetFeeProtocol` or `CollectProtocol` events. After a local database reset, rerun discovery before analysis so the pool row exists. See [Pool analysis operations](#pool-analysis-operations) for address, provider, replay-range, and request-volume failures. ## Contracts ### Base contract and Multicall3 `BaseContract` batches contract calls through Multicall3 (`0xcA11bde05977b3631167028862bE2a173976CA11`): - Multicall uses `tryAggregate(requireSuccess: false)`, so each result reports its own success or failure and the contract wrapper decides whether to reject it. - Reads execute against a single block context. - Transport and provider failures surface as RPC errors. ### ERC-20 metadata `Erc20Contract` reads `name`, `symbol`, and `decimals` through Multicall. The adapter can skip pools whose token metadata is malformed, raw bytes, or empty. ### Uniswap V3 pools `UniswapV3PoolContract` reads global pool state, active ticks, and positions. - Large pools can exceed provider payload, gas, or timeout limits. - RPC-snapshot hydration fails closed if the final-state read fails. - Very large pools may need a lower `multicall_calls_per_rpc_request` or a stronger provider. PancakeSwap V3 reuses the Uniswap V3 read contract because `slot0`, `ticks`, `positions`, `liquidity`, and fee-growth reads share the same ABI. Fee-protocol encoding differs: - Uniswap V3 packs two 4-bit fee denominators into one `uint8`. - PancakeSwap V3 stores two 16-bit basis-point shares in `slot0.feeProtocol` and emits `SetFeeProtocol(uint32,uint32,uint32,uint32)`. - PancakeSwap V3 snapshots store `fee_protocol0_basis_points` and `fee_protocol1_basis_points`, and replay computes protocol fees as `fee * basis_points / 10000`. ## Execution :::warning The execution client is not production-ready. `BlockchainExecutionClient` implements preflight, explicit WETH wrap and ERC-20 approval, local EIP-1559 signing, durable reconciliation, and one Uniswap V3 swap flow. Other order operations fail closed with no on-chain or durable side effects. ::: Execution uses these terms throughout this section: | Term | Meaning | | ------------------- | ------------------------------------------------------------------------------------------------------------- | | Decision height | The minimum fresh head accepted across all three RPC sources for one authorizing read set. | | Verification source | One authoritative endpoint or one of exactly two read-only verifiers in a distinct configured failure domain. | | Deployment manifest | Reviewed contracts, code hashes, proxy bindings, identities, pools, tokens, and permitted call edges. | | Intent | One durable logical wrap, approve, or swap operation, independent of its transaction-hash history. | | Signer ownership | Exclusive control of the wallet signer and, after assignment, its active nonce. | | Finalized boundary | The point where all sources agree on finalized ancestry through the transaction's inclusion block. | ### Connection and account state Client construction requires one authoritative RPC endpoint and exactly two read-only verification providers. Each source needs a distinct endpoint, provider ID, operator ID, and pairwise-disjoint set of failure-domain IDs. #### Enforced operating conditions - The authoritative execution endpoint and both verifier endpoints must use HTTPS. Cleartext HTTP is accepted only for a canonical IPv4 loopback literal in `127.0.0.0/8` or exactly `[::1]`. Hostnames, IPv4-mapped IPv6, private and link-local addresses, and noncanonical numeric forms do not qualify. - HyperSync uses the same HTTPS rule. This validation runs before the HyperSync token is loaded or its client is created. - Blockchain HTTP clients reject redirects. A canonical loopback RPC connection also bypasses configured and ambient proxies. Remote HTTPS execution RPC clients continue to honor ambient proxy environment variables. - A Postgres-backed execution connection requires an active payload key, a stable deployment ID, ready protected storage, and every key referenced by stored envelopes. It authenticates every retained payload before loading the signer or making an execution RPC call. - An attached Postgres database can be unprotected only while the client is disconnected for checks, protection, or rollback work. Rewrap requires protected storage. An unprotected database cannot provide execution capability. #### Operator assumptions A failure domain represents any shared upstream, reseller, gateway, proxy, account, network path, or hosting control plane. Distinct URLs and distinct configured identities do not prove operational independence. The operator must verify that the three providers do not share a control or failure domain. The operator must also identify and monitor every party or governance mechanism that can change a manifest-pinned deployment's code or a manifest-pinned proxy's implementation. If such a deployment change or proxy upgrade is announced or suspected, stop execution and revoke every outstanding allowance whose spender is a router address in the affected deployment. Complete the revocations before the changed code or implementation is present at the decision block used for signing. Once it is, the pre-sign deployment check fails closed for every client transaction, including revocation. The operator must keep the signing key exclusive to one live client and control access to the host environment, database, replicas, backups, and exports. Connect completes these checks before it loads the signer: 1. Open the durable execution store. When Postgres is configured, require ready protected storage and authenticate every retained payload before loading any existing verification ledger. 1. Require all three sources to match the local chain ID and reviewed finalized checkpoint. 1. Extend or recheck the durable finalized-header ancestry in windows of at most 4,096 blocks. 1. Require an exact finalized-height signer nonce from all three sources. 1. Verify the reviewed deployment manifest at the finalized height. This includes runtime code, proxy slots, implementations, router and factory relationships, pool identity, token decimals, and the pinned quote contract. 1. Probe archive, finalized-tag, explicit-height state and call, gas, storage, quote, and call-trace capabilities on every source. 1. Atomically install the verification ledger or migrate retained execution history with the evidence that authorized each classification. 1. Load the private key from `signer_private_key_env`, require its address to equal `wallet_address`, and reconcile any active intent. 1. Read the native balance and configured ERC-20 balances, install the complete wallet snapshot, and publish one `AccountState` under the configured account ID. Without Postgres, the client can connect and publish balances, but all transaction operations are refused. A verification, migration, reconciliation, balance, or exact amount conversion failure keeps the client disconnected. Any loaded signer is removed, the previous complete snapshot stays installed, and no partial wallet state is published. Duplicate token symbols also reject the snapshot because symbols define currency identity. The verification providers never receive signed transaction bytes and have no broadcast method. The authoritative endpoint alone receives `eth_sendRawTransaction`. Security-critical unsigned reads that authorize a signature, rebroadcast, or durable transition go to all three sources. Diagnostic preflight and connect-time balance publication use the authoritative endpoint and cannot authorize execution. This protects integrity, not order confidentiality. Operators who need route or amount confidentiality need a separate execution design. Published balances use `total = free` and `locked = 0`. The wallet account applies local reservations when it derives effective free and locked balances, as described in [Wallet accounts](../concepts/accounting.md#wallet-accounts). After the client starts, `QueryAccount` republishes the installed snapshot without another RPC read. It fails when: - The requested account ID differs from the client account ID. - The client has not started. - No complete snapshot exists. Disconnect removes the signer and aborts in-flight submission tasks. Transaction operations reject a disconnected client before any execution RPC call. ### Supported order slice The client accepts one market-order shape: | Axis | Accepted | Rejected | | ----------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | Chain | The chain configured on the execution client. | An instrument venue for another chain. | | DEX | Uniswap V3. | Every other DEX, including PancakeSwap V3. | | Pool | An address-based pool in `Cache::pool` with a fee tier. | Unknown pools, V4 pool IDs, and pools without a fee tier. | | Order | A single `MarketOrder` with side `BUY` or `SELL`. | Non-market orders submitted through `SubmitOrder`. | | Quantity | Base-denominated size within `max_order_amount`; a BUY also needs a matching quote-spend limit. | Quote-denominated input or an amount above either applicable ceiling. | | Orientation | Tokens with distinct model priorities. | A pair whose tokens have equal priority and are ambiguous. | The `InstrumentId` selects the pool, for example `0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV3`. Its venue must parse as `:`, and its symbol must parse as an address `PoolIdentifier`. `Pool::get_base_token` and `Pool::get_quote_token` apply the model's token-priority convention: stablecoins are quote assets, wrapped native assets have the next priority, and other tokens become base assets against them. Equal `Token::get_token_priority` values are ambiguous and reject the order. Venue routing admits Uniswap V3 on any configured chain whose venue matches. Swap preparation also requires a registered Uniswap V3 deployment and factory for that chain. Order lists deny each open order with `OrderDenied`; modify, cancel, and batch-cancel commands reject each referenced cached order with `OrderModifyRejected` or `OrderCancelRejected`; cancel-all commands and order queries log a warning without an event. Mass status returns `Ok(None)` so startup reconciliation logs and continues. Order, fill, and position report probes return an error so LiveNode does not treat an empty answer as absence. These paths never sign, broadcast, or persist an intent. A swap stays `Submitted` until finality, and venue status queries cannot resolve it. Set `inflight_check_interval_ms = 0` and leave open-order checks off. The engine's default in-flight timeout would otherwise reject a live swap. Execution routing follows Nautilus's multi-venue broker pattern because the client represents a wallet and RPC connection for one chain while each instrument venue identifies both its chain and DEX. A strategy may select the client explicitly through `client_id`; node configuration may instead register the client for instrument venues through `RoutingConfig.venues` or use it as the default execution client. After client selection, `ExecutionClient::handles_order_venue` accepts only a venue whose parsed chain matches the client configuration and whose DEX is supported by the client. The instrument retains its `:` venue rather than being rewritten to `BLOCKCHAIN`. The order maps to a single `exactInputSingle` call on the original Uniswap SwapRouter (the deployment whose signature carries a deadline). `allowed_token_pairs` is directional `(token_in, token_out)`: a SELL requires the base-to-quote pair, and a BUY requires the quote-to-base pair. Listing only one direction does not admit the other. | Parameter | Source | | ------------------- | ------------------------------------------------------------------------------------------ | | `tokenIn` | SELL: pool base token. BUY: pool quote token. | | `tokenOut` | SELL: pool quote token. BUY: pool base token. | | `fee` | Pool fee tier. | | `recipient` | Execution wallet address. | | `deadline` | Verified decision-header timestamp plus configured `deadline_seconds`. | | `amountIn` | SELL: `Quantity` as raw base units. BUY: quote input from the verified exact-output quote. | | `amountOutMinimum` | Derived from the verified quote at the decision height (see below). | | `sqrtPriceLimitX96` | `0` (slippage is bounded by `amountOutMinimum`). | ### BUY quote-spend limits Every BUY needs one `quote_spend_limits` entry for its directed quote-to-base pair. The entry repeats the quote-token address and decimals beside `max_amount`, a base-10 string in the token's raw units. Client construction rejects a second entry for the same directed pair, a `spend_token` that differs from `token_in`, a `max_amount` that is not a base-10 unsigned integer within the `U256` range, and pairs outside `allowed_token_pairs`. Order preparation also checks the configured token and decimals against the selected pool (see [Execution configuration](#execution-configuration) for an example entry). The client compares the independently verified exact-output quote's `amountIn` with this limit before signing. Equality is accepted; a quote one raw unit above the limit is denied. `max_order_amount` remains a separate ceiling on the submitted base quantity, and SELL orders do not use `quote_spend_limits`. ### Slippage protection `amountOutMinimum` is always derived, never caller-supplied: 1. Require an initialized `PoolProfiler` with a processed event watermark in the shared engine cache (`Cache::pool_profiler`). Its local simulation must consume the full SELL input or produce a nonzero BUY input, but its amount does not set a signed field. A live data-side subscription normally maintains this state. 1. Choose a decision height from the minimum fresh head reported by the three sources. The head skew must remain within `verification.chain_anchor.max_head_skew_blocks`. 1. Require the profiler watermark to include the block hash observed during ingestion. All three sources must return that exact explicit-height header. A block-scoped snapshot must also carry the header hash as its snapshot identifier. 1. For an event watermark, require a successful canonical receipt whose transaction, block, and index metadata match the profiler position. The selected log must come from the expected pool and use a supported pool-event signature. 1. Verify one unanimous parent-linked ancestry from the profiler height through the decision height. The distance must not exceed `max_quote_age_blocks`, which must be in `1..=4095`. 1. Call the manifest-pinned `IQuoterV2` contract at the decision height through all three sources. SELL uses `quoteExactInputSingle`; BUY uses `quoteExactOutputSingle`. The full decoded result must agree, including amount, resulting square-root price, initialized ticks crossed, and gas estimate. 1. Immediately before signing, reread the checkpoint, profiler header, decision header, ancestry, and quote. An unavailable or changed result blocks signing. 1. For SELL, compute `amountOutMinimum` from the verified exact-input output. For BUY, use the verified exact-output input as `amountIn` and derive `amountOutMinimum` from the requested base output. Integer arithmetic rejects a zero minimum. Profiler divergence can request a data refresh, but it cannot override a verified quote or weaken the signed limits. The slippage comes from the `slippage_bps` configuration field, overridable per order through a `slippage_bps` entry in the submit command's `params`; an override above the `max_slippage_bps` ceiling is rejected before signing. Pre-upgrade event rows can lack an ingestion block hash because the schema migration does not backfill one. Such rows cannot authorize execution. Refresh the traded pool through the normal live data subscription, or resync its events and rebuild its snapshot, before submitting an order. ### Preflight, wrapping, and approval Preflight, WETH wrapping, and router approval are explicit operations on the client, separate from `submit_order`: | Operation | State change | Pre-broadcast checks | Completion check | | --------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | Preflight | None. | Authoritative chain, deployed-code, balance, allowance, and current-fee diagnostics. | Returns a structured, sanitized report. | | Wrap | Calls WETH `deposit()` with value. | Verified decision ancestry, deployment, WETH balance, native balance, gas, fee, nonce, and explicit-height simulation. | WETH balance increased by the exact wrapped amount. | | Approve | Calls `approve(router, amount)`. | Wrap checks plus router policy, factory and WETH identity, input-token membership, zero allowance, and approval simulation. | Allowance at the inclusion block equals the target. | Preflight resolves the pool from `Cache::pool`. Its report contains no RPC URL, private key, or raw signed transaction. It reports the expected and observed chain IDs, pool, router and token code checks, token balances and allowances, native balance, base and priority fees, the derived maximum fee, whether the fee stays within its ceiling, overall readiness, and every failed check. Approve rejects a standard `false` return and accepts tokens that return no data. A nonzero approval is limited to configured input tokens and requires the existing allowance to be zero. With `unlimited_approval`, every nonzero request targets `U256::MAX`. The final allowance must equal the target exactly. A zero request revokes an allowlisted router even when router deployment metadata is unavailable, so a broken router check cannot prevent revocation. During an uninterrupted call, wrap and approve use the shared EIP-1559 path, persist the intent and signed hash before broadcast, and return after stable finality and the operation's postcondition. Wrap compares the WETH balance immediately before and at the inclusion block, which avoids a stale pre-broadcast baseline. A failed postcondition returns an error after finality, so the transaction may still have changed on-chain state. Before signing a swap, order submission requires exact agreement from all three sources for: - The decision header and parent-linked ancestry from the durable finalized ledger. - The deployment manifest at the decision height, including every configured code hash, proxy binding, and role probe. - The router reports the registered factory and configured WETH, and the factory resolves the exact pool for the input token, output token, and fee tier. - Both tokens report the decimals stored in the reviewed manifest. - The manifest-pinned quote contract returns one exact quote. - Router allowance and input-token balance sufficient for the raw input amount. - Native balance sufficient for transaction value plus the maximum gas cost. - Canonical and pending nonce observations that agree with the durable nonce ledger. - The maximum gas estimate, median priority fee, and local gas and fee ceilings. The decision header supplies the deadline, quote-age boundary, durable `created_block`, and EIP-1559 base fee. State, call, code, storage, gas, balance, and allowance reads use its explicit block number. Immediately before local signing, the client repeats the chain, header, ancestry, deployment, quote, canonical nonce, and pending nonce checks. It persists this evidence atomically with nonce assignment. A failure before signing produces `OrderDenied` and no broadcast. The client releases its preparation slot only after the durable recoverable transition succeeds; a failed transition keeps ownership for reconciliation. The input token is the base token for a SELL and the quote token for a BUY. Preflight readiness still reports the base-token allowance used by SELL setup. A BUY needs a separate quote-token approval; submission denies the order if that allowance or balance is short. Submission never wraps or approves. An insufficient allowance or balance emits `OrderDenied`. ### Transaction signing and broadcast #### Local signing The client builds and signs EIP-1559 typed transactions locally with Alloy: - It builds `alloy::consensus::TxEip1559` with the chain ID, nonce, gas, fees, destination, value, and calldata. - It signs `SignableTransaction::signature_hash()` with `alloy::signers::local::PrivateKeySigner`, producing `Signed`. - It encodes the EIP-2718 envelope with `alloy::eips::eip2718::Encodable2718::encoded_2718()` and sends the raw bytes through `eth_sendRawTransaction`. The private key comes from the environment variable named by `signer_private_key_env`. It is never logged, serialized, or stored in configuration. Zeroizing buffers hold the temporary key text and decoded bytes while the signer is constructed. The client supports one signer, whose derived address must match `wallet_address` at connect. #### Signer and nonce ownership At most one transaction can be in flight across wraps, approvals, and swaps: - The client claims the local slot before the first preparation RPC call. - The durable canonical nonce comes from unanimous explicit finalized-height reads. Pending nonce is an additional mempool observation and never proves canonical consumption. - A new signature requires canonical nonce `N`, no unexplained pending use, and an intent that can atomically own `N` with its verification evidence. - A preparation failure releases the slot only when no signature exists. - After signing, the slot stays claimed through persistence, broadcast, finality, and required order-event persistence. - A persistence error keeps the slot claimed because Postgres may have committed before the client lost the acknowledgement. - Cancelling an operation during persistence or broadcast does not release the slot and admit a new transaction. Fee and gas policy also runs before signing: - All paths use the unanimous decision header's base fee. Three priority-fee values select the median before `base_fee_buffer_bps` is applied. The client rejects a derived fee above `max_fee_per_gas_wei`. - All three sources estimate the exact unsigned transaction at the decision height. The client selects the maximum estimate, applies `gas_buffer_bps`, and rejects a result above `gas_limit`; it does not clamp the estimate. #### Persist before broadcast The client reserves a durable intent before it assigns a nonce or signs. It then stores the nonce, an authenticated signed-payload envelope, and the local hash before broadcast. A transaction cannot be submitted without a ready protected durable store. Immediately before sending, the client records the `broadcast` transition. Any outcome after that write, including a node rejection, is treated as uncertain until canonical nonce and receipt observation resolves it. A signed intent without a durable broadcast transition remains active and blocks connect pending explicit recovery. The adapter has no automated recovery command or client method for that state. An operator must inspect the durable `execution_intent` and `execution_transaction_hash` records and make an explicit, reviewed recovery decision; the adapter does not release the signer slot or resend the transaction automatically. A durable `broadcast` intent may resend only its exact persisted bytes before observation resumes. Broadcast and receipt handling follow these rules: - Each execution JSON-RPC request has a 10-second timeout. Errors omit the endpoint URL, request payload, and signed bytes. - `already known` counts as acceptance. - A timeout, reset, node rejection, unreadable response, or returned hash that differs from the signed hash enters reconciliation under the persisted intent. - Three null receipts are retryable. Partial propagation is retryable. Conflicting present receipts are disagreement and cannot authorize a state change. - Receipt observation retries transient RPC errors within the configured finality poll window. - Poll exhaustion records `dropped` and leaves the signer slot occupied. - Exact-byte rebroadcast uses only the authenticated retained envelope. Before sending it again, all three sources must verify the chain, ancestry, deployment, canonical and pending nonce, receipt absence, and purpose-specific explicit-height simulation. Only the authoritative source receives the bytes. ### Risk and validation boundaries Generic pre-trade risk stays in the engine. Venue-specific gates live in the adapter as a configuration-driven limiter: | Check | Boundary | Enforcement | | --------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- | | Chain identity | Adapter | Reviewed checkpoint, three-source chain ID, fresh headers, and parent-linked ancestry | | Deployment identity | Adapter + risk | Reviewed code hashes, proxy bindings, role probes, pool identity, and inclusion call graph | | Token-pair allowlist | Risk (adapter) | Directional pairs for swaps; input-token membership for nonzero approvals | | Order amount | Risk (adapter) | `max_order_amount` on submitted base quantity; pair-specific `quote_spend_limits` on verified BUY quote input | | Quote provenance | Adapter | Canonical profiler watermark, bounded ancestry, pinned QuoterV2 result, and final pre-signature recheck | | Gas and fee | Risk (adapter) | Maximum three-source gas estimate, median priority fee, and local `gas_limit` and `max_fee_per_gas_wei` ceilings | | Balance sufficiency | Adapter + risk | Explicit-height three-source input-token and native balance checks | | Allowance sufficiency | Adapter | Explicit-height three-source router allowance checks | | Slippage | Risk (adapter) | `max_slippage_bps` ceiling and verified quote-derived minimum output | | In-flight limit | Adapter + DB | Local slot plus durable canonical nonce and signer ownership | Every limiter rejection refuses the order before signing and reports a structured reason. ### Order events Order submission emits only events justified by known transaction state: | Observation | Event | Result | | ---------------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Failure before the persisted broadcast transition. | `OrderDenied` | No transaction left the client. | | Persisted broadcast attempt, including an ambiguous reply. | `OrderSubmitted` | The signed intent remains the observation authority. | | Verified finalized revert. | `OrderRejected` | The stable event ID and terminal marker permit signer release. | | Finalized transaction, trace, or deployment mismatch. | No terminal event | The client refuses to derive a fill and keeps signer ownership quarantined. | | Verified finalized success with exact transaction and log. | `OrderFilled` | Wallet refresh and marker persistence then complete the intent. A BUY that executes below the order quantity also emits `OrderCanceled` for the remainder. A BUY that executes above the order quantity reports the full output. | | Timeout, disagreement, unavailable read, or reorg. | No terminal event | The order stays submitted and signer ownership remains occupied. | A successful receipt at first inclusion does not emit a fill. The client waits for the stable finalized boundary described below. ### Persistence and reconciliation #### Durable record model Execution schema version 2 separates the logical wallet operation from its physical transaction hashes. Verification schema version 2 adds the canonical nonce, finalized header, decision evidence, and replacement-scan ledgers: | Record | Represents | Recovery use | | ----------------- | ------------------------------------------------------- | ------------------------------------------------------------------- | | Intent | One logical `wrap`, `approve`, or `swap` operation. | Owns signer, nonce, call fields, order identity, and markers. | | Hash history | Authenticated signed envelopes and their hashes. | Selects the current hash and stores receipt observations. | | Transition | Append-only status history for an intent and hash. | Records observations; recovery reads current intent and hash state. | | Canonical nonce | Next signer nonce proven at a finalized height. | Prevents pending mempool state from releasing or skipping a nonce. | | Finalized headers | Parent-linked headers from the reviewed checkpoint. | Resumes ancestry and bounded replacement scans. | | Decision evidence | Sanitized results for each authorizing verification. | Proves which read class authorized a durable transition. | | Scan cursor | Last fully scanned finalized replacement-search height. | Resumes multi-window scans and rescans the unfinalized tail. | `purpose` is an adapter-local execution field with the values `wrap`, `approve`, and `swap`. It tells reconciliation whether the intent also owns a Nautilus order lifecycle. It is not a field in the generic Nautilus order or `SubmitOrder` specification. The schema keeps the legacy execution transaction table. Its connect-time migration: - Takes an exclusive lock on the legacy table. - Refuses unresolved legacy rows that cannot be mapped safely. - Fences legacy writes after schema version 2 activates. - Preserves existing data. The first connection after enabling independent verification also classifies every retained execution intent. It authenticates every retained payload before remote reconstruction. An unsigned active `prepared` intent becomes inactive `recoverable`. A signed active intent at the canonical nonce remains active for reconciliation. A consumed nonce requires archive proof of its receipt, finalized ancestry, full transaction identity, call trace, deployment identity, and terminal status. Released history must already have a consistent marker and must not retain signed ownership. Duplicate nonce ownership, missing archive state, a payload mismatch, or changed history blocks migration before the signer loads. Partial unique indexes enforce one active intent per signer, one active owner per signer and nonce, and one intent per client order. An intent also stores separate acknowledgement, fill, and terminal event markers. A finalized or reverted intent remains active until its fill or terminal marker is durable. #### States | State | Detection or transition | Ownership and event effect | | ------------- | ------------------------------------------------------------------- | --------------------------------------------------------------- | | `prepared` | Intent reserved before nonce assignment. | Owns the signer; no transaction exists. | | `signed` | Nonce assigned; any completed signature is stored before broadcast. | Owns the signer and nonce. | | `broadcast` | Broadcast attempt persisted before send. | A swap can record its `OrderSubmitted` marker. | | `included` | Receipt block hash matches the canonical numbered block. | Nonterminal; no fill. | | `replaced` | Another canonical hash consumed the signer nonce. | The replacement joins the original intent. | | `reorged` | Receipt disappears or its block hash stops matching. | Observation resumes; no terminal event. | | `dropped` | No stable finalized receipt within the poll window. | Remains active and blocks new signing. | | `finalized` | Successful receipt reaches a stable finalized boundary. | Stays active until a fill or terminal marker. | | `reverted` | Failed receipt reaches a stable finalized boundary. | Terminal marker releases ownership; swap emits `OrderRejected`. | | `recoverable` | Preparation fails, or restart finds an unsigned `prepared` intent. | Becomes inactive because no signature exists. | #### Restart and replacement On connect, the client reloads the active signer intent before enabling new signing: - An unsigned `prepared` intent becomes `recoverable` and inactive. - A `signed` intent remains active and keeps its nonce reserved. Connect fails until an explicit recovery decision is available. - A durable `broadcast` intent may resend only the exact authenticated stored bytes, and only after the three-source rebroadcast checks pass. Later states restore the local in-flight slot and observe the current hash without another send. - A legacy `recoverable` intent that still has signed bytes also fails connect rather than releasing its nonce. - A restored wrap or approve revalidates destination, calldata, and value, including a same-nonce replacement, then reruns its live postcondition before reporting success. - A swap also requires its order, instrument, and pool to be restored in the engine cache. Missing or inconsistent state fails connect. When no receipt exists and the verified canonical signer nonce has advanced, the client scans unanimous canonical full blocks from the intent's creation height. Each attempt covers at most 4,096 blocks. A finalized cursor commits with its verification evidence; the next attempt resumes there and rescans the unfinalized tail. A same-nonce transaction can attach only when its hash and full decoded identity match an authenticated retained envelope for that intent. An unknown or mismatched replacement remains quarantined, emits no order rejection, and does not release signer ownership. A disappearing receipt or changed canonical block records `reorged` and resumes observation. Poll timeout records `dropped` and keeps the signer slot occupied for the next connect attempt. :::warning Keep the signing key exclusive to this client while an intent is active. On restart, a restored wrap or approve must match the persisted destination, calldata, and value, including a same-nonce replacement. The wrap then rereads WETH balances at the inclusion block and the previous block; the approve rereads router allowance at the inclusion block. A call-identity mismatch or a failed postcondition keeps the intent active, occupies the in-flight signer slot, and fails connect, including on a later process. A mismatched swap emits no terminal event and remains active. ::: #### Finality and fills Finality uses each source's `finalized` block tag through `eth_getBlockByNumber`, not a confirmation count. The client: 1. Requires three identical non-null normalized receipts. 1. Matches the receipt block hash to one unanimous explicit inclusion header. 1. Waits until the minimum verified finalized height reaches the inclusion height. 1. Extends the unanimous parent-linked finalized ancestry. 1. Requires the full transaction to match the authenticated signed envelope. 1. Requires three identical `debug_traceTransaction` call trees and admits each internal call only when its purpose, caller, target, and call type match one manifest edge exactly. Contract creation and self-destruction are denied. 1. Rechecks the deployment manifest at inclusion, then commits finality evidence, receipt state, header ancestry, and canonical nonce advancement in one database transaction. After on-chain execution, deployment or call-trace drift detected by the final-inclusion checks leaves signer ownership quarantined. Those checks cannot reverse on-chain effects from a contract upgrade after the decision block used by the final pre-sign check, including an upgrade after that check completes and before transaction execution. All three RPC sources must support the `finalized` tag. An unsupported tag fails reconciliation closed. For a successful swap, the full finalized transaction must match the persisted signer, nonce, destination, calldata, and value. The receipt must contain exactly one `Swap` log from the selected pool. A SELL requires the log's positive base input to equal the persisted amount. A BUY requires the log's positive quote input to equal the persisted amount and a negative base output. Existing Uniswap V3 parsing derives the executed amount. A BUY fill price is the quote spent divided by the emitted last quantity. The fill contains: - The original order quantity for a SELL. For a BUY, the executed base output converted at `FIXED_PRECISION`. A BUY can fill more than the submitted quantity when the pool price improves; set `allow_overfills = true` on the live execution engine so that fill is applied. - The average fill price. For a BUY, quote spent divided by the emitted last quantity. - The transaction hash as venue order ID. - A deterministic trade ID derived from the transaction hash and log index. - `effectiveGasPrice * gasUsed` as native-currency commission. Before emitting the fill, the client verifies native and tracked-token state at the finalized inclusion height through all three sources. It then publishes the stable-ID order event and wallet account state, stores the fill marker, and releases signer ownership. A wallet refresh or event dispatch failure keeps the finalized intent active for reconciliation. #### Event delivery across restarts Reconciliation checks persisted event markers and restored order state before it emits a repeated order event. Terminal event IDs are deterministic from the transaction hash and event kind, and trade IDs are deterministic from the transaction hash and log index. These identities suppress duplicates once the corresponding state is durable and let downstream consumers deduplicate a retry after a crash. Event publication and marker persistence are separate operations. A process crash between them can therefore cause an event to be delivered again after restart. Consumers must handle order events idempotently; this adapter does not provide an atomic exactly-once delivery guarantee. ### Signed transaction storage Postgres-backed execution requires protected signed-transaction storage. Protection seals every signed EIP-2718 transaction with AES-256-GCM and clears its live plaintext column. The authenticated context binds the exact signed bytes to the deployment, chain, signer, intent, signer nonce, and transaction hash. Storage maintenance uses direct Rust methods on a disconnected `BlockchainExecutionClient`; Python and the CLI do not expose them. Connect never activates, resumes, or repairs protection implicitly. Every Postgres-backed connect requires ready protected state and authenticates every retained payload before loading the signer. Protected storage has these key and deployment constraints: - Supply every payload sealing key as the hexadecimal encoding of exactly 32 raw bytes, with an optional `0x` prefix. The operator must generate those bytes with a cryptographically secure pseudorandom number generator (CSPRNG) and use a unique raw key for each independently sealing database. Keep key values out of configuration, logs, shell history, and process arguments. - Keep `payload_deployment_id` stable for the life of the protected database. A changed deployment ID makes existing envelopes unreadable by design. - List every old key variable in `payload_key_retired_env` until no stored envelope references it. Retired keys can open existing envelopes but never seal new ones. - Keep the complete key set available on every connect. A missing active or retired key, malformed envelope, failed authentication tag, durable-context mismatch, or unexpected plaintext row fails closed. Protected storage never falls back to `raw_transaction`. - Treat `2^32` seals as one lifetime budget for a raw key, not a limit that resets per database. Rotate the active key before aggregate use reaches that ceiling. With the client disconnected, configure the replacement as active, retain the old key as retired, and run the Rust rewrap method before the next connect. A restored copy that becomes write-active with the original raw key shares the same budget. Each database reserves and counts its own seals, including migration and rewrap work, and rejects further local use when its counter exhausts the numeric ceiling, but database-local counters cannot enforce the aggregate after copies diverge. The operator must count shared pre-restore history once and every later seal from each copy once. | Method | Requirement | Result | | -------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | `protect_payload_storage()` | Active key, stable deployment ID, and disconnected clients. | Records the marker first, then seals plaintext rows in bounded, resumable batches. | | `check_payload_storage(batch_size)` | Disconnected client and every referenced key. | Authenticates a stable snapshot without returning payload bytes and reports row, key-ID, and role counts. | | `rewrap_payload_storage(batch_size)` | Protected storage, new active key, and all old keys retained. | Rewraps in bounded, resumable batches; a full check must pass before an old key is removed. | | `rollback_payload_storage(batch_size)` | Incident recovery, disconnected clients, and every key. | Restores and verifies exact plaintext, clears envelopes, and removes the protection marker last. | Run the full check after protection, restore, rewrap, or rollback. Its role report includes direct table owners and roles with `SELECT` grants. The full report also contains protected status, deployment ID, plaintext, original, replacement, and authenticated row counts, and referenced key IDs. Superuser and inherited privileges still require a server-level review. Rewrap changes one database copy but cannot revoke a transaction or envelope copied before rewrap. Rollback leaves execution unavailable until storage is protected and passes a full protected check again, and it does not remove signed bytes from WAL, replicas, backups, snapshots, or earlier exports. :::warning Signed transaction bytes remain bearer capabilities until their signer nonce is consumed. Protected storage covers live database payloads; it does not cover bytes before persistence, process memory, dead tuples, WAL, point-in-time recovery archives, replicas, backups, snapshots, restores, or operational exports. PostgreSQL statement or bind-parameter logging can also capture plaintext in the default mode and during rollback. Debug output, operational checks, and execution RPC errors do not expose the bytes. A restored protected database requires its original `payload_deployment_id` and complete key inventory. Each database enforces signer and nonce ownership independently, so never run a restored copy against a signer used by another live deployment. Do not point two copies with the same deployment ID at the same signer. ::: ### Execution configuration `BlockchainExecutionClientConfig` follows the `BlockchainDataClientConfig` pattern and exposes these fields to Python: | Field | Default | Description | | -------------------------------- | --------- | ------------------------------------------------------------------------ | | `client_id` | Required | Account ID for the client. | | `chain` | Required | Blockchain chain configuration. | | `wallet_address` | Required | Wallet address for the execution client. | | `http_rpc_url` | Required | Sole authoritative RPC endpoint and broadcast destination. | | `verification` | Required | Two read-only providers, local chain anchor, and deployment manifest. | | `signer_private_key_env` | Required | Environment variable that holds the signer key. | | `payload_key_env` | `None` | Active 32-byte key variable; required with Postgres execution. | | `payload_key_retired_env` | `[]` | Environment variables for old keys that may only open envelopes. | | `payload_deployment_id` | `None` | Stable database identity; required with Postgres execution. | | `router_addresses` | Required | SwapRouter allowlist; at least one address is required. | | `max_fee_per_gas_wei` | Required | Maximum derived fee per gas in wei. | | `base_fee_buffer_bps` | Required | Buffer over the unanimous decision-header base fee. | | `gas_limit` | Required | Gas ceiling; a higher buffered estimate is rejected. | | `gas_buffer_bps` | Required | Buffer applied over `eth_estimateGas`. | | `unlimited_approval` | `false` | Request unlimited approval instead of the exact amount. | | `weth_address` | Required | Wrapped native token used by `wrap`. | | `allowed_token_pairs` | Required | Directional input/output pairs; BUY needs the reverse pair. | | `quote_spend_limits` | `None` | Directed quote-token ceilings; a BUY without a matching entry is denied. | | `slippage_bps` | Required | Default slippage used to derive the minimum output. | | `max_slippage_bps` | Required | Ceiling for a per-order slippage override. | | `max_order_amount` | Required | `u64` ceiling on submitted base quantity, in raw base-token units. | | `deadline_seconds` | Required | Swap deadline offset from the verified decision-header timestamp. | | `max_quote_age_blocks` | Required | Maximum profiler-to-decision ancestry distance, in blocks. | | `receipt_timeout_secs` | Required | Deadline for the receipt and finality polling loop. | | `tokens` | `None` | ERC-20 addresses read and published when the client connects. | | `rpc_requests_per_second` | `None` | Per-client HTTP RPC rate limit used by all three sources. | | `postgres_cache_database_config` | `None` | Durable execution store; transaction submission requires it. | | `transport_backend` | `Sockudo` | Compatibility field; unused by the execution client. | The adapter enforces these constraints during configuration validation, client setup, connection, or transaction authorization: | Constraint | Rule | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | Transaction limits | `allowed_token_pairs`, both slippage fields, `max_order_amount`, `deadline_seconds`, `max_quote_age_blocks`, and `receipt_timeout_secs` must all be set. | | Slippage | `slippage_bps` must not exceed `max_slippage_bps`, and `max_slippage_bps` must be below 10,000. | | Quote age | `max_quote_age_blocks` must be in `1..=4095`. | | Routers | `router_addresses` must contain at least one valid address. | | Verification topology | The configuration must contain exactly two verifiers, with distinct provider, operator, endpoint, and failure-domain identities across all three sources. | | Remote transport | Remote execution, verifier, and HyperSync endpoints must use HTTPS; only canonical loopback literals may use HTTP. | | Durable execution | Transaction operations require Postgres, an active payload key, a stable deployment ID, and ready protected storage. | The first allowlisted router executes swaps, so preflight readiness requires allowance on that router. `receipt_timeout_secs` controls the polling deadline for swaps, wraps, and approvals. It is not a strict upper bound on the full call because final RPC and persistence operations can add time. `BlockchainVerificationConfig` contains: | Field | Requirement | | --------------------- | ------------------------------------------------------------------------------------------------- | | `authoritative` | Stable identity for `http_rpc_url`; it has no second URL in this object. | | `verifiers` | Exactly two `BlockchainVerificationProviderConfig` values with read-only HTTP URLs. | | `chain_anchor` | Chain ID and name, finalized checkpoint height/hash/timestamp, and nonzero head freshness limits. | | `manifest_version` | Reviewed deployment version, equal to `deployment_manifest.version`. | | `manifest_digest` | Keccak-256 digest of the canonical JSON serialization of `deployment_manifest`. | | `deployment_manifest` | Reviewed contracts, tokens, pools, proxy bindings, identity probes, and exact call edges. | Each `BlockchainProviderIdentity` has a stable `provider_id`, `operator_id`, and one or more opaque `failure_domain_ids`. All provider IDs and operator IDs must be distinct, every pair of failure domain sets must be disjoint, and the three normalized endpoint URIs must differ. Do not place RPC URLs, credentials, or provider response bodies in the manifest or retained evidence. Python constructs `BlockchainVerificationConfig` with `deployment_manifest_json`. The manifest is parsed locally and its configured digest is checked before the client is created. The RPC sources cannot create, update, or approve a manifest. A contract upgrade, new token, new pool, or changed call edge needs an independently reviewed manifest and checkpoint update before execution resumes. Proxy bindings support the EIP-1967 implementation slot and the Zeppelinos unstructured implementation slot used by Circle FiatToken deployments. Each binding pins the exact storage value, implementation address, and implementation runtime code hash. The following entry admits a USDC-to-WETH BUY only when the derived USDC input is at most 1,000 USDC. Pass the list as `quote_spend_limits` when constructing `BlockchainExecutionClientConfig`: ```python from nautilus_trader.adapters.blockchain import QuoteSpendLimit quote_spend_limits = [ QuoteSpendLimit( token_in="0xaf88d065e77c8cC2239327C5EDb3A432268e5831", token_out="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", spend_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831", spend_token_decimals=6, max_amount="1000000000", ), ] ``` ### Validation coverage The execution tests have two layers: | Layer | External state | Main coverage | | --------------- | ----------------------------------------- | ------------------------------------------------------------------ | | Unit/mocked RPC | Scripted three-source JSON-RPC responses. | Typed outcomes, hostile disagreement, signing, and reconciliation. | | Postgres | Temporary schema when Postgres is active. | Evidence ordering, migration, nonce ownership, and crash recovery. | Default tests do not connect to a live chain. They cover: - Transaction primitives: `deposit`, `approve`, `allowance`, and `exactInputSingle` calldata; EIP-1559 signing against a fixed-key vector; canonical nonce selection with pending-nonce validation; fee and gas derivation; and ceiling rejection. - RPC verification: wrong chain or checkpoint, head skew and freshness, broken ancestry, deployment and proxy changes, quote disagreement, pending and reverted receipts, partial receipt propagation, disappearing receipts, divergent traces, unauthorized internal calls, exact-intent same-nonce replacements, `already known`, node rejection, and timeout after send. - Safety checks: invalid provider independence, signer revocation, cancellation around persistence and dispatch, router/factory/WETH/pool identity, approval transitions and return values, preflight readiness, wrap and approval postconditions, exact wallet snapshots, connect and repeated account queries, order validation, limiter denials, canonical quote provenance, slippage, token orientation, exact quote-spend boundaries, stable event IDs, final fill fields, and commission. - Durability: submission ordering, one in-flight transaction, pre-broadcast signature quarantine, authorized exact-byte rebroadcast, authenticated replacement scans, durable ancestry resume, retained-history migration, event retry identity, wallet refresh ownership, and atomic evidence with authorizing transitions. Database tests skip when Postgres is unavailable. JSON-RPC fixtures live under `crates/adapters/blockchain/test_data/execution/`. The shared network HTTP unit suite covers redirect rejection. Execution validation on public networks must remain read-only and must not load a signer or call `eth_sendRawTransaction`. State-changing validation requires an isolated local environment. ## Limitations - Order submission supports BUY and SELL market orders through a registered Uniswap V3 deployment on the client's chain. Order lists are denied, modify and cancel operations are rejected, and venue report probes return an error except mass status, which returns `Ok(None)`; all fail closed with no on-chain or durable side effects. LiveNode must disable in-flight checks and leave open-order checks off. Quote-denominated and multi-hop orders are not supported. See [Execution](#execution). - Postgres-backed execution requires authenticated signed-transaction envelopes. Disconnected rollback can restore plaintext for incident work, but the adapter rejects execution until the database is protected and passes a full check again. Treat database storage, replicas, backups, and exports as broadcast-capable material in either representation. See [Signed transaction storage](#signed-transaction-storage). - Recovery is not fully automated. A signed intent without a durable `broadcast` transition blocks connect, and a same-nonce replacement search over 4,096 blocks requires an explicit recovery decision. See [Persistence and reconciliation](#persistence-and-reconciliation). - Order event publication and its durable marker are separate writes, so the adapter does not guarantee atomic exactly-once event delivery across a process crash. - Very large Uniswap V3 pools can still hit provider payload, timeout, or rate limits during final-state Multicall hydration. - On-chain snapshot validation supports Uniswap V3 and PancakeSwap V3 through their shared V3 pool read ABI. Pools with a different ABI can sync events and produce replay snapshots, but cannot reach `validation_state = on_chain`. # Bybit Source: https://nautilustrader.io/docs/latest/integrations/bybit/ Founded in 2018, Bybit is one of the largest cryptocurrency exchanges in terms of daily trading volume and open interest of crypto assets and crypto derivative products. NautilusTrader provides Bybit integration for live market data and execution. The adapter is implemented in Rust and exposed to Python through the same public configurations, factories, and data types. ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/bybit/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/bybit/examples/) ## Overview This guide assumes a trader is setting up for both live market data feeds and trade execution. The Bybit adapter includes multiple components, which can be used together or separately depending on the use case. - `BybitDataClientConfig` and `BybitExecutionClientConfig`: Live client configuration. - `BybitDataClientFactory` and `BybitExecutionClientFactory`: Trading node client factories. - `BybitDataClient`: A market data feed manager, built by the data client factory. - `BybitExecutionClient`: An account management and trade execution gateway, built by the execution client factory. - `BybitHttpClient`: Low-level HTTP API connectivity. - `BybitWebSocketClient`: Low-level WebSocket API connectivity for Rust callers. - `BYBIT`, `BYBIT_CLIENT_ID`, `BYBIT_VENUE`: Public identifiers. - `BybitEnvironment`, `BybitProductType`, `BybitMarginMode`, `BybitPositionIdx`, `BybitPositionMode`: Public enums used by the configurations and order params. :::note Most users define a configuration for a live trading node (as below), and won't need to work with the lower-level components directly. ::: ## Bybit documentation Bybit provides extensive documentation for users which can be found in the [Bybit help center](https://www.bybit.com/en/help-center). It's recommended you also refer to the Bybit documentation in conjunction with this NautilusTrader integration guide. ## Products A product is an umbrella term for a group of related instrument types. :::note Product is also referred to as `category` in the Bybit v5 API. ::: The following product types are supported on Bybit: | Product Type | Supported | Notes | | --------------------------- | --------- | ----------------------------------------- | | Spot cryptocurrencies | ✓ | Native spot markets with margin support. | | Linear perpetual contracts | ✓ | USDT/USDC margined perpetual swaps. | | Linear futures contracts | ✓ | Delivery-settled linear futures. | | Inverse perpetual contracts | ✓ | Coin-margined perpetual swaps. | | Inverse futures contracts | ✓ | Coin-margined delivery futures. | | Option contracts | ✓ | European options settled in USDT or USDC. | ## Symbology To distinguish between different product types on Bybit, Nautilus uses specific product category suffixes for symbols: - `-SPOT`: Spot cryptocurrencies - `-LINEAR`: Perpetual and futures contracts - `-INVERSE`: Inverse perpetual and inverse futures contracts - `-OPTION`: Option contracts These suffixes must be appended to the Bybit raw symbol string to identify the specific product type for the instrument ID. For example: - The Ether/Tether spot currency pair is identified with `-SPOT`, such as `ETHUSDT-SPOT`. - The BTCUSDT perpetual futures contract is identified with `-LINEAR`, such as `BTCUSDT-LINEAR`. - The BTCUSD inverse perpetual futures contract is identified with `-INVERSE`, such as `BTCUSD-INVERSE`. - A BTC USDT-settled put option: `BTC-27MAR26-70000-P-USDT-OPTION`. - A ETH USDC-settled call option: `ETH-28FEB25-2800-C-OPTION`. Bybit's option symbols include the settlement currency for USDT-settled contracts (e.g. `BTC-27MAR26-70000-P-USDT`) but omit it for USDC-settled contracts (e.g. `ETH-28FEB25-2800-C`). The adapter appends `-OPTION` to whatever symbol the API returns. ## Instrument loading The data and execution clients load all instruments for their configured `product_types` when they connect. The default is `LINEAR`. Include each product type required by your subscriptions or orders. ## Environments Bybit provides three trading environments. Configure the appropriate environment with the `environment` enum on your client configuration. | Environment | Config | Description | | ----------- | -------------------------- | ---------------------------------------------------------------- | | **Mainnet** | `BybitEnvironment.MAINNET` | Production trading with real funds. | | **Demo** | `BybitEnvironment.DEMO` | Practice trading with simulated funds on mainnet infrastructure. | | **Testnet** | `BybitEnvironment.TESTNET` | Separate test network for development and integration testing. | ### Mainnet (Production) The default environment for live trading with real funds. ```python from nautilus_trader.adapters.bybit import BybitEnvironment from nautilus_trader.adapters.bybit import BybitExecutionClientConfig config = BybitExecutionClientConfig( api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", environment=BybitEnvironment.MAINNET, ) ``` Environment variables: `BYBIT_API_KEY`, `BYBIT_API_SECRET` ### Demo trading Demo trading uses Bybit's mainnet infrastructure with simulated funds. Create demo API keys from the [Bybit demo trading page](https://www.bybit.com/en/demo-trading). ```python from nautilus_trader.adapters.bybit import BybitEnvironment from nautilus_trader.adapters.bybit import BybitExecutionClientConfig config = BybitExecutionClientConfig( api_key="YOUR_DEMO_API_KEY", api_secret="YOUR_DEMO_API_SECRET", environment=BybitEnvironment.DEMO, ) ``` Environment variables: `BYBIT_DEMO_API_KEY`, `BYBIT_DEMO_API_SECRET` :::warning **Demo environment limitations:** - The WebSocket Trade API is **not supported** for demo trading. NautilusTrader automatically uses the HTTP REST API for order operations in demo mode, including order lists and batch cancels, which are sent as individual requests. - Native TP/SL and option params (`order_iv`, `mmp`) on new orders work in demo via the HTTP create-order endpoint. - The custom TP/SL trigger prices `tp_trigger_price` and `sl_trigger_price` are not supported in demo (orders setting them are denied); the create-order endpoint cannot carry them. - Demo private streams use `wss://stream-demo.bybit.com`, but public market data uses Bybit's mainnet public stream `wss://stream.bybit.com`. ::: ### Testnet A separate test network for development and integration testing. ```python from nautilus_trader.adapters.bybit import BybitEnvironment from nautilus_trader.adapters.bybit import BybitExecutionClientConfig config = BybitExecutionClientConfig( api_key="YOUR_TESTNET_API_KEY", api_secret="YOUR_TESTNET_API_SECRET", environment=BybitEnvironment.TESTNET, ) ``` Environment variables: `BYBIT_TESTNET_API_KEY`, `BYBIT_TESTNET_API_SECRET` :::note Testnet supports all trading features including the WebSocket Trade API. It uses completely separate infrastructure from mainnet, so market data and liquidity differ significantly from production. ::: When `environment=BybitEnvironment.TESTNET`, the adapter resolves Bybit's documented testnet endpoints automatically: - REST API: `https://api-testnet.bybit.com` - Public WebSocket: `wss://stream-testnet.bybit.com/v5/public/{spot|linear|inverse|option}` - Private WebSocket: `wss://stream-testnet.bybit.com/v5/private` - Trade WebSocket: `wss://stream-testnet.bybit.com/v5/trade` ### Testnet setup To set up a Bybit testnet account and credentials: 1. Open [testnet.bybit.com](https://testnet.bybit.com) in a desktop browser. 2. Create a separate testnet account or sign in to your existing testnet account. 3. Request test coins from **Assets -> Assets Overview -> Request Test Coins** so the account has balances for testing. 4. Open **API Management** at [testnet.bybit.com/app/user/api-management](https://testnet.bybit.com/app/user/api-management). 5. Click **Create New Key**. 6. Select the required permissions for your use case. 7. Complete the 2FA prompt and copy the API key and secret. 8. Export the credentials in your shell: ```bash export BYBIT_TESTNET_API_KEY="YOUR_TESTNET_API_KEY" export BYBIT_TESTNET_API_SECRET="YOUR_TESTNET_API_SECRET" ``` Bybit's current testnet guidance also notes: - API keys are created on the website, not in the mobile app. - New users may be unable to create API keys for the first 48 hours after registration. - Testnet is separate from mainnet. Do not deposit real funds into a testnet account. - Bybit currently documents testnet account setup through a desktop browser. ## Quotes and order books For SPOT, LINEAR, and INVERSE products, quote subscriptions use Bybit's depth-1 order book snapshots. This feed provides best bid/ask prices and sizes at a documented 10 ms push frequency. OPTION quotes use the ticker feed. See the [Bybit order book specification](https://bybit-exchange.github.io/docs/v5/websocket/public/orderbook). Quotes and depth-1 book subscriptions share one WebSocket topic. Unsubscribing from one leaves the topic active while the other is still subscribed. A deeper book subscription uses its own topic alongside the depth-1 quote feed, so adding book deltas does not change the quote source. Book deltas only come from the requested depth. Subscribe to one book depth per instrument; unsubscribe from the existing book before selecting another depth. ## Orders capability Bybit offers a flexible combination of trigger types, enabling a broader range of Nautilus orders. All the order types listed below can be used as *either* entries or exits. ### Order types | Order Type | Spot | Linear | Inverse | Option | Notes | | ---------------------- | ---- | ------ | ------- | ------ | -------------------------------------- | | `MARKET` | ✓ | ✓ | ✓ | ✓ | Quote quantity: Spot only. | | `LIMIT` | ✓ | ✓ | ✓ | ✓ | | | `STOP_MARKET` | ✓ | ✓ | ✓ | - | *Not supported for Options*. | | `STOP_LIMIT` | ✓ | ✓ | ✓ | - | *Not supported for Options*. | | `MARKET_IF_TOUCHED` | ✓ | ✓ | ✓ | - | *Not supported for Options*. | | `LIMIT_IF_TOUCHED` | ✓ | ✓ | ✓ | - | *Not supported for Options*. | | `TRAILING_STOP_MARKET` | - | - | - | - | See [Trailing stops](#trailing-stops). | An order with a type the adapter does not support is denied locally at submission, with an `UNSUPPORTED_ORDER_TYPE` reason, rather than being sent to the venue. ### Execution instructions | Instruction | Spot | Linear | Inverse | Option | Notes | | ------------- | ---- | ------ | ------- | ------ | ----------------------------------------------------------------- | | `post_only` | ✓ | ✓ | ✓ | ✓ | Limit order types only; sent as Bybit's `PostOnly` time in force. | | `reduce_only` | - | ✓ | ✓ | ✓ | *Not supported for Spot*. | ### Time in force | Time in force | Spot | Linear | Inverse | Option | Notes | | ------------- | ---- | ------ | ------- | ------ | ------------------------------- | | `GTC` | ✓ | ✓ | ✓ | ✓ | Good Till Canceled. | | `GTD` | - | - | - | - | *Not supported*; sent as `GTC`. | | `FOK` | ✓ | ✓ | ✓ | ✓ | Fill or Kill. | | `IOC` | ✓ | ✓ | ✓ | ✓ | Immediate or Cancel. | ### Advanced order features | Feature | Spot | Linear | Inverse | Option | Notes | | ------------------ | ---- | ------ | ------- | ------ | -------------------------------------- | | Order Modification | ✓ | ✓ | ✓ | ✓ | Price and quantity modification. | | Bracket/OCO Orders | - | - | - | - | Not implemented; submit legs yourself. | | Iceberg Orders | - | - | - | - | Not implemented. | ### Batch operations | Operation | Spot | Linear | Inverse | Option | Notes | | ------------ | ---- | ------ | ------- | ------ | ----------------------------------------- | | Batch Submit | ✓ | ✓ | ✓ | ✓ | Submit multiple orders in single request. | | Batch Modify | - | - | - | - | Not wired into the execution client. | | Batch Cancel | ✓ | ✓ | ✓ | ✓ | Cancel multiple orders in single request. | Batch submit and batch cancel use the trade WebSocket on mainnet and testnet. In demo mode the adapter falls back to individual HTTP requests, because the demo environment has no trade WebSocket. Bybit accepts at most 10 Spot orders, 20 Linear or Inverse orders, or 5 Option orders in one batch request. Linear, Inverse, and Spot batches consume UID quota per order, while an Option batch consumes one request. The adapter splits Spot, Linear, and Inverse batches into groups of 10 by default so one request cannot exceed the standard rolling UID allowance. It splits Option batches into groups of five. The HTTP batch-cancel method accepts up to 20 Option operations in one call. ### Position management | Feature | Spot | Linear | Inverse | Option | Notes | | ---------------- | ---- | ------ | ------- | ------ | ----------------------------------------------------------- | | Query positions | - | ✓ | ✓ | ✓ | Real-time position updates. | | Position mode | - | ✓ | ✓ | - | One-Way only for Options. | | Leverage control | - | ✓ | ✓ | - | Not applicable for Options. | | Margin mode | - | ✓ | ✓ | ✓ | `ISOLATED_MARGIN`, `REGULAR_MARGIN`, or `PORTFOLIO_MARGIN`. | Set `margin_mode` on the execution client config to apply a `BybitMarginMode` to the account when the client connects. #### Hedge mode (BothSides) Bybit only accepts Both Sides mode on USDT linear perpetuals. Configure the position mode at Bybit, then pass `position_idx` through the order `params`: `1` for the long side or `2` for the short side. Use `0` or omit the parameter for one-way mode. Bybit documents these values in the V5 [switch position mode](https://bybit-exchange.github.io/docs/v5/position/position-mode) and [place order](https://bybit-exchange.github.io/docs/v5/order/create-order#request-parameters) APIs. Orders and reports with `positionIdx=0` (one-way / Merged Single mode) carry no venue position ID. For hedge-mode indexes `1` and `2`, the adapter maps reports to venue position IDs ending in `-LONG` and `-SHORT`, and carries the same ID onto fills when Bybit execution messages do not include `positionIdx`. In hedge mode `positionIdx` identifies the position being affected, not the trade direction, so a reduce-only sell resolves to the long index and a reduce-only buy resolves to the short index. To override, pass `position_idx` via `params`: ```python params = {"position_idx": 1} # 0 one-way, 1 long, 2 short ``` ### Risk events | Feature | Spot | Linear | Inverse | Option | Notes | | -------------------- | ---- | ------ | ------- | ------ | ----------------------------------------------------- | | Liquidation handling | - | ✓ | ✓ | ✓ | Takeover fills flagged as exchange-generated. | | ADL handling | - | ✓ | ✓ | ✓ | Auto-deleveraging fills flagged and logged. | | ADL rank warnings | - | ✓ | ✓ | ✓ | Position reports logged when `adlRankIndicator >= 4`. | Bybit emits venue-initiated fills with `execType` set to: - `AdlTrade`: Auto-deleveraging execution. An opposing profitable position was selected to close the undercollateralised counterparty after the insurance fund could not cover the loss. - `BustTrade`: Liquidation takeover. The liquidation engine seized the position after margin was exhausted. - `Delivery`: USDC futures delivery. - `Settle`: Inverse futures settlement. - `ForwardSplitSettle`: Forward stock split fractional share settlement. - `ReverseSplitSettle`: Reverse stock split fractional share settlement. - `Dividend`: Dividend distribution. The venue previously issued `CorporateAction` for stock splits and reverse splits. The adapter still accepts it in execution history recorded before its replacement by the settle and dividend types above. The adapter flags each as exchange-generated and logs a warning containing the execution ID, symbol, side, quantity, and price. Fills flow through the normal `FillReport` path; because these orders carry an empty `orderLinkId`, the execution engine treats them as external and assigns them through the instrument's active external order claim, configured initially with `external_order_instrument_ids`, or to the `EXTERNAL` strategy by default. Execution types the adapter does not recognize are handled as `UNKNOWN` rather than rejected, so future venue additions still flow through the fill path instead of being dropped. Funding settlements use `execType=Funding`, but they are balance adjustments rather than fills. The adapter ignores them in historical fill reports and standard private `execution` messages, so it emits neither a `FillReport` nor an `OrderFilled` event and does not change local position quantity. During reconciliation, funding records do not count toward the requested fill-report limit. Bybit also publishes an ADL ranking on position updates via the `adlRankIndicator` field. The range is 0 (flat / no position) to 5 (next to deleverage). The adapter logs a warning whenever an open position carries a rank of 4 or higher so you can react before the venue force-closes. Upstream references: - [V5 `execType` values](https://bybit-exchange.github.io/docs/v5/enum#exectype) - [V5 `createType` values](https://bybit-exchange.github.io/docs/v5/enum#createtype) - [Liquidation mechanism](https://www.bybit.com/en/help-center/article/Liquidation-Process-Derivatives-Trading) - [Auto-Deleveraging mechanism](https://www.bybit.com/en/help-center/article/Auto-Deleveraging-ADL-Derivatives-Trading) ### Order querying | Feature | Spot | Linear | Inverse | Option | Notes | | -------------------- | ---- | ------ | ------- | ------ | ------------------------------ | | Query open orders | ✓ | ✓ | ✓ | ✓ | List all active orders. | | Query order history | ✓ | ✓ | ✓ | ✓ | Historical order data. | | Order status updates | ✓ | ✓ | ✓ | ✓ | Real-time order state changes. | | Trade history | ✓ | ✓ | ✓ | ✓ | Execution and fill reports. | ### Contingent orders | Feature | Spot | Linear | Inverse | Option | Notes | | ------------------ | ---- | ------ | ------- | ------ | -------------------------------------- | | Order lists | ✓ | ✓ | ✓ | ✓ | Submitted as a batch via WebSocket. | | OCO orders | - | - | - | - | Not implemented; submit legs yourself. | | Bracket orders | - | - | - | - | Not implemented; submit legs yourself. | | Conditional orders | ✓ | ✓ | ✓ | - | Stop and limit-if-touched orders. | An order list is validated as a unit before any leg is sent. When one leg fails validation, that leg is denied with its specific reason and the remaining legs are denied with `ORDER_LIST_DENIED`, so a partially submitted list cannot reach the venue. ### Order parameters Individual orders can be customized using the `params` dictionary when submitting orders: | Parameter | Type | Description | | ------------------ | ---------------- | ------------------------------------------------------------------- | | `is_leverage` | `bool` | Spot only. Enables margin trading (borrowing). Default: `False`. | | `take_profit` | `str` or `float` | TP trigger price. Attaches a native TP to the order. | | `stop_loss` | `str` or `float` | SL trigger price. Attaches a native SL to the order. | | `tp_trigger_by` | `str` | TP trigger type: `"LastPrice"`, `"IndexPrice"`, or `"MarkPrice"`. | | `sl_trigger_by` | `str` | SL trigger type: `"LastPrice"`, `"IndexPrice"`, or `"MarkPrice"`. | | `tp_order_type` | `str` | TP execution type: `"Market"` or `"Limit"`. | | `sl_order_type` | `str` | SL execution type: `"Market"` or `"Limit"`. | | `tp_limit_price` | `str` or `float` | Limit price for TP when `tp_order_type` is `"Limit"`. | | `sl_limit_price` | `str` or `float` | Limit price for SL when `sl_order_type` is `"Limit"`. | | `tp_trigger_price` | `str` or `float` | Explicit TP trigger price sent alongside `take_profit`. | | `sl_trigger_price` | `str` or `float` | Explicit SL trigger price sent alongside `stop_loss`. | | `tpsl_mode` | `str` | TP/SL mode: `"Full"` or `"Partial"`. | | `close_on_trigger` | `bool` | Close the position when TP/SL triggers. | | `position_idx` | `int` | Hedge-mode position index. See [Hedge mode](#hedge-mode-bothsides). | | `bbo_side_type` | `str` | Linear/inverse BBO side: `"Queue"` or `"Counterparty"`. | | `bbo_level` | `str` or `int` | Linear/inverse BBO book level: `"1"` through `"5"`. | | `smp_type` | `str` | Self-match prevention. See [SMP](#self-match-prevention). | Parameters left unset are omitted from the request, so Bybit's own defaults apply. :::warning Bybit's `close_on_trigger` parameter is not the generic `close_position` whole-position exit contract used by the risk engine. The adapter sends the order quantity, and it ignores an unknown `close_position` parameter. Do not add `BYBIT` to `full_position_exit_venues` based on `close_on_trigger`; leave the venue unlisted so ordinary quantity and notional checks apply. ::: The adapter validates these params before emitting `OrderSubmitted` and denies the order with a `VALIDATION_FAILED` reason when a rule is broken: - Every TP override field (`tp_trigger_by`, `tp_order_type`, `tp_limit_price`, `tp_trigger_price`) requires `take_profit`, and every SL override field likewise requires `stop_loss`. - `tp_order_type="Limit"` requires `tp_limit_price`, and `tp_limit_price` requires `tp_order_type="Limit"`. The same pairing applies to `sl_order_type` and `sl_limit_price`. - `bbo_side_type` and `bbo_level` must be provided together. - `smp_type` must be `"None"`, `"CancelMaker"`, `"CancelTaker"`, or `"CancelBoth"`, matched case-insensitively. When `take_profit` or `stop_loss` is set without `tpsl_mode`, the adapter sends `Full`. When a TP or SL price is set without its own `tp_trigger_by` or `sl_trigger_by`, the adapter derives the trigger type from the order's trigger type. :::note On demo, native TP/SL params route through the HTTP create-order endpoint, with one exception: the custom trigger prices `tp_trigger_price` and `sl_trigger_price` are not supported because that endpoint cannot carry them, and orders that set either are denied. The `is_leverage` param applies to Spot products only. See [Bybit's isLeverage documentation](https://bybit-exchange.github.io/docs/v5/order/create-order#request-parameters). ::: When `bbo_side_type` and `bbo_level` are set, Nautilus sends Bybit's `bboSideType` and `bboLevel` fields and omits the order price from the API request. BBO orders are supported for linear and inverse limit, stop-limit, and limit-if-touched orders. #### Self-match prevention Self-match prevention (SMP) tells Bybit what to do when one of your orders would trade against another of your own orders. Bybit accepts four values on an order, shown below in their canonical wire spellings. The adapter matches them case-insensitively and always sends the canonical spelling. | Value | Behavior | | ------------- | -------------------------------- | | `None` | No self-match prevention. | | `CancelMaker` | Cancel the resting maker order. | | `CancelTaker` | Cancel the incoming taker order. | | `CancelBoth` | Cancel both orders. | Set `smp_type` on the execution client config to send that value on every order the client submits: ```python from nautilus_trader.adapters.bybit import BybitExecutionClientConfig config = BybitExecutionClientConfig( api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", smp_type="CancelMaker", ) ``` Pass `smp_type` through the order `params` to override the configured default for a single order: ```python params = {"smp_type": "CancelBoth"} ``` The adapter denies an order whose `smp_type` is not one of the four values above, and it rejects a client config carrying any other value. With neither the config nor the param set, the adapter omits `smpType`, so Bybit's own default applies. Bybit overrides the setting in some regions: derivatives accounts in Kazakhstan have SMP force-enabled and cannot opt out, and spot accounts in Turkey, Kazakhstan, and Georgia fall back to `CancelMaker` when `smpType` is omitted, `None`, or invalid. Bybit documents the values and the regional rules in the V5 [SMP guide](https://bybit-exchange.github.io/docs/v5/smp). #### Example: Order with native TP/SL ```python order = strategy.order_factory.limit( instrument_id=InstrumentId.from_str("BTCUSDT-LINEAR.BYBIT"), order_side=OrderSide.BUY, quantity=Quantity.from_str("0.01"), price=Price.from_str("60000.0"), params={ "take_profit": "65000.0", "stop_loss": "58000.0", "tp_trigger_by": "LastPrice", "sl_trigger_by": "LastPrice", }, ) strategy.submit_order(order) ``` #### Example: BBO order ```python order = strategy.order_factory.limit( instrument_id=InstrumentId.from_str("BTCUSDT-LINEAR.BYBIT"), order_side=OrderSide.BUY, quantity=Quantity.from_str("0.01"), price=Price.from_str("60000.0"), params={"bbo_side_type": "Queue", "bbo_level": 1}, ) strategy.submit_order(order) ``` #### Example: Spot margin trading ```python # Submit a Spot order with margin enabled order = strategy.order_factory.market( instrument_id=InstrumentId.from_str("BTCUSDT-SPOT.BYBIT"), order_side=OrderSide.BUY, quantity=Quantity.from_str("0.1"), params={"is_leverage": True}, # Enable margin for this order ) strategy.submit_order(order) ``` :::note Without `is_leverage=True` in the params, Spot orders use your available balance and do not borrow funds, even if you have auto-borrow enabled on your Bybit account. ::: For a complete example of using order parameters including `is_leverage`, see the [Python execution tester](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/bybit/exec_tester.py). ### Spot trading limitations The following limitations apply to Spot products, as positions are not tracked on the venue side: - `reduce_only` orders are *not supported*. - Trailing stop orders are *not supported*. ### Options trading Bybit lists European-style options on BTC and ETH, settled in USDT or USDC. The adapter uses the `CryptoOption` instrument type and the `-OPTION` symbol suffix. See the [symbology section](#symbology) for the full symbol format. #### Options data The adapter supports real-time options market data through the WebSocket ticker channel: | Data type | Description | | -------------------------- | ------------------------------------------------------------------------ | | Quotes (bid/ask) | Top-of-book prices and sizes for each option contract. | | Greeks | Delta, gamma, vega, theta, plus bid/ask/mark IV. Bybit publishes no rho. | | Mark price | Exchange mark price for each option contract. | | Index price | Underlying index price. | | Underlying reference price | Per-expiry venue reference used for ATM determination. | | Open interest | Per-contract open interest. | | Order book deltas | L2 MBP updates from the option orderbook stream. | Subscribe to per-instrument Greeks or aggregate them into option chain snapshots with ATM-relative strike filtering. See the [options concept guide](../concepts/options.md) for subscription patterns and the [options data tutorial](../tutorials/options_data_bybit.md) for a step-by-step walkthrough. NautilusTrader builds the option chain view locally from Bybit's per-contract option market data. Bar (kline) data is not available for options. Bybit does not provide kline streams for this product type. #### Options order parameters In addition to the standard order parameters, option orders accept: | Parameter | Type | Description | | ---------- | ---------------- | ---------------------------------------------------------------- | | `order_iv` | `str` or `float` | Place or amend the order by implied volatility instead of price. | | `mmp` | `bool` | Enable Market Maker Protection for the order. | These parameters are passed through `params` on `SubmitOrder`. On mainnet they flow through the WebSocket trade channel; on demo they route through the HTTP create-order endpoint. Amending an existing order by `order_iv` is not supported in demo mode. #### Options trading limitations - Amending an order by implied volatility (`order_iv`) and other WS-trade-only features are not supported in demo mode. - Leverage is not configurable. Option buyers pay premium; sellers post margin. - Position mode is one-way only. Hedge mode is not supported. - Conditional order types (`STOP_MARKET`, `STOP_LIMIT`, `MARKET_IF_TOUCHED`, `LIMIT_IF_TOUCHED`) are not supported. - Trading stops (TP/SL on positions) are not supported. - Funding rates do not apply to options. - Options require a Unified Trading Account (UTA). ### Trailing stops The adapter does not submit Nautilus `TRAILING_STOP_MARKET` orders to Bybit. Submitting one denies the order locally with an `UNSUPPORTED_ORDER_TYPE` reason. Bybit models trailing stops as an attribute of a netted position rather than as an order, so a trailing stop has no client order ID on the venue side and cannot be queried until it is already open. Attach a trailing stop through the Bybit interface if you need one, and manage the resulting position exit outside Nautilus. ## Spot margin borrowing and repayment NautilusTrader provides automated spot margin borrow repayment functionality to prevent interest accrual after closing short positions on Bybit. ### Background When trading Spot with margin enabled (`is_leverage=True`), Bybit automatically borrows coins when you execute short positions. However, after you close the short position (BUY order fills), the borrowed coins are **NOT automatically repaid** - they continue accruing hourly interest charges until manually repaid. This can result in significant interest costs if left unattended. ### Automatic repayment (recommended) The execution client can automatically repay spot margin borrows after BUY orders fully fill on Spot instruments. This feature is disabled by default, so set `auto_repay_spot_borrows=True` to opt in. **How it works:** 1. When a Spot BUY order fully fills on the standard `execution` channel, the execution client attempts to repay the base coin borrow. 1. The repayment is capped at the lesser of the outstanding borrow and the base quantity acquired across the order's executions. 1. The execution client uses Bybit's converting repay endpoint to cover base-denominated trading fees. For MNT, which Bybit excludes from converting repayment, it uses no-convert repay and subtracts MNT-denominated fees from the amount. 1. A failed request or `FA` result status is logged without crashing the execution client. A `P` result status is logged as processing, not complete. 1. The execution client defers queued repayments during Bybit's UTC blackout window. **Example:** ```python from nautilus_trader.adapters.bybit import BybitExecutionClientConfig config = BybitExecutionClientConfig( api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", product_types=[BybitProductType.SPOT], auto_repay_spot_borrows=True, # Opt in; default is False ) ``` ### UTC blackout window Bybit blocks both repayment endpoints from **4 minutes through 5 minutes 30 seconds past every UTC hour** for interest calculation. Auto-repayment keeps the request queued and attempts it at 5 minutes 31 seconds past the hour. ### Auto-repayment configuration | Option | Type | Default | Description | | ------------------------- | ------ | ------- | -------------------------------------------------------------------------------------------------------------------------- | | `auto_repay_spot_borrows` | `bool` | `False` | If `True`, automatically repay Spot margin borrows after BUY orders fully fill. Repayment is deferred during the blackout. | ### Auto-repayment notes - Auto-repayment only triggers on **Spot BUY orders**, not derivatives. - Repayment uses converting repayment except for MNT, which uses no-convert repayment. - Bybit documents the endpoint restrictions and result statuses in [Manual Repay](https://bybit-exchange.github.io/docs/v5/account/repay) and [Manual Repay Without Asset Conversion](https://bybit-exchange.github.io/docs/v5/account/no-convert-repay). - Manual borrowing is still required before opening short positions unless auto-borrow is enabled on your Bybit account. ## Funding rates The adapter receives funding rate data from the [Linear Ticker](https://bybit-exchange.github.io/docs/v5/websocket/public/ticker#linear-inverse-perpetual-response) WebSocket stream. Bybit provides the `fundingIntervalHour` field in ticker updates, which the adapter uses to populate the `interval` field on `FundingRateUpdate`. The adapter caches the last known `fundingIntervalHour` per symbol so that partial ticker updates (which may omit the field) still carry the correct interval. For historical funding rate requests, the adapter computes the interval from consecutive funding timestamps. The oldest record in a response has no earlier timestamp to pair with, so its interval is unset. ## Rate limiting The adapter queues requests against exact rolling windows before it creates an authentication timestamp or signature. HTTP clients and the trade WebSocket share UID state for the same API key and environment. Data and execution clients also share IP state when they use the same origin and proxy. | Scope | Bybit limit | Adapter behavior | | ----------------------------- | ------------------------------------- | --------------------------------------------- | | HTTP IP | 600 requests per 5 seconds | Shared by origin and proxy | | HTTP and trade WebSocket UID | Varies by endpoint and product | Shared by API key and environment | | Trade WebSocket IP | 3,000 requests per second | Shared by WebSocket origin and proxy | | WebSocket connection attempts | 500 attempts per 5 minutes per domain | Shared across initial connects and reconnects | | Option subscriptions | 2,000 arguments per connection | Rejected before subscription state changes | The UID limiter includes the documented lower-rate account and user routes, 50-request read routes, product-specific order routes, cancel-all limits, and weighted batch operations. HTTP and trade WebSocket responses update the configured UID limit from `X-Bapi-Limit`, track `X-Bapi-Limit-Status`, and honor a future `X-Bapi-Limit-Reset-Timestamp` when the remaining count reaches zero. The execution client's `recv_window_ms` applies to signed REST requests and trade WebSocket order commands. A queued WebSocket order gets its timestamp and receive-window header only after all applicable quotas allow the send. A reconnect retry rebuilds the command with a fresh header and uses a connection-bound write, so the transport cannot replay a stale order payload. :::warning Bybit returns `retCode` `10006` ("Too many visits") when the API rate limit is exceeded. Exceeding the IP ceiling of 600 requests per 5 seconds returns HTTP 403 and bans the IP for at least 10 minutes. A matching 403 discards the affected pooled HTTP session and starts a shared 10-minute cooldown. Other 403 responses do not activate the cooldown. ::: :::warning Coordination is process-local. Another process or host using the same API key or public IP can consume venue quota that this adapter cannot reserve in advance. Response headers reduce this gap for UID limits, but separate processes still require operational coordination. ::: Explicit venue rate-limit responses are terminal rejections for the affected order operation. Transport timeouts, service restarts, and duplicate request identifiers remain subject to order reconciliation because they do not prove whether the venue accepted the order. :::info For more details on rate limiting, see the official documentation: . ::: ## Account types The execution client factory determines the account type and OMS type from the configured product types: - **Spot only**: `CASH` account type with a `HEDGING` OMS type. - **Derivatives or mixed products**: `MARGIN` account type (UTA - Unified Trading Account) with a `NETTING` OMS type. This allows you to trade Spot alongside derivatives in a single Unified Trading Account, which is the standard account type for most Bybit users. :::info **Unified Trading Accounts (UTA) and Spot margin trading** Most Bybit users now have Unified Trading Accounts (UTA) as Bybit steers new users to this account type. Classic accounts are considered legacy. For Spot margin trading on UTA accounts: - Borrowing is **NOT automatically enabled** - it requires explicit API configuration - To use Spot margin via API, you must submit orders with `is_leverage=True` in the parameters (see [Bybit docs](https://bybit-exchange.github.io/docs/v5/order/create-order#request-parameters)) - If auto-borrow/auto-repay is enabled on your Bybit account, the venue will automatically borrow/repay funds for those margin orders - Without auto-borrow enabled, you'll need to manually manage borrowing through Bybit's interface **Important**: The Nautilus Bybit adapter defaults to `is_leverage=False` for Spot orders, meaning they won't use margin unless you explicitly enable it. ::: ## Fee currency logic Understanding how Bybit determines the currency for trading fees is important for accurate accounting and position tracking. The fee currency rules vary between Spot and derivatives products. The adapter takes the commission amount and currency directly from the venue's `execFee` and `feeCurrency` fields, so the rules below describe what Bybit reports rather than a local calculation. ### Spot trading fees For Spot trading, the fee currency depends on the order side and whether the fee is a rebate (negative fee for maker orders): #### Normal fees (positive) - **BUY orders**: Fee is charged in the **base currency** (e.g., BTC for BTCUSDT) - **SELL orders**: Fee is charged in the **quote currency** (e.g., USDT for BTCUSDT) #### Maker rebates (negative fees) When maker fees are negative (rebates), the currency logic is **inverted**: - **BUY orders with maker rebate**: Rebate is paid in the **quote currency** (e.g., USDT for BTCUSDT) - **SELL orders with maker rebate**: Rebate is paid in the **base currency** (e.g., BTC for BTCUSDT) :::note **Taker orders never have inverted logic**, even if the maker fee rate is negative. Taker fees always follow the normal fee currency rules. ::: #### Example: BTCUSDT Spot - **Buy 1 BTC as taker (0.1% fee)**: Pay 0.001 BTC in fees - **Sell 1 BTC as taker (0.1% fee)**: Pay equivalent USDT in fees - **Buy 1 BTC as maker (-0.01% rebate)**: Receive USDT rebate (inverted) - **Sell 1 BTC as maker (-0.01% rebate)**: Receive BTC rebate (inverted) ### Derivatives trading fees For all derivatives products (LINEAR, INVERSE, OPTION), fees are always charged in the **settlement currency**: | Product Type | Settlement Currency | Fee Currency | | ------------ | -------------------------------- | ------------ | | LINEAR | USDT (typically) | USDT | | INVERSE | Base coin (e.g., BTC for BTCUSD) | Base coin | | OPTION | USDT or USDC | Settle coin | ### Missing fee data Bybit's `execution.fast` private channel omits the fee and execution type fields. Fill reports parsed from that channel therefore carry zero commission. Subscribe to the standard `execution` channel when exact fee data is required. ### Official documentation For complete details on Bybit's fee structure and currency rules, refer to: - [Bybit WebSocket Private Execution](https://bybit-exchange.github.io/docs/v5/websocket/private/execution) - [Bybit Spot Fee Currency Instruction](https://bybit-exchange.github.io/docs/v5/enum#spot-fee-currency-instruction) ## Configuration The product types for each client must be specified in the configurations. ### Data client configuration options | Option | Default | Description | | ---------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------- | | `product_types` | `[LINEAR]` | Sequence of `BybitProductType` values to enable. | | `environment` | `MAINNET` | Bybit environment enum. Use `BybitEnvironment.MAINNET`, `BybitEnvironment.DEMO`, or `BybitEnvironment.TESTNET`. | | `api_key` | `None` | API key; loaded from the matching environment variable when omitted. | | `api_secret` | `None` | API secret; loaded from the matching environment variable when omitted. | | `base_url_http` | `None` | Override for the REST base URL. | | `base_url_ws_public` | `None` | Override for the public WebSocket URL. | | `base_url_ws_private` | `None` | Override for the private WebSocket URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `http_timeout_secs` | `60` | Timeout (seconds) for REST requests. | | `max_retries` | `3` | Maximum retry attempts for REST requests. | | `retry_delay_initial_ms` | `1,000` | Initial retry delay (milliseconds). | | `retry_delay_max_ms` | `10,000` | Maximum retry delay (milliseconds). | | `heartbeat_interval_secs` | `20` | Heartbeat interval (seconds) for WebSocket clients. | | `recv_window_ms` | `5,000` | Receive window (milliseconds) for signed REST requests. | | `update_instruments_interval_mins` | `60` | Interval (minutes) between instrument catalog refreshes. | | `instrument_status_poll_secs` | `60` | Interval (seconds) between instrument and status polls; `0` disables polling. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | ### Execution client configuration options | Option | Default | Description | | --------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------- | | `product_types` | `[LINEAR]` | Sequence of `BybitProductType` values to enable. | | `environment` | `MAINNET` | Bybit environment enum. Use `BybitEnvironment.MAINNET`, `BybitEnvironment.DEMO`, or `BybitEnvironment.TESTNET`. | | `api_key` | `None` | API key; loaded from the matching environment variable when omitted. | | `api_secret` | `None` | API secret; loaded from the matching environment variable when omitted. | | `base_url_http` | `None` | Override for the REST base URL. | | `base_url_ws_private` | `None` | Override for the private WebSocket base URL. | | `base_url_ws_trade` | `None` | Override for the trade WebSocket base URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `http_timeout_secs` | `60` | Timeout (seconds) for REST requests. | | `max_retries` | `3` | Maximum retry attempts for REST requests. | | `retry_delay_initial_ms` | `1,000` | Initial retry delay (milliseconds). | | `retry_delay_max_ms` | `10,000` | Maximum retry delay (milliseconds). | | `heartbeat_interval_secs` | `5` | Heartbeat interval (seconds) for WebSocket clients. | | `auth_timeout_secs` | `None` | Optional WebSocket authentication timeout (seconds). | | `recv_window_ms` | `5,000` | Receive window (milliseconds) for signed REST and trade WebSocket requests. | | `account_id` | `None` | Optional account ID associated with this client. | | `use_spot_position_reports` | `False` | Report Spot wallet balances as positions for scoped requests; bulk reports omit Spot (no pair attribution). | | `auto_repay_spot_borrows` | `False` | Automatically repay tracked Spot margin borrows after BUY orders fully fill. | | `margin_mode` | `None` | Unified margin mode setting for the account. | | `smp_type` | `None` | Self-match prevention sent on every order. See [SMP](#self-match-prevention). | | `transport_backend` | `Sockudo` | WebSocket transport backend. | The compiled default is Sockudo when the `transport-sockudo` Cargo feature is enabled and Tungstenite otherwise. Use `BybitDataClientConfig` with `BybitDataClientFactory` and `BybitExecutionClientConfig` with `BybitExecutionClientFactory`. The current Python examples show the complete `LiveNode.builder(...)` configuration for data and execution clients. ### API credentials There are two options for supplying your credentials to the Bybit clients. Either pass the corresponding `api_key` and `api_secret` values to the configuration objects, or set the following environment variables: For Bybit live clients, you can set: - `BYBIT_API_KEY` - `BYBIT_API_SECRET` For Bybit demo clients, you can set: - `BYBIT_DEMO_API_KEY` - `BYBIT_DEMO_API_SECRET` For Bybit testnet clients, you can set: - `BYBIT_TESTNET_API_KEY` - `BYBIT_TESTNET_API_SECRET` :::tip We recommend using environment variables to manage your credentials. ::: When starting the trading node, you'll receive immediate confirmation of whether your credentials are valid and have trading permissions. ## Contributing :::info For additional features or to contribute to the Bybit adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Coinbase Source: https://nautilustrader.io/docs/latest/integrations/coinbase/ Founded in 2012, Coinbase is one of the largest US-regulated cryptocurrency exchanges, offering trading across spot, perpetual swaps, and dated futures via the Advanced Trade API. This adapter supports live market data ingest and order execution on both spot (Cash) and CFM derivatives (Margin) accounts through a shared execution client, with the account type selected by the factory (see [Execution scope](#execution-scope)). ## Overview The Coinbase adapter is implemented in Rust and exposed to Python through configurations, factories, enums, and constants. Components: - `CoinbaseRawHttpClient`: Low-level REST client owning transport, JWT signing, and rate limits. - `CoinbaseHttpClient`: Domain REST client parsing venue responses into Nautilus types. - `CoinbaseWebSocketClient`: Low-level WebSocket connectivity with JWT subscribe auth. - `CoinbaseInstrumentProvider`: Instrument parsing and loading. - `CoinbaseDataClient`: Market data feed manager. - `CoinbaseDataClientFactory`: Data client factory. - `CoinbaseExecutionClient`: Execution client (spot or CFM derivatives; REST orders + WS streams). - `CoinbaseExecutionClientFactory`: Execution client factory; spot vs CFM derivatives is selected by `account_type` on the config. Python surface available from `nautilus_trader.adapters.coinbase`: - `CoinbaseDataClientConfig`, `CoinbaseExecutionClientConfig` - `CoinbaseDataClientFactory`, `CoinbaseExecutionClientFactory` - `CoinbaseEnvironment`, `CoinbaseMarginType` - `COINBASE`, `COINBASE_CLIENT_ID`, and `COINBASE_VENUE` ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/coinbase/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/coinbase/examples/) ## Coinbase documentation Coinbase provides documentation for the Advanced Trade API: - [REST API reference](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/introduction) - [Order management guide](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/guides/orders) - [WebSocket channels](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-channels) - [API key authentication](https://docs.cdp.coinbase.com/coinbase-app/authentication-authorization/api-key-authentication) - [Rate limiting](https://docs.cdp.coinbase.com/coinbase-app/api-architecture/rate-limiting) It's recommended you also refer to the Coinbase documentation in conjunction with this NautilusTrader integration guide. :::info This adapter targets the Coinbase Advanced Trade API. The separate [Coinbase International Exchange (INTX)](https://international.coinbase.com) venue is not covered; its adapter was removed in NautilusTrader 1.224.0. ::: ## Products A product is an umbrella term for a group of related instrument types. The following product types are supported: | Product Type | Supported | Notes | | ------------------- | --------- | ------------------------------------------------- | | Spot | ✓ | USD, USDC, and USDT-quoted spot pairs. | | Perpetual contracts | ✓ | USD-margined perpetual swaps on the FCM venue. | | Futures contracts | ✓ | Dated delivery futures (nano BTC, nano ETH, etc). | ## Symbology Coinbase uses the venue's native `product_id` field directly as the Nautilus symbol. The instrument ID is `{product_id}.COINBASE`. | Product | Format | Examples | | ------------ | ------------------------------- | ---------------------------------- | | Spot | `{base}-{quote}` | `BTC-USD`, `ETH-USDC`, `SOL-USDT`. | | Perpetual | `{contract_code}-{ddMMMyy}-CDE` | `BIP-20DEC30-CDE` (BTC PERP). | | Dated future | `{contract_code}-{ddMMMyy}-CDE` | `BIT-24APR26-CDE` (BTC Apr 2026). | The `-CDE` suffix denotes the Coinbase Derivatives Exchange (FCM venue). Perpetuals carry an exchange-assigned far-future expiry (e.g. `20DEC30`) but are classified as `CryptoPerpetual` based on the presence of an ongoing funding rate. Dated futures are classified as `CryptoFuture`. The adapter resolves the product type structurally from API metadata (`future_product_details.contract_expiry_type` and, when that is `EXPIRING`, the presence of a non-empty `future_product_details.funding_rate` as a perpetual-only structural signal); the fallback heuristic checks `display_name` for `PERP` or `Perpetual` substrings. Examples of full Nautilus instrument IDs: - `BTC-USD.COINBASE` (spot Bitcoin/USD). - `ETH-USDC.COINBASE` (spot Ether/USDC). - `BIP-20DEC30-CDE.COINBASE` (BTC perpetual swap). - `BIT-24APR26-CDE.COINBASE` (BTC dated future, Apr 2026). ### Aliased products (USDC and USD) Coinbase consolidates USDC- and USD-quoted versions of the same pair into a single matching-engine book and exposes the relationship in `GET /products` via the `alias` and `alias_to` fields: ```text BTC-USD : alias="" alias_to=["BTC-USDC"] # canonical BTC-USDC: alias="BTC-USD" alias_to=[] # alias of BTC-USD ``` When a caller subscribes or submits using the alias side, the venue rewrites the request to the canonical id on the wire. The adapter records the `product_id -> alias` map at instrument bootstrap and handles the rewrite transparently on both sides: - Data subscriptions go out on the canonical id. The data WebSocket client holds the reverse mapping and re-keys inbound messages back to the caller-supplied id before parsing. - Orders are submitted on the caller's `product_id`. The execution client records that id under the `client_order_id` and re-keys the canonical id the user channel echoes back, so an alias-side order is never reported against the canonical instrument. A strategy holding only USDC can therefore trade `BTC-USDC.COINBASE` end to end without referencing the canonical `BTC-USD`. Settlement currency is determined by the submitted `product_id`, so an order placed on `BTC-USDC.COINBASE` always debits or credits the USDC wallet. ## Environments Coinbase provides two trading environments. Configure the appropriate environment using the `environment` field in your client configuration. | Environment | `environment` value | REST base URL | | ----------- | ----------------------------- | ---------------------------------- | | Live | `CoinbaseEnvironment.LIVE` | `https://api.coinbase.com` | | Sandbox | `CoinbaseEnvironment.SANDBOX` | `https://api-sandbox.coinbase.com` | ### Live (production) The default environment for live trading with real funds. ```python config = CoinbaseExecutionClientConfig( api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", # environment=CoinbaseEnvironment.LIVE (default) ) ``` Environment variables: `COINBASE_API_KEY`, `COINBASE_API_SECRET`. ### Sandbox A static-mock test environment for integration plumbing, per the [Sandbox docs](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/sandbox). ```python config = CoinbaseExecutionClientConfig( api_key="ANY_NON_EMPTY_STRING", # required by the adapter constructor api_secret="ANY_NON_EMPTY_STRING", environment=CoinbaseEnvironment.SANDBOX, ) ``` The sandbox venue does not enforce authentication, but `CoinbaseExecutionClient::new` still requires both fields (or the matching environment variables) to be present in order to construct. :::warning **Sandbox is not a parallel trading venue:** - All responses are static and pre-defined; there is no live market or dynamic pricing. - Only Accounts and Orders endpoints are available; other resources are not. - Authentication is not required (and not enforced). - A custom `X-Sandbox` request header can trigger predefined error scenarios. Use sandbox to wire up your client and verify request/response shape; use production (with real funds and care) for any realistic behavior testing. ::: ## Authentication Coinbase Advanced Trade uses ES256 JWT authentication. Each REST request and each WebSocket subscription generates a short-lived JWT signed with your EC private key. The adapter resolves credentials from environment variables or from the config fields. ### Creating an API key Coinbase has several key types. The adapter requires a **Coinbase App Secret API key** with the **ECDSA** signature algorithm (not Ed25519). Go to the CDP portal API keys page: [portal.cdp.coinbase.com/projects/api-keys](https://portal.cdp.coinbase.com/projects/api-keys). Select the **Secret API Keys** tab and click **Create API key**. Enter a nickname (e.g. `nautilus-trading`). Expand **API restrictions** and set permissions to **View** and **Trade**. Expand **Advanced Settings** and change the signature algorithm from Ed25519 to **ECDSA**. This step is required: Ed25519 keys do not work with the Advanced Trade API. Click **Create API key**. Save the key name and private key from the modal. The key name looks like `organizations/{org_id}/apiKeys/{key_id}`. The private key is a PEM-encoded EC key (SEC1 format). :::warning Coinbase no longer auto-downloads the key file. Copy the values from the creation modal or click the download button before closing it. You cannot retrieve the private key afterward. ::: :::info Do not use expired legacy Coinbase App API keys. Create a CDP API key and select the ECDSA algorithm; the adapter signs requests with ES256. See Coinbase's [legacy key migration guide](https://docs.cdp.coinbase.com/coinbase-app/authentication-authorization/legacy-keys). ::: For full details see the Coinbase [API key authentication guide](https://docs.cdp.coinbase.com/coinbase-app/authentication-authorization/api-key-authentication). ### Environment variables | Variable | Description | | --------------------- | ----------------------------------------------------- | | `COINBASE_API_KEY` | Key name (`organizations/{org_id}/apiKeys/{key_id}`). | | `COINBASE_API_SECRET` | PEM-encoded EC private key (full multi-line string). | Example: ```bash export COINBASE_API_KEY="organizations/abc-123/apiKeys/def-456" export COINBASE_API_SECRET="$(cat ~/path/to/cdp_api_key.pem)" ``` :::tip We recommend using environment variables to manage your credentials. ::: ### JWT lifetime Coinbase JWTs expire after 120 seconds. Per the [WebSocket overview](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-overview), a different JWT must be generated for each authenticated WebSocket message (i.e. for each subscribe). The adapter regenerates a fresh JWT for every signed REST request and for every authenticated subscribe message; no manual rotation is required. ## Portfolios A Coinbase account holds one or more **portfolios**. Each portfolio has its own wallets (USD, USDC, BTC, etc.), balances, and order scope. Every account has a `DEFAULT` portfolio; users can create additional `CONSUMER` portfolios to segregate strategies, risk, or tax lots. A CDP API key is **bound to a single portfolio at creation time**. Every authenticated request (account lookup, order submission, cancel) operates against that portfolio unless a different one is explicitly specified. ### Finding your portfolio UUIDs Run the adapter's authenticated probe binary; it prints the portfolios visible to your CDP key, the account balances in the bound portfolio, and a few reference REST calls: ```bash cargo run --bin coinbase-http-private --package nautilus-coinbase ``` Sample output: ```text Found 1 portfolio(s) name=Default type=DEFAULT uuid=ca7244bc-21d1-5e4c-bfe5-80f208ac5723 deleted=false Account has 3 balance(s) USDC total=100.00000000 USDC free=100.00000000 USDC locked=0.00000000 USDC AUD total=0.00 AUD free=0.00 AUD locked=0.00 AUD BTC total=0.00000000 BTC free=0.00000000 BTC locked=0.00000000 BTC ``` Equivalent curl (you have to sign your own ES256 JWT with your CDP PEM key first): ```bash curl -H "Authorization: Bearer $JWT" \ https://api.coinbase.com/api/v3/brokerage/portfolios ``` ### When `retail_portfolio_id` is required Coinbase's `POST /orders` endpoint routes to the key's bound portfolio by default, so a single-portfolio account does not need to set this field. Set it on [`CoinbaseExecutionClientConfig`](#execution-client-configuration-options) when either is true: - The account holds multiple portfolios and you want to trade against one that is not the key's default. - The venue rejects orders with `account is not available` and the wallet diagnosis below has been ruled out. :::note Coinbase's [Create Order reference](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/create-order) marks `retail_portfolio_id` as deprecated and applicable only to legacy keys, stating that CDP keys default to the key's permissioned portfolio. The adapter still sends the field when configured, so it remains available if the venue's default routing does not match your account layout. ::: ### Creating a new portfolio Most users will not need to create a new portfolio; the account's default works out of the box. Create one on [coinbase.com/portfolios](https://www.coinbase.com/portfolios) only if you want to: - Segregate API-driven trading from manual retail activity. - Isolate risk or P&L between strategies. - Work around a restricted default (e.g. a Vault). After creating a portfolio, fund it (transfer from the default portfolio's wallet on coinbase.com) before sending any orders, otherwise the venue returns `account is not available` for the quote currency. ### Troubleshooting `account is not available` The venue returns this error for several distinct reasons; diagnose by running the probe binary above and inspecting the portfolio wallet list. | Symptom | Likely cause | Fix | | -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Rejected only for a specific product (e.g. `BTC-USD` with only USDC) | Portfolio is missing a wallet for the product's quote currency. USD and USDC are separate on Coinbase, and the venue routes orders by the submitted `product_id`, not by the canonical alias. | Submit against the product whose quote currency you hold (e.g. `BTC-USDC` for USDC wallets). The adapter resolves the data-side alias internally; no config change needed. Funding the missing wallet via coinbase.com is also an option but unnecessary when only one currency is held. | | Every order rejected across all products | Key is bound to a non-default portfolio and `retail_portfolio_id` is unset. | Set `retail_portfolio_id` on `CoinbaseExecutionClientConfig` to the target portfolio UUID. | | Rejected for `*-USD` products on a non-US account | Jurisdictional restriction (e.g. AU accounts cannot trade USD-quoted pairs). | Use locally-available quotes (USDC, AUD, EUR, etc.) instead of USD. | | Rejected right after key rotation | New key was created in a different portfolio than the previous one. | Update `retail_portfolio_id` to match the new key's portfolio, or move funds. | ## Market data The data client serves everything except derivatives index and funding data from WebSocket channels. Coinbase Advanced Trade does not publish index prices or funding rates on any WebSocket channel, so those two streams are sourced from REST polling instead. | Nautilus subscription | Source | Notes | | --------------------- | --------------------------- | --------------------------------------------------------------------------- | | Book deltas | `level2` channel | `L2_MBP` only; other book types are rejected. | | Quotes | `ticker` channel | Top-of-book from the venue's ticker payload. | | Trades | `market_trades` channel | Also available as a REST request. | | Bars | `candles` channel | Fixed five-minute buckets; the venue accepts no granularity parameter. | | Instrument status | `status` channel | See [Instrument status](#instrument-status). | | Index prices | REST `/products/{id}` poll | Derivatives only, at `derivatives_poll_interval_secs`. | | Funding rates | REST `/products/{id}` poll | Perpetuals only; see [Funding rates](#funding-rates). | | Mark prices | *Not supported by Coinbase* | The subscription is rejected rather than synthesized from settlement price. | A `heartbeats` subscription is always sent on connect and replayed on every reconnect. It satisfies the venue's five-second subscribe deadline and keeps the connection alive when the subscribed product topics are quiet. ### Bars Historical bar requests accept EXTERNAL aggregation at the granularities the adapter maps: 1m, 5m, 15m, 30m, 1h, 2h, 6h, and 1d. Any other step or aggregation is rejected. Coinbase's `/products/{id}/candles` endpoint also accepts `FOUR_HOUR`, which the adapter does not currently map. Live bar subscriptions are different: the WebSocket `candles` channel takes no granularity parameter and publishes five-minute buckets only. The adapter stamps each received candle with the `BarType` registered for that product, so subscribing at any other bar specification yields five-minute bars labeled with the requested type. Request a `5-MINUTE-LAST-EXTERNAL` bar type for live subscriptions, and use historical requests for the other granularities. ### Funding rates The adapter polls the REST `/products/{id}` endpoint at `derivatives_poll_interval_secs` (default 15 s) and emits a `FundingRateUpdate` from the FCM `future_product_details` payload when `funding_rate` is present. The funding interval is parsed from the `funding_interval` field (typically `"3600s"`, hourly funding) and the next funding timestamp from `funding_time`. Coinbase Advanced Trade does not publish `funding_rate` on the WebSocket `ticker` channel, so REST polling is the only live source. Historical funding rate requests are not implemented. ### Instrument status `subscribe_instrument_status` joins the Coinbase WebSocket `status` channel on first subscription (the venue publishes one status feed for all products), filters incoming events to the subscribed instruments, and emits `InstrumentStatus` events with `MarketStatusAction::Trading` for `online`, `Halt` for `offline`, and `Close` for `delisted`. Products reporting an unset status (futures) or a status the adapter does not model carry no information for the data engine and are skipped. The channel subscription is dropped when the last instrument unsubscribes. ## Orders capability The tables below describe the Coinbase **venue** order surface. The shipped [`CoinbaseExecutionClient`](#execution-scope) handles spot or CFM derivatives based on the configured `account_type`. Coinbase order capabilities differ between Spot and Derivatives (perpetuals and dated futures share the same FCM order surface). ### Execution scope `CoinbaseExecutionClientFactory` produces a single `CoinbaseExecutionClient` type. The product family is selected by the `account_type` field on `CoinbaseExecutionClientConfig`: | `account_type` | Bootstrap instruments | Account state source | | --------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | `AccountType::Cash` | `CoinbaseProductType::Spot` only. | `/accounts` REST endpoint. | | `AccountType::Margin` | `CoinbaseProductType::Future` (perp + dated). | CFM `balance_summary` REST + `futures_balance_summary` WS, plus position reports from `cfm/positions`. | Other account types are rejected at factory creation. OMS is always `Netting` because the venue does not expose hedge mode. To prevent cross-account bleed-through: 1. Connect-time instrument bootstrap is limited to the configured product family; the other family's products never enter the in-process cache. 2. `submit_order` denies any order whose instrument is outside that cache. 3. `generate_order_status_report(s)` and `generate_fill_reports` post-filter their output through the same cache, so a Coinbase account that holds both spot and derivative activity will not surface the other scope's reports through a single client. Run one execution client per scope; if you need both spot and CFM activity on the same trader, instantiate two clients with distinct `account_type` values (and distinct `account_id`s). ### Order types The matrix lists order types as exposed through the Nautilus model. The right column shows the corresponding `order_configuration` keys the adapter emits. Coinbase order types not in this table (TWAP, Bracket, Scaled, SOR LIMIT IOC) are documented under [Advanced order features](#advanced-order-features) and noted there as *Not currently implemented* by the adapter. | Order Type | Spot | Perpetual | Future | Wire shape | | ---------------------- | ---- | --------- | ------ | --------------------------------------------------------------- | | `MARKET` | ✓ | ✓ | ✓ | `market_market_ioc` (all products); `market_market_fok` (perps) | | `LIMIT` | ✓ | ✓ | ✓ | `limit_limit_gtc` / `limit_limit_gtd` / `limit_limit_fok` | | `STOP_LIMIT` | ✓ | ✓ | ✓ | `stop_limit_stop_limit_gtc` / `stop_limit_stop_limit_gtd` | | `STOP_MARKET` | - | - | - | *Not supported by Coinbase*. | | `MARKET_IF_TOUCHED` | - | - | - | *Not supported by Coinbase*. | | `LIMIT_IF_TOUCHED` | - | - | - | *Not supported by Coinbase*. | | `TRAILING_STOP_MARKET` | - | - | - | *Not supported by Coinbase*. | ### Execution instructions | Instruction | Spot | Perpetual | Future | Notes | | ------------- | ---- | --------- | ------ | ----------------------------------------------------------------------------- | | `post_only` | ✓ | ✓ | ✓ | LIMIT GTC and LIMIT GTD only. | | `reduce_only` | - | - | - | *Not supported by Coinbase*; see [Derivatives trading](#derivatives-trading). | ### Time in force The adapter accepts the values in this matrix; combinations not listed are rejected at submit time with `"Unsupported TIF {tif} for {order_type}"`. | Order type | GTC | GTD | IOC | FOK | Notes | | ------------ | --- | --- | --- | --- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `MARKET` | ✓ | - | ✓ | (✓) | GTC is mapped to IOC; explicit IOC is honored. FOK builds `market_market_fok`, which Coinbase documents as perpetuals-only and rejects on spot. | | `LIMIT` | ✓ | ✓ | - | ✓ | GTD requires `expire_time`. LIMIT IOC *not currently implemented* (see [SOR LIMIT IOC](#advanced-order-features)). | | `STOP_LIMIT` | ✓ | ✓ | - | - | Requires `trigger_price`. | ### Advanced order features | Feature | Spot | Perpetual | Future | Notes | | ------------------ | ---- | --------- | ------ | ---------------------------------------------------------------------------------------------- | | Order Modification | ✓ | - | - | Open GTC variants only; Coinbase rejects futures-venue edits with `CANNOT_EDIT_FUTURES_ORDER`. | | Bracket Orders | - | - | - | *Not currently implemented*. Venue exposes `trigger_bracket_gtc` / `trigger_bracket_gtd`. | | OCO Orders | - | - | - | *Not supported by Coinbase* as a distinct order type. | | Iceberg Orders | - | - | - | *Not supported by Coinbase*. | | TWAP Orders | - | - | - | *Not currently implemented*. Venue exposes `twap_limit_gtd`. | | Scaled Orders | - | - | - | *Not currently implemented*. Venue exposes `scaled_limit_gtc`. | | SOR LIMIT IOC | - | - | - | *Not currently implemented*. Venue exposes `sor_limit_ioc` for smart-order-routed LIMIT IOC. | See the [Create Order reference](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/create-order) and [Edit Order reference](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/edit-order) for the underlying venue specification. ### Position controls (derivatives) | Control | Notes | | ------------- | ------------------------------------------- | | Leverage | Set per order; default `1.0`. | | Margin type | Set per order: cross (default) or isolated. | | Position mode | One-way only; hedge mode is not exposed. | ### Batch operations | Operation | Notes | | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | | Batch Submit | Not supported. Each order is one `Create Order` request. | | Batch Modify | Not supported. Each edit is one `Edit Order` request. | | Batch Cancel | `POST /api/v3/brokerage/orders/batch_cancel` accepts an `order_ids` array. No documented max size; per-order success/failure in the response. | ### Order querying | Feature | Spot | Perpetual | Future | Notes | | -------------------- | ---- | --------- | ------ | ------------------------------------------- | | Query open orders | ✓ | ✓ | ✓ | List all active orders. | | Query order history | ✓ | ✓ | ✓ | Historical order data with cursor paging. | | Order status updates | ✓ | ✓ | ✓ | Real-time state changes via `user` channel. | | Trade history | ✓ | ✓ | ✓ | Execution and fill reports. | ### Spot trading limitations - MARKET FOK is not accepted on spot; Coinbase documents `market_market_fok` as perpetuals-only and rejects it with `UNSUPPORTED_ORDER_CONFIGURATION`. - Quote-denominated MARKET orders are supported; LIMIT orders are sized in base units. ### Derivatives trading Coinbase derivatives trade through the FCM (Futures Commission Merchant) venue. The exec client submits orders through the same `POST /orders` endpoint used for spot; per-order `leverage` and `margin_type` (`CROSS` or `ISOLATED`) defaults come from `CoinbaseExecutionClientConfig.default_leverage` and `default_margin_type`. Margin balances update from both the REST `cfm/balance_summary` endpoint (connect-time snapshot, `query_account`, and on WebSocket reconnect) and the authenticated `futures_balance_summary` WebSocket channel. Position reports come from the REST `cfm/positions` endpoints. Coinbase's Advanced Trade API does not document a `reduce_only` field on the create-order schema. The execution client rejects reduce-only orders before transport instead of submitting them without the instruction. The adapter logs a warning when a REST order status report describes a forced-close order, and when the CFM balance summary reports a liquidation buffer below 20% of the liquidation threshold. Coinbase does not flag auto-deleveraging separately from liquidation, so both surface through the same warning. The user channel carries no equivalent warning, so forced closes are visible from reconciliation rather than from the live stream. ## Execution client behavior This section documents how `CoinbaseExecutionClient` translates Nautilus order commands and Coinbase venue events into Nautilus execution events. ### Order submission `submit_order` builds the Coinbase `order_configuration` shape directly from Nautilus order fields: - `MARKET` IOC and GTC (the Nautilus default) -> `market_market_ioc`; FOK -> `market_market_fok`. `Day` and `Gtd` are rejected before the HTTP call so callers do not silently receive IOC semantics. A `MARKET` order built with `Gtc` executes as IOC at the venue; strategies that require strict backtest/live parity should construct `MarketOrder` with `Ioc` explicitly. - `LIMIT` GTC -> `limit_limit_gtc`, GTD -> `limit_limit_gtd` (requires `expire_time`), FOK -> `limit_limit_fok`. - `STOP_LIMIT` GTC -> `stop_limit_stop_limit_gtc`, GTD -> `stop_limit_stop_limit_gtd`. Stop direction is derived from the order side (`Buy` -> `STOP_DIRECTION_STOP_UP`, `Sell` -> `STOP_DIRECTION_STOP_DOWN`). - `STOP_MARKET`, `MARKET_IF_TOUCHED`, `LIMIT_IF_TOUCHED`, and trailing-stop variants are not supported by Coinbase. They surface as `OrderRejected` carrying the `build_order_configuration` error from the spawned submit task (the order is emitted as `OrderSubmitted` first). On a successful HTTP create, an `OrderAccepted` is emitted carrying the venue order ID returned in `success_response.order_id`. On a `success=false` response, `OrderRejected` is emitted with the formatted venue failure reason. Because any submit attempt may have reached Coinbase, a transport error, timeout, rate-limit response, decode failure, or HTTP 5xx does not prove rejection. The adapter leaves the order in flight and retains its submit metadata until the user channel or reconciliation resolves it. ### Order modification `modify_order` posts to `/orders/edit` with the typed `EditOrderRequest`. Coinbase supports edits on open GTC variants only, and rejects edits on futures-venue orders with `CANNOT_EDIT_FUTURES_ORDER`, so modification is effectively spot-only. Other order types must use cancel-replace. Coinbase's `/orders/edit` requires both `price` and `size` even when only one is changing; an omitted `size` is read as 0 and rejected with `INVALID_EDITED_SIZE` or `CANNOT_EDIT_TO_BELOW_FILLED_SIZE`. The exec client auto-fills missing fields from the cached order, so strategies can call `modify_order(price=X)` without repeating the current quantity. Values from the `ModifyOrder` command win; otherwise the cached order's current `price` and `quantity` are used. Venue edit failures emit `OrderModifyRejected` with the typed `EditOrderResponse` reason (preferring `edit_failure_reason`, falling back to `preview_failure_reason`). HTTP failures with unknown venue outcome leave the order in `PENDING_UPDATE` until an update, query result, or reconciliation resolves it. ### Cancellation - `cancel_order` posts a single-id `batch_cancel`. An explicit per-order venue failure surfaces as `OrderCancelRejected`; a whole-request transport failure with unknown venue outcome leaves the order in `PENDING_CANCEL` for reconciliation. - `cancel_all_orders` lists open orders via REST without the `OPEN`-only filter (because Coinbase's `OPEN` filter excludes `PENDING` and `QUEUED` orders that are still cancelable), filters locally to `{Accepted, Triggered, PendingUpdate, PartiallyFilled}` and the requested side, then chunks `batch_cancel` calls in groups of 100. Per-order venue failures emit `OrderCancelRejected`; whole-request failures with unknown venue outcome leave affected orders pending reconciliation. - `batch_cancel_orders` chunks the same way and surfaces explicit per-order venue failures as `OrderCancelRejected`. Transport failures with unknown venue outcome leave affected orders pending reconciliation. ### User WebSocket channel `CoinbaseExecutionClient` subscribes to the `user` channel with no `product_ids` filter and a fresh JWT, parses each event into an `OrderStatusReport`, and feeds it to the execution event stream. Coinbase reports cumulative state per order rather than per-trade fills, so the exec client synthesizes a `FillReport` from the cumulative delta. The per-fill price is derived as `(avg_now * qty_now - avg_prev * qty_prev) / delta_qty` so multi-fill orders carry the correct trade price, not the cumulative weighted average. The original quantity is restored on terminal updates (`CANCELLED`, `EXPIRED`, `FAILED`) where the venue zeroes `leaves_quantity`. The user channel does not echo `price`, `stop_price`, `trigger_type`, or maker/taker classification. The exec client caches these at submit time under the `client_order_id` and patches reports before emit, so the reconciler does not observe a `Some(price) -> None` divergence and `post_only` fills are correctly stamped `liquidity_side = Maker`. Order status `PENDING`, `QUEUED`, and `OPEN` all map to `OrderStatus::Accepted` to avoid spurious backwards-transition warnings when user-channel updates race the REST `OrderAccepted` event. A `submit_order` rejection carrying `INVALID_LIMIT_PRICE_POST_ONLY` (or the preview/new-order equivalent) is emitted with `due_post_only = true` so strategies can react to post-only crossings (typically by re-quoting against the new TOB). On reconnect, account state is re-fetched via REST so balance changes during the disconnect window are recovered. Cumulative per-order tracking persists across reconnects so synthesized fill deltas remain correct. ### Fill deduplication The user-channel WebSocket can replay events on reconnect. The execution client maintains a 10,000-entry FIFO dedup keyed on `(venue_order_id, trade_id)` and drops any fill whose synthesized trade ID matches a recently-seen one. The cumulative-state map is bounded with the same capacity to protect against orders that never receive a terminal event in this client's lifetime. After very long disconnections (beyond the in-memory dedup window) replayed fills may emit duplicate `FillReport` values; strategies should rely on REST reconciliation to recover canonical state in that case. ### Position reconciliation For Cash (spot) accounts the client returns no position reports because Coinbase spot has no positions. For Margin accounts position reports come from the REST `cfm/positions` (list) and `cfm/positions/{product_id}` (single) endpoints and are post-filtered to the bootstrap instrument cache. Open orders and historical fills are reconciled from REST via `generate_order_status_report(s)` and `generate_fill_reports` on connect and on the standard reconciliation interval set by `LiveExecutionEngineConfig`. ## Rate limiting Coinbase publishes the following limits for the Advanced Trade APIs: | Surface | Limit | Source | | ------------------------------ | ----------------------------------------------------------------------------------- | ------------------------------------ | | WebSocket connections | 8 per second per IP address | Advanced Trade WebSocket Rate Limits | | WebSocket unauthenticated msgs | 8 per second per IP address | Advanced Trade WebSocket Rate Limits | | WebSocket subscribe deadline | First subscribe message must arrive within 5 s of connect or the server disconnects | Advanced Trade WebSocket Overview | | Authenticated WebSocket JWT | 120 s; a fresh JWT must be generated for every authenticated subscribe message | Advanced Trade WebSocket Overview | | REST per-key quota | 10,000 requests per hour per API key (Coinbase App general policy) | Coinbase App Rate Limiting | When the REST limit is exceeded, Coinbase returns HTTP `429` with this body: ```json { "errors": [ { "id": "rate_limit_exceeded", "message": "Too many requests" } ] } ``` The adapter additionally throttles all REST traffic client-side at 30 requests per second, and subscribe and unsubscribe messages at 8 per second, so bursts are shaped before they reach the venue. :::info Coinbase's current Advanced Trade documentation publishes WebSocket limits but no Advanced Trade-specific REST quota (per-second ceilings, per-portfolio limits), so the Coinbase App per-hour quota above is the most specific documented value. References: [WebSocket rate limits](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-rate-limits), [WebSocket overview](https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-overview), [Coinbase App rate limiting](https://docs.cdp.coinbase.com/coinbase-app/api-architecture/rate-limiting). ::: ## Reconnect and resubscribe The WebSocket client uses exponential backoff with a base of 250ms and a cap of 30s on reconnect. Retained subscriptions are replayed automatically after the handshake completes. Coinbase disconnects a client that has not sent a subscribe message within 5 seconds of connecting, so the replay always includes the `heartbeats` topic, which is marked before the first replay and kept for the client's lifetime. For authenticated channels (`user`, and `futures_balance_summary` on Margin clients), the adapter generates a fresh JWT for every subscribe message, as Coinbase requires a different JWT for each authenticated WebSocket message. A topic that requires authentication is skipped rather than sent unsigned when no credentials are configured or the JWT cannot be built, and the failure surfaces as an error on the client's message stream. Once a subscription is accepted the data flow continues for the lifetime of the WebSocket connection without further authentication. If the execution client is connected again while a prior user WebSocket is still active or reconnecting, it tears that connection down and rebuilds the inner client rather than reusing the existing state machine. This guarantees a fresh command channel, output channel, and shutdown signal even when the previous session's `Disconnect` command lost a race with the shutdown signal. ## Configuration ### Data client configuration options | Option | Default | Description | | ---------------------------------- | --------- | --------------------------------------------------------------------------------- | | `api_key` | `None` | Falls back to `COINBASE_API_KEY` env var. | | `api_secret` | `None` | Falls back to `COINBASE_API_SECRET` env var. | | `base_url_rest` | `None` | Override for the REST base URL. | | `base_url_ws` | `None` | Override for the WebSocket market data URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `environment` | `Live` | `Live` or `Sandbox`. | | `http_timeout_secs` | `10` | HTTP request timeout (seconds). | | `ws_timeout_secs` | `30` | WebSocket timeout (seconds). | | `update_instruments_interval_mins` | `60` | Interval between instrument catalog refreshes. | | `derivatives_poll_interval_secs` | `15` | Interval between REST polls that emit `IndexPriceUpdate` and `FundingRateUpdate`. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | ### Execution client configuration options | Option | Default | Description | | ------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `account_id` | `Venue` | Nautilus account identifier; defaults to `COINBASE-001`. | | `api_key` | `None` | Falls back to `COINBASE_API_KEY` env var. | | `api_secret` | `None` | Falls back to `COINBASE_API_SECRET` env var. | | `base_url_rest` | `None` | Override for the REST base URL. | | `base_url_ws` | `None` | Override for the user data WebSocket URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `environment` | `Live` | `Live` or `Sandbox`. | | `http_timeout_secs` | `10` | HTTP request timeout (seconds). | | `max_retries` | `3` | Maximum retry attempts for HTTP requests. | | `retry_delay_initial_ms` | `100` | Initial retry delay (milliseconds). | | `retry_delay_max_ms` | `5,000` | Maximum retry delay (milliseconds). | | `account_type` | `Cash` | `Cash` for spot or `Margin` for CFM derivatives. See [Execution scope](#execution-scope). | | `default_margin_type` | `None` | Default `CoinbaseMarginType` (`Cross` or `Isolated`) applied to derivatives orders. Ignored on Cash. | | `default_leverage` | `None` | Default leverage applied to derivatives orders. Ignored on Cash. | | `retail_portfolio_id` | `None` | CDP retail portfolio UUID, sent on create-order when set. Coinbase marks the field deprecated for CDP keys. See [Portfolios](#portfolios). | | `transport_backend` | `Sockudo` | WebSocket transport backend. | Configurations are constructed from the adapter's public Python module: ```python from nautilus_trader.adapters.coinbase import CoinbaseDataClientConfig from nautilus_trader.adapters.coinbase import CoinbaseEnvironment from nautilus_trader.adapters.coinbase import CoinbaseExecutionClientConfig from nautilus_trader.model import AccountId data_config = CoinbaseDataClientConfig( api_key="YOUR_COINBASE_API_KEY", api_secret="YOUR_COINBASE_API_SECRET", environment=CoinbaseEnvironment.LIVE, ) exec_config = CoinbaseExecutionClientConfig( account_id=AccountId("COINBASE-001"), api_key="YOUR_COINBASE_API_KEY", api_secret="YOUR_COINBASE_API_SECRET", environment=CoinbaseEnvironment.LIVE, ) ``` The current Python examples show how to pair these configs with `CoinbaseDataClientFactory` and `CoinbaseExecutionClientFactory` in `LiveNode.builder(...)`. ## Known limitations ### Venue-side - Order modification is restricted to open GTC orders and is rejected on futures-venue orders with `CANNOT_EDIT_FUTURES_ORDER`; everything else must use cancel-replace. - OCO orders are not exposed as a distinct order type. - Trailing stop, MARKET_IF_TOUCHED, LIMIT_IF_TOUCHED, and iceberg orders are not supported by Coinbase. - Mark prices are not published on REST or WebSocket, so mark price subscriptions are rejected. - Batch submit and batch modify are not available; only batch cancel is. - Sandbox is a static-mock environment (Accounts and Orders endpoints only, pre-defined responses, no real market data). - The user-channel WebSocket reports cumulative per-order state, not per-trade fills. The exec client derives per-fill quantity, price, and commission from the cumulative delta; per-trade `trade_id`s are synthesized from `(venue_order_id, cumulative_quantity)`. ### Adapter-side - **Stable fill identity differs across live and REST paths.** The user channel does not provide Coinbase's per-fill `trade_id`, so live `FillReport` values use IDs synthesized from the venue order ID and cumulative quantity. REST reconciliation uses the venue `trade_id`, so the identifiers can differ across live processing and reconciliation. - **One product family per client.** Submission, modification, cancellation, and report generation are filtered to the configured product family (spot under `AccountType::Cash`; perp + dated futures under `AccountType::Margin`). Orders whose instrument falls outside the bootstrapped cache are denied. See [Execution scope](#execution-scope). - **Position reports are always empty for Cash accounts.** Coinbase spot has no positions. Derivatives (CFM) position reports come from `cfm/positions` and appear only on Margin clients. - **User-channel updates omit `price`, `stop_price`, and `trigger_type`.** For orders this client submitted, the missing fields are patched from a cache populated at `submit_order` time. For external orders (submitted by another process or via the Coinbase UI), the user-channel handler enriches the report on first sight by fetching `/orders/historical/{venue_order_id}` and caching the result. The REST call adds latency to the first user-channel update for an external order; subsequent updates use the cached enrichment. - **Cancel-all and batch-cancel REST list failures are logged only.** If the list-open-orders REST call fails, no per-order `OrderCancelRejected` is emitted; orders remain in `PendingCancel` until the next reconciliation recovers them. Mirrors the Bybit adapter pattern. - **Newly listed products require a reconnect to be tradeable.** The instrument cache is populated on connect; products listed after that are not in the cache and `submit_order` will deny them. - **MARKET orders default to IOC.** A `MarketOrder` constructed with the Nautilus default `TimeInForce::Gtc` is mapped to `market_market_ioc` at the venue. Explicit `TimeInForce::Ioc` is honored; `TimeInForce::Fok` routes to `market_market_fok`, which Coinbase documents as perpetuals-only and rejects at runtime on spot with `UNSUPPORTED_ORDER_CONFIGURATION`. `Day` and `Gtd` are rejected at submit time. - **Historical funding rate requests are not implemented.** Funding rates are available only as live updates from the derivatives REST poll. - **Live bar subscriptions ignore the requested granularity.** The venue's `candles` channel publishes five-minute buckets and accepts no granularity parameter, and `subscribe_bars` does not reject other bar specifications. A subscription at any other step receives five-minute bars stamped with the requested `BarType`. See [Bars](#bars). ## Diagnostic binaries Three binaries assist with connectivity checks, live verification, and account hygiene: - `coinbase-http-public` requests spot instruments, a product book, and recent trades without credentials. Use it to confirm connectivity before configuring an API key. - `coinbase-http-private` lists portfolios, prints wallet balances, runs `/orders/preview` for `BTC-USD` and `BTC-USDC`, and surfaces per-product gating flags. Recommended first stop when bringing a new account online. - `coinbase-cancel-all-open` cancels every open order on the authenticated CDP key. Useful between test runs to clear resting orders. The two authenticated binaries read `COINBASE_API_KEY` and `COINBASE_API_SECRET` from the environment. ## Contributing :::info For additional features or to contribute to the Coinbase adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Databento Source: https://nautilustrader.io/docs/latest/integrations/databento/ NautilusTrader includes an adapter for the [Databento](https://databento.com/) API and for data in [Databento Binary Encoding (DBN)](https://databento.com/docs/standards-and-conventions/databento-binary-encoding). Databento is a market data provider only, so the adapter does not include an execution client. Pair it with a sandbox for simulated execution, route execution through another adapter such as Interactive Brokers, or use it to calculate traditional asset class signals for crypto trading. The adapter supports: - Loading historical data from DBN files and decoding to Nautilus objects for backtesting or catalog storage. - Requesting historical data decoded to Nautilus objects for live trading and backtesting. - Subscribing to real-time data feeds decoded to Nautilus objects for live trading and sandbox environments. :::tip [Databento](https://databento.com/signup) offers $125 in free data credits for new sign-ups. Apply the credits to historical data requests, or offset them against a subscription plan. Credits are shared across a team and expire six months after signup. With careful requests, this covers testing and evaluation. Check the [metadata.get_cost](https://databento.com/docs/api-reference-historical/metadata/metadata-get-cost) endpoint before requesting data. ::: ## Overview The adapter uses the [databento-rs](https://crates.io/crates/databento) crate, Databento's official Rust client library. :::info You do not need to install `databento` separately. The adapter compiles as a static library and links automatically during the build. ::: The following adapter classes are available: - `DatabentoDataLoader`: Loads DBN data from files. - `DatabentoHistoricalClient`: Fetches historical market data and instrument definitions via the Databento HTTP API. - `DatabentoLiveClient`: Subscribes to real-time data feeds via Databento's raw TCP API. - `DatabentoDataClient`: Data client for live trading nodes, wrapping the historical and live clients. - `DatabentoDataClientFactory`: Builds the data client from a `DatabentoDataClientConfig` for `LiveNode`. :::note Most users configure a live trading node (covered below) and do not work with these components directly. ::: ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/databento/) Rust examples live under [`crates/adapters/databento/examples/`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/databento/examples/). The data tester subscribes to live quotes and trades for the configured instrument when run: ```bash cargo run --example databento-data-tester --package nautilus-databento ``` ## Databento documentation See the [Databento new users guide](https://databento.com/docs/quickstart/new-user-guides). Refer to it alongside this integration guide. ## Databento Binary Encoding (DBN) Databento Binary Encoding (DBN) is a fast message encoding and storage format for normalized market data. The [DBN spec](https://databento.com/docs/standards-and-conventions/databento-binary-encoding) includes a self-describing metadata header and a fixed set of struct definitions that standardize how market data is normalized. The adapter decodes DBN data to Nautilus objects. The same Rust decoder handles: - Loading and decoding DBN files from disk. - Decoding historical and live data in real time. ## Supported schemas The following Databento schemas are supported by NautilusTrader: | Databento schema | Nautilus data type | Description | | :--------------------------------------------------------------------------- | :------------------------------- | :------------------------------ | | [MBO](https://databento.com/docs/schemas-and-data-formats/mbo) | `OrderBookDelta` | Market by order (L3). | | [MBP_1](https://databento.com/docs/schemas-and-data-formats/mbp-1) | `(QuoteTick, TradeTick \| None)` | Market by price (L1). | | [MBP_10](https://databento.com/docs/schemas-and-data-formats/mbp-10) | `OrderBookDepth10` | Market depth (L2). | | [BBO_1S](https://databento.com/docs/schemas-and-data-formats/bbo-1s) | `QuoteTick` | 1-second best bid/offer. | | [BBO_1M](https://databento.com/docs/schemas-and-data-formats/bbo-1m) | `QuoteTick` | 1-minute best bid/offer. | | [CMBP_1](https://databento.com/docs/schemas-and-data-formats/cmbp-1) | `(QuoteTick, TradeTick \| None)` | Consolidated MBP across venues. | | [CBBO_1S](https://databento.com/docs/schemas-and-data-formats/cbbo-1s) | `QuoteTick` | Consolidated 1-second BBO. | | [CBBO_1M](https://databento.com/docs/schemas-and-data-formats/cbbo-1m) | `QuoteTick` | Consolidated 1-minute BBO. | | [TCBBO](https://databento.com/docs/schemas-and-data-formats/tcbbo) | `(QuoteTick, TradeTick)` | Trade-sampled consolidated BBO. | | [TBBO](https://databento.com/docs/schemas-and-data-formats/tbbo) | `(QuoteTick, TradeTick)` | Trade-sampled best bid/offer. | | [TRADES](https://databento.com/docs/schemas-and-data-formats/trades) | `TradeTick` | Trade ticks. | | [OHLCV_1S](https://databento.com/docs/schemas-and-data-formats/ohlcv-1s) | `Bar` | 1-second bars. | | [OHLCV_1M](https://databento.com/docs/schemas-and-data-formats/ohlcv-1m) | `Bar` | 1-minute bars. | | [OHLCV_1H](https://databento.com/docs/schemas-and-data-formats/ohlcv-1h) | `Bar` | 1-hour bars. | | [OHLCV_1D](https://databento.com/docs/schemas-and-data-formats/ohlcv-1d) | `Bar` | Daily bars. | | [DEFINITION](https://databento.com/docs/schemas-and-data-formats/definition) | `Instrument` (various types) | Instrument definitions. | | [IMBALANCE](https://databento.com/docs/schemas-and-data-formats/imbalance) | `DatabentoImbalance` | Auction imbalance data. | | [STATISTICS](https://databento.com/docs/schemas-and-data-formats/statistics) | `DatabentoStatistics` | Market statistics. | | [STATUS](https://databento.com/docs/schemas-and-data-formats/status) | `InstrumentStatus` | Market status updates. | :::note Databento also documents reference schemas, including corporate actions, adjustment factors, and security master data. This adapter maps only the schemas listed above to Nautilus data types. Daily Databento OHLCV uses `ohlcv-1d`, and `ohlcv-eod` records also decode to daily bars. Official settlement prices and open interest come from the `statistics` schema, not OHLCV bars. ::: :::info Instrument definitions for unsupported `instrument_class` values (`'I'` Index, `'B'` Bond) are skipped with a warning rather than aborting the batch. FX spot definitions with currencies that Nautilus cannot map are also skipped. Index definitions come mainly from the Cboe Global Indices Feed (`MAIN.CGIF`) and the OPRA options publishers; `publishers.json` lists every publisher ID with its dataset and venue. Open an issue if you need Nautilus modeling for these. Statistics messages with `stat_type` values outside the modeled range (1-20) are also skipped with a warning. This includes the venue-specific values `VenueSpecificVolume1` (10001) and `VenueSpecificPrice1` (10002), which exceed the `u8` Arrow column width used for persistence. ::: ### Schema considerations - **TBBO and TCBBO**: Trade-sampled feeds that pair every trade with the BBO immediately *before* the trade's effect. Use them for trades aligned with contemporaneous quotes without managing two streams. - **MBP-1 and CMBP-1 (L1)**: Event-level updates that emit trades only on trade events. Choose them for a complete top-of-book event tape. For quote and trade alignment, prefer TBBO or TCBBO. - **MBP-10 (L2)**: Top 10 levels with trades. Use it for depth-aware strategies that do not need full MBO data. Includes orders per level. Databento serves this schema at 10 levels only, so depth requests must use `depth=10`. - **MBO (L3)**: Per-order events for queue position modeling and exact book reconstruction. `subscribe_book_deltas()` requests no snapshot, so a node subscription streams from the point of subscription with no initial book state. A strategy that needs a complete book must start before the trading session, seed the book from a historical request, or drive `DatabentoLiveClient.subscribe` directly with `snapshot=True`. - **BBO_1S/BBO_1M and CBBO_1S/CBBO_1M**: Sampled top-of-book updates at fixed intervals (1s or 1m). The adapter emits `QuoteTick` only for these schemas. Use them for monitoring, spreads, and low-cost signals. They are not suited for microstructure work. - **TRADES**: Trades only. For quote context alongside trades, subscribe with MBP-1, which emits a `TradeTick` on every trade event, or use TBBO or TCBBO. - **OHLCV**: Aggregated bars from trades. Use them for higher-timeframe analytics. Bars carry close timestamps by default; set `bars_timestamp_on_close=False` to timestamp on the interval open. Daily bars use `ohlcv-1d`; use `statistics` for official settlements and open interest. - **Imbalance and statistics**: Venue operational data with no built-in Nautilus equivalent. Reach them through the historical client, the data loader, or the direct live client, not through node subscriptions or requests (see [Imbalance and statistics](#imbalance-and-statistics)). - **Status**: Venue trading-state updates. Subscribe via `subscribe_instrument_status`. :::tip Consolidated schemas (CMBP_1, CBBO_1S, CBBO_1M, TCBBO) aggregate data across multiple venues. Useful for cross-venue analysis. ::: :::info See also the Databento [Schemas and data formats](https://databento.com/docs/schemas-and-data-formats) guide. ::: ## Dataset availability and selection Databento dataset IDs are separate from Nautilus venue identifiers. The adapter supports the schemas listed above, but each Databento dataset exposes its own subset. Check the metadata endpoints before adding a new dataset or schema to a live configuration: ```bash databento_auth="$(printf '%s:' "$DATABENTO_API_KEY" | base64 | tr -d '\n')" curl --header "Authorization: Basic ${databento_auth}" \ "https://hist.databento.com/v0/metadata.list_schemas?dataset=EQUS.MINI" curl --header "Authorization: Basic ${databento_auth}" \ "https://hist.databento.com/v0/metadata.list_unit_prices?dataset=EQUS.MINI" curl --header "Authorization: Basic ${databento_auth}" \ "https://hist.databento.com/v0/metadata.get_cost" \ --data-urlencode "dataset=EQUS.MINI" \ --data-urlencode "symbols=AAPL" \ --data-urlencode "stype_in=raw_symbol" \ --data-urlencode "schema=bbo-1s" \ --data-urlencode "start=2026-06-24T14:30:00Z" \ --data-urlencode "end=2026-06-24T14:31:00Z" ``` For the two common evaluation datasets: - `GLBX.MDP3` is the CME Globex MDP 3.0 dataset for CME, CBOT, NYMEX, and COMEX futures, options on futures, and spreads. It supports MBO, MBP-1, MBP-10, TBBO, trades, BBO intervals, OHLCV, definitions, statistics, and status. It does not expose the consolidated equity schemas (`cmbp-1`, `cbbo-*`, or `tcbbo`). - `EQUS.MINI` is Databento US Equities Mini. It is a derived aggregated top-of-book dataset with anonymized component venues. It supports MBP-1, TBBO, trades, BBO intervals, OHLCV, and definitions. It does not support MBO, MBP-10, imbalance, statistics, status, or consolidated schemas. Use `EQUS` as the Nautilus venue for US Equities Mini instruments: `AAPL.EQUS`, `MSFT.EQUS`, and so on. The built-in venue-to-dataset map routes `EQUS` to `EQUS.MINI`. Venue codes such as `XNAS` and `XNYS` refer to venue-specific datasets unless you override them with `venue_dataset_map`. :::warning If you override a venue such as `XNAS` to `EQUS.MINI`, keep downstream instrument IDs consistent. Mini records carry the consolidated `EQUS` publisher, and file or historical decoding without an explicit `instrument_id` emits `*.EQUS` identifiers. ::: Cost depends on the schema, symbols, and time range. For exploration, start with tight ranges, `definition`, `bbo-1s`, `bbo-1m`, or `trades`, and call `metadata.get_cost` before pulling historical time series data. Avoid duplicate quote and trade subscriptions when a combined schema such as `mbp-1` or `tbbo` already carries the data needed by the strategy. ## Subscriptions and requests Nautilus subscription methods map to Databento schemas as follows: | Nautilus subscription method | Default schema | Available Databento schemas | Nautilus data type | | :------------------------------ | :------------- | :--------------------------------------------------------------------------- | :----------------- | | `subscribe_instrument()` | `definition` | `definition` | `Instrument` | | `subscribe_quotes()` | `mbp-1` | `mbp-1`, `bbo-1s`, `bbo-1m`, `cmbp-1`, `cbbo-1s`, `cbbo-1m`, `tbbo`, `tcbbo` | `QuoteTick` | | `subscribe_trades()` | `trades` | `trades`, `tbbo`, `tcbbo`, `mbp-1`, `cmbp-1` | `TradeTick` | | `subscribe_book_deltas()` | `mbo` | `mbo` | `OrderBookDeltas` | | `subscribe_instrument_status()` | `status` | `status` | `InstrumentStatus` | Pass a non-default schema through the `schema` subscription parameter, as shown in the examples below. Only `subscribe_quotes()` and `subscribe_trades()` accept a choice; the other methods always use the single schema listed. The matching historical requests, `request_quotes()` and `request_trades()`, take the same `schema` values and defaults. :::warning The "Available Databento schemas" column lists adapter-supported choices for that Nautilus subscription method. The selected dataset must also support the schema. For example, `EQUS.MINI` cannot serve `mbo`, `mbp-10`, `statistics`, or `status`. ::: :::warning The live data client does not handle `subscribe_book_depth10()`, `subscribe_bars()`, or `subscribe_data()`. Those commands log a "handler not implemented" warning and deliver no data. Reach MBP-10 depth and OHLCV bars through historical requests (`request_book_depth()` and `request_bars()`), and imbalance and statistics through the historical client or the data loader. ::: :::note The examples below assume a `Strategy` or `DataActor` context where `self` has subscription methods. Import the required types: ```python from nautilus_trader.model import BarType from nautilus_trader.model import BookType from nautilus_trader.model import ClientId from nautilus_trader.model import InstrumentId DATABENTO_CLIENT_ID = ClientId.from_str("DATABENTO") instrument_id = InstrumentId.from_str("ES.c.0.GLBX") ``` ::: ### Instrument definition subscriptions ```python # Stream definition messages, which also populate the live price precision map self.subscribe_instrument( instrument_id=instrument_id, client_id=DATABENTO_CLIENT_ID, ) ``` ### Quote subscriptions (MBP and L1) ```python # Default MBP-1 quotes (also emits trades on trade events) self.subscribe_quotes(instrument_id, client_id=DATABENTO_CLIENT_ID) # Explicit MBP-1 schema self.subscribe_quotes( instrument_id=instrument_id, params={"schema": "mbp-1"}, client_id=DATABENTO_CLIENT_ID, ) # 1-second BBO snapshots (adapter emits QuoteTick only) self.subscribe_quotes( instrument_id=instrument_id, params={"schema": "bbo-1s"}, client_id=DATABENTO_CLIENT_ID, ) # Consolidated quotes across venues self.subscribe_quotes( instrument_id=instrument_id, params={"schema": "cbbo-1s"}, # or "cmbp-1" for consolidated MBP client_id=DATABENTO_CLIENT_ID, ) # Trade-sampled BBO (includes quotes and trades) self.subscribe_quotes( instrument_id=instrument_id, params={"schema": "tbbo"}, # Receives QuoteTick and TradeTick on the message bus client_id=DATABENTO_CLIENT_ID, ) ``` ### Trade subscriptions ```python # Trade ticks only self.subscribe_trades(instrument_id, client_id=DATABENTO_CLIENT_ID) # Trades from MBP-1 feed (only when trade events occur) self.subscribe_trades( instrument_id=instrument_id, params={"schema": "mbp-1"}, client_id=DATABENTO_CLIENT_ID, ) # Trade-sampled data (includes quotes at trade time) self.subscribe_trades( instrument_id=instrument_id, params={"schema": "tbbo"}, # Also provides quotes at trade events client_id=DATABENTO_CLIENT_ID, ) ``` ### Order book deltas subscriptions (MBO and L3) ```python # Subscribe to full order book updates (market by order) self.subscribe_book_deltas( instrument_id=instrument_id, book_type=BookType.L3_MBO, # Uses MBO schema client_id=DATABENTO_CLIENT_ID, ) # Deltas stream from the point of subscription with no initial book snapshot ``` ### Instrument status subscriptions ```python # Subscribe to venue trading-state updates self.subscribe_instrument_status( instrument_id=instrument_id, client_id=DATABENTO_CLIENT_ID, ) ``` ### Historical requests for depth and bars MBP-10 depth and OHLCV bars are available as historical requests. The bar aggregation in the `BarType` selects the OHLCV schema (`ohlcv-1s`, `ohlcv-1m`, `ohlcv-1h`, or `ohlcv-1d`), and depth requests use `mbp-10`: ```python import pandas as pd # Request 1-minute bars (uses the ohlcv-1m schema) self.request_bars( bar_type=BarType.from_str(f"{instrument_id}-1-MINUTE-LAST-EXTERNAL"), start=pd.Timestamp("2024-03-06", tz="UTC"), end=pd.Timestamp("2024-03-07", tz="UTC"), client_id=DATABENTO_CLIENT_ID, ) # Request top 10 levels of market depth (Databento serves depth=10 only) self.request_book_depth( instrument_id=instrument_id, depth=10, start=pd.Timestamp("2024-03-06T14:30", tz="UTC"), end=pd.Timestamp("2024-03-06T14:31", tz="UTC"), client_id=DATABENTO_CLIENT_ID, ) ``` ## Instrument IDs and symbology Databento market data includes an `instrument_id` field: a numeric ID assigned by the publisher in most cases, or synthesized by Databento when the publisher does not provide one. Databento only guarantees this ID is unique within a given day. This differs from the Nautilus `InstrumentId`, a string of symbol + venue separated by a period: `"{symbol}.{venue}"`. The decoder maps the Databento `raw_symbol` to the Nautilus `symbol`. Publisher IDs map to the default Nautilus venue through `publishers.json`. Subscription `InstrumentId` metadata can also seed the symbol-to-venue map before market data arrives. Dataset IDs follow Databento's [dataset naming conventions](https://databento.com/docs/api-reference-historical/basics/datasets), which are distinct from the venue code in a Nautilus `InstrumentId`. For historical requests and live subscriptions, the adapter sends the Nautilus symbol portion of each `InstrumentId` as the Databento symbol and infers `stype_in` from that string: - Symbols ending in `.FUT` or `.OPT` use Databento parent symbology, for example `ES.FUT.XCME`. - Three-part symbols whose last part is numeric use continuous symbology, for example `ES.c.0.GLBX`. - All-numeric symbols use Databento `instrument_id` symbology. - All other symbols use raw symbol symbology, for example `ESZ6.XCME` or `AAPL.EQUS`. All symbols in one request or subscription must use the same symbology type. Batch `AAPL.EQUS` with `MSFT.EQUS`, or `ES.FUT.XCME` with `NQ.FUT.XCME`, but do not mix raw and parent symbols in one Databento request. For CME Globex MDP 3.0 (`GLBX.MDP3`), publisher defaults map to the `GLBX` venue. When `use_exchange_as_venue=True`, definition messages can override `GLBX` with the instrument's exchange MIC: - `CBCM`: XCME-XCBT inter-exchange spread - `NYUM`: XNYM-DUMX inter-exchange spread - `XCBT`: Chicago Board of Trade (CBOT) - `XCEC`: Commodities Exchange Center (COMEX) - `XCME`: Chicago Mercantile Exchange (CME) - `XFXS`: CME FX Link spread - `XNYM`: New York Mercantile Exchange (NYMEX) :::info Other venue MICs are in the `venue` field of responses from the [metadata.list_publishers](https://databento.com/docs/api-reference-historical/metadata/metadata-list-publishers) endpoint. ::: ## Timestamps Databento data includes these timestamp fields: - `ts_event`: Matching-engine-received timestamp in nanoseconds since the UNIX epoch. - `ts_in_delta`: Matching-engine-sending timestamp in nanoseconds before `ts_recv`. - `ts_recv`: Capture-server-received timestamp in nanoseconds since the UNIX epoch. - `ts_out`: Databento sending timestamp (live only). Nautilus data requires at least two timestamps (per the `Data` contract): - `ts_event`: UNIX timestamp (nanoseconds) when the data event occurred. - `ts_init`: UNIX timestamp (nanoseconds) when the data instance was created. Quote and trade-like schemas map Databento `ts_recv` to Nautilus `ts_event` because it is more reliable and monotonically increases per Databento symbol. Bars use the DBN bar interval timestamp; `bars_timestamp_on_close` controls whether Nautilus bars use the interval open or close timestamp. `InstrumentStatus` uses the DBN record header `ts_event`. `DatabentoImbalance` and `DatabentoStatistics` preserve Databento timestamp fields because they are adapter-specific types. :::info See these Databento docs for details: - [Databento standards and conventions - timestamps](https://databento.com/docs/standards-and-conventions/common-fields-enums-types#timestamps) - [Databento timestamping guide](https://databento.com/docs/architecture/timestamping-guide) ::: ## Data types This section maps Databento schemas to Nautilus data types. :::info See Databento [schemas and data formats](https://databento.com/docs/schemas-and-data-formats). ::: ### Instrument definitions Databento uses a single schema for all instrument classes. The decoder maps each to the appropriate Nautilus `Instrument` type. | Databento instrument class | Code | Nautilus instrument type | | -------------------------- | ---- | ------------------------ | | Stock | `K` | `Equity` | | Future | `F` | `FuturesContract` | | Call | `C` | `OptionContract` | | Put | `P` | `OptionContract` | | Future spread | `S` | `FuturesSpread` | | Option spread | `T` | `OptionSpread` | | Mixed spread | `M` | `OptionSpread` | | FX spot | `X` | `CurrencyPair` | | Index | `I` | Not yet available | | Bond | `B` | Not yet available | ### Option expiration correction OPRA option definitions (dataset `OPRA.PILLAR`) carry the expiration with date-level precision: the time-of-day is zeroed to midnight UTC. An option expiring at 16:00 New York time therefore arrives stamped on the prior evening in New York, which makes the matching engine treat the contract as expired before its final trading session. The loader corrects such midnight-UTC OPRA expirations to 16:00 New York time by default, leaving every other dataset (and any expiration that already carries an intraday time, such as CME Globex) untouched. Override the default, or set per-underlying times, with `expiration_overrides`. It maps a dataset to a mapping of underlying symbol to time, where the reserved key `default` sets the dataset-wide time: ```python loader.load_instruments( filepath=path, use_exchange_as_venue=False, expiration_overrides={ "OPRA.PILLAR": {"default": "16:00", "SPX": "09:30"}, }, ) ``` Times use `HH:MM` or `HH:MM:SS` in the exchange-local timezone (New York for OPRA). Only a dataset with a built-in correction rule can be tuned, and `OPRA.PILLAR` is the only such dataset; an unknown or rule-less dataset such as `GLBX.MDP3` raises a `ValueError`. The correction keys on the option's underlying, so it cannot distinguish series that share an underlying but settle at different times (for example AM-settled SPX versus PM-settled SPXW); set the time that matches the contracts you are loading. ### Price precision Databento raw prices are fixed-point integers scaled by 1e-9. The adapter derives price precision from the instrument's tick size in the definition message. For live feeds, the feed handler maintains a per-instrument precision map populated from `InstrumentDefMsg` records as they arrive. Market data handlers resolve precision in this order: 1. Precision from an `InstrumentDefMsg` already seen for the Databento record `instrument_id`. 2. Subscription-supplied precision matched to the record `instrument_id` through a symbol mapping message. 3. Subscription-supplied precision matched directly on the Nautilus symbol. 4. The USD default precision of 2. Supply precision with the `price_precision` parameter on `subscribe_quotes()` or `subscribe_trades()`, or with `price_precisions` on the direct live client. No other subscription method reads the parameter. Steps 2 and 3 key the same override two ways: matching the record `instrument_id` after symbol mapping lets parent, continuous, and other non-raw symbology subscriptions apply the override before definition metadata arrives. **Instrument definitions must arrive before market data** for correct precision on instruments with non-standard tick sizes (e.g., treasury futures with fractional ticks like 1/256). Subscribe to instrument definitions (the Databento `definition` schema) before or alongside market data subscriptions. For historical requests and file-based loading, precision is resolved per record in this order: 1. An explicit `price_precision` argument on the call. 2. A per-symbol cache populated by loading definitions (`load_instruments` on the file loader, `get_range_instruments` on the historical client) or by an explicit `set_price_precision(symbol, precision)` call. Before each historical request, the data client seeds this cache when the request carries no explicit precision and the symbol has none cached: it fetches the instrument definition for the requested `instrument_id` first. When precision cannot be resolved, loading fails with an explicit error rather than silently defaulting to USD precision. :::tip Call `subscribe_instrument()` for each instrument at strategy start so definition messages populate the live precision map. The feed handler keeps a `price_precision` override per symbol for the whole dataset session, so passing it once on a quote or trade subscription also covers order book deltas for that symbol. `InstrumentStatus` carries no prices and needs no precision. ::: ### MBO (market by order) MBO is the highest granularity data from Databento, representing full order book depth. Some messages include trade data. The decoder produces an `OrderBookDelta` and optionally a `TradeTick`. The live client buffers MBO messages until a record carries the `F_LAST` flag closing the match event, then passes one `OrderBookDeltas` container to the handler. Records that decode to no delta (a fill attribution or a status action) can still carry `F_LAST`, so the client honors the raw flag independently of the decoded payload; otherwise a partial event would be stranded and merged into the next event. Snapshot records (`F_SNAPSHOT`) accumulate into the same buffer and flush with the first non-snapshot event boundary, so a snapshot reaches the handler as one `OrderBookDeltas` container rather than as individual deltas. When a subscription carries a replay `start` anchor, the client suppresses emission until an event timestamp passes that anchor, which keeps replayed history out of the live stream. ### MBP-1 (market by price, top-of-book) MBP-1 represents top-of-book quotes and trades. Some messages carry trade data. The decoder produces a `QuoteTick` and also a `TradeTick` when the message is a trade. ### TBBO and TCBBO (top-of-book with trades) TBBO and TCBBO provide both quote and trade data in each message. Both schemas emit a `TradeTick` per message plus a `QuoteTick`, more efficient than separate quote and trade subscriptions. The quote is skipped when either the bid or ask price is undefined. TCBBO provides consolidated data across venues. #### Trade ID derivation (CMBP-1 and TCBBO) The CMBP-1 and TCBBO schemas do not publish a native trade identifier. The decoder derives a deterministic `TradeId` by FNV-1a hashing the instrument ID, `ts_event`, `ts_recv`, price, size, and aggressor side of the trade. The same venue event yields the same trade ID across replays, so downstream dedup stays intact. Two logically distinct trades with identical fields collide; this matches the venue's inability to distinguish them. ### OHLCV (bar aggregates) Databento timestamps bar messages at the **open** of the interval. By default, the decoder normalizes bar `ts_event` to the bar **close**: the original `ts_event` plus the interval. `ts_init` uses the live receipt time, or the close time for historical and file-based loads when no explicit init timestamp is supplied. Set `bars_timestamp_on_close=False` to timestamp bar `ts_event` on the interval open. ### Imbalance and statistics The `imbalance` and `statistics` schemas have no built-in Nautilus equivalents. The adapter defines `DatabentoImbalance` and `DatabentoStatistics` in Rust, and Python bindings expose both types from `nautilus_trader.adapters.databento`. The live data client does not route these types through node subscriptions or requests. Reach them one of three ways: - `DatabentoDataLoader.load_imbalance` and `load_statistics` for DBN files. - `DatabentoHistoricalClient.get_range_imbalance` and `get_range_statistics` for historical ranges. - `DatabentoLiveClient.subscribe` with the `imbalance` or `statistics` schema for live streams. Request a bounded range of `statistics` for the `ES.FUT` parent symbol (all active E-mini S&P 500 futures). Both `get_range_*` methods are asynchronous. Use Databento's Historical [`metadata.get_cost`](https://databento.com/docs/api-reference-historical/metadata/metadata-get-cost) endpoint before real historical pulls: ```python import os from nautilus_trader.adapters.databento import DatabentoHistoricalClient from nautilus_trader.core import dt_to_unix_nanos from nautilus_trader.model import InstrumentId client = DatabentoHistoricalClient( key=os.environ["DATABENTO_API_KEY"], publishers_filepath="publishers.json", use_exchange_as_venue=False, ) statistics = await client.get_range_statistics( dataset="GLBX.MDP3", instrument_ids=[InstrumentId.from_str("ES.FUT.GLBX")], start=dt_to_unix_nanos("2024-03-06T00:00:00Z"), end=dt_to_unix_nanos("2024-03-07T00:00:00Z"), price_precision=2, ) ``` A fresh historical client holds no cached precision, so the request needs `price_precision` or a preceding `get_range_instruments` call for the same range; otherwise the first decoded record aborts the request. A parent symbol such as `ES.FUT` cannot be seeded with `set_price_precision`, because records resolve to the individual contract symbols behind the parent. ### Arrow encoding for imbalance and statistics Both types implement Arrow record batch encoding and decoding. The `nautilus_databento::arrow` module exposes it in Rust behind the `arrow` feature flag: ```rust use nautilus_databento::arrow::imbalance::{ decode_imbalance_batch, imbalance_to_arrow_record_batch, }; let batch = imbalance_to_arrow_record_batch(&imbalances)?; let metadata = batch.schema().metadata().clone(); let decoded = decode_imbalance_batch(&metadata, &batch)?; ``` The `statistics` module follows the same pattern with `decode_statistics_batch` and `statistics_to_arrow_record_batch`. Call `get_databento_arrow_schema_map(DatabentoImbalance)` from Python to inspect the Arrow field map for either type. :::info Neither type is registered with the `ParquetDataCatalog` custom data encoders, so `write_custom_data` and `query_custom_data` fail with an unregistered-type error, and neither type streams through `BacktestNode` or `BacktestEngine`. For research with imbalance or statistics data, load or request the records and process them directly. ::: ## Performance considerations Two options for backtesting with DBN data: - Store data as DBN (`.dbn.zst`) files and decode to Nautilus objects every run. - Convert DBN files to Nautilus objects once and write to the data catalog (Nautilus Parquet format). The DBN decoder is optimized Rust, but writing to the catalog once gives the best backtest performance. [DataFusion](https://arrow.apache.org/datafusion/) streams Nautilus Parquet data from disk at high throughput, at least an order of magnitude faster than decoding DBN per run. :::note Measured decode and client throughput for this adapter is recorded in [`crates/adapters/databento/benches/BENCHMARKS.md`](https://github.com/nautechsystems/nautilus_trader/blob/master/crates/adapters/databento/benches/BENCHMARKS.md), along with the command that reproduces it. Absolute numbers vary by machine, so only same-machine deltas are meaningful. ::: For live data, decoded delivery from the feed handler to Nautilus is intentionally unbounded. This prevents slow consumers from stalling the feed path; a process under memory pressure should fail rather than block live decoding. ## Loading DBN data `DatabentoDataLoader` decodes DBN files directly into Nautilus objects. It exposes a method for each supported output type, including `load_instruments`, `load_order_book_deltas`, `load_order_book_depth10`, `load_quotes`, `load_trades`, `load_bars`, `load_status`, `load_imbalance`, and `load_statistics`. Pass the publisher metadata file when it is not available beside the running executable: ```python from nautilus_trader.adapters.databento import DatabentoDataLoader from nautilus_trader.model import InstrumentId loader = DatabentoDataLoader(publishers_filepath="publishers.json") instruments = loader.load_instruments( filepath="equity-definitions.dbn.zst", use_exchange_as_venue=True, ) trades = loader.load_trades( filepath="aapl-trades.dbn.zst", instrument_id=InstrumentId.from_str("AAPL.XNAS"), ) ``` Write definition data before market data when writing to a `ParquetDataCatalog`, because the catalog needs the instrument before it can write records for that instrument: ```python from nautilus_trader.persistence import ParquetDataCatalog catalog = ParquetDataCatalog(base_path="catalog") catalog.write_instruments(instruments) catalog.write_trade_ticks(trades) ``` Use the schema-specific methods for files whose schema is not the default for that output type: - `load_bbo_quotes` for BBO interval quotes. - `load_cmbp_quotes` for CMBP-1 quotes. - `load_cbbo_quotes` for CBBO quotes. - `load_tbbo_trades` for TBBO trades. - `load_tcbbo_trades` for TCBBO trades. Call `schema_for_file` to read a file's schema from its DBN metadata header when picking the loader method. Optional `instrument_id` and `price_precision` arguments bypass symbology or precision lookup when those values are already known. The bar loader also accepts `timestamp_on_close`. ## Real-time client architecture The `DatabentoDataClient` wraps the other Databento adapter classes. It creates one live feed handler per dataset on the first subscription for that dataset, and every schema for that dataset shares the handler. The handler runs a single async task that races the next gateway record against the next engine command, so subscriptions added later reach the running session without a reconnect. A single `DatabentoHistoricalClient` serves every historical request from the data client, including the instrument definitions fetched to seed price precision. :::warning Databento drops a replay `start` anchor sent after a live session has started, so a subscription made mid-session streams from that point forward with no history. The feed handler logs an error when it sees a late `start`, and it strips `start` from stored subscriptions so a reconnect never replays history a second time. ::: ## Configuration Create `DatabentoDataClientConfig` from the adapter's public Python module. The API key and `publishers.json` path are required: ```python import os from pathlib import Path from nautilus_trader.adapters.databento import DatabentoDataClientConfig config = DatabentoDataClientConfig( api_key=os.environ["DATABENTO_API_KEY"], publishers_filepath=Path("publishers.json"), use_exchange_as_venue=False, ) ``` Download the canonical [`publishers.json`](https://github.com/nautechsystems/nautilus_trader/blob/master/crates/adapters/databento/publishers.json) and point `publishers_filepath` at the local copy. | Option | Default | Description | | ------------------------- | -------- | ------------------------------------------------------- | | `api_key` | Required | Databento API key. | | `publishers_filepath` | Required | Local path to Databento publisher metadata. | | `use_exchange_as_venue` | `False` | Use exchange MIC venues for GLBX instruments. | | `bars_timestamp_on_close` | `True` | Timestamp bars on close instead of the interval open. | | `venue_dataset_map` | `None` | Override venue-to-dataset mappings from publisher data. | Use `DatabentoDataClientConfig` with `DatabentoDataClientFactory`. The current [Python example](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/databento/data_tester.py) shows the complete `LiveNode.builder(...)` configuration. ### Connection stability The live client reconnects automatically on: - **Network interruptions**: Temporary connectivity issues. - **Gateway restarts**: Databento scheduled live gateway restarts. See the [maintenance schedule](https://databento.com/docs/api-reference-live/basics/maintenance-schedule). - **Market closures**: Sessions ending during off-hours. #### Reconnection strategy The factory-backed live client uses an internal 10-minute reconnection window with exponential backoff from 1 second, capped at 60 seconds. The Python `DatabentoDataClientConfig` constructor does not expose a reconnection timeout. Once the window elapses without a successful session, the client gives up and reports an error rather than retrying indefinitely. Stalled connections are detected by the upstream Databento client, which raises a heartbeat timeout when no data arrives within the heartbeat interval plus 5 seconds. The feed handler treats that as a connection error and enters the same backoff loop. All reconnections include: - **Jitter**: Random delay (up to 1 second) to prevent simultaneous reconnection storms. - **Automatic resubscription**: Restores all active subscriptions after reconnecting. - **Cycle reset**: Each successful session (>60s) resets the timeout clock and the backoff delay. - **Command buffering**: Commands received during backoff are applied to the next session. Individual unsubscribe requests log a warning and are ignored because Databento live sessions do not support granular unsubscribe. Stop the session to remove a subscription from the live gateway. #### Scheduled maintenance Databento restarts live gateways on this schedule (all clients disconnect): | Dataset | Restart time | | ------------------ | ----------------- | | CME Globex | Saturday 02:15 CT | | All ICE venues | Sunday 09:45 UTC | | All other datasets | Sunday 10:30 UTC | The internal 10-minute timeout covers typical restarts. See the [Databento Maintenance Schedule](https://databento.com/docs/api-reference-live/basics/maintenance-schedule) for details. ## Contributing :::info To contribute, see the [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Deribit Source: https://nautilustrader.io/docs/latest/integrations/deribit/ Founded in 2016, Deribit is a cryptocurrency derivatives exchange for options, futures, perpetuals, spot, and combo instruments. It is one of the largest crypto options exchanges by volume, and a leading platform for crypto derivatives trading. This integration supports live market data ingest and order execution with Deribit. ## Overview The Deribit adapter is implemented in Rust and exposed to Python through configurations, factories, clients, enums, and constants. Deribit uses JSON-RPC 2.0 over both HTTP and WebSocket transports. The adapter prefers WebSocket for subscriptions, real-time data, and order operations, and uses HTTP for instrument loading, historical requests, and reconciliation. Components: - `DeribitRawHttpClient`: Low-level HTTP client owning JSON-RPC framing, signing, rate limits, and retries. - `DeribitHttpClient`: Domain HTTP client parsing venue responses into Nautilus types; reach the raw client with `inner()`. - `DeribitWebSocketClient`: Low-level WebSocket connectivity for Rust callers. - `DeribitDataClient`: Market data feed manager. - `DeribitDataClientFactory`: Data client factory. - `DeribitExecutionClient`: Account management and trade execution gateway. - `DeribitExecutionClientFactory`: Execution client factory. Python surface available from `nautilus_trader.adapters.deribit`: - `DeribitDataClientConfig`, `DeribitExecutionClientConfig` - `DeribitDataClientFactory`, `DeribitExecutionClientFactory` - `DeribitHttpClient` - `DeribitCurrency`, `DeribitEnvironment`, `DeribitProductType` - `DeribitBookSummary`, `DeribitVolatilityIndex` - `get_deribit_http_base_url`, `get_deribit_ws_url` - `DERIBIT`, `DERIBIT_CLIENT_ID`, and `DERIBIT_VENUE` :::note Most users will define a configuration for a live trading node (as shown below), and won't need to work directly with these lower-level components. ::: ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/deribit/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/deribit/examples/) ## Deribit documentation - [API reference](https://docs.deribit.com/) - [Rate limits](https://docs.deribit.com/articles/rate-limits) - [Connection management](https://docs.deribit.com/articles/connection-management-best-practices) - [Market data collection](https://docs.deribit.com/articles/market-data-collection-best-practices) - [Order management](https://docs.deribit.com/articles/order-management-best-practices) - [Access scope](https://docs.deribit.com/articles/access-scope) It's recommended you also refer to the Deribit documentation in conjunction with this NautilusTrader integration guide. ## Product support Each client loads only the product families listed in its `product_types` configuration, once on connect. Neither client refreshes the instrument set afterwards, so restart the node to pick up newly listed instruments. On the data client, `auto_load_missing_instruments` instead fetches an uncached instrument on first subscribe; see [Lazy-load on subscribe](#lazy-load-on-subscribe). | Product type | Data feed | Trading | Notes | | ----------------- | --------- | ------- | ---------------------------------------------- | | Perpetual futures | ✓ | ✓ | Loaded with `DeribitProductType.FUTURE`. | | Dated futures | ✓ | ✓ | Loaded with `DeribitProductType.FUTURE`. | | Options | ✓ | ✓ | Loaded with `DeribitProductType.OPTION`. | | Spot | ✓ | ✓ | Loaded with `DeribitProductType.SPOT`. | | Future combos | ✓ | ✓ | Loaded with `DeribitProductType.FUTURE_COMBO`. | | Option combos | ✓ | ✓ | Loaded with `DeribitProductType.OPTION_COMBO`. | ## Symbology Deribit uses specific symbol conventions for different instrument types. All instrument IDs should include the `.DERIBIT` suffix when referencing them (e.g., `BTC-PERPETUAL.DERIBIT` for BTC perpetual). ### Quantity units Nautilus quantities map to Deribit's `amount` field, not the optional `contracts` field. Deribit reports perpetual and inverse futures amounts in USD units, and reports options and linear futures amounts in the underlying base currency. The Deribit `contract_size` field converts between `amount` and contract count; the adapter does not apply it again as the Nautilus multiplier. ### Perpetual futures Format: `{Currency}-PERPETUAL` Examples: - `BTC-PERPETUAL` - Bitcoin perpetual swap. - `ETH-PERPETUAL` - Ethereum perpetual swap. To subscribe to BTC perpetual in your strategy: ```python InstrumentId.from_str("BTC-PERPETUAL.DERIBIT") ``` ### Dated futures Format: `{Currency}-{DDMMMYY}` Examples: - `BTC-25DEC26` - Bitcoin future expiring December 25, 2026. - `ETH-26MAR27` - Ethereum future expiring March 26, 2027. ```python InstrumentId.from_str("BTC-25DEC26.DERIBIT") ``` ### Options Format: `{Currency}-{DDMMMYY}-{Strike}-{Type}` Examples: - `BTC-25DEC26-100000-C` - Bitcoin call option, $100,000 strike, expiring December 25, 2026. - `BTC-25DEC26-80000-P` - Bitcoin put option, $80,000 strike, expiring December 25, 2026. - `ETH-26MAR27-4000-C` - Ethereum call option, $4,000 strike, expiring March 26, 2027. Where: - `C` = Call option. - `P` = Put option. ```python InstrumentId.from_str("BTC-25DEC26-100000-C.DERIBIT") ``` ### Spot Format: `{BaseCurrency}_{QuoteCurrency}` Examples: - `BTC_USDC` - Bitcoin against USDC. - `ETH_USDC` - Ethereum against USDC. ```python InstrumentId.from_str("BTC_USDC.DERIBIT") ``` ### Future combos Format: `{Currency}-FS-{LegA}_{LegB}` Legs are dated futures or the perpetual (denoted `PERP` inside combo names, even though the standalone instrument is `BTC-PERPETUAL`). The combo expires with its earliest leg. Examples: - `BTC-FS-25DEC26_PERP` - calendar spread between the December 2026 future and the perpetual. - `BTC-FS-26MAR27_25DEC26` - inter-month spread between two dated futures. ```python InstrumentId.from_str("BTC-FS-25DEC26_PERP.DERIBIT") ``` The adapter models future combos as `CryptoFuturesSpread`, priced in USD as the spread between legs, with crypto settlement currency and `is_inverse` set per the upstream `instrument_type`. ### Option combos Format: `{Currency}-{Strategy}-{DDMMMYY}-{Strikes}` Strategy codes include CS (call spread), PS (put spread), STRG (strangle), STRD (straddle), BOX (box), and RR (risk reversal). The strikes segment separates multiple strikes with `_`. Examples: - `BTC-CS-25DEC26-70000_75000` - 70k / 75k call spread expiring December 25, 2026. - `BTC-STRG-26MAR27-72000_80000` - 72k / 80k strangle expiring March 26, 2027. - `BTC-STRD-26MAR27-77000` - 77k straddle expiring March 26, 2027. - `BTC-BOX-26MAR27-58000_60000` - 58k / 60k box expiring March 26, 2027. ```python InstrumentId.from_str("BTC-STRG-26MAR27-72000_80000.DERIBIT") ``` The adapter models option combos as `CryptoOptionSpread`, priced in the base currency under Deribit's inverse-option convention; fractional `size_increment` (e.g. `0.1`) is preserved end-to-end. ## Traded expirations Deribit exposes active traded expirations through the `public/get_expirations` HTTP endpoint. Option-chain loaders can use the high-level HTTP client to refresh active option series without scanning every instrument. ```rust tab="Rust" use nautilus_deribit::http::models::DeribitCurrency; let expirations = client .request_option_expirations(DeribitCurrency::BTC) .await?; ``` ```python tab="Python" from nautilus_trader.adapters.deribit import DeribitCurrency from nautilus_trader.adapters.deribit import DeribitHttpClient client = DeribitHttpClient() expirations = await client.request_option_expirations(DeribitCurrency.BTC) ``` The high-level method returns option expirations only. For lower-level Rust requests, call `client.inner().get_expirations(...)` with `GetExpirationsParams`. Deribit returns a currency-keyed result for concrete currencies such as `BTC`, and a direct kind-keyed result for `currency=any`; the adapter handles both shapes. ## Combo instruments Instrument loading includes combos when `product_types` contains the future-combo or option-combo variant. In Python, use `DeribitProductType.FUTURE_COMBO` or `DeribitProductType.OPTION_COMBO`. Deribit exposes the leg makeup of every active combo on `/public/get_combos`, and the combo's trading metadata (tick size, contract size, expiration, min trade amount) on the standard `/public/get_instruments?kind=option_combo|future_combo` response. The adapter merges both, attaching `deribit_combo_id`, `deribit_combo_state`, and `deribit_combo_legs` to the loaded instrument's `info` map so strategies can resolve legs without a second venue call. ### Trade publishing Deribit publishes each combo trade twice: - On the combo's trade channel (`trades.{combo_name}.{interval}`): the parent trade plus a `legs[]` array describing each leg fill. - On each leg's trade channel (`trades.{leg_instrument}.{interval}`): a standalone trade for the leg, tagged with `combo_id` and `combo_trade_id` pointing back to the parent. A subscriber to a plain option or future therefore sees combo-origin fills on its existing trade stream, and a subscriber to the combo itself sees the combo-level trade. The adapter does not fan out combo parent messages into extra leg ticks; it forwards the upstream parent and per-leg messages as separate `TradeTick`s against their respective `InstrumentId`s, so a subscriber to both the combo and an underlying leg sees one combo tick plus one leg tick for that combo trade, not duplicate ticks against the same instrument. To have the Deribit data client open the real leg trade channels alongside a combo trade subscription, pass `params={"subscribe_combo_legs": True}` to `subscribe_trades`. When unsubscribing that combo trade stream, Nautilus also closes the leg subscriptions opened by this opt-in. Deribit already publishes block trades and Block RFQs per leg, so the adapter forwards them through the standard 1:1 trade path. See [Trade ID provenance](#trade-id-provenance) for how block- and RFQ-origin trades are tagged on the resulting `TradeTick`. ### Historical combo trades The standard per-instrument trades endpoint accepts combo instrument names. To sweep all combos of a given product kind in one call, use `get_last_trades_by_currency` via `DeribitHttpClient::inner()`: ```rust use nautilus_deribit::http::{ models::{DeribitCurrency, DeribitProductType}, query::GetLastTradesByCurrencyParams, }; let params = GetLastTradesByCurrencyParams::builder() .currency(DeribitCurrency::BTC) .kind(DeribitProductType::FutureCombo) .count(50_u32) .include_old(true) .build()?; let resp = client.inner().get_last_trades_by_currency(params).await?; ``` Each returned `DeribitPublicTrade` carries `legs: Option>` plus the `combo_id` and `combo_trade_id` fields used to correlate per-leg trades. ## Trade ID provenance Public `TradeTick`s emitted by the adapter prefix the venue trade ID when the trade originated from a Block RFQ, a block trade, or a combo. Strategies that need to distinguish these from plain trades can pattern-match the prefix on `TradeTick.trade_id`. The raw Deribit `trade_id` is preserved after the prefix, so reconciliation against Deribit's own IDs is a prefix strip. | Prefix | Source field | Meaning | | ------------ | ---------------- | -------------------------------------------------------------- | | `RFQ-` | `block_rfq_id` | Trade originated from a Block RFQ. | | `BLK-` | `block_trade_id` | Trade is a non-RFQ block trade. | | `COMBO-` | `combo_id` | Per-leg trade whose parent originated from a combo instrument. | | *unprefixed* | (none of above) | Standard trade. | Precedence when multiple tags are present: `RFQ-` > `BLK-` > `COMBO-`. Block RFQs are themselves block trades on Deribit, so the RFQ tag wins; combos executed as block trades are tagged `BLK-` because the block flow is the more important reconciliation signal. This applies only to public trades (`TradeTick`). `FillReport.trade_id` is unchanged so reconciliation against `get_user_trades_*` keeps working. :::note This is a one-way convention introduced in NautilusTrader 1.228.0. Replay data captured before that release lacks prefixes. Strategies that store and compare `trade_id` strings across releases should strip the prefix on the new-data side, or filter by prefix only on data they know was captured after upgrading. ::: ## Market data Live subscriptions are served from WebSocket channels; historical requests go over HTTP. Every channel that takes an `{interval}` resolves it through the `interval` subscription parameter and the default selection rules described under [Order book subscriptions](#order-book-subscriptions). | Nautilus subscription | Deribit channel | Notes | | --------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | Instruments | `instrument.state.{kind}.{currency}` | `kind` and `currency` params both default to `any`. | | Instrument status | `instrument.state.{kind}.{currency}` | Channel derived from the instrument ID; every instrument on it is emitted. | | Book deltas | `book.{instrument}.raw` or `book.{instrument}.{group}.{depth}.{interval}` | `L2_MBP` only; see [Order book subscriptions](#order-book-subscriptions). | | Book depth10 | `book.{instrument}.{group}.10.{interval}` | Always the grouped channel, fixed at depth 10. | | Quotes | `quote.{instrument}` | Top of book; this channel takes no interval. | | Trades | `trades.{instrument}.{interval}` | See [Trade publishing](#trade-publishing) for combo behavior. | | Bars | `chart.trades.{instrument}.{resolution}` | See [Bars](#bars). | | Mark prices | `ticker.{instrument}.{interval}` | Parsed from the ticker payload. | | Index prices | `ticker.{instrument}.{interval}` | Parsed from the ticker payload. | | Option greeks | `ticker.{instrument}.{interval}` | Options only. | | Funding rates | `perpetual.{instrument}.{interval}` | Perpetuals only; other instruments are rejected. | | Volatility index | `deribit_volatility_index.{index_name}` | Custom data; see [Volatility index](#volatility-index). | | Nautilus request | Deribit endpoint | Notes | | ---------------------------- | ----------------------------------------------- | --------------------------------------------------- | | Instruments | `public/get_instruments` | Scoped to the configured `product_types`. | | Trades | `public/get_last_trades_by_instrument_and_time` | Accepts combo instrument names. | | Bars | `public/get_tradingview_chart_data` | See [Bars](#bars). | | Book snapshot | `public/get_order_book` | Requires the instrument in the client cache. | | Option-chain reference price | `public/ticker` | Used internally to bootstrap dynamic strike ranges. | | `DeribitBookSummary` | `public/get_book_summary_by_currency` | Custom data; see [Book summaries](#book-summaries). | ### Bars Deribit chart resolutions are `1`, `3`, `5`, `10`, `15`, `30`, `60`, `120`, `180`, `360`, and `720` minutes, plus `1D`. Both the live and historical paths require `EXTERNAL` aggregation. Historical requests are strict: a bar type that does not map onto one of those resolutions is rejected, as is any aggregation other than minute, hour, and day. Live subscriptions snap instead. The requested step is rounded **up** to the next supported resolution, so a 2-minute step opens Deribit's 3-minute channel and a 6-minute step opens the 10-minute channel; anything longer than 720 minutes becomes `1D`. Aggregations other than minute, hour, and day fall back to 1 minute with a warning. Each received candle is stamped with the `BarType` derived from the venue resolution, always with a `LAST` price type. Subscribe with a bar type naming a supported resolution and `LAST`; otherwise the bars publish under a different `BarType` than the one you subscribed with and never reach your handler. `Bar.volume` comes from the chart `cost` field (USD) for inverse perpetuals and inverse futures, and from the `volume` field (base currency) for everything else, including options and option spreads flagged inverse. This keeps `Bar.volume` and `TradeTick.size` on one unit per instrument. ## Order book subscriptions Deribit publishes L2 (market-by-price) book data only, so `subscribe_book_deltas` and `subscribe_book_depth10` reject any book type other than `BookType.L2_MBP`. Two feed families are available, each suited to different use cases. ### Raw feeds (tick-by-tick) Raw channels deliver every single update as an individual message. Subscribing to a raw order book gives you a notification for every order insertion, update, or deletion in the book. - Requires authenticated connection (safeguard against abuse). - Use when you need every price level change for HFT or market making. - Higher message volume. - Delivers one initial snapshot of the full book, then incremental deltas. ### Aggregated feeds (batched) Aggregated channels deliver updates in batches at a fixed interval (e.g., every 100ms). This groups multiple order book changes into single messages. - Available without authentication. - Recommended for most use cases. - Lower message volume, easier to process. - Default unauthenticated interval: 100ms. - Every message is a complete depth-limited snapshot, so each update replaces the book rather than patching it. ### Subscription parameters The Nautilus adapter supports both feed types via subscription parameters: | Parameter | Values | Notes | | ---------- | ---------------------- | ------------------------------------------------------------------------- | | `interval` | `raw`, `100ms`, `agg2` | `agg2` batches at about 1 second intervals. `raw` requires auth. | | `group` | `none`, price group | Default: `none`. Applies only to grouped non-raw book channels. | | `depth` | `1`, `10`, `20` | Default: `10`. Number of price levels per side for grouped book channels. | The data client chooses the order book interval as follows: 1. Uses `params["interval"]` when supplied. 2. Uses `raw` when the WebSocket connection is authenticated and no interval is supplied. 3. Uses Deribit's public `100ms` grouped feed when the connection is not authenticated. ```python from nautilus_trader.model import BookType from nautilus_trader.model import InstrumentId instrument_id = InstrumentId.from_str("BTC-PERPETUAL.DERIBIT") # Public 100ms aggregated feed when no API credentials are configured. strategy.subscribe_book_deltas(instrument_id, BookType.L2_MBP) # Raw feed. This is also the authenticated default when no interval is supplied. strategy.subscribe_book_deltas( instrument_id, BookType.L2_MBP, params={"interval": "raw"}, ) # Force an aggregated feed on an authenticated connection. strategy.subscribe_book_deltas( instrument_id, BookType.L2_MBP, params={"interval": "100ms", "depth": 10}, ) ``` :::note Raw order book feeds require an authenticated WebSocket connection. Ensure API credentials are configured before subscribing to raw feeds. ::: :::tip For most strategies, the 100ms aggregated feed provides sufficient granularity with lower message overhead. Set `params={"interval": "100ms"}` when you provide credentials but do not need raw tick-by-tick book updates. ::: ### Sequence gap recovery The adapter records the `change_id` of every book message and checks the `prev_change_id` of each incremental update against it. Only raw-channel updates carry `prev_change_id`; aggregated messages are self-contained snapshots and need no sequence check. When a gap is detected (a missed message), the adapter automatically: 1. Drops the delta that exposed the gap, and every further delta for the affected instrument. 2. Unsubscribes from that instrument's book channels. 3. Resubscribes once the unsubscribe is acknowledged, to obtain a fresh snapshot. 4. Resumes normal processing once the snapshot arrives, reseeding the sequence from it. During resync, the strategy will not receive stale or incomplete book updates. A user-initiated unsubscribe while a resync is pending cancels the resubscribe instead of reopening the channel. ## Funding rates Deribit exchanges funding continuously (every few seconds) rather than at fixed intervals like most other exchanges. The `interval` field on `FundingRateUpdate` is `None` for Deribit because this continuous model does not map to a discrete period. ## Deribit specific data ### Book summaries Request `DeribitBookSummary` custom data to fetch one bulk snapshot filtered by currency and product kind. Each response item includes the Nautilus instrument ID, implied volatility, open interest, prices, volume, and other fields returned by `public/get_book_summary_by_currency`. The actor or strategy receives the complete response through one `on_historical_data` callback. Each item is a `CustomData` wrapper containing a `DeribitBookSummary` in its `data` field: ```python from nautilus_trader.adapters.deribit import DERIBIT_CLIENT_ID from nautilus_trader.adapters.deribit import DeribitBookSummary from nautilus_trader.model import CustomData from nautilus_trader.model import DataType def on_start(self) -> None: self.request_data( DataType( DeribitBookSummary.__name__, metadata={"currency": "BTC", "kind": "option"}, ), DERIBIT_CLIENT_ID, ) def on_historical_data(self, data: list[CustomData]) -> None: for item in data: summary = item.data if isinstance(summary, DeribitBookSummary): self.log.info( f"{summary.instrument_id}: mark_iv={summary.mark_iv}, " f"open_interest={summary.open_interest}", ) ``` The `currency` metadata field is required. The optional `kind` field defaults to `option`. Decimal-backed venue fields, such as `mark_iv` and `open_interest`, are exposed to Python as strings or `None`. An empty response invokes `on_historical_data` once with an empty list. A failed venue request also invokes the callback with an empty list after logging an error. A request rejected before it reaches the venue, such as one missing `currency`, produces no callback. ### Volatility index The adapter emits `DeribitVolatilityIndex` custom data from Deribit's `deribit_volatility_index.{index_name}` WebSocket channel. Deribit provides volatility index streams such as `btc_usd` and `eth_usd`. | Field | Type | Description | | ------------ | ------- | -------------------------------------------------------- | | `index_name` | `str` | Deribit volatility index name, for example `btc_usd`. | | `volatility` | `float` | Current volatility index value. | | `ts_event` | `int` | UNIX timestamp in nanoseconds when the update occurred. | | `ts_init` | `int` | UNIX timestamp in nanoseconds when the object was built. | Subscribe from an actor or strategy with `DataType(DeribitVolatilityIndex.__name__)`. The `index_name` metadata key is required: ```python from nautilus_trader.adapters.deribit import DeribitVolatilityIndex from nautilus_trader.model import ClientId from nautilus_trader.model import DataType self.subscribe_data( data_type=DataType(DeribitVolatilityIndex.__name__, metadata={"index_name": "btc_usd"}), client_id=ClientId.from_str("DERIBIT"), ) ``` ## Orders capability Below are the order types, execution instructions, and time-in-force options supported on Deribit. ### Order types | Nautilus order type | Deribit order type | Supported | Notes | | ---------------------- | ------------------ | --------- | --------------------------------------- | | `MARKET` | `market` | ✓ | Immediate execution at market price. | | `LIMIT` | `limit` | ✓ | Execution at specified price or better. | | `STOP_MARKET` | `stop_market` | ✓ | Conditional market order on trigger. | | `STOP_LIMIT` | `stop_limit` | ✓ | Conditional limit order on trigger. | | `MARKET_IF_TOUCHED` | `take_market` | ✓ | Take-profit style market order. | | `LIMIT_IF_TOUCHED` | `take_limit` | ✓ | Take-profit style limit order. | | `TRAILING_STOP_MARKET` | `trailing_stop` | - | *Not currently implemented*. | | `TRAILING_STOP_LIMIT` | N/A | - | *Not supported by Deribit*. | | `MARKET_TO_LIMIT` | `market_limit` | - | *Not currently implemented*. | ### Execution instructions | Instruction | Supported | Notes | | ------------- | --------- | -------------------------------------------------------------------------------- | | `post_only` | ✓ | Order will be rejected if it would take liquidity. Uses `reject_post_only=true`. | | `reduce_only` | ✓ | Order can only reduce an existing position. | ### Time in force | Time in force | Supported | Notes | | ------------- | --------- | ---------------------------------------------------- | | `GTC` | ✓ | Good till canceled (`good_til_cancelled`). | | `GTD` | ✓ | Good till day. Expires at 8:00 UTC (`good_til_day`). | | `IOC` | ✓ | Immediate or cancel (`immediate_or_cancel`). | | `FOK` | ✓ | Fill or kill (`fill_or_kill`). | Deribit applies time in force to limit-style orders. The adapter omits `time_in_force` for `MARKET`, `STOP_MARKET`, and `MARKET_IF_TOUCHED` orders because Deribit rejects that parameter on market-style order types. :::note **GTD on Deribit**: Unlike other exchanges where GTD accepts an arbitrary expiry time, Deribit's `good_til_day` always expires at 8:00 UTC the same or next day. Custom expiry times will be logged as warnings and the order will use the exchange's fixed expiry behavior. ::: ### Trigger types Conditional orders (stop orders) support different trigger price sources: | Trigger type | Supported | Notes | | ------------- | --------- | ---------------------------------------------------- | | `last_price` | ✓ | Uses the last traded price. Also used for `DEFAULT`. | | `mark_price` | ✓ | Uses the mark price. | | `index_price` | ✓ | Uses the underlying index price. | Any other Nautilus trigger type is dropped from the request rather than mapped. Deribit requires `trigger` on stop-loss and take-profit orders, so use one of the three above for conditional orders. ```python # Example: Stop loss using mark price trigger stop_order = order_factory.stop_market( instrument_id=instrument_id, order_side=OrderSide.SELL, quantity=Quantity.from_str("0.1"), trigger_price=Price.from_str("45000.0"), trigger_type=TriggerType.MARK_PRICE, # Use mark price for trigger ) strategy.submit_order(stop_order) ``` ### Batch operations | Operation | Supported | Notes | | ------------------------ | --------- | ------------------------------------------------------------------------ | | Submit order list | ✓ | Sends each order as an individual Deribit order. No atomic venue batch. | | Batch cancel by order ID | ✓ | Sends individual `private/cancel` requests for each venue order ID. | | Cancel all by instrument | ✓ | Uses `private/cancel_all_by_instrument` when no side filter is supplied. | | Side-filtered cancel all | ✓ | Filters cached open orders locally, then cancels each matching order. | | Batch modify | - | *Not currently implemented*: single order modify is supported. | ### Post-only behavior Deribit offers two post-only modes: 1. **Price adjustment (Deribit default)**: If a post-only order would cross the spread and execute, Deribit automatically adjusts the price to one tick inside the spread. 2. **Reject mode**: Order is immediately rejected if it would cross the spread. The Nautilus adapter uses **reject mode** (`reject_post_only=true`) for deterministic behavior. If a post-only order would take liquidity, it is rejected with error code `11054`, and an `OrderRejected` event is emitted with the `due_post_only` flag set to `true`. This allows strategies to differentiate between: - Orders rejected due to post-only violation (attempted to take liquidity). - Orders rejected for other reasons (insufficient margin, invalid price, etc.). ### Order modification The adapter uses Deribit's native `private/edit` endpoint rather than cancel-and-replace. This provides several advantages: | Benefit | Description | | --------------------------- | ----------------------------------------------------------------- | | Single request | Faster execution, lower latency than cancel + new order. | | Queue priority preservation | Keeps position when only reducing quantity or keeping same price. | | Fill history maintained | Partial fills remain linked to the same order ID. | **Queue priority rules:** - **Decreasing quantity only**: Keeps queue position. - **Same price**: Keeps queue position. - **Increasing quantity or changing price**: Loses queue position (treated as new order). A modify command must carry the venue order ID and a price; either one missing rejects the command locally without reaching Deribit. Quantity is optional and falls back to the cached order's current quantity. ### Position management | Feature | Supported | Notes | | ---------------- | --------- | ------------------------------------------------------------------ | | Query positions | ✓ | Fetched from `private/get_positions` for reconciliation. | | Position mode | - | *Not supported by Deribit*: net position mode only. | | Leverage control | - | *Not supported by Deribit*: no direct leverage setting. | | Margin mode | - | *Not currently implemented*: Deribit exposes account margin modes. | The execution client subscribes to `user.orders`, `user.trades`, and `user.portfolio` (balance and margin), not to Deribit's combined `user.changes` channel, so no position push reaches the adapter. Live position state is maintained by Nautilus from the fills on `user.trades`, and `private/get_positions` supplies the venue snapshot that reconciliation compares against. ### Order querying | Feature | Supported | Notes | | -------------------- | --------- | ------------------------------ | | Query open orders | ✓ | List all active orders. | | Query order history | ✓ | Historical order data. | | Order status updates | ✓ | Real-time order state changes. | | Trade history | ✓ | Execution and fill reports. | ### Contingent orders | Feature | Supported | Notes | | ------------------------------ | --------- | ------------------------------------------------------------------------------ | | Order lists | ✓ | Submitted sequentially as independent orders; there is no atomic venue list. | | Native linked orders | - | *Not currently implemented*: Deribit exposes `linked_order_type`. | | OCO orders | - | *Not currently implemented*: Deribit exposes `one_cancels_other`. | | Bracket orders | - | *Not currently implemented*: Deribit exposes `one_triggers_one_cancels_other`. | | Conditional stop orders | ✓ | Stop market and stop limit orders. | | Conditional take-profit orders | ✓ | Market-if-touched and limit-if-touched orders. | ### Liquidation handling Deribit tags any trade that was triggered by a liquidation. On the `user.trades` stream and `private/get_user_trades_*` endpoints, the optional `liquidation` field indicates which side was being liquidated: | Value | Meaning | | ------ | ----------------------------- | | `"M"` | Maker side was liquidated. | | `"T"` | Taker side was liquidated. | | `"MT"` | Both sides were liquidated. | | absent | Normal non-liquidation trade. | The adapter logs a warning for each liquidation-tagged fill with the instrument, trade ID, order ID, and liquidation side, and then emits the `FillReport` through the normal pipeline. Deribit does not operate an ADL mechanism distinct from the liquidation + insurance-fund / portfolio margin process, so there is no separate ADL signal to surface. Upstream references: - [`user.trades.{instrument_name}.{interval}` channel](https://docs.deribit.com/subscriptions/user/usertradesinstrument_nameinterval) - [Liquidation documentation](https://support.deribit.com/hc/en-us/articles/25944769313309-Liquidations) ## Rate limiting Deribit uses credit-based and endpoint-specific rate limits. The official Deribit limits are authoritative, and they can vary by endpoint, account tier, and current venue policy. The adapter adds local token buckets to reduce avoidable throttling, but it does not replace Deribit's own server-side checks. ### HTTP limits Every HTTP request consumes the global bucket, plus the order or account bucket when the JSON-RPC method falls into that category, plus a per-method bucket keyed `deribit:{method}` that uses the global quota. | Bucket / key | Adapter bucket | Notes | | ------------------ | --------------------- | -------------------------------------------------------------- | | `deribit:global` | 20 req/sec, 100 burst | Applied to every HTTP request. | | `deribit:orders` | 5 req/sec, 20 burst | Matching-engine methods when driving the HTTP client directly. | | `deribit:account` | 5 req/sec, 5 burst | Account, position, order state, and user trade endpoints. | | `deribit:{method}` | 20 req/sec, 100 burst | Per-method bucket; falls back to the global quota. | ### WebSocket limits | Operation | Adapter bucket | Notes | | --------------------- | ------------------- | -------------------------------------------------------------- | | Subscribe/unsubscribe | 3 req/sec, 10 burst | Also the default bucket for every non-order WebSocket request. | | Order operations | 5 req/sec, 20 burst | Buy, sell, edit, and cancel via WebSocket. | :::note The Nautilus adapter uses WebSocket for order submission (not HTTP) for lower latency. Order operations are rate-limited by `DERIBIT_WS_ORDER_QUOTA` (5 req/sec, 20 burst). ::: ### Credit-based system details Deribit replenishes non-matching-engine credits continuously. Current public documentation lists the default non-matching-engine pool as follows: **Non-matching engine requests:** | Parameter | Value | Notes | | ---------------- | ------------------ | ------------------------------- | | Cost per request | 500 credits | Each API call consumes credits. | | Maximum pool | 50,000 credits | Allows 100 request burst. | | Refill rate | 10,000 credits/sec | ~20 sustained requests/second. | **Matching engine requests (default tier):** | Parameter | Value | Notes | | -------------- | -------------- | -------------------------------- | | Sustained rate | 5 requests/sec | Continuous rate limit. | | Burst capacity | 20 requests | Maximum burst before throttling. | Higher matching-engine limits are available for market makers and high-volume traders based on 7-day trading volume tiers. Some Deribit endpoints have stricter method-specific limits. For example, current venue docs list `public/get_instruments` at 1 request per second with a 50 request burst, and subscription methods at about 3.3 requests per second with a 10 request burst. Keep `product_types` scoped to the families you need and avoid repeated full instrument reloads in live systems. The Nautilus adapter implements broad token bucket rate limiters configured as: - `DERIBIT_HTTP_REST_QUOTA`: 20 req/sec with 100 burst (global and per-method HTTP) - `DERIBIT_HTTP_ORDER_QUOTA`: 5 req/sec with 20 burst (matching-engine HTTP) - `DERIBIT_HTTP_ACCOUNT_QUOTA`: 5 req/sec with 5 burst (account HTTP) - `DERIBIT_WS_ORDER_QUOTA`: 5 req/sec with 20 burst (matching-engine WebSocket) - `DERIBIT_WS_SUBSCRIPTION_QUOTA`: 3 req/sec with 10 burst (subscribe, unsubscribe, and other non-order requests) For more details, see the [Rate limits article](https://docs.deribit.com/articles/rate-limits). :::warning Deribit returns error code `10028` (too_many_requests) when you exceed the allowed quota. Repeated violations may result in temporary throttling. ::: ## Connection management ### Platform limits | Limit | Current Deribit guidance | | ------------------------------------------------ | ------------------------ | | Active sessions per API key or login | 16 | | Session and connection-scoped connections per IP | 32 | | Web app connections per browser session | 2 | ### Session-based authentication The adapter uses **separate WebSocket sessions** for data and execution clients, each with its own authentication scope: | Client | Session Name | Purpose | | ---------------- | -------------------- | --------------------------------------------------- | | Data client | `nautilus-data` | Market data subscriptions (raw feeds require auth). | | Execution client | `nautilus-execution` | Order operations (buy, sell, edit, cancel). | **Authentication flow:** 1. WebSocket connects to Deribit. 2. Client authenticates using `client_signature` grant type with session scope. 3. Tokens are refreshed before expiry. 4. On reconnection, re-authentication is retried with exponential backoff (up to 3 attempts). If all attempts fail, only public channel subscriptions are restored. This session-based approach allows: - Independent token management per client type. - Isolated failure domains (data auth failure does not affect execution). - Clear audit trail in Deribit's session logs. ### Best practices The adapter follows Deribit's [recommended connection practices](https://docs.deribit.com/articles/connection-management-best-practices): 1. **Uses WebSocket subscriptions** for real-time data instead of REST polling, resulting in fewer requests, lower latency, and reduced rate limit consumption. 2. **Authenticates all connections** when credentials are provided. Authenticated users benefit from higher rate limits and are less likely to be IP rate-limited. 3. **Implements heartbeats** (30 second interval by default) to maintain connection health and detect disconnections early. 4. **Handles reconnection** automatically with re-authentication and subscription recovery. :::tip Always provide API credentials even for public data access. Authenticated connections have higher rate limits, and Deribit contacts authenticated clients before applying restrictions during high-load periods. ::: :::note The adapter uses a 30 second heartbeat interval by default. Deribit requires WebSocket heartbeat intervals to be at least 10 seconds. ::: ## Authentication Deribit uses API key authentication with HMAC-SHA256 signatures for private endpoints. To create API credentials: 1. Log into your Deribit account at [deribit.com](https://www.deribit.com) (or [test.deribit.com](https://test.deribit.com) for testnet). 2. Navigate to **Account** -> **API**. 3. Click **Add new key** and configure permissions: - Enable **read** for market data access - Enable **trade** for order execution - Enable **wallet** if you need account balance access 4. Note down your **Client ID** (API key) and **Client Secret** (API secret). :::warning Keep your API secret secure. Never share it or commit it to version control. ::: ### API key scopes Each API key on Deribit is assigned a default access scope, which defines the maximum permissions. Configure appropriate permissions when [creating your API key](https://docs.deribit.com/articles/creating-api-key): | Scope | Required For | | ------------------ | -------------------------------------- | | `account:read` | Account information, portfolio data. | | `trade:read` | View orders and positions. | | `trade:read_write` | Place, modify, and cancel orders. | | `wallet:read` | View balances and transaction history. | **Recommended minimum for trading:** `account:read`, `trade:read_write`, `wallet:read` :::tip Follow the principle of least privilege. For data-only access (market data, no trading), create a read-only key without `trade:read_write`. ::: ## Testnet Deribit provides a testnet environment for testing strategies without real funds. To use the testnet, set `environment=DeribitEnvironment.TESTNET` in your client configuration: ```python from nautilus_trader.adapters.deribit import DeribitDataClientConfig from nautilus_trader.adapters.deribit import DeribitEnvironment from nautilus_trader.adapters.deribit import DeribitExecutionClientConfig from nautilus_trader.adapters.deribit import DeribitProductType from nautilus_trader.model import AccountId product_types = [DeribitProductType.FUTURE] account_id = AccountId.from_str("DERIBIT-001") data_config = DeribitDataClientConfig( product_types=product_types, environment=DeribitEnvironment.TESTNET, ) exec_config = DeribitExecutionClientConfig( account_id=account_id, product_types=product_types, environment=DeribitEnvironment.TESTNET, ) ``` When testnet mode is enabled: - HTTP requests use `https://test.deribit.com`. - WebSocket connections use `wss://test.deribit.com/ws/api/v2`. - Loads credentials from `DERIBIT_TESTNET_API_KEY` and `DERIBIT_TESTNET_API_SECRET` environment variables. :::note Testnet API keys are separate from production keys. Create API keys specifically for the testnet through the testnet interface at [test.deribit.com](https://test.deribit.com). ::: ## Configuration ### Data client configuration options | Option | Default | Description | | ---------------------------------- | ---------- | ------------------------------------------------------------------ | | `api_key` | `None` | Deribit API key. Loads from environment variables when omitted. | | `api_secret` | `None` | Deribit API secret. Loads from environment variables when omitted. | | `product_types` | `[FUTURE]` | Product types to load. | | `environment` | `MAINNET` | Environment enum (`MAINNET` or `TESTNET`). | | `base_url_http` | `None` | Override for the HTTP JSON-RPC base URL. | | `base_url_ws` | `None` | Override for the WebSocket base URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `http_timeout_secs` | `60` | Request timeout in seconds for HTTP calls. | | `max_retries` | `3` | Maximum retry attempts for recoverable errors. | | `retry_delay_initial_ms` | `1,000` | Initial delay in milliseconds before retrying. | | `retry_delay_max_ms` | `10,000` | Maximum delay in milliseconds between retries. | | `heartbeat_interval_secs` | `30` | WebSocket heartbeat interval. | | `auth_timeout_secs` | `None` | Seconds to await the WebSocket auth result; unset means 30. | | `update_instruments_interval_mins` | `60` | Accepted but not yet acted on; instruments load once on connect. | | `auto_load_missing_instruments` | `False` | Lazy-load uncached instruments on subscribe. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | #### Lazy-load on subscribe `subscribe_*` commands look up the instrument in the local cache before sending the WebSocket subscribe so the handler can parse the inbound frames. With `auto_load_missing_instruments = False` (the default), a subscribe for an instrument that was not preloaded (because of the configured `product_types`) returns an error up front rather than silently succeeding and dropping subsequent frames at the handler. Set `auto_load_missing_instruments = True` to instead fetch the instrument over HTTP on the first subscribe, seed the WebSocket handler cache, and then forward the subscribe. HTTP failures are logged and the WebSocket subscribe is skipped. ### Execution client configuration options | Option | Default | Description | | ------------------------ | ---------- | ------------------------------------------------------------------ | | `account_id` | Required | Nautilus account ID for generated reports and events. | | `api_key` | `None` | Deribit API key. Loads from environment variables when omitted. | | `api_secret` | `None` | Deribit API secret. Loads from environment variables when omitted. | | `product_types` | `[FUTURE]` | Product types to load. | | `environment` | `MAINNET` | Environment enum (`MAINNET` or `TESTNET`). | | `base_url_http` | `None` | Override for the HTTP JSON-RPC base URL. | | `base_url_ws` | `None` | Override for the WebSocket base URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `http_timeout_secs` | `60` | Request timeout in seconds for HTTP calls. | | `max_retries` | `3` | Maximum retry attempts for recoverable errors. | | `retry_delay_initial_ms` | `1,000` | Initial delay in milliseconds before retrying. | | `retry_delay_max_ms` | `10,000` | Maximum delay in milliseconds between retries. | | `auth_timeout_secs` | `None` | Seconds to await the WebSocket auth result; unset means 30. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | `transport_backend` selects between the `Sockudo` and `Tungstenite` WebSocket transports. The default is `Sockudo` when the `transport-sockudo` Cargo feature is enabled, which the published Python wheels do; a Rust build without that feature defaults to `Tungstenite`. ### Production configuration Below is an example live node using Deribit data and execution clients: ```python from nautilus_trader.adapters.deribit import DeribitDataClientConfig from nautilus_trader.adapters.deribit import DeribitDataClientFactory from nautilus_trader.adapters.deribit import DeribitEnvironment from nautilus_trader.adapters.deribit import DeribitExecutionClientConfig from nautilus_trader.adapters.deribit import DeribitExecutionClientFactory from nautilus_trader.adapters.deribit import DeribitProductType from nautilus_trader.common import Environment from nautilus_trader.live import LiveNode from nautilus_trader.model import AccountId from nautilus_trader.model import TraderId product_types = [DeribitProductType.FUTURE] trader_id = TraderId.from_str("TRADER-001") account_id = AccountId.from_str("DERIBIT-001") node = ( LiveNode.builder("DERIBIT-NODE-001", trader_id, Environment.LIVE) .add_data_client( None, DeribitDataClientFactory(), DeribitDataClientConfig( product_types=product_types, environment=DeribitEnvironment.MAINNET, api_key=None, api_secret=None, ), ) .add_exec_client( None, DeribitExecutionClientFactory(), DeribitExecutionClientConfig( account_id=account_id, product_types=product_types, environment=DeribitEnvironment.MAINNET, api_key=None, api_secret=None, ), ) .build() ) ``` ### API credentials There are multiple options for supplying your credentials to the Deribit clients. Either pass the corresponding values to the configuration objects, or set the following environment variables: For Deribit live (production) clients: - `DERIBIT_API_KEY` - `DERIBIT_API_SECRET` For Deribit testnet clients: - `DERIBIT_TESTNET_API_KEY` - `DERIBIT_TESTNET_API_SECRET` :::tip We recommend using environment variables to manage your credentials. ::: ### Product types The `product_types` configuration option controls which Deribit product families are loaded. Available options via the `DeribitProductType` enum: - `DeribitProductType.FUTURE` - Perpetual and dated futures. - `DeribitProductType.OPTION` - Call and put options. - `DeribitProductType.SPOT` - Spot trading pairs. - `DeribitProductType.FUTURE_COMBO` - Future spread instruments. - `DeribitProductType.OPTION_COMBO` - Option spread instruments. Example loading multiple product types: ```python from nautilus_trader.adapters.deribit import DeribitDataClientConfig from nautilus_trader.adapters.deribit import DeribitProductType config = DeribitDataClientConfig( product_types=[ DeribitProductType.FUTURE, DeribitProductType.OPTION, ], # ... other config ) ``` ### Base URLs The `environment` option selects these defaults. Set `base_url_http` or `base_url_ws` to point either transport somewhere else, such as a local proxy or a capture harness. | Environment | HTTP URL | WebSocket URL | | ----------- | -------------------------- | ---------------------------------- | | Production | `https://www.deribit.com` | `wss://www.deribit.com/ws/api/v2` | | Testnet | `https://test.deribit.com` | `wss://test.deribit.com/ws/api/v2` | ## Server infrastructure Deribit's matching engine is located in **Equinix LD4, Slough, UK**. For latency-sensitive strategies, consider hosting in or near London. Colocation and cross-connect options are available directly from Deribit for institutional clients. For most users connecting via internet, the adapter's built-in retry logic, heartbeat monitoring, and automatic reconnection handling provide reliable connectivity. For more details, see the [Server Infrastructure article](https://support.deribit.com/hc/en-us/articles/25944617582877-Server-Infrastructure). ## Contributing :::info For additional features or to contribute to the Deribit adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Derive Source: https://nautilustrader.io/docs/latest/integrations/derive/ Derive (formerly Lyra) is a decentralized derivatives venue offering European-style options and cash-settled perpetual swaps, and one of the largest on-chain options markets. Trading runs against a per-user smart-contract wallet on the Derive Chain, so collateral stays in the user's custody while orders match through the venue's orderbook. The Derive Chain is an optimistic rollup that settles to Ethereum. Orders match off chain and settle on chain, pairing orderbook execution with self-custody. Orders are authorized with EIP-712 typed-data signatures from a session key scoped to a subaccount, which keeps the signing key separate from the wallet owner and lets users rotate or revoke access without moving funds. ## Overview The Derive adapter is implemented in Rust under `crates/adapters/derive`. It exposes: - `DeriveHttpClient`: Low-level REST connectivity to `api.lyra.finance` (mainnet) or `api-demo.lyra.finance` (testnet). - `DeriveWebSocketClient`: JSON-RPC WebSocket transport with subscription tracking, reconnect, and signed order entry. - `DeriveInstrumentProvider`: Per-currency instrument fetch and caching. - `DeriveDataClient`: Live market data client. - `DeriveDataClientFactory`: Data client factory for the live node builder. - `DeriveExecutionClient`: Live execution client for signed order, cancel, query, and report flows. - `DeriveExecutionClientFactory`: Execution client factory for the live node builder. Execution flows use EIP-712 typed-data signing against the Derive Chain per-action module contracts. Python surface available from `nautilus_trader.adapters.derive`: - `DeriveDataClientConfig`, `DeriveExecutionClientConfig` - `DeriveDataClientFactory`, `DeriveExecutionClientFactory` - `DeriveEnvironment` - `DERIVE`, `DERIVE_CLIENT_ID`, and `DERIVE_VENUE` ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/derive/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/derive/examples/) ## Derive documentation Derive publishes API documentation at [docs.derive.xyz](https://docs.derive.xyz). Refer to it alongside this guide for additional details. ## Products | Product type | Supported | Notes | | ---------------------- | --------- | -------------------------------------------------------------------- | | ERC-20 spot | ✓ | USDC-quoted pairs such as `ETH-USDC`; parsed as `CurrencyPair`. | | Perpetual swaps | ✓ | Cash-settled in USDC, with per-currency listings such as `ETH-PERP`. | | Options (calls / puts) | ✓ | European-style options using `{CURRENCY}-{EXPIRY}-{STRIKE}-{C\|P}`. | ## Symbology Derive instruments use the native venue symbol with the venue suffix `.DERIVE`: - Spot: `ETH-USDC.DERIVE` (base currency, quote currency). - Perpetual: `ETH-PERP.DERIVE`, `BTC-PERP.DERIVE`. - Option: `ETH-20260626-3000-C.DERIVE` (currency, expiry, strike, kind). The first hyphen-separated segment of the symbol is the underlying currency. The provider fetches `public/get_instruments` once per currency, so subscribing to a new currency triggers a lazy REST fetch when `auto_load_missing_instruments` is enabled (the default). The adapter routes on the venue `instrument_type` (`perp`, `option`, `erc20`), not on the symbol suffix, so spot pairs need no special symbology parsing. Spot reuses the same Trade-module signing path as perps and options; the in-repo fixtures under `crates/adapters/derive/test_data/spot/` capture the spot instrument, order book, ticker, and trade field shapes the parser and execution paths are pinned to. :::warning Spot trading has had less live exercise than perpetuals and options. Testnet accepts and cancels a passive `ETH-USDC` limit order at the `0.1 ETH` minimum amount, and mainnet place/cancel has been exercised manually. Public spot trade channels (`trades.erc20.ETH`, `trades.ETH-USDC`) subscribe successfully but can be low-volume, so expect sparse trade frames. When the spot book is empty the venue still broadcasts ticker and order book frames with a zeroed top of book; the adapter drops those partial quotes (logged at DEBUG), so the quote feed stays silent until a book forms. Trade frames are match-driven and independent of book state, so a trade that empties the book still emits an event. Spot orders under Standard Margin are margined by initial margin fraction, not full notional. ::: ## Environments Configure the environment with the `DeriveEnvironment` enum on either client config. | Environment | Config | REST | WebSocket | | ----------- | ---------------------------- | ------------------------------- | -------------------------------- | | Mainnet | `DeriveEnvironment::Mainnet` | `https://api.lyra.finance` | `wss://api.lyra.finance/ws` | | Testnet | `DeriveEnvironment::Testnet` | `https://api-demo.lyra.finance` | `wss://api-demo.lyra.finance/ws` | Testnet is a separate chain with its own session keys and balances; mainnet and testnet API keys are not interchangeable. Public market data (book, ticker, trades) does not require credentials. The EIP-712 protocol constants (`DOMAIN_SEPARATOR`, `ACTION_TYPEHASH`, per-action module addresses) for both networks are shipped in `crates/adapters/derive/src/common/consts.rs` and tracked against Derive's [Protocol Constants reference](https://docs.derive.xyz/reference/protocol-constants). `DeriveExecutionClientConfig::domain_separator`, `action_typehash`, and `trade_module_address` accept per-instance overrides that take precedence over the shipped values. ## Testnet onboarding Derive labels the demo environment "testnet" in the web app and "demo" in the API hostname. This guide uses "testnet" to match the dashboard and our `DeriveEnvironment::Testnet` enum. Steps to reach a position where the execution client can submit a signed order: 1. **Sign in to the testnet dashboard.** Open [testnet.derive.xyz](https://testnet.derive.xyz) and connect an EVM wallet (MetaMask, WalletConnect, social login, etc.). This is the owner EOA that authorizes the smart-contract wallet below. 2. **Register the Derive Chain smart-contract wallet.** First sign-in deploys a per-user smart-contract wallet on the Derive testnet chain. The address shown under "Developers" -> "Derive Wallet" is the `wallet_address` (and the `X-LYRAWALLET` header) the client uses. It is distinct from the EOA you just connected. 3. **Create a subaccount.** Open a subaccount under the wallet (Standard Margin is the simplest mode for test trading). The integer id is the `subaccount_id` the client signs each `private/order` request against. 4. **Generate a session key.** Under "Developers" -> "Session Keys", create a session key scoped to the subaccount and copy the raw secp256k1 private key. This is the `session_key` value; it never leaves the client and is redacted from `Debug` output. Session keys can be rotated or revoked from the same panel. 5. **Fund the subaccount via the faucet.** The testnet dashboard exposes a USDC faucet that drips test collateral. Deposit into the subaccount so the on-chain balance shows non-zero collateral; the API will reject orders until the subaccount has enough margin for the requested size. 6. **Set the environment variables.** Export the three values the client reads in testnet mode (or pass them on `DeriveExecutionClientConfig`, where the config field wins): ```bash export DERIVE_TESTNET_WALLET_ADDRESS="0x..." # Derive Chain smart-contract wallet export DERIVE_TESTNET_SESSION_PRIVATE_KEY="0x..." # secp256k1 session-key private key export DERIVE_TESTNET_SUBACCOUNT_ID="12345" # integer subaccount id ``` ### Minimum funding There is no fixed venue minimum. The matching engine accepts any order that satisfies the subaccount's initial-margin requirement for the resulting position. Treat these as practical floors for the smallest viable test: - **Smoke test (submit and cancel, no fills):** any positive USDC balance covers the signed-order plumbing. - **Round-trip an `ETH-PERP` fill:** budget for the worst-case slippage-adjusted notional plus the initial-margin cushion. For one contract at $3500 and the venue's ~10% IM, that is roughly $350 collateral plus $400 cushion. Around $1000 USDC is a comfortable working balance for a first-fill test. - **Options:** options carry higher IM than perps. Pull `public/get_instrument` for the option, multiply the contract size by mark price, then add the option-specific IM (visible on the instrument response) before sizing the deposit. Use the `private/get_subaccount` endpoint after funding to confirm `initial_margin` stays positive once the intended order's initial margin is applied: `initial_margin` and `maintenance_margin` are the subaccount's signed net health (collateral credit minus the corresponding requirement), the venue rejects risk-increasing orders that would drive `initial_margin` negative, and a negative `maintenance_margin` exposes the subaccount to liquidation. The adapter's `query_account` command emits this snapshot as an `AccountState` event so the strategy layer can gate trading on it. ## Mainnet onboarding Mainnet onboarding mirrors testnet against the production dashboard. Use real funds. 1. **Sign in to the mainnet dashboard.** Open [derive.xyz](https://derive.xyz) and connect the EVM owner wallet (MetaMask, WalletConnect, social login, etc.). First sign-in deploys your Derive Chain smart-contract wallet. 2. **Copy the wallet address.** Under "Developers" -> "Derive Wallet", copy the smart-contract wallet address. This is the `wallet_address` the client signs against; it is **distinct** from the EOA you signed in with. Verify on the Derive Chain explorer that the address has contract code (EOAs do not). 3. **Create or pick a subaccount.** Open a subaccount under the wallet (Standard Margin is the simplest mode; switch to Portfolio Margin only once you understand the cross-margin semantics). The integer id is the `subaccount_id`. 4. **Generate a mainnet session key.** Under "Developers" -> "Session Keys", create a session key scoped to the subaccount and copy the raw secp256k1 private key. Session keys can be rotated or revoked from the same panel; prefer short-lived keys for exploratory tester runs. 5. **Fund the subaccount.** Deposit USDC (or supported collateral) into the subaccount via the dashboard's deposit flow. Confirm via `private/get_subaccount` (or the adapter's `query_account`) that the deposit lands in `collaterals_value` and `initial_margin` stays positive after the intended order. 6. **Set the environment variables.** Export the three mainnet values (or pass them on `DeriveExecutionClientConfig`, where the config field wins): ```bash export DERIVE_WALLET_ADDRESS="0x..." # Derive Chain smart-contract wallet export DERIVE_SESSION_PRIVATE_KEY="0x..." # secp256k1 session-key private key export DERIVE_SUBACCOUNT_ID="12345" # integer subaccount id ``` Each Rust example (`node_data_tester`, `node_exec_tester`, `node_delta_neutral`) pins the network with a `const DERIVE_ENVIRONMENT: DeriveEnvironment` literal near the top of the file. Check that constant before every run and edit it to switch networks; the examples do not read the network from the environment. Production deployments select the network via `DeriveDataClientConfig::environment` / `DeriveExecutionClientConfig::environment`. ## Referral code attribution Every signed order, replace, and trigger order carries the hard-coded NautilusTrader referral code. Derive funds the referral program from its own revenue, so attribution adds no trading cost, needs no approval, and is not configurable. This helps us gauge real usage of the integration and prioritize ongoing maintenance. ## Capabilities ### Market data | Capability | Supported | Notes | | ------------------------------ | --------- | ----------------------------------------------------------------------- | | Request instrument (REST) | ✓ | `public/get_instrument`; loads one instrument into the local cache. | | Request all instruments (REST) | ✓ | `public/get_instruments`; salvages valid rows for each currency. | | Instrument subscription | - | *Not supported.* Use the configured REST refresh interval. | | Order book deltas (L2_MBP) | ✓ | Channel: `orderbook.{instrument}.{group}.{depth}`. | | Order book depth10 (L2_MBP) | ✓ | Same order book channel with `depth=10`. | | Order book at interval | - | *Not supported.* Maintain interval books from deltas locally. | | Order book snapshot (REST) | - | *Not supported.* The venue has no book snapshot endpoint. | | Historical book deltas (REST) | - | *Not supported.* The venue has no historical book endpoint. | | Quotes (`ticker_slim`) | ✓ | Channel: `ticker_slim.{instrument}.{interval}`. | | Quote snapshot (REST) | ✓ | One-shot `public/get_tickers`; emits a single `QuoteTick`. | | Historical quotes (REST) | - | *Not supported.* The venue exposes ticker snapshots only. | | Trades | ✓ | Channel: `trades.{instrument_type}.{currency}`. | | Historical trades (REST) | ✓ | Chronological and deduplicated; `limit` retains the newest trades. | | Bars / OHLC (REST) | ✓ | Closed minute, hour, day, and week bars stamped at bucket close. | | Bars / OHLC (WS) | - | *Not supported.* The venue has no candle subscription channel. | | Mark price stream | ✓ | Derived from `ticker_slim`; shares the quote subscription. | | Index price stream | ✓ | Derived from `ticker_slim`; shares the quote subscription. | | Funding rate stream | ✓ | Derived from the funding rate field on perp tickers. | | Funding rate history (REST) | ✓ | Chronological for perpetuals; `limit` retains the newest valid rows. | | Instrument status | - | *Not supported.* The instrument definition carries `is_active`. | | Instrument close | - | *Not supported.* The venue publishes option settlement over REST only. | | Option greeks | ✓ | Derived from `option_pricing` on option tickers. | | Option chain | ✓ | Aggregated from quotes and greeks; `public/get_tickers` bootstraps ATM. | #### Instrument loading `request_instrument` calls `public/get_instrument` for the requested `InstrumentId` and caches the returned definition before emitting the response. The cached instrument carries the precision and increment fields used by later quote, trade, book, and bar parsing. Instrument loading treats venue error `12001` as an empty result for the affected product type, so a currency without a perp, option, or spot listing does not block its other products. Invalid instrument rows are logged and skipped while valid rows continue to load. #### Historical data Historical requests use `public/get_trade_history`, `public/get_tradingview_chart_data`, and `public/get_funding_rate_history`. The bar `end` bound still selects buckets by their start time at the venue. Responses omit any bucket whose close is after the request time, including the still-forming bucket returned by the venue. Trade history returns one maker row and one taker row per trade under the same `trade_id`, and each row's `direction` is that participant's own side, while the public WS trades feed defines `direction` as the taker's side. Trade requests emit one `TradeTick` per trade whose aggressor side is the taker's direction, independent of row order; rows with an absent or `unknown` `liquidity_role` fall back to treating `direction` as the taker's side. Bars require `EXTERNAL` aggregation and `PriceType::Last`, since Derive candles are trade-based. The venue's candle periods map to 1, 5, 15, and 30 minute, 1, 4, and 8 hour, 1 day, and 1 week steps; any other bar specification is rejected before the request goes out. #### Order book feeds Derive exposes book deltas and depth10 snapshots through the same `orderbook.{instrument}.{group}.{depth}` channel family. `subscribe_book_deltas` publishes snapshot deltas as `OrderBookDeltas`, while `subscribe_book_depth10` fixes `depth=10` and publishes `OrderBookDepth10` snapshots. ### Execution Derive uses the configured session key for authenticated execution: - Order submission and replacement requests carry locally generated EIP-712 signatures. - The live execution client sends order writes over the authenticated WebSocket and receives account, order, trade, and balance updates through private channels. - Report generation, account refreshes, and instrument lookups use REST. :::note `DeriveHttpClient` also exposes HTTP order-entry methods for tooling and tests. ::: Perpetuals, options, and ERC-20 spot pairs all use the Derive Trade module. Spot has no separate signing path, and reconciliation treats spot instruments like other instrument classes except for the reduce-only guard described below. The adapter supports ordinary `private/order` requests: `LIMIT` and `MARKET` orders with `GTC`, `IOC`, or `FOK` time-in-force values. It also supports Derive trigger orders for the Nautilus-native stop and if-touched order types listed below. Unsupported Nautilus order types are denied locally with `OrderDenied` before submission, so they cannot fill at the venue. Market orders require a cached quote before submission; without one the adapter emits `OrderDenied` and never signs. After the async submit task resolves the instrument, it refreshes the current ticker snapshot and derives the signed slippage-bound `limit_price` from that refreshed quote. #### Account state Derive holds margin at the subaccount level, so the `AccountState` mapping is: - Balances: `total` is the collateral `amount` (for example USDC or ETH) in its native units and `locked` is zero, because the venue reports no per-collateral reservation. `collaterals[].initial_margin` is USD credit contributed by that collateral, not locked funds. - Margins: one account-wide `MarginBalance` where `initial = positions_initial_margin + open_orders_margin` and `maintenance = positions_maintenance_margin`. These venue fields are USD requirements, stamped with the subaccount currency. Immediately after a closing trade the venue can transiently report negative `positions_*_margin` values equal to the not-yet-settled cash movement (realized PnL and fees); the fields return to zero once settlement lands in the collateral balance. Gate trading on `net_initial_margin` / `net_maintenance_margin` in `AccountState.info`, not on these requirement fields. - Info: the subaccount's signed net health is not a margin requirement, so it travels in the `AccountState.info` map as `net_initial_margin` and `net_maintenance_margin` alongside `positions_initial_margin`, `positions_maintenance_margin`, `open_orders_margin`, and `is_under_liquidation`. Decimal values are JSON strings. #### Conditional orders Derive trigger orders use the WebSocket-only `private/trigger_order` endpoint, not the normal `private/order` endpoint. The venue stores them with `order_status=untriggered` until its trigger worker submits the signed child order. Reconciliation therefore reads both `private/get_open_orders` and `private/get_trigger_orders`. Derive mainnet requires trigger-order signatures to expire 30 to 90 days from venue time. The adapter signs trigger orders with a fixed 31-day expiry; `signature_expiry_secs` still controls ordinary `private/order` and `private/replace` writes, and must be greater than the 300s venue minimum. | Nautilus order type | Supported | Derive `order_type` | Derive `trigger_type` | Notes | | ------------------- | --------- | ------------------- | --------------------- | ------------------------------ | | `StopMarket` | ✓ | `market` | `stoploss` | Uses trigger price as bound. | | `StopLimit` | ✓ | `limit` | `stoploss` | Sends limit and trigger price. | | `MarketIfTouched` | ✓ | `market` | `takeprofit` | Uses trigger price as bound. | | `LimitIfTouched` | ✓ | `limit` | `takeprofit` | Sends limit and trigger price. | | `MarketToLimit` | - | - | - | *Not supported by Derive*. | | Trailing stops | - | - | - | *Not supported by Derive*. | | TWAP / algo / RFQ | - | - | - | *Not exposed by this adapter*. | The adapter maps Nautilus `TriggerType::Default` and `TriggerType::MarkPrice` to Derive `trigger_price_type=mark`. Derive's current error-code reference states that index and last-trade trigger price types are not supported yet, so `IndexPrice`, `LastPrice`, `BidAsk`, and other trigger price types are denied locally with `OrderDenied` before submission. Derive error `11054` states that trigger orders cannot replace or be replaced. The adapter therefore rejects Nautilus modify requests for trigger orders with an `OrderModifyRejected` event; cancel and resubmit for trigger updates. Derive validates the trigger price side and rejects a trigger that does not sit beyond the current price in the expected direction with error `11051`. The trigger price is fixed when the order is signed, so a tight offset on a fast-moving or high-priced instrument can drift onto the wrong side before the venue receives the order. Size the trigger offset to comfortably exceed expected price movement during submission (for `ETH-PERP`, tens of dollars rather than a few cents); a too-tight offset produces spurious `11051` rejections. #### Bulk cancellation ##### Selection and routing Derive supports both multi-order cancellation methods exposed by `Strategy`. | Strategy method | Supported | Parameters | Notes | | ------------------------ | --------- | --------------------------------------------------------------------- | ------------------------------------------ | | `cancel_orders(...)` | ✓ | `client_order_ids`, `client_id`, `params` | All orders must use the same instrument. | | `cancel_all_orders(...)` | ✓ | `instrument_id`, `order_side`, `client_id`, `strategy_only`, `params` | Defaults to the calling strategy's orders. | The Derive execution client applies these methods as follows: - `cancel_orders` cancels each requested regular or trigger order individually. - `cancel_all_orders` with `strategy_only=True` expands cached matches into individual cancels. - `cancel_all_orders` with `strategy_only=False` and a Buy or Sell filter selects open regular and trigger orders from the cache for the configured execution client, account, exact instrument, and side, then cancels each match. It never widens to both sides. - `cancel_all_orders` with `strategy_only=False` and no side filter selects matching open triggers from the same execution client, account, and instrument scope and cancels them individually, then sends `private/cancel_by_instrument` for regular orders. It never sends `private/cancel_all`. `cancel_all_orders` treats the cache as authoritative and does not query venue order state before cancellation. Eligibility follows the cache-selection rules above. ##### Failure handling If an eligible cached order lacks a venue order ID, the command fails closed, logs a warning, sends no cancellation request, and emits no order event. `private/cancel_by_instrument` cancels regular open orders only. A successful request with `cancelled_orders == 0` is an expected no-op and logs at debug level. A failed trigger cancellation logs a warning but does not suppress the regular instrument cancellation. A failed bulk request has no per-order outcome to emit; private channel updates and later reconciliation remain responsible for observed order state. #### Execution instructions | Instruction | Supported | Derive value | Notes | | ------------- | --------- | ------------- | ----------------------------------------------------------- | | `post_only` | ✓ | `post_only` | Requires `GTC`; rejects if the order would take liquidity. | | `reduce_only` | ✓ | `reduce_only` | Perps and options, market or `IOC`/`FOK` only; spot denied. | #### Time in force Derive documents `gtc`, `post_only`, `fok`, and `ioc` as its `time_in_force` values. Nautilus values with no Derive equivalent are denied locally with `OrderDenied` before submission. Derive exposes post-only as a `time_in_force` value, so `post_only` cannot combine with `IOC` or `FOK`. | Time in force | Supported | Derive value | Notes | | -------------- | --------- | ------------ | -------------------------- | | `GTC` | ✓ | `gtc` | Good Till Canceled. | | `IOC` | ✓ | `ioc` | Immediate or Cancel. | | `FOK` | ✓ | `fok` | Fill or Kill. | | `GTD` | - | - | *Not supported by Derive*. | | `DAY` | - | - | *Not supported by Derive*. | | `AT_THE_OPEN` | - | - | *Not supported by Derive*. | | `AT_THE_CLOSE` | - | - | *Not supported by Derive*. | #### Spot reduce-only orders Derive spot has no position concept, so a reduce-only spot order can never reduce anything. The venue always rejects it with error `11025`; the adapter avoids that round-trip when it knows the instrument is spot. Cached spot instruments are denied with `OrderDenied`; lazily resolved spot instruments are rejected with `OrderRejected` during submit. Reduce-only orders for perpetuals and options still reach the venue, where the outcome depends on the subaccount's position state. The `derive-flatten` bin closes derivative positions only and never spot, since flattening a spot balance would dump the base asset into a different quote. Derive only honors `reduce_only` on market orders or non-resting limits (`IOC`/`FOK`). A resting `GTC` or post-only limit with `reduce_only` is rejected by the venue with error `11024 Reduce only not supported with this time in force`. As a result a Nautilus bracket whose take-profit leg is a reduce-only `GTC` limit cannot rest on Derive: the entry and stop-loss legs submit, but the take-profit is rejected. Use a reduce-only `IOC`/`FOK` close or a non-reduce-only take-profit when targeting Derive. #### Order rejection semantics State-changing writes (`submit_order`, `modify_order`, `cancel_order`) are sent once over the WebSocket and are not replayed. The adapter keys terminal vs ambiguous handling off the WebSocket request outcome. It emits a terminal rejection event (`OrderRejected`, `OrderModifyRejected`, `OrderCancelRejected`) for definitive venue failures: - Signed-action rejections such as invalid params, insufficient margin, or unknown orders. - Venue business codes such as `11009 Zero liquidity`. - Post-only crossing rejections (`11008 Post only order cannot cross the market`), reported as `OrderRejected` with `due_post_only=true`. - Rate-limit responses (`-32000 Rate limit exceeded`), where the gateway rejects the request before the matching engine sees it. - A successful `private/cancel_by_label` response with `cancelled_orders == 0`, which means no open order matched the client order ID label. For other `cancelled_orders` values, the adapter emits no terminal event and waits for the venue order notification or later reconciliation to settle the state. The venue may return `cancelled_orders == -1` for a cancel that matched its label-wildcard path; this is treated as success and settled the same way. For post-only orders that reach the venue, Derive rejects a crossing order with JSON-RPC `11008` and message `Post only order cannot cross the market`. The adapter marks that terminal rejection with `due_post_only=true`; if a WebSocket/order-report rejection carries the same reason, the tracked order path applies the same classification. Local denials for unsupported post-only IOC/FOK combinations are `OrderDenied` events without `due_post_only`, because they do not represent a venue crossing rejection. For ambiguous write outcomes, the adapter emits no terminal event and lets WebSocket reconciliation or later status reports settle the state. The ambiguous set is deliberately narrow: - `-32603`, a generic JSON-RPC internal error. - A response that cannot be decoded (the action may have been processed). - Request timeouts, dropped responses on reconnect, and transport errors. This distinction protects both sides of the order lifecycle. A false terminal rejection can make the engine treat a live order as rejected; a false ambiguous outcome can leave an unplaced order hanging in `Submitted` forever because no WebSocket frame will arrive. ## Rate limiting ### Window model Derive refills every request allowance discretely at fixed five-second window boundaries, not one request at a time (source: [venue rate limits](https://docs.derive.xyz/reference/rate-limits)). The adapter mirrors this with a fixed-window limiter: each request class may spend its full window allowance in one burst, and the next request waits for the boundary before it can depart. Windows are aligned to client construction because the venue's own window phase cannot be observed from the client, so a wait is at most one full window and the long-run average rate stays at the venue allowance. A burst can still straddle a venue window boundary, in which case the venue answers with `-32000 Rate limit exceeded`, which the adapter surfaces as a definitive rejection (see [order rejection semantics](#order-rejection-semantics)). ### Request buckets Each window allowance is the documented requests-per-second rate times the five-second window, so the configured `max_matching_requests_per_second` and `max_per_instrument_matching_requests_per_second` (1 each for Trader) admit five requests per window: | Bucket | Trader-tier allowance | | ---------------------------------- | ---------------------- | | Matching (account-wide) | 5 requests per window | | Per-instrument matching | 5 requests per window | | REST non-matching (per IP) | 50 requests per window | | WebSocket non-matching (logged in) | 25 requests per window | | `private/cancel_all` | 5 requests per window | | Unscoped `private/cancel_by_label` | 50 requests per window | The REST per-IP allowance is flat across tiers. The WebSocket non-matching allowance applies to sessions authorized via `public/login`; the venue applies a reduced, unspecified allowance to unauthenticated sessions, so public data-client traffic can hit venue limits earlier. The venue also caps concurrent WebSocket connections per IP (4 for a Trader). Matching-engine writes that carry an instrument (order, replace, trigger order, single-order cancel, and `private/cancel_by_instrument`) draw on both the account-wide matching bucket and that instrument's independent bucket, so a Market Maker's account-wide override never inflates the per-instrument allowance. Trigger order create and cancel methods are paced as matching writes even though the venue does not list them explicitly; `private/cancel_trigger_order` carries no instrument and draws on the account-wide bucket only. This conservative classification stays within Derive's documented rate contract. ### Signing and venue responses Pacing waits happen before the request is signed, so a delay never consumes the validity of the REST `X-LYRA*` authentication headers or of the nonces and EIP-712 signatures on the WebSocket order path. A matching write whose window rolls between signing and dispatch is re-paced once before departing (at most one further window, covered by the >300s signature TTL). A venue rate-limit response remains a definitive rejection; see [order rejection semantics](#order-rejection-semantics). The remaining live allowances can be checked manually over the WebSocket via `private/getRateLimits`; the adapter does not call it. ## WebSocket recovery The data and execution clients reconnect automatically after a peer close, transport error, or heartbeat timeout. The adapter sends protocol Ping frames every 30 seconds and treats 60 seconds without any inbound frame as a dead connection; the selected transport can report a missed Pong sooner. The transport retries connections with exponential backoff and jitter. Recovery completes in this order: 1. The transport reconnects. If another disconnect occurs, recovery follows the latest connection. 1. Credentialed sessions log in again, then every session replays its confirmed subscriptions. Acknowledged unsubscriptions are not replayed. 1. The execution client refreshes account state and generates mass status for orders, fills, and positions. State-changing requests follow the [order rejection semantics](#order-rejection-semantics): the adapter sends them once and never replays them. After three failed login or subscription recovery attempts, the client logs the error, marks itself disconnected, and stops that WebSocket session. `ws_timeout_secs` applies to individual WebSocket operations, not heartbeat detection or reconnect backoff. ## Subscription parameters `subscribe_book_deltas` and `subscribe_book_depth10` accept these `subscribe_params` keys: | Key | Type | Default | Allowed | | ------- | ------ | ------- | ------------------------------ | | `group` | string | `"1"` | `"1"`, `"10"`, `"100"` | | `depth` | string | `"10"` | `"1"`, `"10"`, `"20"`, `"100"` | `subscribe_quotes` accepts: | Key | Type | Default | Allowed | | ---------- | ------ | -------- | ----------------- | | `interval` | string | `"1000"` | `"100"`, `"1000"` | Unknown values are rejected at subscribe time. ### Shared ticker subscription Quotes, mark prices, index prices, funding rates, and option greeks are all derived from the same `ticker_slim.{instrument}.{interval}` WebSocket subscription. The adapter reference-counts the underlying WS subscribe call: the first feed subscribed for an instrument opens the channel and the last unsubscribe closes it. As a consequence, the `interval` from the first subscribe wins; subsequent feeds subscribing with a different interval share the existing channel. Mark prices, index prices, funding rates, and option greeks read fields from the ticker payload. Both the full ticker shape and the compact `ticker_slim` shape carry these fields, so the derived feeds work on either: `mark_price` and `index_price` are required (a frame missing them fails to deserialize and is logged, rather than silently dropped), while `funding_rate` and `option_pricing` are optional and present only for the relevant instrument class. The quote feed always works because bid/ask are present in both shapes. Funding rates are only meaningful for perpetuals, and option greeks only for options. Subscribing the wrong feed for an instrument's class (e.g. funding rates for an option) is accepted and the WebSocket subscription opens, but the parser returns no events for that feed because the venue payload omits the funding rate field for non-perps and `option_pricing` for non-options. Verify the instrument class before subscribing to derivative-specific feeds. ## Configuration ### Data client configuration options Class/struct: `DeriveDataClientConfig`. | Option | Default | Description | | ---------------------------------- | --------- | ------------------------------------------------------------------------------------------- | | `base_url_rest` | `None` | Override for the REST base URL. | | `base_url_ws` | `None` | Override for the WebSocket base URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `environment` | `Mainnet` | Network selector (`MAINNET` or `TESTNET` in Python). | | `http_timeout_secs` | `10` | REST request timeout in seconds. | | `ws_timeout_secs` | `None` | Per-operation WebSocket timeout (login, subscribe, read, write) in seconds. Unset uses 10s. | | `update_instruments_interval_mins` | `60` | Interval in minutes between instrument refreshes. | | `currencies` | `[]` | Currencies to bulk-load on connect. Empty means lazy-load on demand. | | `include_expired` | `false` | Include expired option rows from `public/get_instruments`. | | `auto_load_missing_instruments` | `true` | Lazy-load an unknown instrument before sending a subscribe request. | | `transport_backend` | `Sockudo` | WebSocket transport when `transport-sockudo` is enabled. | `auto_load_missing_instruments` covers subscribe commands only. The request commands (`request_quotes`, `request_trades`, `request_bars`, and `request_funding_rates`) fail when the instrument is not already cached, so bulk-load its currency or subscribe first. `request_instrument` is the exception: it always fetches `public/get_instrument`. Option-chain subscriptions also require a cached option from the series to fetch the initial reference price. ### Execution client configuration options Class/struct: `DeriveExecutionClientConfig`. | Option | Default | Description | | ------------------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `account_id` | `Venue` | Nautilus account identifier; defaults to `DERIVE-001`. | | `wallet_address` | `None` | Derive Chain smart-contract wallet address. Falls back to env vars below. | | `session_key` | `None` | secp256k1 session-key private key. Falls back to env vars below. | | `subaccount_id` | `None` | Derive subaccount id. Falls back to env vars below. | | `base_url_rest` | `None` | Override for the REST base URL. | | `base_url_ws` | `None` | Override for the WebSocket base URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `environment` | `Mainnet` | Network selector (`MAINNET` or `TESTNET` in Python). | | `http_timeout_secs` | `10` | REST request timeout in seconds. | | `ws_timeout_secs` | `None` | Per-operation WebSocket timeout (login, subscribe, read, write) in seconds. Unset uses 10s. | | `max_retries` | `3` | Retry attempts for idempotent REST reads. Order writes are sent once and never replayed. | | `retry_delay_initial_ms` | `100` | Initial retry delay in milliseconds. | | `retry_delay_max_ms` | `5,000` | Maximum retry delay in milliseconds. | | `max_fee_per_contract` | Required | Positive per-contract USDC fee cap signed into each order. | | `domain_separator` | `None` | Optional EIP-712 domain separator override. | | `action_typehash` | `None` | Optional EIP-712 action typehash override. | | `trade_module_address` | `None` | Optional Trade module contract address override. | | `signature_expiry_secs` | `600` | Order/replace TTL; must be >300s. Trigger orders use fixed 31-day TTL. | | `market_order_slippage_bps` | `50` | Slippage bound for market-order limit prices. | | `max_matching_requests_per_second` | `None` | Account-wide matching-engine write requests/sec (order/replace/cancel incl. trigger methods). Defaults to the Trader-tier limit of 1 when unset; raise for Market Maker accounts. | | `max_per_instrument_matching_requests_per_second` | `None` | Per-instrument matching write requests/sec, enforced independently of the account-wide limit. Defaults to the Trader-tier limit of 1 when unset; raise for Market Maker accounts. | | `transport_backend` | `Sockudo` | WebSocket transport when `transport-sockudo` is enabled. | The default transport falls back to `Tungstenite` when the build disables the `transport-sockudo` feature. The `wallet_address`, `session_key`, and `subaccount_id` fall back to environment variables when unset: | Field | Mainnet variable | Testnet variable | | ---------------- | ---------------------------- | ------------------------------------ | | `wallet_address` | `DERIVE_WALLET_ADDRESS` | `DERIVE_TESTNET_WALLET_ADDRESS` | | `session_key` | `DERIVE_SESSION_PRIVATE_KEY` | `DERIVE_TESTNET_SESSION_PRIVATE_KEY` | | `subaccount_id` | `DERIVE_SUBACCOUNT_ID` | `DERIVE_TESTNET_SUBACCOUNT_ID` | The session key is the secp256k1 private key registered on the wallet for API signing. The `session_key` field is redacted in `Debug` output and Python `repr`. ### Python live node Python nodes use `LiveNode.builder(...)` and pass concrete factory instances. The node supplies the trader identifier, while `DeriveExecutionClientConfig` supplies the account identifier. ```python from decimal import Decimal from nautilus_trader.adapters.derive import DeriveDataClientConfig from nautilus_trader.adapters.derive import DeriveDataClientFactory from nautilus_trader.adapters.derive import DeriveEnvironment from nautilus_trader.adapters.derive import DeriveExecutionClientConfig from nautilus_trader.adapters.derive import DeriveExecutionClientFactory from nautilus_trader.common import Environment from nautilus_trader.live import LiveNode from nautilus_trader.model import AccountId from nautilus_trader.model import TraderId trader_id = TraderId("TESTER-001") data_config = DeriveDataClientConfig( environment=DeriveEnvironment.TESTNET, currencies=["ETH", "BTC"], ) exec_config = DeriveExecutionClientConfig( account_id=AccountId("DERIVE-001"), environment=DeriveEnvironment.TESTNET, max_fee_per_contract=Decimal("1000"), ) node = ( LiveNode.builder("DERIVE-001", trader_id, Environment.LIVE) .add_data_client(None, DeriveDataClientFactory(), data_config) .add_exec_client(None, DeriveExecutionClientFactory(), exec_config) .build() ) ``` ### Rust data client ```rust use nautilus_derive::{ common::enums::DeriveEnvironment, config::DeriveDataClientConfig, }; let config = DeriveDataClientConfig { environment: DeriveEnvironment::Testnet, currencies: vec!["ETH".to_string(), "BTC".to_string()], ..Default::default() }; ``` ### Rust execution client ```rust use nautilus_derive::{ common::enums::DeriveEnvironment, config::DeriveExecutionClientConfig, }; use rust_decimal::Decimal; let config = DeriveExecutionClientConfig { wallet_address: Some("0x...".to_string()), session_key: Some("0x...".to_string()), subaccount_id: Some(1), environment: DeriveEnvironment::Testnet, max_fee_per_contract: Some(Decimal::from(1000)), ..Default::default() }; ``` `max_fee_per_contract` is required and must be greater than zero. Execution-client construction fails before creating venue clients when the field is missing or non-positive. ## Known limitations - `request_instruments` requires at least one configured currency in `DeriveDataClientConfig::currencies`; the venue's `public/get_instruments` endpoint is scoped per-currency and the adapter does not enumerate the currency universe. - The venue does not push instrument status, instrument close, or candle subscriptions; the instrument definition carries `is_active` and the scheduled activation/deactivation timestamps, and bars are REST-only. - The book snapshot REST endpoint and historical book deltas / historical quote endpoints are not exposed by the venue. See the capabilities table above. - Derive's official REST docs mark `public/get_ticker` as deprecated in favor of `public/get_tickers` as of December 1, 2025. The adapter uses `public/get_tickers` for quote snapshots and option-chain reference-price bootstrap. # dYdX Source: https://nautilustrader.io/docs/latest/integrations/dydx/ dYdX is one of the largest decentralized cryptocurrency exchanges for crypto derivative products. This integration supports live market data ingestion and order execution with dYdX v4, running on its own Cosmos SDK application-specific blockchain (dYdX Chain) with CometBFT consensus. The order book and matching engine run on-chain as part of the validator process. Orders are submitted as Cosmos transactions via gRPC and settled each block. An Indexer service exposes REST and WebSocket APIs for market data and account state. ## Installation :::note No additional installation extras are required. The adapter is implemented in Rust and compiled into the core `nautilus_trader` package automatically during the build. ::: ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/dydx/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/dydx/examples/) ## Overview This adapter is implemented in Rust with Python bindings via PyO3. It provides direct integration with dYdX's Indexer API (REST/WebSocket) for market data and gRPC for Cosmos SDK transaction submission, without requiring external client libraries. ### Product support | Product Type | Data Feed | Trading | Notes | | ----------------- | --------- | ------- | ---------------------------------------------------------- | | Perpetual Futures | ✓ | ✓ | All perpetuals are USDC-settled. | | Spot | - | - | dYdX offers spot on Solana; not supported by this adapter. | | Options | - | - | *Not available on dYdX*. | :::note This adapter supports perpetual futures only. All markets are quoted in USD and settled in USDC. ::: ## Chain architecture Unlike centralized exchanges (CEXs) that expose a single REST/WebSocket API, dYdX v4 runs on its own **Cosmos SDK application-specific blockchain**. This means every trade is a Cosmos transaction that goes through consensus, and the adapter must manage sequences, gas, and block-height-based expiration. ### Transport layers The adapter communicates through three independent transport layers: ``` ┌─────────────────────────────────────────────┐ │ dYdX v4 Chain │ │ │ ┌──────────┐ HTTP │ ┌──────────────────────┐ │ │ │───────────►│ │ Indexer (read-only) │ │ │ │ WebSocket │ │ - REST API │ │ │ Nautilus │───────────►│ │ - Streaming API │ │ │ Adapter │ │ └──────────────────────┘ │ │ │ gRPC │ ┌──────────────────────┐ │ │ │───────────►│ │ Validator (write) │ │ └──────────┘ │ │ - Cosmos Tx submit │ │ │ │ - Sequence mgmt │ │ │ └──────────────────────┘ │ └─────────────────────────────────────────────┘ ``` | Layer | Target | Direction | Purpose | | --------- | --------- | --------- | ---------------------------------------------------- | | HTTP | Indexer | Read-only | Instrument metadata, historical data, account state. | | WebSocket | Indexer | Read-only | Real-time market data, order/fill/position updates. | | gRPC | Validator | Write | Order placement, cancellation, and batch operations. | ### Block-based settlement Trades settle on block commit, and short-term orders expire by block height rather than wall-clock time. The adapter tracks block heights and timestamps from the WebSocket feed over a rolling 100-block window and estimates `seconds_per_block` from it, then uses that estimate to convert time-based order expiry into block-height offsets. Until five block samples have been collected, the estimate falls back to **500 ms** per block. Observed mainnet block times run closer to one second, so the fallback understates the short-term window and routes borderline orders to the long-term path rather than the reverse. ## Architecture The dYdX v4 adapter includes multiple components which can be used together or separately: - `DydxHttpClient`: HTTP client for Indexer REST API queries. - `DydxWebSocketClient`: WebSocket client for Rust callers. - `DydxGrpcClient`: gRPC client for Cosmos SDK transaction submission. - `InstrumentCache`: Instrument parsing and loading, shared by the HTTP, WebSocket, and execution clients. - `DydxDataClient`: Market data feed manager. - `DydxExecutionClient`: Account management and trade execution gateway. - `DydxDataClientFactory`: Factory for dYdX v4 data clients (used by the trading node builder). - `DydxExecutionClientFactory`: Factory for dYdX v4 execution clients (used by the trading node builder). :::note Most users will define a configuration for a live trading node (as below), and won't need to work with these lower level components directly. ::: :::warning[First-time account activation] A dYdX v4 trading account (sub-account 0) is created only after the wallet's first deposit or trade. Until then, gRPC and Indexer account queries return not-found, so `DydxExecutionClient.connect()` fails while initializing the transaction sequence. Before starting a `LiveNode`, send any positive amount of USDC or other supported collateral from the same wallet on the same network (mainnet/testnet). Once the transaction has finalized (a few blocks), restart the node and the client will connect cleanly. ::: ## Troubleshooting ### gRPC `NotFound` on connect **Cause:** The wallet/sub-account has never been funded and therefore does not yet exist on-chain. **Fix:** 1. Deposit any positive amount of USDC to sub-account 0 on the correct network. 2. Wait for finality (roughly 30 seconds on mainnet, longer on testnet). 3. Restart the `LiveNode`; the connection should now succeed. :::tip In unattended deployments, wrap the `connect()` call in an exponential-backoff loop so the client retries until the deposit appears. ::: ## Symbology dYdX uses specific symbol conventions for perpetual futures contracts. ### Symbol format Format: `{Base}-USD-PERP` All perpetuals on dYdX are: - Quoted in USD - Settled in USDC - Use the `.DYDX` venue suffix in Nautilus Examples: - `BTC-USD-PERP.DYDX` - Bitcoin perpetual futures - `ETH-USD-PERP.DYDX` - Ethereum perpetual futures - `SOL-USD-PERP.DYDX` - Solana perpetual futures To subscribe in your strategy: ```python InstrumentId.from_str("BTC-USD-PERP.DYDX") InstrumentId.from_str("ETH-USD-PERP.DYDX") ``` :::info The dYdX Indexer ticker for a perpetual is `{Base}-USD` (for example `BTC-USD`). The adapter appends the `-PERP` suffix for consistency with other adapters and to leave room for other product types. ::: ## Orders capability dYdX supports perpetual futures trading with a full set of order types and execution features. The adapter automatically classifies each order as short-term, long-term, or conditional from its type, time-in-force, and expiry, so no manual tagging is needed. ### Order types | Order Type | Perpetuals | Notes | | ---------------------- | ---------- | -------------------------------------------------- | | `MARKET` | ✓ | Immediate execution at best available price. | | `LIMIT` | ✓ | | | `STOP_MARKET` | ✓ | Stop-loss conditional order, always stateful. | | `STOP_LIMIT` | ✓ | Conditional order, always stateful. | | `MARKET_IF_TOUCHED` | ✓ | Take-profit market order, triggers on price touch. | | `LIMIT_IF_TOUCHED` | ✓ | Take-profit limit order, triggers on price touch. | | `TRAILING_STOP_MARKET` | - | *Not supported*. | | `TRAILING_STOP_LIMIT` | - | *Not supported*. | ### Execution instructions | Instruction | Perpetuals | Notes | | ------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `post_only` | ✓ | Supported on LIMIT, STOP_LIMIT, and LIMIT_IF_TOUCHED orders. A post-only order priced to cross the spread is **accepted then immediately canceled** by the venue (not rejected with a reason). | | `reduce_only` | ✓ | Accepted by the chain **only on orders that execute immediately** (IOC). Anything else is rejected on-chain with `code=9003`, `Reduce-only is currently disabled for non-IOC orders`. | How the adapter handles the flag depends on the order type: | Order type | `reduce_only` behavior | | ----------------------------------------- | ------------------------------------------------------------------------------ | | `MARKET` | Forwarded as an IOC order. | | `LIMIT`, `STOP_LIMIT`, `LIMIT_IF_TOUCHED` | Forwarded with your time in force. Use `IOC` or the chain rejects it. | | `STOP_MARKET`, `MARKET_IF_TOUCHED` | Forwarded, but these carry no time in force, so the chain always rejects them. | Set `reduce_only` on market orders or use it with `IOC` on the supported limit order types. ### Time in force options | Time in force | Perpetuals | Notes | | ------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GTC` | ✓ | Good Till Canceled. | | `GTD` | ✓ | Good Till Date. The venue reports expiry as a cancel event; the adapter maps this to `OrderExpired` (not `OrderCanceled`) when the order's `expire_time` has passed. | | `IOC` | ✓ | Immediate or Cancel. | | `FOK` | - | *Deprecated by dYdX v4*. The chain rejects FOK orders with `code=48`; the adapter generates `OrderDenied` locally and does not broadcast. | | `DAY` | - | *Not supported*. The adapter generates `OrderDenied` locally and does not broadcast. | ### Advanced order features | Feature | Perpetuals | Notes | | ------------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Order modification | - | Not supported. dYdX supports short-term order [replacement](https://docs.dydx.xyz/concepts/trading/limit-orderbook#replacements) (same ID, higher GTB); not yet exposed as `ModifyOrder`. | | Bracket/OCO orders | - | *Not supported*. | | Iceberg orders | - | *Not supported*. | ### Batch operations | Operation | Perpetuals | Notes | | ------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------- | | Batch submit | ✓ | Supported for long-term `LIMIT` orders. Short-term orders are submitted individually. | | Batch modify | - | *Not supported*. | | Batch cancel | ✓ | Partitioned: short-term orders use `MsgBatchCancel` (single gRPC call), long-term orders use batched `MsgCancelOrder`. | ### Position management | Feature | Perpetuals | Notes | | ---------------- | ---------- | --------------------------------------------------------------- | | Query positions | ✓ | Real-time position updates. | | Position mode | - | Netting only (see below). | | Leverage control | - | Set by each market's margin fractions; no per-account override. | | Margin mode | - | Cross margin only. | :::note dYdX nets positions (one position per instrument) at the venue level, so the adapter operates in `NETTING` mode only. ::: ### Order querying | Feature | Perpetuals | Notes | | -------------------- | ---------- | ------------------------------ | | Query open orders | ✓ | List all active orders. | | Query order history | ✓ | Historical order data. | | Order status updates | ✓ | Real-time order state changes. | | Trade history | ✓ | Execution and fill reports. | ### Contingent orders | Feature | Perpetuals | Notes | | ------------------ | ---------- | ------------------------------------------------ | | Order lists | - | *Not supported*. | | OCO orders | - | *Not supported*. | | Bracket orders | - | *Not supported*. | | Conditional orders | ✓ | Stop, take-profit market, and take-profit limit. | ### Equity tier limit dYdX caps how many **stateful orders** (long-term and conditional) a subaccount may hold open at once, based on the subaccount's net collateral. Short-term orders are exempt from the cap. Submitting past the cap is rejected on-chain with `code=10001` and a log message of the form `Opening order would exceed equity tier limit of N`. Cancel existing stateful orders before placing more, or split strategies across subaccounts. | Net collateral | Maximum open stateful orders | | ------------------- | ---------------------------- | | Under $20 | 0 | | $20 to $100 | 10 | | $100 to $1,000 | 20 | | $1,000 to $10,000 | 40 | | $10,000 to $100,000 | 100 | | $100,000 and above | 200 | The tiers are governance-adjustable. Query the live values from a node's `/dydxprotocol/clob/equity_tier` endpoint, or see [equity tier limits](https://docs.dydx.xyz/concepts/trading/limits/equity-tier-limits). ### MIT and LIT round-tripping dYdX's protocol uses a single `TAKE_PROFIT` order type with a price (`subticks`) and trigger price; whether it behaves as market-on-trigger or limit-on-trigger is implicit in the price. The adapter submits Nautilus `MARKET_IF_TOUCHED` as a take-profit with the price set to the 5% pay-through worst-case, and `LIMIT_IF_TOUCHED` as a take-profit at the user's limit price. Both forms are returned by the Indexer as `"type":"TAKE_PROFIT"`. On reconciliation, the adapter recovers the original Nautilus order type from how far the reported price sits from the trigger price. A drift of **2% or more** means the price came from the 5% pay-through buffer, so the order is reconciled as `MARKET_IF_TOUCHED`; anything closer is treated as a user-chosen limit and reconciled as `LIMIT_IF_TOUCHED`. The 2% threshold separates the pay-through band from typical take-profit limit offsets, which sit well under 1%. ### Liquidation and ADL (deleveraging) handling dYdX v4 applies two sequential risk mechanisms: 1. **Liquidation** runs when an account drops below its maintenance margin. Positions close against the insurance fund within a bounded spread from the oracle price. 2. **Deleveraging (ADL)** activates when either liquidation cannot fully restore collateralisation, or when a large oracle jump drives an account negative in a single step. Deleveraging closes the undercollateralised position against randomly selected offsetting accounts. The Indexer exposes the classification via the `type` field on each `Fill` record: | `type` | Meaning | | ------------- | -------------------------------------------------- | | `LIMIT` | Normal fill. | | `LIQUIDATED` | Taker side of a liquidation (undercollateralised). | | `LIQUIDATION` | Maker side of a liquidation (insurance fund). | | `DELEVERAGED` | Taker side of a deleveraging (ADL closure). | | `OFFSETTING` | Maker side of a deleveraging (offsetting account). | Any other value the venue introduces is decoded as an unknown fill type and handled like a normal fill, so a new classification never drops the fill. The adapter logs a warning with instrument, side, size, and price for each liquidation / deleveraging fill, then emits the `FillReport` through the normal path. A position the venue reports as `LIQUIDATED` is treated as closed, which closes out the corresponding position report. Upstream references: - [Liquidations](https://docs.dydx.xyz/concepts/trading/liquidations) - [Contract loss mechanisms (deleveraging)](https://help.dydx.trade/en/articles/166973-contract-loss-mechanisms-on-dydx-chain) ### Order classification dYdX classifies every order into one of three on-chain categories. The adapter automatically determines the category based on time-in-force and expiry, so no manual configuration is required. | Category | Placement | Expiry | Typical use | | ----------- | --------- | --------------- | ------------------------------------------------- | | Short-term | In-memory | Block height | IOC, or orders expiring within 40 blocks. | | Long-term | On-chain | Timestamp (UTC) | GTC/GTD with expiry beyond the short-term window. | | Conditional | On-chain | Timestamp (UTC) | Stop-loss and take-profit triggers. | At the protocol level, **all dYdX orders are limit orders**. The `MARKET` order type is a Nautilus convenience that the adapter implements as an aggressive IOC limit order priced well through the book. This means market orders follow the same `Submitted > Accepted > Filled` lifecycle as limit orders (an `OrderAccepted` event is expected before the fill). See the [dYdX order documentation](https://docs.dydx.xyz/concepts/trading/orders) for full protocol-level details on short-term vs stateful order mechanics. #### Short-term orders Short-term orders live **in validator memory only** and expire by block height. The protocol's `ShortBlockWindow` caps their lifetime at **40 blocks** past the current height. They are the fastest order type on dYdX because they skip on-chain storage. **Properties**: - **IOC (and the deprecated FOK) are always short-term**, regardless of other parameters - **GTD orders** are automatically classified as short-term when the expiry falls within the dynamic short-term window (`40 blocks × seconds_per_block`) - Use Good-Til-Block (GTB) for replay protection instead of Cosmos SDK sequences - Can be broadcast **concurrently** (no semaphore, cached sequence) - Expire silently without generating cancel events - Cannot be batched in a single transaction (one `MsgPlaceOrder` per tx) #### Long-term orders Long-term (stateful) orders are **stored on-chain** and expire by UTC timestamp. They generate explicit cancel events when they expire or are cancelled. **Properties**: - **GTC** orders default to 90-day expiration (protocol limit is 95 days) - **GTD** orders use the user-provided expiry timestamp - Require proper Cosmos SDK sequence management (serialized via semaphore) - Must be broadcast **serially** with incrementing sequence numbers - Can be batched in a single transaction #### Conditional orders Conditional orders (stop-loss, take-profit) are **always stored on-chain** and triggered by price conditions on the validator. **Properties**: - Always use timestamp-based expiry (default 90 days for GTC, protocol limit 95 days) - Always use the long-term broadcast path (serialized with semaphore) - Include `StopMarket`, `StopLimit`, `TakeProfitMarket`, and `TakeProfitLimit` #### Automatic routing The adapter determines order lifetime automatically from the estimated block time: ``` max_short_term_secs = 40 blocks (ShortBlockWindow) × seconds_per_block ``` If the order's time until expiry is within `max_short_term_secs`, it is routed as short-term. Otherwise, it is routed as long-term. No manual configuration is needed. #### MARKET order implementation dYdX has no native market order type. The adapter implements `MARKET` orders as aggressive **IOC limit orders** priced at: - **Buy**: `oracle_price × (1 + 0.05)` (5% above oracle) - **Sell**: `oracle_price × (1 - 0.05)` (5% below oracle) This 5% slippage buffer (`DEFAULT_MARKET_ORDER_SLIPPAGE = 0.05`) sets the worst-case price (the "pay-through price"). Because the order is IOC, unfilled slippage is not consumed. The buffer is intentionally wide to maximize fill probability across volatile conditions. ### Client order ID encoding dYdX requires `u32` client IDs on-chain, but Nautilus uses string-based `ClientOrderId` values (e.g., `O-20260220-031943-001-000-51`). The adapter encodes these bidirectionally so that orders can be reconciled across restarts without persisted state. For the standard O-format (`O-YYYYMMDD-HHMMSS-TTT-SSS-CCC`), the encoding is deterministic: | dYdX field | Bits | Contents | | ----------------- | ---- | -------------------------------------------------- | | `client_id` | 32 | `[trader:10][strategy:10][count:12]` (unique key). | | `client_metadata` | 32 | Seconds since 2020-01-01 UTC (timestamp). | Because the encoding is deterministic, the adapter can decode any reconciled order back to its original `ClientOrderId` string without needing a database or mapping file. A `ClientOrderId` that is a plain number is also deterministic: the number becomes `client_id` and `client_metadata` is set to a fixed marker, so it decodes across restarts as well. Any other format falls back to sequential allocation with an in-memory reverse map, and those IDs can only be decoded within the same session. #### Restart collision prevention On restart, Nautilus resets the internal order counter based on the number of reconciled orders, which may be lower than the highest counter value used in the previous session (e.g., if some orders have expired from the API response). This can cause a new order to produce the same `client_id` as a previous session's order, resulting in a duplicate venue order UUID. The adapter prevents this by registering every `client_id` seen during reconciliation. If a new O-format encoding produces a `client_id` that was already used, the encoder logs a warning and falls back to sequential allocation. Sequential allocation also skips any registered values. :::note This protection is automatic and requires no user configuration. The warning log `[ENCODER] client_id ... collides with reconciled order` is informational. The order will still be submitted successfully with an alternative ID. ::: ## Broadcasting and retry strategy ### Short-term broadcast Short-term orders use Good-Til-Block (GTB) for replay protection. The chain's `ClobDecorator` ante handler skips Cosmos SDK sequence checking for short-term messages, so: - **No semaphore**: broadcasts are fully concurrent - **Cached sequence**: no increment or allocation needed - **No retry**: if the broadcast fails, it fails immediately - Benign cancel errors are treated as success (see below) ### Long-term broadcast Long-term and conditional orders require proper Cosmos SDK sequence management: - **Semaphore** with 1 permit serializes all long-term broadcasts - **Exponential backoff**: 500ms -> 1s -> 2s -> 4s (max 5 retries) - **10-second total budget** prevents indefinite retry loops - On sequence mismatch, the sequence is **resynced from chain** before retry - Transient gRPC failures (unavailable, deadline exceeded, resource exhausted) also resync before retry, so repeated timeouts cannot drift the local sequence ahead of the chain ### Sequence mismatch detection | Error code | Source | Meaning | | ---------- | ------------------ | ------------------------------------------------ | | `code=32` | Cosmos SDK | Account sequence mismatch | | `code=104` | dYdX authenticator | Signature verification failed (sequence-related) | Both trigger automatic resync + retry via the `RetryManager`. ### Benign cancel errors These errors during short-term cancel operations are treated as **success**: | Error code | Meaning | | ----------- | ----------------------------------------------------------------- | | `code=19` | Transaction already in mempool cache (duplicate tx) | | `code=9` | Cancel already exists in memclob with >= GoodTilBlock | | `code=3006` | Order to cancel does not exist (already filled/expired/cancelled) | ### Batch cancel partitioning When cancelling multiple orders, the adapter partitions them by lifetime: 1. **Short-term orders**: single `MsgBatchCancel` via `broadcast_short_term()` 2. **Long-term orders**: batched `MsgCancelOrder` messages via `broadcast_with_retry()` This ensures each group uses the appropriate broadcast strategy. ## Funding rates dYdX perpetual futures use a fixed 1-hour funding interval. The adapter sets `interval` to `60` (minutes) on all `FundingRateUpdate` objects for both WebSocket and historical funding data. ## Rate limiting ### gRPC rate limiting The adapter rate-limits gRPC `broadcast_tx` calls to prevent `ResourceExhausted` (429) errors from validator nodes. | Setting | Default | Description | | ---------------------------- | ------- | --------------------------------------------------------------------- | | `grpc_rate_limit_per_second` | `4` | Maximum gRPC broadcast requests per second. Set to `None` to disable. | This is a config-struct field, not a parameter of the Python `DydxExecutionClientConfig` constructor. ### Provider limits Known rate limits for public gRPC providers: | Provider | Limit | | --------- | -------------------- | | Polkachu | 300 req/min (~5/s) | | KingNodes | 250 req/min (~4.2/s) | | AutoStake | 4 req/s | The default of 4 req/s is conservative and works across all public providers. ### Multiple gRPC URL fallback The adapter connects to the first reachable node in a list of gRPC URLs, falling back to the next one when a connection fails. This matters on a DEX, where individual public nodes go down without notice. The execution config resolves that list in order: 1. `grpc_urls`, when non-empty. 2. `grpc_endpoint`, as a single-URL list. Setting only this field gives up the fallback. 3. The default public validator nodes for the selected network. Both fields are config-struct fields and are not parameters of the Python `DydxExecutionClientConfig` constructor, so Python configs always get the network defaults with their built-in fallback. ## Price and size quantization dYdX uses integer-based quantization for prices and sizes. The adapter handles all conversions automatically via `OrderMessageBuilder`, but understanding the parameters helps with debugging. ### Market parameters | Parameter | Description | | ----------------------------- | ------------------------------------------------------- | | `atomic_resolution` | Exponent for converting human-readable size to quantums | | `quantum_conversion_exponent` | Exponent for converting quantums to tokens | | `step_base_quantums` | Minimum order size step in quantums | | `subticks_per_tick` | Price granularity within each tick | ### Market order pricing Orders submitted without an explicit price use the oracle price with a 5% slippage buffer (the "pay-through price"). This covers `MARKET`, `STOP_MARKET`, and `MARKET_IF_TOUCHED`: - **Buy**: `oracle_price × 1.05` - **Sell**: `oracle_price × 0.95` Order pricing reads the oracle price from the instrument cache, which the Indexer populates when the client connects and does not refresh afterwards, so the pay-through band stays anchored to the oracle price observed at connect time. This is a separate path from the live oracle prices the execution client tracks off the markets channel, which it uses to value account state and positions rather than to price orders. ### Automatic handling All price and size quantization is handled automatically by `OrderMessageBuilder`. No manual conversion is needed when submitting orders through Nautilus. ## Data subscriptions The adapter supports the following data subscriptions: | Data type | Subscription | Historical request | Notes | | -------------------- | ------------ | ------------------ | ------------------------------------------------ | | Trade ticks | ✓ | ✓ | | | Quote ticks | ✓ | - | Synthesized from order book top-of-book. | | Order book deltas | ✓ | - | L2 depth only. | | Order book snapshots | - | ✓ | One-time snapshot via HTTP request. | | Bars | ✓ | ✓ | See supported resolutions below. | | Mark prices | ✓ | - | Via markets channel. | | Index prices | ✓ | - | Via markets channel. | | Funding rates | ✓ | ✓ | Real-time via markets channel, history via HTTP. | | Instrument status | ✓ | - | Via markets channel. | ### Supported bar resolutions | Resolution | dYdX candle | | ---------- | ----------- | | 1-MINUTE | `1MIN` | | 5-MINUTE | `5MINS` | | 15-MINUTE | `15MINS` | | 30-MINUTE | `30MINS` | | 1-HOUR | `1HOUR` | | 4-HOUR | `4HOURS` | | 1-DAY | `1DAY` | ## Subaccounts dYdX supports multiple subaccounts per wallet address, allowing segregation of trading strategies and risk management within a single wallet. ### Concepts - Each wallet address can have multiple numbered subaccounts (0, 1, 2, ..., 127). Numbers 128 and above are the venue's isolated-margin child subaccounts, which this adapter does not support. - Subaccount 0 is the **default** and is automatically created on first deposit. - Each subaccount maintains its own: - Positions - Open orders - Collateral balance - Margin requirements ### Configuration Specify the subaccount number in the execution client config: ```python from nautilus_trader.adapters.dydx import DydxExecutionClientConfig from nautilus_trader.model import AccountId exec_config = DydxExecutionClientConfig( account_id=AccountId.from_str("DYDX-001"), subaccount_number=0, ) ``` :::note Most users will use subaccount `0` (the default). Advanced users can configure multiple execution clients for different subaccounts to implement strategy segregation or risk isolation. ::: ## Testnet setup The dYdX testnet (`dydx-testnet-4`) is a full replica of mainnet for testing strategies without risking real funds. All default testnet endpoints are resolved automatically when `network=DydxNetwork.TESTNET`. ### 1. Create a testnet wallet **Option A: Via the dYdX testnet web app (easiest)** 1. Go to [v4.testnet.dydx.exchange](https://v4.testnet.dydx.exchange) 2. Connect with MetaMask, Keplr, Phantom, or WalletConnect 3. A dYdX account is generated automatically 4. Export your secret phrase: click your address (top-right) and select "Export secret phrase" **Option B: Use an existing secp256k1 private key** Any 32-byte hex-encoded secp256k1 private key will work. The adapter derives the `dydx1...` address from the key automatically using Cosmos bech32 encoding. ### 2. Fund the testnet account A subaccount must be funded before the adapter can connect (see [First-time account activation](#architecture)). **Via the testnet web app:** Click the deposit/recharge button on [v4.testnet.dydx.exchange](https://v4.testnet.dydx.exchange) to receive testnet USDC automatically. **Via the faucet API directly:** ```bash # Fund subaccount 0 with 2000 USDC curl -X POST https://faucet.v4testnet.dydx.exchange/faucet/tokens \ -H "Content-Type: application/json" \ -d '{"address": "dydx1...", "subaccountNumber": 0, "amount": 2000}' # Fund native tokens (for gas fees) curl -X POST https://faucet.v4testnet.dydx.exchange/faucet/native-token \ -H "Content-Type: application/json" \ -d '{"address": "dydx1..."}' ``` ### 3. Set environment variables ```bash export DYDX_TESTNET_WALLET_ADDRESS="dydx1..." export DYDX_TESTNET_PRIVATE_KEY="0x..." # hex-encoded, 0x prefix optional ``` ### 4. Configure the trading node Set `network=DydxNetwork.TESTNET` on both data and execution clients: ```python from nautilus_trader.adapters.dydx import DydxDataClientConfig from nautilus_trader.adapters.dydx import DydxExecutionClientConfig from nautilus_trader.adapters.dydx import DydxNetwork from nautilus_trader.model import AccountId data_config = DydxDataClientConfig(network=DydxNetwork.TESTNET) exec_config = DydxExecutionClientConfig( account_id=AccountId.from_str("DYDX-001"), network=DydxNetwork.TESTNET, wallet_address=None, # Falls back to DYDX_TESTNET_WALLET_ADDRESS private_key=None, # Falls back to DYDX_TESTNET_PRIVATE_KEY subaccount_number=0, ) ``` ### Testnet endpoints The Python constructors select the default testnet endpoints automatically and do not expose endpoint overrides. | Service | Default URL | | --------- | ---------------------------------------------------- | | HTTP | `https://indexer.v4testnet.dydx.exchange` | | WebSocket | `wss://indexer.v4testnet.dydx.exchange/v4/ws` | | gRPC | `https://test-dydx-grpc.kingnodes.com:443` (primary) | | Faucet | `https://faucet.v4testnet.dydx.exchange` | | Web app | `https://v4.testnet.dydx.exchange` | ### Mainnet endpoints The Python constructors select the default mainnet endpoints automatically and do not expose endpoint overrides. | Service | Default URL | | --------- | --------------------------------------------------- | | HTTP | `https://indexer.dydx.trade` | | WebSocket | `wss://indexer.dydx.trade/v4/ws` | | gRPC | `https://dydx-ops-grpc.kingnodes.com:443` (primary) | ## Configuration Configure the dYdX adapter through the trading node configuration. Execution clients support environment variable fallbacks for credentials. Data clients use public endpoints and do not require wallet credentials. ### Data client configuration options | Option | Default | Description | | ----------- | --------- | ----------------------------------------------- | | `network` | `MAINNET` | `DydxNetwork.MAINNET` or `DydxNetwork.TESTNET`. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket use. | ### Execution client configuration options | Option | Default | Description | | ------------------- | --------- | --------------------------------------------------------------------------------- | | `account_id` | Required | Nautilus account ID for the client. | | `network` | `MAINNET` | `DydxNetwork.MAINNET` or `DydxNetwork.TESTNET`. | | `private_key` | `None` | Hex-encoded signing key; falls back to the network-specific environment variable. | | `wallet_address` | `None` | dYdX wallet address; falls back to the network-specific environment variable. | | `subaccount_number` | `0` | Subaccount number from `0` through `127`. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket use. | ### Basic setup Register `DydxDataClientConfig` with `DydxDataClientFactory` and `DydxExecutionClientConfig` with `DydxExecutionClientFactory` on the node builder. The [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/dydx/) show the complete `LiveNode.builder(...)` wiring for both clients. ### API credentials Credentials can be passed directly via the Python config (`wallet_address`, `private_key`) or resolved automatically from environment variables based on the configured `network`. #### Environment variables | Variable | Network | Description | | ----------------------------- | ------- | ---------------------------------------------- | | `DYDX_WALLET_ADDRESS` | Mainnet | Bech32-encoded wallet address (`dydx1...`). | | `DYDX_PRIVATE_KEY` | Mainnet | Hex-encoded secp256k1 private key for signing. | | `DYDX_TESTNET_WALLET_ADDRESS` | Testnet | Testnet wallet address (`dydx1...`). | | `DYDX_TESTNET_PRIVATE_KEY` | Testnet | Testnet private key. | #### Resolution priority 1. Value passed in the Python config (if non-empty) 2. Environment variable selected by `network` ### Permissioned key trading #### What are API Trading Keys API Trading Keys let you delegate trading to a separate signing key without sharing your main wallet's seed phrase. The API key can place trades using all available margin in the owner's cross-margin account, but cannot withdraw funds or transfer assets. #### Creating an API key 1. In the dYdX web app, navigate to **More > API Trading Keys** 2. Click **Generate New API Key** 3. Save the **API Wallet Address** and **Private Key** (shown once, not stored by dYdX) 4. Click **Authorize API Key** (this registers the key on-chain as an authenticator) 5. The key is now active and can be used for trading See the [dYdX permissioned keys documentation](https://docs.dydx.xyz/interaction/permissioned-keys) for the authenticator model, and the [front-end walkthrough](https://help.dydx.trade/en/articles/267486-api-trading-keys-creating-a-new-key-on-the-front-end) for creating and managing keys in the web app. #### Adapter configuration Set the API key's private key as `DYDX_PRIVATE_KEY` and the owner's wallet address as `DYDX_WALLET_ADDRESS`. The adapter detects the mismatch during connect and automatically queries the chain for matching authenticator IDs. ```python from nautilus_trader.adapters.dydx import DydxExecutionClientConfig from nautilus_trader.model import AccountId config = DydxExecutionClientConfig( account_id=AccountId.from_str("DYDX-001"), wallet_address="dydx1owner...", # Owner account (holds margin) private_key="0xapikey...", # API Trading Key private key ) ``` The public Python config does not accept manual authenticator IDs. :::note API Trading Keys only work with **cross-margin** accounts and cross markets. Isolated margin is not supported. ::: ## Order books Order books can be maintained at full depth or top-of-book quotes depending on the subscription. The venue does not provide quotes directly. Instead, the adapter subscribes to order book deltas and synthesizes quotes for the `DataEngine` when there is a top-of-book price or size change. Only L2 (MBP) book type is supported. ## Contributing :::info For additional features or to contribute to the dYdX adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Hyperliquid Source: https://nautilustrader.io/docs/latest/integrations/hyperliquid/ [Hyperliquid](https://hyperliquid.gitbook.io/hyperliquid-docs) is a decentralized perpetual futures and spot exchange built on the Hyperliquid L1, a purpose-built blockchain optimized for trading. HyperCore provides a fully on-chain order book and matching engine. This integration supports live market data ingest and order execution on Hyperliquid. ## Overview This adapter is implemented in Rust with Python bindings. It provides direct integration with Hyperliquid's REST and WebSocket APIs without requiring external client libraries. The Hyperliquid adapter includes multiple components: - `HyperliquidHttpClient`: HTTP API connectivity, instrument loading and parsing, and reconciliation reports. - `HyperliquidWebSocketClient`: WebSocket API connectivity for Rust callers. - `HyperliquidDataClient`: Market data feed manager. - `HyperliquidExecutionClient`: Account management and trade execution gateway. - `HyperliquidDataClientFactory`: Factory for Hyperliquid data clients (used by the live node builder). - `HyperliquidExecutionClientFactory`: Factory for Hyperliquid execution clients (used by the live node builder). :::note Most users configure a live trading node (see [Live node configuration](#live-node-configuration)) and never work directly with these lower-level components. ::: ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/hyperliquid/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/hyperliquid/examples/) ## Builder code attribution Submitted mainnet orders carry the NautilusTrader builder code at a **zero fee rate**, so attribution adds no trading cost. This helps us gauge real usage of the integration and prioritize ongoing maintenance. Users who attribute order flow may also qualify for direct support through the [Institutional](https://nautilustrader.io/institutional/) tier when trading at scale. You may opt out of attribution with `include_builder_attribution: false` in serialized config, or `include_builder_attribution=False` in Python. The builder address is omitted from orders in three cases: - **Testnet**: Hyperliquid testnet rejects orders that include a builder address the wallet has not explicitly approved (faucet-funded testnet wallets typically have no approval), so testnet orders never include the builder. - **Vault trading** (`vault_address` configured): Hyperliquid does not allow vaults to approve builder fees, so including the builder address would cause the exchange to reject the order. - **Attribution disabled** (`include_builder_attribution=False`): Users who choose not to attribute their order flow can disable builder attribution explicitly. ```python from nautilus_trader.adapters.hyperliquid import HyperliquidExecutionClientConfig config = HyperliquidExecutionClientConfig( include_builder_attribution=False, ) ``` ### Builder fee approval Hyperliquid requires a one-time `ApproveBuilderFee` approval before orders can carry the builder address: - Orders from a wallet that has never approved a builder fee are rejected with the reason `Builder fee has not been approved` (any prior approval, including at a 0% rate, satisfies the check). - The approval must be signed by the master wallet's private key, which the adapter does not hold in agent (API) wallet setups, so it runs as a one-time script rather than at execution client startup. - The 0% max fee rate permits attribution only: no builder fee is ever charged, and raising the rate would require a new approval signed by you. Run the approval script once per wallet (reads `HYPERLIQUID_PK`, or `HYPERLIQUID_TESTNET_PK` with `HYPERLIQUID_TESTNET=true`): ```bash cargo run -p nautilus-hyperliquid --bin hyperliquid-builder-fee-approve ``` Or from Python: ```python from nautilus_trader.adapters.hyperliquid import builder_fee_approve builder_fee_approve() ``` ### Revoking the approval Use revocation to cap a previously approved builder fee at 0% (for example, an approval from a version that charged builder fees). Revocation caps the fee; it does not remove the approval record, so attribution continues unless `include_builder_attribution` is disabled. ```bash cargo run -p nautilus-hyperliquid --bin hyperliquid-builder-fee-revoke ``` Or from Python: ```python from nautilus_trader.adapters.hyperliquid import builder_fee_revoke builder_fee_revoke() ``` The Rust scripts print a summary of the action and pause for an Enter keypress before signing; abort with `Ctrl+C` if anything in the summary looks wrong, or append `-- --yes` to the `cargo run` command to skip the prompt. The Python bindings do not prompt, so make sure to review the active environment variables before calling. ## Testnet setup Hyperliquid provides a testnet environment for testing strategies with mock funds. :::info **Mainnet account required.** Hyperliquid's testnet faucet only works for wallets that have previously deposited on mainnet. You must fund a mainnet account first before you can obtain testnet USDC. ::: ### Getting testnet funds To receive testnet USDC, you must first have deposited on **mainnet** using the same wallet address: 1. Visit the [Hyperliquid mainnet portal](https://app.hyperliquid.xyz/) and make a deposit with your wallet. 2. Visit the [testnet faucet](https://app.hyperliquid-testnet.xyz/drip) using the same wallet. 3. Claim 1,000 mock USDC from the faucet. :::note **Email wallet users**: Email login generates different addresses for mainnet vs testnet. To use the faucet, export your email wallet from mainnet, import it into MetaMask or Rabby, then connect the extension to testnet. ::: ### Creating a testnet account 1. Visit the [Hyperliquid testnet portal](https://app.hyperliquid-testnet.xyz/). 2. Connect your wallet (MetaMask, WalletConnect, or email). 3. The testnet automatically creates an account for your wallet address. ### Exporting your private key To use your testnet account with NautilusTrader, you need to export your wallet's private key: **MetaMask:** 1. Click the three dots menu next to your account. 2. Select "Account details". 3. Click "Show private key". 4. Enter your password and copy the private key. :::warning **Never share your private keys.** Store private keys securely using environment variables; never commit them to version control. ::: ### Setting environment variables Set your testnet credentials as environment variables: ```bash export HYPERLIQUID_TESTNET_PK="your_private_key_here" # Optional: for vault trading export HYPERLIQUID_TESTNET_VAULT="vault_address_here" ``` The adapter automatically loads these when `environment=HyperliquidEnvironment.TESTNET` in the configuration. :::warning **Agent / API wallets**: if `HYPERLIQUID_TESTNET_PK` is an [agent wallet](#agent-wallets) approved under a master account (the typical setup when you create an API wallet on the Hyperliquid UI), you must also set `HYPERLIQUID_ACCOUNT_ADDRESS` to the master account address. Without it, `OrderStatusReport` requests and WebSocket user feeds come back empty even though orders are live on the venue. See [GH-4010](https://github.com/nautechsystems/nautilus_trader/issues/4010). ::: ## Product support Hyperliquid offers linear perpetual futures, HIP-3 builder-deployed perpetuals, native spot markets, and HIP-4 binary outcome markets. | Product Type | Data Feed | Trading | Notes | | ----------------- | --------- | ------- | ------------------------------------------------ | | Spot | ✓ | ✓ | Native spot markets. | | Perpetual Futures | ✓ | ✓ | USDC-settled linear perps (validator-operated). | | HIP-3 Perpetuals | ✓ | ✓ | Builder-deployed perps with per-dex collateral. | | HIP-4 Outcomes | ✓ | ✓ | Fully-collateralized binary outcome side tokens. | All four product types load automatically at connect; no per-product opt-in is required. :::note Standard Hyperliquid perpetuals are settled in USDC. HIP-3 dexes may settle in their own collateral token, such as USDH, USDE, or USDT0, while keeping Nautilus symbols quoted as `USD`. Spot markets are standard currency pairs. See [HIP-3 builder-deployed perpetuals](#hip-3-builder-deployed-perpetuals) and [HIP-4 outcome markets](#hip-4-outcome-markets) for the details of each. ::: ## Symbology Hyperliquid uses a specific symbol format for instruments: ### Spot markets Format: `{Base}-{Quote}-SPOT` Examples: - `PURR-USDC-SPOT` - PURR/USDC spot pair - `HYPE-USDC-SPOT` - HYPE/USDC spot pair To subscribe in your strategy: ```python InstrumentId.from_str("PURR-USDC-SPOT.HYPERLIQUID") ``` Spot instruments loaded from `spotMeta` preserve venue metadata in `CurrencyPair.info`: | Field | Value | | ------------- | ----------------------------------------------- | | `name` | Raw venue pair label | | `tokens` | Base and quote indexes into `spotMeta.tokens` | | `index` | Pair index before the `10000` spot asset offset | | `isCanonical` | Venue canonical classification | Read `info.get("isCanonical")` in Python or `Params.get_bool("isCanonical")` in Rust to inspect the venue's canonical classification. :::note Spot instruments may include vault tokens (prefixed with `vntls:`). Hyperliquid does not list these in `spotMeta`, so the HTTP client synthesizes a `{coin}-USDC-SPOT` instrument on first sight to keep balances and fills resolvable. These synthetic instruments carry no venue metadata: `info` is `None` in Rust and an empty dict in Python. ::: ### Perpetual futures Format: `{Base}-USD-PERP` Examples: - `BTC-USD-PERP` - Bitcoin perpetual futures - `ETH-USD-PERP` - Ethereum perpetual futures - `SOL-USD-PERP` - Solana perpetual futures To subscribe in your strategy: ```python InstrumentId.from_str("BTC-USD-PERP.HYPERLIQUID") InstrumentId.from_str("ETH-USD-PERP.HYPERLIQUID") ``` ### HIP-3 perpetuals Format: `{dex}:{Asset}-USD-PERP` [HIP-3](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/hip-3-builder-deployed-perpetuals) markets use a dex prefix separated by a colon. The dex name identifies which builder-deployed perp dex the market belongs to. Examples: - `xyz:TSLA-USD-PERP` - Tesla perp on trade.xyz - `xyz:GOLD-USD-PERP` - Gold perp on trade.xyz - `flx:NVDA-USD-PERP` - Nvidia perp on Felix - `vntl:SPACEX-USD-PERP` - SpaceX perp on Ventuals To subscribe in your strategy: ```python InstrumentId.from_str("xyz:TSLA-USD-PERP.HYPERLIQUID") ``` ### HIP-4 outcome side tokens Format: `{outcome_index}-{YES|NO}-OUTCOME.HYPERLIQUID`, where `outcome_index` is the `outcome` field from `outcomeMeta` and the middle segment names the binary side. The `-OUTCOME` suffix is symmetric with `-PERP` / `-SPOT`. [HIP-4](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/hip-4-outcome-markets) side tokens are binary contracts that settle in the market's quote token at `0` (loser) or `1` (winner). The Nautilus symbol uses the human-readable form above; the wire `raw_symbol` uses the venue coin form `#{encoding}` (where `encoding = 10 * outcome_index + side`, `side` is `0` for Yes / `1` for No), which is what `l2Book` and `allMids` accept. Examples (outcome 25): - `25-YES-OUTCOME.HYPERLIQUID`: Yes side. Encoding `250`, wire coin `#250`, token name `+250`, action asset id `100_000_250`. - `25-NO-OUTCOME.HYPERLIQUID`: No side. Encoding `251`, wire coin `#251`, token name `+251`, action asset id `100_000_251`. To subscribe in your strategy: ```python InstrumentId.from_str("25-YES-OUTCOME.HYPERLIQUID") ``` :::note The outcome universe cycles. Each settlement removes the resolved outcome from `outcomeMeta`, and the venue's next listing advances the index. Inspect the live universe with: ```bash curl -s -X POST https://api.hyperliquid.xyz/info \ -H 'Content-Type: application/json' \ -d '{"type":"outcomeMeta"}' ``` ::: See [HIP-4 outcome markets](#hip-4-outcome-markets) for the trading flow, settlement, and current limitations. ## HIP-3 builder-deployed perpetuals [HIP-3](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/hip-3-builder-deployed-perpetuals) allows qualified deployers to launch permissionless perp dexes on Hyperliquid. These markets include equities (TSLA, NVDA, AAPL), commodities (gold, crude oil), indices (S&P 500), and pre-IPO tokens (SpaceX, OpenAI). In a `LiveNode`, HIP-3 perpetuals load automatically alongside standard perpetuals at connect: the adapter fetches every perp dex (standard and builder-deployed) from `allPerpMetas`, so no additional client configuration is required. The data client exposes no per-dex filter; strategies select the markets they trade by `instrument_id`. For direct `HyperliquidHttpClient` usage, the HIP-3 perp dexes are excluded unless you opt in through `load_instrument_definitions`: ```python from nautilus_trader.adapters.hyperliquid import HyperliquidEnvironment from nautilus_trader.adapters.hyperliquid import HyperliquidHttpClient client = HyperliquidHttpClient.from_env(HyperliquidEnvironment.MAINNET) instruments = await client.load_instrument_definitions( include_spot=True, include_perps=True, include_perps_hip3=True, include_outcomes=False, ) ``` ### Open-order and position reconciliation #### Startup mass status At `LiveNode` startup, unfiltered open-order and position reconciliation queries the default perp dex and each unique HIP-3 dex named by the wallet's recent historical orders or fills: - Cached dexes without wallet activity do not generate startup requests. - If either history response reaches its 2,000-record limit, reconciliation instead queries every dex returned by the venue's current perp dex list so bounded history cannot hide older open orders or positions. - Position reconciliation also includes spot holdings. The returned mass status records its own coverage under the [mass-status history contract](../concepts/execution/reconciliation.md#mass-status-history-contract): when a lookback is configured, `lookback_start` carries its lower bound (with no configured lookback the snapshot is unbounded), and `reports_complete` is `false` when a history response reached its record limit (and may be truncated) or when a venue row needed for the snapshot could not be decoded, resolved to an instrument, or converted into a report. Valid rows remain in the report set. A snapshot whose venue responses decoded cleanly within the record limits is authoritative, including an empty one. #### Command and direct requests Outside startup mass status, unfiltered `LiveNode` open-order and position report commands and direct `HyperliquidHttpClient` requests query the default perp dex and each builder dex represented by the cached perpetual instruments: - A request filtered to a HIP-3 instrument derives the builder dex from the symbol's dex prefix and queries only that dex. - A standard perpetual filter queries only the default dex. - Spot and outcome position filters keep their existing spot-only routing. - If any required request fails, or a venue row cannot be decoded, resolved to an instrument, or converted into a report, the request returns an error rather than a partial snapshot; fill and historical-order report requests fail the same way. - A targeted order-status lookup on the HTTP client that matches a venue row it cannot use returns an error instead of reporting the order as missing, and the `GenerateOrderStatusReport` command does the same once no venue order ID fallback remains. ### Differences from standard perpetuals HIP-3 markets trade on the same HyperCore matching engine and use the same order API. The key differences are: - **Higher fees**: each dex sets its own deployer fee scale, which multiplies the base perp fee by `scale + 1` below `1` and by `scale * 2` at or above it, with the deployer taking up to half. Check a dex's live rate rather than assuming the standard perp schedule. - **Isolated margin**: HIP-3 markets default to isolated-only margin. - **Per-dex collateral**: Each HIP-3 dex declares its settlement token through its `collateralToken` entry in `allPerpMetas`. Nautilus resolves that token through `spotMeta` and keeps the symbol's quote leg as `USD`. If a non-USDC collateral token cannot resolve from `spotMeta`, instrument loading returns an error rather than falling back to USDC. - **Deployer-managed oracles**: The deployer operates the oracle feed, not validators. - **Growth mode**: Dexes whose markets are disjoint from validator-operated perps can opt into growth mode, which Hyperliquid documents as at least a 90% cut to all-in fees. For full protocol details, see the Hyperliquid docs: - [HIP-3 proposal](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperliquid-improvement-proposals-hips/hip-3-builder-deployed-perpetuals) - [HIP-3 deployer actions](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/hip-3-deployer-actions) - [Asset IDs](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids) - [Fees](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees) ### Wildcard character sanitization Some HIP-3 dexes deploy assets whose venue names contain `*` or `?` bytes (for example `dex:STREAMABCD****-USD-PERP`). Those bytes collide with the Nautilus message bus pattern syntax (`*` = zero-or-more, `?` = one-char) and would corrupt subscription routing if embedded in topic strings unchanged. The Hyperliquid adapter substitutes both bytes with `x` when constructing the `InstrumentId.symbol`, so a HIP-3 asset named `dex:STREAMABCD****` is exposed to strategies as: ```python InstrumentId.from_str("dex:STREAMABCDxxxx-USD-PERP.HYPERLIQUID") ``` The substitution applies only to the Nautilus-internal symbol used in topics, caches, logs, and config. The venue-official name is preserved on the instrument's `raw_symbol` field for HTTP and WebSocket wire calls, and order submissions reference the numeric asset index, so the round-trip with Hyperliquid is unaffected. When subscribing to a HIP-3 instrument with wildcard bytes in its venue name, use the sanitized form. Symbols without `*` or `?` are passed through unchanged. The substitution is lossy: two distinct venue names such as `dex:FOO*` and `dex:FOO?` would normalize onto the same Nautilus symbol. Such collisions use the first-write-wins behavior described in [Instrument loading](#instrument-loading). ## HIP-4 outcome markets [HIP-4](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids#outcomes) markets are fully-collateralized binary contracts. Each market has two side tokens (Yes / No) that settle to `1` (winner) or `0` (loser) quote tokens on the resolution date. The venue publishes outcome metadata through the `outcomeMeta` info endpoint. The adapter treats that payload as best-effort and skips HIP-4 instruments when the venue does not return it, so a venue that drops or renames the endpoint degrades to perps and spot rather than failing instrument loading. ### Loading outcome instruments In a `LiveNode`, outcome instruments load automatically (best-effort) when the venue exposes `outcomeMeta`. No client configuration is required. For direct `HyperliquidHttpClient` usage, opt in through `load_instrument_definitions`: ```python from nautilus_trader.adapters.hyperliquid import HyperliquidEnvironment from nautilus_trader.adapters.hyperliquid import HyperliquidHttpClient client = HyperliquidHttpClient.from_env(HyperliquidEnvironment.MAINNET) instruments = await client.load_instrument_definitions( include_spot=True, include_perps=True, include_perps_hip3=False, include_outcomes=True, ) ``` Loading emits two `BinaryOption` instruments per outcome (one per side). Symbols use the form `{outcome_index}-{YES|NO}-OUTCOME.HYPERLIQUID`. `expiration_ns` is parsed from the venue description (`expiry:YYYYMMDD-HHMM`, UTC). Standalone binaries carry their own expiry; named and fallback outcomes inherit from their parent question. Defaults: `0.0001` per tick, `0.01` per lot. Each instrument's `BinaryOption.info` carries the parsed venue metadata as a key/value map (consumed via `info["key"]` in Python or `Params.get_str(...)` in Rust). Derived identifiers are always populated; description-derived fields appear when the venue includes them. | Field | Source | Notes | | ------------------ | ------------------------------ | ------------------------------------------------- | | `outcome_index` | derived | `outcome` from `outcomeMeta` | | `outcome_side` | derived | `0` = Yes, `1` = No | | `side_name` | `outcomeMeta` `sideSpecs` | venue side label, `"Yes"` / `"No"` when absent | | `encoding` | derived | `10 * outcome_index + side` | | `asset_id` | derived | `100_000_000 + encoding` | | `market_name` | `outcomeMeta.outcomes[*].name` | venue market label | | `class` | description | `priceBinary` or `priceBucket` | | `underlying` | description | underlying asset code | | `expiry` | description | `YYYYMMDD-HHMM` UTC | | `target_price` | description | binary settlement threshold | | `period` | description | recurrence period (e.g. `1d`, `3m`) | | `price_thresholds` | description | comma-separated thresholds (bucket markets) | | `named_index` | named-outcome description | position in parent `named_outcomes` array | | `is_fallback` | fallback-outcome description | `true` for the `other` outcome of a question | | `question` | parent question | question id | | `question_name` | parent question | question label | | `question_*` | parent question description | every parsed question field, `question_` prefixed | Description keys are lowered from venue camelCase to snake_case (`targetPrice` -> `target_price`, `priceThresholds` -> `price_thresholds`). Values are kept as strings to preserve wire fidelity; numeric identifiers (`outcome_index`, `outcome_side`, `encoding`, `asset_id`, `question`, `named_index`) are stored as JSON numbers. ### Settlement currency The adapter denominates every outcome instrument in USDH (token index 360, traded on the `USDH/USDC` spot pair `@230`). USDH is registered at 8-decimal precision on first outcome instrument creation, so `BinaryOption.currency`, `quote_currency`, and the commission currency on zero-fee outcome fills all resolve to USDH. The registration is explicit so the precision is deterministic rather than dependent on whichever code path first triggers currency auto-registration. USDH spot balances merge with the perp clearinghouse view, so `AccountState` carries USDH alongside USDC and any other non-zero spot holdings. :::warning Hyperliquid reports the quote token per outcome in the `outcomeMeta` `quoteToken` field, and mainnet outcomes currently quote in USDC rather than USDH. The adapter does not yet read that field, so outcome instruments loaded from mainnet carry a USDH quote currency that does not match the venue. Treat HIP-4 support as testnet-ready until the per-outcome quote token is honored. ::: ### Trading flow Outcome side tokens (`{outcome_index}-{YES|NO}-OUTCOME.HYPERLIQUID`) trade through the standard order path. Submit `SubmitOrder` as you would for any perp or spot instrument; the execution client routes it through the same `Order` action against the venue's `#{encoding}` orderbook (where `encoding = 10 * outcome_index + outcome_side`). No HIP-4-specific call is needed. Settlement is venue-driven; see [Settlement dispatch](#settlement-dispatch). #### Advanced workflows The full `userOutcome` action set is reachable directly on `HyperliquidHttpClient` (Rust and PyO3) for strategies that need to manage side-token inventory off-book: ```python from decimal import Decimal from nautilus_trader.adapters.hyperliquid import HyperliquidEnvironment from nautilus_trader.adapters.hyperliquid import HyperliquidHttpClient client = HyperliquidHttpClient.from_env(HyperliquidEnvironment.MAINNET) # Mint matched Yes + No side tokens from USDH (e.g. dual-side market making) await client.submit_split_outcome(50, Decimal("1.0")) # Burn a matched Yes + No pair back to USDH (amount=None merges the max) await client.submit_merge_outcome(50, None) # Multi-outcome priceBucket operations await client.submit_merge_question(9, None) await client.submit_negate_outcome(9, 52, Decimal("1.0")) ``` | Action | Use case | | ----------------------- | ------------------------------------------------------------------------------------ | | `submit_split_outcome` | Mint paired Yes + No tokens from quote (initial market making, dual-side hedges) | | `submit_merge_outcome` | Burn a matched Yes + No pair back to quote without crossing the spread | | `submit_merge_question` | Close a full multi-outcome basket back to quote atomically | | `submit_negate_outcome` | Convert No shares of one outcome into Yes shares of every other in the same question | For directional bets the ordinary `SubmitOrder` path is sufficient; the methods above are only needed when you want to create or destroy side-token inventory off-book. ### Order constraints Outcome side tokens behave like spot tokens (no margin, no funding, no liquidation). The execution client rejects features that don't apply: - `reduce_only` orders. - Trigger order types (`StopMarket`, `StopLimit`, `MarketIfTouched`, `LimitIfTouched`, trailing stops). `Limit` and `Market` orders with `GTC`, `IOC`, or `ALO` time-in-force are supported. The venue minimum is 10 quote tokens of notional; size `order_qty` so that `order_qty * limit_price >= 10`. ### Settlement dispatch At expiry the venue closes held side-token balances and emits a `Settlement` fill per side. The adapter consumes these through the standard user-fills stream (HTTP poll and WebSocket); no synthetic dispatch runs. Each settlement fill: - `order_side = SELL`, zero commission. - Price `1` quote token for the winning side, `0` for the loser. - Surfaces as a `FillReport`. - Also emits `OrderFilled` when WebSocket dispatch links the position to a tracked order. Covers standalone `priceBinary` outcomes and multi-outcome `priceBucket` questions uniformly. ### Position reconciliation HIP-4 side tokens arrive on `spotClearinghouseState` with `coin` set to the `+E` token form and no `token` field. The adapter: - Treats `SpotBalance.token` as optional during deserialization. - Resolves `+E` / `#E` coins to their `BinaryOption` instrument when generating `PositionStatusReport`s. - Skips the perp clearinghouse fetch when the position-status filter is an outcome instrument (outcomes never appear in `assetPositions`). ### Multi-outcome (priceBucket) markets The venue exposes multi-outcome markets via the top-level `questions` array in `outcomeMeta`. Each question references a fallback outcome plus a sequence of named outcomes whose individual descriptions point back at the question via `index:N`. Each side token is modeled as an independent `BinaryOption` instrument; the `submit_merge_question` and `submit_negate_outcome` actions on `HyperliquidHttpClient` operate at the question level for basket close and cross-outcome rotation. ## Instrument loading The data client loads the full Hyperliquid universe at connect. One pass covers spot markets, standard perpetuals, every HIP-3 builder-deployed perp dex, and HIP-4 outcome side tokens; the client config exposes no per-product or per-symbol filter. Strategies select the instruments they trade through their own `instrument_id` configuration. The loader includes every pair in `spotMeta.universe`, including non-canonical pairs. When several pairs share a base token, it caches the canonical pair first so balances and fills that identify the asset by its base token resolve to the canonical Nautilus instrument. Any later definition whose Nautilus symbol collides with an earlier definition is dropped with a warning and cannot be traded. The data client then refetches the universe every `update_instruments_interval_mins` minutes: - It publishes the definitions that are new or materially changed; unchanged definitions are not republished. - The execution client receives those updates and registers each instrument's asset index, so a market listed after startup becomes tradable without a process restart. - A `RequestInstrument` or `RequestInstruments` also refetches the whole universe and publishes new or changed definitions the same way. - Set `update_instruments_interval_mins` to `0` to disable the periodic refresh; requests and a data client reconnect still refetch the universe on demand. Submitting for a symbol the execution client has never loaded is denied with `INSTRUMENT_NOT_FOUND`. Failures degrade per product rather than aborting the load: - Missing spot or perp metadata is logged as a warning and that product is skipped. - An absent `outcomeMeta` payload is skipped at debug level. - A perp dex whose non-USDC collateral token cannot be resolved through `spotMeta` is the one hard failure, because guessing the settlement currency would misprice the market. To fetch a narrower set outside a `LiveNode`, call `load_instrument_definitions` on `HyperliquidHttpClient` directly with the product flags you want: ```python from nautilus_trader.adapters.hyperliquid import HyperliquidEnvironment from nautilus_trader.adapters.hyperliquid import HyperliquidHttpClient client = HyperliquidHttpClient.from_env(HyperliquidEnvironment.MAINNET) instruments = await client.load_instrument_definitions( include_spot=False, include_perps=True, include_perps_hip3=False, include_outcomes=False, ) ``` ## Data subscriptions The adapter supports the following data subscriptions. All perpetual data types (mark prices, index prices, funding rates) apply to both standard and HIP-3 perps. | Data type | Sub. | Snapshot | Hist. | Nautilus type | Notes | | ----------------- | ---- | -------- | ----- | ----------------------------- | ------------------------------------------------ | | Trade ticks | ✓ | - | ✓ | `TradeTick` | WebSocket trades; `recentTrades`. | | Public trades | ✓ | - | ✓ | `HyperliquidPublicTrade` | Opt-in custom data with counterparties and hash. | | Quote ticks | ✓ | - | - | `QuoteTick` | Best bid/offer. | | Order book deltas | ✓ | ✓ | - | `OrderBookDelta` | L2 snapshots. | | Order book depth | ✓ | - | - | `OrderBookDepth10` | Top-10 L2 snapshots. | | Bars | ✓ | - | ✓ | `Bar` | Supported intervals below. | | Mark prices | ✓ | - | - | `MarkPriceUpdate` | Perpetual mark price ticks. | | Index prices | ✓ | - | - | `IndexPriceUpdate` | Underlying reference prices. | | Funding rates | ✓ | - | ✓ | `FundingRateUpdate` | `fundingHistory` endpoint. | | Open interest | ✓ | - | - | `HyperliquidOpenInterest` | Custom data from `activeAssetCtx`. | | All mids | ✓ | - | - | `HyperliquidAllMids` | Custom data from `allMids`. | | All dex contexts | ✓ | - | - | `HyperliquidAllDexsAssetCtxs` | Custom data from `allDexsAssetCtxs`. | | TWAP history | ✓ | - | - | `HyperliquidTwapHistory` | Opt-in custom data from `userTwapHistory`. | | TWAP slice fills | ✓ | - | - | `HyperliquidTwapSliceFill` | Opt-in custom data from `userTwapSliceFills`. | :::note Historical quote requests are not supported. Historical trade requests use the `recentTrades` info endpoint, which returns a recent snapshot of public trades (newest first) with no time range. `request_trades` filters that snapshot to the requested `[start, end]` window and applies `limit` by keeping the most recent trades. When the request reaches below the snapshot's oldest trade, the adapter logs a warning and serves the available subset (or an empty response). The endpoint depends on the Hyperliquid indexer: self-hosted `/info` nodes return HTTP 422, which the adapter treats as no coverage and answers with an empty response. Real-time trades remain available via the WebSocket `trades` channel. ::: ### Order book precision controls The `l2Book` subscription accepts optional `nSigFigs` and `mantissa` parameters that thin the venue-side book aggregation. Pass them as `n_sig_figs` and `mantissa` in the `params` dict on `subscribe_book_deltas` or `subscribe_book_depth10`, and the adapter forwards them to the venue. Hyperliquid accepts `nSigFigs` values `2`, `3`, `4`, `5`, or omitted for full precision. `mantissa` is only valid when `nSigFigs=5` and accepts `1`, `2`, or `5`. ```python from nautilus_trader.model import BookType self.subscribe_book_deltas( instrument_id=instrument_id, book_type=BookType.L2_MBP, params={"n_sig_figs": 5, "mantissa": 2}, ) ``` Omitting both params subscribes to the full-depth book. Book deltas and depth10 snapshots for the same instrument share one venue `l2Book` stream: - The first subscription opens the stream and sets its precision options. - Requesting different options while the stream is active logs a warning and keeps the active options. - The stream closes when the last of the two uses unsubscribes. - Reconnects restore the stream with its original precision options. ### Hyperliquid specific data The adapter emits Hyperliquid-specific custom data types: - `HyperliquidAllMids` from the WebSocket `allMids` feed. Each update carries all currently reported mid prices in one payload. - `HyperliquidAllDexsAssetCtxs` from the WebSocket `allDexsAssetCtxs` feed. Each update carries normalized per-instrument asset-context entries across the default perp dex and HIP-3 builder dexes. - `HyperliquidOpenInterest` from the shared `activeAssetCtx` feed used by mark prices, index prices, and funding rates. - `HyperliquidPublicTrade` from `trades` and `recentTrades`. Each event is self-contained and includes the buyer, seller, and venue hash. - `HyperliquidTwapHistory` from the WebSocket `userTwapHistory` feed. Each event is one history/lifecycle row for a user address. - `HyperliquidTwapSliceFill` from the WebSocket `userTwapSliceFills` feed. Each event is one TWAP child-slice fill. | Field | Type | Description | | ---------- | --------------------------- | -------------------------------------------------------------------------- | | `mids` | `dict[InstrumentId, Price]` | Canonical Nautilus instrument ID to mid price mapping. | | `ts_event` | `int` | UNIX timestamp in nanoseconds when the update occurred. Mirrors `ts_init`. | | `ts_init` | `int` | UNIX timestamp in nanoseconds when the object was built. | Subscribe from an actor or strategy with `DataType(HyperliquidAllMids.__name__)`, which covers the default perp dex. To follow a HIP-3 builder dex instead, pass its venue identifier in `metadata["dex"]`: ```python from nautilus_trader.adapters.hyperliquid import HYPERLIQUID_CLIENT_ID from nautilus_trader.adapters.hyperliquid import HyperliquidAllMids from nautilus_trader.model import DataType self.subscribe_data( data_type=DataType(HyperliquidAllMids.__name__, metadata={"dex": "xyz"}), client_id=HYPERLIQUID_CLIENT_ID, ) ``` The `dex` value is a venue-defined builder dex identifier from `perpDexs`, such as `xyz`, `flx`, or `vntl`. Omit the key (or pass an empty string) for the default perp dex. `HyperliquidOpenInterest` carries the latest open interest for one perpetual instrument. Subscribe with the canonical Nautilus `instrument_id` in `metadata["instrument_id"]`: | Field | Type | Description | | --------------- | -------------- | -------------------------------------------------------------------------- | | `instrument_id` | `InstrumentId` | Canonical Nautilus instrument ID. | | `open_interest` | `Decimal` | Open interest parsed for direct arithmetic use. | | `ts_event` | `int` | UNIX timestamp in nanoseconds when the update occurred. Mirrors `ts_init`. | | `ts_init` | `int` | UNIX timestamp in nanoseconds when the object was built. | ```python from nautilus_trader.adapters.hyperliquid import HYPERLIQUID_CLIENT_ID from nautilus_trader.adapters.hyperliquid import HyperliquidOpenInterest from nautilus_trader.model import DataType self.subscribe_data( data_type=DataType( HyperliquidOpenInterest.__name__, metadata={"instrument_id": str(self.instrument_id)}, ), client_id=HYPERLIQUID_CLIENT_ID, ) ``` `HyperliquidOpenInterest` reuses the same single underlying `activeAssetCtx` venue subscription that already backs mark prices, index prices, and funding rates for the same coin. Adding OI does not open a second parallel `activeAssetCtx` subscription. `HyperliquidPublicTrade` is an opt-in alternative to generic `TradeTick` for public order-flow research. It has `instrument_id`, `price`, `size`, `aggressor_side`, `trade_id`, `buyer`, `seller`, `hash`, `ts_event`, and `ts_init`. Subscribe with the same canonical instrument metadata: ```python from nautilus_trader.adapters.hyperliquid import HYPERLIQUID_CLIENT_ID from nautilus_trader.adapters.hyperliquid import HyperliquidPublicTrade from nautilus_trader.model import DataType self.subscribe_data( data_type=DataType( HyperliquidPublicTrade.__name__, metadata={"instrument_id": str(self.instrument_id)}, ), client_id=HYPERLIQUID_CLIENT_ID, ) ``` It shares the one venue `trades` subscription with `TradeTick` when both are requested. Unlike a sidecar `users` event, each `HyperliquidPublicTrade` is independently Arrow-serializable and can be recorded to and queried from a Nautilus catalog without a join. `RequestCustomData` for this type uses the same recent-only `recentTrades` snapshot as historical trade requests. `HyperliquidTwapHistory` and `HyperliquidTwapSliceFill` are opt-in user-keyed custom data. They are **not** included in the execution account `subscribe_all_user_channels` path. Subscribe with the target wallet address in `metadata["user"]` (the address need not be the adapter trading account). `HyperliquidTwapHistory` fields: | Field | Type | Description | | -------------------- | ----------------------- | ------------------------------------------------------------- | | `user` | `str` | User address from the subscription envelope. | | `twap_id` | `int \| None` | Venue `twapId` when present on the history row. | | `coin` | `str` | Raw Hyperliquid coin symbol. | | `instrument_id` | `InstrumentId \| None` | Resolved Nautilus instrument ID when the coin is known. | | `side` | `OrderSide` | TWAP order side. | | `size` | `Decimal` | Total TWAP size. | | `executed_size` | `Decimal` | Executed size so far. | | `executed_notional` | `Decimal` | Executed notional so far. | | `minutes` | `int` | TWAP duration in minutes. | | `reduce_only` | `bool` | Whether the TWAP is reduce-only. | | `randomize` | `bool` | Whether slice timing is randomized. | | `status` | `HyperliquidTwapStatus` | Venue status (`activated`/`terminated`/`finished`/`error`/…). | | `status_description` | `str` | Venue status description (often set when status is `error`). | | `state_timestamp` | `int` | `state.timestamp` as UNIX nanoseconds. | | `is_snapshot` | `bool` | Whether this row belongs to a venue snapshot batch. | | `ts_event` | `int` | History row time (`history.time`) as UNIX nanoseconds. | | `ts_init` | `int` | UNIX timestamp in nanoseconds when the object was built. | `HyperliquidTwapSliceFill` fields: | Field | Type | Description | | --------------- | ---------------------- | -------------------------------------------------------- | | `user` | `str` | User address from the subscription envelope. | | `twap_id` | `int` | Venue TWAP order identifier. | | `coin` | `str` | Raw Hyperliquid coin symbol. | | `instrument_id` | `InstrumentId \| None` | Resolved Nautilus instrument ID when the coin is known. | | `price` | `Decimal` | Fill price. | | `size` | `Decimal` | Fill size. | | `side` | `OrderSide` | Fill side. | | `hash` | `str` | L1 transaction hash. | | `oid` | `int` | Venue order id for the slice. | | `tid` | `int` | Venue trade id. | | `crossed` | `bool` | Whether the fill crossed the spread (taker). | | `fee` | `Decimal` | Fee amount (negative means rebate). | | `fee_token` | `str` | Token the fee was paid in. | | `dir` | `str` | Venue frontend direction string. | | `closed_pnl` | `Decimal` | Closed PnL for the fill. | | `is_snapshot` | `bool` | Whether this fill belongs to a venue snapshot batch. | | `ts_event` | `int` | Fill time as UNIX nanoseconds. | | `ts_init` | `int` | UNIX timestamp in nanoseconds when the object was built. | ```python from nautilus_trader.adapters.hyperliquid import HYPERLIQUID_CLIENT_ID from nautilus_trader.adapters.hyperliquid import HyperliquidTwapHistory from nautilus_trader.adapters.hyperliquid import HyperliquidTwapSliceFill from nautilus_trader.model import DataType self.subscribe_data( data_type=DataType( HyperliquidTwapHistory.__name__, metadata={"user": "0x..."}, ), client_id=HYPERLIQUID_CLIENT_ID, ) self.subscribe_data( data_type=DataType( HyperliquidTwapSliceFill.__name__, metadata={"user": "0x..."}, ), client_id=HYPERLIQUID_CLIENT_ID, ) ``` Venue snapshot batches set `is_snapshot=True` on every row/fill from that batch so consumers can clear and rebuild local TWAP state. In a Python strategy running inside a `LiveNode`, the payload is delivered to `on_data` as the concrete custom data type itself: ```python from decimal import Decimal from nautilus_trader.adapters.hyperliquid import HyperliquidOpenInterest def on_data(self, data) -> None: if isinstance(data, HyperliquidOpenInterest): if data.open_interest > Decimal("1000"): self.log.info(f"OI {data.instrument_id} -> {data.open_interest}") ``` `HyperliquidAllDexsAssetCtxs` exposes a whole-feed aggregate rather than one topic per instrument, so strategies subscribe once and filter the normalized entries they need: | Field | Type | Description | | ----------------- | --------------------------------- | -------------------------------------------------------------------------- | | `dex` | `str` | Perp dex identifier from Hyperliquid `perpDexs`. `""` is the default dex. | | `instrument_id` | `InstrumentId` | Canonical Nautilus instrument ID for the entry. | | `mark_price` | `Price` | Current mark price. | | `oracle_price` | `Price` | Current oracle / index reference price. | | `prev_day_price` | `Price` | Previous day reference price from the venue payload. | | `mid_price` | `Price \| None` | Mid price when present in the venue payload. | | `impact_prices` | `HyperliquidImpactPrices \| None` | Best bid / ask impact prices when present. | | `funding_rate` | `Decimal` | Funding rate parsed for direct arithmetic use. | | `open_interest` | `Decimal` | Open interest parsed for direct arithmetic use. | | `premium` | `Decimal \| None` | Premium when present in the venue payload. | | `day_ntl_volume` | `Decimal` | 24h notional volume. | | `day_base_volume` | `Decimal` | 24h base volume. | | `ts_event` | `int` | UNIX timestamp in nanoseconds when the update occurred. Mirrors `ts_init`. | | `ts_init` | `int` | UNIX timestamp in nanoseconds when the object was built. | The underlying Hyperliquid wire payload arrives as `ctxs: [[dex, ctxs[]], ...]`. The adapter decodes that live venue format and normalizes it into the per-entry output shown above before the strategy sees the data. This aggregate is live-only and JSON-backed rather than Arrow-backed, so unlike the other three types it is not written to a Parquet catalog. The adapter does not invent `dex` values. It bootstraps the ordered dex universe from Hyperliquid `meta` / `allPerpMetas` and resolves builder dex identifiers from the live `perpDexs` info endpoint. The empty string `""` represents Hyperliquid's default perp dex; non-empty values such as `xyz`, `flx`, or `vntl` are venue-defined builder dex identifiers. The mapping is rebuilt from the cached instruments at connect, on every instrument refresh, and on every instrument request, and the feed is positional (no per-entry coin name), so perps listed later appear after the next rebuild. When `allPerpMetas` is unavailable the rebuild covers only the default dex and keeps the existing mapping for every builder dex. A context-count mismatch for a dex logs a warning until the next rebuild; entries stay aligned positionally, which is correct for appended listings. ```python from nautilus_trader.adapters.hyperliquid import HYPERLIQUID_CLIENT_ID from nautilus_trader.adapters.hyperliquid import HyperliquidAllDexsAssetCtxs from nautilus_trader.model import DataType self.subscribe_data( data_type=DataType(HyperliquidAllDexsAssetCtxs.__name__), client_id=HYPERLIQUID_CLIENT_ID, ) def on_data(self, data) -> None: if isinstance(data, HyperliquidAllDexsAssetCtxs): for entry in data.entries: if entry.dex == "xyz": self.log.info(f"{entry.instrument_id} OI={entry.open_interest}") ``` ### Supported bar intervals | Resolution | Hyperliquid candle | | ---------- | ------------------ | | 1-MINUTE | `1m` | | 3-MINUTE | `3m` | | 5-MINUTE | `5m` | | 15-MINUTE | `15m` | | 30-MINUTE | `30m` | | 1-HOUR | `1h` | | 2-HOUR | `2h` | | 4-HOUR | `4h` | | 8-HOUR | `8h` | | 12-HOUR | `12h` | | 1-DAY | `1d` | | 3-DAY | `3d` | | 1-WEEK | `1w` | | 1-MONTH | `1M` | ## Orders capability Hyperliquid supports a full set of order types and execution options. In the tables below, "Perpetuals" covers both standard validator-operated perps and HIP-3 builder-deployed perps: the same order types, time-in-force options, and execution instructions apply to both. ### Order types | Order Type | Perpetuals | Spot | Notes | | ------------------- | ---------- | ---- | --------------------------------------------------- | | `MARKET` | ✓ | ✓ | IOC limit with configurable slippage from best BBO. | | `LIMIT` | ✓ | ✓ | | | `STOP_MARKET` | ✓ | ✓ | Stop loss orders. | | `STOP_LIMIT` | ✓ | ✓ | Stop loss with limit execution. | | `MARKET_IF_TOUCHED` | ✓ | ✓ | Take profit at market. | | `LIMIT_IF_TOUCHED` | ✓ | ✓ | Take profit with limit execution. | Conditional orders (stop and if-touched) are implemented using Hyperliquid's native trigger order functionality with automatic TP/SL mode detection. All trigger orders are evaluated against the [mark price](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/robust-price-indices). Standalone trigger orders rest on the venue until triggered, independent of the reduce-only flag. Grouped (bracket) TP/SL children are always submitted as reduce-only by the adapter. ### Market-order pricing Market orders are submitted as IOC limit orders priced from the best ask (for buys) or best bid (for sells) with a configurable slippage buffer (default 50 bps). Prices are rounded to Hyperliquid's price constraints before submission. The slippage buffer is controlled by `market_order_slippage_bps` on `HyperliquidExecutionClientConfig` and can be overridden per-order via the `market_order_slippage_bps` key in `SubmitOrder.params`. `STOP_MARKET` and `MARKET_IF_TOUCHED` orders do not carry a limit price. The adapter derives one from the trigger price with the same configurable slippage buffer (default 50 bps), rounds to 5 significant figures, and clamps to the venue decimal limit (ceiling for buys, floor for sells). This guarantees Hyperliquid's `limit_px >= trigger_px` (buys) / `limit_px <= trigger_px` (sells) constraint. :::info **Market orders require cached quote data.** Without a cached quote the adapter emits `OrderDenied` rather than guessing a price. Subscribe to quotes for any instrument you intend to trade with market orders. ::: ### Quote-denominated quantities Hyperliquid has no native quote-quantity order: the exchange endpoint takes a base `s` for every order. Orders with a quote-denominated quantity (`quote_quantity=True` on the order factory) are converted to a base size at submission, using the cached quote's best ask (for buys) or best bid (for sells) as the reference price, rounded to the instrument's size increment. Consequences of the conversion: - The converted size is an estimate at the reference price: the venue executes the base size it receives, so the filled notional can differ slightly from the requested quote amount. - Venue fills arrive in base units while the order's local quantity stays quote-denominated, so these orders reconcile from venue status reports instead of local quantity comparison. - Modifying a quote-denominated order is rejected locally, because the venue modify replaces a base size that cannot be reconciled against a quote target. Cancel and resubmit with a new amount instead. - The raw HTTP and WebSocket client methods (`submit_order`, `modify_order`) take explicit base sizes, and the OrderAny-based raw submits (`submit_orders`, `submit_order_from_order_any`) reject quote-denominated orders. :::info **Conversion requires a cached quote.** Without a cached quote, or when the rounded base size is zero, the adapter emits `OrderDenied` rather than guessing a size. Subscribe to quotes for any instrument you intend to trade with quote-denominated quantities. ::: ### Price normalization :::warning **Price normalization is enabled by default.** Hyperliquid enforces a maximum of 5 significant figures on order prices, plus a per-asset decimal limit based on `szDecimals` (`6 - szDecimals` for perps, `8 - szDecimals` for spot). For example, if ETH is trading at $2,600 (4 integer digits), only 1 decimal place is allowed despite the instrument having `price_precision=2`. By default, the adapter normalizes all outgoing limit and trigger prices to 5 significant figures and clamps them to the instrument price precision to prevent order rejections. This means your submitted prices may shift slightly. To disable this and take full control of price formatting, set `normalize_prices=False` in your `HyperliquidExecutionClientConfig`. If you disable normalization, you can apply the same rounding in your strategy: ```python from decimal import Decimal def round_to_sig_figs(price: Decimal, sig_figs: int = 5) -> Decimal: if price == 0: return Decimal(0) shift = sig_figs - int(price.adjusted()) - 1 if shift <= 0: factor = Decimal(10) ** (-shift) return (price / factor).to_integral_value() * factor return round(price, shift) ``` When normalization is disabled, the adapter validates each outgoing limit and trigger price against the instrument's decimal limit and denies the order locally when the price carries more decimal places. The venue parses prices into its canonical form before verifying the request signature, so an over-precise price fails signature verification and the venue answers with a misleading "user or API wallet does not exist" error instead of an order validation error. ::: ### Time in force | Time in force | Perpetuals | Spot | Notes | | ------------- | ---------- | ---- | -------------------- | | `GTC` | ✓ | ✓ | Good Till Canceled. | | `IOC` | ✓ | ✓ | Immediate or Cancel. | | `FOK` | - | - | *Not supported*. | | `GTD` | - | - | *Not supported*. | Venue `orderStatus` and `historicalOrders` payloads can report `FrontendMarket` or `LiquidationMarket` instead of `IOC`. The adapter maps both to `IOC` and does not submit those labels. :::note When an IOC order cannot match any resting liquidity, Hyperliquid reports `iocCancelRejected` with `Order could not immediately match against any resting orders`. The adapter preserves this venue rejection as `OrderRejected`. It does not synthesize an `OrderAccepted` followed by `OrderCanceled`. A partially filled IOC still keeps its fills and cancels only the unfilled remainder. ::: ### Execution instructions | Instruction | Perpetuals | Spot | Notes | | ---------------- | ---------- | ---- | ------------------------------------------------------------ | | `post_only` | ✓ | ✓ | Equivalent to ALO time in force. | | `reduce_only` | ✓ | ✓ | Close-only orders. | | `quote_quantity` | ✓ | ✓ | Quote amount converted to a base size from the cached quote. | :::info Post-only orders that would immediately match are rejected by Hyperliquid. The adapter detects this and generates an `OrderRejected` event. Post-only orders are routed through Hyperliquid's ALO (Add-Liquidity-Only) lane. ::: ### Order operations | Operation | Perpetuals | Spot | Notes | | ----------------- | ---------- | ---- | ---------------------------------------------- | | Submit order | ✓ | ✓ | Single order submission. | | Submit order list | ✓ | ✓ | Batch order submission (single API call). | | Modify order | ✓ | ✓ | Requires venue order ID. | | Cancel order | ✓ | ✓ | Cancel by client order ID. | | Cancel all orders | ✓ | ✓ | Batched `cancelByCloid` for open orders. | | Batch cancel | ✓ | ✓ | Batched `cancelByCloid` for the provided list. | :::info Cancels prefer `cancelByCloid` and fall back to `cancel` by numeric OID when no CLOID is cached; fast and standard cancels dispatch as separate batched actions, so one cancel request can produce more than one venue call. Definite local cancel failures and authoritative venue rejections emit `OrderCancelRejected` for each affected order. Per-order errors in a batch response leave the other cancels intact; an explicit whole-request rejection applies to every cancel in that dispatched action. Rejection events preserve the venue's error message. After dispatch, transport failures and responses that leave the venue outcome unknown keep orders available for reconciliation. ::: :::info Orders placed outside NautilusTrader (e.g. via the Hyperliquid web UI or another client) are detected and tracked as external orders. They appear in order status reports and position reconciliation. ::: ### Modify as cancel-replace Hyperliquid implements order modification as a **cancel-replace**. The `modify` action on the [exchange endpoint](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-an-order) cancels the original order (old `oid`) and opens a replacement with a new `oid`. Both legs share the same client order ID (`cloid`). The modify HTTP response only confirms success. The [`orderUpdates` WebSocket subscription](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions) then delivers an `ACCEPTED(new_oid)` status report, followed by a `CANCELED(old_oid)` for the original leg. `HyperliquidExecutionClient` runs detection, deduplication, and event promotion through the [`WsDispatchState`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/hyperliquid/src/websocket/dispatch.rs) it owns, so strategies never see the replacement as a new order. On submission the client registers an `OrderContext` keyed by `client_order_id`, using its strategy, instrument, side, type, quantity, and last-known price. Each inbound status report or fill is routed through the dispatch: tracked orders emit typed `OrderEventAny::*` events via `ExecutionEventEmitter::send_order_event`; external orders fall back to the raw `OrderStatusReport` / `FillReport` so the engine can reconcile. The dispatch compares the report's `venue_order_id` against the last cached value for the `cloid`; when they differ it promotes the `ACCEPTED` to `OrderUpdated` and suppresses the paired stale cancel: ```mermaid sequenceDiagram participant Strategy participant ExecClient as HyperliquidExecutionClient participant Dispatch as WsDispatchState participant HTTP as Hyperliquid HTTP participant WS as Hyperliquid WS Strategy->>ExecClient: ModifyOrder(cloid, old_oid) ExecClient->>HTTP: POST /exchange { action: "modify", oid: old_oid } HTTP-->>ExecClient: { status: "ok" } ExecClient->>Dispatch: mark_pending_modify(cloid, old_oid) WS-->>ExecClient: ACCEPTED(new_oid, cloid) ExecClient->>Dispatch: dispatch_order_event() Dispatch->>Dispatch: cached_voi != new_oid -> promote to OrderUpdated,
claim_front_modify, record_venue_order_id(new_oid) Dispatch-->>Strategy: OrderUpdated(venue_order_id=new_oid) WS-->>ExecClient: CANCELED(old_oid, cloid) ExecClient->>Dispatch: dispatch_order_event() Dispatch->>Dispatch: cached_voi != old_oid -> Skip (stale cancel) ``` Modify happy path: the strategy sees one `OrderUpdated` carrying the new `oid`, and the venue's paired cancel of the old leg never reaches it. #### Early cancel before the replacement If Hyperliquid delivers `CANCELED(old_oid)` before `ACCEPTED(new_oid)` for an in-flight modify, a pending-modify intent lets the dispatch drop the old leg's cancel and still route the subsequent `ACCEPTED` through the `OrderUpdated` path. The intent is queued before the HTTP call, so an early cancel is suppressed even while the request is still in flight. If the request fails before dispatch, or the venue rejects it, the adapter emits `OrderModifyRejected` and clears its own intent. A failure after dispatch with an unknown venue outcome keeps the intent, so a modify that reaches the venue despite a client-side timeout still suppresses the early `CANCELED(old_oid)` and promotes the eventual `ACCEPTED(new_oid)` to `OrderUpdated` (detection otherwise falls back to the cached `venue_order_id`, which the late `ACCEPTED` no longer matches). See [GH-3827](https://github.com/nautechsystems/nautilus_trader/issues/3827). #### Chained modifies Rapid repeated modifies under the same `cloid` queue as a chain of in-flight intents rather than a single marker. A later modify does not overwrite an earlier intent's old-leg suppression, and a failed modify clears only its own attempt, leaving newer queued modifies intact. Each replacement `ACCEPTED` promotes the oldest queued intent and advances the next intent's old leg to the promoted replacement, so every leg's stale cancel is suppressed and each `OrderUpdated` carries its own target quantity. The same chain guards the inflight query and single-order reconcile paths. While a modify is in flight, `query_order` and `generate_order_status_report` drop a `Canceled` for the superseded leg, so an out-of-band status probe that resolves the old `oid` before the replacement appears cannot terminate the live order. A non-cancel status for the old leg (such as a late `Filled`) is still forwarded so reconciliation can recover it. #### Dropped replacement acceptance These paths also promote the replacement. Hyperliquid lists the replacement under the same `cloid` with a new `oid` in `frontendOpenOrders`, so when the replacement `ACCEPTED(new_oid)` was dropped on the WebSocket and no fill has arrived, the query resolves it by `cloid` and promotes it to `OrderUpdated` directly (rebinding the `cloid` to `new_oid` and advancing the modify chain). The order is therefore not left bound to the canceled leg, and subsequent modifies and cancels target the live replacement. See [GH-4270](https://github.com/nautechsystems/nautilus_trader/issues/4270). #### Fills racing the replacement A `FillReport` for the replacement leg can also race ahead of `ACCEPTED(new_oid)`. When the pending-modify marker is set and the report's `oid` does not match the cached value, the dispatch promotes the binding directly from the fill (`OrderUpdated` then `OrderFilled`) using the modify target price. If no price is available to promote with, it buffers the fill instead and drains it on the matching `ACCEPTED`, so `OrderFilled` always follows the promoting `OrderUpdated` against up-to-date state. See [GH-3972](https://github.com/nautechsystems/nautilus_trader/issues/3972). :::note A chained-modify edge case is deferred: if a delayed fill from a *prior* leg arrives during a *new* in-flight modify and that new modify then fails, the buffered fill is stranded until terminal cleanup. Reconciliation (`request_fill_reports`) recovers it. Fully closing this requires additional design work (retired-VOI tracking or drain on modify-failure paths). ::: ## Order books Order books are maintained via L2 WebSocket subscription. Each message delivers a full-depth snapshot (clear + rebuild), not incremental deltas. :::note A trader instance maintains one order book per instrument, so all subscribers to an instrument share the same book and the same venue-side precision options. ::: ## Account and position management `AccountState` merges perp margin and spot balances. Perp margin and cross-margin usage come from `clearinghouseState`; non-zero spot tokens (USDC, USDH, HYPE, vault tokens, HIP-4 outcome side tokens, etc.) come from `spotClearinghouseState`. USDC comes from the perp summary when it reflects non-zero collateral, margin, or withdrawable balance; when the perp summary is absent or zeroed, spot USDC is used instead. Standard perps default to cross margin; HIP-3 perps default to isolated. On connect, the execution client reconciles orders, fills, and positions against Hyperliquid's clearinghouse state. Spot positions are reconstructed from held balances (long-only); HIP-4 side tokens reconcile against their matching `BinaryOption` instruments. See [HIP-3 reconciliation](#open-order-and-position-reconciliation) for per-dex open-order and position fan-out. :::note Leverage is managed directly through the Hyperliquid web UI or API, not through the adapter. Set your desired leverage per instrument on Hyperliquid before trading. ::: ## Liquidation and ADL handling Hyperliquid signals venue-initiated closures through three surfaces on the `userEvents` subscription: - **`liquidation` event**: emitted when an account is liquidated. Carries a `liquidation ID`, liquidator address, liquidated user, liquidated notional position, and liquidated account value. The adapter logs these at warning level for operator visibility. - **Fill-level `liquidation` metadata**: each entry in the `fills` array can carry an optional `liquidation` object with `method`, `markPx`, and `liquidatedUser`. The `method` value is either `market` (liquidated into the book) or `backstop` (closed against the backstop vault, the equivalent of an ADL close when the insurance mechanism steps in). - **`Auto-Deleveraging` fill direction**: a fill whose `dir` is `Auto-Deleveraging` is an ADL closure taken against a counterparty position. It carries no `liquidation` object, so the adapter recognizes it from the direction alone and logs it at warning level with the instrument, order ID, price, and size. The adapter emits the standard `FillReport` for each of these fills. The liquidation or deleveraging detail is logged alongside the fill so you can correlate closures to venue-side events. No strategy-side changes are required; existing risk and reconciliation logic runs over these fills as for any other TAKER fill. Upstream references: - [WebSocket `userEvents` (`liquidation` and `FillLiquidation`)](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions) - [Liquidation mechanics](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/liquidations) ## Connection management The adapter automatically reconnects on WebSocket disconnection using exponential backoff (starting at 250ms, up to 5s). On reconnect, all active subscriptions are resubscribed automatically, order book snapshots are rebuilt, and a `Reconnected` event is forwarded after those resubscription commands are queued. No manual intervention is required. A heartbeat ping is sent every 30 seconds to keep the connection alive (Hyperliquid closes idle connections after 60 seconds). The shared transport treats 90 seconds without any inbound frame as a dead peer and starts the same reconnect path. Live data and execution clients publish `SocketStateChanged` on `hyperliquid-data-streams` and `hyperliquid-user-streams`. Both endpoints register a reconnect handle, so `reconnect_socket` can target them without cycling the containing client. ### Stream health and recovery The data client tracks receive freshness for order book deltas, depth-10 snapshots, and BBO quotes: - `stale_stream_receive_timeout_secs` sets the stale threshold. - `stale_stream_warning_cooldown_secs` controls repeat warnings. - A fresh BBO stream for the same instrument changes stale book warnings to relative-staleness warnings. BBO quotes are only a freshness reference, not order book input. Recovery is off by default. When `stale_stream_recovery_enabled` is set: - The first stale check always warns. - A still-stale stream receives one targeted resubscribe per `stale_stream_recovery_cooldown_secs`. - `l2Book` resubscribes preserve the original precision options. - After `stale_stream_max_targeted_resubscribes` attempts, the client requests a full WebSocket reconnect. - Fresh data resets the stream's recovery ladder. ## API credentials There are two options for supplying your credentials to the Hyperliquid clients. Either pass the corresponding values to the configuration objects, or set the following environment variables: | Environment | Variables | | ------------------------------- | ------------------------------------------------------------------------------- | | Mainnet | `HYPERLIQUID_PK`; `HYPERLIQUID_VAULT` (optional, for vault trading) | | Testnet | `HYPERLIQUID_TESTNET_PK`; `HYPERLIQUID_TESTNET_VAULT` (optional, vault trading) | | Either, for agent (API) wallets | `HYPERLIQUID_ACCOUNT_ADDRESS` (master account address; shared by both) | :::tip We recommend using environment variables to manage your credentials. ::: ## Agent wallets Hyperliquid lets a master account approve an [agent wallet](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets) (also called an API wallet or sub-key) that signs orders on the master's behalf. Orders signed by the agent belong to the master account, not to the agent's address. If your `HYPERLIQUID_PK` (or `HYPERLIQUID_TESTNET_PK`) is an agent wallet, you must also set `account_address` (or the `HYPERLIQUID_ACCOUNT_ADDRESS` environment variable) to the master account address. Otherwise the adapter queries the agent's address for balances, orders, and WebSocket events, which owns nothing, and submitted orders will never reconcile (no `OrderStatusReport`, no fills surfaced). The execution factory resolves one account address and passes that same value to REST account queries and WebSocket user subscriptions. Signing still uses the configured private key, and vault trading still sends `vaultAddress` in the signed exchange payload when `vault_address` is set. Explicit config values take precedence over environment variables. Environment variables fill only omitted config values. Resolution order for the execution account address used by info queries and WebSocket subscriptions: 1. `account_address` (master account when using an agent wallet). 2. `vault_address` (vault sub-account). 3. `HYPERLIQUID_ACCOUNT_ADDRESS`. 4. `HYPERLIQUID_VAULT` or `HYPERLIQUID_TESTNET_VAULT`. 5. The address derived from the private key (the wallet itself). :::note `HYPERLIQUID_ACCOUNT_ADDRESS` is a single env var shared by both mainnet and testnet (unlike `HYPERLIQUID_PK` / `HYPERLIQUID_TESTNET_PK`). If your agent wallet is approved under the same master address on both environments, one value covers both. ::: :::tip Email-login wallets generate different addresses for mainnet and testnet, so the master address may differ. In that case, prefer setting `account_address` explicitly in `HyperliquidExecutionClientConfig` per environment rather than relying on the shared environment variable. ::: ## Vault trading Hyperliquid supports [vault trading](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/vaults), where a wallet operates on behalf of a vault (sub-account). Orders are signed with the wallet's private key but include the vault address in the signature payload. To trade via a vault, set the `vault_address` in your execution client config (or set the `HYPERLIQUID_VAULT` / `HYPERLIQUID_TESTNET_VAULT` environment variable). :::warning For normal vault trading, leave `account_address` unset so `vault_address` becomes the account address used for REST queries and WebSocket user subscriptions. If both `account_address` and `vault_address` are set, `account_address` wins for queries and subscriptions, while `vault_address` still goes into the signed exchange payload. ::: ## Funding rates Hyperliquid perpetual futures use a fixed 1-hour funding interval. The adapter sets `interval` to `60` (minutes) on all `FundingRateUpdate` objects. ## Rate limiting Hyperliquid applies limits by IP address and user address. The adapter uses the fixed venue limits from the [Hyperliquid rate limits documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/rate-limits-and-user-limits). It does not expose higher overrides. ### REST limits #### Sharing scope The adapter shares one 1,200-weight-per-minute token bucket among clients in the same process when their environment, HTTP endpoint origin, and proxy route match. The `/info` and `/exchange` paths on one origin consume the same bucket. Separate processes, programs, proxy routes, and HTTP clients outside this adapter do not coordinate through the in-memory bucket. Deployments that share an egress IP must leave capacity for that traffic. #### Request weights | Endpoint | Request | Base weight | | ----------- | ------------------------ | -----------------------------: | | `/exchange` | All actions | `1 + floor(batch length / 40)` | | `/info` | `l2Book` | 2 | | `/info` | `allMids` | 2 | | `/info` | `clearinghouseState` | 2 | | `/info` | `orderStatus` | 2 | | `/info` | `spotClearinghouseState` | 2 | | `/info` | `exchangeStatus` | 2 | | `/info` | `userRole` | 60 | | `/info` | All other requests | 20 | An order or cancel batch counts as one IP request. Some `/info` responses add weight based on the number of returned items: | Data | Requests | Added weight | | ------------------------- | --------------------------------------------------------------- | -----------------------: | | Candles | `candleSnapshot` | +1 per 60 returned items | | Trades and orders | `recentTrades`, `historicalOrders` | +1 per 20 returned items | | Fills | `userFills`, `userFillsByTime` | +1 per 20 returned items | | Funding | `fundingHistory`, `userFunding`, `nonUserFundingUpdates` | +1 per 20 returned items | | TWAP | `twapHistory`, `userTwapSliceFills`, `userTwapSliceFillsByTime` | +1 per 20 returned items | | Delegators and validators | `delegatorHistory`, `delegatorRewards`, `validatorStats` | +1 per 20 returned items | #### Retries Each HTTP attempt consumes its full request weight. | Request or response | Behavior | | ------------------------------------------- | --------------------------------------- | | HTTP 408, 429, or 5xx from `/info` | Retry up to three times. | | HTTP 429 with integer-seconds `Retry-After` | Use the header value as the delay. | | Retryable response without a valid delay | Use capped full-jitter backoff. | | Response failure from `/exchange` | No retry; venue outcome may be unknown. | ### WebSocket limits #### Sharing scope Clients in the same process share WebSocket limits when their environment, WebSocket endpoint origin, and proxy route match. #### Enforced limits | Limit | Maximum | Applies to | | ----------------- | -----------: | ---------------------------------------------------------------- | | Outbound messages | 2,000/minute | Subscriptions, unsubscriptions, posts, heartbeats, and pongs. | | In-flight posts | 100 | Simultaneous post requests. | | Connections | 10 | Simultaneous connections. | | New connections | 30/minute | Initial connections and reconnect attempts. | | Subscriptions | 1,000 | Active and pending subscriptions. | | Unique users | 10 | User-specific subscriptions; addresses match case-insensitively. | #### Reconnects and releases Automatic reconnects retain the logical connection slot and active subscription reservations. They still consume the new-connection rate. A confirmed unsubscribe, explicit client disconnect, or terminal handler exit releases the corresponding subscription reservations. #### Post deadlines and retries WebSocket post requests use one caller deadline while waiting for an in-flight slot, the command channel, the outbound-message quota, the active connection, and the response. The client retries a post send only when the network layer proves that writing did not start. A write timeout or broken connection after writing starts has an unknown venue outcome, so the adapter returns the error and does not resend the action. ### Address and exchange limits Hyperliquid also enforces server-side limits that one adapter process cannot calculate reliably. #### Action limits Each address starts with 10,000 action requests and accrues one request per cumulative USDC traded. Once limited, the address may send one request every 10 seconds. Subaccounts have independent limits. #### Cancel allowance Cancels receive `min(action limit + 100,000, action limit * 2)` requests. A batch of `n` actions consumes one IP request but `n` address requests. #### Open-order limits Each address starts with 1,000 open orders, gains one additional order per $5 million of cumulative volume, and is capped at 5,000. Hyperliquid rejects a new reduce-only or trigger order when the address already has at least 1,000 other open orders. #### Congestion During congestion, an address's prior UTC-day maker share and the asset's fee tier determine its block-space allowance. Do not resend a cancel after Hyperliquid returns a response. #### Enforcement boundary Hyperliquid remains authoritative for these limits because volume, open orders, and requests can come from other processes and clients. Venue rejections are returned to the caller. The adapter does not use Hyperliquid's explorer API or the official EVM JSON-RPC endpoint. Their separate weights and request limits therefore remain outside this adapter's limiter. ## Configuration ### Data client configuration options | Option | Default | Description | | ---------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------- | | `private_key` | `None` | Optional EVM private key for authenticated endpoints. | | `base_url_ws` | `None` | Override for the WebSocket base URL. | | `base_url_http` | `None` | Override for the HTTP info URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `environment` | `None` | Environment enum (`MAINNET` or `TESTNET`); resolves to `MAINNET` when unset. | | `http_timeout_secs` | `60` | Timeout (seconds) applied to REST calls. | | `ws_timeout_secs` | `30` | Timeout (seconds) applied to WebSocket connections. | | `stale_stream_receive_timeout_secs` | `120` | Receive age threshold (seconds) for stale market data stream warnings. Set to `0` to disable the stream health monitor. | | `stream_health_check_interval_secs` | `15` | Interval (seconds) between market data stream health checks. Set to `0` to disable the stream health monitor. | | `stale_stream_warning_cooldown_secs` | `60` | Cooldown (seconds) between stale warnings for the same market data stream. | | `stale_stream_recovery_enabled` | `False` | Enable automated recovery of stale market data streams (targeted resubscribe, then reconnect). | | `stale_stream_recovery_cooldown_secs` | `120` | Cooldown (seconds) between recovery actions for the same market data stream. Must be positive for recovery to run. | | `stale_stream_max_targeted_resubscribes` | `3` | Targeted resubscribe attempts for a stale stream before escalating to a full WebSocket reconnect. | | `update_instruments_interval_mins` | `60` | Interval (minutes) between instrument catalog refreshes. Set to `0` to disable the refresh. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | ### Execution client configuration options | Option | Default | Description | | ------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `account_id` | `Venue` | Nautilus account identifier; defaults to `HYPERLIQUID-001`. | | `private_key` | `None` | EVM private key; loaded from `HYPERLIQUID_PK` or `HYPERLIQUID_TESTNET_PK` when omitted. | | `vault_address` | `None` | Vault address; loaded from `HYPERLIQUID_VAULT` or `HYPERLIQUID_TESTNET_VAULT` if omitted. | | `account_address` | `None` | Main account address for agent wallet trading; loaded from `HYPERLIQUID_ACCOUNT_ADDRESS`. | | `environment` | `None` | Environment enum (`MAINNET` or `TESTNET`); resolves to `MAINNET` when unset. | | `base_url_ws` | `None` | Override for the WebSocket base URL. | | `base_url_http` | `None` | Override for the HTTP info base URL. | | `base_url_exchange` | `None` | Override for the exchange API base URL. | | `max_retries` | `3` | Maximum retry attempts for submit, cancel, or modify order requests. | | `retry_delay_initial_ms` | `100` | Initial delay (milliseconds) between retries. | | `retry_delay_max_ms` | `5,000` | Maximum delay (milliseconds) between retries. | | `http_timeout_secs` | `60` | Timeout (seconds) applied to REST calls. | | `ws_post_timeout_secs` | `10` | Timeout (seconds) applied to WebSocket post trading requests. | | `normalize_prices` | `True` | Normalize order prices to 5 significant figures before submission. | | `include_builder_attribution` | `True` | Include zero-fee Nautilus builder attribution on eligible mainnet orders. | | `market_order_slippage_bps` | `50` | Slippage buffer (bps) applied to MARKET and stop trigger derivations. Overridable per-order via `SubmitOrder.params`. | | `outcome_settlement_poll_secs` | `0` | HIP-4 `outcomeMeta` settlement poll interval (seconds). Rust-only; venue `Settlement` fills cover settlement, so polling is disabled by default. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | :::note `outcome_settlement_poll_secs` is the only Rust-only option: it is not exposed on the `HyperliquidExecutionClientConfig` Python constructor and always uses its default. The `max_retries`, `retry_delay_initial_ms`, and `retry_delay_max_ms` fields are accepted on both the Rust and Python config but are not yet consumed by the execution client (its HTTP client is constructed with only the request timeout and proxy). These fields do not change the bounded read-only REST retries or the pre-write-only WebSocket post retries described in [Rate limiting](#rate-limiting). ::: ### Live node configuration Register `HyperliquidDataClientConfig` with `HyperliquidDataClientFactory` on the node builder. Register `HyperliquidExecutionClientConfig` directly with `HyperliquidExecutionClientFactory`. The node supplies the `TraderId`, while the execution client config supplies the `AccountId`. The [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/hyperliquid/) show the complete `LiveNode.builder(...)` wiring for both clients. When `environment=HyperliquidEnvironment.TESTNET`, the adapter uses `HYPERLIQUID_TESTNET_PK` and `HYPERLIQUID_TESTNET_VAULT` instead of the mainnet environment variables. ## Contributing :::info For additional features or to contribute to the Hyperliquid adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Integrations Source: https://nautilustrader.io/docs/latest/integrations/ NautilusTrader uses modular *adapters* to connect to trading venues and data providers, translating raw APIs into a unified interface and normalized domain model. The following integrations are currently supported: | Name | ID | Type | Status | Docs | | :--------------------------------------------------------- | :-------------------- | :---------------------- | :--------------------------------------------------- | :------------------------------ | | [AX Exchange](https://architect.exchange) | `AX` | Derivatives Exchange | ![status](https://img.shields.io/badge/stable-green) | [Guide](architect_ax.md) | | [Betfair](https://betfair.com) | `BETFAIR` | Sports Betting Exchange | ![status](https://img.shields.io/badge/stable-green) | [Guide](betfair.md) | | [Binance](https://binance.com) | `BINANCE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](binance.md) | | [Coinbase](https://coinbase.com) | `COINBASE` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](coinbase.md) | | [BitMEX](https://www.bitmex.com) | `BITMEX` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](bitmex.md) | | [Blockchain](blockchain.md) | `BLOCKCHAIN` | DeFi Data Provider | ![status](https://img.shields.io/badge/stable-green) | [Guide](blockchain.md) | | [Bybit](https://www.bybit.com) | `BYBIT` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](bybit.md) | | [Databento](https://databento.com) | `DATABENTO` | Data Provider | ![status](https://img.shields.io/badge/stable-green) | [Guide](databento.md) | | [Deribit](https://www.deribit.com) | `DERIBIT` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](deribit.md) | | [Derive](https://www.derive.xyz) | `DERIVE` | Crypto Exchange (DEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](derive.md) | | [dYdX](https://dydx.trade) | `DYDX` | Crypto Exchange (DEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](dydx.md) | | [Hyperliquid](https://hyperliquid.xyz) | `HYPERLIQUID` | Crypto Exchange (DEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](hyperliquid.md) | | [Interactive Brokers](https://www.interactivebrokers.com) | `INTERACTIVE_BROKERS` | Brokerage (multi-venue) | ![status](https://img.shields.io/badge/stable-green) | [Guide](interactive_brokers.md) | | [Kraken](https://kraken.com) | `KRAKEN` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](kraken.md) | | [Lighter](https://lighter.xyz) | `LIGHTER` | Crypto Exchange (DEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](lighter.md) | | [Lighter on Robinhood](https://robinhoodchain.lighter.xyz) | `LIGHTER_ROBINHOOD` | Crypto Exchange (DEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](lighter.md) | | [OKX](https://okx.com) | `OKX` | Crypto Exchange (CEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](okx.md) | | [Polymarket](https://polymarket.com) | `POLYMARKET` | Prediction Market (DEX) | ![status](https://img.shields.io/badge/stable-green) | [Guide](polymarket.md) | | [Tardis](https://tardis.dev) | `TARDIS` | Crypto Data Provider | ![status](https://img.shields.io/badge/stable-green) | [Guide](tardis.md) | - **ID**: The default client ID for the integrations adapter clients. - **Type**: The type of integration (often the venue type). For Lighter on Robinhood, `LIGHTER_ROBINHOOD` is the venue and explicit client ID to register. The shared Lighter factory keeps `LIGHTER` as its compatibility default. ## Status - `planned`: Planned for future development. - `building`: Under construction and likely not in a usable state. - `beta`: Completed to a minimally working state and in a 'beta' testing phase. - `stable`: Stabilized feature set and API, the integration has been tested by both developers and users to a reasonable level (some bugs may still remain). ## Implementation goals The primary goal of NautilusTrader is to provide a unified trading system for use with a variety of integrations. To support the widest range of trading strategies, priority will be given to *standard* functionality: - Requesting historical market data. - Streaming live market data. - Reconciling execution state. - Submitting standard order types with standard execution instructions. - Modifying existing orders (if possible on an exchange). - Canceling orders. The implementation of each integration aims to meet the following criteria: - Low-level client components should match the exchange API as closely as possible. - The full range of an exchange's functionality (where applicable to NautilusTrader) should *eventually* be supported. - Exchange specific data types will be added to support the functionality and return types which are reasonably expected by a user. - Actions unsupported by an exchange or NautilusTrader will be logged as a warning or error when invoked. ::::warning[Trace logging and credentials] TRACE logs may include raw outbound WebSocket payloads, which can contain authentication data for some venues. Use TRACE only for local debugging, and redact TRACE logs before sharing them. :::: ## API unification All integrations must conform to NautilusTrader's system API, requiring normalization and standardization: - Symbols should use the venue's native symbol format unless disambiguation is required (e.g., Binance Spot vs. Binance Futures). - Timestamps must use UNIX epoch nanoseconds. If milliseconds are used, field/property names should explicitly end with `_ms`. # Interactive Brokers Source: https://nautilustrader.io/docs/latest/integrations/interactive_brokers/ Interactive Brokers (IB) provides market access across equities, options, futures, currencies, bonds, funds, and other asset classes. The NautilusTrader adapter connects to Trader Workstation (TWS) or IB Gateway through the [TWS API](https://ibkrcampus.com/campus/ibkr-api-page/twsapi-doc/). The adapter provides live market data, execution, historical data, instrument loading, and optional Dockerized IB Gateway management through the same Rust implementation and Python bindings. ## Installation Install NautilusTrader using the [installation guide](../getting_started/installation.md). The Interactive Brokers adapter and Docker gateway support are included in the Python package; no adapter-specific extra is required. ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/interactive_brokers/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/interactive_brokers/examples/) ## Getting started Run either TWS or IB Gateway before starting a client, and configure that application to accept socket API connections. IB uses different default ports for each application and trading mode: | Application | Paper trading | Live trading | | ----------- | ------------: | -----------: | | TWS | `7497` | `7496` | | IB Gateway | `4002` | `4001` | The adapter defaults to `127.0.0.1:4002`, which matches a local paper-trading IB Gateway. Set the port explicitly when using TWS or a live account. ### Connect to TWS or IB Gateway Import the public configuration types from `nautilus_trader.adapters.interactive_brokers`: ```python from nautilus_trader.adapters.interactive_brokers import InteractiveBrokersDataClientConfig from nautilus_trader.adapters.interactive_brokers import InteractiveBrokersExecutionClientConfig from nautilus_trader.adapters.interactive_brokers import MarketDataType data_config = InteractiveBrokersDataClientConfig( host="127.0.0.1", port=7497, client_id=101, market_data_type=MarketDataType.DELAYED, ) exec_config = InteractiveBrokersExecutionClientConfig( host="127.0.0.1", port=7497, client_id=101, account_id="DU123456", ) ``` Use a distinct client ID for each process connected to the same TWS or IB Gateway session. An execution client ID cannot be a multiple of `1000` because the adapter partitions order IDs by `client_id % 1000`. The current [TWS example](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/interactive_brokers/connect_with_tws.py) and [Dockerized gateway example](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/interactive_brokers/connect_with_dockerized_gateway.py) show how to add these configs and their factories to a `LiveNode`. ### Use a Dockerized IB Gateway The adapter can manage the [gnzsnz IB Gateway container](https://github.com/gnzsnz/ib-gateway-docker). Supply credentials in the config or through `TWS_USERNAME` and `TWS_PASSWORD`: ```python from nautilus_trader.adapters.interactive_brokers import DockerizedIBGateway from nautilus_trader.adapters.interactive_brokers import DockerizedIBGatewayConfig from nautilus_trader.adapters.interactive_brokers import TradingMode gateway = DockerizedIBGateway( DockerizedIBGatewayConfig( trading_mode=TradingMode.PAPER, read_only_api=True, ), ) gateway.start_blocking() print(gateway.host) print(gateway.port) ``` Start `DockerizedIBGateway` separately, then pass its `host` and `port` to the data and execution configs. Passing a non-`None` `dockerized_gateway` argument to either client config raises `ValueError` because Python does not own the container lifecycle. Set `read_only_api=False` only when the gateway must submit orders. The default container is `ghcr.io/gnzsnz/ib-gateway:stable`; `vnc_port` accepts ports from `5900` through `5999` when remote desktop access is required. ## Components The public Python module exports these main components: - `InteractiveBrokersDataClientFactory`: creates live market data clients. - `InteractiveBrokersExecutionClientFactory`: creates live execution clients. - `InteractiveBrokersInstrumentProvider`: resolves IB contracts and Nautilus instruments. - `HistoricalInteractiveBrokersClient`: requests historical instruments, bars, and ticks. - `DockerizedIBGateway`: manages a containerized IB Gateway. ## Symbology and instruments `InteractiveBrokersInstrumentProviderConfig` supports two symbology methods: | Method | Purpose | Example | | ---------------------------- | --------------------------------------------- | ---------- | | `SymbologyMethod.SIMPLIFIED` | Uses shorter, readable symbols. | `EUR/USD` | | `SymbologyMethod.RAW` | Preserves the IB security type in the symbol. | `AAPL=STK` | The default is `SIMPLIFIED`. Use `RAW` when the security type must remain explicit in the instrument ID, as in `AAPL=STK.SMART`. Configure instruments by Nautilus instrument ID or by IB contract dictionaries: ```python from nautilus_trader.adapters.interactive_brokers import InteractiveBrokersInstrumentProviderConfig from nautilus_trader.adapters.interactive_brokers import SymbologyMethod from nautilus_trader.model import InstrumentId provider_config = InteractiveBrokersInstrumentProviderConfig( symbology_method=SymbologyMethod.RAW, load_ids={InstrumentId.from_str("AAPL=STK.SMART")}, load_contracts=[ { "symbol": "MSFT", "secType": "STK", "exchange": "SMART", "currency": "USD", }, ], ) ``` The same provider config can be passed to both data and execution client configs. This keeps contract resolution and instrument IDs consistent across both clients. ### Instrument provider options | Option | Default | Purpose | | ------------------------------- | ------------ | ------------------------------------------------------- | | `symbology_method` | `SIMPLIFIED` | Select simplified or raw instrument symbols. | | `load_ids` | Empty | Load Nautilus instrument IDs at startup. | | `load_contracts` | Empty | Load IB contract dictionaries at startup. | | `min_expiry_days` | `None` | Set the minimum expiry for chain loading. | | `max_expiry_days` | `None` | Set the maximum expiry for chain loading. | | `build_options_chain` | `None` | Control full option chain construction. | | `build_futures_chain` | `None` | Control full futures chain construction. | | `cache_validity_days` | `None` | Set the lifetime of cached instrument data. | | `convert_exchange_to_mic_venue` | `False` | Convert IB exchange codes to MIC venues. | | `symbol_to_mic_venue` | Empty | Override MIC venues for selected symbols. | | `filter_sec_types` | Empty | Exclude selected IB security types. | | `filter_callable` | `None` | Apply a Python callable by fully qualified import path. | | `cache_path` | `None` | Persist the instrument cache at the selected path. | ### Derivative chains and spreads Set chain flags on a contract dictionary to use that contract as the underlying or chain seed. The provider-level `min_expiry_days` and `max_expiry_days` values limit the contracts loaded: ```python from nautilus_trader.adapters.interactive_brokers import InteractiveBrokersInstrumentProviderConfig provider_config = InteractiveBrokersInstrumentProviderConfig( load_contracts=[ { "symbol": "SPY", "secType": "STK", "exchange": "SMART", "currency": "USD", "build_options_chain": True, }, { "symbol": "ES", "secType": "CONTFUT", "exchange": "CME", "currency": "USD", "build_futures_chain": True, }, ], min_expiry_days=7, max_expiry_days=60, ) ``` When `CONTFUT` has a chain flag, the adapter qualifies it and loads the matching dated futures or futures options. Without a chain flag, it represents IB's continuous future, which IB limits to historical data. It cannot provide live market data or accept orders. See the [IB continuous futures documentation](https://www.interactivebrokers.com/docs/general/contracts/futures/continuous-futures). The adapter also resolves IB `BAG` contracts from Nautilus spread instrument IDs. Request a spread before subscribing to it or trading it: ```python from nautilus_trader.model import InstrumentId spread_id = InstrumentId.from_str("(1)SPY C400_((1))SPY C410.SMART") self.request_instrument(spread_id) ``` Single parentheses mark a positive leg ratio; double parentheses mark a negative ratio. All legs must use the same venue. IB requires a contract ID, ratio, action, and exchange for each combo leg; see [Spreads in the TWS API](https://www.interactivebrokers.com/docs/general/contracts/spread-contracts/twsapi-spreads/spreads-in-the-tws-api). ## Historical data `HistoricalInteractiveBrokersClient` connects with an instrument provider and data client config. Its async Python methods support: - `request_instruments` for contract and instrument discovery. - `request_bars` for one or more bar specifications. - `request_ticks` for historical trade or bid-ask ticks. For `CONTFUT` bar requests, the client omits `end_date_time` because IB rejects an explicit end date. It requests only the first duration segment, anchored to the current time, so returned bars may fall outside the requested start and end range. IB controls historical availability, pacing, bar sizes, durations, and regular-trading-hours filtering. Check the [official historical bars](https://ibkrcampus.com/campus/ibkr-api-page/twsapi-doc/#historical-bars) and [historical time and sales](https://ibkrcampus.com/campus/ibkr-api-page/twsapi-doc/#historical-time-sales) documentation before selecting a request range. ## Order routing and IB attributes Pass `params={"exchange": "..."}` when submitting an order, submitting an order list, or modifying an order to override the cached contract exchange for that command. An empty or omitted value keeps the cached exchange: ```python self.submit_order(order, params={"exchange": "IEX"}) ``` Pass IB-specific order attributes as a tag prefixed with `IBOrderTags:` and followed by a JSON object. The adapter overlays recognized IB order fields and supports price, time, margin, execution, volume, and percent-change conditions: ```python import json ib_attributes = { "ocaGroup": "MY_OCA_GROUP", "ocaType": 1, "conditionsCancelOrder": False, "conditions": [ { "type": "price", "conId": 265598, "exchange": "SMART", "isMore": True, "price": 250.0, "triggerMethod": 0, }, ], } tags = [f"IBOrderTags:{json.dumps(ib_attributes)}"] ``` Pass `tags` to the order factory. OCA type `1` cancels the remaining orders with overfill protection; types `2` and `3` proportionally reduce the remaining orders with and without that protection. See the [IB order reference](https://www.interactivebrokers.com/docs/tws-api/ref/order-class-reference/introduction) for the supported order attributes. ## Configuration ### Data client | Option | Default | Purpose | | -------------------------------- | ------------- | ----------------------------------------------------- | | `host` | `127.0.0.1` | TWS or IB Gateway host. | | `port` | `4002` | TWS or IB Gateway socket port. | | `client_id` | `1` | IB API client ID. | | `use_regular_trading_hours` | `True` | Restrict requests to regular trading hours. | | `market_data_type` | `REALTIME` | Select real-time, frozen, delayed, or delayed frozen. | | `ignore_quote_tick_size_updates` | `False` | Ignore quote updates that change size only. | | `connection_timeout` | `300` seconds | Set the socket connection timeout. | | `request_timeout` | `60` seconds | Set the IB API request timeout. | | `handle_revised_bars` | `False` | Process revised real-time bars. | | `batch_quotes` | `True` | Use `reqMktData` instead of tick-by-tick quotes. | | `instrument_provider` | Default | Configure contract and instrument loading. | ### Execution client | Option | Default | Purpose | | -------------------------------------------- | ------------- | ----------------------------------------------- | | `host` | `127.0.0.1` | TWS or IB Gateway host. | | `port` | `4002` | TWS or IB Gateway socket port. | | `client_id` | `1` | IB API client ID. | | `account_id` | `None` | Select the IB account. | | `connection_timeout` | `300` seconds | Set the socket connection timeout. | | `request_timeout` | `60` seconds | Set the IB API request timeout. | | `fetch_all_open_orders` | `False` | Request all open orders visible to the session. | | `track_option_exercise_from_position_update` | `False` | Infer option exercise from position updates. | | `instrument_provider` | Default | Configure contract and instrument loading. | ## Troubleshooting - Confirm TWS or IB Gateway is running and logged in. - Confirm socket API access is enabled and the configured port matches the application and trading mode. - Confirm the API client ID is not already in use. - Confirm the account has the required market data subscriptions. Use `MarketDataType.DELAYED` only when delayed data is acceptable. For IB error codes and connection settings, see the [official TWS API reference](https://ibkrcampus.com/campus/ibkr-api-page/twsapi-doc/). ## Contributing For additional features or to contribute to the Interactive Brokers adapter, see the [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). # Kraken Source: https://nautilustrader.io/docs/latest/integrations/kraken/ Kraken offers spot and derivatives trading across a wide range of digital assets. This integration connects to Kraken Pro and supports live market data and order execution for Kraken Spot and Kraken Derivatives (Futures). ## Overview The adapter is implemented in Rust with Python bindings and does not require an external Kraken client library. Each data or execution configuration selects a Spot or Futures client through its `product_type`. The main Python components are: - `KrakenDataClientConfig` and `KrakenExecutionClientConfig`: Live client configuration. - `KrakenDataClientFactory` and `KrakenExecutionClientFactory`: Factories used by the trading node builder. - `KrakenSpotHttpClient` and `KrakenFuturesHttpClient`: Lower-level HTTP access for direct requests. The Rust crate also exposes `KrakenSpotWebSocketClient` and `KrakenFuturesWebSocketClient` for lower-level WebSocket access. :::note Most users configure these components through a live trading node and do not need to work directly with the lower-level clients. ::: ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/kraken/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/kraken/examples/) ## Kraken documentation Kraken provides detailed documentation for users: - [Kraken API documentation](https://docs.kraken.com/) - [Kraken Spot REST API](https://docs.kraken.com/exchange/guides/rest/introduction) - [Kraken Derivatives API](https://docs.kraken.com/exchange/guides/futures/introduction) Refer to the Kraken documentation in conjunction with this NautilusTrader integration guide. ## Products The adapter supports these product categories: | Product type | Supported | Notes | | --------------------- | --------- | --------------------------------------------------- | | Spot currency pairs | ✓ | Cash trading and margin on eligible pairs. | | Spot tokenized assets | ✓ | Loaded from Kraken's `tokenized_asset` asset class. | | Futures | ✓ | Instruments returned by the Kraken Futures API. | :::warning Kraken Futures can return instrument definitions that need more than standard-precision mode's nine decimal places. Keep [high-precision mode](../getting_started/installation.md#precision-mode) enabled for Futures. Standard-precision mode continues to support Spot, but Futures clients fail to start or return instruments when any definition cannot be parsed. Futures catalog requests return no partial result and never round, clamp, or omit an unsupported definition. ::: :::note **Single product type per client**: Each Kraken data or execution client is configured for a single `product_type` (`SPOT` or `FUTURES`); a single client does not span both markets. ::: ## Spot instrument fees When both `api_key` and `api_secret` are configured, the adapter loads the account's current maker and taker rates for currency pairs and tokenized assets from Kraken's [`TradeVolume` endpoint](https://docs.kraken.com/api-reference/account-data/get-trade-volume). The API key must include the `Funds permissions - Query` permission, shown as **Query Funds** when creating the key. If `TradeVolume` fails or omits any requested pair, the Spot data or execution client cannot connect. The adapter does not silently publish instruments with public base-tier fees. For pairs without a maker/taker schedule, Kraken returns one fee, which the adapter applies to both maker and taker activity. Without Spot API credentials, the adapter uses the public base-tier rates from `AssetPairs`. These rates can differ from the account's actual fee tier. ## Bar streaming ### Supported intervals The Kraken adapter supports real-time bar (OHLC) streaming for Spot markets via WebSocket. The following intervals are available: | Interval | BarType specification | | ---------- | --------------------- | | 1 minute | `1-MINUTE-LAST` | | 5 minutes | `5-MINUTE-LAST` | | 15 minutes | `15-MINUTE-LAST` | | 30 minutes | `30-MINUTE-LAST` | | 1 hour | `1-HOUR-LAST` | | 4 hours | `4-HOUR-LAST` | | 1 day | `1-DAY-LAST` | | 1 week | `1-WEEK-LAST` | | 15 days | `15-DAY-LAST` | :::note **Futures limitation**: Kraken Futures does not support bar streaming via WebSocket. Use `request_bars()` for historical bar data instead. ::: ### Bar emission latency Kraken's [Spot WebSocket OHLC channel](https://docs.kraken.com/exchange/api-reference/spot-websocket-v2/ohlc) updates the current, incomplete bar on trade events. It does not provide a field that marks a bar as closed. During normal streaming, the adapter buffers the current bar and emits it after receiving an update with a new `interval_begin`. The delay therefore depends on the first trade in the next interval and is not bounded to one bar period when a market has no trades. When the WebSocket message handler stops, the adapter flushes its buffered bars, including a current bar that may still be incomplete. The adapter uses buffering instead of timer-based emission because: - Timer-based emission could miss the final update before the bar closes. - Kraken's updates are not guaranteed to arrive at exact interval boundaries. This favors the latest venue update at the cost of latency. :::tip If bar latency matters for your strategy, consider using trade tick data and aggregating bars locally with `BarAggregator`. ::: :::tip For most use cases, we recommend using `INTERNAL` bar aggregation (subscribing to trades and aggregating bars locally) rather than `EXTERNAL` exchange-provided bars: - Bars are emitted immediately when complete, with no buffering delay. - Consistent behavior across all exchanges, simplifying multi-venue strategies. ::: ## Symbology ### Spot symbol normalization Kraken uses different Bitcoin symbol conventions across their APIs: | Market | Symbol Format | Example | Notes | | ------- | ------------- | ------------------ | ------------------------------------------- | | Spot | `BTC` | `BTC/USD.KRAKEN` | Adapter normalizes XBT to BTC at load time. | | Futures | `XBT` | `PI_XBTUSD.KRAKEN` | Uses Kraken's native XBT format. | :::note Kraken's REST API can return `XBT` for Bitcoin, while its WebSocket v2 API requires `BTC`. The adapter normalizes Spot symbols to `BTC` when loading instruments, whether `XBT` appears as the base currency (for example, `XBT/USD` to `BTC/USD`) or quote currency (for example, `ETH/XBT` to `ETH/BTC`). Futures retain Kraken's native `XBT` format. ::: Kraken also uses `XDG` for Dogecoin in some Spot responses. The adapter normalizes it to `DOGE`, including in quote currency symbols. ### Spot markets NautilusTrader uses normalized, slash-separated symbols for Kraken Spot instruments. The adapter translates them to Kraken's native format internally. **Instrument ID format:** ```python InstrumentId.from_str("BTC/USD.KRAKEN") # Spot BTC/USD InstrumentId.from_str("ETH/USD.KRAKEN") # Spot ETH/USD InstrumentId.from_str("SOL/USD.KRAKEN") # Spot SOL/USD InstrumentId.from_str("BTC/USDT.KRAKEN") # Spot BTC/USDT InstrumentId.from_str("ETH/BTC.KRAKEN") # Spot ETH/BTC (normalized from ETH/XBT) ``` ### Futures markets Kraken Futures instruments use a specific naming convention with prefixes: - `PI_` - Perpetual Inverse contracts (e.g., `PI_XBTUSD`) - `PF_` - Perpetual Fixed-margin contracts (e.g., `PF_XBTUSD`) - `PV_` - Perpetual Vanilla contracts (e.g., `PV_XRPXBT`) - `FI_` - Fixed maturity Inverse contracts (e.g., `FI_XBTUSD_230929`) - `FF_` - Flex futures contracts **Instrument ID format:** ```python InstrumentId.from_str("PI_XBTUSD.KRAKEN") # Perpetual inverse BTC InstrumentId.from_str("PI_ETHUSD.KRAKEN") # Perpetual inverse ETH InstrumentId.from_str("PF_XBTUSD.KRAKEN") # Perpetual fixed-margin BTC ``` ## Data capability ### Subscriptions (real-time) | Data type | Spot | Futures | Notes | | ------------------- | ---- | ------- | ---------------------------------------- | | `QuoteTick` | ✓ | ✓ | Spot ticker; Futures L2 book. | | `TradeTick` | ✓ | ✓ | | | `OrderBookDeltas` | ✓ | ✓ | Spot L2/L3 and Futures L2 updates. | | `OrderBookDepth10` | - | - | Use `OrderBookDeltas` with depth `10`. | | `Bar` | ✓ | - | Spot WS OHLC channel. See bar section. | | `MarkPriceUpdate` | - | ✓ | From futures ticker feed. | | `IndexPriceUpdate` | - | ✓ | From futures ticker feed. | | `FundingRateUpdate` | - | ✓ | Perpetuals only. | | `InstrumentStatus` | - | - | Live clients do not emit status updates. | ### Requests (historical) | Data type | Spot | Futures | Notes | | ---------------------- | ---- | ------- | -------------------------------------- | | `TradeTick` | ✓ | ✓ | | | `Bar` | ✓ | ✓ | | | `OrderBook` (snapshot) | ✓ | ✓ | Via HTTP depth endpoint. | | `FundingRateUpdate` | - | ✓ | Client-side start/end/limit filtering. | ## L3 order book (market-by-order) Kraken exposes Spot per-order book data via the WebSocket v2 `level3` channel at `wss://ws-l3.kraken.com/v2`. This gives venue order IDs, per-order quantities, and true incremental events (`add`, `modify`, `delete`). The adapter hashes each venue order ID into the `u64` `BookOrder.order_id` field used by NautilusTrader. ### Prerequisites L3 subscriptions require Spot API credentials because Kraken's `level3` channel is authenticated. Pass them to `KrakenDataClientConfig`: ```python from nautilus_trader.adapters.kraken import KrakenDataClientConfig config = KrakenDataClientConfig( api_key="YOUR_KEY", api_secret="YOUR_SECRET", ) ``` Then subscribe with `book_type=BookType.L3_MBO`: ```python from nautilus_trader.model import BookType await client.subscribe_book_deltas( instrument_id=instrument_id, book_type=BookType.L3_MBO, depth=1000, # valid: 10, 100, 1000 ) ``` Valid depths are `10`, `100`, and `1000`. A `depth` of `0` uses `1000`. ### CRC32 checksum validation By default, the adapter validates the CRC32 checksum on each L3 snapshot and update when Kraken provides one. On mismatch, it emits a `Clear` delta, clears local L3 state, refreshes the auth token, and resubscribes so Kraken sends a fresh snapshot. To disable validation for benchmarking: ```python config = KrakenDataClientConfig( api_key="...", api_secret="...", validate_l3_checksum=False, ) ``` ### Storage recommendations `OrderBookDelta` already carries `order_id: u64` in its Arrow schema, so L3 data is stored identically to L2 in the `ParquetDataCatalog`. L3 generates significantly more events per instrument than L2. Recommended settings: - Lower chunk size (e.g. `chunk_size=50_000`) for faster parallel reads. - Enable `zstd` compression in catalog config. - Use per-instrument path partitioning (enabled by default). ## Orders capability ### Order types | Order type | Spot | Futures | Notes | | ---------------------- | ---- | ------- | ------------------------------------------ | | `MARKET` | ✓ | ✓ | Immediate execution at market price. | | `LIMIT` | ✓ | ✓ | Execution at specified price or better. | | `STOP_MARKET` | ✓ | ✓ | Conditional market order (stop-loss). | | `MARKET_IF_TOUCHED` | ✓ | ✓ | Conditional market order (take-profit). | | `STOP_LIMIT` | ✓ | ✓ | Conditional limit order (stop-loss-limit). | | `LIMIT_IF_TOUCHED` | ✓ | ✓ | Maps to `take_profit` with `limit_price`. | | `TRAILING_STOP_MARKET` | ✓ | - | Trailing stop with `trailing_offset`. | | `TRAILING_STOP_LIMIT` | ✓ | - | Trailing stop-limit with `limit_offset`. | ### Time in force | Time in Force | Spot | Futures | Notes | | ------------- | ---- | ------- | --------------------------------------------------- | | `GTC` | ✓ | ✓ | Good Till Canceled. | | `GTD` | ✓ | - | Good Till Date (Spot only, requires `expire_time`). | | `IOC` | ✓ | ✓ | Immediate or Cancel. | | `FOK` | ✓ | - | Spot limit orders only. | :::note **Market orders** are inherently immediate and do not support time-in-force. `IOC` only applies to limit-type orders. ::: ### Execution instructions | Instruction | Spot | Futures | Notes | | ---------------- | ---- | ------- | ---------------------------------------------------------- | | `post_only` | ✓ | ✓ | Available for limit orders. | | `reduce_only` | ✓ | ✓ | Spot requires a margin account and resolved leverage. | | `quote_quantity` | ✓ | - | Spot only. Volume in quote currency (`viqc`); REST routed. | | `display_qty` | ✓ | - | Spot only. Iceberg orders (`displayvol`). | ### Trigger types Conditional orders (stop, take-profit, trailing stop) support a trigger price reference on Spot: | Trigger Type | Spot | Futures | Notes | | ------------- | ---- | ------- | --------------------------- | | `LAST_PRICE` | ✓ | ✓ | Default. Last traded price. | | `INDEX_PRICE` | ✓ | ✓ | Broader market index price. | | `MARK_PRICE` | - | ✓ | Futures only. | :::note The adapter rejects unsupported trigger types (e.g., `BID_ASK`) at submission time rather than silently coercing them. ::: ### Batch operations | Operation | Spot | Futures | Notes | | ------------ | ---- | ------- | ------------------------------------------------------ | | Batch Submit | ✓ | ✓ | Spot chunks at 15 orders. Futures chunks at 10. | | Batch Modify | - | ✓ | Futures HTTP method only. Execution sends one command. | | Batch Cancel | ✓ | ✓ | Auto-chunks into batches of 50. | :::note **Cancel all orders**: - With no side filter, Spot cancels all open orders across all symbols, while Futures cancels all orders for the requested instrument. - With a side filter, both clients select matching cached orders for the requested instrument and cancel them individually. ::: ### Position management | Feature | Spot | Futures | Notes | | ---------------- | ---- | ------- | --------------------------------------------------- | | Query positions | ✓ | ✓ | Spot margin via `OpenPositions`; spot cash opt-in. | | Position mode | - | - | Single position per instrument. | | Leverage control | ✓ | - | Spot tiers; per-order `params={"leverage": N}`. | | Margin mode | ✓ | ✓ | Spot/Futures cross margin; no isolated spot margin. | ### Order querying | Feature | Spot | Futures | Notes | | -------------------- | ---- | ------- | -------------------------------------------- | | Query open orders | ✓ | ✓ | List all active orders. | | Query order history | ✓ | ✓ | Historical order data with pagination. | | Order status updates | ✓ | ✓ | Real-time order state changes via WebSocket. | | Trade history | ✓ | ✓ | Execution and fill reports. | ### Contingent orders | Feature | Spot | Futures | Notes | | ------------------ | ---- | ------- | ------------------------------------------- | | Linked order lists | - | - | Submitted lists contain independent orders. | | OCO orders | - | - | *Not supported*. | | Bracket orders | - | - | *Not supported*. | | Conditional orders | ✓ | ✓ | Stop and take-profit orders. | ### Maker Protection (Futures) Kraken Futures applies [Maker Protection](https://docs.kraken.com/exchange/guides/futures/maker-protection) on selected markets: placements and edits that could take liquidity are held for the market's configured window before reaching the matching engine. The classification is by order type, so any order not marked `post_only` is held even when it would in fact have rested. Post-only placements and all cancellations are never held, and no held-order state is exposed on any API. The venue applies the hold per market to every client; the adapter decodes the per-market window (`makerProtectionMillis`) on the raw venue instrument model and exposes no configuration for it. Order-state handling accounts for the held-order semantics: - A cancel acknowledged while an order is held is not terminal. The order is released as IOC and can still fill. Fills and terminal states are driven by venue order updates, never by the cancel acknowledgement itself. - An order that cannot trade after such a release is reported with the venue status `iocWouldNotExecute` on REST (`IOC_WOULD_ENTER_BOOK` on market data), which the adapter treats as a terminal rejection. On the order-update feed the same outcome arrives as a terminal cancellation whose venue reason the adapter preserves. - A released order cancels a resting order of the same account it would match, overriding the configured self-trade strategy. The resting order is reported canceled with reason `CANCELLED_BY_SELF_TRADE`. The adapter closes an order only once the venue's fills for it are accounted. #### Order-update feed A removal with `is_cancel=true` and reason `partial_fill` discards the remainder and is terminal (a converted hold, or any IOC-style order). The delta carries the venue's cumulative filled. - For a tracked order, the adapter closes from the feed once the fills stream has accounted that quantity. A fill still in flight is never orphaned, and a tracked order is not left open after its fills are accounted. - For a removal it cannot match, the adapter skips and converges through reconciliation. | Unmatched removal | Reason | | ----------------------------- | ----------------------------- | | Cancel-only message | Carries no cumulative filled. | | No resolvable client order ID | Cannot match a tracked order. | #### Reconciliation A held order never reaches the book, so it is absent from `/openorders`. Mass status, open-only report runs, and targeted single-order queries consult `POST /orders/status` before treating the order as missing. That window reports orders that are open or were filled or canceled in the last 5 seconds. A hold that fills after a cancel acknowledgement reconciles to its true terminal state with the venue's cumulative filled, not a premature cancellation. ## Order routing (Spot) The Spot execution client routes order submission, modification, cancellation, and batch cancellation through Kraken's authenticated WebSocket v2 trade channel by default. It falls back to REST when the WebSocket is inactive. Set `use_ws_trade=False` on `KrakenExecutionClientConfig` to route these operations through REST. ### Order shapes routed via REST Kraken's [Spot WebSocket v2 `add_order` method](https://docs.kraken.com/exchange/api-reference/spot-websocket-v2/add_order) supports these shapes, but the adapter routes them through REST: | Shape | Adapter behavior | | -------------------------- | ----------------------------------------------------------------- | | `FOK` time in force | The WebSocket parameter builder does not encode `FOK`. | | Trailing stop / stop-limit | The WebSocket parameter builder does not encode trailing offsets. | | Iceberg (`display_qty`) | The WebSocket parameter builder does not encode iceberg orders. | | Quote-quantity orders | WS supports non-margin buy market orders; the adapter uses REST. | Mixed-symbol order lists also use REST because Kraken's WebSocket `batch_add` request requires one shared symbol. Unsupported trigger references fall back to the REST path, which rejects them locally before sending a request to Kraken. The per-call `params={"use_ws_trade": False}` override forces a single command through REST regardless of the configured default. Set it on `SubmitOrder`, `ModifyOrder`, `CancelOrder`, `SubmitOrderList`, or `BatchCancelOrders`. ### WebSocket request timeout When a WebSocket round-trip exceeds `ws_request_timeout_secs` (default `5`), the venue outcome remains unknown. Submit, modify, cancel, and batch-add requests remain in flight without a terminal rejection. The dispatcher retains the request ID so a delayed matching response can still apply the normal success or definitive rejection handling. Submit and batch-add timeouts also send a best-effort compensating cancel over the same WebSocket for every affected client order ID. This cancel limits exposure if Kraken accepted the order but delayed its response. It does not replace the unknown outcome with local terminal state. Stream updates and the live execution reconciliation engine resolve orders when no matching response arrives. Targeted status queries can resolve modify or cancel requests that already have a venue order ID. A matching response or execution client shutdown retires the retained request correlation. :::tip Set `ws_request_timeout_secs` comfortably above your observed round-trip latency. A premature timeout can send a compensating cancel for a submit or batch add that Kraken accepted. ::: ### WebSocket order-routing options `KrakenExecutionClientConfig` exposes: | Option | Default | Description | | ------------------------- | ------- | ----------------------------------------------------- | | `use_ws_trade` | `True` | Route orders via WS when the trade channel is active. | | `ws_request_timeout_secs` | `5` | Seconds to wait for a Spot WS order response. | ## Reconciliation The Kraken adapter provides reconciliation capabilities for both Spot and Futures markets, allowing traders to synchronize their local state with the exchange state at startup or during operation. ### Spot reconciliation **Order status reports:** - Open orders: Fetches all currently active orders. - Closed orders: Fetches historical orders with pagination support. - Time-bounded queries: Supports filtering by start/end timestamps. **Fill reports:** - Trade history: Fetches execution history with pagination. - Time-bounded queries: Supports filtering by start/end timestamps. - All fill types: Market, limit, and conditional order fills. **Account balances:** - Wallet balances: Fetched from `POST /0/private/BalanceEx`, which reports both the total and the held (`hold_trade`) amount per asset. The held amount populates `AccountBalance.locked`, so `free` excludes funds Kraken has reserved against resting orders. For accounts with a credit line, net credit (`credit - credit_used`) is included in `AccountBalance.total`, so `free` matches Kraken's available balance of `balance + credit - credit_used - hold_trade`. **Margin position reports** (when `spot_account_type=Margin`): - Open positions: Fetched from `POST /0/private/OpenPositions` and aggregated by (pair, side) into `PositionStatusReport` entries. - Synthetic FLAT cleanup: If the local cache has an open spot margin position that no longer appears on the venue (Kraken omits closed positions from `OpenPositions`), the adapter emits a synthetic FLAT report on the next position-check tick so the engine reconciles to closed. - Margin balances: `POST /0/private/TradeBalance` is called alongside the account-state refresh; used margin populates `MarginBalance.initial`, while equity and free margin populate the summary balance (see Spot margin trading). ### Futures reconciliation **Order status reports:** - Open orders: Fetches all currently active futures orders. - Historical orders: Fetches closed and filled orders when `open_only=False`. - Order events: Full order lifecycle history via `/api/history/v2/orders` endpoint. **Fill reports:** - Fill history: Fetches all execution reports. - Time filtering: Client-side filtering by start/end timestamps (parses RFC3339 timestamps). - All fill types: Maker and taker fills with fee information. **Position status reports:** - Open positions: Fetches all active futures positions. - Real-time data: Includes unrealized funding, average price, and position size. :::note **Futures time filtering**: The Kraken Futures fills endpoint does not support server-side time range filtering. The adapter implements client-side filtering by parsing `fillTime` fields and comparing against requested start/end timestamps. ::: ### Spot position reports (cash mode) In cash mode, the Kraken adapter can optionally report wallet balances as position status reports for spot instruments. This feature is disabled by default and must be explicitly enabled via configuration. Margin-mode accounts should leave it disabled and rely on `OpenPositions` instead (see Spot margin trading). **How it works:** - When enabled, wallet balances are converted to `PositionStatusReport` objects. - Positive balances are reported as `LONG` positions. - Only instruments matching the configured quote currency are reported (default: `USDT`). - This prevents duplicate reports when the same asset is available with multiple quote currencies (e.g., BTC/USD, BTC/USDT, BTC/EUR). **Configuration:** ```python from nautilus_trader.adapters.kraken import KrakenExecutionClientConfig from nautilus_trader.model import AccountId exec_config = KrakenExecutionClientConfig( account_id=AccountId.from_str("KRAKEN-001"), api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", use_spot_position_reports=True, spot_positions_quote_currency="USDT", # Default ) ``` :::warning **Use with caution**: Enabling spot position reports may lead to unintended behavior if your strategy is not designed to handle spot positions. For example, a strategy that expects to close positions may attempt to sell your wallet holdings. ::: ## Spot margin trading Kraken Spot supports leveraged trading on selected pairs. Per-pair availability and the valid leverage tiers are advertised by Kraken on the instruments endpoint as `AssetPairInfo.leverage_buy` and `leverage_sell`; the adapter caches these at instrument-load time and validates the requested tier before order submission. Margin trading is enabled per-execution-client via `spot_account_type`, with per-order `leverage` params. ### Configuration ```python from nautilus_trader.adapters.kraken import KrakenExecutionClientConfig from nautilus_trader.model import AccountId from nautilus_trader.model import AccountType exec_config = KrakenExecutionClientConfig( account_id=AccountId.from_str("KRAKEN-001"), api_key="YOUR_API_KEY", api_secret="YOUR_API_SECRET", spot_account_type=AccountType.MARGIN, default_leverage=3, # Optional config-level default margin_balance_asset="ZGBP", # Optional summary-display asset ) ``` `margin_balance_asset` controls only the denomination of the account-summary metrics returned by Kraken's `TradeBalance` endpoint (equity, free margin, used margin, etc.). Per-position figures from `OpenPositions` are always in the traded pair's quote currency. ### Per-order leverage Override the configured default on a single order via `params`: ```python order = strategy.order_factory.limit( instrument_id=BTC_USD, order_side=OrderSide.BUY, quantity=Quantity.from_str("0.01"), price=Price.from_str("50000.00"), params={"leverage": 5}, ) ``` The adapter validates the requested tier against `AssetPairInfo.leverage_buy` / `leverage_sell` for the pair before submitting; an invalid tier produces an `OrderDenied` event and never hits the venue. ### Reduce-only Margin orders can carry `reduce_only=True` so they reduce an existing position without opening a larger opposite position. Set `spot_account_type=Margin` and supply either `default_leverage` or per-order `params={"leverage": N}`. The adapter denies cash orders with `reduce_only` before sending them to Kraken. ### Account state When `spot_account_type=Margin`, the execution client calls Kraken's `TradeBalance` endpoint during account refreshes. The live account state uses: - Equity (`e`) and free margin (`mf`) for the balance denominated by `margin_balance_asset`. - Used margin (`m`) for `MarginBalance.initial`. Maintenance margin is zero because Kraken does not return a separate maintenance-margin amount. The lower-level `KrakenSpotHttpClient` methods `request_margin_metrics()` and `request_account_state_with_metrics()` return the full `TradeBalance` metrics dictionary for direct consumers. The live execution client does not attach that dictionary to `AccountState.info`. ### Position reconciliation Open spot margin positions are surfaced via `POST /0/private/OpenPositions` on each `position_check_interval_secs` tick. Closed positions on the venue that still appear open in the local cache are reconciled to FLAT on the next sweep. This path is independent of `use_spot_position_reports` (which is wallet-derived, cash-mode-only). ## Funding rates The adapter receives funding rate data from the [Futures ticker](https://docs.kraken.com/exchange/api-reference/futures-websocket/ticker) WebSocket feed, which provides `relative_funding_rate` and `next_funding_rate_time` for perpetual futures. The `interval` field on `FundingRateUpdate` is `None` for Kraken because the ticker feed does not include a funding interval field and the Kraken API documentation does not specify a fixed funding period. ## Rate limiting Each Kraken HTTP client applies an adapter-side request throttle. The default is five requests per second and `max_requests_per_second` can override it. This is a request-count throttle, not a complete model of Kraken's endpoint costs or account-tier budgets. Kraken applies different venue limits to Spot and Futures: - [Spot REST rate limits](https://docs.kraken.com/exchange/guides/rest/ratelimits) use a tier-dependent call counter. Ledger and trade history calls add `2`, most other REST calls add `1`, and order management uses a separate trading limiter. - [Derivatives rate limits](https://docs.kraken.com/exchange/guides/futures/ratelimits) use endpoint costs and separate budgets for `/derivatives` and `/history` paths. The current Spot REST call-counter limits are: | Spot tier | Maximum counter | Counter decay | | ------------ | --------------- | ------------- | | Starter | 15 | 0.33/second | | Intermediate | 20 | 0.5/second | | Pro | 20 | 1/second | If the adapter's fixed request rate is too high for the endpoint mix and account tier, Kraken can still reject or throttle requests. ### Reconciliation interval guidance The execution engine's `open_check_interval_secs` and `position_check_interval_secs` settings create sustained private REST API load. Short intervals can exhaust Kraken's venue budgets even when the adapter stays below its configured requests-per-second throttle. Use conservative intervals as a starting point, especially for a Spot Starter account: ```python exec_engine = LiveExecutionEngineConfig( reconciliation=True, open_check_interval_secs=30.0, # Conservative Spot Starter-tier starting point position_check_interval_secs=120.0, ) ``` Tune these values for the account tier, enabled reconciliation checks, and other clients using the same API key. If Kraken returns `EAPI:Rate limit exceeded`, increase the intervals or reduce `max_requests_per_second`. ## Configuration The product type for each client is specified via the `product_type` option. ### Data client configuration options | Option | Default | Description | | ------------------------- | --------- | -------------------------------------------------------------- | | `product_type` | `SPOT` | Product type for this client (`SPOT` or `FUTURES`). | | `environment` | `LIVE` | Trading environment (`LIVE` or `DEMO`); demo only for Futures. | | `api_key` | `None` | API key for Spot L3 data and account fee rates. | | `api_secret` | `None` | API secret for Spot L3 data and account fee rates. | | `base_url` | `None` | Override for the Kraken REST base URL. | | `ws_public_url` | `None` | Override for the public WebSocket URL. | | `ws_private_url` | `None` | Override for the private WebSocket URL. | | `ws_l3_url` | `None` | Override for the Spot L3 WebSocket URL. | | `validate_l3_checksum` | `True` | Validate Kraken Spot L3 checksums and resync on mismatch. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `timeout_secs` | `30` | HTTP request timeout in seconds. | | `heartbeat_interval_secs` | `30` | WebSocket heartbeat interval in seconds. | | `ws_idle_timeout_ms` | `10,000` | Data-silence timeout for the Spot v2 WebSocket; `0` disables. | | `max_requests_per_second` | `None` | Per-client request throttle; default is 5 req/s. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | ### Execution client configuration options | Option | Default | Description | | ------------------------------- | --------- | --------------------------------------------------------------------- | | `account_id` | required | Account ID for the Kraken account. | | `api_key` | required | Kraken API key. | | `api_secret` | required | Kraken API secret. | | `product_type` | `SPOT` | Product type for this client (`SPOT` or `FUTURES`). | | `environment` | `LIVE` | Trading environment (`LIVE` or `DEMO`); demo only for Futures. | | `base_url` | `None` | Override for the Kraken REST base URL. | | `ws_url` | `None` | Override for the Kraken WebSocket URL. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket transports. | | `timeout_secs` | `30` | HTTP request timeout in seconds. | | `heartbeat_interval_secs` | `30` | WebSocket heartbeat interval in seconds. | | `auth_timeout_secs` | `None` | Futures WebSocket auth timeout; `None` uses the client default. | | `max_requests_per_second` | `None` | Per-client request throttle; default is 5 req/s. | | `max_retries` | `3` | Maximum retry attempts for retryable REST requests. | | `spot_account_type` | `CASH` | Account type for spot trading; `MARGIN` enables leverage and reports. | | `default_leverage` | `None` | Default spot margin leverage sent as `"N:1"` when set. | | `use_spot_position_reports` | `False` | Report wallet balances as positions; cash mode only. | | `spot_positions_quote_currency` | `"USDT"` | Quote currency filter for spot wallet position reports. | | `margin_balance_asset` | `None` | Summary asset for `TradeBalance`; `None` defaults to `ZUSD`. | | `use_ws_trade` | `True` | Use Spot WebSocket v2 for order operations when active. | | `ws_request_timeout_secs` | `5` | Spot WebSocket order response timeout. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | For spot margin, `default_leverage` applies when an order has no per-order leverage param. `margin_balance_asset` only changes the `TradeBalance` summary denomination; per-position figures remain in the pair's quote currency. ### Demo environment setup To test with Kraken Futures demo (paper trading): 1. Sign up at [Kraken Futures demo](https://demo-futures.kraken.com) and generate API credentials. 1. Set environment variables with your demo credentials: - `KRAKEN_FUTURES_DEMO_API_KEY` - `KRAKEN_FUTURES_DEMO_API_SECRET` 1. Read the credentials and pass them to `KrakenExecutionClientConfig`, then set `environment=KrakenEnvironment.DEMO` and `product_type=KrakenProductType.FUTURES`. The [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/kraken/) show the complete demo and live `LiveNode` configurations. ### Production configuration Use `KrakenDataClientConfig` with `KrakenDataClientFactory`, and use `KrakenExecutionClientConfig` with `KrakenExecutionClientFactory`. The Python examples show the complete `LiveNode.builder(...)` configuration for data and execution clients. ### API credentials Live-node configuration objects do not read credential environment variables automatically. Pass `api_key` and `api_secret` explicitly to `KrakenExecutionClientConfig` and, for Spot L3 data or account-specific instrument fees, to `KrakenDataClientConfig`. Public market data does not require credentials. The lower-level Python HTTP and WebSocket clients load the following variables when their credential arguments are omitted. Rust applications can use `KrakenCredential::from_env_spot()` or `KrakenCredential::from_env_futures(demo)` to load them before constructing live-node configs. | Environment Variable | Description | | -------------------------------- | ---------------------------------------- | | `KRAKEN_SPOT_API_KEY` | API key for Kraken Spot live trading. | | `KRAKEN_SPOT_API_SECRET` | API secret for Kraken Spot live trading. | | `KRAKEN_FUTURES_API_KEY` | Kraken Futures live API key. | | `KRAKEN_FUTURES_API_SECRET` | Kraken Futures live API secret. | | `KRAKEN_FUTURES_DEMO_API_KEY` | API key for Kraken Futures (demo). | | `KRAKEN_FUTURES_DEMO_API_SECRET` | API secret for Kraken Futures (demo). | :::note **Demo environment**: Only Kraken Futures offers a demo environment (`https://demo-futures.kraken.com`) for testing without real funds. Kraken Spot does not have a demo or testnet environment. ::: :::tip Use environment variables to store credentials, then pass their values into live-node configuration at the application boundary. ::: Authentication errors are reported when a private client connects or performs a private operation. Required permissions depend on the requested data or trading operation. ## Contributing :::info For additional features or to contribute to the Kraken adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Lighter Source: https://nautilustrader.io/docs/latest/integrations/lighter/ [Lighter](https://lighter.xyz) is a decentralized central-limit-order-book exchange for spot and perpetual futures. The venue settles through an Ethereum zero-knowledge rollup, while matching and sequencing run off-chain. The adapter also supports the Robinhood Chain deployment of the Lighter protocol. The NautilusTrader Lighter adapter is implemented by the `nautilus-lighter` crate. It provides Rust data and execution clients, typed REST and WebSocket models, and an in-tree L2 transaction signer for the venue's Schnorr / ECgFp5 signing flow. Measured L2 signing cost, including a comparison with the official Go SDK, is recorded in [`crates/adapters/lighter/benches/BENCHMARKS.md`](../../crates/adapters/lighter/benches/BENCHMARKS.md). Absolute numbers vary by machine, so only same-machine deltas are meaningful. ## Overview The main components are: - `LighterRawHttpClient`: low-level REST client for the public and account endpoints. - `LighterHttpClient`: domain client which parses instruments, trades, books, orders, and account state into Nautilus model types. - `LighterWebSocketClient`: reconnecting WebSocket client for public market and private account streams. - `LighterDataClient`: Nautilus data client for instruments, trades, quotes, and L2 MBP books. - `LighterExecutionClient`: Nautilus execution client for account streams, order submission, modification, cancellation, and reconciliation reports. - `LighterDataClientFactory` and `LighterExecutionClientFactory`: live-node factory wiring. The Python surface is intentionally narrow. The Python extension exposes configuration, deployment and environment selection, factory classes, and integrator revocation; data and execution clients are consumed through the Rust trait surface. ## Examples Python examples live in [`examples/live/lighter/`](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/lighter/) and run out of the box: settings live in module-level constants at the top of each file, and running a script connects and starts immediately. Edit `LIGHTER_DEPLOYMENT` and `LIGHTER_ENVIRONMENT` to select a deployment and environment. The execution tester places real orders by default (`dry_run=False`), stated in a warning at the top of the module. From the repository root: ```bash uv run --project python --no-sync python examples/live/lighter/data_tester.py uv run --project python --no-sync python examples/live/lighter/exec_tester.py ``` Rust examples live under `crates/adapters/lighter/examples/`. Both testers connect when run. The execution tester has `DRY_RUN = false` and selects Lighter Mainnet in its source, so the command below can submit live orders: ```bash cargo run --example lighter-data-tester --package nautilus-lighter --features examples cargo run --example lighter-exec-tester --package nautilus-lighter --features examples ``` :::warning Examples can connect to live venues. Execution examples with live order flow enabled can submit orders when pointed at a funded account on either mainnet deployment. Review the selected instrument, quantity, and environment before running them. ::: ### Emergency account cleanup `cargo run --bin lighter-flatten -p nautilus-lighter` is a convenience command that cancels open orders and closes positions for the selected deployment account. It submits Lighter's account-wide immediate cancellation, reads one position snapshot, and submits reduce-only IOC closes for the positions in that snapshot. :::warning Stop other writers for the account before running this command. Cleanup is account-wide, not strategy-scoped, so review the active account and positions first. ::: The command does not confirm the requests or retry until the account is flat. A successful exit means the cancellation and discovered close requests were submitted without a known error. Check the account state after it exits and rerun the command if anything remains. One run can submit at most 15 position closes because the account-wide cancellation uses one slot in its 16-transaction nonce window. An incomplete position snapshot or a request or submission failure returns an error. Set `LIGHTER_DEPLOYMENT` to `lighter` or `robinhood` and `LIGHTER_ENVIRONMENT` to `mainnet` or `testnet`; omitted selectors default to Lighter Mainnet and select the matching credential namespace. ## Product support | Product type | Data feed | Trading | Notes | | ----------------- | --------- | ------- | ------------------------------------------------------------ | | Spot | ✓ | ✓ | Spot markets using Lighter market indexes 2048-4094. | | Perpetual futures | ✓ | ✓ | Linear perpetual markets using Lighter market indexes 0-254. | | Dated futures | - | - | *Not supported*. | | Options | - | - | *Not supported*. | ## Limitations The current adapter scope is deliberately narrower than the venue's full transaction surface: - Grouped order lists, OCO/OTO groups, brackets, TWAP, trailing stops, and iceberg display size are not implemented. Batch submit does not use `CreateGroupedOrders`. - Order-list submit and batch cancel fan out independent transactions sequentially over WebSocket. Both operations are capped at 15 transactions per command. - The execution client implements `CancelAllOrders` from cached open orders for the requested instrument. It does not use the native account-wide transaction because that can affect unrelated markets. - Spot trading supports market and limit orders. Conditional stop-loss and take-profit orders are limited to perpetual markets. - Account state and position reports come from private WebSocket streams. `query_account` and position status generation replay the latest cached stream state. - Unscoped order reconciliation is bounded to configured or observed active markets to avoid a full venue-wide fan-out under the standard REST quota. ## Symbology Lighter identifies markets by numeric `market_index` values. The adapter bootstraps the mapping from `GET /api/v1/orderBookDetails`, then converts the raw venue symbol into a Nautilus `InstrumentId`. | Deployment product | Nautilus symbol format | Example | Notes | | ------------------- | --------------------------------------- | ---------------------------------- | ------------------------ | | Lighter perpetual | `{BASE}-PERP.LIGHTER` | `BTC-PERP.LIGHTER` | Raw venue symbol `BTC`. | | Lighter spot | `{BASE}/{QUOTE}-SPOT.LIGHTER` | `ETH/USDC-SPOT.LIGHTER` | Raw symbol `ETH/USDC`. | | Robinhood perpetual | `{BASE}-PERP.LIGHTER_ROBINHOOD` | `SNDK-PERP.LIGHTER_ROBINHOOD` | Raw venue symbol `SNDK`. | | Robinhood spot | `{BASE}/{QUOTE}-SPOT.LIGHTER_ROBINHOOD` | `SNDK/USDG-SPOT.LIGHTER_ROBINHOOD` | Raw symbol `SNDK/USDG`. | The suffix separates spot and perpetual listings. Outbound requests strip it and use the cached `market_index`; spot symbols retain the venue pair. ## Deployments and environments | Deployment | Environment | REST URL | WebSocket URL | L2 signing chain ID | Settlement | Default venue | | ---------- | ----------- | ------------------------------------- | ------------------------------------------ | ------------------- | ---------- | ------------------- | | Lighter | Mainnet | `https://mainnet.zklighter.elliot.ai` | `wss://mainnet.zklighter.elliot.ai/stream` | 304 | USDC | `LIGHTER` | | Lighter | Testnet | `https://testnet.zklighter.elliot.ai` | `wss://testnet.zklighter.elliot.ai/stream` | 300 | USDC | `LIGHTER` | | Robinhood | Mainnet | `https://api.rh.lighter.xyz` | `wss://api.rh.lighter.xyz/stream` | 466324 | USDG | `LIGHTER_ROBINHOOD` | | Robinhood | Testnet | `https://api.rh-testnet.lighter.xyz` | `wss://api.rh-testnet.lighter.xyz/stream` | 300 | USDG | `LIGHTER_ROBINHOOD` | These chain IDs are Lighter L2 signing-domain values, not EVM network chain IDs. Use `LighterDeployment::Lighter` or `LighterDeployment::Robinhood` to select the protocol deployment. Use `LighterEnvironment::Mainnet` or `LighterEnvironment::Testnet` to select its environment. The deployment and environment together control the default URLs, chain ID, settlement currency, default venue, and attribution policy. Robinhood Testnet and Lighter Testnet both use chain ID 300, so the adapter does not infer deployment behavior from the numeric chain ID. URL overrides are available for private gateways and local test fixtures. They replace only the transport endpoint. The selected deployment and environment still control transaction signing, settlement currency, and attribution policy. ### Custom venue identity Set `venue` on both data and execution configs when separate Lighter-protocol endpoints must have distinct Nautilus identities. This scopes instruments, cache entries, message topics, socket state, and execution routing without changing the selected deployment's protocol behavior. `ClientId` remains the name supplied when registering each client. The shared factory name remains `LIGHTER` for compatibility. When routing a Robinhood client by `ClientId`, register it as `LIGHTER_ROBINHOOD`; the Rust and Python examples derive this name from `LIGHTER_DEPLOYMENT`. Explicit custom client names remain supported. The execution `account_id` issuer must equal the resolved venue because Nautilus routes account commands by issuer. For example, venue `LIGHTER_RH_ALT` requires an account ID such as `LIGHTER_RH_ALT-001`. A custom venue does not enable a custom chain ID or custom attribution. ## Account and API key setup Public market data does not require an account. Private account streams and execution require an account index, an API key index, and the API private key from the same deployment and environment. Each row below has a separate account and API-key namespace: | Deployment | Environment | Account and API key page | Account issuer | Credential prefix | | ---------- | ----------- | --------------------------------------------------------------- | ------------------- | ----------------------------- | | Lighter | Mainnet | [Lighter Mainnet](https://app.lighter.xyz/apikeys) | `LIGHTER` | `LIGHTER_*` | | Lighter | Testnet | [Lighter Testnet](https://testnet.app.lighter.xyz/apikeys) | `LIGHTER` | `LIGHTER_TESTNET_*` | | Robinhood | Mainnet | [Robinhood Mainnet](https://robinhoodchain.lighter.xyz/apikeys) | `LIGHTER_ROBINHOOD` | `LIGHTER_ROBINHOOD_*` | | Robinhood | Testnet | [Robinhood Testnet](https://rhctestnet.lighter.xyz/apikeys) | `LIGHTER_ROBINHOOD` | `LIGHTER_ROBINHOOD_TESTNET_*` | Do not mix an account index or API key from one row with another. This also applies to the two testnets even though both use L2 signing chain ID 300. 1. Open the account page for the target deployment, sign in with the account used there, and create or select the trading account. Select the intended sub-account before generating its API key. 1. Follow Lighter's [account-index lookup](https://apidocs.lighter.xyz/docs/get-started#find-your-account-index) against the target deployment's REST URL. This example selects Robinhood Mainnet; replace the URL with the exact value from the [deployment table](#deployments-and-environments) for another row: ```bash LIGHTER_SETUP_API_URL="https://api.rh.lighter.xyz" LIGHTER_SETUP_L1_ADDRESS="0xYOUR_ETHEREUM_ADDRESS" curl -sS --get \ "${LIGHTER_SETUP_API_URL}/api/v1/accountsByL1Address" \ --data-urlencode "l1_address=${LIGHTER_SETUP_L1_ADDRESS}" ``` Read the `index` from the required entry in `sub_accounts`. A wallet can own a main account and several sub-accounts, each with a separate account index and API keys. 1. On the selected account's API key page, choose **Generate API Key**. Use an unused index from `4` through `254`; Lighter's [API key documentation](https://apidocs.lighter.xyz/docs/api-keys) reserves indexes `0-3` for its interfaces, while `255` is an API query sentinel. 1. Save the generated private key before closing the dialog. Lighter does not display it again. 1. Configure `account_index`, `api_key_index`, and `private_key` directly, or use the environment variables listed in [API credentials](#api-credentials). The Nautilus `account_id` is separate from the venue account index: use an issuer from the table above, such as `LIGHTER-001` or `LIGHTER_ROBINHOOD-001`. 1. Confirm that the target deployment recognizes the selected account and key indexes: ```bash LIGHTER_SETUP_ACCOUNT_INDEX="123456" LIGHTER_SETUP_API_KEY_INDEX="4" curl -sS --get \ "${LIGHTER_SETUP_API_URL}/api/v1/apikeys" \ --data-urlencode "account_index=${LIGHTER_SETUP_ACCOUNT_INDEX}" \ --data-urlencode "api_key_index=${LIGHTER_SETUP_API_KEY_INDEX}" ``` A successful response has `"code": 200` and lists the selected key. This public lookup confirms the indexes, but it does not expose or validate the private key. :::warning Lighter API keys authorize trading, private account access, and some withdrawal operations. Store the private key in a secret manager or protected environment configuration. Do not commit it to a repository or share it in logs. ::: ## Integrator attribution On Lighter Mainnet, create and modify transactions from Plus and Premium accounts carry the NautilusTrader integrator account index in `L2TxAttributes` to measure adapter usage. Maker and taker integrator fees are zero. The execution client submits the required **zero-fee** `ApproveIntegrator` approval during startup when the account is Plus or Premium and the API key is not maker-only. All other account tiers, sessions without an account snapshot, Lighter Testnet, and both Robinhood environments leave `L2TxAttributes` empty and omit `ApproveIntegrator` during startup. Robinhood Mainnet uses the account-level `NAUTILUS` referral code instead. During startup, the execution client authenticates with the configured L2 API key and applies `NAUTILUS` to the account's public L1 address. Selecting Robinhood Mainnet opts the account into this attribution. Application failures log a warning and do not block trading. Robinhood Testnet performs no referral attribution. Custom venue names do not change either policy: attribution is evaluated from the typed deployment, environment, and fetched account tier. On Lighter Mainnet, maker-only API keys cannot submit `ApproveIntegrator`. The execution client detects these keys and skips automatic approval. Approval is account-scoped, so a non-maker-only key on the same account must approve the integrator before a maker-only key can trade through the adapter. ### Revoking the approval Use revocation as cleanup when leaving the adapter on a Lighter Mainnet account that has previously approved the integrator, including a Standard account approved by an older adapter version. It sends `ApproveIntegrator` with `approval_expiry = 0` and zero max fees. The next Plus or Premium Lighter Mainnet execution-client startup with a non-maker-only key records a new zero-fee approval. ```bash export LIGHTER_API_KEY_INDEX=5 export LIGHTER_API_SECRET=REPLACE_ME export LIGHTER_ACCOUNT_INDEX=123456 cargo run -p nautilus-lighter --bin lighter-integrator-revoke # Lighter Mainnet ``` Script source: [`crates/adapters/lighter/bin/integrator_revoke.rs`](https://github.com/nautechsystems/nautilus_trader/blob/master/crates/adapters/lighter/bin/integrator_revoke.rs). ```python # Python (PyO3 binding) - reads the same env vars as the Rust bin from nautilus_trader.adapters.lighter import revoke_lighter_integrator await revoke_lighter_integrator() # Lighter Mainnet (default) ``` The Rust script prints a summary of the action and pauses for an Enter keypress before signing or sending; abort with `Ctrl+C` before that point if anything in the summary looks wrong. The Python binding does not prompt: review the active env vars yourself before calling. ## Data subscriptions | Data type | Sub. | Snapshot | Hist. | Nautilus type | Notes | | -------------------- | ------------ | -------- | ----- | ------------------- | -------------------------------------------------------- | | Instrument metadata | Cache replay | ✓ | - | `InstrumentAny` | Loaded from `orderBookDetails`. | | Trade ticks | ✓ | - | ✓ | `TradeTick` | WebSocket trades; public `recentTrades` REST history. | | Quote ticks | ✓ | - | - | `QuoteTick` | Best bid and ask ticker stream. | | Order book deltas | ✓ | ✓ | - | `OrderBookDeltas` | `L2_MBP` only. | | Order book depth10 | ✓ | - | - | `OrderBookDepth10` | Live top-10 view from maintained book; no REST snapshot. | | Order book snapshots | - | ✓ | - | `OrderBook` | REST snapshot, max depth 250. | | Mark prices | ✓ | - | - | `MarkPriceUpdate` | Perp market stats stream. | | Index prices | ✓ | - | - | `IndexPriceUpdate` | Market and spot stats streams. | | Funding rates | ✓ | - | ✓ | `FundingRateUpdate` | Current estimates and REST hourly history. | | Bars | ✓ | - | ✓ | `Bar` | WebSocket candle stream; REST history for backfill. | | Instrument status | REST | ✓ | - | `InstrumentStatus` | `active` / `inactive` snapshots. | Only `BookType::L2_MBP` is accepted for book-delta and depth10 subscriptions. Other book types return an error before subscribing. The WebSocket order book initializes only from `subscribed/order_book`. If an `update/order_book` arrives before that snapshot, the adapter drops it and waits for the real snapshot because incremental updates do not contain the full visible book. Depth10 subscriptions use the same WebSocket `order_book` stream as deltas. The adapter emits a refreshed top-10 view after each accepted snapshot or incremental update. Bar subscriptions use the venue's `candle/{market_id}/{resolution}` WebSocket channel. Lighter batches in-progress updates for the open bar every ~500 ms; the adapter emits a Nautilus `Bar` only when the candle start timestamp advances, so consumers see one event per closed period. The in-progress cache is cleared on reconnect and on unsubscribe. The stream supports `1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `12h`, and `1d`. `1w` is REST-only via `request_bars`; subscribing to a `1-WEEK` bar type returns an error. REST bar history omits venue gap rows whose open, high, low, or close is missing, null, zero, or negative. These rows cannot form valid Nautilus bars and do not stop later valid rows from loading. Instrument status subscriptions replay the latest cached `orderBookDetails` status when available and otherwise fetch a REST snapshot. Lighter does not expose a WebSocket status-change stream. See [Funding rates](#funding-rates) for live and historical funding semantics. Trade subscriptions use the public WebSocket trade stream. Historical trade requests use the public `/api/v1/recentTrades` endpoint, which needs no credentials; the adapter clamps the request to the venue per-call cap and filters the returned ticks to the requested time range. ### Unsupported data requests `request_quotes` is not implemented. Lighter exposes best bid and offer data through the WebSocket `ticker` stream, but the REST endpoints available to the adapter do not provide a timestamped quote snapshot or quote history that can map safely to `QuoteTick`. `request_book_depth` is not implemented. The documented REST book endpoints do not provide a venue event timestamp for `OrderBookDepth10.ts_event`; use `subscribe_book_depth10` for a live depth10 stream or `request_book_snapshot` for a REST `OrderBook` snapshot. ## Orders capability ### Order identification Lighter uses a numeric venue order index and a caller-supplied `client_order_index`. The adapter derives a 31-bit index from the Nautilus `ClientOrderId` and probes forward on a collision. Because the collision-probed value cannot be re-derived after restart, order reconciliation resolves each raw venue order ID through the core cache and restores its actual `client_order_index` before translating order and fill reports. Open cached orders return to active tracking, while terminal orders use bounded replay tracking. Recovery never infers a client order ID from the integer alone: the cached venue order ID must match. It requires reconciliation to include the order and the core cache to retain its venue-order-ID mapping; otherwise reports use the unique venue order ID as their external client order ID. Query paths use the numeric venue order ID for active or terminal history. Before that ID is known, a Nautilus client order ID can query active orders by its derived client index. Client-index-only queries do not search terminal history, and duplicate active matches fail as ambiguous. ### Order types | Order type | Perpetuals | Spot | Notes | | ---------------------- | ---------- | ---- | ------------------------------------------------------- | | `MARKET` | ✓ | ✓ | Cap derived from cached far-side quote + slippage. | | `LIMIT` | ✓ | ✓ | Requires a limit price. | | `STOP_MARKET` | ✓ | - | Perp only; cap derived from `trigger_price` + slippage. | | `STOP_LIMIT` | ✓ | - | Perp only; maps to Lighter stop-loss limit orders. | | `MARKET_IF_TOUCHED` | ✓ | - | Perp only; cap derived from `trigger_price` + slippage. | | `LIMIT_IF_TOUCHED` | ✓ | - | Perp only; maps to Lighter take-profit limit orders. | | `MARKET_TO_LIMIT` | - | - | *Not supported*. | | `TRAILING_STOP_MARKET` | - | - | *Not supported*. | | `TRAILING_STOP_LIMIT` | - | - | *Not supported*. | | `TWAP` | - | - | *Not supported*; no Nautilus mapping. | Conditional orders require `trigger_price`. The adapter rejects missing triggers for `STOP_MARKET` and `MARKET_IF_TOUCHED`, any trigger that truncates to `0` ticks at the instrument's price precision, and spot conditionals that Lighter does not support. Lighter requires a worst-acceptable `price` for market-style orders. The adapter starts from the cached far-side `QuoteTick` for `MARKET`, or `trigger_price` for `STOP_MARKET` and `MARKET_IF_TOUCHED`, then applies `market_order_slippage_bps` (default 50 bps) and rounds at the instrument's price precision, up for buys or down for sells. A `MARKET` order without a cached quote is denied. Override the slippage with `SubmitOrder.params["market_order_slippage_bps"]`. ### Contingent orders | Feature | Perpetuals | Spot | Notes | | --------------------- | ---------- | ---- | -------------------------------------------------- | | Stop-loss market | ✓ | - | `STOP_MARKET` maps to Lighter `STOP_LOSS`. | | Stop-loss limit | ✓ | - | `STOP_LIMIT` maps to Lighter `STOP_LOSS_LIMIT`. | | Take-profit market | ✓ | - | `MARKET_IF_TOUCHED` maps to Lighter `TAKE_PROFIT`. | | Take-profit limit | ✓ | - | `LIMIT_IF_TOUCHED` maps to `TAKE_PROFIT_LIMIT`. | | Trigger price | ✓ | - | Required for every supported conditional order. | | Trigger price type | - | - | *Not supported*; no trigger source selector. | | Grouped order lists | - | - | *Not supported*. | | OCO / OTO orders | - | - | *Not supported*. | | Bracket orders | - | - | *Not supported*. | | `CreateGroupedOrders` | - | - | *Not supported*; order lists use independent txs. | ### Order options | Option | Perpetuals | Spot | Notes | | ---------------- | ---------- | ---- | ------------------------------------------------------------------------- | | `post_only` | ✓ | ✓ | Maps to Lighter's post-only time-in-force. | | `reduce_only` | ✓ | - | Passed through to `CreateOrder`; use only to reduce an existing position. | | `quote_quantity` | - | - | *Not supported*; submit base quantity instead. | | `display_qty` | - | - | *Not supported*; Lighter exposes no iceberg display quantity field. | ### Adapter order params | Param | Perpetuals | Spot | Notes | | ------------------------------------------ | ---------- | ---- | --------------------------------------------------- | | `market_order_slippage_bps` | ✓ | ✓ | Overrides the config default for market-style caps. | | `post_only` through `SubmitOrder.params` | - | - | *Not supported*; use the Nautilus order flag. | | `reduce_only` through `SubmitOrder.params` | - | - | *Not supported*; use the Nautilus order flag. | ### Time in force | Time in force | Perpetuals | Spot | Notes | | -------------- | ---------- | ---- | ----------------------------------------------------------------------------- | | `GTC` | ✓ | ✓ | Limit-style uses `GoodTillTime`; market-style uses `IOC`. | | `DAY` | ✓ | ✓ | Limit-style and conditional orders use a positive order expiry. | | `GTD` | ✓ | ✓ | Supplied expiry must be 5 minutes to 30 days from submission. | | `IOC` | ✓ | ✓ | Plain `MARKET`/`LIMIT` use expiry `0`; conditional limit uses trigger expiry. | | `FOK` | - | - | *Not supported*. | | `AT_THE_OPEN` | - | - | *Not supported*. | | `AT_THE_CLOSE` | - | - | *Not supported*. | The adapter sends `MARKET`, `STOP_MARKET`, and `MARKET_IF_TOUCHED` as Lighter `ImmediateOrCancel`; the venue rejects market-style `GoodTillTime` orders. Plain `MARKET` uses `OrderExpiry = 0`, while conditional market orders keep a positive expiry until triggered. The adapter denies Nautilus `IOC` for conditional market orders because Lighter reserves IOC for post-trigger execution. Conditional limit orders can use `IOC`: their trigger rests with a positive expiry, then the child uses `ImmediateOrCancel`. Without an explicit GTD expiry, limit-style `GTC`, `DAY`, and `GTD` orders default to the current time plus 28 days; conditional `GTC`, `DAY`, and limit-style `IOC` use the same default. Lighter rejects `-1` and accepts expiries from 5 minutes to 30 days after submission. The adapter enforces that window with a one-second signing and transport margin, so an expiry of exactly 5 minutes is denied locally before signing; tester configurations expressed in whole minutes should use at least 6 minutes. ### Execution instructions | Instruction | Perpetuals | Spot | Notes | | ------------- | ---------- | ---- | --------------------------------------------------------- | | `post_only` | ✓ | ✓ | Overrides the TIF and sends Lighter `PostOnly`. | | `reduce_only` | ✓ | - | Position-reducing flag for existing derivative positions. | Use `post_only` on limit-style orders. The adapter does not synthesize maker-only market orders. Live Lighter Mainnet testing confirms `reduce_only=true` for closing perpetual positions. Invalid reduce-only opens can be dropped by Lighter without a venue order report; the adapter reconciles them as `INFLIGHT_TIMEOUT` rather than a venue-supplied rejection reason. ### Advanced order features | Feature | Perpetuals | Spot | Notes | | ------------------ | ---------- | ---- | ---------------------------------------------------------- | | Order modification | ✓ | ✓ | Modify quantity, price, and trigger price on a live order. | | Bracket orders | - | - | *Not supported*. | | Iceberg orders | - | - | *Not supported*. | | Trailing stops | - | - | *Not supported*. | | Pegged orders | - | - | *Not supported*. | | TWAP orders | - | - | *Not supported*; no Nautilus mapping. | | Leverage update | ✓ | - | Perp only; submits a signed `UpdateLeverage` tx. | | Native cancel-all | - | - | *Not supported*; adapter scopes cancel-all per instrument. | | Dead man's switch | - | - | *Not supported*. | ### Order operations | Operation | Perpetuals | Spot | Notes | | ------------------- | ---------- | ---- | -------------------------------------------------------------- | | Submit order | ✓ | ✓ | Sends a signed `L2CreateOrder` transaction over WebSocket. | | Submit order list | ✓ | ✓ | Sequential fanout of up to 15 independent create transactions. | | Modify order | ✓ | ✓ | Sends a signed `ModifyOrder`; reports may restate accepts. | | Cancel order | ✓ | ✓ | Sends a signed `L2CancelOrder` transaction. | | Cancel all orders | ✓ | ✓ | Iterates cached open orders for the requested instrument. | | Set leverage | ✓ | - | Perp only; submits a signed `UpdateLeverage` tx. | | Batch cancel orders | ✓ | ✓ | Sequential fanout of up to 15 independent cancel transactions. | | Query order | ✓ | ✓ | Requires credentials and REST lookup. | | Query account | ✓ | ✓ | Replays the latest private WebSocket account state. | | Mass status | ✓ | ✓ | Bounded to account-active markets from WS and REST reports. | `SubmitOrderList` and `BatchCancelOrders` sign and hand off each child transaction in order through the hash-correlated WebSocket `sendTx` path. The adapter allocates each nonce only after the prior child handoff completes. Each transaction therefore receives normal acknowledgement, rejection, and nonce recovery handling. Fanout is not atomic: it does not create grouped venue orders or provide OCO/OTO or bracket semantics. `UpdateLeverage` is exposed as `LighterExecutionClient::update_leverage(instrument_id, initial_margin_fraction, margin_mode)`. The `initial_margin_fraction` is in venue ticks (1e-4 fraction): `500` is 5% initial margin (20x leverage), `1000` is 10% (10x), and so on. `UpdateLeverage`, `CancelAllOrders`, modify orders with integrator attributes, and conditional create orders are byte-pinned against the signer distributed with the official `lighter-python` SDK version 1.1.2. ### Order querying and reconciliation | Feature | Perpetuals | Spot | Notes | | -------------------- | ---------- | ---- | ------------------------------------------------------------ | | Query open orders | ✓ | ✓ | REST `accountActiveOrders` scoped by market. | | Query order history | ✓ | ✓ | REST `accountInactiveOrders` with cursor pagination. | | Order status updates | ✓ | ✓ | Private WebSocket order streams plus status reports. | | Trade history | ✓ | ✓ | REST `trades`; credentials are required for account history. | | Fill reports | ✓ | ✓ | REST and private WebSocket trade payloads. | | Position reports | ✓ | - | Perp only; replays cached position stream. | | Account state | ✓ | ✓ | Replays the cached merged account state snapshot. | | Mass status | ✓ | ✓ | Combines orders, fills, and cached positions. | Authenticated inactive-order and fill pagination rejects repeated cursors and stops after 1,000 pages. Fill reconciliation remains repeatable across calls while suppressing fills already emitted from the live WebSocket stream. Historical order and fill reports bind a mapped client index only to its matching venue order ID so reused numeric indexes cannot merge unrelated lifecycles. Each bounded mass status captures one cutoff for its inactive orders and fills. The adapter marks the report set complete only when the required order, fill, and position sources succeed and every historical fill maps to its order. If a historical source fails, active orders remain available for reconciliation while historical fills follow the engine's [order-only projection](../concepts/execution/reconciliation.md#order-only-fill-projection) rules. The `trades` endpoint retains only the most recent 3,000 trades per `account_index`, so a bounded lookback can request more fill history than the venue serves. Pagination walks back from the newest trade, and only a trade older than the lookback start proves the window was served: - Trade older than the start: the report set stays complete. - Cursor exhausted first: the adapter logs the uncovered span and marks the report set incomplete. - No retained trades: nothing can have been truncated, so the report set stays complete. An exhausted cursor cannot distinguish truncation from an account with no older trades, so a young account reports incomplete even though nothing is missing. Choose a lookback the venue can serve. The `export` endpoint serves full trade history for auditing fills the lookback cannot cover, and the adapter does not read it. A strategy that opens a position immediately on start can trigger a transient position-check discrepancy warning (`cached=0, venue=N`) when the venue's `account_all_positions` frame arrives a few milliseconds before the matching fill event is processed. The warning self-resolves once the fill applies; no reconciliation orders are generated. ## Account and position management Authenticated execution clients subscribe to these private streams: - `account_all_orders`: order status reports. - `account_all_trades`: fill reports. - `account_all_positions`: initial position snapshot and live updates. - `account_all_assets`: per-asset balance snapshots (spot balance plus perp collateral). - `user_stats`: perp-account margin rollup (collateral and available balance). The adapter merges `account_all_assets` and `user_stats` into a single account state and emits it only after both streams have delivered their first frame. The execution client requires credentials before connecting because private account streams and nonce refresh are mandatory. A client can be constructed without credentials, but live execution will not connect until `private_key`, `account_index`, and `api_key_index` resolve. Perpetual positions use netting mode with one position per market; spot balances use account asset state. A `subscribed/account_all_positions` frame is an authoritative snapshot: omitted markets and rows with a zero `position` value flatten cached positions, and an empty `positions` map flattens the entire cache. Cached positions for rows the adapter cannot map or parse are retained, so they do not cause false flat reports. For bounded reconciliation, the adapter also records which markets the current connection's snapshot covers. A reconnect invalidates that coverage. An absent touched market produces an explicit flat report only after a current snapshot covers it; an unmapped or malformed row leaves the mass status incomplete instead of proving flat. An `update/account_all_positions` frame is incremental. Non-zero rows replace the cached position for their market, explicit zero rows flatten that market, and omitted markets remain cached. An empty update retains all cached positions. | Feature | Perpetuals | Spot | Notes | | ----------------------- | ---------- | ---- | ------------------------------------------------------------ | | Account balances | ✓ | ✓ | Merged assets + `user_stats`, replayed from cache on query. | | Position state | ✓ | - | Perp only; initial snapshot plus live updates. | | Netting positions | ✓ | - | One Nautilus position per perpetual market. | | Cross margin | ✓ | - | Passed through `LighterPositionMarginMode::Cross`. | | Isolated margin | ✓ | - | Passed through `LighterPositionMarginMode::Isolated`. | | Leverage updates | ✓ | - | Signed `UpdateLeverage` transaction. | | Spot margin / borrowing | - | - | *Not supported*. | | Deposits / withdrawals | - | - | Use venue tools or Lighter APIs outside the trading adapter. | ## Liquidation and ADL handling | Event or field | Support | Notes | | --------------------------- | ------- | ------------------------------------------------------------- | | Liquidation trades | ✓ | Account trade rows can parse as fills, with no special event. | | Deleverage trades | ✓ | Account trade rows can parse as fills, with no special event. | | Liquidation price reporting | - | *Not supported*; reports omit this field. | | ADL event stream | - | *Not supported*. | ## Funding rates Perpetual `market_stats` frames emit `MarkPriceUpdate`, `IndexPriceUpdate`, and `FundingRateUpdate`. The live funding update uses `current_funding_rate` as the upcoming estimate; `funding_rate` and `funding_timestamp` describe the last completed payment. Because market stats provide no future settlement time, live updates leave `interval` and `next_funding_ns` unset. Spot `spot_market_stats` frames emit `IndexPriceUpdate`. Historical requests use public `/api/v1/fundings` rows at `1h` resolution and set `interval=60`. `direction=long` stays positive, while `short` becomes negative. Pagination covers the requested range up to the adapter's page cap, subject to an explicit `limit`; see [Rate limiting](#rate-limiting). Account-specific `positionFunding` is not used. ## Account tiers Lighter account tiers set latency, rate limits, fees, and integrator attribution. The execution client reads the tier from `GET /api/v1/account` and logs it, including unknown raw `account_type` values. Only Plus and Premium accounts include integrator approval and order attribution. If the account snapshot is unavailable, the client also omits attribution for that session. The client does not raise limits automatically because a local quota override does not grant a higher venue limit. | Tier | Latency (maker / taker) | REST weighted limit | `sendTx` limit | Fees (maker / taker) | Notes | | -------- | ----------------------- | ------------------- | -------------------- | ------------------------- | --------------------------------------- | | Standard | 200 ms / 300 ms | 60 req/min | 60 req/min | 0 / 0 | Zero-fee default tier. | | Premium | 0 ms / 140-200 ms | 24,000 req/min | 4,000-48,000 req/min | 0.28-0.40 / 1.96-2.80 bps | Lowest latency; scales with staked LIT. | | Plus | 200 ms / 300 ms | 24,000 req/min | 4,000 req/min | 0.5 / 0.5 bps | Raised limits, standard latency. | | Builder | - | 240,000 req/min | - | - | Highest REST throughput. | Premium figures scale with staked LIT and can change. Before raising a local quota, confirm that Lighter applies the matching tier limit to the client's traffic, then set the quota explicitly (see [Rate limiting](#rate-limiting)). ## Rate limiting Lighter limits both IP and L1 addresses. Each data and execution client owns a separate REST limiter and defaults to the standard-account quota. Configure their combined traffic within the venue limit. Higher [account tiers](#account-tiers) still require explicit client quotas: - `rest_quota_per_min`: REST read-bucket quota in requests per minute. Unset keeps 60 req/min. Available on both the data and execution clients. - `sendtx_quota_per_min`: transaction quota in requests per minute, metered in a bucket separate from reads. Unset keeps it at the standard 60 req/min, independent of `rest_quota_per_min`. Execution client only. These options change local pacing only. Public data requests remain unauthenticated, so setting a higher local quota does not make those requests eligible for an account-level venue limit. ### L1-address transaction limit The venue also enforces a 40 req/min limit per L1 address on transaction traffic, below the default `sendtx_quota_per_min` of 60. A Lighter Mainnet quoting session amending on every quote drift hit `code=23000` (`Too Many Requests`) after roughly 40 modify transactions in a minute; see [Volume quota and no-fill quoting](#volume-quota-and-no-fill-quoting) for the related quota that modify transactions also spend. Set `sendtx_quota_per_min` to 40 or lower for transaction-heavy quoting workloads. The limiter is shared across all `sendTx` traffic, so a lower quota also paces creates and cancels. The REST limiter counts one token per call rather than venue endpoint weights. Set `rest_quota_per_min` for the effective endpoint mix: a 24,000 weighted req/min premium limit yields 40 calls/minute to endpoints with weight 600, such as `/api/v1/trades` and `/api/v1/recentTrades`. The venue meters transactions per account across both transports in one bucket. The execution client enforces `sendtx_quota_per_min` with a single shared limiter across WebSocket `sendTx` (including order-list and cancel fanout) and the HTTP `sendTx` used for startup integrator approval. Low-level raw `sendTx` and `sendTxBatch` calls use that limiter when the client is constructed with it; otherwise, they fall back to the raw client's REST limiter. The clients share one WebSocket message limiter per venue URL. It paces non-transaction control frames at 200 messages/minute across both clients. A closed-loop subscription gate caps unacknowledged requests at 35, below the venue's 50-message per-IP ceiling; this count depends on acknowledgement latency, not send rate. `sendTx` does not count against the client-message bucket. | Scope | Venue limit | Adapter behavior | | ------------------------------------ | --------------------------- | ---------------------------------------------------- | | REST, standard account | 60 req/min | Default; set `rest_quota_per_min` to override. | | REST, premium account | 24,000 weighted req/min | Local override required; venue attribution applies. | | REST, plus account | 24,000 weighted req/min | Local override required; venue attribution applies. | | REST, builder account | 240,000 weighted req/min | Local override required; venue attribution applies. | | `sendTx` / `sendTxBatch`, standard | 60 req/min | Execution orders use WebSocket `sendTx`. | | `sendTx` / `sendTxBatch`, premium | 4,000-48,000 req/min | Set `sendtx_quota_per_min` (scales with staked LIT). | | `sendTx` / `sendTxBatch`, plus | 4,000 req/min | Set `sendtx_quota_per_min` to use it. | | Default transaction type limit | 40 req/min | Applies to tx types not covered by volume quota. | | `L2UpdateLeverage` transaction limit | 40 req/min | Relevant to `update_leverage`. | | Pending orders | 500/account, 16/market | Venue limit; adapter does not pre-count it. | | Active orders | 1,500/account, 1,000/market | Venue limit; adapter does not pre-count it. | Common REST endpoint weights from the official docs: | Endpoint group | Weight | Adapter behavior | | ------------------------------------ | ------ | ----------------------------------------------- | | `sendTx`, `sendTxBatch`, `nextNonce` | 6 | Tx calls use tx limiter; `nextNonce` uses REST. | | `accountInactiveOrders` | 100 | Adapter counts one REST token per HTTP call. | | `trades`, `recentTrades` | 600 | Adapter counts one REST token per HTTP call. | | Other endpoints | 300 | Adapter counts one REST token per HTTP call. | | Endpoint or transport | Limit | Notes | | -------------------------------------- | ---------- | ---------------------------------------------------- | | `/api/v1/trades` | 100 rows | Adapter paginates reconciliation at this cap. | | `/api/v1/accountInactiveOrders` | 100 rows | Adapter follows `next_cursor` at this cap. | | `/api/v1/orderBookOrders` | 250 levels | Snapshot depth is clamped to the venue cap. | | `/api/v1/candles` | 500 rows | Adapter caps REST bar pages at this venue maximum. | | `/api/v1/fundings` | 100 rows | Adapter paginates funding pages at this venue cap. | | WebSocket connections | 255 / IP | Venue limit. | | WebSocket subscriptions / connection | 500 | Venue limit. | | WebSocket unique accounts / connection | 500 | Venue limit. | | WebSocket connections / minute | 255 | Venue limit. | | WebSocket client messages / minute | 200 | Paces non-tx frames; heartbeat pings bypass it. | | WebSocket inflight messages | 50 | Venue cap; subscriptions use a 35-frame closed loop. | | WebSocket `sendTxBatch` batch size | 15 txs | Venue limit; adapter fanout is also capped at 15. | | WebSocket keepalive | 2 minutes | Adapter sends heartbeats every 30 seconds. | | WebSocket outbound command queue | Not capped | Paced before writes; no queue-depth cap. | Historical bar and funding-rate requests stop after 500 REST pages. This covers up to 250,000 bars or 49,500 hourly funding intervals. If the cap leaves part of the requested range uncovered, the HTTP client returns `LighterHttpError::HistoryIncomplete` instead of partial history and does not retry the capped request. Completion on the final allowed page remains successful. A request with an explicit `start` also remains successful when its explicit `limit` is satisfied. The data client logs the incomplete error and emits no response; narrow the requested range to continue. ## Volume quota and no-fill quoting Volume quota is separate from transport limits. `L2CreateOrder`, `L2CancelAllOrders`, `L2ModifyOrder`, and `L2CreateGroupedOrders` spend it; completed volume and any free allowance replenish it. The adapter does not inspect remaining quota. See Lighter's [Volume Quota](https://apidocs.lighter.xyz/docs/volume-quota-program) documentation for current rules and figures. Repeated no-fill quote refreshes can exhaust this quota even when the WebSocket and `sendTx` limiters work. For live tests, prefer slower one-sided quoting, wider refresh thresholds, testnet, or a bounded strategy that earns enough fills to replenish its quota. ## Connection management The WebSocket client sends heartbeats every 30 seconds and reconnects with exponential backoff from 250 milliseconds to 30 seconds. It treats a connection carrying no inbound frame for 90 seconds as dead and reconnects, which recovers a stalled socket that the venue never closes. The venue answers each heartbeat with a pong, so a healthy connection refreshes that window even when no market data flows. Private subscriptions use auth tokens with an 8-hour maximum TTL; the adapter mints 7-hour tokens, rotates them every 6 hours, and resubscribes. A transparent reconnect triggers a fresh token and account resubscription after tracked subscriptions start replaying. On execution reconnect, the adapter starts a nonce-baseline refresh through `GET /api/v1/nextNonce`. New signed transaction dispatch is rejected until that refresh, or its background retry, installs the replacement connection's nonce baseline. Within a session, venue confirmations advance the local nonce window, definitive rejections or pre-handoff failures may roll back its latest nonce, and stale state triggers a `GET /api/v1/nextNonce` resync. Outcomes that may have reached the venue retain their pending nonce and order identity for WebSocket or reconciliation recovery. `LighterExecutionClient::connect()` waits up to 30 seconds for every account stream (`account_all_orders`, `account_all_trades`, `account_all_positions`, `account_all_assets`, `user_stats`) to satisfy its readiness condition. For positions, only the `subscribed/account_all_positions` snapshot satisfies this wait; a live update does not. The adapter does not use REST account payloads as a fallback, so `connect()` blocks on these streams as its ground truth. Each attempt clears old position and account caches before awaiting the session's frames. Transparent WebSocket reconnects and auth-token rotations do not re-enter `connect()`. Both retain cached positions until the next `subscribed/account_all_positions` frame applies the snapshot replacement rules. Live update frames merge into the retained cache without evicting omitted markets. ## API credentials Lighter signing requires all three credential values: - Account index: numeric Lighter account identifier. - API key index: numeric API key slot. Lighter reserves indexes `0-3`; use a user-created key in the `4-254` range. Do not use `255`; it is an `apikeys` query sentinel, not a signing key. - API private key: 40-byte hex private key, with or without a `0x` prefix. Config values take precedence. A missing config field, or a blank API private key (empty or whitespace only), falls back to the corresponding environment variable selected by `deployment` and `environment`. | Deployment | Environment | API key index | API private key | Account index | | ---------- | ----------- | ----------------------------------------- | -------------------------------------- | ----------------------------------------- | | Lighter | Mainnet | `LIGHTER_API_KEY_INDEX` | `LIGHTER_API_SECRET` | `LIGHTER_ACCOUNT_INDEX` | | Lighter | Testnet | `LIGHTER_TESTNET_API_KEY_INDEX` | `LIGHTER_TESTNET_API_SECRET` | `LIGHTER_TESTNET_ACCOUNT_INDEX` | | Robinhood | Mainnet | `LIGHTER_ROBINHOOD_API_KEY_INDEX` | `LIGHTER_ROBINHOOD_API_SECRET` | `LIGHTER_ROBINHOOD_ACCOUNT_INDEX` | | Robinhood | Testnet | `LIGHTER_ROBINHOOD_TESTNET_API_KEY_INDEX` | `LIGHTER_ROBINHOOD_TESTNET_API_SECRET` | `LIGHTER_ROBINHOOD_TESTNET_ACCOUNT_INDEX` | The four namespaces let one process run clients for multiple deployment targets without sharing credentials. Execution rejects incomplete credentials. The data client runs without credentials: its subscriptions and REST requests (instruments, book, trades, bars, funding) all use public endpoints. ## Configuration ### Data client configuration options | Option | Default | Description | | ---------------------------------- | --------- | ------------------------------------------------------------- | | `environment` | `Mainnet` | `LighterEnvironment::Mainnet` or `Testnet`. | | `deployment` | `Lighter` | `LighterDeployment::Lighter` or `Robinhood`. | | `venue` | `None` | Optional Nautilus venue override; defaults from `deployment`. | | `account_index` | `None` | Optional factory field; public data calls do not use it. | | `api_key_index` | `None` | Optional factory field; public data calls do not use it. | | `private_key` | `None` | Optional factory field; public data calls do not use it. | | `base_url_http` | `None` | Optional REST URL override. | | `base_url_ws` | `None` | Optional WebSocket URL override. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket. | | `http_timeout_secs` | `60` | HTTP request timeout in seconds. | | `ws_timeout_secs` | `30` | WebSocket connection and reconnection timeout. | | `update_instruments_interval_mins` | `60` | Instrument metadata refresh interval in minutes. | | `rest_quota_per_min` | `None` | REST quota override; unset keeps 60 req/min. | | `transport_backend` | Default | WebSocket transport backend. | ### Execution client configuration options | Option | Default | Description | | --------------------------- | ------------- | ------------------------------------------------------------- | | `environment` | `Mainnet` | `LighterEnvironment::Mainnet` or `Testnet`. | | `deployment` | `Lighter` | `LighterDeployment::Lighter` or `Robinhood`. | | `venue` | `None` | Optional Nautilus venue override; defaults from `deployment`. | | `account_id` | `LIGHTER-001` | Nautilus account ID; issuer must match the resolved venue. | | `account_index` | `None` | Lighter account index. | | `api_key_index` | `None` | Lighter API key slot. | | `private_key` | `None` | Hex private key for auth and L2 transaction signing. | | `base_url_http` | `None` | Optional REST URL override. | | `base_url_ws` | `None` | Optional WebSocket URL override. | | `proxy_url` | `None` | Optional proxy URL for HTTP and WebSocket. | | `http_timeout_secs` | `60` | HTTP request timeout in seconds. | | `ws_timeout_secs` | `30` | WebSocket connection and reconnection timeout. | | `market_order_slippage_bps` | `50` | Slippage cap (bps) for `MARKET` / `STOP_MARKET` / `MIT`. | | `rest_quota_per_min` | `None` | REST quota override; unset keeps 60 req/min. | | `sendtx_quota_per_min` | `None` | Transaction quota override; unset keeps 60 req/min. | | `transport_backend` | Default | WebSocket transport backend. | ### Configuration example ```rust use nautilus_lighter::{ common::enums::{LighterDeployment, LighterEnvironment}, config::{LighterDataClientConfig, LighterExecutionClientConfig}, }; use nautilus_model::identifiers::AccountId; let data_config = LighterDataClientConfig::builder() .environment(LighterEnvironment::Testnet) .deployment(LighterDeployment::Lighter) .build(); let exec_config = LighterExecutionClientConfig::builder() .environment(LighterEnvironment::Testnet) .deployment(LighterDeployment::Lighter) .account_id(AccountId::from("LIGHTER-001")) .build(); let robinhood_data_config = LighterDataClientConfig::builder() .environment(LighterEnvironment::Mainnet) .deployment(LighterDeployment::Robinhood) .build(); let robinhood_exec_config = LighterExecutionClientConfig::builder() .environment(LighterEnvironment::Mainnet) .deployment(LighterDeployment::Robinhood) .account_id(AccountId::from("LIGHTER_ROBINHOOD-001")) .build(); ``` Each execution config resolves credentials from the environment-variable set selected by its `deployment` and `environment`; set the credential fields directly to override them. Use `LiveExecutionEngineConfig.reconciliation_instrument_ids` to scope reconciliation and `reconciliation_lookback_mins` to bound inactive order and fill replay. ## Official documentation - [Get started](https://apidocs.lighter.xyz/docs/get-started) - [Trading and signing](https://apidocs.lighter.xyz/docs/trading) - [API keys](https://apidocs.lighter.xyz/docs/api-keys) - [Account types](https://apidocs.lighter.xyz/docs/account-types) - [Rate limits](https://apidocs.lighter.xyz/docs/rate-limits) - [Volume quota](https://apidocs.lighter.xyz/docs/volume-quota-program) - [Data structures, constants, and errors](https://apidocs.lighter.xyz/docs/data-structures-constants-and-errors) - [REST OpenAPI](https://raw.githubusercontent.com/elliottech/lighter-python/main/openapi.json) - [WebSocket reference](https://apidocs.lighter.xyz/docs/websocket-reference) ## Contributing :::info For additional features or to contribute to the Lighter adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # OKX Source: https://nautilustrader.io/docs/latest/integrations/okx/ Founded in 2017, OKX is a cryptocurrency exchange that offers spot, margin, perpetual swap, futures, options, spread, and event contract trading. This integration supports live market data ingest and order execution on OKX. ## Overview This adapter is implemented in Rust and exposed to Python through PyO3 bindings. It does not require external OKX client libraries. The OKX adapter includes multiple components, which can be used separately or together: - `OKXHttpClient`: Low-level HTTP API connectivity. - `OKXWebSocketClient`: Low-level WebSocket API connectivity for Rust callers. - `OKXDataClient`: Market data feed manager. - `OKXExecutionClient`: Account management and trade execution gateway. - `OKXDataClientFactory`: Factory for OKX data clients. - `OKXExecutionClientFactory`: Factory for OKX execution clients. :::note Most users will define a configuration for a live trading node (as shown below), and won't need to work directly with these lower-level components. ::: ## Examples - [Python examples](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/okx/) - [Rust examples](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/okx/examples/) ## Product support | Product | Instrument source | Data | Exec | Notes | | --------------- | ---------------------------- | ---- | ---- | -------------------------------------------- | | Spot | `public/instruments` | Yes | Yes | Spot trading pairs. | | Margin | `public/instruments` | Yes | Yes | Spot instruments with margin or leverage. | | Perpetual swaps | `public/instruments` | Yes | Yes | Linear and inverse contracts. | | Futures | `public/instruments` | Yes | Yes | Dated futures contracts. | | Options | `public/instruments` | Yes | Yes | Limit-style orders; requires family filters. | | Spreads | `sprd/spreads` | Yes | Yes | Snapshots, quotes, trades on business WS. | | Event contracts | `event-contract/*` endpoints | Yes | Yes | Parsed as Nautilus `BinaryOption`. | Relevant OKX docs: - [Get instruments](https://www.okx.com/docs-v5/en/#public-data-rest-api-get-instruments). - [Get limit price](https://www.okx.com/docs-v5/en/#public-data-rest-api-get-limit-price). - [Get Spreads (Public)](https://www.okx.com/docs-v5/en/#spread-trading-rest-api-get-spreads-public). - [Spread trading place order](https://www.okx.com/docs-v5/en/#spread-trading-rest-api-place-order). - [Event contract series](https://www.okx.com/docs-v5/en/#public-data-rest-api-get-series). :::note **Options support**: The adapter supports options market data, venue-provided Greeks (`subscribe_option_greeks`), and order execution for options instruments. See the [Options trading](#options-trading) section below for details and the [Options](../concepts/options.md) guide for subscription patterns. ::: :::info **Instrument multipliers**: For derivatives (`SWAP`, `FUTURES`, `OPTION`), instrument multipliers are calculated as the product of OKX's `ctMult` and `ctVal` fields. This keeps position sizing aligned with OKX contract size and value. ::: :::info **Price limits**: OKX exposes `initPxLmtPct`, `floatPxLmtPct`, and `maxPxLmtPct` on `public/instruments` for spot, margin, swap, and futures instruments. The adapter preserves non-empty values in the instrument `info` field as `okx_init_px_lmt_pct`, `okx_float_px_lmt_pct`, and `okx_max_px_lmt_pct`. These fields describe exchange band percentages, so they are not parsed as static Nautilus `min_price` or `max_price` values. Use `OKXHttpClient.request_price_limit(instrument_id)` when you need the current computed buy and sell limits from OKX's `GET /api/v5/public/price-limit` endpoint. OKX documents the percentage fields as empty for options and event contracts; the adapter leaves their instrument `info` unchanged. ::: :::note OKX finance-product endpoints such as `/api/v5/finance/okusd/*` are outside the OKX trading adapter surface. ::: ## Instrument updates The data client loads its instrument cache over REST at connect and subscribes to the OKX instruments WebSocket channel for each configured instrument type. All update paths honor the configured instrument types, families, and contract types, and unchanged definitions are never republished. | Source | Trigger | Publishes downstream | | ------------------- | ----------------------------------------------- | ------------------------------------------------------ | | Connect load | REST at connect | Full cache, once | | Instruments channel | Venue push (incremental) | New or changed definitions, `InstrumentStatus` on each | | REST reconciliation | `update_instruments_interval_mins` (default 60) | New or changed definitions only | Each update first writes the data client, HTTP, and WebSocket caches, then publishes new or changed definitions as `DataEvent::Instrument`, so consumers never observe a definition the caches do not hold. A material change is any serialized field other than `ts_event` and `ts_init`. - The instruments channel is incremental rather than a snapshot feed: a subscription or reconnect can begin without an initial payload, so reconnect replay alone does not reconcile the instrument cache. - Set the interval to `0` to disable periodic reconciliation; instruments channel updates are always applied. One refresh task runs per connection lifecycle and is cancelled on disconnect, failed-connect teardown, stop, and dispose. Spread instruments are included when `load_spreads` is set. - Instruments that disappear from a REST response are retained in the cache; they may still back open subscriptions. Suspension, expiry, and delisting arrive as `InstrumentStatus` events through the instruments channel. ## Symbology OKX uses specific symbol conventions for different instrument types. Add the `.OKX` suffix when referencing instruments in Nautilus, for example `BTC-USDT.OKX`. ### Symbol format by instrument type #### SPOT Format: `{BaseCurrency}-{QuoteCurrency}` Examples: - `BTC-USDT` - Bitcoin against USDT (Tether) - `BTC-USDC` - Bitcoin against USDC - `ETH-USDT` - Ethereum against USDT - `SOL-USDT` - Solana against USDT To subscribe to spot Bitcoin USD in your strategy: ```python InstrumentId.from_str("BTC-USDT.OKX") # For USDT-quoted spot InstrumentId.from_str("BTC-USDC.OKX") # For USDC-quoted spot ``` #### SWAP (perpetual swaps) Format: `{BaseCurrency}-{QuoteCurrency}-SWAP` Examples: - `BTC-USDT-SWAP` - Bitcoin perpetual swap (linear, USDT-margined) - `BTC-USD-SWAP` - Bitcoin perpetual swap (inverse, coin-margined) - `ETH-USDT-SWAP` - Ethereum perpetual swap (linear) - `ETH-USD-SWAP` - Ethereum perpetual swap (inverse) Linear vs inverse contracts: - **Linear** (USDT-margined): Uses stablecoins like USDT as margin. - **Inverse** (coin-margined): Uses the base cryptocurrency as margin. #### FUTURES (dated futures) Format: `{BaseCurrency}-{QuoteCurrency}-{YYMMDD}` Examples: - `BTC-USD-261225` - Bitcoin futures expiring December 25, 2026 - `ETH-USD-261225` - Ethereum futures expiring December 25, 2026 - `BTC-USD-270326` - Bitcoin futures expiring March 26, 2027 Futures can be linear or inverse. The adapter derives this from OKX's `ctType` field. #### SPREADS Format: `{Leg1InstrumentId}_{Leg2InstrumentId}` Examples: - `BTC-USDT_BTC-USDT-SWAP` - Spread between BTC-USDT spot and BTC-USDT perpetual swap - `ETH-USD-SWAP_ETH-USD-261225` - Spread between ETH-USD perpetual swap and dated future Set `load_spreads=True` on the data client to load live OKX spread instruments from the OKX [Get Spreads (Public)](https://www.okx.com/docs-v5/en/#spread-trading-rest-api-get-spreads-public) endpoint. The adapter maps each OKX `sprdId` to a Nautilus spread instrument ID with the `.OKX` venue suffix. Spread instrument notes: - Spread market data streams on the OKX business WebSocket: quotes (`sprd-bbo-tbt`), trades (`sprd-public-trades`), and 5-level book snapshots (`sprd-books5`). Spreads have no incremental book channel, so each `sprd-books5` update is a full snapshot delivered through the order book subscription (flagged as a snapshot, not incremental L2 deltas). - The parser represents spot, swap, and futures leg combinations. It also represents option-leg spread definitions when OKX returns them through the same spread endpoint. - OKX option RFQ and block trading workflows are separate from the Nitro spread order book API and are not routed by this spread path. #### OPTIONS Format: `{BaseCurrency}-{QuoteCurrency}-{YYMMDD}-{Strike}-{Type}` Examples: - `BTC-USD-261225-100000-C` - Bitcoin call option, $100,000 strike, expiring December 25, 2026 - `BTC-USD-261225-100000-P` - Bitcoin put option, $100,000 strike, expiring December 25, 2026 - `ETH-USD-261225-4000-C` - Ethereum call option, $4,000 strike, expiring December 25, 2026 Where: - `C` = Call option - `P` = Put option #### EVENTS OKX event contract instrument IDs use the market ID returned by the OKX instruments API. The adapter represents these markets as Nautilus `BinaryOption` instruments. Example: - `BTC-ABOVE-DAILY-261224-1600-65000` - Event contract market in the `BTC-ABOVE-DAILY` series. ### Common questions **Q: How do I know which contract type to use?** A: Linear and inverse instruments have distinct symbols. The public Python configs do not expose a contract-type filter, so the adapter loads both for the selected derivative instrument types. **Q: How do I load event contracts?** A: Use `OKXInstrumentType.EVENTS`. The public Python configs load all discoverable event contract series and do not expose a series filter. ## Retail price improvement (RPI) Use Retail Price Improvement (RPI) to consume OKX's consolidated organic and RPI depth, place RPI maker orders, or let standard orders take RPI liquidity. The adapter maps these features to existing Nautilus order book, order, and lifecycle types. RPI routing is opt-in, so standard subscriptions and orders remain unchanged. ### RPI market data Pass `params={"rpi": True}` to `subscribe_book_deltas` or `request_book_snapshot` to use the public `books-rpi` channel or `GET /api/v5/market/books-rpi`. The feed combines organic quantity with RPI quantity that is available for execution. Each raw depth level has the wire shape `[price, totalQty, nonRpiQty, count]`: | Wire field | Rust type | Meaning | | ----------- | --------- | -------------------------------------------- | | `price` | `Decimal` | Price level. | | `totalQty` | `Decimal` | Organic and available RPI quantity. | | `nonRpiQty` | `Decimal` | Quantity available without RPI taker access. | | `count` | `u64` | Aggregated order count at the price level. | Nautilus `OrderBookDeltas` and `OrderBook` use `totalQty` as the level quantity. The typed raw model retains `nonRpiQty`; the difference between the two quantities is the available RPI liquidity. WebSocket snapshots and updates retain `seqId` and `prevSeqId`. Emitted deltas carry `seqId` as their sequence. The data client checks each update's `prevSeqId` against the last accepted `seqId`; the values do not need to increase by one. On a mismatch, the client: - Drops the mismatched frame. - Suppresses later updates for that instrument. - Replaces the subscription once to request a fresh snapshot. - Resumes emission after a snapshot with `prevSeqId: -1`. If the snapshot does not arrive before the configured snapshot timeout, the book monitor logs a warning and the client remains fail-closed. The adapter applies the same linkage rule to standard incremental OKX book channels when `prevSeqId` is present. `books-rpi` has no checksum. For WebSocket subscriptions, `rpi=True` selects `books-rpi` instead of depth or VIP channel selection. For REST snapshots, the requested depth becomes `sz`; OKX defaults to one level per side and accepts up to 400. The low-level Rust clients expose: - WebSocket: `OKXWebSocketClient.subscribe_book_rpi` and `unsubscribe_book_rpi`. - REST: `OKXRawHttpClient.get_rpi_order_book` and `OKXHttpClient.request_rpi_book_snapshot`. Public instrument responses expose the venue's RPI spacing thresholds: | Wire field | Rust type | Instrument `info` key | | -------------- | ----------------- | --------------------- | | `rpiMinLevel` | `Option` | `okx_rpi_min_level` | | `rpiMinPxBand` | `Option` | `okx_rpi_min_px_band` | `rpiMinLevel` counts organic price levels, while `rpiMinPxBand` measures basis points from the opposite-side organic best price. The `info` map stores the price band as its exact decimal string. The adapter does not reject or round an order from these values because OKX applies the authoritative instrument and account rules. Use `rpi_px_round` or handle the venue rejection. ### RPI execution Pass RPI controls through the `submit_order`, `submit_order_list`, or `modify_order` command `params`. These controls work with HTTP and private WebSocket execution: | Parameter | Type | Operations | Behavior | | ------------------ | ------ | ----------------------------- | --------------------------------------------------------------- | | `rpi` | `bool` | Place and batch place | Sends `ordType: rpi`; the Nautilus order must be `LIMIT`. | | `rpi_taker_access` | `bool` | Place and amend, single/batch | Lets a standard order take RPI liquidity. | | `rpi_px_round` | `bool` | Place and amend, single/batch | Lets OKX round an RPI maker price outward to an eligible level. | ```python order = strategy.order_factory.limit( instrument_id=instrument_id, order_side=OrderSide.SELL, quantity=instrument.make_qty("250000"), price=instrument.make_price("0.0001600"), ) strategy.submit_order( order, params={ "rpi": True, "rpi_px_round": True, }, ) ``` Use `rpi_taker_access` only with regular limit, market, FOK, or IOC orders. When it is enabled, OKX applies its taker speed bump to eligible orders, including post-only orders. Use `rpi_px_round` only on RPI maker orders. Omit inapplicable controls instead of passing `False`, because OKX can reject unsupported combinations. Both controls default to `false`, and `rpi_taker_access` is not inherited during an amendment. Repeat `rpi_taker_access=True` on every amendment that must retain access. The low-level Rust clients expose the same single and batch matrix: | Operation | REST method | WebSocket method | | ----------- | -------------- | --------------------- | | Place | `place_order` | `submit_order` | | Batch place | `place_orders` | `batch_submit_orders` | | Amend | `amend_order` | `modify_order` | | Batch amend | `amend_orders` | `batch_modify_orders` | The WebSocket batch amend tuple accepts an optional request ID and serializes it as `reqId`; it does not replace the order's client ID. ### RPI minimum notional RPI maker orders must meet both the instrument's `minSz` and the [RPI minimum notional](https://www.okx.com/docs-v5/log_en/#2026-08-18-rpi-maker-minimum-notional-amount): - `SWAP` and `FUTURES`: 10,000 USD. - `SPOT`: 1,000 USD. - `EVENTS`: exempt from the RPI minimum notional. OKX rejects an order below the applicable notional threshold with `54051`; the execution client emits `OrderRejected` for a rejected placement. An amend that includes `newSz` is checked again, with or without `newPx`. A rejected amend leaves the original order active; the adapter emits `OrderModifyRejected` and stops tracking the amend as pending. A price-only amend does not trigger this check. Each sub-order in a batch place or amend request is checked independently. Orders already on the book when the rule took effect in production on August 18, 2026, are grandfathered. Non-RPI orders, including orders with `rpiTakerAccess: true`, are exempt from this notional rule. An order that meets `minSz` can still fail the RPI minimum-notional check. ### RPI responses and lifecycle Private order messages parse both `ordType: rpi` and the migration alias `ordType: elp`. If an unfilled RPI placement first appears on the private order channel as `state: canceled`, with `accFillSz` zero or empty, the adapter emits a post-only order rejection without first emitting acceptance. The fallback reason is `RPI order canceled before acceptance`. OKX can use this path when an RPI price fails its spacing rule and `rpiPxRound` is false. Order reports represent RPI orders as Nautilus `LIMIT` orders with `post_only=True`. Use `get_account_instruments` to read the typed `OKXRpiPermission` value: - `Disabled` maps to `rpi: "0"`. - `Enabled` maps to `rpi: "1"` and does not grant permission to place RPI orders. - `Permitted` maps to `rpi: "2"` and grants permission to place RPI orders. The public instrument endpoint does not return account permissions. Raw fee responses expose `rpiMaker` as an optional `Decimal`; an empty value means RPI is not applicable. Responses may contain both RPI and ELP field names during the transition. The adapter prefers `rpi` and `rpiMaker`, reads `elp` and `elpMaker` as response aliases, and sends only RPI names. Raw trade messages describe `source: "1"` as an RPI order. ### RPI exclusions The adapter deliberately excludes the following: - It does not expose obsolete `books-elp` subscriptions or emit `ordType: elp`. - It does not treat the published RPI spacing thresholds as authoritative client-side validation. - It does not apply RPI controls to algo orders. The regular HTTP order path rejects RPI controls for spread orders. - It does not add generic post-only replay deduplication as part of RPI support. OKX ignores `rpiPxRound` for options and event contracts. See the [OKX RPI migration changelog](https://www.okx.com/docs-v5/log_en/#2026-07-28) and [RPI program guide](https://www.okx.com/help/okx-retail-price-improvement-program-rpi). ## Orders capability Below are the order types, execution instructions, and time-in-force options supported for linear perpetual swap products on OKX. ### WebSocket order identification OKX WebSocket order operations use `instIdCode` (a numeric instrument identifier) instead of the string `instId` parameter. The adapter resolves `instIdCode` values from the instrument definitions fetched during startup and caches them for the session lifetime. Order submissions fail with a clear error if the required `instIdCode` is missing from the cache. The initial execution connection requires usable instruments from every requested instrument type or family. A failed request or a scope with no usable instruments aborts the connection before WebSockets open, even if another scope succeeds. Pre-open instruments and entries that cannot be parsed do not satisfy this requirement. Options without configured instrument families remain skipped. ### USD to USDC spot migration OKX is consolidating USD and USDC spot books. This is a breaking venue change. Affected `Crypto-USD` instruments are replaced by `Crypto-USDC` instruments. See the [OKX changelog](https://www.okx.com/docs-v5/log_en/#upcoming-changes-okx-to-migrate-usd-spot-trading-pairs). | Event | Time | | ---------------------- | ------------------------------- | | Parallel trading opens | 08:00 UTC on 23 September 2026. | | USD pairs delisted | 08:00 UTC on 30 September 2026. | #### Instrument IDs Subscribe to and trade the replacement instrument IDs: | Before | After | | ------------- | -------------- | | `BTC-USD.OKX` | `BTC-USDC.OKX` | OKX does not map old USD `instId` or `instIdCode` values to the new USDC instruments. The adapter does not rewrite USD keys in the instrument or `instIdCode` caches. After delisting, requests and subscriptions that still use a USD ID may fail or return no data. #### Trading quote currency The default `tradeQuoteCcy` is the quote currency in `instId`. Switching only the instrument ID from `Crypto-USD` to `Crypto-USDC` changes the default trading quote from USD to USDC. Set `spot_trade_quote_ccy` on `OKXExecutionClientConfig`: | `spot_trade_quote_ccy` | Effect | | ---------------------- | --------------------------------------------------------------------------------- | | Unset (`None`) | Omits the field; OKX uses the quote currency in `instId` (USDC on `Crypto-USDC`). | | `"USD"` | Keeps trading in USD on a `Crypto-USDC` instrument. | The adapter sends `tradeQuoteCcy` on regular REST and WebSocket spot orders. It does not send the field on algo or conditional orders. The adapter rejects the order locally when: - The configured value is absent from that instrument's `tradeQuoteCcyList`. - The list is unknown. The list is retained from instrument definitions, including `GET /api/v5/account/instruments`, and stored on the instrument `info` map as `okx_trade_quote_ccy_list`. #### Account activation :::warning Before trading a `Crypto-USDC` instrument, call `OKXHttpClient.activate_feature("1")` once per master account and once per sub-account to enable USDC order book trading, if that account has not already traded USDC. The adapter never activates accounts implicitly. ::: ### Client order ID requirements OKX requires client order IDs to be alphanumeric (letters and numbers only) and at most 32 characters. Hyphens (`-`) are rejected, so set the following on your strategy config: ```python use_hyphens_in_client_order_ids = False ``` Nautilus client order IDs longer than 32 characters are also rejected. When you need UUID-based identifiers, combine `use_uuid_client_order_ids=True` with `use_hyphens_in_client_order_ids=False` so the generated value fits within the OKX limit. ### Order types | Order type | Linear perpetual swap | Notes | | ---------------------- | --------------------- | ----------------------------------------------------------- | | `MARKET` | ✓ | Immediate execution at market price. | | `MARKET_TO_LIMIT` | ✓ | Market order converted to IOC limit. | | `LIMIT` | ✓ | Execution at specified price or better. | | `STOP_MARKET` | ✓ | Conditional market order through OKX algo orders. | | `STOP_LIMIT` | ✓ | Conditional limit order through OKX algo orders. | | `MARKET_IF_TOUCHED` | ✓ | Conditional market order through OKX algo orders. | | `LIMIT_IF_TOUCHED` | ✓ | Conditional limit order through OKX algo orders. | | `TRAILING_STOP_MARKET` | ✓ | Trailing stop market order through OKX advance algo orders. | :::info **Conditional orders**: `STOP_MARKET`, `STOP_LIMIT`, `MARKET_IF_TOUCHED`, `LIMIT_IF_TOUCHED`, and `TRAILING_STOP_MARKET` use OKX algo orders. The `TRAILING_STOP_MARKET` path uses OKX's advance algo order API (`move_order_stop`) and requires the `cancel-advance-algos` endpoint for cancellation. ::: ### Spread orders OKX spread instruments use a separate spread trading order book and API family. The execution client routes spread orders by spread instrument ID, for example `ETH-USD-SWAP_ETH-USD-261225.OKX`, through the HTTP `/api/v5/sprd/*` endpoints. The adapter uses OKX's spread REST endpoints for submit, cancel, mass cancel, order status, and trade reports. It subscribes to the OKX business WebSocket [`sprd-orders` channel](https://www.okx.com/docs-v5/en/#spread-trading-websocket-private-channel-order-channel) for live spread order updates. OKX `sprd-orders` WebSocket updates do not include fee fields. The adapter fails closed and discards the whole update, so it emits neither a fill event nor an order-state update. Startup reconciliation recovers the order from REST; set `open_check_interval_secs` to poll open orders continuously. Historical and reconciliation fill reports from the REST [`sprd/trades` endpoint](https://www.okx.com/docs-v5/en/#spread-trading-rest-api-get-trades) include OKX fee data. Supported spread order instructions: - `LIMIT` with GTC time-in-force. - `LIMIT` with IOC time-in-force. - `LIMIT` with post-only execution. Spread order lists, conditional orders, FOK time-in-force, and modify requests are not supported by the OKX spread trading API path. Relevant OKX docs: - [Spread order placement](https://www.okx.com/docs-v5/en/#spread-trading-rest-api-place-order). - [Spread order details](https://www.okx.com/docs-v5/en/#spread-trading-rest-api-get-order-details). - [Spread order channel](https://www.okx.com/docs-v5/en/#spread-trading-websocket-private-channel-order-channel). ### Execution instructions | Instruction | Linear perpetual swap | Notes | | ------------- | --------------------- | ----------------------------------------------------- | | `post_only` | ✓ | Only for limit orders. | | `reduce_only` | ✓ | See the product and position-mode restrictions below. | The adapter sends OKX's literal `reduceOnly` field for margin orders in `isolated` or `cross` trade mode and for futures or swap orders in `net` position mode. In `long/short` position mode, OKX does not accept that field. The adapter uses the closing `side` and `posSide` combination as the enforcing venue instruction instead. It rejects reduce-only orders for cash, option, and event products, and rejects a long/short-mode combination that would increase the selected side. See OKX's [place order documentation](https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order). ### Time in force | Time in force | Linear perpetual swap | Notes | | ------------- | --------------------- | ------------------------------------ | | `GTC` | ✓ | Good Till Canceled. | | `FOK` | ✓ | Fill or Kill. | | `IOC` | ✓ | Immediate or Cancel. | | `GTD` | - | *No native OKX order time-in-force.* | :::note **GTD (Good Till Date) time in force**: OKX supports request expiry through `expTime`, but that is a request timeout rather than a native order expiry instruction. If you need GTD functionality, use Nautilus's strategy-managed GTD feature. It handles order expiration by canceling the order at the specified expiry time. ::: ### Batch operations | Operation | Linear perpetual swap | Notes | | ------------ | --------------------- | ----------------------------------------- | | Batch Submit | ✓ | Submit multiple orders in single request. | | Batch Modify | ✓ | Modify multiple orders in single request. | | Batch Cancel | ✓ | Cancel multiple orders in single request. | ### Position management | Feature | Linear perpetual swap | Notes | | ---------------- | --------------------- | ------------------------------------ | | Query positions | ✓ | Real-time position updates. | | Position mode | ✓ | Net vs Long/Short mode (see below). | | Leverage control | - | Not exposed by the execution client. | | Margin mode | ✓ | Supports isolated and cross modes. | #### Position modes OKX supports two position modes for derivatives trading: - **Net mode** (netting): One position per instrument. Buy and sell orders net against each other. This is the default and recommended mode for most traders. - **Long/Short mode** (hedging): Separate long and short positions for the same instrument. This mode supports simultaneous long and short exposure. :::note Position mode applies account-wide. Set it through the OKX web or app interface, or with `OKXHttpClient.set_position_mode`; the client configs do not set it. The adapter handles both modes when reporting positions: in net mode it derives the position side from the signed quantity, and in long/short mode it uses the `posSide` reported by OKX. ::: ### Trade modes and margin configuration OKX's unified account system supports different trade modes for spot and derivatives. Configure the account mode first through the OKX web or app interface; the API cannot set it for the first time. For account mode details, see the [OKX Account Mode documentation](https://www.okx.com/docs-v5/en/#overview-account-mode). #### Trade modes overview The Python execution config selects trade modes as follows: | Instrument | Trade mode | Configuration | | ---------- | ---------- | ------------------------------------------------- | | Spot | `cash` | Automatic. | | Derivative | `isolated` | Default, or `margin_mode=OKXMarginMode.ISOLATED`. | | Derivative | `cross` | `margin_mode=OKXMarginMode.CROSS`. | ```python from nautilus_trader.adapters.okx import OKXExecutionClientConfig from nautilus_trader.adapters.okx import OKXInstrumentType from nautilus_trader.adapters.okx import OKXMarginMode from nautilus_trader.model import AccountId exec_config = OKXExecutionClientConfig( account_id=AccountId.from_str("OKX-001"), instrument_types=[OKXInstrumentType.SWAP], margin_mode=OKXMarginMode.CROSS, ) ``` The public Python config does not expose spot margin selection, so spot orders use cash mode. In a mixed spot and derivatives client, `margin_mode` applies to derivatives only. :::warning **Manual trade mode override**: You can override the trade mode per order with `params={"td_mode": "..."}`. This bypasses adapter selection and can lead to order rejection when the value does not match the instrument type, such as `isolated` for spot instruments. Only use manual override for requirements that cannot be met through configuration. ::: ### Order querying | Feature | Linear perpetual swap | Notes | | -------------------- | --------------------- | ------------------------------ | | Query open orders | ✓ | List all active orders. | | Query order history | ✓ | Historical order data. | | Order status updates | ✓ | Real-time order state changes. | | Trade history | ✓ | Execution and fill reports. | ### Contingent orders | Feature | Linear perpetual swap | Notes | | ------------------ | --------------------- | -------------------------------------- | | Order lists | ✓ | Batch via WS; regular orders only. | | OCO orders | - | Not submitted by `OKXExecutionClient`. | | Bracket orders | - | Not submitted by `OKXExecutionClient`. | | Conditional orders | ✓ | Stop and limit-if-touched orders. | The low-level HTTP client models OKX attached TP/SL and OCO payloads, but `OKXExecutionClient` does not translate Nautilus OCO or bracket order lists into those payloads. #### Conditional order architecture Conditional orders (OKX algo orders) use a hybrid architecture: - **Submission**: HTTP REST API (`/api/v5/trade/order-algo`). - **Status updates**: WebSocket business endpoint (`/ws/v5/business`). Stop and touched orders use `orders-algo`; trailing stops use `algo-advance`. - **Cancellation**: HTTP REST API while the algo parent is active, then the regular order path after a triggered child becomes authoritative. The `orders-algo` channel sends updates only, while `algo-advance` also sends a snapshot on subscription. The adapter keeps tracked order context across transport reconnects and deduplicates replayed advance-algo snapshots. REST reconciliation remains responsible for cold-start and missed-update recovery. This design ensures: - Immediate submission acknowledgment through HTTP. - Real-time status updates through WebSocket. - Stable order identity while venue authority moves from the algo parent ID to the triggered child order ID. #### Supported conditional order types | Order type | Trigger types | Notes | | ---------------------- | ----------------- | ---------------------------------------------------- | | `STOP_MARKET` | Last, Mark, Index | Market execution when triggered. | | `STOP_LIMIT` | Last, Mark, Index | Limit order placement when triggered. | | `MARKET_IF_TOUCHED` | Last, Mark, Index | Market execution when price touched. | | `LIMIT_IF_TOUCHED` | Last, Mark, Index | Limit order placement when price touched. | | `TRAILING_STOP_MARKET` | - | Callback ratio or spread; optional activation price. | :::warning OKX's `close_fraction` conditional-order parameter is not normalized to the generic `close_position` risk contract. Do not add `OKX` to `full_position_exit_venues` based on `close_fraction`; leave the venue unlisted so ordinary quantity and notional checks apply. ::: #### Trigger price types Stop and touched orders support different trigger price sources: - **Last price** (`TriggerType.LAST_PRICE`): Uses the last traded price (default). - **Mark price** (`TriggerType.MARK_PRICE`): Uses the mark price. - **Index price** (`TriggerType.INDEX_PRICE`): Uses the underlying index price. ```python # Example: Stop loss using mark price trigger stop_order = order_factory.stop_market( instrument_id=instrument_id, order_side=OrderSide.SELL, quantity=Quantity.from_str("0.1"), trigger_price=Price.from_str("45000.0"), trigger_type=TriggerType.MARK_PRICE, # Use mark price for trigger ) strategy.submit_order(stop_order) ``` ## Risk management ### Liquidation and ADL event handling The OKX adapter detects exchange-initiated risk management events: - **Liquidation warnings**: When `instrument_types` includes `MARGIN`, `SWAP`, `FUTURES`, or `OPTION`, the execution client subscribes to the `liquidation-warning` channel with `instType=ANY` and logs a warning when OKX reports a position nearing liquidation. This is an early warning only: the position may already be liquidated by the time the message arrives, and the adapter surfaces it as a log message rather than a strategy-facing event. - **Liquidation orders**: When the exchange liquidates a position, the adapter detects the liquidation category and logs warnings with order details. These orders continue through the normal order and fill pipeline. - **Auto-deleveraging (ADL)**: When OKX closes your position to offset a counterparty's liquidation, the adapter detects and logs the ADL event with position details. Liquidation-order and ADL detection is driven by the `category` field on the order record. The recognized values are: | `category` | Meaning | | ----------------------- | ----------------------------- | | `full_liquidation` | Full position liquidation. | | `partial_liquidation` | Partial position liquidation. | | `adl` | Auto-deleveraging close. | | `delivery` | Contract delivery at expiry. | | `normal` / other values | Regular order flow. | Category detection runs on both paths: - WebSocket `orders` channel (live order and fill updates). - HTTP `GET /api/v5/trade/orders-history` (used during reconciliation and cold-start mass status). :::info **Liquidation and ADL events are logged at WARNING level** with details including order ID, instrument, and state. Liquidation warnings instead log position side, size, margin ratio, mark price, and margin mode. Monitor these logs as part of your risk management process. The adapter forwards these exchange-generated orders as `OrderStatusReport` and `FillReport` messages and sends position updates as `PositionStatusReport` messages. Because the orders are untracked at dispatch time, this path does not emit strategy-owned order events directly. ::: Upstream references: - [Order channel and `category` field](https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-order-channel) - [Liquidation warning channel](https://www.okx.com/docs-v5/en/#trading-account-websocket-liquidation-warning-channel) - [Auto-Deleveraging mechanism](https://www.okx.com/help/okx-contract-auto-deleveraging-adl) - [Liquidation mechanism](https://www.okx.com/help/introduction-to-liquidation) ## Options trading The OKX adapter supports trading options (`OPTION` instrument type) with some differences from other derivatives. OKX options are inverse contracts settled in the underlying cryptocurrency. For full API details see the [OKX Options Trading documentation](https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order). ### Supported order types Only limit-style orders are supported. OKX does not allow market orders for options. | Order type | Supported | Notes | | ----------------- | --------- | ------------------------------------------------ | | `LIMIT` | ✓ | Standard limit order. | | `MARKET` | - | Rejected by the adapter before reaching the API. | | `MARKET_TO_LIMIT` | - | Rejected by the adapter before reaching the API. | Options support FOK and IOC time-in-force. OKX uses a dedicated `op_fok` order type for options FOK orders; the adapter handles this mapping automatically. Conditional/algo orders (`STOP_MARKET`, `STOP_LIMIT`, `MARKET_IF_TOUCHED`, `LIMIT_IF_TOUCHED`, `TRAILING_STOP_MARKET`) are not supported for options and are denied. ### Pricing modes Options orders can be priced in three mutually exclusive ways. Pass the pricing mode via order `params`: | Mode | Parameter | Description | | ----- | --------- | ------------------------------------------------ | | Price | (default) | Standard limit price in the contract's currency. | | USD | `px_usd` | Price in USD terms. | | IV | `px_vol` | Price in implied volatility (1.0 = 100%). | ```python # Price in USD order = strategy.order_factory.limit( instrument_id=InstrumentId.from_str("BTC-USD-261225-50000-C.OKX"), order_side=OrderSide.BUY, quantity=Quantity.from_int(1), price=Price.from_str("0"), # Placeholder; px_usd takes precedence params={"px_usd": "100.5"}, ) # Price in implied volatility order = strategy.order_factory.limit( instrument_id=InstrumentId.from_str("BTC-USD-261225-50000-C.OKX"), order_side=OrderSide.BUY, quantity=Quantity.from_int(1), price=Price.from_str("0"), # Placeholder; px_vol takes precedence params={"px_vol": "0.55"}, ) ``` When modifying an order, the same `px_usd` or `px_vol` params can be passed to the modify command to amend the price in the original pricing mode. ### Option Greeks OKX publishes two parallel greek sets on the `opt-summary` channel: - **Black-Scholes (`BLACK_SCHOLES`)**: Greeks denominated in USD. Matches the convention used by the Deribit and Bybit adapters. - **Price-adjusted (`PRICE_ADJUSTED`)**: Greeks denominated in the underlying coin units. Matches OKX's native contract convention. By default the adapter emits both on every `opt-summary` tick. Each emitted `OptionGreeks` carries a `convention` field set to `GreeksConvention.BLACK_SCHOLES` or `GreeksConvention.PRICE_ADJUSTED`, so receivers can branch per message. To narrow the stream, pass `params["greeks_convention"]` on subscribe: - Single string: `"BLACK_SCHOLES"` or `"PRICE_ADJUSTED"` (case-insensitive). - List of strings: `["BLACK_SCHOLES", "PRICE_ADJUSTED"]`. - Omitted: adapter emits both. Unknown entries log a warning and are skipped. If every requested entry is unknown, the adapter falls back to emitting both. ```python # Default (both conventions, receiver branches) self.subscribe_option_greeks(instrument_id) def on_option_greeks(self, greeks: OptionGreeks) -> None: if greeks.convention == GreeksConvention.BLACK_SCHOLES: self._handle_bs(greeks) else: self._handle_pa(greeks) ``` ```python # Single-convention narrowing self.subscribe_option_greeks( instrument_id, params={"greeks_convention": "PRICE_ADJUSTED"}, ) ``` ```python # Explicit list (equivalent to the default when both are listed) self.subscribe_option_greeks( instrument_id, params={"greeks_convention": ["BLACK_SCHOLES", "PRICE_ADJUSTED"]}, ) ``` :::note The data engine deduplicates option-greeks subscriptions by `instrument_id`, so if two actors on one node subscribe to the same instrument with different single conventions only the first one reaches the adapter. The second actor gets the first actor's convention set. Workaround: either actor can subscribe without `params` (or with the full list) to receive both streams and filter locally on `greeks.convention`. ::: ### Position Greeks OKX position payloads include position-level Black-Scholes Greeks (`delta_bs`, `gamma_bs`, `theta_bs`, and `vega_bs`). The adapter's standard `PositionStatusReport` does not expose these fields. The `opt-summary` stream described above provides the adapter's exposed per-instrument Greeks. ### Restrictions - Reduce-only option orders are rejected by the adapter because OKX does not support the instruction for options. - Position side defaults to `Net`. ### Configuration :::warning Option discovery requires at least one `instrument_families` value, for example `BTC-USD`. Pass it to `OKXDataClientConfig` when loading options from Python. The public Python execution config constructor does not expose this field, so selecting `OKXInstrumentType.OPTION` only on `OKXExecutionClientConfig` skips option loading and logs a warning. ::: ## Event contracts OKX exposes prediction market contracts through `instType=EVENTS`. The adapter loads these instruments as Nautilus `BinaryOption` instruments and preserves OKX metadata in the instrument `info` field under the keys `series_id`, `inst_category`, `inst_id_code`, `state`, and `rule_type`. ### Loading event contract instruments Use `OKXInstrumentType.EVENTS` in the data or execution client config. The adapter requests the event contract series list, then requests instruments for each series. ```python from nautilus_trader.adapters.okx import OKXDataClientConfig from nautilus_trader.adapters.okx import OKXInstrumentType data_config = OKXDataClientConfig(instrument_types=[OKXInstrumentType.EVENTS]) ``` ### Event contract market data The low-level HTTP client exposes OKX's public event contract discovery endpoints: - `request_event_contract_series`. - `request_event_contract_events`. - `request_event_contract_markets`. The low-level WebSocket client supports the `event-contract-markets` channel through `subscribe_event_contract_markets` and `unsubscribe_event_contract_markets`. This channel publishes market status and floor-strike generation updates, has no initial snapshot, and does not include `instId`, so the adapter forwards it as raw venue JSON. :::note OKX's standard market data endpoints return YES-side data for `EVENTS`. Derive NO-side prices from YES-side prices when a strategy needs both outcomes. ::: ### Event contract trading Pass the OKX event outcome through order `params` when submitting event contract orders: ```python order = strategy.order_factory.limit( instrument_id=InstrumentId.from_str("BTC-ABOVE-DAILY-261224-1600-65000.OKX"), order_side=OrderSide.BUY, quantity=Quantity.from_int(1), price=Price.from_str("0.42"), params={"outcome": "yes"}, ) strategy.submit_order(order) ``` OKX requires `outcome` for `EVENTS` orders, which the adapter validates before sending. OKX ignores the obsolete `speedBump` request parameter, so the adapter omits it. Remove `speed_bump` from existing client calls and order `params`. Settlement fills arrive with OKX order category `delivery`. The adapter parses this category during live order updates and reconciliation. Upstream references: - [Event contract REST endpoints](https://www.okx.com/docs-v5/en/#public-data-rest-api-get-series). - [WS channel](https://www.okx.com/docs-v5/en/#public-data-websocket-event-contract-markets-channel). - [Place order request fields](https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order). - [Removal of `speedBump`](https://www.okx.com/docs-v5/log_en/#2026-07-24). ## Authentication To use the OKX adapter, create API credentials in your OKX account: 1. Log into your OKX account and navigate to the API management page. 2. Create a new API key with the required permissions for trading and data access. 3. Record your API key, secret key, and passphrase. You can provide these credentials through environment variables: ```bash export OKX_API_KEY="your_api_key" export OKX_API_SECRET="your_api_secret" export OKX_API_PASSPHRASE="your_passphrase" ``` Or pass them directly in the configuration (not recommended for production). ## Demo trading OKX provides a demo trading environment for testing strategies without real funds. ### Setting up a demo account 1. Log into your OKX account at [okx.com](https://www.okx.com). 2. Navigate to **Trade** > **Demo Trading**. 3. Go to **Personal Center** within Demo Trading. 4. Select **Demo Trading API** and create a new API key. 5. Record your demo API key, secret key, and passphrase. You can provide demo credentials through environment variables: ```bash export OKX_API_KEY="your_demo_api_key" export OKX_API_SECRET="your_demo_api_secret" export OKX_API_PASSPHRASE="your_demo_passphrase" ``` ### Configuration Set `environment=OKXEnvironment.DEMO` in your client configuration: ```python from nautilus_trader.adapters.okx import OKXDataClientConfig from nautilus_trader.adapters.okx import OKXEnvironment data_config = OKXDataClientConfig(environment=OKXEnvironment.DEMO) ``` When demo mode is enabled: - REST API requests reuse the region's live host with the `x-simulated-trading: 1` header. - WebSocket connections use demo endpoints (`wspap.okx.com` for the global region). :::note Demo API keys are separate from production keys. Create API keys for demo trading through the Demo Trading interface. Production API keys do not work in demo mode. ::: ## Regional endpoints OKX serves distinct endpoints per region, and an API key is only valid against the region where it was registered (using a key against another region's endpoints returns `API key doesn't exist`). Set `region` to select the correct endpoint set: | Region | Registered on | REST | WebSocket host | | -------- | ------------- | ------------- | --------------- | | `GLOBAL` | `www.okx.com` | `www.okx.com` | `ws.okx.com` | | `EEA` | `my.okx.com` | `eea.okx.com` | `wseea.okx.com` | | `US` | `app.okx.com` | `us.okx.com` | `wsus.okx.com` | Despite its enum name, `US` also selects the endpoints for Australian accounts registered on `app.okx.com`. `region` defaults to `GLOBAL`. For example, an EEA account: ```python from nautilus_trader.adapters.okx import OKXDataClientConfig from nautilus_trader.adapters.okx import OKXRegion data_config = OKXDataClientConfig(region=OKXRegion.EEA) ``` `region` selects the regional defaults, and combines with `environment` to pick the demo hosts (for example `wseeapap.okx.com` for EEA demo). Explicit `base_url_http` and `base_url_ws` overrides always take precedence over the region defaults. ## Funding rates The adapter receives funding rate data from the [Funding Rate Channel](https://www.okx.com/docs-v5/en/#public-data-websocket-funding-rate-channel) WebSocket stream. OKX provides both `fundingTime` and `nextFundingTime` in each message, and the adapter computes `interval` as the difference between these two values. For historical funding rate requests, the adapter computes the interval from consecutive funding timestamps returned by the [Get Funding Rate History](https://www.okx.com/docs-v5/en/#public-data-rest-api-get-funding-rate-history) endpoint. ## Rate limiting The adapter enforces OKX's per-endpoint quotas while keeping sensible defaults for REST and WebSocket calls. :::warning OKX enforces per-endpoint and per-account quotas. A rate-limited request returns OKX error code `50011`; throttle requests on the affected key before retrying. ::: ### REST limits Every request passes through an internal global bucket of 250 requests per second, plus the endpoint-specific bucket below. The endpoint quotas mirror OKX's published limits where available. | Key / endpoint | Limit (req/sec) | Notes | | --------------------------------------- | --------------- | ----------------------------------------- | | `okx:global` | 250 | Adapter-level shared bucket. | | `/api/v5/account/set-position-mode` | 2 | OKX 5 requests / 2 seconds, rounded down. | | `/api/v5/account/balance` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/account/trade-fee` | 2 | OKX 5 requests / 2 seconds, rounded down. | | `/api/v5/account/instruments` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/account/positions` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/account/positions-history` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/public/instruments` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/public/position-tiers` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/public/event-contract/series` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/public/event-contract/events` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/public/event-contract/markets` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/public/opt-summary` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/public/price-limit` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/public/time` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/public/mark-price` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/public/funding-rate-history` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/market/index-tickers` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/market/books` | 20 | OKX 40 requests / 2 seconds. | | `/api/v5/market/books-rpi` | 20 | Adapter bucket; OKX publishes 20 / 2 sec. | | `/api/v5/market/candles` | 20 | OKX 40 requests / 2 seconds. | | `/api/v5/market/history-candles` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/market/history-trades` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/sprd/spreads` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/sprd/order` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/sprd/cancel-order` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/sprd/mass-cancel` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/sprd/orders-pending` | 5 | OKX 10 requests / 2 seconds. | | `/api/v5/sprd/orders-history` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/sprd/trades` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/trade/order` | 30 | OKX 60 requests / 2 seconds. | | `/api/v5/trade/batch-orders` | 7 | OKX 300 orders / 2 seconds, rounded down. | | `/api/v5/trade/amend-order` | 30 | OKX 60 requests / 2 seconds. | | `/api/v5/trade/amend-batch-orders` | 7 | OKX 300 orders / 2 seconds, rounded down. | | `/api/v5/trade/cancel-batch-orders` | 7 | OKX 300 orders / 2 seconds, rounded down. | | `/api/v5/trade/orders-pending` | 30 | OKX 60 requests / 2 seconds. | | `/api/v5/trade/orders-history` | 20 | OKX 40 requests / 2 seconds. | | `/api/v5/trade/fills` | 30 | OKX 60 requests / 2 seconds. | | `/api/v5/trade/order-algo` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/trade/cancel-algos` | 1 | OKX 20 orders / 2 seconds. | | `/api/v5/trade/cancel-advance-algos` | 1 | Conservative bucket, see below. | | `/api/v5/trade/amend-algos` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/trade/orders-algo-pending` | 10 | OKX 20 requests / 2 seconds. | | `/api/v5/trade/orders-algo-history` | 10 | OKX 20 requests / 2 seconds. | All keys include the `okx:global` bucket. URLs are normalized with query strings removed before rate limiting, so requests with different filters share the same quota. The adapter's `/api/v5/market/books-rpi` bucket is 20 requests per second, while OKX publishes 20 requests per 2 seconds. The venue limit remains authoritative, so callers should keep RPI book snapshot traffic within the published quota. For order-based batch quotas, the adapter uses request-level buckets that assume full batch sizes: 20 orders per request for regular batch operations and 10 orders per request for algo cancels. OKX's public docs do not list a rate limit for `/api/v5/trade/cancel-advance-algos`, so the adapter applies a conservative bucket; the HTTP client calls that endpoint to cancel advance algo orders such as trailing stops. ### WebSocket limits - Connection establishment: 3 requests per second (per IP). - Subscription operations (subscribe/unsubscribe/login): 480 requests per hour per connection. Order operation buckets mirror OKX's published limits where available. | Operation key | Limit (req/sec) | Notes | | -------------- | --------------- | ---------------------------------------------------------- | | `order` | 30 | OKX 60 requests / 2 seconds. | | `cancel` | 30 | OKX 60 requests / 2 seconds. | | `amend` | 30 | OKX 60 requests / 2 seconds. | | `batch-order` | 7 | OKX 300 orders / 2 seconds, rounded down for full batches. | | `batch-cancel` | 7 | OKX 300 orders / 2 seconds, rounded down for full batches. | | `batch-amend` | 7 | OKX 300 orders / 2 seconds, rounded down for full batches. | | `mass-cancel` | 2 | OKX 5 requests / 2 seconds, rounded down. | | `algo-order` | 10 | OKX 20 requests / 2 seconds. | | `algo-cancel` | 1 | OKX 20 orders / 2 seconds, rounded down for full batches. | :::info See the [OKX rate limit documentation](https://www.okx.com/docs-v5/en/#rest-api-rate-limit). ::: ## Reconciliation The OKX adapter applies separate reconciliation policies to current venue state and terminal history: | Data | Unset lookback | Explicit lookback | OKX source | | ------------------------- | --------------------- | --------------------- | ----------------------------- | | Pending regular orders | All current orders | All current orders | Regular pending orders | | Live algo orders | All current orders | All current orders | Algo pending orders | | Current positions | All current positions | All current positions | Account positions | | Terminal orders and fills | 3 days | Up to 7 days | Order and trade history | | Fill lookback <= 3 days | Recent fills | Recent fills | `/api/v5/trade/fills` | | Fill lookback > 3 days | Not requested | Extended fills | `/api/v5/trade/fills-history` | Values above 7 days are clamped to the longest complete window across the regular order history and spread trade history endpoints used for reconciliation. This is not a limit on all archived data available from OKX. ## Configuration ### Data client The OKX data client provides the following Python configuration options. | Option | Default | Description | | ---------------------------------- | -------------------------- | ------------------------------------------------------------------------------ | | `instrument_types` | `[OKXInstrumentType.SPOT]` | OKX instrument types to load. | | `instrument_families` | `None` | Required for options (`BTC-USD`); filters futures, swaps, and events when set. | | `load_spreads` | `False` | Loads live spread instruments. | | `base_url_http` | `None` | Override for the OKX REST endpoint. | | `base_url_ws_public` | `None` | Override for the public WebSocket URL. | | `base_url_ws_business` | `None` | Override for the business WebSocket URL. | | `api_key` | `None` | Falls back to `OKX_API_KEY` when unset. | | `api_secret` | `None` | Falls back to `OKX_API_SECRET` when unset. | | `api_passphrase` | `None` | Falls back to `OKX_API_PASSPHRASE`. | | `environment` | `LIVE` | Environment enum (`LIVE` or `DEMO`). | | `region` | `GLOBAL` | Region enum (`GLOBAL`, `EEA`, or `US`). | | `http_timeout_secs` | `60` | REST market data request timeout. | | `max_retries` | `3` | Retry attempts for recoverable REST errors. | | `retry_delay_initial_ms` | `1,000` | Initial delay before retrying. | | `retry_delay_max_ms` | `10,000` | Maximum exponential backoff delay. | | `update_instruments_interval_mins` | `60` | REST instrument cache reconciliation interval in minutes; `0` disables. | | `book_stale_check_interval_secs` | `5` | Stale book check interval. | | `book_stale_threshold_secs` | `30` | Idle time before a stale book warning. | | `book_snapshot_timeout_secs` | `3` | Post-reconnect snapshot wait. | | `vip_level` | `None` | Enables higher-depth books by VIP tier. | | `proxy_url` | `None` | Optional HTTP and WebSocket proxy URL. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | Set `book_stale_check_interval_secs`, `book_stale_threshold_secs`, or `book_snapshot_timeout_secs` to `0` to disable that health monitor. Quiet markets can idle without book updates; increase `book_stale_threshold_secs` for sparse instruments. Supported data client `instrument_types` values are `SPOT`, `MARGIN`, `SWAP`, `FUTURES`, `OPTION`, and `EVENTS`. See [Options trading](#options-trading) before selecting `OPTION` from Python. Spread instruments use `load_spreads` instead of `instrument_types` because OKX serves them from `/api/v5/sprd/spreads`. ### Execution client The OKX execution client provides the following Python configuration options. | Option | Default | Description | | ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------- | | `instrument_types` | `[OKXInstrumentType.SPOT]` | Tradable OKX instrument types. | | `load_spreads` | `False` | Loads live spread instruments. | | `account_id` | Required | Nautilus account ID for the client. | | `base_url_http` | `None` | Override for the OKX trading REST endpoint. | | `base_url_ws_private` | `None` | Override for the private WebSocket URL. | | `base_url_ws_business` | `None` | Override for the business WebSocket URL. | | `api_key` | `None` | Falls back to `OKX_API_KEY` when unset. | | `api_secret` | `None` | Falls back to `OKX_API_SECRET` when unset. | | `api_passphrase` | `None` | Falls back to `OKX_API_PASSPHRASE`. | | `environment` | `LIVE` | Environment enum (`LIVE` or `DEMO`). | | `region` | `GLOBAL` | Region enum (`GLOBAL`, `EEA`, or `US`). | | `margin_mode` | `None` | Margin mode (`ISOLATED` or `CROSS`). | | `spot_trade_quote_ccy` | `None` | SPOT `tradeQuoteCcy` override. Set `"USD"` to keep USD after migrating to `Crypto-USDC`. | | `http_timeout_secs` | `60` | REST trading request timeout. | | `max_retries` | `3` | Retry attempts for recoverable REST errors. Order submission endpoints are exempt and always send once. | | `retry_delay_initial_ms` | `1,000` | Initial delay before retrying. | | `retry_delay_max_ms` | `10,000` | Maximum exponential backoff delay. | | `auth_timeout_secs` | `None` | Override WebSocket authentication timeout. | | `proxy_url` | `None` | Optional HTTP and WebSocket proxy URL. | | `transport_backend` | `Sockudo` | WebSocket transport backend. | Supported execution client `instrument_types` values are `SPOT`, `MARGIN`, `SWAP`, `FUTURES`, `OPTION`, and `EVENTS`. See [Options trading](#options-trading) before selecting `OPTION` from Python. Spread instruments use OKX spread IDs instead of `instrument_types`; load them with `load_spreads=True` on the data and execution clients before trading them. See [USD to USDC spot migration](#usd-to-usdc-spot-migration) for `spot_trade_quote_ccy`. ### Manual endpoint overrides Setting `region` (see [Regional endpoints](#regional-endpoints)) selects the correct EEA or US endpoints automatically, which is the recommended approach. The explicit `base_url_*` overrides below remain available for proxies, custom routing, or endpoints not covered by a region; they take precedence over the `region` default. The EEA bases are shown as an example. | Config field | Live base | Demo base | WebSocket path | | ---------------------- | -------------------------- | ----------------------------- | ----------------- | | `base_url_http` | `https://eea.okx.com` | `https://eea.okx.com` | | | `base_url_ws_public` | `wss://wseea.okx.com:8443` | `wss://wseeapap.okx.com:8443` | `/ws/v5/public` | | `base_url_ws_private` | `wss://wseea.okx.com:8443` | `wss://wseeapap.okx.com:8443` | `/ws/v5/private` | | `base_url_ws_business` | `wss://wseea.okx.com:8443` | `wss://wseeapap.okx.com:8443` | `/ws/v5/business` | For WebSocket fields, join the base and path in the same row. Use `base_url_ws_public` with data client configs and `base_url_ws_private` with execution client configs. When overriding either WebSocket URL, also set `base_url_ws_business` because the adapter does not derive a custom business WebSocket URL from the other override. See the [OKX EEA API documentation](https://my.okx.com/docs-v5/en/) for the current official endpoint list. Use `OKXDataClientConfig` with `OKXDataClientFactory` and `OKXExecutionClientConfig` with `OKXExecutionClientFactory`. The Python examples show a complete `LiveNode.builder(...)` configuration for data and execution clients. ## Contributing :::info For additional features or to contribute to the OKX adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Polymarket Source: https://nautilustrader.io/docs/latest/integrations/polymarket/ Founded in 2020, Polymarket is a decentralized prediction market platform that enables traders to speculate on event outcomes by buying and selling outcome tokens. NautilusTrader provides a venue integration for data and execution via Polymarket's Central Limit Order Book (CLOB) API. The adapter is implemented in Rust and exposed to Python at `nautilus_trader.adapters.polymarket`; data, execution, signing, and WebSocket operations therefore have the same behavior from Rust and Python. The adapter handles order preparation and signing for several wallet configurations. This guide covers market data, trade execution, and Deposit Wallet position operations. ## Installation The Python package includes the Polymarket adapter; no adapter-specific extra is required. To install the latest pre-release build: ```bash uv pip install --pre nautilus_trader ``` To build the Python package from source, run from the repository root: ```bash make build-debug ``` For development wheels and source-build prerequisites, see the [installation guide](../getting_started/installation.md). ## Examples The maintained examples are available in [`crates/adapters/polymarket/examples`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/polymarket/examples) for Rust. For Python, use the Rust-native [data tester](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/polymarket/data_tester.py), [execution tester](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/polymarket/exec_tester.py), or [Up/Down smoke tester](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/polymarket/updown_smoke_tester.py). The exec tester configurations apply the [close precision](#exec-tester-close-residuals) needed for Polymarket market SELL orders. ## Binary options A [binary option](https://en.wikipedia.org/wiki/Binary_option) is a type of financial exotic option contract in which traders bet on the outcome of a yes-or-no proposition. If the prediction is correct, the trader receives a fixed payout; otherwise, they receive nothing. NautilusTrader represents Polymarket outcome tokens as `BinaryOption` instruments. Polymarket uses [pUSD](#pusd) as the collateral token for trading. ## Polymarket documentation Polymarket offers resources for different audiences: - [Polymarket Learn](https://learn.polymarket.com/): Educational content and guides for users to understand the platform and how to engage with it. - [Polymarket CLOB API](https://docs.polymarket.com/getting-started/api): Technical documentation for developers interacting with the Polymarket CLOB API. ## Overview This guide assumes a trader is setting up for both live market data feeds and trade execution. The Rust implementation includes multiple components, which can be used together or separately depending on the use case. - `PolymarketWebSocketClient`: Low-level WebSocket API connectivity built on the Nautilus Rust `WebSocketClient`. - `PolymarketInstrumentProvider`: Instrument parsing and loading functionality for `BinaryOption` instruments. - `PolymarketDataClient`: A market data feed manager. - `PolymarketExecutionClient`: A trade execution gateway. - `PolymarketDataClientFactory`: Factory for Polymarket data clients (used by the live node builder). - `PolymarketExecutionClientFactory`: Factory for Polymarket execution clients (used by the live node builder). - `PolymarketPositionClient`: Deposit Wallet split, merge, and redeem operations. :::note Python users configure live nodes through the exported configuration and factory classes, and call position operations through `PolymarketPositionClient`. The direct WebSocket, provider, data client, and execution client types are Rust-only implementation components. ::: ## pUSD **pUSD** is the collateral token used for trading on Polymarket. It is a standard ERC-20 token on Polygon, backed by USDC. The proxy contract address is [0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB](https://polygonscan.com/address/0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB) on Polygon. Direct on-chain funding wraps Polygon USDC.e (bridged USDC) into pUSD through the [CollateralOnramp](https://docs.polymarket.com/resources/contracts). The Bridge API can also deposit supported assets from other chains and credit pUSD after conversion. ## Wallets and accounts To trade on Polymarket via NautilusTrader, use a Polygon-compatible wallet, such as MetaMask. ### Signature types Polymarket supports multiple signature types for order signing and verification: | Signature Type | Wallet Type | Description | Use Case | | -------------- | ------------------------------ | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | | `0` | EOA (Externally Owned Account) | Standard EIP712 signatures from wallets with direct private key control. | **Adapter default.** Allowlisted EOA trading where the funder and signer are the same address. | | `1` | Proxy Wallet | Legacy smart contract wallet created through email or social login. | Requires the Proxy Wallet `funder` address. | | `2` | Safe Wallet | Legacy Gnosis Safe wallet created with an external browser wallet. | Requires the Safe Wallet `funder` address. | | `3` | Deposit Wallet | ERC-1271 smart wallet used for new Polymarket account wallets. | Requires the Deposit Wallet `funder`; API credentials stay bound to the signer. | :::info Polymarket uses Deposit Wallets for account wallets deployed on or after May 4, 2026. Direct EOA trading requires an allowlisted EOA. See the Polymarket [wallet and authentication guide](https://docs.polymarket.com/trading/wallets-auth) for the account types and setup flows. ::: NautilusTrader defaults to signature type 0 (EOA). Set `signature_type` to use another supported wallet type. Proxy signature clients fail during construction unless `funder` is present and differs from the signing address. A single wallet address is supported per trader instance when using environment variables, or multiple wallets can be configured through multiple execution client instances. Fund your wallet with pUSD before submitting orders. An unfunded wallet produces the "not enough balance or allowance" API error. ### Setting EOA allowances The adapter includes a direct on-chain allowance command for EOA accounts. Use it only when the funding wallet is the signer (`SignatureType::Eoa`). Fund the EOA with POL for gas, set `POLYMARKET_PK`, and run: ```bash cargo run -p nautilus-polymarket --bin polymarket-set-allowances ``` The command grants maximum pUSD and CTF approvals to the CTF Exchange, Neg Risk CTF Exchange, and `NegRiskCtfCollateralAdapter`. It uses `https://polygon.drpc.org` by default; set `POLYGON_RPC_URL` to use another Polygon RPC endpoint. Run it again if Polymarket changes the required contracts. The command grants approvals only; it does not revoke approvals for contracts that are no longer targets. Treat revocation as a separate on-chain operation and confirm that no remaining redemption or settlement flow depends on the legacy approval before submitting it. ### Setting smart-wallet allowances Do not run the EOA command for a proxy, Safe, or Deposit Wallet funder. It signs transactions from the EOA key and cannot grant approvals from a smart contract wallet. Use Polymarket's [wallet and authentication flow](https://docs.polymarket.com/trading/wallets-auth) to submit the approvals from the account wallet. Deposit Wallet approvals use an ordered `WALLET` batch authorized by the signer and submitted through the Relayer. Safe and Proxy Wallet approvals need their wallet-specific SDK payloads. ### Refreshing and verifying allowances After the approval transaction confirms, refresh the CLOB cache. Rust callers can use `PolymarketClobHttpClient::update_balance_allowance` with `AssetType::Collateral` for pUSD. Use `AssetType::Conditional` with a conditional token ID for a conditional-token allowance. Both forms also need the account's signature type. The authenticated request maps to `GET /balance-allowance/update`. Use `SignatureType::Poly1271` for a Deposit Wallet. The balance-allowance endpoint has two decoding paths: | Path | Used for | Allowance handling | Meaning of success | | ------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | `PolymarketClobHttpClient::get_balance_allowance` | Reading spender allowance evidence. | Requires the plural `allowances` map; rejects a missing map, a non-null legacy singular value, malformed or non-canonical keys, and semantic duplicates such as case variants. | Balance plus an unambiguous map; required targets and amounts still need checking. | | Internal balance-only projection | Account state refresh and market-buy fee adjustment. | Ignores allowance fields; its return type cannot expose or grant approval authority. | Balance only; required CLOB spender approvals remain unproven. | Use the strict path whenever a decision depends on allowance evidence so ambiguous wire data cannot become approval authority. ### Account balances Balance refreshes use the venue-reported pUSD total and derive locked collateral from the adapter's local cache of open BUY order reservations for the execution client's account. Each reservation uses the cached limit price and remaining quantity. Submitted orders awaiting acceptance and orders absent from the execution cache are not included. SELL orders reserve outcome tokens rather than pUSD and do not contribute to locked collateral. Locked collateral is capped at the reported total, with free balance equal to total minus locked, following the [balance model](../concepts/accounting.md#balance-model). #### Refresh timing The adapter seeds this cache from the execution cache at connect and updates it as the core processes order events, including reconciled events. The initial balance refresh precedes startup reconciliation; orders discovered during reconciliation affect balances on the next refresh. Refreshes occur at connect, on account queries, after finalized trade updates, and on user WebSocket reconnect. Account balances do not change on each order event. Balance refreshes do not request open orders. :::warning These balances are estimates: HTTP balance responses and order events can reflect different points in time. They do not guarantee funds are reserved before submission or account for spending on trades awaiting settlement. ::: ## API keys ### CLOB credentials The execution client requires CLOB L2 credentials. Create or derive them with Polymarket's [API authentication flow](https://docs.polymarket.com/getting-started/api#authentication). The adapter provides a command that reads `POLYMARKET_PK` and prints the created or derived credentials: ```bash cargo run -p nautilus-polymarket --bin polymarket-create-api-key ``` Set the returned values as: - `POLYMARKET_API_KEY` - `POLYMARKET_API_SECRET` - `POLYMARKET_PASSPHRASE` The credentials authenticate the private-key signer, not a proxy or Deposit Wallet funder. The public data client does not require these credentials. ### Relayer credentials Deposit Wallet split, merge, and redeem operations also require a Relayer API key. Create one under Settings > API Keys > Relayer API Keys, as described in [Connect your account](https://docs.polymarket.com/trading/wallets-auth#connect-your-account), then set: - `POLYMARKET_RELAYER_API_KEY` - `POLYMARKET_RELAYER_SIGNER_ADDRESS` `POLYMARKET_RELAYER_SIGNER_ADDRESS` is the signer address shown when the Relayer key is created. The position client also reads `POLYMARKET_PK` and `POLYMARKET_FUNDER`. ### Deposit Wallet verification Construction fails when the funder equals the signing address. Before signing, the position client: - Checks the current and legacy Deposit Wallet addresses predicted by the canonical Polygon factory. - Requires deployed wallet code and verifies that the signer owns the wallet. - Reads the signed nonce from the wallet's on-chain counter. Polygon RPC defaults to `https://polygon.drpc.org`. Pass `base_url_rpc` to the `PolymarketPositionClient` constructor to use another trusted Polygon endpoint. ## Position operations `PolymarketPositionClient` splits pUSD into complete outcome-token sets, merges complete sets back to pUSD, and redeems resolved positions. The client supports Deposit Wallet (`SignatureType::Poly1271`) only. Safe, Proxy, and EOA paths are not available. The client does not deploy wallets, batch unrelated calls, size a merge to `"max"`, or redeem positions automatically. ### Inputs and approvals Each operation looks up the market on the public CLOB by condition ID and errors if `neg_risk` is absent. It then selects the canonical pUSD token and the standard or negative-risk collateral adapter. Callers pass a 0x-prefixed 32-byte condition ID and do not supply contract addresses. - **Split and merge amounts**: Positive pUSD decimals exactly representable at six decimal places, with no rounding. Amounts use pUSD units, never base units; `"max"` is not accepted. - **Redemption**: No amount argument; redeems both binary index sets `[1, 2]`. **Approvals are not submitted automatically.** Grant them from the Deposit Wallet before submitting: | Operation | Required approval for the market's collateral adapter | | --------------- | ----------------------------------------------------- | | Split | Spend the wallet's pUSD | | Merge or redeem | Act as a Conditional Tokens operator | The standard collateral adapter is absent from the approval plan in `polymarket-set-allowances`; the negative-risk adapter is included. That command signs approvals from the EOA, so it cannot grant either approval for a Deposit Wallet. Use Polymarket's [wallet and authentication flow](https://docs.polymarket.com/trading/wallets-auth) to grant the position-operation approvals from the Deposit Wallet. ### Submit and wait The following example shows a split submission. Replace the illustrative condition ID with the market's actual condition ID before running it. ```python from decimal import Decimal from nautilus_trader.adapters.polymarket import PolymarketPositionClient condition_id = "0x" + "11" * 32 client = PolymarketPositionClient() transaction = await client.split_position(condition_id, Decimal("1")) outcome = await transaction.wait() ``` `split_position` and `merge_positions` take the condition ID and a positive pUSD `Decimal`. `redeem_positions` takes only the condition ID. Each method returns a `PolymarketPositionTransaction` after Relayer submission. Call `wait()` once to poll until a `PolymarketPositionOutcome` is available. A second `wait()` on the same Python handle raises. The handle retains its `transaction_id` after waiting starts, including after cancellation or an error. ### Outcomes and errors | `PolymarketPositionOutcome.status` | Meaning | | ---------------------------------- | ----------------------------------- | | `confirmed` | Relayer reported `STATE_CONFIRMED`. | | `failed` | Relayer reported `STATE_FAILED`. | | `invalid` | Relayer reported `STATE_INVALID`. | Every outcome exposes `transaction_id`. Confirmed and failed outcomes expose `transaction_hash` when the Relayer supplies one; invalid outcomes always return `None`. Failed and invalid outcomes also expose `error_msg` when the Relayer supplies one. - **Submit errors**: An HTTP rejection raises before a transaction handle exists. A timed-out submit or a success response with no `transaction_id` also raises; the on-chain outcome is unknown. - **Wait timeout**: The default is 120 seconds, checked between polling requests. An in-flight request or polling delay can extend the elapsed time. A timeout raises an error naming the Relayer transaction ID; the terminal state remains unknown. Python does not expose the Rust wait-timeout or poll-interval setters. - **Response validation**: Relayer redirects are rejected, and polling rejects a response whose transaction ID differs from the submitted ID. Submit is not retried. ### Submission coordination Clients in the same process share submission state for each wallet. A later operation requires a matching terminal Relayer result and an advanced wallet nonce. A failed or invalid operation that does not advance the nonce remains blocked. Every submit error, including an HTTP rejection, timeout, cancellation, or response without a transaction ID, blocks further operations for that wallet, even if the client is recreated. An HTTP status alone does not prove that a signed batch cannot execute. :::warning Do not restart and blindly retry an unknown submission. A process restart clears local submission state but does not cancel a signed transaction. Use a single submitting process per wallet; coordination does not extend across processes or external wallet tools. ::: ### Recover an unknown submission #### Retain the submission record Enable and retain INFO logs before submitting position operations. Before each submit request, the client emits a `Deposit Wallet submission` record containing: - Wallet address, reserved nonce, and signed deadline (Unix seconds). - Operation, target contract, and value. - Unsigned calldata identifying the condition and, for split or merge, the amount. The record excludes signatures and credentials, but contains trading intent; restrict access to retained logs. Keep the transaction ID from the returned handle when available. :::warning Logging is not a durable transaction journal. Disabled logging or a process crash can leave no retained record, preventing safe recovery without further evidence. ::: #### Reconcile before retrying 1. **Stop submissions.** Include other processes and external wallet tools using the wallet. 1. **Establish whether the operation executed.** Find the submission record and any Relayer transaction ID. Match the intended wallet, target, and calldata against finalized on-chain transaction receipts and effects. A successful HTTP response, a Relayer failure, or an advanced nonce alone does not establish whether the intended operation executed. If it executed, do not repeat it. 1. **Keep the wallet blocked while execution remains unknown.** To reconcile by expiry, perform all the [expiry checks](#expiry-checks) below. If the nonce advanced, inspect the transaction that consumed it instead of assuming failure. 1. **Prove that retrying is safe before restarting.** Establish both that the intended operation did not execute and that the original signed batch can no longer execute. If the record is missing, the RPC cannot provide consistent finalized state, or contract behavior is unverified, resolve the uncertainty with Polymarket before retrying. #### Expiry checks The signed deadline is 1,800 seconds after signing by default. Rust callers can change this with `with_deadline_secs`; longer deadlines delay expiry-based recovery. A `wait()` timeout does not shorten the signed deadline or cancel the batch. First verify that the deployed wallet contract enforces the signed deadline and consumes its nonce when a batch executes. Then read the wallet's `nonce()` with `eth_call` at a finalized Polygon block and obtain that same block's timestamp. Require both: - The nonce is unchanged from the reserved nonce. - The block timestamp is strictly after the signed deadline. Elapsed local time is insufficient. An advanced nonce requires inspection of the transaction that consumed it; it does not establish failure. The client does not perform these recovery checks or release a reservation automatically. Consult the [Polymarket contract registry](https://docs.polymarket.com/resources/contracts) when identifying the deployed contracts and the [JSON-RPC reference](https://ethereum.org/en/developers/docs/apis/json-rpc/) for block-specific calls. ## Configuration Configure signing and authentication through these parameters or their environment-variable fallbacks: | Parameter | Environment variable | Purpose | | ------------- | ----------------------- | ------------------------------------------------------------ | | `private_key` | `POLYMARKET_PK` | Wallet key used to sign orders according to `signature_type` | | `funder` | `POLYMARKET_FUNDER` | pUSD funding wallet address | | `api_key` | `POLYMARKET_API_KEY` | CLOB L2 API key | | `api_secret` | `POLYMARKET_API_SECRET` | CLOB L2 API secret | | `passphrase` | `POLYMARKET_PASSPHRASE` | CLOB L2 API passphrase | When a parameter is not supplied explicitly, the client reads its environment variable. CLOB L2 credentials authenticate the private-key signer. For `POLY_1271`, the Deposit Wallet remains the `funder`; it is not the L2 authentication address. :::tip Use environment variables to supply credentials without embedding them in client configuration code. ::: Instrument loading also has two common controls: - `auto_load_missing_instruments` (default `True`): Subscribe and request commands for uncached instruments trigger an ad-hoc Gamma API load. When disabled, subscribing to an uncached instrument returns an error. See [Runtime instrument loading](#runtime-instrument-loading). - `auto_load_debounce_ms` (default `100`): Window in milliseconds for coalescing concurrent auto-load requests into a single batched Gamma call. ## Data capability Polymarket supports live `L2_MBP` order book deltas, quotes, trades, and resolution `InstrumentStatus`/`InstrumentClose` events. Instrument definitions are published by bootstrap, configured refreshes, on-demand loading, single-instrument requests, new-market discovery, and tick-size changes. ## Orders capability Polymarket operates as a prediction market with a more limited set of order types and instructions compared to traditional exchanges. :::tip For Polymarket live execution, set both the disconnection timeout and post-stop delay to 30 seconds with `with_timeout_disconnection_secs(30)` and `with_delay_post_stop_secs(30)`. The delay allows residual order and cancellation events to arrive before disconnection, while the timeout gives each client time to shut down cleanly. ::: ### Order types | Order Type | Binary Options | Notes | | ---------------------- | -------------- | ------------------------------------------------------------------------- | | `MARKET` | ✓ | **BUY orders require quote quantity**, SELL orders require base quantity. | | `LIMIT` | ✓ | BUY orders accept base or quote quantity; SELL orders require base. | | `STOP_MARKET` | - | *Not supported by Polymarket*. | | `STOP_LIMIT` | - | *Not supported by Polymarket*. | | `MARKET_IF_TOUCHED` | - | *Not supported by Polymarket*. | | `LIMIT_IF_TOUCHED` | - | *Not supported by Polymarket*. | | `TRAILING_STOP_MARKET` | - | *Not supported by Polymarket*. | ### Quantity semantics Polymarket interprets order quantities differently depending on the order type, side, and `quote_quantity` setting: - **Limit orders with `quote_quantity=False`** interpret `quantity` as the number of conditional tokens (base units). - **Limit BUY orders with `quote_quantity=True`** interpret `quantity` as pUSD collateral: - The adapter truncates collateral to cents, signs it as the maker amount, and derives shares at the market's amount precision. - It updates the local order to the signed share quantity before processing the venue response. - The signed amounts must preserve the limit price exactly. Otherwise, the adapter rejects the order before HTTP. For example, 10.00 pUSD at 0.33 is not representable, while 9.90 pUSD at 0.33 produces exactly 30 shares. - **Market BUY** orders interpret `quantity` as quote notional in **pUSD**. - **Market SELL** orders use base-unit quantities. Quote-sized limit SELL orders are not supported. The adapter denies them before submission. It also denies any limit order whose base or quote quantity truncates to zero at the two-decimal signing boundary. To cap a limit BUY by collateral, set `quote_quantity=True`: ```python # Limit BUY with quote quantity (spend $10 pUSD at a limit price of 0.50) order = strategy.order_factory.limit( instrument_id=instrument_id, order_side=OrderSide.BUY, quantity=instrument.make_qty(10.0), price=instrument.make_price(0.50), time_in_force=TimeInForce.GTC, quote_quantity=True, ) strategy.submit_order(order) ``` When submitting market BUY orders, set `quote_quantity=True` on the order. The adapter converts the quote amount (pUSD) to the signed base-unit share amount before posting to the CLOB. The Polymarket execution client denies base-denominated market buys to prevent unintended fills. A market BUY submitted with a base-denominated quantity can execute far more size than intended. ```python # Market BUY with quote quantity (spend $10 pUSD) order = strategy.order_factory.market( instrument_id=instrument_id, order_side=OrderSide.BUY, quantity=instrument.make_qty(10.0), time_in_force=TimeInForce.IOC, # Maps to Polymarket FAK quote_quantity=True, # Interpret as pUSD notional ) strategy.submit_order(order) ``` ### Execution instructions | Instruction | Binary Options | Notes | | ------------- | -------------- | ---------------------------------------------------- | | `post_only` | ✓ | Supported for limit orders with `GTC` or `GTD` only. | | `reduce_only` | - | *Not supported by Polymarket*. | ### Time-in-force options Polymarket calls the `POST /order` field `orderType`. In NautilusTrader, this maps to `TimeInForce`. The valid combinations depend on the Nautilus order type: | Nautilus TIF | Polymarket `orderType` | Nautilus order scope | Notes | | ------------ | ---------------------- | -------------------- | --------------------------------------------------------- | | `GTC` | `GTC` | `LIMIT` only | Good-Til-Cancelled; rests on the book. | | `GTD` | `GTD` | `LIMIT` only | Good-Til-Date; rests until expiration, fill, or cancel. | | `FOK` | `FOK` | `LIMIT` or `MARKET` | Fill the full size immediately or cancel the whole order. | | `IOC` | `FAK` | `LIMIT` or `MARKET` | Fill available size immediately and cancel the remainder. | Polymarket uses `FAK` (Fill-And-Kill) for the semantics NautilusTrader calls `IOC` (Immediate or Cancel). Polymarket docs classify `FOK` and `FAK` as market order types, while `GTC` and `GTD` are limit order types. For Nautilus `MARKET` orders, the adapter accepts only `IOC` and `FOK`; `GTC` and `GTD` are valid for resting `LIMIT` orders only. #### Minimum order size Read each market's `min_order_size` from its order book; active markets commonly report five shares. Marketable orders can also be rejected below **1 pUSD** in notional value with `invalid amount for a marketable BUY order … min size: $1`. The adapter leaves instrument `min_quantity` unset because quote-sized BUY quantities use pUSD while base-sized orders use shares. #### GTD expiry Set `GTD` expiry at least three minutes after submission. The adapter denies shorter expiries before signing, using whole Unix seconds, and accepts the exact three-minute boundary. The venue reports expiry as an `OrderCanceled` event, not `OrderExpired`. ### Advanced order features | Feature | Binary Options | Notes | | ------------------ | -------------- | ------------------------------------------------------- | | Order modification | Yes | Adapter-managed cancel-replace for open `LIMIT` orders. | | Bracket/OCO orders | - | *Not supported by Polymarket.* | | Iceberg orders | - | *Not supported by Polymarket.* | Polymarket has no in-place modify endpoint. The execution client cancels the current venue order, reconciles its final confirmed fills, and signs a replacement for the remaining quantity. The `ModifyOrder.quantity` value is the absolute target for the logical order, not the replacement leg. The replacement keeps the `ClientOrderId` and receives a new `VenueOrderId`. The resulting logical quantity reflects the exact signed base quantity after venue precision normalization, so it can be slightly lower than the requested target. The adapter submits no replacement unless the cancel response, canceled order state, and confirmed trade totals agree. An ambiguous cancel emits `OrderModifyRejected`. An ambiguous replacement stays in flight under its deterministic signed order hash so a later order update, fill, or order reconciliation can establish the replacement without emitting a second `OrderAccepted`. Later modify and cancel commands remain blocked until that happens. This recovery state is not persisted across an execution-client process restart. ### Batch operations | Operation | Binary Options | Notes | | ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | Batch Submit | ✓ | The adapter uses `POST /orders` for independent limit-order batches (max 15 orders per request). See [Batch submit](#batch-submit). | | Batch Modify | - | *Not supported by Polymarket*. | | Batch Cancel | ✓ | The adapter uses `DELETE /orders`. See [Batch cancel](#batch-cancel). | #### Batch submit `SubmitOrderList` commands are routed to Polymarket's `POST /orders` endpoint. The endpoint accepts at most 15 orders per request (`BATCH_ORDER_LIMIT`); larger lists are split into sequential 15-order chunks. - Only `LIMIT` orders are batched. `MARKET` orders inside the list are routed to the single-order path, which signs a marketable order and submits it with `FAK` or `FOK` based on Nautilus `time_in_force`. - `reduce_only` orders, quote-sized SELL orders, and `post_only` with market TIF (`IOC` or `FOK`) are denied before submission. - A single eligible order falls through to `POST /order` so it keeps the single-order retry semantics; the batch path deliberately disables retry because the venue does not expose an idempotency key. - If the batch response omits a leg, that order stays submitted for reconciliation. The adapter registers the signed order's expected hash so later WebSocket events and cancels still resolve to the local order. An omitted response cannot prove that the venue rejected the order. #### Batch cancel `BatchCancelOrders` commands with resolved venue order IDs use Polymarket's [`DELETE /orders`](https://docs.polymarket.com/api-reference/trade/cancel-multiple-orders) endpoint. The adapter sends sequential chunks and chooses each new chunk from the smaller of the endpoint's 1,000-ID limit and the signer's current cancellation burst. A signer starts with the Standard 120-token burst, and a tier reported by one response applies to the next new chunk. Each chunk retries independently with the same order IDs unless a lower reported tier requires smaller chunks before the retry. The adapter merges the completed responses and processes each requested order once after every chunk succeeds. If a later chunk exhausts its retries, earlier chunks may already have changed venue state, but the adapter emits no partial per-order results; reconciliation resolves the unknown overall outcome. Without a side filter, `CancelAllOrders` applies to the selected outcome token for the authenticated execution account, across strategies, even when the local order cache has no matches. The adapter sends the instrument's raw token ID as `asset_id` to [`DELETE /cancel-market-orders`](https://docs.polymarket.com/api-reference/trade/cancel-orders-for-a-market). For a `Buy` or `Sell` filter, the venue mass-cancel endpoint cannot express the side. The adapter therefore selects matching open orders from the local cache and sends their venue order IDs through the same chunked `DELETE /orders` path. A matching order that is still awaiting its venue order ID retains a pending cancellation, which is sent after submission resolves. ### Submit response handling Polymarket's public documentation describes successful [`POST /order`](https://docs.polymarket.com/api-reference/trade/post-a-new-order) responses with `success`, `orderID`, `status`, and `errorMsg`, and documents [API errors](https://docs.polymarket.com/resources/error-codes) as structured error responses. It does not document statusless client exceptions or transport failures as venue rejections. #### Successful responses For a successful response with a non-empty `orderID`, the adapter uses `status` to choose the initial Nautilus state and whether an order with `FOK` time-in-force needs the five-second REST check. The venue meanings follow Polymarket's [order lifecycle](https://docs.polymarket.com/concepts/order-lifecycle). | Submit `status` | Venue meaning | Initial Nautilus state | `FOK` REST check | | --------------- | -------------------------------------------- | -------------------------------------------------------------- | -------------------- | | `live` | Resting on the book | `Accepted` | Kept | | `matched` | Matched immediately | `Accepted` | Skipped | | `delayed` | Matching delay in progress | `Submitted` until WebSocket or REST activity proves acceptance | Kept | | `unmatched` | Delay completed without a match; now resting | `Accepted` | Kept | | Absent or empty | No status supplied | `Accepted` unless a proven FOK/FAK error rejects it | Kept unless rejected | These meanings apply to the submit response. The adapter treats `delayed` as a submit outcome, not as a market configuration signal. A `matched` response skips the REST check because the submit already confirms an immediate match. An absent or empty status emits `OrderAccepted` for compatibility and keeps the REST check unless a proven unfilled `FOK` or no-match `FAK` response causes immediate rejection. For a successful response with a non-empty `orderID`, an explicit `status` takes precedence over an unfilled `FOK` or no-match `FAK` error string. #### Delayed responses A `delayed` response: - Registers the venue order identity and fill tracking immediately and retains them independently of bounded replay caches. Later order queries, WebSocket events, and reconciliation reports can then resolve the local `ClientOrderId`. - Leaves the order `Submitted` until a fill, order update, or REST result proves acceptance. - Emits `OrderAccepted` before any fill, cancellation, expiry, or filled status that proves acceptance. - Resolves an unfilled `FOK` directly as `OrderRejected` when REST returns `UNMATCHED`. #### Definitive and ambiguous outcomes Polymarket applies the shared [command outcome policy](../concepts/execution/policies.md#command-outcomes) and the adapter guide's [diagnostic and strategy reason boundary](../developer_guide/adapters.md#separate-diagnostics-from-strategy-facing-reasons) at its execution boundary. Ambiguous failures include: - Transport failures and timeouts. - Retry exhaustion after an attempt with an unknown outcome. - Response serialization or decoding failures. - Local I/O failures. - Server-side failures. - HTTP 425 responses. - HTTP 429 responses that lack CLOB signer-limiter headers. | Outcome | Nautilus result | Reason | | ----------------------------------------------------------------------------------------- | --------------------------- | ----------------------------------------- | | `success=false`, a documented processing error, or another non-retryable client/API error | `OrderRejected` | The response proves rejection. | | Single or batch `FOK`: `success=true`, no status, and the unfilled error | Immediate `OrderRejected` | The venue proves it killed the order. | | Single or batch `IOC`/`FAK`: `success=true`, no status, and the exact no-match error | Immediate `OrderRejected` | The venue proves no quantity matched. | | Batch leg: `success=true`, empty `orderID`, and a populated `errorMsg` | `OrderRejected` with reason | The venue proves it rejected that leg. | | No `orderID` and no reason | Remains `Submitted` | The response does not prove rejection. | | Any ambiguous failure | Remains `Submitted` | The adapter cannot determine the outcome. | | Definitive retry error after an earlier ambiguous attempt | Remains `Submitted` | The earlier attempt may have succeeded. | | Failure before `POST /order`, such as a failed pUSD balance lookup | `OrderDenied` | The adapter did not submit the order. | Local denials format the strategy-facing reason from `OrderDeniedReason`. The leading token is the stable code, such as `VALIDATION_FAILED` or `UNSUPPORTED_ORDER_TYPE`. The proven unfilled `FOK` and no-match `FAK` responses resolve as immediate rejections. The `FOK` response skips the REST check. After an ambiguous single-order attempt, a later HTTP error or decoded rejection does not prove that the first attempt failed. An accepted response carrying the matching valid order ID confirms the deterministic signed order; a rejection does not, even with a matching ID. #### Error reasons Diagnostic errors retain the HTTP status and transport or rate-limit context. For venue HTTP status, rate-limit, and exchange errors, strategy-facing rejection events use the venue reason; other failures use the bounded error description. The adapter reads the first non-blank string from `error`, then `errorMsg`, and collapses whitespace and control characters. An empty body becomes `empty response body`. A plain-text or malformed response uses the same bounded fallback. Invalid UTF-8 is decoded lossily before that handling. An HTML response uses its title when available, or its visible text otherwise. Reasons are limited to 512 characters, including the literal `... [truncated]` truncation marker and its preceding space. On single and batch submit responses, the exact normalized reason `order_version_mismatch` becomes `Polymarket CLOB order version mismatch; adapter supports V2 only`. Other submit response reasons remain unchanged after normalization. The venue reports a post-only crossing as `invalid post-only order: order crosses book`. Only that exact normalized reason sets `OrderRejected.due_post_only=true`; other post-only errors remain ordinary rejections. #### Retry classification Retry-managed single-order submit and cancel requests retry HTTP 425, 429, and 5xx responses with the configured backoff. After retries are exhausted, submit classification is: | HTTP status | Retried | Submit result | Notes | | ------------------------------------ | ------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | 425 | Yes | Remains `Submitted` | Too Early does not prove rejection. | | 429 with CLOB signer-limiter headers | Yes | `OrderRejected` | Requires `Poly-RateLimit-Remaining`, `Poly-RateLimit-Reset`, or `Poly-RateLimit-Tier`. An earlier unknown attempt stays `Submitted`. | | 429 without those headers | Yes | Remains `Submitted` | Cloudflare or another hop may have seen the request. | | 5xx | Yes | Remains `Submitted` | The command may already have been applied. | | 400, 401, 403, 404 | No | `OrderRejected` | Non-retryable client or API error. | A malformed successful submit response also remains unknown and enters reconciliation instead of becoming a terminal rejection. Cancel classification uses the same evidence classes. A non-retryable client or API error after the cancel is sent, or a local failure that proves the cancel was never transmitted, emits `OrderCancelRejected`. HTTP 425, headerless 429, and 5xx leave the cancel in flight. #### Unknown-outcome reconciliation For an unknown outcome, the adapter: - Derives the expected Polymarket order hash from the signed EIP-712 order when possible and caches it as the `VenueOrderId`. Later WebSocket events and reconciliation reports attach to the local `ClientOrderId` instead of becoming external orders. - Applies the signed quote-to-base quantity update for a quote-quantity market BUY. - Defers a pending cancel until the expected venue order ID is known. - Registers fill tracking under that venue order ID. ### Position management | Feature | Binary Options | Notes | | -------------------- | -------------- | --------------------------------------------------------------------------- | | Query positions | ✓ | Current user positions from the Polymarket Data API. | | Split, merge, redeem | ✓ | Deposit Wallet operations; see [Position operations](#position-operations). | | Position mode | - | Binary outcome positions only. | | Leverage control | - | No leverage available. | | Margin mode | - | No margin trading. | ### Order querying | Feature | Binary Options | Notes | | -------------------- | -------------- | ------------------------------ | | Query open orders | ✓ | Active orders only. | | Query order history | ✓ | Limited historical data. | | Order status updates | ✓ | Real-time order state changes. | | Trade history | ✓ | Execution and fill reports. | ### Contingent orders | Feature | Binary Options | Notes | | ------------------ | -------------- | ------------------------------------------------------------------------- | | Order lists | - | Independent order batches exist, but linked contingency semantics do not. | | OCO orders | - | *Not supported by Polymarket*. | | Bracket orders | - | *Not supported by Polymarket*. | | Conditional orders | - | *Not supported by Polymarket*. | ### Precision limits Polymarket enforces different precision constraints based on tick size and `orderType`. Every Polymarket `BinaryOption` uses a canonical `price_precision` of 4, independent of its active tick size. The instrument's `price_increment` carries the active tick, and order signing derives the venue tick decimals from that increment. Binary option instruments typically support up to six decimal places for amounts with a 0.0001 tick size. The signing rules also depend on the venue order type. #### Market order types: FAK and FOK The direct maker amount is limited to **two decimal places**. The computed taker amount uses the market tick decimals plus two size decimals. A limit order submitted with `FAK` or `FOK` must also satisfy this stricter market-order amount validation; the venue rejects values that are valid for a resting order but not for that market-order type. For limit BUY orders: - **Base-sized orders**: `quantity` is the nominal share quantity at the limit price. With `FAK` or `FOK`, Polymarket spends the resulting pUSD maker budget, so price improvement can return more shares; the adapter updates the order quantity to the actual fill. The adapter denies the order before signing when `quantity * price` is not an exact cent amount. It does not round and recompute the nominal share quantity because that would change the signed price/amount ratio. - **Collateral-sized orders**: The adapter truncates the direct maker amount to cents. The computed share amount uses the market tick decimals plus two size decimals. The signed integer amounts must preserve the requested limit price exactly after this quantization. #### Resting limit order types: GTC and GTD Resting orders allow more flexible precision based on market tick size. ### Tick size precision hierarchy | Tick size | Tick decimals | Size decimals | Amount decimals | | --------- | ------------- | ------------- | --------------- | | 0.1 | 1 | 2 | 3 | | 0.01 | 2 | 2 | 4 | | 0.005 | 3 | 2 | 5 | | 0.0025 | 4 | 2 | 6 | | 0.001 | 3 | 2 | 5 | | 0.0001 | 4 | 2 | 6 | #### Tick validation - The adapter validates tick size before signing. It also denies base-sized limit `FAK` or `FOK` BUYs whose maker amount has more than two decimal places. This applies to single and batch submissions. - The adapter requires instrument tick sizes to be exactly representable at four decimals. It rejects instrument definitions and tick-size events that do not meet this requirement; a rejected event leaves the current tick active. - Tick decimals control signing and amount precision. They do not change the instrument's canonical four-decimal price precision. - Base-sized resting `GTC` and `GTD` limit orders and all SELL orders keep their tick-derived amount precision. Collateral-sized limit BUYs use cents for the direct maker amount and tick-derived precision for the computed share amount. - The adapter rejects limit prices outside the current market's `[tick_size, 1 - tick_size]` interval before signing. - The published `BinaryOption` advertises `min_price` and `max_price` equal to `tick_size` and `1 - tick_size`, so consumers that clamp to the instrument bounds stay within that accepted range. - Market-order precision limits include two decimals for the sell size plus tick-derived bounds for the computed amount. - Tick sizes can change dynamically during market conditions, particularly when markets become one-sided. ### Tick size change handling When a market's tick size changes (`tick_size_change` WebSocket event), old book levels can be invalid on the new grid (for example `0.505` fits a `0.001` tick but not a `0.01` tick). To keep old-grid prices out of the new epoch, the adapter treats the change as a book epoch transition: 1. Publish the updated `BinaryOption` with the new `price_increment`, canonical four-decimal `price_precision`, and tick-relative `min_price`/`max_price` bounds. 2. Drop the local order book for the instrument. 3. Mark the instrument as awaiting a fresh snapshot. 4. Drop incremental `price_change` book deltas until the snapshot arrives. 5. Reseed the book from the snapshot and resume normal processing. Trade ticks and the instrument update flow through unchanged. Quote handling follows `drop_quotes_missing_side`: when enabled, quote ticks require both bid and ask prices; when disabled, missing sides use Polymarket boundary prices with zero size. The adapter can keep quotes flowing during the gap by reading `best_bid` and `best_ask` from each `price_change`. Gamma instrument data supplies the active tick until the first `tick_size_change` for that token. The WebSocket tick then remains authoritative across later Gamma refreshes and instrument requests. The adapter ignores older tick-size events by venue timestamp. A data-client reset starts a new generation and allows Gamma to supply each token's tick again. ## Trades Trades on Polymarket can have the following statuses: - `MATCHED`: Trade has been matched and sent to the executor service. The executor submits it as a transaction to the Exchange contract. - `MINED`: Trade is observed to be mined into the chain, and no finality threshold is established. - `CONFIRMED`: Trade has achieved strong probabilistic finality and was successful. - `RETRYING`: Trade transaction has failed (revert or reorg) and is being retried/resubmitted by the operator. - `FAILED`: Trade has failed and is not being retried. Once a trade is initially matched, subsequent status updates arrive through the user WebSocket. The execution adapter emits one `OrderFilled` at `MATCHED`. It treats `MINED` and `RETRYING` as settlement updates without emitting another fill. `CONFIRMED` records finality and refreshes the account. If the trade reaches `FAILED`, the adapter emits one `OrderFillVoided` for each locally applied fill and refreshes the account. The correction does not relist the failed quantity, but it preserves any maker-order remainder that was already working. An execution-complete order becomes `VOIDED`. Matched WebSocket fills retain the raw trade fields in the `info` field of the `OrderFilled` event. ### Trade ID derivation Polymarket does not publish a trade ID on `last_trade_price` market-data events. The adapter derives a deterministic `TradeId` from the asset ID, side, price, size, and timestamp via the Rust `determine_trade_id` function using FNV-1a. For execution fills, taker reports use the venue's trade `id` in both REST reconciliation and the user WebSocket, so the same fill deduplicates across sources. A maker trade can fill more than one of the user's resting orders, so maker reports combine the venue trade ID with the maker venue order ID. The same venue event yields the same trade ID across replays. For historical Data API trades, the loader uses `{transactionHash[-24:]}-{asset[-4:]}-{seq:06d}` to distinguish fills in one transaction. ## Instrument metadata `BinaryOption.event_id` contains the Gamma parent event ID. The provider's [event-based discovery](#instrument-provider-options) and `PolymarketDataLoader.from_event_slug` use the enclosing event; direct market loading uses the unique ID in the market's `events` array. Missing or ambiguous relationships leave `event_id` unset. Both outcome instruments share the same event ID. ### Live metadata Live instruments retain the complete received Gamma market JSON string in `info["gamma_market"]`, including unknown fields and nested events, tags, and series when returned by Gamma. Event-based discovery also retains the enclosing response in `info["gamma_event"]`, including its full `markets` array. This event snapshot is repeated for each outcome instrument, so events with many markets increase metadata size. The existing normalized metadata keys remain available, including `token_id`, `condition_id`, `market_id`, and `event_id` when known. These strings contain the received JSON objects before enrichment. Tick-size updates preserve them; use the instrument's typed price increment for the current tick size. The adapter does not fetch additional related resources solely to populate metadata. Storing the original JSON strings preserves unknown fields and numeric precision across serialization formats. Decode them when needed: ```python import json from decimal import Decimal market = json.loads(instrument.info["gamma_market"], parse_float=Decimal) ``` ### Historical metadata The historical `PolymarketDataLoader` retains these JSON strings under `resolution_metadata["gamma_market"]` and, for event loading, `resolution_metadata["gamma_event"]` instead of `instrument.info`, because fetched responses can contain terminal outcomes that were not known during the historical period. Decode these strings with `json.loads` as above. They precede CLOB enrichment, so the raw token IDs and outcome labels can differ from the constructed instrument. ## Numeric precision Financial wire values are decoded directly as decimals. Values outside the supported decimal or domain-type range fail HTTP decoding or report construction. WebSocket and RTDS handlers log and skip invalid updates. Report construction does not substitute zero for an invalid price or quantity. ## Fees The adapter reads each instrument's `fee_schedule` and applies its `rate` and `exponent` as: ```text platform fee = shares * rate * (price * (1 - price)) ^ exponent ``` The current public schedule uses exponent `1`, which is Polymarket's published `C * feeRate * p * (1 - p)` formula. Platform fees peak at `p = 0.50`, decrease symmetrically toward the extremes, and apply only to taker fills. | Category | Taker `feeRate` | Maker `feeRate` | Maker rebate | | --------------- | --------------- | --------------- | ------------ | | Crypto | 0.07 | 0 | 20% | | Sports | 0.05 | 0 | 15% | | Finance | 0.04 | 0 | 25% | | Politics | 0.04 | 0 | 25% | | Economics | 0.05 | 0 | 25% | | Culture | 0.05 | 0 | 25% | | Weather | 0.05 | 0 | 25% | | Other / General | 0.05 | 0 | 25% | | Mentions | 0.04 | 0 | 25% | | Tech | 0.04 | 0 | 25% | | Geopolitics | 0 | 0 | - | Every order signed by the adapter carries the hard-coded Nautilus builder code. Its builder fee rate is fixed at zero and is not configurable. ### Fill commission handling Instrument `fee_schedule` metadata stores decimal parameters as strings; readers also accept legacy numeric metadata. The live fee curve retains the reference SDK's floating-point power calculation. Fee inputs remain decimals until that step; negative rates or exponents and arithmetic overflow return errors. `FillReport.commission` is denominated in pUSD and rounds the platform fee to five decimal places. If the exact result cannot be represented as `Money`, the adapter returns an error instead of using zero or a generic commission. See the [commission failure contract](../developer_guide/adapters.md#commission-failure-handling). A commission construction error fails a direct fill report request, terminal trade-history recovery, or complete mass status. Startup returns a mass-status error without applying that client's reports. When an active order report cannot enrich matched quantity from confirmed fills, the adapter logs the error and caps matched quantity to local and previously tracked evidence so reconciliation defers the unsupported residual. The adapter does not drop a failed fill while returning an order or position report that could recreate its quantity without the Polymarket commission. For the latest public schedule, see Polymarket's [Fees](https://docs.polymarket.com/trading/fees) documentation. ### Backtest fee model Use `PolymarketFeeModel` for backtests that include taker fees and maker rebates. The model reads `rate`, `rebateRate`, `exponent`, and `takerOnly` from each binary option instrument's `fee_schedule`. It requires a maker or taker liquidity side, a fill price in `[0, 1]`, and a taker-only schedule with exponent `1`. Unsupported instruments and invalid inputs return an error; an instrument without a fee schedule produces zero commission. ```rust tab="Rust" use nautilus_execution::models::fee::FeeModelHandle; use nautilus_polymarket::models::PolymarketFeeModel; let fee_model = FeeModelHandle::new(PolymarketFeeModel); ``` ```python tab="Python" from nautilus_trader.adapters.polymarket import PolymarketFeeModel fee_model = PolymarketFeeModel() ``` Pass the Rust handle through `nautilus_backtest::config::SimulatedVenueConfig::builder().fee_model(...)`. In Python, pass the model to `BacktestEngine.add_venue` as `fee_model` or set it on `BacktestVenueConfig.fee_model`. #### Maker rebate approximation For maker fills, `fee_equivalent` is the platform fee formula above using the schedule's taker `rate`. The model credits `fee_equivalent * rebateRate` as negative commission. This approximates Polymarket's daily pool allocation because a backtest does not know the total fee equivalent from other makers in that market. Live maker fills have zero commission; Polymarket pays the actual pUSD rebate separately each day. The model does not represent that payment as a separate event, and it does not model competition between makers, daily aggregation, or the minimum payout threshold. See Polymarket's [Maker Rebates Program](https://docs.polymarket.com/programs/maker-rebates) for the venue formula. ## Reconciliation The Polymarket API returns either all active (open) orders or specific orders when queried by the Polymarket order ID (`venue_order_id`). The execution reconciliation procedure for Polymarket is as follows: - Generate order reports for all instruments with active (open) orders, as reported by Polymarket. - Generate position reports from current user positions reported by Polymarket's Data API. - Compare these reports with Nautilus execution state. - Generate missing orders to bring Nautilus execution state in line with positions reported by Polymarket. An individual order lookup can return a live or terminal status. When it instead returns no order, the adapter recovers a cached individual order from trade history if its terminal WebSocket update was missed. Only `CONFIRMED` trades contribute to recovered fills; pending and failed settlement states do not. ### Mass-status reconciliation Mass-status reconciliation pairs each order report with its venue fill reports. It applies the real fills first to preserve trade IDs and commissions, then infers only any residual quantity needed to reach the venue-reported status. When mass status declares no lookback, REST order reports cap matched quantity to the greater of locally applied fills and authenticated `CONFIRMED` trade history, so pending settlement cannot create an inferred fill. A bounded mass status keeps the venue open-order `size_matched` so a live partial fill outside the lookback window is not understated. Runtime order checks fetch confirmed trade history when the venue reports more matched quantity than the local order and WebSocket fill tracker contain. Unpaired fill reports retain the normal fill-only path. A commission construction error fails the complete REST report request. Startup returns the error without applying a mass status; periodic and targeted reconciliation defer the affected work. The adapter does not drop the failed fill because an order or position report could then recreate its quantity without the Polymarket commission. ### Single-order recovery from trades `/data/order/{id}` can return live or terminal orders. When it returns no order for a known ID, `generate_order_status_report` falls back to `/data/trades` filtered by the venue order ID. This avoids the engine resolving a local `ACCEPTED` order as `REJECTED`, which would discard fills that already happened at the venue. The cached order is resolved via `client_order_id`, falling back to the cache's `venue_order_id` index when only the venue ID is known. When the request supplies or resolves to a `client_order_id`, the cached order must be a base-denominated `LIMIT` order; otherwise the request returns an error. An unassociated venue-order request without a cached order defers to the engine rather than synthesizing an external order from trade history alone: - Cached order + recovered fills covering the cached quantity (within `DUST_SNAP_THRESHOLD` for CLOB cent-tick truncation): returns `Filled`. The engine reconciles any delta over the cached `filled_qty` via inferred fill. - Cached order + recovered fills that fall short of the cached quantity by more than dust: returns `Canceled` with the recovered `filled_qty`. The engine's CANCELED branch transitions the order at the cached `filled_qty`, so any newly recovered fills that arrived only via REST (not WS) are not applied in this rare partial-cancel case. Closing the order is preferred over leaving it stuck open; if exact fill metadata matters in this scenario the venue trade history can be reviewed manually. - Cached order, no trades: returns `Canceled` with `cancel_reason="ORDER_NOT_FOUND_AT_VENUE"`. - Cached order with any `MATCHED`, `MINED`, or `RETRYING` trade: a singular order query preserves the locally applied matched quantity while terminal REST recovery waits for `CONFIRMED` or `FAILED`. - No cached order and no known client association (regardless of trades): returns `None`; the engine's not-found-at-venue path resolves the local entry. The bulk open-order check cannot use this fallback for matched orders omitted by `GET /orders`. With the default `open_check_open_only=true`, the engine leaves those cached orders open for later reconciliation. With `open_check_open_only=false`, missing-order retries can mark an order rejected before its pending settlement confirms. A singular order query or the next startup reconciliation recovers the settled quantity from confirmed trade history. ## Fill quantity normalization Polymarket wire amounts use six-decimal fixed-point mantissas. Market SELL signing truncates the share-denominated `makerAmount` to two decimal places, while market BUY quote conversion can leave a few microshares of drift between the registered and filled quantities. Both effects are fixed in absolute share terms, so the adapter uses `DUST_SNAP_THRESHOLD = 0.01` shares. Anything at or above that threshold remains a real partial fill or overfill. | Direction | Source | Adapter behavior | | --------- | ---------------------------------------------- | -------------------------------------------- | | Overfill | Market BUY quote conversion (microshares) | Snap fill down to `submitted_qty` | | Underfill | Signed or venue quantity truncation (`< 0.01`) | Normalize atomic FOK; cancel a FAK remainder | Terminal quantity normalization triggers from the `MATCHED` order update for resting maker orders, or directly on the confirming taker trade for atomic FOK orders. It emits a reconciliation `OrderUpdated` which lowers the order quantity to the cumulative venue fill. It does not emit a fill and does not change positions, balances, or commissions. IOC maps to venue FAK. Once a taker trade confirms, every positive difference between `original_size` and `size_matched` is an unfilled remainder which the venue has killed. The adapter therefore emits `OrderCanceled` after the real fills instead of normalizing quantity or leaving the order partially filled. REST reports apply the same rule when a `MATCHED` FAK has `size_matched < original_size`. The same terminal handling runs after buffered fills drain when a confirmed trade arrives before the submit response. A buffered `Canceled`, `Expired`, or `Rejected` report takes precedence. `FillReport.commission` always reflects the venue-reported size, not the snapped quantity. The few-ulp difference is sub-microcent in pUSD. The fill tracker is keyed by `venue_order_id` and registered on order accept, so fill reports for orders placed in another session pass through unchanged. `DUST_SNAP_THRESHOLD` is not configurable per-strategy; it lives in `nautilus_polymarket::common::consts`. ### Order message size denomination The user channel reports `original_size` on an `order` message as the signed `makerAmount`. For a market order type (`FAK` or `FOK`) BUY that amount is the pUSD budget rather than a share count, so a BUY of 100 shares at 0.01 reports `1`. The adapter divides by the order price when it must express that venue amount as shares in an order status report. Locally submitted quote-sized limit BUYs use the share quantity derived during signing as their authoritative fill-tracker quantity. A SELL signs shares as its maker amount and needs no conversion. Resting types (`GTC` and `GTD`) pass through unchanged: their denomination is unconfirmed, and converting a share-denominated size would misreport every externally-managed resting order. ### Exec tester close residuals `close_positions_qty_precision` is an `ExecTesterConfig` option. It defaults to `None`, which submits the full position quantity. The Rust and Python Polymarket examples set it to `2` because [market order maker amounts allow two decimals](#precision-limits). The examples also set `close_positions_time_in_force=IOC`; custom configurations must use `IOC` or `FOK` because Polymarket rejects `GTC` market orders. On stop, the tester truncates only the submitted market SELL quantity to the configured decimal precision and logs the exact difference at WARN level. It does not round the position state or create a synthetic fill. A 5 pUSD BUY that fills 5.1975 shares therefore submits a 5.19-share close. After the venue fills that order, the position remains open at exactly 0.0075 shares. If the whole position is below 0.01 shares, the tester warns and submits no zero-quantity order. Treat close-on-stop as best-effort and check the position and warning before assuming the account is flat. A non-zero close must also meet the [1 pUSD marketable-order minimum](#minimum-order-size); rejection leaves the full position open. See the [position reporting limitation](#limitations-and-considerations) for sub-0.01-share venue reports. ## WebSockets `PolymarketWebSocketClient` uses the Nautilus Rust `WebSocketClient`. ### Data The data adapter opens `market` subscriptions dynamically as instruments are requested. It spreads those subscriptions across a pool of market WebSocket connections so that no single connection carries more than `ws_max_subscriptions` assets. The pool grows lazily (a universe below the cap stays on one connection) and closes a secondary connection once it owns no assets. Each connection replays only its own assets on reconnect. A single `price_change` payload can contain interleaved updates for several assets. The adapter groups updates by instrument and publishes one atomic order book delta batch per instrument, while quote processing remains in the venue payload order. #### Quote ticks The adapter exposes one quote tick subscription type. It does not expose separate subscriptions for snapshot-derived, price-change-derived, and `best_bid_ask` quotes. Quote, book delta, and trade subscriptions for the same instrument share one asset-scoped `market` WebSocket subscription. A book delta subscription alone does not emit quote ticks; quote output remains gated by an active quote subscription. | Venue message | Trigger | Price source | Size source | | -------------- | -------------------------------------------- | --------------------------------- | ----------------------------------------------------------- | | `book` | Book snapshot | Snapshot best bid and ask | Snapshot best-level sizes | | `price_change` | Subscribed level update | Message `best_bid` and `best_ask` | Changed best-level size; previous quote or zero otherwise | | `best_bid_ask` | Top move with `subscribe_new_markets = true` | Direct message best bid and ask | Maintained-book top or prior quote, depending on book state | ```mermaid flowchart LR Q[Quote tick subscription] --> W[Asset market WebSocket subscription] W -->|book| S[Snapshot quote
prices and sizes] W -->|price_change| P[Incremental quote
changed-side size] W -->|best_bid_ask
subscribe_new_markets=true| B[Direct top-price quote] L[Maintained L2 book
book-delta subscription + effective deltas] -. matching top sizes .-> B S --> M[Validate and merge] P --> M B --> M M --> D[Deduplicate prices and sizes] D --> T[QuoteTick] ``` All three venue message paths converge on the same quote tick stream. Deduplication compares prices and sizes with the last emitted quote regardless of which message type produced it. ##### `best_bid_ask` handling With `subscribe_new_markets` enabled, the venue also sends `best_bid_ask` events when an asset's top of book moves. Every market connection requests these asset-scoped events; only the primary connection forwards global new-market and resolution events. The payload carries prices only, so the adapter selects each side's size as follows: - With [effective deltas](#effective-deltas), an active book delta subscription, and book updates not gated pending a valid snapshot, a side takes its size from the maintained local book when its top price matches. Before the first snapshot, or when the top does not match, its size is zero. - Without a maintained local book, or while book updates are gated pending a valid snapshot, a side keeps the previous quote size when its top price matches. A moved or unknown side has zero size. The adapter ignores events older than the last emitted quote or maintained local book. It also rejects locked, crossed, out-of-range, and off-grid events. An empty price, a bid at or below zero, or an ask at or above one is a missing side. By default, `drop_quotes_missing_side` drops the event. When missing sides are allowed, the missing price uses the current tick-relative venue bound and its size is zero. #### Book snapshot validation When a `book` snapshot includes a hash and its full preimage, the adapter reproduces it from the exact wire values and level order. It logs and rejects a mismatch before the snapshot can update local book state, emit snapshot-derived deltas or quotes, or resume gated book deltas. Polymarket also sends hashed book updates that omit fields included in the server's hash preimage, such as `tick_size` and `last_trade_price`. The adapter accepts these updates without hash verification because their exact hash preimage is unavailable. Snapshots without a hash remain compatible. #### Effective deltas `compute_effective_deltas` defaults to `false`. Enable it to trade extra processing for smaller snapshot batches (see [Data client options](#data-client-options)): - A full book snapshot with prior local state emits only net level changes: `ADD` for new levels, `UPDATE` for resized levels, and `DELETE` with the last known size for removed levels. No-op snapshots emit nothing, and the final record carries `F_LAST`. - Without prior state, such as after a [tick size change](#tick-size-change-handling), the snapshot passes through unchanged to seed the new book epoch. - Incremental `price_change` batches remain unchanged and update the local comparison state. - When book deltas are subscribed, the maintained comparison book can supply matching sizes to `best_bid_ask` quote ticks. This can change those quote sizes and their unchanged-quote suppression, and the carried sizes can affect later `price_change` quotes. Trades are unchanged. #### RTDS custom data The data client also supports Polymarket's real-time data (RTDS) crypto, crypto TWAP, and equity topics. Subscribe through generic custom data with a required, non-empty `symbol` metadata value. TWAP subscriptions also require `window_seconds` equal to `30` or `60`: ```python from nautilus_trader.adapters.polymarket import POLYMARKET_CLIENT_ID from nautilus_trader.adapters.polymarket import PolymarketRtdsCryptoPrice from nautilus_trader.adapters.polymarket import PolymarketRtdsCryptoTwap from nautilus_trader.adapters.polymarket import PolymarketRtdsEquityPrice from nautilus_trader.model import DataType crypto_type = DataType( PolymarketRtdsCryptoPrice.__name__, metadata={"symbol": "btcusdt"}, ) equity_type = DataType( PolymarketRtdsEquityPrice.__name__, metadata={"symbol": "AAPL"}, ) twap_type = DataType( PolymarketRtdsCryptoTwap.__name__, metadata={"symbol": "BTC/USD", "window_seconds": 60}, ) strategy.subscribe_data(crypto_type, client_id=POLYMARKET_CLIENT_ID) strategy.subscribe_data(equity_type, client_id=POLYMARKET_CLIENT_ID) strategy.subscribe_data(twap_type, client_id=POLYMARKET_CLIENT_ID) ``` Symbol matching is case-insensitive, and published symbols are lowercase. Crypto RTDS uses the `crypto_prices` topic; equity RTDS uses `equity_prices`. Equity updates prefer `full_accuracy_value` when the venue supplies it and fall back to `value` for snapshots or updates that omit it. Crypto TWAP uses `crypto_prices_twap_thirty` or `crypto_prices_twap_sixty`, requires the frame's `window_s` to match the subscription, and exposes the exact signed-E18 `full_accuracy_value` as a Rust `Decimal`. Python receives the exact decimal string, which can be converted with `decimal.Decimal`; the display-only `value` is required and decimal-like for wire conformance but is never published. Polymarket [TWAP subscriptions](https://docs.polymarket.com/market-data/chainlink-twap#stream-behavior) start with the next update and provide no snapshot, history, or replay after a disconnect. The adapter restores subscriptions after reconnect and resumes with the next update, so the disconnect interval remains a data gap. The replay guard survives reconnect, so a redelivery of the last observation remains suppressed. The adapter also suppresses older observations. A different value for the same observation timestamp is not emitted; it is logged at error level with the topic, symbol, timestamp, prior value, and received value. The stream continues with the prior observation authoritative, and emission resumes at the next newer observation timestamp. ### Runtime instrument loading Polymarket lists thousands of active markets and new markets appear throughout the day, so preloading the full universe at startup is rarely practical. The data adapter auto-loads missing instruments on demand so that strategies can subscribe to markets that are not in the cache: - When a strategy issues `subscribe_quotes`, `subscribe_trades`, `subscribe_book_deltas`, `subscribe_instrument_status`, `subscribe_instrument_close`, or `request_instrument` for an instrument that is not cached, the adapter registers the request and waits `auto_load_debounce_ms` (default 100 ms) so that concurrent requests coalesce. - It then issues a single batched Gamma API call. Batches larger than the Gamma `condition_ids` query ceiling (about 100) are split across multiple calls and merged. - Once the instruments are loaded, they are published to the data engine (populating the cache) and the deferred subscriptions open their WebSocket subscriptions atomically. A strategy that unsubscribes while the auto-load is in flight does not see a spurious subscription opened. The feature is enabled by default. Disable it by setting `auto_load_missing_instruments=False` on `PolymarketDataClientConfig`. To preload a known set of markets at startup instead, supply any of these on `PolymarketInstrumentProviderConfig`: - `load_ids` - `filters` - `event_slugs` - `market_slugs` - `event_slug_builder` - `series_ids` These scopes compose rather than override each other: filter-driven queries run alongside any explicit slug or series scope, and `load_ids` loads additively on top. Only the unfiltered full-universe fetch is suppressed once an explicit scope is present. The same composition applies to the periodic refresh driven by `update_instruments_interval_mins`, so a scope configured at startup keeps refreshing for the life of the client, and the bootstrap and refresh universes match. #### Markets awaiting CLOB metadata Newly listed markets pass through a CLOB hydration window of several minutes during which Gamma reports `active=true` but `GET /markets/{cid}` returns either a 404 or a 200 with empty `token_id` strings. The adapter classifies these as transient and retries auto-load with bounded exponential backoff plus jitter. Tune the cadence with `auto_load_max_retries` (default 12), `auto_load_retry_delay_initial_secs` (default 5.0), and `auto_load_retry_delay_max_secs` (default 15.0); the defaults cap the retry window near 3 minutes. Set `auto_load_max_retries=0` to disable retry. 5-minute markets (e.g. updown crypto) can expire before the venue finishes hydrating, so budget for that or raise the cap. After the retry budget is exhausted, a condition still missing on Gamma is logged as a terminal miss and the caller must resubscribe after the market becomes available. ### Market resolution events The Rust data client tracks Polymarket exposure at `condition_id` level so both YES and NO legs close together when the venue resolves the market. Position events add open Polymarket binary option instruments to an internal watchlist. Data clients can also watch an instrument without a position by subscribing to `InstrumentStatus`, `InstrumentClose`, or both. These subscriptions are independent: a status subscription emits only the status close, while a close subscription emits only the settlement price. Unsubscribing from one does not remove the other. #### Subscription ownership and pending instruments Cached instruments establish a watch when the subscription is accepted. Missing instruments first pass through auto-loading and the configured instrument filters. Unsubscribing removes only that data owner; open positions retain their independent ownership. If loading cannot produce usable metadata, no automatic watch is created. An accepted unresolved intent can still be checked with an explicit manual resolution selector. If an outcome arrives while a subscribed instrument is still loading, the client retains that outcome until its metadata passes the configured filters. Already admitted data and position owners settle immediately; a pending sibling does not delay them. Completing the pending subscription emits only its requested events and does not reopen ordinary market-data streams. Unsubscribing its last event type or rejecting its instrument filter discards the retained outcome. #### Automatic resolution paths Once a watched condition expires, the data client waits `resolve_poll_grace_secs`, then polls Gamma every `resolve_poll_interval_secs` until the condition resolves or `resolve_poll_max_wait_secs` elapses. | Delivery path | Configuration and eligibility | Release | | --------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------ | | Auto-load outcome. | A strict outcome in a fetched Gamma payload, regardless of polling. | Applied immediately; the auto-load task completes. | | Gamma/CLOB polling. | `resolve_poll_enabled=true`, within the expiration-based poll window. | Resolution, last owner removal, timeout, or shutdown. | | Resolution WebSocket. | `subscribe_new_markets=true`, with an active, unpaused data watch. | Resolution, last data owner removal, timeout, or shutdown. | | Manual request. | Any configuration, using explicit selectors or the watchlist rules. | Request completion; successful resolution removes the watch. | The shipped defaults use polling, without a resolution-only WebSocket subscription. Disabling polling does not enable WebSocket resolution: that path requires `subscribe_new_markets=true`. With both disabled, later recovery requires a manual request. These WebSocket ownership rules apply to the data subscription's token, not the independently configured venue-wide discovery feed. Releasing the token does not disconnect that feed. Valid resolutions received there still use the shared apply path for existing data and position owners. #### Winner inference and settlement Resolution uses strict winner inference: - Gamma must return a closed binary market with exactly two token IDs, two outcomes, and a binary `outcomePrices` shape. - If Gamma does not provide a strict result for the condition, the client falls back to CLOB `GET /markets/{condition_id}` and uses `tokens[].winner`. - Non-binary, ambiguous, malformed, or still-unresolved payloads are skipped. They remain on the watchlist until the poll window times out or a manual request resolves them. Auto-loading applies a strict outcome from either its normal lookup or positive closure probe immediately. An expiration that is future, stale, or missing does not discard an outcome already obtained. Without a strict outcome, existing expiration deadlines still apply: late subscriptions do not receive a fresh polling window, and missing expiration does not cause indefinite polling. When the client applies a resolution, position-owned legs emit one `InstrumentStatus` close and one `InstrumentClose`. Data-only legs emit whichever event types have active subscriptions. The winner leg closes at `1`, and the losing leg closes at `0`. The close type is `InstrumentCloseType.CONTRACT_EXPIRED`. This event closes Nautilus exposure and does not redeem tokens or claim funds on-chain. Deposit Wallet users can redeem winning tokens with [Position operations](#position-operations). #### Closure and subscription release Gamma's positive `closed=true` evidence stops normal quote, trade, and book-delta streams for both outcome siblings, even when the payload cannot produce usable instruments. Closure alone does not establish a winner or emit settlement events. Existing resolution owners remain available for polling or manual recovery; an enabled, unpaused resolution WebSocket may remain until resolution or timeout. Later live subscriptions cannot reopen the closed condition. The same apply path handles auto-load outcomes, WebSocket `market_resolved` events, automatic polling, and manual requests. Successful resolution emits each admitted owner's event types once, removes those owners and the condition's watch, and releases its WebSocket subscriptions. Existing pending data intents retain their outcome until admission or cancellation; new subscriptions cannot re-enroll the resolved condition. Automatic delivery never bypasses instrument-filter admission. #### Timeout, reconnect, and reset After `resolve_poll_max_wait_secs`, the watch pauses and releases resolution-only WebSocket ownership, including when polling is disabled. An open market's independent quote, trade, or book subscriptions are unaffected by this pause. The client retains settlement metadata and ownership for manual recovery; a manual request does not restart the automatic deadline. Disconnect stops network work, and reconnect resumes unfinished loading and replays cached, active resolution WebSocket subscriptions. Reset discards retained ownership and outcomes and requires fresh subscriptions. #### Manual resolution requests Use `request_data()` with data type `PolymarketResolveRequest` to force a resolution check. The request accepts any of these params: | Param | Type | Description | | ---------------- | -------------------- | ------------------------------------------------------------ | | `condition_id` | `str` | Resolve one Polymarket condition. | | `condition_ids` | `str` or `list[str]` | Resolve one or more Polymarket conditions. | | `instrument_ids` | `str` or `list[str]` | Resolve Polymarket instrument IDs; other venues are ignored. | If a request omits all selectors, the client uses the watchlist. With automatic polling enabled, the fallback selects paused or timed-out entries. With automatic polling disabled, it selects all expired eligible entries, so operators can run the recovery flow manually. The response payload is custom data with this dictionary shape: | Key | Meaning | | ---------------------------- | ------------------------------------------------------------------------- | | `requested_condition_ids` | Deduplicated condition IDs checked by the request. | | `fetched_markets` | Gamma markets returned across the batched lookup. | | `resolved_markets` | Conditions with a strict Gamma result or successful CLOB fallback result. | | `skipped_non_binary_markets` | Gamma markets skipped for non-binary or ambiguous resolution shape. | | `clob_fallback_successes` | Conditions resolved through the CLOB fallback path. | | `emitted_condition_ids` | Conditions that emitted at least one `InstrumentClose`. | | `failed_condition_ids` | Conditions where both Gamma and CLOB lookup failed. | | `used_watchlist_fallback` | Whether the request selected conditions from the watchlist. | | `timed_out_watchlist` | Timed-out watchlist entries seen during fallback selection. | | `error` | First summary error, if one occurred. | Redemption is a separate account or execution workflow. Do not extend the data client resolution path to claim funds; it only publishes market-outcome close events into Nautilus. ### Purging instruments at runtime Polymarket auto-loads instruments on demand, so a long-running session keeps growing the cache as markets resolve, new markets appear, and strategies cycle through events. Use `cache.purge_instrument` to drop markets the strategy no longer tracks. The call removes the instrument record and every cache-owned map keyed by it (order book, quotes, trades, bars). ```python class PolymarketHousekeeping(Strategy): def on_position_closed(self, event: PositionClosed) -> None: # Drop the market once the position is closed and you have no further interest. instrument_id = event.instrument_id self.unsubscribe_quotes(instrument_id) self.unsubscribe_book_deltas(instrument_id) self.cache.purge_instrument(instrument_id) ``` Common triggers on Polymarket: - A market resolves and produces no further trades. - An event ends and the strategy rotates off its markets. - The strategy rotates a fixed-size watchlist and drops the oldest entry. The purge skips any instrument that still has non-terminal orders (initialized, submitted, accepted, emulated, released, or inflight) or non-closed positions, so it is safe to call without coordinating with the execution client. Active WebSocket subscriptions belong to the data engine. Unsubscribe before purging if you no longer want updates. The cache also exposes `purge_order`, `purge_position`, `purge_closed_orders`, `purge_closed_positions`, and `purge_account_events` for trimming closed execution state. For long-running Polymarket nodes, schedule the bulk purges from `LiveExecutionEngineConfig` (15 min interval, 60 min buffer is a sensible default). See [Cache: purging cached data](../concepts/cache.md#purging-cached-data) for the full set. :::warning The caller decides when an instrument is no longer needed. Purging an instrument that another actor, strategy, or engine still relies on causes missing instrument lookups and loses market-data history. ::: ### Execution Before starting its WebSocket or initializing account state, the execution client queries unauthenticated `GET /version`. Startup continues only when the venue reports numeric version `2`. Any other version stops startup with an unsupported-version error; a missing, malformed, or errored response stops startup with a version-query failure. The execution adapter subscribes once to an account-wide `user` channel for order and trade events. It does not open market-channel subscriptions for instruments seen during trading. The shared WebSocket client logs a peer close code and reason before reconnecting. Malformed payload warnings and venue rejection reasons use the same bounded text handling as HTTP responses. Order rejections received through WebSocket or reconciliation use the same exact post-only classification as submit responses. #### Fill recovery and deduplication Matched WebSocket fills and their corrections are restored from cached order history and deduplicated across reconnects. If a trade arrives before its instrument is available, the adapter leaves it out of the dedup state. A redelivered event or later REST reconciliation can apply it after instrument loading completes. The adapter also constructs every owned fill report for a trade before emitting any of them or recording the trade as processed. If commission construction fails, it emits no fill for that trade and leaves its deduplication, confirmation, and terminal state unchanged. A duplicate or reconnect replay can retry the trade, while scheduled REST reconciliation remains the authoritative recovery path. #### Terminal quantity normalization For a fully matched order, terminal quantity normalization waits for every trade ID in the order's `associate_trades` list to confirm before lowering the order quantity to its actual fills. If a confirmed trade is recovered through REST after a WebSocket gap, reconciliation applies the same order-only normalization. If a `MATCHED` WebSocket update omits `associate_trades`, the adapter does not infer that settlement is final; the next REST reconciliation recovers the residual after the trade reaches `CONFIRMED`. ### Subscription limits Polymarket does not publish a WebSocket subscription cap in its current rate-limit documentation. `ws_max_subscriptions` (default 200) is therefore a conservative, self-chosen per-connection reliability bound rather than a venue-enforced limit: high per-connection subscription counts have been observed to silently stall a connection. The adapter enforces the bound by sharding asset subscriptions across a pool of market connections, opening a new connection only when the existing ones are full and closing a secondary connection once it owns no assets. ## Rate limiting Polymarket applies Cloudflare IP limits to its APIs and separate per-signer token buckets to CLOB order and cancellation requests. The adapter enforces the signer limits in process. All clients for one signer use the same limiter, which has independent order and cancellation buckets. ### Per-signer CLOB trading limits The adapter starts each signer at the Standard tier. Polymarket determines tier eligibility from the maker wallet's cumulative 30-day trading volume, even when the maker differs from the signer, and refreshes assignments every three hours. The adapter does not calculate eligibility: a recognized `Poly-RateLimit-Tier` response header selects one of these encoded profiles and updates both buckets, while an unknown tier is logged and ignored. | Tier | 30-day maker volume | Order rate (tokens/s) | Order burst | Cancel rate (tokens/s) | Cancel burst | Negative cancel balance | | -------- | ------------------- | --------------------: | ----------: | ---------------------: | -----------: | ----------------------- | | Standard | - | 40 | 60 | 80 | 120 | Yes | | Copper | $30,000+ | 60 | 90 | 120 | 180 | Yes | | Bronze | $50,000+ | 80 | 120 | 160 | 240 | Yes | | Silver | $100,000+ | 200 | 300 | 400 | 600 | Yes | | Gold | $500,000+ | 400 | 600 | 800 | 1,200 | Yes | | Platinum | $2.5M+ | 450 | 675 | 900 | 1,350 | No | | Diamond | $5M+ | 525 | 787 | 1,050 | 1,575 | No | | Elite | $10M+ | 600 | 900 | 1,200 | 1,800 | No | Covered requests consume: | Bucket | Request | Token cost | | ------------ | ------------------------------ | ---------------------------------------- | | Order | `POST /order` | 1 | | Order | `POST /orders` | Number of orders | | Cancellation | `DELETE /order` | 1 | | Cancellation | `DELETE /orders` | Number of submitted order IDs | | Cancellation | `DELETE /cancel-all` | 1 plus successful cancellations | | Cancellation | `DELETE /cancel-market-orders` | 1 plus successful matching cancellations | A request waits for its full token cost and is rejected locally only when that cost exceeds the current tier's burst. Before each new `DELETE /orders` chunk, the adapter recomputes its cap from the smaller of the endpoint's 1,000-ID limit and that burst. Cancel-all and cancel-market requests debit one token before the request, then debit each successful cancellation after the response. Standard through Gold tiers can enter cancellation debt; Platinum through Elite tiers floor the balance at zero. `Poly-RateLimit-Remaining` can lower the local balance, and `Poly-RateLimit-Reset` extends a rejected or indebted bucket's wait. The adapter logs `Poly-RateLimit-Warning` responses with the endpoint, token cost, tier, remaining balance, and reset time. A `429 Too Many Requests` response with `Retry-After` blocks the applicable bucket for at least that delay before retry. Without `Retry-After`, the retry manager uses its configured exponential backoff. Submit classification of 425 and 429 is in [Definitive and ambiguous outcomes](#definitive-and-ambiguous-outcomes). ### Selected IP-based REST limits Polymarket changes these quotas over time. As of 2026-08-04, the official limits are: | Endpoint | Burst (10s) | Sustained (10 min) | Notes | | ----------------------------------- | ----------- | ------------------ | ------------------------------------------- | | General rate limiting | 15,000 | - | Global documented rate limit. | | Health check (`/ok`) | 100 | - | Health endpoint. | | CLOB general | 9,000 | - | Aggregate across CLOB endpoints. | | CLOB `POST /order` | 5,000 | 120,000 | Single-order submit. | | CLOB `POST /orders` | 2,000 | 21,000 | Batch submit (up to 15 orders per request). | | CLOB `DELETE /order` | 5,000 | 120,000 | Single-order cancel. | | CLOB `DELETE /orders` | 2,000 | 15,000 | Batch cancel. | | CLOB `DELETE /cancel-all` | 250 | 6,000 | Cancel all orders. | | CLOB `DELETE /cancel-market-orders` | 1,500 | 21,000 | Cancel orders for one market. | | CLOB `GET /balance-allowance` | 200 | - | Balance and allowance queries. | | CLOB API key endpoints | 100 | - | Key management. | | Gamma general | 4,000 | - | Aggregate across Gamma endpoints. | | Gamma `/markets` | 300 | - | Market metadata. | | Gamma `/events` | 500 | - | Event metadata. | | Data general | 1,000 | - | Aggregate across Data API endpoints. | | Data `/trades` | 200 | - | Trade history. | | Data `/positions` | 150 | - | Current positions. | ### WebSocket limits The WebSocket quotas are not part of the published REST rate-limits table. The adapter enforces `ws_max_subscriptions` (default 200) by sharding subscriptions across a pool of market connections. :::warning Exceeding the IP-based limits triggers Cloudflare throttling. Requests are queued using sliding windows rather than rejected immediately, but sustained overshoot can result in HTTP 429 responses or temporary blocking. ::: For the latest limits, see the official Polymarket [CLOB trading rate limits](https://docs.polymarket.com/api-reference/trading-rate-limits) and [general rate limits](https://docs.polymarket.com/api-reference/rate-limits). ## Limitations and considerations - Reduce-only orders are not supported. - Batch submit (`POST /orders`) accepts at most 15 orders per request; the adapter splits larger `SubmitOrderList` commands into sequential 15-order chunks. - Batch cancel (`DELETE /orders`) accepts at most 1,000 order IDs per request; the adapter also limits each new chunk to the signer's current cancellation burst and recomputes that limit before the chunk. - Position reports omit balances below 0.01 shares. Do not treat an omitted report as proof that a dust position is flat; a sub-minimum residual cannot be exited through the market's minimum order size, which active markets commonly report as five shares. Position reconciliation therefore tolerates differences through 0.009999 shares and reconciles differences of 0.01 shares or more. ## Client configuration Rust structs and Python classes expose the same client configuration. The only Rust-only fields are the programmatic `filters` and `new_market_filter` trait objects on `PolymarketDataClientConfig`. ### Data client options Class/struct: `PolymarketDataClientConfig`. | Option | Default | Description | | -------------------------------------- | ---------- | ----------------------------------------------------------------------------------------- | | `instrument_config` | `None` | Bootstrap scope, passed as `PolymarketInstrumentProviderConfig`. | | `filters` | `[]` | Rust-only instrument filters applied during loading and discovery. | | `base_url_http`, `base_url_ws` | `None` | Override the CLOB HTTP or WebSocket endpoint. | | `base_url_gamma`, `base_url_data_api` | `None` | Override the Gamma or Data API endpoint. | | `base_url_rtds` | `None` | Override the RTDS endpoint. | | `proxy_url` | `None` | HTTP or HTTPS proxy for every data transport. | | `http_timeout_secs`, `ws_timeout_secs` | `60`, `30` | HTTP and WebSocket timeout in seconds. | | `ws_max_subscriptions` | `200` | Per-connection subscription cap; the market pool shards across connections at this bound. | | `update_instruments_interval_mins` | `60` | Instrument catalog refresh interval; pass `None` to disable it. | | `subscribe_new_markets` | `false` | Subscribe to discovery and resolution events; also enables `best_bid_ask` quote ticks. | | `new_market_filter` | `None` | Rust-only filter applied to newly discovered markets before instrument emission. | | `new_market_fetch_max_concurrency` | `8` | Bound concurrent market fetches from discovery events. | | `drop_quotes_missing_side` | `true` | Drop quotes that do not contain both a bid and an ask. | | `compute_effective_deltas` | `false` | Emit net snapshot changes when prior book state exists. | | `auto_load_missing_instruments` | `true` | Load unknown instruments for supported requests and subscriptions. | | `auto_load_debounce_ms` | `100` | Coalesce concurrent auto-load requests. | | `auto_load_max_retries` | `12` | Retry transient CLOB hydration misses; `0` disables retry. | | `auto_load_retry_delay_initial_secs` | `5.0` | Initial auto-load retry delay. | | `auto_load_retry_delay_max_secs` | `15.0` | Maximum auto-load retry delay. | | `resolve_poll_enabled` | `true` | Poll expired watched conditions for resolution. | | `resolve_poll_interval_secs` | `30` | Resolution polling interval. | | `resolve_poll_grace_secs` | `10` | Delay after expiry before polling begins. | | `resolve_poll_max_wait_secs` | `1,800` | Pause automatic polling after this wait. | | `transport_backend` | `Sockudo` | WebSocket transport implementation. | ### Execution client options Class/struct: `PolymarketExecutionClientConfig`. | Option | Default | Description | | --------------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------- | | `account_id` | `POLYMARKET-001` | Account identifier for this execution client. | | `private_key` | `POLYMARKET_PK` | EIP-712 signing key. | | `api_key`, `api_secret`, `passphrase` | environment variables | CLOB L2 authentication credentials. | | `funder` | `POLYMARKET_FUNDER` | Funding wallet; proxy and deposit-wallet signatures require it to differ from the signing address. | | `signature_type` | `Eoa` | `Eoa`, `PolyProxy`, `PolyGnosisSafe`, or `Poly1271`. | | `base_url_http`, `base_url_ws`, `base_url_data_api` | `None` | Override the respective production endpoint. | | `proxy_url` | `None` | HTTP or HTTPS proxy for every execution transport. | | `http_timeout_secs` | `60` | HTTP timeout in seconds. | | `max_retries` | `3` | Retries for single-order submit/cancel requests and for each batch-cancel chunk. | | `retry_delay_initial_ms` | `1,000` | Initial retry delay. | | `retry_delay_max_ms` | `10,000` | Maximum retry delay. | | `heartbeat_enabled` | `false` | Send an authenticated order-safety heartbeat immediately after execution readiness and every five seconds thereafter. | | `transport_backend` | `Sockudo` | WebSocket transport implementation. | | `instrument_config` | `None` | Same `PolymarketInstrumentProviderConfig` as the data client. Unmapped records use its `load_ids`. | #### Order-safety heartbeats Enable `heartbeat_enabled` for a dedicated automated execution process only when every order owned by its CLOB API credentials should be canceled if the process stops responding. Use dedicated credentials for each heartbeat-owning process. :::warning A normal disconnect stops heartbeats and causes cancellation after the venue timeout. Leave `heartbeat_enabled` disabled when orders must survive client shutdown or another process uses the same CLOB API credentials. ::: Enabling this option starts Polymarket's order-safety heartbeat contract for those credentials. Polymarket cancels their open orders when it does not receive a heartbeat within 10 seconds, with an additional 5-second buffer. The adapter sends the first empty heartbeat ID, chains each returned ID, and uses a replacement ID from an HTTP 400 response to resynchronize. The execution client reports as disconnected until the first heartbeat is acknowledged. It also reports as disconnected after any of these failures: - Authentication or venue rejection. - Two consecutive retryable request failures. - A request or retry delay that cannot finish with a one-second margin before the 10-second safety deadline. After such a failure, explicitly disconnect and reconnect the client to restore heartbeats. ### Proxy routing Set `proxy_url` to apply one HTTP or HTTPS proxy to every transport owned by that client. The data client routes CLOB HTTP, Gamma HTTP, Data API HTTP, the market WebSocket pool, and RTDS through the proxy. The execution client routes authenticated CLOB HTTP, Data API HTTP, and the authenticated user WebSocket through it. Configure the same value on both clients when running data and execution together. SOCKS URLs and malformed URLs fail configuration validation. When `proxy_url` is `None`, the adapter does not configure an explicit proxy: HTTP uses environment proxy settings and WebSockets connect directly. Treat credential-bearing proxy URLs as secrets because serialized configs contain the supplied URL. Python exposes only `has_proxy_url`; configuration `Debug` output and transport diagnostics redact proxy credentials. ### Instrument provider options Pass the same `PolymarketInstrumentProviderConfig` as `instrument_config` on the data client config and the execution client config. `load_ids` is the only reconciliation scope. When that set is non-empty, unmapped records outside it are expected absences. When `load_ids` is unset or empty, every unmapped open order and position is in scope and fails the report request. `event_slugs`, `market_slugs`, `series_ids`, `filters`, and `event_slug_builder` discover instruments; they do not classify unmapped records. A node that scopes discovery with those fields and still wants scoped reconciliation must also set `load_ids`. | Option | Default | Description | | -------------------- | ------- | ------------------------------------------------------- | | `load_all` | `false` | Load the full venue catalog at startup. | | `load_ids` | `None` | Load exact Nautilus instrument IDs. | | `filters` | `None` | Validated Gamma market keyset filters. | | `event_slugs` | `None` | Resolve all markets for the listed events at bootstrap. | | `market_slugs` | `None` | Load the listed Gamma market slugs at bootstrap. | | `event_slug_builder` | `None` | Rust-backed Up/Down event-slug generator. | | `series_ids` | `None` | Load markets for the listed Gamma series at bootstrap. | | `log_warnings` | `true` | Emit provider warnings. | | `use_gamma_markets` | `false` | Reserved compatibility field with no additional effect. | #### Gamma query filters The adapter uses the Gamma market and event keyset endpoints. It validates filters before the first HTTP request, follows `next_cursor`, and applies the endpoint page ceilings of 100 markets and 500 events. Market keyset fields: | Class | Fields | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Scalar | `limit`, `order`, `ascending`, `closed`, `decimalized`, `liquidity_num_min`, `liquidity_num_max`, `volume_num_min`, `volume_num_max`, `start_date_min`, `start_date_max`, `end_date_min`, `end_date_max`, `related_tags`, `tag_match`, `cyom`, `rfq_enabled`, `uma_resolution_status`, `game_id`, `include_tag`, `locale` | | Repeated | `id`, `slug`, `clob_token_ids`, `condition_ids`, `question_ids`, `market_maker_address`, `tag_id`, `sports_market_types` | | Compatibility | `active`, `archived` | | Alias | `is_active` | | Client only | `offset`, `max_markets` | The provider `filters` dictionary accepts only market fields. Rust callers configure event discovery with `EventParamsFilter` and `GetGammaEventsParams`; event-only fields such as `live` or `tag_slug` are not valid provider dictionary keys. Event keyset fields: | Class | Fields | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Scalar | `limit`, `order`, `ascending`, `closed`, `live`, `featured`, `cyom`, `title_search`, `liquidity_min`, `liquidity_max`, `volume_min`, `volume_max`, `start_date_min`, `start_date_max`, `end_date_min`, `end_date_max`, `start_time_min`, `start_time_max`, `tag_slug`, `related_tags`, `tag_match`, `event_date`, `event_week`, `featured_order`, `recurrence`, `parent_event_id`, `include_children`, `partner_slug`, `include_chat`, `include_template`, `include_best_lines`, `locale` | | Repeated | `id`, `slug`, `tag_id`, `exclude_tag_id`, `series_id`, `game_id`, `created_by` | | Compatibility | `active`, `archived` | | Client only | `offset`, `max_events` | Repeated fields are sent as repeated query keys. `offset` is applied across returned keyset pages and is never sent to Gamma. `max_markets` caps markets locally, with each binary market normally producing two instruments. `max_events` caps events locally; each event can contain many markets. `condition_ids` accepts at most 100 values, and event `tag_id` values cannot overlap `exclude_tag_id` values. The provider `filters` dictionary accepts strings in the native Rust config and also accepts Python `bool`, `int`, finite `float`, string, or lists of those scalar values when converting a mapping-shaped Python config. The Python conversion ignores `None` entries; native config entries must be strings. `is_active=true` supplies `active=true`, `archived=false`, and `closed=false`; explicit values override those defaults. Unknown keys, malformed values, empty lists, invalid date or numeric bounds, and invalid combinations raise `ValueError` during Python config conversion. See the official [market keyset](https://docs.polymarket.com/api-reference/markets/list-markets-keyset-pagination) and [event keyset](https://docs.polymarket.com/api-reference/events/list-events-keyset-pagination) references for the venue contract. #### Filter scopes Filters come in two forms: the `filters` map on `PolymarketInstrumentProviderConfig`, and Rust `InstrumentFilter`s registered on the client. Registered filters take precedence: when both are present the `filters` map is ignored and the provider logs a warning. A filter that sources markets is a complete bootstrap scope on its own and does not need `load_all` or a slug or series scope alongside it. A registered filter sources markets when it supplies any of: - Market or event slugs - Gamma market query params - Gamma event params - Search params A non-empty `filters` map qualifies on the same basis. A filter that only accepts or rejects instruments, such as `PredicateFilter`, refines another source's results and still needs one of those alongside it. #### Event slug builder The adapter treats Python as a configuration, factory, and user strategy boundary. Provider, data, and execution operations run in Rust. `event_slug_builder` therefore accepts a Rust-backed `PolymarketUpDownEventSlugConfig`; it does not accept Python callable paths. Use this for predictable Polymarket Up/Down event slugs without downloading the full venue catalog. The builder emits slugs with the pattern `{asset}-updown-{interval_mins}m-{unix_timestamp}` for the configured window of aligned periods. ```python from nautilus_trader.adapters.polymarket import PolymarketInstrumentProviderConfig from nautilus_trader.adapters.polymarket import PolymarketUpDownEventSlugConfig instrument_config = PolymarketInstrumentProviderConfig( event_slug_builder=PolymarketUpDownEventSlugConfig( assets=["btc"], interval_mins=5, periods=3, start_offset_periods=0, ), ) ``` For custom event patterns, pass explicit `event_slugs`, pass direct `market_slugs`, scope by `series_ids`, or add a Rust filter or builder. The adapter rejects Python callable `event_slug_builder` values so adapter operations do not cross into Python during live trading. #### Series IDs A Gamma *series* groups a recurring market family, such as the 5-minute Up/Down crypto intervals or a daily weather market. Scoping by `series_ids` loads the markets of every active, unresolved event in those series, which avoids reconstructing slugs client-side as each interval rolls over: ```python from nautilus_trader.adapters.polymarket import PolymarketInstrumentProviderConfig instrument_config = PolymarketInstrumentProviderConfig( series_ids=[10684, 10192], ) ``` The provider resolves each series through the Gamma events endpoint with `active=true` and `closed=false`, then loads the markets of the matching events. Because the query is re-evaluated on every refresh, pairing `series_ids` with `update_instruments_interval_mins` on the data client keeps a rolling family of markets current without any slug arithmetic. Find the series ID for a market family in the `series` field of its Gamma event payload. ## Python discovery and historical data The Python package exports a Rust-backed `PolymarketDataLoader` for public discovery, instrument construction, and historical trades. It uses the Rust Gamma, CLOB, and Data API clients, so it does not require trading credentials or run networking in Python. All network methods are asynchronous. Build a loader from a market slug and select its outcome token by index: ```python from nautilus_trader.adapters.polymarket import PolymarketDataLoader loader = await PolymarketDataLoader.from_market_slug( "will-jd-vance-win-the-2028-us-presidential-election", token_index=0, ) instrument = loader.instrument token_id = loader.token_id condition_id = loader.condition_id ``` `instrument` is a normalized `BinaryOption`. When the source fields are available, resolution data is retained as follows: | Data | `instrument.info` | `resolution_metadata` | | -------------------------- | ---------------------- | ------------------------------------ | | Market description | `description` | - | | Event start | `event_start_time` | - | | Market end | `end_date` | - | | Resolution source | `resolution_source` | `resolutionSource` | | Crypto resolution config | `crypto_market_config` | - | | Raw Gamma market JSON | - | `gamma_market` | | Raw Gamma event JSON | - | `gamma_event` (event loading) | | Closed state | - | `closed` | | Closure time | - | `closedTime` | | UMA resolution status | - | `umaResolutionStatus` | | Token outcome/winner state | - | `tokens` with `outcome` and `winner` | Read `resolution_metadata` after a backtest or simulation to inspect the lifecycle snapshot: ```python metadata = loader.resolution_metadata winner = next( (token["outcome"] for token in metadata["tokens"] if token["winner"]), None, ) ``` An event factory returns one loader for each market in the event: ```python loaders = await PolymarketDataLoader.from_event_slug( "how-many-fed-rate-cuts-in-2026", token_index=1, ) ``` A negative token index or an index outside a market's token list raises `ValueError`. Construction also fails clearly when Gamma has no matching slug or CLOB has not populated usable token IDs. ### Public discovery Static query methods return stable Python mappings and lists while Rust owns validation and pagination. JSON values map to Python as follows: | JSON value | Python value | Scope | | ----------------- | ----------------- | ------------------------------------------------------------------------ | | Fractional number | `decimal.Decimal` | Includes nested event markets, fee schedules, and CLOB rewards | | Integer | `int` | A financial field can be `int` or `Decimal`, depending on its JSON token | | String | `str` | JSON-encoded strings such as `outcomePrices` are not parsed further | | Null | `None` | Preserves absence | Use decimal operands when calculating with these values; Python does not mix `Decimal` and `float` arithmetic. The Gamma competitiveness score is returned as `Decimal` after an approximate Rust floating-point conversion. ```python market = await PolymarketDataLoader.query_market_by_slug("some-market") details = await PolymarketDataLoader.query_market_details(market["conditionId"]) event = await PolymarketDataLoader.query_event_by_slug("some-event") markets = await PolymarketDataLoader.query_markets( filters={ "is_active": True, "tag_id": [21, 42], "order": "volume", "max_markets": 200, }, ) events = await PolymarketDataLoader.query_events( filters={ "active": True, "closed": False, "max_events": 100, }, ) tags = await PolymarketDataLoader.query_tags() results = await PolymarketDataLoader.query_search( "bitcoin", events_status="active", limit_per_type=20, ) ``` Market and event filter dictionaries use the fields listed under [Gamma query filters](#gamma-query-filters). The provider config accepts only the market fields, while `query_events` accepts the event fields. Unknown or malformed filters raise `ValueError` before any request. ### Historical trades `load_trades` returns normalized `TradeTick` objects in chronological order: ```python from datetime import UTC, datetime, timedelta end = datetime.now(UTC) start = end - timedelta(days=1) trades = await loader.load_trades( start=start, end=end, limit=1_000, ) ``` The window is inclusive. The Data API records trade timestamps in whole seconds, so Rust keeps all trades in the `start` and `end` boundary seconds. The public API caps offset-based pagination at 10,000: | Request | Meaning of `limit` | Behavior at the pagination ceiling | | --------------- | -------------------------------------- | -------------------------------------------------------- | | With `start` | Earliest matching trades in the window | Error; completeness from the requested start is unproven | | Without `start` | Most recent matching trades | Available partial result and a warning | If a start-anchored request reaches the ceiling, narrow the time window and retry. ### Closed market cleanup Gamma `endDate` is a scheduled end, not proof that trading stopped. The client keeps cached instruments while Gamma reports `closed=false` and removes live state after a positive `closed=true`. The closure check runs on every resolve-poll tick for expired cached instruments still reported open. It retries failed requests on the next tick, so request failures or delayed venue data can delay retirement beyond one cycle. A failed condition ID batch does not discard the closures confirmed by the other batches. If both Gamma lookups omit a market, the client keeps it because closure was not observed. Only live instruments carry this state. The historical data loader reports terminal state through `resolution_metadata` instead, so a backtest cannot see a market's current closure through `instrument.info`. ## Contributing To contribute features or fixes to the Polymarket adapter, see the [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). # Tardis Source: https://nautilustrader.io/docs/latest/integrations/tardis/ Tardis provides granular cryptocurrency market data, including tick-by-tick order book snapshots and updates, trades, open interest, funding rates, option summaries, and liquidations. NautilusTrader integrates with the Tardis API, Tardis Machine WebSocket server, and Tardis CSV formats. The capabilities of this adapter include: - CSV loading and streaming functions read Tardis-format files into Nautilus data in bulk or bounded chunks. - `run_tardis_machine_replay` replays historical data and writes Nautilus Parquet catalog files. - `TardisDataClientConfig` and `TardisDataClientFactory` connect a Nautilus node to a configured historical replay or real-time Tardis Machine stream. - Python and Rust expose `TardisMachineClient` and `TardisHttpClient` for lower-level access to normalized streams and instrument metadata. :::info A `TARDIS_API_KEY` is required for Nautilus instrument metadata calls. Tardis Machine uses `TM_API_KEY` for historical dates outside the free first day of each month. See also [environment variables](#environment-variables). ::: ## Overview The adapter is implemented in Rust with optional Python bindings. Its components are compiled into NautilusTrader, so it does not require a separate Tardis client library installation. Consult the [Tardis documentation](https://docs.tardis.dev/) for the upstream APIs, formats, and server. ## Supported formats Tardis provides *normalized* market data, a unified format consistent across supported exchanges. This normalization lets one parser handle data from any [Tardis-supported exchange](#venues). NautilusTrader does not support exchange-native Tardis market data formats in this adapter. The following normalized Tardis Machine formats are supported by NautilusTrader. See the official [Tardis data type reference](https://docs.tardis.dev/tardis-machine/data-types) for field schemas. | Tardis format | Nautilus data type | | :------------------ | :------------------------------------------------------------ | | `book_change` | `OrderBookDeltas` | | `book_snapshot_*` | `OrderBookDepth10` or `OrderBookDeltas` | | `quote` | `QuoteTick` | | `quote_10s` | `QuoteTick` | | `trade` | `TradeTick` | | `trade_bar_*` | `Bar` | | `derivative_ticker` | `FundingRateUpdate`, `MarkPriceUpdate`, or `IndexPriceUpdate` | | `option_summary` | `OptionGreeks`; optional `QuoteTick` from BBO fields | | `disconnect` | Ignored | **Notes:** - Tardis documents `quote` as an alias for `book_snapshot_1_0ms`. - Tardis documents `quote_10s` as an alias for `book_snapshot_1_10s`. - `quote`, `quote_10s`, and one-level snapshots are parsed as `QuoteTick`. - The data client emits funding rate, mark price, and index price updates from `derivative_ticker` messages only when their values change. The catalog replay pipeline does not write these updates. - Tardis `option_summary` messages include best bid/offer fields. Nautilus always maps this feed to `OptionGreeks`; set `extract_bbo_as_quotes` to `true` to also emit `QuoteTick` from those BBO fields. - The adapter does not parse the Tardis `book_ticker`, `liquidation`, or `error` normalized formats. :::info See also the Tardis [Tardis Machine quickstart](https://docs.tardis.dev/tardis-machine/quickstart). ::: ## Bars The adapter converts Tardis trade bar intervals and suffixes to Nautilus `BarType`s. This includes the following: | Tardis suffix | Meaning | Nautilus bar aggregation | | :------------ | :-------------- | :------------------------- | | `ms` | Milliseconds | `MILLISECOND` | | `s` | Seconds | `SECOND` | | `m` | Minutes | `MINUTE`, `HOUR`, or `DAY` | | `ticks` | Number of ticks | `TICK` | | `vol` | Volume size | `VOLUME` | Minute intervals that divide evenly into hours or days use the canonical Nautilus `HOUR` or `DAY` aggregation. ## Symbology and normalization The Tardis integration ensures compatibility with NautilusTrader's crypto exchange adapters by consistently normalizing symbols. Typically, NautilusTrader uses the native exchange naming conventions provided by Tardis. For certain exchanges, raw symbols are adjusted to adhere to Nautilus symbology normalization, as outlined below: ### Common rules - All symbols are converted to uppercase. - Market type suffixes are appended with a hyphen for some exchanges. - Original exchange symbols are preserved in the Nautilus instrument definitions `raw_symbol` field. ### Exchange-specific normalizations - **Binance**: Nautilus appends the suffix `-PERP` to perpetual symbols from `binance`, `binance-futures`, `binance-us`, `binance-dex`, and `binance-jersey`. - **Bybit**: Nautilus uses product category suffixes, including `-SPOT`, `-LINEAR`, `-INVERSE`, and `-OPTION`. - **dYdX v3**: Nautilus appends the suffix `-PERP` to perpetual symbols from `dydx`. - **Gate.io**: Nautilus appends the suffix `-PERP` to perpetual symbols from `gate-io-futures`. - **MEXC**: Nautilus appends the suffix `-PERP` to perpetual symbols from `mexc-futures`. For detailed symbology documentation per exchange: - [Binance symbology](./binance.md#symbology) - [Bybit symbology](./bybit.md#symbology) - [dYdX symbology](./dydx.md#symbology) ## Venues Some exchanges on Tardis are partitioned into multiple venues. The table below outlines the mappings between Nautilus venues and corresponding Tardis exchanges: | Nautilus venue | Tardis exchange(s) | | :----------------- | :----------------------------------------------------------------------------------------------------------- | | `ASCENDEX` | `ascendex` | | `BINANCE` | `binance`, `binance-dex`, `binance-european-options`, `binance-futures`, `binance-jersey`, `binance-options` | | `BINANCE_DELIVERY` | `binance-delivery` (*COIN-margined contracts*) | | `BINANCE_US` | `binance-us` | | `BITFINEX` | `bitfinex`, `bitfinex-derivatives` | | `BITFLYER` | `bitflyer` | | `BITGET` | `bitget`, `bitget-futures` | | `BITMEX` | `bitmex` | | `BITNOMIAL` | `bitnomial` | | `BITSTAMP` | `bitstamp` | | `BLOCKCHAIN_COM` | `blockchain-com` | | `BYBIT` | `bybit`, `bybit-options`, `bybit-spot` | | `COINBASE` | `coinbase` | | `COINBASE_INTX` | `coinbase-international` | | `COINFLEX` | `coinflex` (*historical data only*) | | `CRYPTO_COM` | `crypto-com` | | `CRYPTOFACILITIES` | `cryptofacilities` | | `DELTA` | `delta` | | `DERIBIT` | `deribit` | | `DYDX` | `dydx` | | `DYDX_V4` | `dydx-v4` | | `FTX` | `ftx`, `ftx-us` (*historical data only*) | | `GATE_IO` | `gate-io`, `gate-io-futures` | | `GEMINI` | `gemini` | | `HITBTC` | `hitbtc` | | `HUOBI` | `huobi`, `huobi-dm`, `huobi-dm-linear-swap`, `huobi-dm-options` | | `HUOBI_DELIVERY` | `huobi-dm-swap` | | `HYPERLIQUID` | `hyperliquid` | | `KRAKEN` | `kraken` | | `KUCOIN` | `kucoin`, `kucoin-futures` | | `LIGHTER` | `lighter` | | `MANGO` | `mango` | | `MEXC` | `mexc`, `mexc-futures` | | `OKCOIN` | `okcoin` | | `OKEX` | `okex`, `okex-futures`, `okex-options`, `okex-spreads`, `okex-swap` | | `PHEMEX` | `phemex` | | `POLONIEX` | `poloniex` | | `SERUM` | `serum` (*historical data only*) | | `STAR_ATLAS` | `star-atlas` | | `UPBIT` | `upbit` | | `WOO_X` | `woo-x` | Some exchange IDs represent delisted venues retained for historical data. Consult the official [historical data details](https://docs.tardis.dev/historical-data-details) for availability and delisting status. ## Environment variables The following environment variables are used by Tardis and NautilusTrader. - `TM_API_KEY`: API key passed to the Tardis Machine process for historical data access. - `TARDIS_API_KEY`: API key for Nautilus instrument metadata requests. - `TARDIS_MACHINE_WS_URL` (optional): Tardis Machine WebSocket base URL. - `NAUTILUS_PATH` (optional): Parent directory containing the `catalog/` subdirectory for replay output. The Tardis instruments metadata API requires bearer-token authorization and is available to active pro and business Tardis subscriptions. ## Running Tardis Machine historical replays The [Tardis Machine Server](https://docs.tardis.dev/tardis-machine/quickstart) is a locally runnable server with built-in data caching. It provides tick-level historical and consolidated real-time cryptocurrency market data through HTTP and WebSocket APIs. You can run complete Tardis Machine WebSocket replays from Python or Rust and write the results in Nautilus Parquet format. Both interfaces call the same Rust replay implementation. The end-to-end `run_tardis_machine_replay` data pipeline function uses a specified [configuration](#configuration) to execute the following steps: - Connect to the Tardis Machine server. - Request and parse all instrument definitions for the configured exchanges from the Tardis instruments metadata API. - Stream all requested instruments and data types for the specified time ranges from Tardis Machine. - For each data type and date (UTC), write catalog-compatible `.parquet` files by instrument or bar type. - Finish the stream and flush the remaining data to disk. ### Output files Files are written one per UTC day and instrument, or per bar type, using ISO 8601 timestamp ranges: - **Format**: `{start_timestamp}_{end_timestamp}.parquet` - **Example**: `2023-10-01T00-00-00-000000000Z_2023-10-01T23-59-59-999999999Z.parquet` - **Relative path**: `{data_type}/{instrument_id}/{filename}`, or `bars/{bar_type}/{filename}` for bars. This format is compatible with Nautilus data catalog queries, consolidation, and management. :::note You can request data for the first day of each month without a Tardis Machine API key. Other dates require `TM_API_KEY`. ::: This process is optimized for direct output to a Nautilus Parquet data catalog. Set `NAUTILUS_PATH` to the parent directory that contains the `catalog/` subdirectory. Parquet files are written under `/catalog/data/` in subdirectories by data type and instrument or bar type. If no `output_path` is specified and `NAUTILUS_PATH` is unset, output defaults to the current working directory. ### Procedure :::warning Do not publish Tardis Machine ports on the host address `0.0.0.0`. Docker [publishes ports on all host interfaces by default](https://docs.docker.com/engine/network/port-publishing/) when a mapping omits the host address. On Linux, Docker [diverts published container traffic before `ufw` applies its rules](https://docs.docker.com/engine/network/packet-filtering-firewalls/#docker-and-ufw), which can bypass the expected firewall restrictions. Bind both ports to `127.0.0.1` unless you require and separately secure remote access. ::: For dates outside the free first day of each month, set `TM_API_KEY` in the host environment. Then start the `tardis-machine` Docker container: ```bash docker run \ -p 127.0.0.1:8000:8000 \ -p 127.0.0.1:8001:8001 \ -e TM_API_KEY \ -d tardisdev/tardis-machine ``` This command starts the `tardis-machine` server without a persistent local cache, which may affect performance. For better replay performance, run it with a persistent volume. ### Configuration Next, ensure you have a configuration JSON file available. **Configuration JSON fields** - `tardis_ws_url` (`str | null`): Tardis Machine WebSocket URL. Defaults to `TARDIS_MACHINE_WS_URL`. - `normalize_symbols` (`bool | null`): applies Nautilus symbol normalization. Defaults to `true`. - `output_path` (`str | null`): output directory for Parquet data. When unset, uses `/catalog/data` if `NAUTILUS_PATH` is set, then the current working directory. - `book_snapshot_output` (`"deltas" | "depth10" | null`): output format for snapshots. Defaults to `"deltas"`. - `extract_bbo_as_quotes` (`bool | null`): also writes `QuoteTick` data from best bid/offer fields in Tardis Machine `option_summary` messages. Defaults to `false`. - `compression` (`"zstd" | "snappy" | "uncompressed" | null`): Parquet compression codec. Defaults to `"zstd"` level 3. - `proxy_url` (`str | null`): proxy URL for Tardis HTTP requests. Defaults to no proxy. - `options` (`JSON[]`): required replay request option objects. An example configuration file is available at `crates/adapters/tardis/bin/example_config.json`: ```json { "tardis_ws_url": "ws://localhost:8001", "output_path": null, "options": [ { "exchange": "bitmex", "symbols": [ "xbtusd", "ethusd" ], "data_types": [ "trade" ], "from": "2019-10-01", "to": "2019-10-02" } ] } ``` ### Book snapshot output The `book_snapshot_output` configuration option controls how Tardis `book_snapshot_*` messages are converted and stored. | Value | Nautilus type | Output directory | Description | | :-------- | :----------------- | :------------------- | :-------------------------------------- | | `deltas` | `OrderBookDeltas` | `order_book_deltas/` | Clear and add deltas for each snapshot. | | `depth10` | `OrderBookDepth10` | `order_book_depths/` | Snapshots with up to 10 price levels. | **When to use each format:** - **`deltas` (default)**: use when you need to reconstruct book state or combine snapshots with `book_change` data. Each snapshot becomes a clear delta followed by an add delta for each level. - **`depth10`**: use when a strategy needs periodic depth snapshots. Each snapshot is a single record, and snapshots with more than 10 levels keep only the first 10. **Avoiding file overwrites:** When downloading both `book_snapshot_*` and `book_change` data for the same instrument and date range, `depth10` writes snapshots to `order_book_depths/` and avoids overwriting `order_book_deltas/`. Example configuration with explicit format: ```json { "tardis_ws_url": "ws://localhost:8001", "book_snapshot_output": "depth10", "options": [ { "exchange": "binance-futures", "symbols": ["btcusdt"], "data_types": ["book_snapshot_5_100ms", "book_change"], "from": "2024-01-01", "to": "2024-01-02" } ] } ``` ### Option summary BBO extraction Set `extract_bbo_as_quotes` to `true` when requesting Tardis Machine `option_summary` data and the backtest also needs option BBO quotes. Nautilus still writes `OptionGreeks` from every `option_summary` message. When all best bid/offer fields are present and sizes are valid, it also writes a `QuoteTick` for the same instrument and timestamps. This option only applies to Tardis Machine `option_summary` replay and stream messages. It does not change Tardis CSV loading. ```json { "tardis_ws_url": "ws://localhost:8001", "extract_bbo_as_quotes": true, "options": [ { "exchange": "deribit", "symbols": ["BTC-28JUN24-70000-C"], "data_types": ["option_summary"], "from": "2024-01-01", "to": "2024-01-02" } ] } ``` ### Python replays To run a replay in Python, create a script similar to the following: ```python import asyncio from pathlib import Path from nautilus_trader.adapters.tardis import run_tardis_machine_replay async def run(): config_filepath = Path("YOUR_CONFIG_FILEPATH") await run_tardis_machine_replay(str(config_filepath.resolve())) if __name__ == "__main__": asyncio.run(run()) ``` ### Rust replays To run a replay in Rust, create a binary similar to the following: ```rust use std::path::PathBuf; use nautilus_tardis::replay::run_tardis_machine_replay_from_config; #[tokio::main] async fn main() -> Result<(), Box> { nautilus_common::logging::ensure_logging_initialized(); let config_filepath = PathBuf::from("YOUR_CONFIG_FILEPATH"); run_tardis_machine_replay_from_config(&config_filepath).await?; Ok(()) } ``` Logging defaults to INFO level. To enable debug logging, export the following environment variable: ```bash export NAUTILUS_LOG=debug ``` A working example binary is available at `crates/adapters/tardis/bin/example_replay.rs`. This can also be run using cargo: ```bash cargo run -p nautilus-tardis --bin tardis-replay ``` ### Option-chain backtest catalog An option-chain backtest starts after the Tardis replay has written data to the Nautilus catalog. The backtest loader does not request missing Tardis data during a run, so the catalog must contain: - Option instruments from the Tardis instrument metadata API. - `QuoteTick` data from one-level option book snapshots, quote data, or `option_summary` BBO extraction. - `OptionGreeks` data from Tardis `option_summary` messages. Use both `QuoteTick` and `OptionGreeks` in the `BacktestDataConfig` list for the same option instrument IDs. The option-chain manager aggregates the replayed BBO and Greeks into `OptionChainSlice` snapshots. Use `snapshot_interval_ms=None` for raw publishing, or set an interval in milliseconds to publish thinned snapshots. Strategies can select contracts by moneyness with ATM-relative or ATM-percent strike ranges, by delta with `StrikeRange.delta(target, tolerance)`, or by fixed strike with `StrikeRange.fixed([...])`. Option order matching in backtests is quote-driven: marketable orders fill as takers against the opposing BBO, while passive limits can fill as makers when later BBO updates trade through the limit. Configure option fees explicitly on the simulated venue with structural fee models such as `CappedOptionFeeModel` or `TieredNotionalOptionFeeModel`. There is no automatic Tardis exchange to fee model mapping. ### Option-chain CSV catalog conversion For historical option chains from downloadable Tardis CSV files, use `convert_tardis_options_chain_csv(...)` to convert `options_chain` rows into Nautilus catalog data. This path does not call Tardis Machine or the instrument metadata API, so it is useful when you already have Tardis CSV files or want a no-API-key catalog bootstrap from downloaded data. The converter writes `OptionGreeks` for every selected row. With the default `extract_bbo_as_quotes=True`, complete best bid/offer rows also write `QuoteTick`. Keep this enabled for option-chain backtests: greeks-only catalogs do not provide quotes, so the chain manager cannot publish populated `OptionChainSlice` snapshots for strikes without BBO data. Instrument derivation supports only Deribit options. For other option venues, set `write_instruments=False` before conversion and load the instruments through another source before backtesting. Leaving it enabled for a non-Deribit file can fail after data files have been written to the catalog. Pass daily `options_chain` CSV paths in chronological order. The `underlyings` filter matches symbol prefixes such as `["BTC-"]`. Set `snapshot_interval_ms` to keep the last row per instrument per interval within each input file, or use `None` to write every selected row. Rows must be ordered by `local_timestamp` within each file when thinning. Provide explicit `price_precision` and `size_precision` for deterministic quote metadata. Inferred precision can increase as later rows are read, so data written earlier in a file can keep lower precision metadata. ```python from pathlib import Path from nautilus_trader.adapters.tardis import convert_tardis_options_chain_csv convert_tardis_options_chain_csv( filepaths=[Path("deribit_options_chain_2020-06-08.csv")], catalog_path=Path("catalog"), underlyings=["BTC-"], snapshot_interval_ms=60_000, price_precision=4, size_precision=1, ) ``` ## Loading Tardis CSV data Tardis-format CSV data can be loaded using either Python or Rust. The loader reads the CSV text data from disk and parses it into Nautilus data. Both interfaces call the same Rust loader. You can also specify a `limit` parameter for the `load_*` functions to control the maximum number of rows loaded. :::note Loading mixed-instrument CSV files is challenging due to precision requirements and is not recommended. Use single-instrument CSV files instead. The `load_tardis_options_chain`, `stream_tardis_options_chain`, and `convert_tardis_options_chain_csv` functions are the exception: Tardis `options_chain` files are mixed-instrument chain files, and these paths track precision per instrument. Explicit precisions are still recommended for deterministic output. ::: ### Loading CSV data in Python You can load Tardis-format CSV data in Python using the module-level `load_tardis_*` functions. When loading data, you can optionally specify the instrument ID, price precision, and size precision. Providing the instrument ID improves loading performance. Price and size precision are inferred from the CSV when omitted, but explicit values are recommended for deterministic output, especially with large files. To load the data, create a script similar to the following: ```python from pathlib import Path from nautilus_trader.adapters.tardis import load_tardis_deltas from nautilus_trader.model import InstrumentId instrument_id = InstrumentId.from_str("BTC-PERPETUAL.DERIBIT") deltas = load_tardis_deltas( filepath=Path("YOUR_CSV_DATA_PATH"), price_precision=1, size_precision=0, instrument_id=instrument_id, ) ``` ### Loading CSV data in Rust You can load Tardis-format CSV data in Rust using the loading functions in `crates/adapters/tardis/src/csv/mod.rs`. When loading data, you can optionally specify the instrument ID, price precision, and size precision. Providing the instrument ID improves loading performance. Price and size precision are inferred from the CSV when omitted, but explicit values are recommended for deterministic output. For a complete example, see `crates/adapters/tardis/bin/example_csv.rs`. To load the data, you can use code similar to the following: ```rust use std::path::Path; use nautilus_model::identifiers::InstrumentId; use nautilus_tardis::csv::load_deltas; fn main() -> Result<(), Box> { // Optionally specify precisions and the CSV filepath let price_precision = Some(1); let size_precision = Some(0); let filepath = Path::new("YOUR_CSV_DATA_PATH"); // Optionally specify an instrument ID and/or limit let instrument_id = InstrumentId::from("BTC-PERPETUAL.DERIBIT"); let limit = None; let _deltas = load_deltas( filepath, price_precision, size_precision, Some(instrument_id), limit, )?; Ok(()) } ``` ## Streaming Tardis CSV data For memory-efficient processing of large CSV files, the Tardis integration can load and process data in configurable chunks rather than loading entire files into memory at once. This is useful for processing multi-gigabyte CSV files without exhausting system memory. Python provides streaming functions for the following CSV data: - Order book deltas (`stream_tardis_deltas` and `stream_tardis_batched_deltas`). - Order book depth snapshots (`stream_tardis_depth10_from_snapshot5` and `stream_tardis_depth10_from_snapshot25`). - Quote ticks (`stream_tardis_quotes`). - Trade ticks (`stream_tardis_trades`). - Funding rates (`stream_tardis_funding_rates`). - Options chain rows (`stream_tardis_options_chain`). Rust exposes the equivalent `stream_*` functions. ### Streaming CSV data in Python The module-level `stream_tardis_*` functions return iterators of bounded chunks. The `chunk_size` parameter accepts values in `[1, 1_000_000]` and controls how many records are read per chunk: ```python from pathlib import Path from nautilus_trader.adapters.tardis import stream_tardis_trades from nautilus_trader.model import InstrumentId instrument_id = InstrumentId.from_str("BTC-PERPETUAL.DERIBIT") filepath = Path("large_trades_file.csv") trades = stream_tardis_trades( filepath=filepath, chunk_size=100_000, price_precision=1, size_precision=0, instrument_id=instrument_id, ) # Stream trade ticks in chunks for chunk in trades: print(f"Processing chunk with {len(chunk)} trades") # Process each chunk - only this chunk is in memory for trade in chunk: # Your processing logic here pass ``` ### Streaming order book data For order book data, streaming is available for both deltas and depth snapshots: ```python from pathlib import Path from nautilus_trader.adapters.tardis import stream_tardis_deltas from nautilus_trader.adapters.tardis import stream_tardis_depth10_from_snapshot5 filepath = Path("book_snapshot_5.csv") # Stream order book deltas for chunk in stream_tardis_deltas(filepath): print(f"Processing {len(chunk)} deltas") # Process delta chunk # Stream depth10 snapshots from snapshot_5 files for chunk in stream_tardis_depth10_from_snapshot5(filepath): print(f"Processing {len(chunk)} depth snapshots") # Process depth chunk ``` ### Streaming quote data Quote data can be streamed similarly: ```python from pathlib import Path from nautilus_trader.adapters.tardis import stream_tardis_quotes filepath = Path("quotes.csv") # Stream quote ticks for chunk in stream_tardis_quotes(filepath): print(f"Processing {len(chunk)} quotes") # Process quote chunk ``` ### Memory use Streaming bounds the number of parsed records retained at one time: - **Controlled memory use**: Only one chunk is loaded in memory at a time. - **Large file processing**: The iterator can process files larger than available RAM. - **Configurable chunk sizes**: Tune `chunk_size` within `[1, 1_000_000]` based on your system's memory and performance requirements (default 100,000). :::warning When using streaming with precision inference, the inferred precision may differ from bulk loading the entire file. Precision inference works within chunk boundaries, and different chunks may contain values with different precision requirements. For deterministic precision behavior, provide explicit `price_precision` and `size_precision` parameters. ::: ### Streaming CSV data in Rust The underlying streaming functionality is implemented in Rust and can be used directly: ```rust use std::path::Path; use nautilus_model::identifiers::InstrumentId; use nautilus_tardis::csv::stream_trades; fn main() -> Result<(), Box> { let filepath = Path::new("large_trades_file.csv"); let chunk_size = 100_000; let price_precision = Some(1); let size_precision = Some(0); let instrument_id = Some(InstrumentId::from("BTC-PERPETUAL.DERIBIT")); // Stream trades in chunks let stream = stream_trades( filepath, chunk_size, price_precision, size_precision, instrument_id, )?; for chunk in stream { let chunk = chunk?; println!("Processing chunk with {} trades", chunk.len()); // Process chunk } Ok(()) } ``` ## Instrument metadata The replay pipeline and data client request metadata for every exchange in their configured Tardis options before connecting to Tardis Machine. They use the [Tardis instruments metadata API](https://docs.tardis.dev/api/instruments-metadata-api) to parse instrument metadata into Nautilus definitions. The data client also publishes those definitions to the Nautilus data engine. :::note A `TARDIS_API_KEY` for an active Tardis pro or business subscription is required. The automatic bootstrap requests all instrument metadata for each configured Tardis exchange. ::: Python and Rust users can also request instrument definitions directly with `TardisHttpClient`. The client accepts optional `api_key`, `base_url`, `timeout_secs`, `normalize_symbols`, and `proxy_url` arguments. It can retrieve one symbol or all instruments for an exchange. Use Tardis lower-kebab exchange IDs such as `binance-futures`. ### Requesting instruments in Python ```python import asyncio from nautilus_trader.adapters.tardis import TardisHttpClient async def run(): http_client = TardisHttpClient() instrument = await http_client.instruments("bitmex", symbol="xbtusd") print(f"Received: {instrument}") instruments = await http_client.instruments("bitmex") print(f"Received: {len(instruments)} instruments") if __name__ == "__main__": asyncio.run(run()) ``` ### Requesting instruments in Rust For a complete example, see `crates/adapters/tardis/bin/example_http.rs`. ```rust use nautilus_tardis::{ common::enums::TardisExchange, http::TardisHttpClient, }; #[tokio::main] async fn main() -> Result<(), Box> { nautilus_common::logging::ensure_logging_initialized(); let client = TardisHttpClient::new(None, None, None, true, None)?; // Tardis instrument definitions let info = client .instruments_info(TardisExchange::Bitmex, Some("XBTUSD"), None) .await?; println!("Received: {info:?}"); // Nautilus instrument definitions let instruments = client .instruments( TardisExchange::Bitmex, Some("XBTUSD"), None, None, None, None, None, None, ) .await?; println!("Received: {instruments:?}"); Ok(()) } ``` ## Nautilus data client `TardisDataClientConfig` and `TardisDataClientFactory` integrate a configured Tardis Machine stream with a Nautilus node. The configuration selects one mode: - A non-empty `options` list connects to the historical `ws-replay-normalized` endpoint. - When `options` is empty, a non-empty `stream_options` list connects to the real-time `ws-stream-normalized` endpoint and reconnects automatically after an interruption. One list must be non-empty. If both are set, `options` selects historical replay mode. These request options determine the upstream exchanges, symbols, and data types. Nautilus subscription commands do not add or remove data from the Tardis Machine WebSocket. The data client adds `derivative_ticker` to every configured request so it can publish funding rates, mark prices, and index prices when their values change. It also supports the other outputs in [supported formats](#supported-formats), including `OptionGreeks` and optional BBO `QuoteTick` data from `option_summary` messages. Create Python stream options from Tardis JSON, then pass them to the public data client config: ```python from nautilus_trader.adapters.tardis import StreamNormalizedRequestOptions from nautilus_trader.adapters.tardis import TardisDataClientConfig from nautilus_trader.adapters.tardis import TardisDataClientFactory stream_options = StreamNormalizedRequestOptions.from_json( b'{"exchange":"binance-futures","symbols":["BTCUSDT"],"dataTypes":["trade","quote"]}', ) config = TardisDataClientConfig(stream_options=[stream_options]) factory = TardisDataClientFactory() ``` Pass `factory` and `config` to `LiveNode.builder(...).add_data_client(...)`. See `examples/live/tardis/data_tester.py` for the node registration pattern and `crates/adapters/tardis/examples/node_data_tester.rs` for a complete Rust replay client. The Rust data client config can set `book_snapshot_output` to `depth10`. The Python data client config uses the default `deltas` output; the standalone replay JSON configuration supports both values. ## Trade ID derivation Trade ticks use the venue-provided trade ID from the Tardis message or CSV row as the `TradeId`. When the venue omits the trade ID (empty string or null on some exchanges), both the WebSocket parser and CSV parser fall back to a deterministic FNV-1a hash of the symbol, timestamp, price, amount, and side. The same venue event yields the same trade ID across replays, keeping downstream dedup intact. ## Limitations and considerations `TardisDataClient` does not implement Nautilus data requests, including instrument, order book, quote, trade, funding rate, and bar requests. Configure historical replay through `options`, or use `run_tardis_machine_replay` for catalog workflows. ## Contributing :::info For additional features or to contribute to the Tardis adapter, please see our [contributing guide](https://github.com/nautechsystems/nautilus_trader/blob/master/CONTRIBUTING.md). ::: # Book Imbalance Backtest (Betfair) Source: https://nautilustrader.io/docs/latest/tutorials/backtest_book_imbalance_betfair/ :::note This is a **Rust-only** system tutorial. It drives the Rust `BacktestEngine` directly with raw Betfair streaming data, bypassing the Python and Parquet paths. ::: This tutorial backtests a `BookImbalanceActor` on a Betfair MATCH_ODDS market. It loads a raw historical streaming `.gz` file, feeds it through the Rust `BacktestEngine`, and tracks the bid/ask quoted-volume imbalance per runner. ## Introduction Betfair is a sports betting exchange where participants back (bid) and lay (ask) outcomes at decimal odds. Each runner has its own L2 order book that behaves like a financial order book. The actor reads `OrderBookDeltas` for every runner and accumulates two running totals per side: bid volume (back orders) and ask volume (lay orders). Per-batch and cumulative imbalance are computed as: ``` imbalance = (bid_volume - ask_volume) / (bid_volume + ask_volume) ``` A positive value means the market is leaning toward backing the outcome. Sports traders use this as a starting block, often combined with price momentum or market-wide features. A release build processes about three million data points per second with full order book maintenance in the matching engine. ```mermaid flowchart LR subgraph Inputs ["Source"] F["Betfair .gz MCM file"] end subgraph Loader ["BetfairDataLoader"] I["Instrument"] DLT["Deltas (L2 snap/update)"] TR["Trade ticks"] ICL["InstrumentClose"] end subgraph Engine ["BacktestEngine"] BK["Per-runner OrderBook (L2)"] ME["Matching engine"] end subgraph Actor ["BookImbalanceActor"] AC["Aggregate batch bid_vol / ask_vol"] SUM["Cumulative volume per runner"] IMB["imbalance = (bid - ask) / (bid + ask)"] end F --> I F --> DLT F --> TR F --> ICL DLT --> BK BK --> ME BK --> AC AC --> SUM --> IMB ``` ## Prerequisites - A working Rust toolchain ([rustup.rs](https://rustup.rs)). - The NautilusTrader repository cloned and building. - A Betfair historical `.gz` file containing MCM (Market Change Message) data. Source it from [Betfair historic data](https://historicdata.betfair.com/), a third-party archive, or by recording the Exchange Streaming API yourself. Place the file at: ``` test_data/local/betfair/1.253378068.gz ``` This path is gitignored and not shipped with the repository. The bundled example dataset is a football MATCH_ODDS market with 3 runners and around 82,000 MCM lines recorded over 18 days. ## Loading the data `BetfairDataLoader` reads gzip-compressed Betfair Exchange Streaming API files and parses each line into Nautilus domain objects: ```rust use nautilus_betfair::loader::{BetfairDataItem, BetfairDataLoader}; use nautilus_model::types::Currency; let mut loader = BetfairDataLoader::new(Currency::GBP(), None); let items = loader.load(&filepath)?; ``` The loader returns a `Vec`: | Variant | Description | Maps to `Data` enum? | | :------------------ | :---------------------------------------------- | :--------------------------- | | `Instrument` | Runner definition from market definition. | No (added separately) | | `Status` | Market status transition (PreOpen, Trading...). | No (`Data` has no variant) | | `Deltas` | Order book snapshot or delta update. | Yes, `Data::BookDeltas` | | `Trade` | Incremental trade tick from cumulative volumes. | Yes, `Data::Trade` | | `Ticker` | Last traded price, volume, BSP near/far. | - | | `StartingPrice` | Betfair Starting Price for a runner. | - | | `BspBookDelta` | BSP-specific book delta. | - | | `InstrumentClose` | Settlement event. | Yes, `Data::InstrumentClose` | | `SequenceCompleted` | Batch completion marker. | - | | `RaceRunnerData` | GPS tracking data (horse/greyhound racing). | - | | `RaceProgress` | Race-level progress data. | - | The backtest engine accepts the `Data` enum, so we map the variants we need and skip the Betfair-specific types: ```rust use nautilus_model::data::Data; let mut instruments = AHashMap::new(); let mut data: Vec = Vec::new(); for item in items { match item { BetfairDataItem::Instrument(inst) => { instruments.insert(inst.id(), *inst); } BetfairDataItem::Deltas(d) => { data.push(Data::BookDeltas(Box::new(d))); } BetfairDataItem::Trade(t) => { data.push(Data::Trade(t)); } BetfairDataItem::InstrumentClose(c) => { data.push(Data::InstrumentClose(c)); } _ => {} } } ``` `Data::BookDeltas` boxes its `OrderBookDeltas` payload to keep the enum small. Instruments are re-emitted on every market definition update in the stream, so the map deduplicates them by keeping the latest version. :::warning The `Status` variant carries market status transitions (PreOpen, Trading, Suspended, Closed) but the `Data` enum has no variant for it. This example does not replay status transitions. If you extend this into a strategy that places orders, the matching engine will not see market suspensions or closures from the stream. Subscribe to instrument status separately or add status routing to the engine. ::: ## The actor NautilusTrader ships `BookImbalanceActor` in the trading crate's examples module. The example wires it up with a per-runner instrument list and a log interval: ```rust use nautilus_trading::examples::actors::BookImbalanceActor; let actor = BookImbalanceActor::new(instrument_ids, 5000, None); engine.add_actor(actor)?; ``` The second argument is the log interval: print a progress line every 5,000 updates per runner. The example reads `IMBALANCE_LOG_INTERVAL` from the environment, so set it to a smaller value (`200`) when you want to capture finer-grained data for the panels at the end of this tutorial. The full source is at [`crates/trading/src/examples/actors/imbalance/actor.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/actors/imbalance/actor.rs). ### How it works A `DataActor` in Rust needs three pieces: 1. A struct holding a `DataActorCore` field plus your own state. 2. `nautilus_actor!(YourType)` to wire up the core, plus a `Debug` implementation. 3. The `DataActor` trait implementation with your callbacks. The framework provides blanket `Actor` and `Component` implementations for runtime actors. The `nautilus_actor!` macro supplies the native runtime wiring when your struct holds a `DataActorCore`, so normal actor code only implements the callbacks it needs. On start the actor subscribes to `OrderBookDeltas` for each instrument. On each update it sums per-side volume from the individual deltas and accumulates running totals. On stop it prints a per-instrument summary. Setting `managed: false` in `subscribe_book_deltas` means the data engine does not maintain a separate order book copy in the cache for the actor. The exchange-side matching engine still maintains its own book through `book.apply_delta()` on every delta. Set `managed: true` if your actor needs to read the full book state from `self.cache().order_book(&instrument_id)`. ## Backtest engine setup ### Create the engine and venue Betfair is a cash-settled betting exchange. The venue uses `AccountType::Cash`, `OmsType::Netting`, and `BookType::L2_MBP`: ```rust let mut engine = BacktestEngine::new(BacktestEngineConfig::default())?; engine.add_venue( SimulatedVenueConfig::builder() .venue(Venue::from("BETFAIR")) .oms_type(OmsType::Netting) .account_type(AccountType::Cash) .book_type(BookType::L2_MBP) .starting_balances(vec![Money::from("1_000_000 GBP")]) .build()?, )?; ``` ### Add instruments, actor, and data ```rust for instrument in instruments.values() { engine.add_instrument(instrument)?; } let actor = BookImbalanceActor::new(instrument_ids, 5000, None); engine.add_actor(actor)?; engine.add_data(data, None, true, true)?; ``` The `add_data` parameters are `(data, client_id, validate, sort)`. With `validate: true` the engine checks the first element's instrument is registered (the batch is assumed homogeneous). With `sort: true` it sorts by timestamp. ### Run ```rust engine.run(None, None, None, false)?; ``` The four parameters are `(start, end, run_config_id, streaming)`. Passing `None` for start/end uses the full time range of the loaded data. ## What happens during the run For each data point in timestamp order the engine: 1. Advances the clock to the data timestamp. 2. Routes the data to the simulated exchange, which applies each delta to the per-instrument `OrderBook` and runs the matching engine cycle. 3. Publishes the data through the data engine and message bus, triggering the actor's `on_book_deltas` callback. 4. Drains command queues and settles venues (processes any pending orders). The matching engine maintains a full order book per instrument. The example has no orders to match, so the book state is ready to use as soon as it is swapped for a `Strategy`. ## Results The bundled MATCH_ODDS dataset has three runners and 143,098 data points; a release build completes in about 48 ms: ``` --- Book imbalance summary --- 1.253378068-2426.BETFAIR updates: 53197 bid_vol: 212225339.34 ask_vol: 117422531.85 imbalance: 0.2876 1.253378068-48783.BETFAIR updates: 36475 bid_vol: 52506905.49 ask_vol: 19104694.72 imbalance: 0.4664 1.253378068-58805.BETFAIR updates: 25426 bid_vol: 24295351.82 ask_vol: 25692733.11 imbalance: -0.0280 ``` Runner `2426` (the eventual winner, settled at BSP 2.22) ends at +0.288: backing flow dominates lay flow throughout the market. Runner `48783` shows even stronger backing pressure (+0.466) over fewer updates, while `58805` ends close to neutral (-0.028). ![Cumulative imbalance per runner](./assets/backtest_book_imbalance_betfair/panel_a_imbalance_lines.png) **Figure 1.** *Cumulative `(bid - ask) / (bid + ask)` per runner across the ~143k updates of the market lifetime. Dashed lines mark each runner's final imbalance.* ![Per-batch signed flow distribution](./assets/backtest_book_imbalance_betfair/panel_b_batch_distribution.png) **Figure 2.** *Distribution of per-batch signed flow ratio `(bid - ask) / (bid + ask)` over `IMBALANCE_LOG_INTERVAL=200` batches per runner. The shape of each runner's batch distribution is a sharper signal than the cumulative imbalance.* ![Cumulative bid and ask volume](./assets/backtest_book_imbalance_betfair/panel_c_cumulative_volume.png) **Figure 3.** *Cumulative back (bid) and lay (ask) volume per runner. Both sides are non-monotonic: lay flow occasionally outpaces back flow within short bursts even when cumulative imbalance stays positive.* ### Regenerate the panels The actor logs `[runner] update #N: batch bid=B ask=A cumulative imbalance=I` on every Nth update. The renderer parses those lines and writes static PNGs using the `nautilus_dark` tearsheet theme. After building NautilusTrader from source, run these commands from the repository root: ```bash make sync IMBALANCE_LOG_INTERVAL=200 cargo run -p nautilus-betfair --features examples --release \ --example betfair-backtest > /tmp/betfair.log 2>&1 BETFAIR_LOG=/tmp/betfair.log \ uv run --project python --no-sync \ python docs/tutorials/assets/backtest_book_imbalance_betfair/render_panels.py ``` ## Running the example ```bash # Debug build cargo run -p nautilus-betfair --features examples --example betfair-backtest # Release build (recommended) cargo run -p nautilus-betfair --features examples --release --example betfair-backtest # Custom data file cargo run -p nautilus-betfair --features examples --release --example betfair-backtest -- path/to/file.gz ``` ## Complete source The complete example is at [`crates/adapters/betfair/examples/betfair_backtest.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/betfair/examples/betfair_backtest.rs). ## Next steps - **Add a strategy**. Replace the actor with a `Strategy` implementation that places back/lay orders based on the imbalance signal. See the `EmaCross` example in `crates/trading/src/examples/strategies/ema_cross/strategy.rs` for the pattern. - **Use managed books**. Set `managed: true` in `subscribe_book_deltas` and read the full book via `self.cache().order_book(&id)` for richer signals like top-of-book spread, depth ratios, or weighted mid-price. - **Multiple markets**. Load several `.gz` files and run them through the same engine to test cross-market signals. - **Compare with Python**. Run the same backtest from Python using the `BacktestEngine` Python API. Both surfaces drive the same Rust engine over the same data pipeline, so the results should match. # Backtest with FX Bar Data Source: https://nautilustrader.io/docs/latest/tutorials/backtest_fx_bars/ Run an EMA cross strategy on USD/JPY 1-minute bid/ask bars with FX rollover interest and a probabilistic fill model. The data ships with the NautilusTrader test kit, so this tutorial runs without any external download. [View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/master/docs/tutorials/backtest_fx_bars.py). ## Introduction The strategy is `EMACross`, a teaching example that compares a fast EMA against a slow EMA on bar closes: - **Fast EMA crosses above slow EMA**: any short position is closed and a new long is opened. - **Fast EMA crosses below slow EMA**: any long position is closed and a new short is opened. The venue is a simulated FX ECN with a `MARGIN` account, `HEDGING` OMS, and multi-currency starting balances of 1,000,000 USD and 10,000,000 JPY. A `FillModel` introduces a 50% probability of one-tick slippage, and the `FXRolloverInterestModule` applies daily rollover at the relevant short-term interest differential. `EMACross` is a teaching strategy and has no edge. ```mermaid flowchart LR subgraph Inputs ["Data streams"] B["1-minute BID bar (FXCM)"] A["1-minute ASK bar (FXCM)"] end subgraph Wrangler ["QuoteTickDataWrangler"] Q["QuoteTick stream"] end subgraph Engine ["Backtest engine"] AGG["5-minute BID INTERNAL aggregator"] BAR["Bar close"] F1(("EMA(10)")) F2(("EMA(20)")) end subgraph Decision ["Crossover decision"] X{{"fast >= slow"}} Y{{"fast < slow"}} end subgraph Orders ["Orders"] L["Close shorts -> BUY market"] S["Close longs -> SELL market"] end B --> Q A --> Q Q --> AGG --> BAR BAR --> F1 --> X BAR --> F2 --> X F1 --> Y F2 --> Y X -->|cross up| L Y -->|cross down| S ``` ## Prerequisites - Python 3.12+ - [NautilusTrader](https://pypi.org/project/nautilus_trader/) 2.x installed (`pip install -U --pre nautilus_trader`). The `visualization` extra is only needed if you also want to regenerate the panels at the end of the tutorial. - pandas (`pip install pandas`). The wheel declares no runtime dependencies. - The sibling [`ema_cross.py`](./ema_cross.py) file. Keep it next to this tutorial when downloading or converting it with Jupytext. ```python from decimal import Decimal from nautilus_trader.common import LogLevel from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.backtest import BacktestEngine from nautilus_trader.backtest import FXRolloverInterestModule from nautilus_trader.backtest import InterestRateRecord from nautilus_trader.config import LoggerConfig from nautilus_trader.config import RiskEngineConfig from nautilus_trader.execution import ProbabilisticFillModel from nautilus_trader.model import AccountType from nautilus_trader.model import BarType from nautilus_trader.model import Currency from nautilus_trader.model import Money from nautilus_trader.model import OmsType from nautilus_trader.model import TraderId from nautilus_trader.model import Venue from nautilus_trader.testkit.providers import TestDataProvider from nautilus_trader.testkit.providers import TestInstrumentProvider from ema_cross import EMACross from ema_cross import EMACrossConfig JPY = Currency.from_str("JPY") USD = Currency.from_str("USD") ``` ## Engine setup Pre-trade risk checks are bypassed so the strategy's market orders flow straight through to the matching engine. ```python config = BacktestEngineConfig( trader_id=TraderId.from_str("BACKTESTER-001"), logging=LoggerConfig(stdout_level=LogLevel.ERROR), risk_engine=RiskEngineConfig(bypass=True), ) engine = BacktestEngine(config=config) ``` ## Simulation modules `FXRolloverInterestModule` charges or credits rollover interest on open positions at the configured cutover time, using the bundled `short-term-interest.csv` rates from the OECD short-term interest series. Without it a backtest spanning many sessions ignores carry. ```python provider = TestDataProvider() interest_rate_data = provider.read_csv("short-term-interest.csv") interest_rate_records = [ InterestRateRecord(location=row.LOCATION, time=row.TIME, value=row.Value) for row in interest_rate_data.itertuples(index=False) ] fx_rollover_interest = FXRolloverInterestModule(records=interest_rate_records) ``` ## Fill model Limit orders fill on a 20% probability per tick when their price is reached, and any market or marketable order draws a one-tick slip on a 50% coin flip. The seed makes the run reproducible. ```python fill_model = ProbabilisticFillModel( prob_fill_on_limit=0.2, prob_slippage=0.5, random_seed=42, ) ``` ## Venue `OmsType.HEDGING` lets the strategy carry concurrent long and short positions in the same instrument and have the venue assign position IDs. The account is multi-currency so PnL on USD/JPY accrues in JPY rather than being converted on every fill. ```python SIM = Venue("SIM") engine.add_venue( venue=SIM, oms_type=OmsType.HEDGING, account_type=AccountType.MARGIN, base_currency=None, starting_balances=[Money(1_000_000, USD), Money(10_000_000, JPY)], fill_model=fill_model, modules=[fx_rollover_interest], ) ``` ## Instrument and data `TestDataProvider.quotes_from_fxcm_bars` synthesizes quote ticks from each minute's open, high, low, and close in the bundled FXCM bid and ask CSVs. The strategy declares `5-MINUTE-BID-INTERNAL`, so the engine builds 5-minute BID bars from the quote stream internally. ```python USDJPY_SIM = TestInstrumentProvider.default_fx_ccy("USD/JPY", SIM) engine.add_instrument(USDJPY_SIM) ticks = provider.quotes_from_fxcm_bars( instrument=USDJPY_SIM, bid_csv="fxcm/usdjpy-m1-bid-2013.csv", ask_csv="fxcm/usdjpy-m1-ask-2013.csv", ) engine.add_data(ticks) ``` ## Strategy Trade size is one million USD per order. EMACross cancels and replaces the position on every crossover, so the strategy is in some position for nearly the whole month. ```python strategy_config = EMACrossConfig( instrument_id=USDJPY_SIM.id, bar_type=BarType.from_str("USD/JPY.SIM-5-MINUTE-BID-INTERNAL"), fast_ema_period=10, slow_ema_period=20, trade_size=Decimal(1_000_000), ) strategy = EMACross(config=strategy_config) engine.add_strategy(strategy=strategy) ``` ## Run The engine processes every quote tick and bar in timestamp order, then returns when the data is exhausted. ```python engine.run() ``` ## Reports `engine.generate_*` returns DataFrames covering the account state, the fills, and the closed positions. ```python engine.generate_account_report(SIM) ``` ```python engine.generate_order_fills_report() ``` ```python engine.generate_positions_report() ``` ## What the run produces A 28-day run prints 8,065 5-minute bars and triggers 234 closed cycles across 468 fills (every crossover after the first emits a closing fill on the previous position and an opening fill on the new one). 72 of the 234 cycles are profitable. The strategy ends down 209,000 JPY: a textbook whipsaw signature on a noisy 5-minute series. ![USD/JPY 5-minute close with EMAs across the month](./assets/backtest_fx_bars/panel_a_price_overview.png) **Figure 1.** *USD/JPY BID close at 5-minute resolution across 2013-02 with EMA(10) and EMA(20) overlaid. Long flat patches are weekend gaps in the FXCM bid feed.* ![Three-day zoom on crossovers](./assets/backtest_fx_bars/panel_b_zoom.png) **Figure 2.** *Zoom on 2013-02-12 to 2013-02-15 UTC. Each marker is a crossover entry: triangles up are long, triangles down are short.* ![Cumulative realized pnl](./assets/backtest_fx_bars/panel_c_pnl_curve.png) **Figure 3.** *Cumulative JPY pnl across all closed cycles. Marker color encodes per-cycle pnl: blue = positive, red = negative.* ![Hold-time and pnl distributions](./assets/backtest_fx_bars/panel_d_distributions.png) **Figure 4.** *Cycle hold time and per-cycle pnl distributions. Most cycles hold for under three hours; the pnl distribution is roughly symmetric and heavily concentrated near zero.* ### Regenerate the panels The panels above are produced by a self-contained renderer that re-runs the backtest, pulls bars and fills from the engine cache, and writes PNGs using the shared `nautilus_dark` tearsheet theme. After building NautilusTrader from source, run these commands from the repository root: ```bash make sync uv run --project python --no-sync \ python docs/tutorials/assets/backtest_fx_bars/render_panels.py ``` ## Next steps - **Slow the signal**. The default 10/20 EMAs whip in low-trend sessions. Try 20/60 on the same bars or move to 15-minute bars to cut the cycle count. - **Add a regime filter**. Suppress entries when realized range is below a threshold so the strategy only trades sessions with directional movement. - **Compare aggregations**. Build the bars from raw tick data via `BarType.from_str("USD/JPY.SIM-5-MINUTE-BID-INTERNAL")` against an externally aggregated dataset to confirm both paths agree. # Backtest with Order Book Depth Data (Binance) Source: https://nautilustrader.io/docs/latest/tutorials/backtest_orderbook_binance/ Replay Binance T_DEPTH order book deltas through `BacktestNode` and run an imbalance strategy that fires fill-or-kill (FOK) limit orders when one side of the book is much thicker than the other. The same pattern works against any venue's L2 delta feed. [View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/master/docs/tutorials/backtest_orderbook_binance.py). ## Introduction Top-of-book imbalance is a microstructure signal: when the smaller resting side at the BBO drops well below the larger side, the book is leaning. The tutorial's `OrderBookImbalance` strategy works in two stages on every order book update: - Compute `min(bid_size, ask_size) / max(bid_size, ask_size)`. Higher means balanced; lower means leaning. - When the larger side is at least `trigger_min_size` and the ratio is below `trigger_imbalance_ratio`, fire a single FOK limit order against the thicker side. A trigger cooldown of `min_seconds_between_triggers` prevents the strategy from re-firing on every micro-update. The strategy is intentionally simple and has no edge. ```mermaid flowchart LR subgraph Inputs ["Data engine"] S["Snap CSV (initial L2 state)"] U["Update CSV (L2 deltas)"] end subgraph Engine ["BacktestEngine"] W["deltas_from_frame"] B["Per-instrument OrderBook"] C["Cache.order_book"] end subgraph Strategy ["OrderBookImbalance"] R{{"larger >= trigger_min_size
AND smaller/larger < ratio
AND cooldown elapsed"}} D{{"bid_size > ask_size?"}} BUY["Submit FOK BUY at best ask"] SELL["Submit FOK SELL at best bid"] end S --> W --> B U --> W B --> C C --> R R -->|yes| D D -->|yes| BUY D -->|no| SELL ``` ## Prerequisites - Python 3.12+ - [NautilusTrader](https://pypi.org/project/nautilus_trader/) 2.x installed (`pip install -U --pre nautilus_trader`) - pandas (`pip install pandas`). The wheel declares no runtime dependencies. - The sibling [`orderbook_data.py`](./orderbook_data.py) and [`orderbook_imbalance.py`](./orderbook_imbalance.py) files. Keep them next to this tutorial when downloading or converting it with Jupytext. - Optionally, Binance T_DEPTH CSVs for the day you want to replay. The documented run uses BTCUSDT 2022-11-01 from [data.binance.vision](https://data.binance.vision), placed under `NAUTILUS_DATA_DIR/Binance/`. Without them the tutorial falls back to a bundled 100-row sample of each file, which runs end to end but is too short to trigger the strategy. ```python import os import shutil from pathlib import Path import pandas as pd from nautilus_trader.adapters.binance import load_binance_order_book_deltas from nautilus_trader.backtest import BacktestNode from nautilus_trader.common import LogLevel from nautilus_trader.config import ( BacktestDataConfig, BacktestEngineConfig, BacktestRunConfig, BacktestVenueConfig, ImportableStrategyConfig, LoggerConfig, ) from nautilus_trader.core.datetime import dt_to_unix_nanos from nautilus_trader.model import ( AccountType, BookType, Currency, CurrencyPair, InstrumentId, OmsType, Price, Quantity, Symbol, Venue, ) from nautilus_trader.persistence import ParquetDataCatalog from orderbook_data import deltas_from_frame, sample_data_path ``` ## Loading data Each row of `_depth_snap.csv` and `_depth_update.csv` is a single L2 level event. The Binance loader maps them to NautilusTrader `OrderBookDelta` objects with `update_type="snap"` for snapshots and `set` / `delete` for updates. The full update file for BTCUSDT 2022-11-01 is ~12 GB (~110 million rows), so the tutorial caps the read at 1,000,000 rows. ```python DATA_DIR = Path(os.environ.get("NAUTILUS_DATA_DIR", "~/Downloads/Data")).expanduser() / "Binance" ``` ```python path_snap = DATA_DIR / "BTCUSDT_T_DEPTH_2022-11-01_depth_snap.csv" path_update = DATA_DIR / "BTCUSDT_T_DEPTH_2022-11-01_depth_update.csv" if not (path_snap.is_file() and path_update.is_file()): path_snap = sample_data_path("binance/btcusdt-depth-snap.csv") path_update = sample_data_path("binance/btcusdt-depth-update.csv") path_snap, path_update ``` ```python # Initial L2 snapshot of the book at session open. df_snap = load_binance_order_book_deltas(path_snap) df_snap.head() ``` ```python # Per-level deltas for the day; capped to 1M rows for a reasonable run time. nrows = 1_000_000 df_update = load_binance_order_book_deltas(path_update, nrows=nrows) df_update.head() ``` ### Build current model objects Define the instrument with the public model API, then convert each loader row to an `OrderBookDelta`. Sort by `ts_init` so the data engine sees deltas in true publication order regardless of how the snapshot and update files interleave. ```python BTCUSDT_BINANCE = CurrencyPair( instrument_id=InstrumentId(Symbol("BTCUSDT"), Venue("BINANCE")), raw_symbol=Symbol("BTCUSDT"), base_currency=Currency.from_str("BTC"), quote_currency=Currency.from_str("USDT"), price_precision=2, size_precision=6, price_increment=Price(0.01, precision=2), size_increment=Quantity(0.000001, precision=6), ts_event=0, ts_init=0, ) deltas = deltas_from_frame(df_snap, BTCUSDT_BINANCE) deltas += deltas_from_frame(df_update, BTCUSDT_BINANCE) deltas.sort(key=lambda x: x.ts_init) deltas[:10] ``` ### Set up the data catalog Persist the instrument and deltas to a fresh `ParquetDataCatalog` so the `BacktestNode` can lazy-load by time range. Re-running the tutorial wipes any prior catalog at the same path. ```python CATALOG_PATH = Path.cwd() / "catalog" if CATALOG_PATH.exists(): shutil.rmtree(CATALOG_PATH) CATALOG_PATH.mkdir() catalog = ParquetDataCatalog(str(CATALOG_PATH)) ``` ```python catalog.write_instruments([BTCUSDT_BINANCE]) catalog.write_order_book_deltas(deltas) ``` ```python catalog.instruments() ``` ```python start = dt_to_unix_nanos(pd.Timestamp("2022-11-01", tz="UTC")) end = dt_to_unix_nanos(pd.Timestamp("2022-11-04", tz="UTC")) deltas = catalog.query_order_book_deltas( identifiers=[str(BTCUSDT_BINANCE.id)], start=start, end=end, ) print(len(deltas)) deltas[:10] ``` ## Configure the backtest `BacktestNode` ingests data from the catalog and builds a `BacktestEngine` per `BacktestRunConfig`. The venue book type must match the data: deltas carry full L2 information so we use `L2_MBP`. ```python instrument = catalog.instruments()[0] book_type = BookType.L2_MBP data_configs = [ BacktestDataConfig( catalog_path=str(CATALOG_PATH), data_type="OrderBookDelta", instrument_id=instrument.id, ), ] venues_configs = [ BacktestVenueConfig( name="BINANCE", oms_type=OmsType.NETTING, account_type=AccountType.CASH, base_currency=None, starting_balances=["20 BTC", "100000 USDT"], book_type=book_type, ), ] strategy_config = ImportableStrategyConfig( strategy_path="orderbook_imbalance:OrderBookImbalance", config_path="orderbook_imbalance:OrderBookImbalanceConfig", config={ "instrument_id": str(instrument.id), "book_type": book_type.name, "max_trade_size": "1.000", "min_seconds_between_triggers": 1.0, }, ) config = BacktestRunConfig( engine=BacktestEngineConfig( logging=LoggerConfig(stdout_level=LogLevel.ERROR), ), data=data_configs, venues=venues_configs, dispose_on_completion=False, ) config ``` ## Run the backtest ```python node = BacktestNode(configs=[config]) node.build() node.add_strategy_from_config(config.id, strategy_config) result = node.run() ``` ```python result ``` ```python node.generate_order_fills_report(config.id) ``` ```python node.generate_positions_report(config.id) ``` ```python node.generate_account_report(config.id, venue=Venue("BINANCE")) ``` ## What the run produces The figures below come from the full T_DEPTH files. The bundled sample covers 100 rows of each, so it completes without firing any orders. With one million updates the data spans roughly the first eleven minutes of the trading day after the initial snapshot is rebuilt. The renderer below uses three million updates (~25 minutes) so the panels show enough trigger events to be informative; the strategy fires the same way on the smaller default window. Across the active update window the strategy submits 47 FOK limit orders and accumulates a net 14 BTC short. Every trigger lands on the bid side, implying ask size dominated bid size for nearly every imbalance event in the recorded window. ![Top of book during the active window with FOK fills](./assets/backtest_orderbook_binance/panel_a_top_book.png) **Figure 1.** *BTCUSDT mid, best bid, and best ask during the FOK trigger window. Triangles down are short entries at the bid; the cross is the closing fill. The strategy is on the bid side throughout.* ![Imbalance ratio distribution](./assets/backtest_orderbook_binance/panel_b_imbalance_dist.png) **Figure 2.** *`smaller / larger` ratio across all sampled top-of-book snapshots, with the 0.20 trigger threshold marked. The mass left of the threshold is the addressable trigger region.* ![Top of book size and mid](./assets/backtest_orderbook_binance/panel_c_size_landscape.png) **Figure 3.** *Mid price (top) and best bid/ask size in BTC (bottom) across the active update window. Top-of-book sizes oscillate over a wide range while the mid drifts in a narrow band.* ![Net position trajectory](./assets/backtest_orderbook_binance/panel_d_position.png) **Figure 4.** *Cumulative signed BTC across the FOK fill sequence. Each marker is a fill; orange is a sell, blue is a buy. The strategy ramps into a -14 BTC short over 25 minutes.* ### Regenerate the panels A self-contained renderer re-runs the backtest with a sampling actor that captures top of book once per second, then writes PNG panels to the asset directory using the shared `nautilus_dark` tearsheet theme. After building NautilusTrader from source, run these commands from the repository root: ```bash make sync NAUTILUS_DATA_DIR=test_data/local \ uv run --project python --no-sync \ python docs/tutorials/assets/backtest_orderbook_binance/render_panels.py ``` Set `NAUTILUS_DATA_DIR` to wherever your `Binance/` data directory lives. ## Next steps - **Tighter trigger**. Drop `trigger_imbalance_ratio` to 0.10 to require a ten-to-one lean before firing. Expect far fewer entries and lower hit rate. - **Longer window**. Bump `nrows` to ten or twenty million to replay several hours and see the strategy stress against more diverse sessions. - **Quote ticks instead of deltas**. See the [Gold Perpetual Book Imbalance](gold_book_imbalance_ax.md) tutorial for a quote-driven imbalance strategy. # Backtest with Order Book Depth Data (Bybit) Source: https://nautilustrader.io/docs/latest/tutorials/backtest_orderbook_bybit/ Replay Bybit `ob500` order book deltas through `BacktestNode` and run the `OrderBookImbalance` strategy. Same shape as the [Binance variant](https://github.com/nautechsystems/nautilus_trader/blob/master/docs/tutorials/backtest_orderbook_binance.py), different loader and different instrument. [View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/master/docs/tutorials/backtest_orderbook_bybit.py). ## Introduction Bybit publishes a single per-symbol L2 deltas archive at depth 500. The tutorial reads the daily ZIP into a DataFrame. The strategy is the same `OrderBookImbalance` as in the Binance tutorial: when the smaller side of the BBO drops below `trigger_imbalance_ratio` of the larger, fire a single FOK limit order on the thicker side. `OrderBookImbalance` is a teaching strategy and has no edge. ```mermaid flowchart LR subgraph Inputs ["Data engine"] Z["ob500 ZIP archive"] end subgraph Engine ["BacktestEngine"] L["load_bybit_order_book_deltas"] W["deltas_from_frame"] B["Per-instrument OrderBook"] C["Cache.order_book"] end subgraph Strategy ["OrderBookImbalance"] R{{"larger >= trigger_min_size
AND smaller/larger < ratio
AND cooldown elapsed"}} D{{"bid_size > ask_size?"}} BUY["Submit FOK BUY at best ask"] SELL["Submit FOK SELL at best bid"] end Z --> L --> W --> B --> C C --> R R -->|yes| D D -->|yes| BUY D -->|no| SELL ``` ## Prerequisites - Python 3.12+ - [NautilusTrader](https://pypi.org/project/nautilus_trader/) 2.x installed (`pip install -U --pre nautilus_trader`) - pandas (`pip install pandas`). The wheel declares no runtime dependencies. - The sibling [`orderbook_data.py`](./orderbook_data.py) and [`orderbook_imbalance.py`](./orderbook_imbalance.py) files. Keep them next to this tutorial when downloading or converting it with Jupytext. - Optionally, a daily Bybit `ob500` ZIP, e.g. `2024-12-01_XRPUSDT_ob500.data.zip` from [public.bybit.com](https://public.bybit.com). Without one the tutorial falls back to a bundled 50-message sample of that archive, which runs end to end over a few seconds of the book. ```python import os import shutil from pathlib import Path import pandas as pd from nautilus_trader.backtest import BacktestNode from nautilus_trader.common import LogLevel from nautilus_trader.config import ( BacktestDataConfig, BacktestEngineConfig, BacktestRunConfig, BacktestVenueConfig, ImportableStrategyConfig, LoggerConfig, ) from nautilus_trader.core.datetime import dt_to_unix_nanos from nautilus_trader.model import ( AccountType, BookType, CryptoPerpetual, Currency, InstrumentId, OmsType, Price, Quantity, Symbol, Venue, ) from nautilus_trader.persistence import ParquetDataCatalog from orderbook_data import ( deltas_from_frame, load_bybit_order_book_deltas, sample_data_path, ) ``` ## Loading data Place the daily archive under `NAUTILUS_DATA_DIR/Bybit/` to replay a full day. The tutorial otherwise reads the bundled sample so it runs without a download. ```python DATA_DIR = Path(os.environ.get("NAUTILUS_DATA_DIR", "~/Downloads/Data")).expanduser() / "Bybit" ``` ```python path_update = DATA_DIR / "2024-12-01_XRPUSDT_ob500.data.zip" if not path_update.is_file(): path_update = sample_data_path("bybit/xrpusdt-ob500.data.zip") path_update ``` ```python # Read the first 1M deltas; the full file is larger. nrows = 1_000_000 df_raw = load_bybit_order_book_deltas(path_update, nrows=nrows) df_raw.head() ``` ### Build current model objects ```python XRPUSDT_BYBIT = CryptoPerpetual( instrument_id=InstrumentId(Symbol("XRPUSDT-LINEAR"), Venue("BYBIT")), raw_symbol=Symbol("XRPUSDT"), base_currency=Currency.from_str("XRP"), quote_currency=Currency.from_str("USDT"), settlement_currency=Currency.from_str("USDT"), is_inverse=False, price_precision=4, size_precision=0, price_increment=Price(0.0001, precision=4), size_increment=Quantity(1, precision=0), ts_event=0, ts_init=0, ) deltas = deltas_from_frame(df_raw, XRPUSDT_BYBIT) deltas.sort(key=lambda x: x.ts_init) deltas[:10] ``` ### Set up the data catalog ```python CATALOG_PATH = Path.cwd() / "catalog" if CATALOG_PATH.exists(): shutil.rmtree(CATALOG_PATH) CATALOG_PATH.mkdir() catalog = ParquetDataCatalog(str(CATALOG_PATH)) ``` ```python catalog.write_instruments([XRPUSDT_BYBIT]) catalog.write_order_book_deltas(deltas) ``` ```python catalog.instruments() ``` ```python start = dt_to_unix_nanos(pd.Timestamp("2024-11-30", tz="UTC")) end = dt_to_unix_nanos(pd.Timestamp("2024-12-04", tz="UTC")) deltas = catalog.query_order_book_deltas( identifiers=[str(XRPUSDT_BYBIT.id)], start=start, end=end, ) print(len(deltas)) deltas[:10] ``` ## Configure the backtest ```python instrument = catalog.instruments()[0] book_type = BookType.L2_MBP data_configs = [ BacktestDataConfig( catalog_path=str(CATALOG_PATH), data_type="OrderBookDelta", instrument_id=instrument.id, ), ] venues_configs = [ BacktestVenueConfig( name="BYBIT", oms_type=OmsType.NETTING, account_type=AccountType.MARGIN, base_currency=None, starting_balances=["200000 XRP", "100000 USDT"], book_type=book_type, ), ] strategy_config = ImportableStrategyConfig( strategy_path="orderbook_imbalance:OrderBookImbalance", config_path="orderbook_imbalance:OrderBookImbalanceConfig", config={ "instrument_id": str(instrument.id), "book_type": book_type.name, "max_trade_size": "1", "min_seconds_between_triggers": 1.0, }, ) config = BacktestRunConfig( engine=BacktestEngineConfig( logging=LoggerConfig(stdout_level=LogLevel.ERROR), ), data=data_configs, venues=venues_configs, dispose_on_completion=False, ) config ``` ## Run the backtest ```python node = BacktestNode(configs=[config]) node.build() node.add_strategy_from_config(config.id, strategy_config) result = node.run() ``` ```python result ``` ```python node.generate_order_fills_report(config.id) ``` ```python node.generate_positions_report(config.id) ``` ```python node.generate_account_report(config.id, venue=Venue("BYBIT")) ``` ## What the run produces The figures below come from a full-day `ob500` archive. The bundled sample replays 3,967 deltas and fires 2 of these orders. The Bybit `ob500` archive sometimes starts a minute before the file's nominal date, so the first trades land just before midnight UTC and the rest inside the file's day. With a 1M delta cap, the active window is roughly the first minute. The strategy fires 43 FOK orders during that window. ![Top of book during the active minute with FOK fills](./assets/backtest_orderbook_bybit/panel_a_top_book.png) **Figure 1.** *XRPUSDT mid, best bid, and best ask during the trigger window. Triangles are entries (up = long, down = short), crosses are closing fills.* ![Imbalance ratio distribution](./assets/backtest_orderbook_bybit/panel_b_imbalance_dist.png) **Figure 2.** *`smaller / larger` BBO size ratio across all sampled top-of-book snapshots, with the 0.20 trigger threshold marked.* ![Top of book size and mid](./assets/backtest_orderbook_bybit/panel_c_size_landscape.png) **Figure 3.** *Mid price (top) and best bid/ask size in XRP (bottom) across the active window.* ![Net XRP position trajectory](./assets/backtest_orderbook_bybit/panel_d_position.png) **Figure 4.** *Cumulative signed XRP position across the FOK fill sequence. Each marker is a fill: blue is a buy, orange is a sell.* ### Regenerate the panels A self-contained renderer re-runs the backtest with a sampling actor that captures top of book once per second, then writes PNG panels to the asset directory using the shared `nautilus_dark` tearsheet theme. After building NautilusTrader from source, run these commands from the repository root: ```bash make sync NAUTILUS_DATA_DIR=test_data/local \ uv run --project python --no-sync \ python docs/tutorials/assets/backtest_orderbook_bybit/render_panels.py ``` ## Next steps - **Tighter trigger**. Drop `trigger_imbalance_ratio` to 0.10 to require a ten-to-one lean. - **Longer window**. Bump `nrows` to ten or twenty million for a multi-hour replay. - **Cross-venue replay**. Run the same strategy in two engines (one Bybit, one Binance) and compare imbalance distributions. # Delta-Neutral Options Strategy (Bybit) Source: https://nautilustrader.io/docs/latest/tutorials/delta_neutral_options_bybit/ :::note This is a **Rust-only** system tutorial. It runs a live delta-neutral short-volatility strategy on Bybit using the Rust `LiveNode`. ::: This tutorial runs a short OTM strangle on Bybit BTC options and delta-hedges with the BTCUSDT perpetual. The strategy selects call and put strikes at startup, enters via implied-volatility limit orders, tracks portfolio delta from venue-provided Greeks, and submits market hedge orders on the perpetual when the delta drifts beyond a threshold. :::warning This strategy trades real money on mainnet. Setting `enter_strangle: false` only disables the initial strangle entry orders. The strategy still hydrates existing positions from the cache at startup and still submits hedge orders on the perpetual when portfolio delta breaches the threshold. If the account holds option or hedge positions from a prior session, the strategy will trade. ::: ## Prerequisites - Completion of the [options data tutorial](options_data_bybit.md), which covers instrument discovery, Greeks subscriptions, and the `DataActor` pattern. - A Bybit API key with **trading permissions** for options and linear perpetuals. - Environment variables: ```bash export BYBIT_API_KEY="your-api-key" export BYBIT_API_SECRET="your-api-secret" ``` ## Strategy overview The `DeltaNeutralVol` strategy ships in the trading crate's `examples` module and runs in five stages: 1. **Strike selection**: queries the instrument cache for all BTC options, filters to the nearest expiry, selects OTM call and put strikes by percentile rank. 2. **Entry**: places SELL limit orders on both legs priced by implied volatility (via Bybit's `order_iv` parameter). Entry is optional and disabled by default in the example. 3. **Greeks tracking**: subscribes to `OptionGreeks` for both legs. Deltas and IVs come directly from Bybit's option ticker stream. 4. **Rehedging**: computes portfolio delta and submits a market order on the BTCUSDT perpetual when the threshold is breached. Triggers on every Greeks update and on a periodic safety timer. 5. **Position tracking**: tracks call, put, and hedge positions via `on_order_filled`. Hydrates existing positions from the cache at startup. ```mermaid flowchart LR subgraph Discovery ["1. Strike selection (on_start)"] L["Cache: BTC option instruments"] F["Filter by nearest expiry, sort by strike"] K["Pick CALL strike at percentile (1 - target_call_delta)
Pick PUT strike at percentile |target_put_delta|"] end subgraph Entry ["2. Entry (optional)"] EI{{"enter_strangle AND
both mark IVs available"}} SL["Submit SELL limit order_iv on each leg"] end subgraph Track ["3. Greeks track + 4. Rehedge"] G["on_option_greeks updates leg delta"] PD["portfolio_delta = call_delta * call_pos
+ put_delta * put_pos
+ hedge_position"] TH{{"|portfolio_delta|
> rehedge_delta_threshold?"}} H["Submit MARKET order on BTCUSDT-LINEAR"] end subgraph Lifecycle ["5. Position tracking"] OF["on_order_filled updates leg / hedge counters"] end L --> F --> K K --> EI EI -->|yes| SL --> OF EI -->|no| OF G --> PD --> TH TH -->|yes| H --> OF OF --> PD ``` ### Portfolio delta The strategy computes net exposure as: ``` portfolio_delta = call_delta * call_position + put_delta * put_position + hedge_position ``` A short strangle starts near delta-neutral because the call and put deltas offset. With the default `target_call_delta = 0.20` and `target_put_delta = -0.20`, the two legs cancel at entry. As the underlying moves, net delta drifts and the strategy hedges to bring it back toward zero. ## Configuration The example file at [`crates/adapters/bybit/examples/node_delta_neutral.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/bybit/examples/node_delta_neutral.rs) configures the strategy: ```rust let hedge_instrument_id = InstrumentId::from("BTCUSDT-LINEAR.BYBIT"); let strategy_config = DeltaNeutralVolConfig::builder() .option_family("BTC".to_string()) .hedge_instrument_id(hedge_instrument_id) .client_id(client_id) .contracts(1) .rehedge_delta_threshold(0.5) .rehedge_interval_secs(30) .enter_strangle(false) .iv_param_key("order_iv".to_string()) .build(); let strategy = DeltaNeutralVol::new(strategy_config); ``` Parameters (defaults shown are the struct defaults; the example overrides `enter_strangle` to `false` and `iv_param_key` to `"order_iv"`): | Parameter | Default | Example | Description | | ------------------------- | ---------- | ---------------- | ------------------------------------------- | | `option_family` | required | `"BTC"` | Underlying filter for instrument discovery. | | `hedge_instrument_id` | required | `BTCUSDT-LINEAR` | Perpetual used for delta hedging. | | `client_id` | required | `"BYBIT"` | Data and execution client identifier. | | `target_call_delta` | `0.20` | - | Target call delta for strike selection. | | `target_put_delta` | `-0.20` | - | Target put delta for strike selection. | | `contracts` | `1` | - | Contracts per leg. | | `rehedge_delta_threshold` | `0.5` | - | Portfolio delta that triggers a hedge. | | `rehedge_interval_secs` | `30` | - | Periodic rehedge timer interval. | | `enter_strangle` | `true` | `false` | Place entry orders when Greeks arrive. | | `entry_iv_offset` | `0.0` | - | Vol points below mark IV for entry pricing. | | `iv_param_key` | `"px_vol"` | `"order_iv"` | Adapter-specific IV parameter key. | The `iv_param_key` is the key difference between venues. Bybit uses `order_iv`, which the adapter maps to the `orderIv` field in the place-order API. OKX uses `px_vol`. Setting this correctly is required for IV-based order placement. ## Node setup The example configures both data and execution clients with `Option` and `Linear` product types: ```rust let data_config = BybitDataClientConfig { api_key: None, api_secret: None, product_types: vec![BybitProductType::Option, BybitProductType::Linear], ..Default::default() }; let exec_config = BybitExecutionClientConfig { api_key: None, api_secret: None, product_types: vec![BybitProductType::Option, BybitProductType::Linear], account_id: Some(account_id), ..Default::default() }; ``` Both product types are needed: `Option` for the strangle legs, `Linear` for the BTCUSDT perpetual hedge instrument. The execution client requires `account_id` for order identity tracking. ```rust let mut node = LiveNode::builder(trader_id, environment)? .with_name("BYBIT-DELTA-NEUTRAL-001".to_string()) .add_data_client(None, Box::new(data_factory), Box::new(data_config))? .add_exec_client(None, Box::new(exec_factory), Box::new(exec_config))? .with_reconciliation(true) .with_delay_post_stop_secs(5) .build()?; node.add_strategy(strategy)?; node.run().await?; ``` `with_reconciliation(true)` queries Bybit at startup for open orders and positions, hydrating the cache before the strategy starts. The strategy then picks up any existing positions from a prior session. ## How the strategy works ### Strike selection On start the strategy queries the cache for all option instruments matching `option_family`. It discards expired options, selects the nearest expiry, separates calls and puts, and sorts each list by strike price. Strikes are chosen by percentile in the sorted list: - **Call**: index = `(1.0 - target_call_delta) * count`. With 0.20 target delta and 50 calls, this selects the 40th strike (80th percentile, OTM). - **Put**: index = `|target_put_delta| * count`. With -0.20 target delta, this selects the 10th strike (20th percentile, OTM). This is a heuristic. Strike price ordering approximates delta ordering for options at the same expiry. A production strategy would subscribe to Greeks for all strikes first, then select by actual delta. ### Entry via implied volatility When `enter_strangle` is `true` and both mark IVs have arrived, the strategy places SELL limit orders using the `order_iv` parameter: ```rust let mut call_params = Params::new(); call_params.insert("order_iv".to_string(), json!(call_entry_iv.to_string())); self.submit_order(call_order, None, Some(client_id), Some(call_params))?; ``` Bybit converts `orderIv` to a limit price server-side and gives it priority over any explicit price. The `entry_iv_offset` config subtracts vol points from mark IV: an offset of 0.02 sells two vol points below mark for faster fills. :::note Bybit's demo environment rejects orders with `order_iv`. The adapter denies them before they reach the API. Use mainnet or testnet for IV-based order placement. ::: ### Rehedging Two triggers check portfolio delta: - **Every Greeks update**: `on_option_greeks` recomputes portfolio delta after updating the leg's delta value. - **Periodic timer**: fires every `rehedge_interval_secs` as a safety net when Greeks updates stop arriving. When `|portfolio_delta| > rehedge_delta_threshold`, the strategy submits a market order on the hedge instrument. A `hedge_pending` flag prevents duplicate submissions while an order is in flight. ### Position tracking The strategy tracks positions via `on_order_filled`, not by querying the cache on every tick. Each fill updates the corresponding position counter (call, put, or hedge). At startup, existing positions are hydrated from the cache (populated by reconciliation). ### Shutdown On stop the strategy cancels open orders, unsubscribes from all data feeds, and resets the hedge-pending flag. It does not close positions. Unwinding the strangle and hedge requires manual action or a separate exit strategy. ## What the run produces A 30-second mainnet run with `enter_strangle: false` against a clean account places no orders. The strategy logs the discovered instruments and the strike selection: ``` Selected call: BTC-28APR26-81000-C-USDT-OPTION.BYBIT (strike=81000) Selected put: BTC-28APR26-75000-P-USDT-OPTION.BYBIT (strike=75000) Strangle: 1 contracts per leg, hedge on BTCUSDT-LINEAR.BYBIT ``` That is enough to reason about the strategy's structural behavior. The panels below visualize the mechanics around the actual selected strikes (75,000 / 81,000) at the captured underlying. ![Short strangle payoff at expiry](./assets/delta_neutral_options_bybit/panel_a_strangle_payoff.png) **Figure 1.** *Pnl at expiry of the short 75,000 PUT plus short 81,000 CALL combination, assuming a 1,500 USDT total premium and zero discount. The flat top is the credit-only zone between strikes; loss grows linearly past either strike.* ![Synthetic delta drift with rehedge](./assets/delta_neutral_options_bybit/panel_c_hedge_threshold.png) **Figure 2.** *Synthetic Brownian delta drift over 150 seconds with `rehedge_delta_threshold=0.5`. The dotted curve is the un-hedged drift; the line is the strategy's portfolio delta after each market hedge fire (crosses).* ![Portfolio delta drift around entry](./assets/delta_neutral_options_bybit/panel_b_delta_drift.png) **Figure 3.** *Toy approximation of how the short call and short put leg deltas move with a 5% spot range around entry, plus the resulting portfolio delta before hedging. Negative gamma compresses the curve in the wings and steepens it across the strikes.* ![Strike selection on the IV smile](./assets/delta_neutral_options_bybit/panel_d_strike_picker.png) **Figure 4.** *The strike-selection heuristic against an illustrative IV smile. The CALL strike sits at the (1 - 0.20) percentile and the PUT at the 0.20 percentile, placing both legs OTM at roughly equal-magnitude deltas around the underlying.* ### Regenerate the panels After building NautilusTrader from source, run these commands from the repository root: ```bash make sync timeout 30 ./target/release/examples/bybit-delta-neutral > /tmp/bybit_dn.log 2>&1 DN_LOG=/tmp/bybit_dn.log \ uv run --project python --no-sync \ python docs/tutorials/assets/delta_neutral_options_bybit/render_panels.py ``` The renderer parses selected strikes from the log; the panels themselves are illustrative because the default config does not place orders. ## Risk considerations - **Gamma risk**: a short strangle has negative gamma. Large underlying moves increase delta exposure faster than the rehedge timer responds. Tighten `rehedge_delta_threshold` and reduce `rehedge_interval_secs` for faster response, at the cost of more hedge trades. - **Vega risk**: an IV spike increases mark-to-market loss on the short options. The strategy does not manage vega exposure. - **Liquidity**: OTM crypto options can have wide spreads. Hedge quality degrades when the underlying gaps or the perpetual trades in coarse size increments. - **Lifecycle risk**: stopping the strategy stops hedging. Positions remain open and unhedged until manually managed. ## Running the example ```bash cargo run --example bybit-delta-neutral --package nautilus-bybit --features examples ``` The example runs with `enter_strangle: false` by default, so it does not place strangle entry orders. It still hydrates existing positions and submits hedge orders if portfolio delta breaches the threshold. On a clean account with no prior positions, no orders are placed. Stop with Ctrl+C. The strategy cancels open orders and unsubscribes before shutdown. ## Complete source - Example runner: [`crates/adapters/bybit/examples/node_delta_neutral.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/bybit/examples/node_delta_neutral.rs) - Strategy implementation: [`crates/trading/src/examples/strategies/delta_neutral_vol/`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/strategies/delta_neutral_vol/) - Strategy README with full config reference: [`crates/trading/src/examples/strategies/delta_neutral_vol/README.md`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/strategies/delta_neutral_vol/README.md) ## See also - [Options data and Greeks on Bybit](options_data_bybit.md): prerequisite tutorial covering Greeks subscriptions and option chain snapshots. - [Options](../concepts/options.md): option instrument types and data architecture. - [Bybit integration](../integrations/bybit.md#options-trading): options order parameters including `order_iv` and `mmp`. # Delta-Neutral Options Strategy (Derive) Source: https://nautilustrader.io/docs/latest/tutorials/delta_neutral_options_derive/ :::note This is a **Rust-only** system tutorial. It runs the live delta-neutral short-volatility strategy on Derive using the Rust `LiveNode`. ::: This tutorial runs the shared `DeltaNeutralVol` strategy with the Derive adapter. The shipped example discovers ETH options, selects an out-of-the-money call and put, subscribes to venue-provided Greeks, and delta-hedges with `ETH-PERP.DERIVE`. The Derive runner starts in hedge-only mode: it sets `enter_strangle: false`, so it does not place the initial option entry orders. It still hydrates existing positions through reconciliation and can submit market hedge orders on the perpetual when portfolio delta breaches the configured threshold. For smoke tests, set `DERIVE_DELTA_NEUTRAL_HEDGE_ENABLED=false` to keep the strategy from submitting hedge orders while it still loads instruments, reconciles the account, and subscribes to Greeks. For an entry-order smoke test, set `DERIVE_DELTA_NEUTRAL_ENTER_STRANGLE=true`; the runner submits Derive-premium option orders instead of IV-priced option orders. :::warning This strategy can trade real money on mainnet. Setting `enter_strangle: false` only disables the initial strangle entry orders. If the selected option legs or the hedge instrument already have open positions, the strategy can submit hedge orders. ::: ## Prerequisites - Completion of the [Derive integration guide](../integrations/derive.md), including wallet, subaccount, session-key, and funding setup. - A Derive testnet or mainnet subaccount with enough USDC collateral for the hedge orders you plan to allow. - A working Rust toolchain and a built NautilusTrader workspace. - Environment variables for the selected Derive environment. For testnet: ```bash export DERIVE_TESTNET_WALLET_ADDRESS="0x..." export DERIVE_TESTNET_SESSION_PRIVATE_KEY="0x..." export DERIVE_TESTNET_SUBACCOUNT_ID="12345" ``` For mainnet: ```bash export DERIVE_WALLET_ADDRESS="0x..." export DERIVE_SESSION_PRIVATE_KEY="0x..." export DERIVE_SUBACCOUNT_ID="12345" export DERIVE_ENVIRONMENT="mainnet" ``` The example defaults to testnet. Set `DERIVE_ENVIRONMENT=mainnet` only for real-funds runs. ## Strategy overview The `DeltaNeutralVol` strategy lives in the trading crate's examples module. The Derive runner uses it in five stages: 1. **Instrument load**: configures the Derive data client with `currencies: ["ETH"]`, so the adapter loads ETH perps and options into the cache. 2. **Strike selection**: filters the cache to live ETH options, selects the nearest expiry, then chooses OTM call and put strikes by percentile rank. 3. **Greeks tracking**: subscribes to `OptionGreeks` for both legs. Derive Greeks come from the shared `ticker_slim` feed and the `option_pricing` payload. 4. **Rehedging**: computes portfolio delta and submits a Derive market order on `ETH-PERP.DERIVE` when the threshold is breached. 5. **Position tracking**: tracks call, put, and hedge positions through fills. Reconciliation hydrates existing positions before the strategy starts. ```mermaid flowchart LR subgraph Derive ["Derive public + private APIs"] T["ticker_slim option payloads"] P["private positions and fills"] H["ETH-PERP order entry"] end subgraph Adapter ["nautilus-derive clients"] G["OptionGreeks from option_pricing"] R["Startup reconciliation"] M["Signed market hedge order"] end subgraph Strategy ["DeltaNeutralVol"] S["Select nearest-expiry ETH strangle"] D["portfolio_delta = call_delta * call_pos
+ put_delta * put_pos
+ hedge_pos"] C{{"|portfolio_delta| > threshold?"}} end T --> G --> S --> D --> C P --> R --> D C -->|yes| M --> H ``` ### Portfolio delta The strategy computes net exposure as: ``` portfolio_delta = call_delta * call_position + put_delta * put_position + hedge_position ``` A short strangle starts close to delta-neutral when the call and put deltas offset. As the underlying moves, net delta drifts and the strategy hedges with the perpetual to bring the portfolio back toward zero. ## Configuration The example runner at `crates/adapters/derive/examples/node_delta_neutral.rs` configures the strategy: ```rust let option_family = env_string("DERIVE_DELTA_NEUTRAL_OPTION_FAMILY", "ETH")?; let default_hedge = format!("{option_family}-PERP.DERIVE"); let hedge_instrument = env_string("DERIVE_DELTA_NEUTRAL_HEDGE_INSTRUMENT", &default_hedge)?; let enter_strangle = env_bool("DERIVE_DELTA_NEUTRAL_ENTER_STRANGLE", false)?; let hedge_enabled = env_bool("DERIVE_DELTA_NEUTRAL_HEDGE_ENABLED", true)?; let rehedge_delta_threshold = if hedge_enabled { env_f64("DERIVE_DELTA_NEUTRAL_REHEDGE_DELTA_THRESHOLD", 0.5)? } else { 1.0e12 }; let hedge_instrument_id = InstrumentId::from(hedge_instrument.as_str()); let mut strategy_config = DeltaNeutralVolConfig::builder() .option_family(option_family) .hedge_instrument_id(hedge_instrument_id) .client_id(client_id) .target_call_delta(env_f64("DERIVE_DELTA_NEUTRAL_TARGET_CALL_DELTA", 0.20)?) .target_put_delta(env_f64("DERIVE_DELTA_NEUTRAL_TARGET_PUT_DELTA", -0.20)?) .contracts(env_u64("DERIVE_DELTA_NEUTRAL_CONTRACTS", 1)?) .rehedge_delta_threshold(rehedge_delta_threshold) .rehedge_interval_secs(env_u64("DERIVE_DELTA_NEUTRAL_REHEDGE_INTERVAL_SECS", 30)?) .enter_strangle(enter_strangle) .entry_iv_offset(env_f64("DERIVE_DELTA_NEUTRAL_ENTRY_IV_OFFSET", 0.0)?) .entry_premium_offset_ticks(env_i32("DERIVE_DELTA_NEUTRAL_ENTRY_PREMIUM_OFFSET_TICKS", 1)?) .build(); if let Some(expiry) = env_optional_string("DERIVE_DELTA_NEUTRAL_EXPIRY")? { strategy_config.expiry_filter = Some(expiry); } let strategy = DeltaNeutralVol::new(strategy_config); ``` Parameters: | Parameter | Default | Derive runner | Description | | ---------------------------- | ---------- | ------------- | --------------------------------------------- | | `option_family` | required | `"ETH"` | Underlying filter for instrument discovery. | | `hedge_instrument_id` | required | `ETH-PERP` | Perpetual used for delta hedging. | | `client_id` | required | `"DERIVE"` | Data and execution client identifier. | | `target_call_delta` | `0.20` | `0.20` | Target call delta for strike selection. | | `target_put_delta` | `-0.20` | `-0.20` | Target put delta for strike selection. | | `contracts` | `1` | `1` | Contracts per option leg. | | `rehedge_delta_threshold` | `0.5` | `0.5` | Portfolio delta that triggers a hedge. | | `rehedge_interval_secs` | `30` | `30` | Periodic rehedge timer interval. | | `expiry_filter` | `None` | unset | Optional expiry substring filter. | | `enter_strangle` | `true` | `false` | Place entry orders when premium data arrives. | | `entry_premium_offset_ticks` | `None` | `1` | Ticks above option ask for sell entry. | | `entry_iv_offset` | `0.0` | `0.0` | Used only when premium mode is disabled. | | `iv_param_key` | `"px_vol"` | unused | IV parameter key for IV-priced venues. | The Derive runner reads these environment variables: | Variable | Default | Description | | ------------------------------------------------- | ---------------------- | ---------------------------------- | | `DERIVE_DELTA_NEUTRAL_OPTION_FAMILY` | `ETH` | Option family / Derive currency. | | `DERIVE_DELTA_NEUTRAL_HEDGE_INSTRUMENT` | `-PERP.DERIVE` | Perpetual hedge instrument. | | `DERIVE_DELTA_NEUTRAL_ENTER_STRANGLE` | `false` | Enable option entry orders. | | `DERIVE_DELTA_NEUTRAL_HEDGE_ENABLED` | `true` | Enable perpetual hedge orders. | | `DERIVE_DELTA_NEUTRAL_REHEDGE_DELTA_THRESHOLD` | `0.5` | Portfolio delta hedge threshold. | | `DERIVE_DELTA_NEUTRAL_REHEDGE_INTERVAL_SECS` | `30` | Periodic hedge check interval. | | `DERIVE_DELTA_NEUTRAL_CONTRACTS` | `1` | Contracts per option leg. | | `DERIVE_DELTA_NEUTRAL_TARGET_CALL_DELTA` | `0.20` | Call strike-selection target. | | `DERIVE_DELTA_NEUTRAL_TARGET_PUT_DELTA` | `-0.20` | Put strike-selection target. | | `DERIVE_DELTA_NEUTRAL_EXPIRY` | unset | Optional expiry substring filter. | | `DERIVE_DELTA_NEUTRAL_ENTRY_PREMIUM_OFFSET_TICKS` | `1` | Sell-entry ticks above option ask. | | `DERIVE_DELTA_NEUTRAL_ENTRY_IV_OFFSET` | `0.0` | Used only outside premium mode. | | `DERIVE_DELTA_NEUTRAL_MAX_FEE_PER_CONTRACT` | `1000` | Signed per-contract fee cap. | | `DERIVE_DELTA_NEUTRAL_MARKET_ORDER_SLIPPAGE_BPS` | adapter default | Market hedge slippage bound. | Derive signs explicit premium limit prices. The runner enables the strategy's premium-entry mode with `entry_premium_offset_ticks=1`, so entry orders use a live option ask when available and fall back to Derive IV fields when the quote side is empty. Bybit and OKX keep using the shared IV-param path. ## Node setup The Derive runner uses the live environment and selects testnet or mainnet from `DERIVE_ENVIRONMENT`: ```rust let environment = Environment::Live; let derive_environment = derive_environment_from_env(); let trader_id = TraderId::from("TESTER-001"); let account_id = AccountId::from("DERIVE-001"); let client_id = *DERIVE_CLIENT_ID; ``` The data client bulk-loads ETH instruments. This is important because the strategy selects option legs from the cache during `on_start`. ```rust let data_config = DeriveDataClientConfig { environment: derive_environment, currencies: vec![option_family.clone()], ..Default::default() }; ``` The execution client reads wallet, session key, and subaccount values from the Derive environment variables when the config fields are left unset. The example sets a fee cap and allows optional protocol-constant overrides for local testing. ```rust let exec_config = DeriveExecutionClientConfig { account_id, environment: derive_environment, max_fee_per_contract: Some(Decimal::from_str_exact("1000")?), domain_separator: env_override( derive_environment, "DERIVE_DOMAIN_SEPARATOR", "DERIVE_TESTNET_DOMAIN_SEPARATOR", ), action_typehash: env_override( derive_environment, "DERIVE_ACTION_TYPEHASH", "DERIVE_TESTNET_ACTION_TYPEHASH", ), trade_module_address: env_override( derive_environment, "DERIVE_TRADE_MODULE_ADDRESS", "DERIVE_TESTNET_TRADE_MODULE_ADDRESS", ), ..Default::default() }; ``` The node enables reconciliation so open orders, positions, balances, and reports are loaded before the strategy starts: ```rust let mut node = LiveNode::builder(trader_id, environment)? .with_name("DERIVE-DELTA-NEUTRAL-001".to_string()) .add_data_client(None, Box::new(data_factory), Box::new(data_config))? .add_exec_client(None, Box::new(exec_factory), Box::new(exec_config))? .with_reconciliation(true) .with_delay_post_stop_secs(5) .build()?; node.add_strategy(strategy)?; node.run().await?; ``` ## How the strategy works ### Strike selection On start the strategy queries the cache for all option instruments matching `option_family`. For Derive, the example uses `ETH`, so matching instruments have symbols such as `ETH-20260626-3000-C.DERIVE`. It discards expired options, optionally applies `expiry_filter`, and uses the nearest expiry when no filter is set. Calls and puts are sorted by strike: - **Call**: index = `(1.0 - target_call_delta) * count`. With the default `0.20`, this selects around the 80th percentile strike. - **Put**: index = `abs(target_put_delta) * count`. With the default `-0.20`, this selects around the 20th percentile strike. This is a strike-ordering heuristic. A production strategy can subscribe to Greeks for the full chain first and then select by actual delta. ### Greeks and shared ticker feeds Derive publishes option pricing fields on the same `ticker_slim` channel used for quotes. The adapter derives `OptionGreeks` from `option_pricing`, so the strategy only needs to subscribe to the two selected option legs: ```rust self.subscribe_option_greeks(call_id, Some(client_id), None); self.subscribe_option_greeks(put_id, Some(client_id), None); ``` The adapter reference-counts the underlying ticker subscription. Quotes, mark prices, index prices, funding rates, and option Greeks for the same instrument can share a single WebSocket channel. ### Rehedging on Derive The Derive execution adapter sends market orders as signed orders with a slippage-bound limit price. Before signing, it refreshes the current ticker snapshot for the hedge instrument and writes the worst acceptable price into the EIP-712 payload. The default `market_order_slippage_bps` is `50`. The strategy submits a hedge when both selected option legs have emitted Greeks and: ``` abs(portfolio_delta) > rehedge_delta_threshold ``` A positive portfolio delta triggers a SELL on `ETH-PERP.DERIVE`; a negative portfolio delta triggers a BUY. A `hedge_pending` flag blocks duplicate submissions while an order is in flight. ### Position tracking The strategy tracks positions via `on_order_filled` rather than polling positions on every update. Reconciliation hydrates existing positions at startup; subsequent fills update the in-memory call, put, and hedge counters. ### Shutdown On stop the strategy cancels open orders for the selected option legs and the hedge instrument, unsubscribes from data feeds, and leaves positions open. Unwinding the strangle and hedge requires manual action or a separate exit strategy. ## What the run produces On testnet with `enter_strangle: false`, the strategy should discover the selected legs, subscribe to Greeks, and place no entry orders. The selected symbols depend on the live Derive chain: ``` Selected call: ETH---C.DERIVE (strike=) Selected put: ETH---P.DERIVE (strike=) Strangle: 1 contracts per leg, hedge on ETH-PERP.DERIVE Strangle entry disabled: hedging externally-held positions only. ``` If no existing positions are present, the run should remain data-only after startup. If the account already holds positions in the selected legs or hedge instrument, the periodic rehedge timer can submit hedge orders. The panels below use the same selected-strike mechanics as the Derive runner. They parse selected strikes from the smoke-test log when available and otherwise fall back to illustrative ETH strikes. ![Derive short strangle payoff at expiry](./assets/delta_neutral_options_derive/panel_a_strangle_payoff.png) **Figure 1.** *Pnl at expiry of the short ETH put plus short ETH call combination, assuming a fixed USDC premium and zero discount. The flat top is the credit-only zone between strikes; loss grows linearly past either strike.* ![Derive portfolio delta drift around entry](./assets/delta_neutral_options_derive/panel_b_delta_drift.png) **Figure 2.** *Toy approximation of how the short call and short put leg deltas move around entry, plus the resulting portfolio delta before hedging.* ![Derive synthetic delta drift with rehedge](./assets/delta_neutral_options_derive/panel_c_hedge_threshold.png) **Figure 3.** *Synthetic Brownian delta drift over 150 seconds with `rehedge_delta_threshold=0.5`. Crosses show where the strategy would submit a hedge order when hedging is enabled.* ![Derive strike selection on the IV smile](./assets/delta_neutral_options_derive/panel_d_strike_picker.png) **Figure 4.** *The strike-selection heuristic against an illustrative IV smile. The call strike sits near the `(1 - target_call_delta)` percentile and the put near `abs(target_put_delta)`.* ### Regenerate the panels After building NautilusTrader from source, run these commands from the repository root: ```bash make sync export DERIVE_ENVIRONMENT=mainnet export DERIVE_DELTA_NEUTRAL_HEDGE_ENABLED=false timeout 45 cargo run --example derive-delta-neutral --package nautilus-derive --features examples \ > /tmp/derive_dn.log 2>&1 export DN_LOG=/tmp/derive_dn.log uv run --project python --no-sync \ python docs/tutorials/assets/delta_neutral_options_derive/render_panels.py ``` The renderer only uses the log to pick strikes. The plots remain illustrative because the no-order smoke configuration disables entry and hedge submissions. ## Risk considerations - **Gamma risk**: a short strangle has negative gamma. Large ETH moves can increase delta exposure faster than the rehedge timer responds. - **Slippage risk**: Derive market orders sign a slippage-bound limit price before submission. Tight bounds can reject useful hedges; loose bounds can fill worse than expected. - **Entry-price risk**: Derive entry uses live option asks plus a tick offset, or computes a premium from Derive IV fields when the quote side is empty. A small or negative offset can cross the book and fill immediately. - **Collateral risk**: Derive rejects orders when the subaccount lacks initial-margin headroom. Check `private/get_subaccount` or the adapter account snapshot before enabling live hedging. - **Lifecycle risk**: stopping the strategy stops hedging. Positions remain open and unhedged until they are managed elsewhere. ## Running the example ```bash cargo run --example derive-delta-neutral --package nautilus-derive --features examples ``` Stop with Ctrl+C. The strategy cancels open orders and unsubscribes before shutdown, but it does not close positions. For a mainnet smoke test that loads the venue and account without submitting orders: ```bash export DERIVE_ENVIRONMENT=mainnet export DERIVE_DELTA_NEUTRAL_HEDGE_ENABLED=false timeout 45 cargo run --example derive-delta-neutral --package nautilus-derive --features examples ``` For a mainnet smoke test that submits Derive-premium option entry orders: ```bash export DERIVE_ENVIRONMENT=mainnet export DERIVE_DELTA_NEUTRAL_ENTER_STRANGLE=true export DERIVE_DELTA_NEUTRAL_ENTRY_PREMIUM_OFFSET_TICKS=1 timeout --signal=INT 45 cargo run --example derive-delta-neutral --package nautilus-derive \ --features examples ``` ## Complete source - Example runner: `crates/adapters/derive/examples/node_delta_neutral.rs` - Strategy implementation: `crates/trading/src/examples/strategies/delta_neutral_vol/` - Strategy README: `crates/trading/src/examples/strategies/delta_neutral_vol/README.md` ## See also - [Derive integration](../integrations/derive.md): environment setup, symbology, capabilities, and execution semantics. - [Options](../concepts/options.md): option instrument types and data architecture. - [Delta-neutral options strategy on Bybit](delta_neutral_options_bybit.md): the same shared strategy with Bybit-specific IV entry parameters. # EMA cross Source: https://nautilustrader.io/docs/latest/tutorials/ema_cross/ Define the reusable bar-based EMA cross strategy used by the FX bars tutorial. ```python from __future__ import annotations from decimal import Decimal from nautilus_trader.config import StrategyConfig from nautilus_trader.indicators import ExponentialMovingAverage from nautilus_trader.model import Bar from nautilus_trader.model import BarType from nautilus_trader.model import InstrumentId from nautilus_trader.model import OrderSide from nautilus_trader.trading import Strategy class EMACrossConfig(StrategyConfig): def __init__( self, *, instrument_id: InstrumentId, bar_type: BarType, trade_size: Decimal, fast_ema_period: int = 10, slow_ema_period: int = 20, **_kwargs: object, ) -> None: super().__init__() self.instrument_id = instrument_id self.bar_type = bar_type self.trade_size = trade_size self.fast_ema_period = fast_ema_period self.slow_ema_period = slow_ema_period class EMACross(Strategy): def __init__(self, config: EMACrossConfig) -> None: super().__init__(config) self.fast_ema = ExponentialMovingAverage(config.fast_ema_period) self.slow_ema = ExponentialMovingAverage(config.slow_ema_period) def on_start(self) -> None: self.register_indicator_for_bars(self.config.bar_type, self.fast_ema) self.register_indicator_for_bars(self.config.bar_type, self.slow_ema) self.subscribe_bars(self.config.bar_type) def on_bar(self, _bar: Bar) -> None: if not self.indicators_initialized(): return if self.fast_ema.value >= self.slow_ema.value: if self.portfolio.is_net_flat(self.config.instrument_id): self.buy() elif self.portfolio.is_net_short(self.config.instrument_id): self.close_all_positions(self.config.instrument_id) self.buy() elif self.fast_ema.value < self.slow_ema.value: if self.portfolio.is_net_flat(self.config.instrument_id): self.sell() elif self.portfolio.is_net_long(self.config.instrument_id): self.close_all_positions(self.config.instrument_id) self.sell() def buy(self) -> None: instrument = self.cache.instrument(self.config.instrument_id) order = self.order_factory.market( self.config.instrument_id, OrderSide.BUY, instrument.make_qty(self.config.trade_size), ) self.submit_order(order) def sell(self) -> None: instrument = self.cache.instrument(self.config.instrument_id) order = self.order_factory.market( self.config.instrument_id, OrderSide.SELL, instrument.make_qty(self.config.trade_size), ) self.submit_order(order) def on_stop(self) -> None: self.close_all_positions(self.config.instrument_id) ``` # Mean Reversion with Proxy FX Data (AX Exchange) Source: https://nautilustrader.io/docs/latest/tutorials/fx_mean_reversion_ax/ This tutorial backtests a Bollinger-band mean-reversion strategy on **EURUSD-PERP** at [AX Exchange](https://architect.exchange) using [TrueFX](https://www.truefx.com) EUR/USD spot ticks as a proxy. ## Introduction The strategy combines two indicators on 1-minute mid bars: - **Bollinger Bands** (`BBMeanReversion`'s `BB(20, 2.0sd)`): a rolling 20-bar mean and a +/-2sd envelope. The bands flag price as overextended relative to recent volatility. - **Relative Strength Index** (`RSI(14)`): a 14-bar momentum oscillator. NautilusTrader RSI is on `[0, 1]`, so the conventional 30/70 thresholds become `0.30` / `0.70`. Entry needs both signals at once: a touch of the lower band with `RSI < 0.30` opens a long; a touch of the upper band with `RSI > 0.70` opens a short. Exit is one-sided: any open position closes when the close crosses back through the BB middle. Existing positions on the opposite side are flattened before a new entry. The shipped `BBMeanReversion` strategy is intentionally simple and has no edge. ```mermaid flowchart LR subgraph Inputs ["Data"] Q["TrueFX bid/ask ticks"] end subgraph Engine ["BacktestEngine"] W["QuoteTick construction"] AGG["1-minute MID INTERNAL aggregator"] BAR["Bar close"] end subgraph Indicators BB(("BB(20, 2.0sd)")) RSI(("RSI(14)")) end subgraph Decision ["Decision"] EX{{"Net long AND close >= mid
OR
net short AND close <= mid"}} ENL{{"close <= lower
AND RSI < 0.30"}} ENS{{"close >= upper
AND RSI > 0.70"}} end subgraph Orders CL["Close all positions"] BUY["BUY market"] SELL["SELL market"] end Q --> W --> AGG --> BAR BAR --> BB BAR --> RSI BB --> ENL BB --> ENS RSI --> ENL RSI --> ENS BB --> EX EX -->|yes| CL ENL -->|yes| BUY ENS -->|yes| SELL CL --> BUY CL --> SELL ``` ### Why proxy data AX Exchange is a new venue not yet covered by historical data vendors. [TrueFX](https://www.truefx.com) publishes free, institutional-grade EUR/USD spot tick archives (Integral and Jefferies pools) that stand in cleanly for AX EURUSD-PERP backtests. ## Prerequisites - Python 3.12+ - [NautilusTrader installed](../getting_started/installation.md). - A free TrueFX account, used to download a monthly tick archive. ## Data preparation ### Download TrueFX EUR/USD ticks 1. Go to the [TrueFX historical downloads page](https://www.truefx.com/truefx-historical-downloads/). 2. Pick **EUR/USD** and a month, for example **December 2025**. 3. Extract the ZIP. The CSV is headerless with columns `pair, timestamp, bid, ask`. ### Load into Nautilus quote ticks Define `EURUSD_PERP` and `instrument_id` in the next section before running this snippet. ```python from pathlib import Path import pandas as pd from nautilus_trader.model import Quantity from nautilus_trader.model import QuoteTick df = pd.read_csv( Path("EURUSD-2025-12.csv"), header=None, names=["pair", "timestamp", "bid", "ask"], ) df["timestamp"] = pd.to_datetime( df["timestamp"], format="%Y%m%d %H:%M:%S.%f", utc=True, ) df = df.set_index("timestamp")[["bid", "ask"]].sort_index() ticks = [] for timestamp, row in df.iterrows(): ts_ns = pd.Timestamp(str(timestamp)).value ticks.append( QuoteTick( instrument_id=instrument_id, bid_price=EURUSD_PERP.make_price(float(row.bid)), ask_price=EURUSD_PERP.make_price(float(row.ask)), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=ts_ns, ts_init=ts_ns, ), ) ``` Each quote carries the proxy instrument ID and one unit of bid and ask size. The strategy declares `1-MINUTE-MID-INTERNAL`, so the engine builds 1-minute MID bars from the tick stream internally. ## Instrument definition Proxy data needs a manual instrument definition. The multiplier of `1000` gives one contract a notional of 1,000 EUR. ```python from decimal import Decimal from nautilus_trader.model import AssetClass from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import PerpetualContract from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol USD = Currency.from_str("USD") instrument_id = InstrumentId.from_str("EURUSD-PERP.AX") EURUSD_PERP = PerpetualContract( instrument_id=instrument_id, raw_symbol=Symbol("EURUSD-PERP"), underlying="EUR", asset_class=AssetClass.FX, quote_currency=USD, settlement_currency=USD, is_inverse=False, price_precision=5, size_precision=0, price_increment=Price.from_str("0.00001"), size_increment=Quantity.from_int(1), multiplier=Quantity.from_int(1000), lot_size=Quantity.from_int(1), margin_init=Decimal("0.05"), margin_maint=Decimal("0.025"), maker_fee=Decimal("0.0002"), taker_fee=Decimal("0.0005"), ts_event=0, ts_init=0, ) ``` Fees and margin are explicit backtest assumptions. Check the [AX Exchange documentation](https://docs.architect.exchange/) for current rates. ## Configuration | Parameter | Value | Description | | -------------------- | ------ | ---------------------------------------------------------- | | `bb_period` | `20` | Rolling window for the BB mean and the standard deviation. | | `bb_std` | `2.0` | Band width in standard deviations. | | `rsi_period` | `14` | RSI lookback in bars. | | `rsi_buy_threshold` | `0.30` | Long entry confirmation (NautilusTrader RSI is `[0, 1]`). | | `rsi_sell_threshold` | `0.70` | Short entry confirmation. | | `trade_size` | `1` | One contract per trade (1,000 EUR notional). | :::note NautilusTrader RSI returns values in `[0.0, 1.0]`, not `[0, 100]`. The `0.30` / `0.70` thresholds correspond to the textbook 30 / 70 levels. ::: ## Backtest setup From the repository root: ```python import sys from pathlib import Path from nautilus_trader.backtest import BacktestEngine from nautilus_trader.common import LogLevel from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.config import LoggerConfig from nautilus_trader.model import AccountType from nautilus_trader.model import BarType from nautilus_trader.model import Money from nautilus_trader.model import OmsType from nautilus_trader.model import TraderId from nautilus_trader.model import Venue sys.path.insert(0, str(Path("examples/live/architect_ax"))) from strategies import BBMeanReversion from strategies import BBMeanReversionConfig engine = BacktestEngine( BacktestEngineConfig( trader_id=TraderId.from_str("BACKTESTER-001"), logging=LoggerConfig(stdout_level=LogLevel.INFO), ), ) AX = Venue("AX") engine.add_venue( venue=AX, oms_type=OmsType.NETTING, account_type=AccountType.MARGIN, base_currency=USD, starting_balances=[Money.from_str("100000 USD")], ) engine.add_instrument(EURUSD_PERP) engine.add_data(ticks) strategy = BBMeanReversion( config=BBMeanReversionConfig( instrument_id=instrument_id, bar_type=BarType.from_str("EURUSD-PERP.AX-1-MINUTE-MID-INTERNAL"), trade_size=Decimal("1"), bb_period=20, bb_std=2.0, rsi_period=14, rsi_buy_threshold=0.30, rsi_sell_threshold=0.70, ), ) engine.add_strategy(strategy) engine.run() ``` Reports come off the engine: ```python print(engine.generate_account_report(venue=AX)) print(engine.generate_order_fills_report()) print(engine.generate_positions_report()) engine.reset() engine.dispose() ``` The self-contained runnable example uses the bundled AUD/USD fixture with the same strategy and setup pattern. It is at [`architect_ax_mean_reversion.py`](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/backtest/architect_ax_mean_reversion.py). ## What the run produces Replaying TrueFX EUR/USD December 2025 through `BBMeanReversion(20, 2sd, RSI 14)` prints 44,591 1-minute mid bars and closes 1,089 positions across 2,178 fills. Cumulative realized pnl ends at **-1,287 USD**: the strategy bleeds steadily through the month with no clear regime-driven recovery. Mean reversion without a regime filter pays the spread on every cycle, and EUR/USD ran a pronounced uptrend through the second half of December which the strategy fought repeatedly. ![EUR/USD 1-minute mid bars across December 2025 with BB envelope](./assets/fx_mean_reversion_ax/panel_a_overview.png) **Figure 1.** *EUR/USD 1-minute mid bars across December 2025 with the BB middle and +/-2sd envelope. Long flat patches are weekend gaps in the TrueFX feed.* ![Twelve-hour zoom on entries, exits, and RSI](./assets/fx_mean_reversion_ax/panel_b_zoom.png) **Figure 2.** *Twelve-hour zoom around the dataset midpoint. Top: mid with BB envelope, long entries (triangles up), short entries (triangles down), and closing fills (crosses). Bottom: RSI(14) with the 0.30 buy / 0.70 sell thresholds.* ![Decision space scatter](./assets/fx_mean_reversion_ax/panel_c_decision_scatter.png) **Figure 3.** *Per-bar BB z-score against RSI for the whole month. Shaded regions mark the entry-eligible quadrants: lower-left (long) and upper-right (short). The diagonal lobe is the natural co-movement of band-relative price and RSI.* ![Cumulative realized pnl per closed position](./assets/fx_mean_reversion_ax/panel_d_pnl.png) **Figure 4.** *Cumulative realized USD pnl across closed positions. The curve declines roughly linearly, dominated by spread and small adverse moves on each cycle.* ### Regenerate the panels A self-contained renderer re-runs the backtest, computes BB and RSI on the captured bars, and writes PNG panels using the shared `nautilus_dark` tearsheet theme. After building NautilusTrader from source, run these commands from the repository root: ```bash make sync TRUEFX_CSV=test_data/local/truefx/EURUSD-2025-12.csv \ uv run --project python --no-sync \ python docs/tutorials/assets/fx_mean_reversion_ax/render_panels.py ``` Set `TRUEFX_CSV` to wherever you saved the EUR/USD archive. ## Next steps - **Add a regime filter**. The drawdown is concentrated in trending sessions. Suppress entries when realized range or a slower trend filter says the market is directional. - **Tune thresholds**. A wider band (`bb_std=2.5`) or stricter RSI cutoffs (`0.25` / `0.75`) cut entries but raise the bar for confirmation. - **Add stops**. Hard stop-loss orders cap downside per cycle and prevent carrying a losing position to the BB middle reversion. - **Go live on the AX sandbox**. Connect to the AX sandbox for paper trading once the backtest behaves. See the [AX Exchange integration guide](../integrations/architect_ax.md) for setup. ## Running live The same `BBMeanReversion` strategy runs live against AX Exchange. The launch script swaps the `BacktestEngine` for a `LiveNode` with the AX data and execution clients configured. See the live example: [`ax_mean_reversion.py`](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/architect_ax/ax_mean_reversion.py). For connection setup and API key configuration, see the [AX Exchange integration guide](../integrations/architect_ax.md). ## Further reading - [`BBMeanReversion` strategy source](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/architect_ax/strategies.py) - [Gold perpetual book imbalance tutorial](gold_book_imbalance_ax.md) - [Architect Exchange documentation](https://docs.architect.exchange/) # Gold Perpetual Book Imbalance with Proxy Futures Data (AX Exchange) Source: https://nautilustrader.io/docs/latest/tutorials/gold_book_imbalance_ax/ This tutorial backtests a top-of-book imbalance strategy on **XAU-PERP** at [AX Exchange](https://architect.exchange) using [Databento](https://databento.com) CME gold futures (`GC.v.0`) `mbp-1` quotes as a proxy. ## Introduction Top-of-book imbalance is a microstructure signal: when one side of the BBO holds significantly more resting size than the other, the book is leaning and short-term price often moves toward the thinner side as the heavier side absorbs flow. The AX example `OrderBookImbalance` strategy fires a fill-or-kill (FOK) limit order against the thicker side every time the ratio between sides clears a threshold and a cooldown has elapsed. Because the strategy only needs the BBO, it works with `mbp-1` (market by price, single best bid/ask) quote data rather than the full L2 book. That keeps source costs down for backtesting. `OrderBookImbalance` is a teaching strategy and has no edge. ```mermaid flowchart LR subgraph Inputs ["Data"] D["Databento mbp-1 quotes"] end subgraph Engine ["BacktestEngine"] L["DatabentoDataLoader"] Q["QuoteTick stream"] B["QuoteTick BBO"] end subgraph Strategy ["OrderBookImbalance"] R{{"larger >= trigger_min_size
AND smaller/larger < ratio
AND cooldown elapsed"}} D2{{"bid_size > ask_size?"}} BUY["Submit FOK BUY at best ask"] SELL["Submit FOK SELL at best bid"] end D --> L --> Q --> B B --> R R -->|yes| D2 D2 -->|yes| BUY D2 -->|no| SELL ``` ### Why proxy data AX Exchange is new and not yet covered by Databento. CME `GC` gold futures are the most liquid gold derivatives globally and provide representative microstructure for backtesting gold strategies. We use the **continuous contract** `GC.v.0` so the file stitches across expiries on the highest-volume contract, mirroring how a perpetual chases liquidity. The `stype_in="continuous"` parameter resolves the symbol through Databento's continuous mapping at request time. The `instrument_id` override at load time is safe because the continuous contract maps to a single underlying instrument at any moment. For a deeper read on the predictive power of book imbalance features, see Databento's [blog post on HFT signals with sklearn](https://databento.com/blog/hft-sklearn-python). ## Prerequisites - Python 3.12+ - [NautilusTrader installed](../getting_started/installation.md). - A Databento API key: ```bash export DATABENTO_API_KEY="your-api-key" ``` - The Databento Python client: `pip install databento`. ## Data preparation ### Download CME gold futures quotes ```python import databento as db from pathlib import Path data_path = Path("gc_gold_quotes.dbn.zst") if not data_path.exists(): client = db.Historical() data = client.timeseries.get_range( dataset="GLBX.MDP3", symbols=["GC.v.0"], stype_in="continuous", schema="mbp-1", start="2024-11-15", end="2024-11-16", ) data.to_file(data_path) ``` This pulls one trading day. The file is reused on subsequent runs. ### Load into Nautilus quote ticks `DatabentoDataLoader.load_quotes` parses the `.dbn.zst` archive and emits `QuoteTick` objects. The `instrument_id` argument overrides the Databento symbology so every tick appears to come from `XAU-PERP.AX`. ```python from nautilus_trader.adapters.databento import DatabentoDataLoader from nautilus_trader.model import InstrumentId instrument_id = InstrumentId.from_str("XAU-PERP.AX") publishers_path = Path("crates/adapters/databento/publishers.json") loader = DatabentoDataLoader(publishers_path) quotes = loader.load_quotes( filepath=data_path, instrument_id=instrument_id, ) ``` ## Instrument definition Proxy data needs a manual instrument definition. Price precision and tick size match the CME source data; margin and fee parameters reflect AX conditions. ```python from decimal import Decimal from nautilus_trader.model import AssetClass from nautilus_trader.model import Currency from nautilus_trader.model import PerpetualContract from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol USD = Currency.from_str("USD") XAU_PERP = PerpetualContract( instrument_id=instrument_id, raw_symbol=Symbol("XAU-PERP"), underlying="XAU", asset_class=AssetClass.COMMODITY, quote_currency=USD, settlement_currency=USD, is_inverse=False, price_precision=2, size_precision=0, price_increment=Price.from_str("0.01"), size_increment=Quantity.from_int(1), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), margin_init=Decimal("0.08"), margin_maint=Decimal("0.04"), maker_fee=Decimal("0.0002"), taker_fee=Decimal("0.0005"), ts_event=0, ts_init=0, ) ``` Fees are explicit backtest assumptions. Check [AX documentation](https://docs.architect.exchange/) for current rates. ## Strategy configuration The strategy subscribes to quotes and compares bid and ask sizes on each `QuoteTick`. It does not subscribe to L2 book deltas. | Parameter | Value | Description | | ------------------------------ | ------ | -------------------------------------------- | | `max_trade_size` | `10` | Cap on contracts per FOK order. | | `trigger_min_size` | `1` | Larger side must hold at least one contract. | | `trigger_imbalance_ratio` | `0.10` | Trigger when smaller / larger < 10%. | | `min_seconds_between_triggers` | `5.0` | Cooldown between consecutive triggers. | The AX examples define the strategy in [`examples/live/architect_ax/strategies.py`](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/architect_ax/strategies.py). From the repository root: ```python import sys from pathlib import Path sys.path.insert(0, str(Path("examples/live/architect_ax"))) from strategies import OrderBookImbalance from strategies import OrderBookImbalanceConfig strategy = OrderBookImbalance( OrderBookImbalanceConfig( instrument_id=instrument_id, max_trade_size=Decimal(10), trigger_min_size=Decimal(1), trigger_imbalance_ratio=Decimal("0.10"), min_seconds_between_triggers=5.0, ), ) ``` ## Backtest setup ```python from nautilus_trader.common import LogLevel from nautilus_trader.backtest import BacktestEngine from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.config import LoggerConfig from nautilus_trader.model import AccountType from nautilus_trader.model import Money from nautilus_trader.model import OmsType from nautilus_trader.model import TraderId from nautilus_trader.model import Venue engine = BacktestEngine( BacktestEngineConfig( trader_id=TraderId.from_str("BACKTESTER-001"), logging=LoggerConfig(stdout_level=LogLevel.INFO), ), ) AX = Venue("AX") engine.add_venue( venue=AX, oms_type=OmsType.NETTING, account_type=AccountType.MARGIN, base_currency=USD, starting_balances=[Money.from_str("100000 USD")], ) engine.add_instrument(XAU_PERP) engine.add_data(quotes) engine.add_strategy(strategy) engine.run() ``` Reports are on the engine: ```python print(engine.generate_account_report(venue=AX)) print(engine.generate_order_fills_report()) print(engine.generate_positions_report()) engine.reset() engine.dispose() ``` The runnable example is at [`architect_ax_book_imbalance.py`](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/backtest/architect_ax_book_imbalance.py). ## What the run produces Replaying 2024-11-15 GC.v.0 mbp-1 (one trading day) through `OrderBookImbalance(0.10, 1.0, 5s)` prints 2,378 FOK fills net into 5 closed position cycles. Cumulative realized pnl ends at **-4,170 USD**: the strategy bleeds steadily across the day, mostly through spread cost on incremental FOK fills that add to existing positions. ![GC.v.0 top of book around an active cycle](./assets/gold_book_imbalance_ax/panel_a_top_book.png) **Figure 1.** *GC.v.0 top of book around the cycle that opened with a short entry near 09:26 and exited near 09:31, then re-entered long until 09:35. Triangles are entries from flat, crosses are returns to flat, open circles are incremental FOK fills that grew the position.* ![Imbalance ratio distribution](./assets/gold_book_imbalance_ax/panel_b_imbalance_dist.png) **Figure 2.** *`smaller / larger` BBO size ratio across all sampled top-of-book snapshots, with the 0.10 trigger threshold marked. The mass left of the threshold is the addressable trigger region.* ![Mid and top-of-book size across the day](./assets/gold_book_imbalance_ax/panel_c_size_landscape.png) **Figure 3.** *Mid price (top) and best bid/ask size in contracts (bottom) across the trading day. Top-of-book sizes flicker between roughly two and fifty contracts; the mid traverses about a fifteen-dollar range.* ![Cumulative realized pnl per closed position](./assets/gold_book_imbalance_ax/panel_d_pnl.png) **Figure 4.** *Cumulative realized USD pnl across the five closed position cycles. The slope is consistently negative and the per-cycle pnl is dominated by spread.* ### Regenerate the panels A self-contained renderer re-runs the backtest with a quote-sampling actor and writes PNGs to the asset directory using the `nautilus_dark` tearsheet theme. After building NautilusTrader from source, run these commands from the repository root: ```bash make sync GC_DBN=test_data/local/Databento/gc_gold_quotes.dbn.zst \ uv run --project python --no-sync \ python docs/tutorials/assets/gold_book_imbalance_ax/render_panels.py ``` ## Next steps - **Stricter trigger**. Lower `trigger_imbalance_ratio` to `0.05` or raise `trigger_min_size` to `5` to require more conviction before firing. - **Different sessions**. Replay regular trading hours (RTH) only or roll through several days to see how the strategy behaves across regimes. - **Other instruments**. AX offers FX perpetuals (`EURUSD-PERP`, `GBPUSD-PERP`) and silver (`XAG-PERP`). The same proxy approach works with the corresponding CME futures. - **Go live on the AX sandbox**. See the [AX Exchange integration guide](../integrations/architect_ax.md) once the backtest behaves. ## Running live The same `OrderBookImbalance` strategy runs live against AX Exchange. The launch script swaps the `BacktestEngine` for a `LiveNode` with the AX data and execution clients configured. See the live example: [`ax_book_imbalance.py`](https://github.com/nautechsystems/nautilus_trader/tree/master/examples/live/architect_ax/ax_book_imbalance.py). For connection setup and API key configuration, see the [AX Exchange integration guide](../integrations/architect_ax.md). ## Further reading - [`OrderBookImbalance` strategy source](https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/architect_ax/strategies.py) - [Mean Reversion with Proxy FX Data tutorial](fx_mean_reversion_ax.md) - [Architect Exchange documentation](https://docs.architect.exchange/) - [Databento: HFT signals with sklearn](https://databento.com/blog/hft-sklearn-python) # On-Chain Grid Market Making with Short-Term Orders (dYdX) Source: https://nautilustrader.io/docs/latest/tutorials/grid_market_maker_dydx/ This tutorial runs the shipped `GridMarketMaker` strategy on dYdX v4 through the Rust `LiveNode`. The strategy places symmetric limit orders around the mid, skews the grid to manage inventory, and lets the venue cycle short-term orders by time-to-block expiry instead of explicit cancels. ## Introduction A grid market maker maintains a ladder of resting buy and sell limits at fixed price intervals around the current mid. When an order fills, the strategy profits from the spread between the buy and sell levels. Inventory management keeps net exposure within `max_position` so the grid does not accumulate a directional position. ```mermaid flowchart LR subgraph Inputs ["Quote feed"] Q["BBO QuoteTick"] end subgraph Strategy ["GridMarketMaker"] M["mid = (bid + ask) / 2"] TH{{"|mid - last_mid|
>= requote_threshold_bps
OR no resting orders"}} CA["cancel_all_orders()"] SK["skew = skew_factor * net_position"] GR["Geometric grid:
buy_n = mid * (1 - bps/10000)^n - skew
sell_n = mid * (1 + bps/10000)^n - skew"] SUB["Submit GTD short-term limits
expire_time_secs = 8"] end subgraph Adapter ["dYdX execution adapter"] CL{{"expire_time_secs
< max_short_term_secs?"}} ST["Short-term path:
GoodTilBlock = current + N"] LT["Long-term path:
standard cancel-on-replace"] end Q --> M --> TH TH -->|yes| CA --> SK --> GR --> SUB TH -->|no| Q SUB --> CL CL -->|yes| ST CL -->|no| LT ``` ### Inventory skewing (Avellaneda-Stoikov inspired) When the position grows long the entire grid shifts down (cheaper buys, cheaper sells) to encourage the next fill on the sell side. When the position grows short the grid shifts up. This mirrors the Avellaneda-Stoikov framework adapted to a discrete grid. ### Why dYdX v4 dYdX v4 fits market-making well: - **Short-term orders** with ~20-second expiry: low-latency placement, no on-chain storage cost. - **~0.5-second block times** for fast confirmation cycles. - **No gas fees for cancellations**: short-term cancels are free under GTB replay protection. - **On-chain order book** with deterministic per-block matching. - **Batch cancel**: one `MsgBatchCancel` clears every short-term order. ## Prerequisites ### Funded dYdX account You need a dYdX account with USDC collateral. See the [Testnet setup](../integrations/dydx.md#testnet-setup) section in the integration guide for instructions on creating and funding a testnet account. The testnet wallet also needs an API trading key registered through the dYdX UI. ### Environment variables ```bash # Mainnet export DYDX_PRIVATE_KEY="0x..." export DYDX_WALLET_ADDRESS="dydx1..." # Testnet export DYDX_TESTNET_PRIVATE_KEY="0x..." export DYDX_TESTNET_WALLET_ADDRESS="dydx1..." ``` ## Strategy overview ### Geometric grid pricing Each level is a fixed percentage (basis points) away from mid: ``` Buy level N: mid * (1 - bps/10000)^N - skew Sell level N: mid * (1 + bps/10000)^N - skew ``` Where `skew = skew_factor * net_position`. For a 3-level grid with `grid_step_bps=100` (1%) around a mid of 1000.00: ``` Sell 3: 1030.30 Sell 2: 1020.10 Sell 1: 1010.00 ─── Mid: 1000.00 ─── Buy 1: 990.00 Buy 2: 980.10 Buy 3: 970.30 ``` With a long-2 position and `skew_factor=1.0`, the entire grid shifts down by 2.0: ``` Sell 3: 1028.30 Sell 2: 1018.10 Sell 1: 1008.00 ─── Mid: 1000.00 ─── Buy 1: 988.00 Buy 2: 978.10 Buy 3: 968.30 ``` ### Inventory management The strategy enforces position limits through two mechanisms: 1. **`max_position`**: a hard cap on net exposure (long or short). When the projected exposure from adding the next grid level would breach this cap, that level is skipped. 2. **Projected exposure tracking**: before placing each level the strategy tracks the worst-case per-side exposure (current position + all pending buy / sell orders) to avoid over-committing. `cancel_all_orders` is asynchronous, so pending orders may still fill between the cancel request and acknowledgement. Tracking worst-case per-side exposure prevents momentary over-exposure during cancel-requote transitions. ### Requote threshold `requote_threshold_bps` controls how much the mid must move before the strategy cancels all open orders and places a fresh grid: - **Lower threshold** (5 bps): more responsive, more cancel/place transactions. - **Higher threshold** (50 bps): fewer transactions, but orders may sit further from the current price. ## Configuration | Parameter | Type | Default | Description | | ----------------------- | -------------- | ---------- | ------------------------------------------------------------------------ | | `instrument_id` | `InstrumentId` | *required* | Instrument to trade (e.g. `ETH-USD-PERP.DYDX`). | | `max_position` | `Quantity` | *required* | Maximum net exposure (long or short). | | `trade_size` | `Quantity` | `None` | Size per grid level. If `None`, uses instrument's `min_quantity` or 1.0. | | `num_levels` | `usize` | `3` | Number of buy and sell levels. | | `grid_step_bps` | `u32` | `10` | Grid spacing in basis points (10 = 0.1%). | | `skew_factor` | `f64` | `0.0` | How aggressively to shift the grid based on inventory. | | `requote_threshold_bps` | `u32` | `5` | Minimum mid-price move in bps before re-quoting. | | `expire_time_secs` | `Option` | `None` | Order expiry in seconds. Uses GTD when set, GTC otherwise. | | `on_cancel_resubmit` | `bool` | `false` | Resubmit grid on next quote after an unexpected cancel. | ### Choosing parameters - **`grid_step_bps`**: 50-100 bps in volatile markets, 5-20 bps in calm conditions. Wider grids capture more spread per fill but fill less often. - **`skew_factor`**: start at `0.0`. A value of `0.5` shifts the grid by 0.5 price units per unit of net position. Too aggressive a skew can move the grid entirely above or below mid. - **`expire_time_secs`**: for dYdX short-term orders, set to `8` seconds. That fits inside the 40-block (~20 s) short-term window and keeps the orders on the fast short-term path. When `None`, orders use GTC and the long-term path. - **`on_cancel_resubmit`**: triggers a resubmission on the next quote tick after a cancel that the strategy did not initiate (short-term order expiry from the indexer, self-trade prevention, risk limits). The indexer emits a cancel event for each short-term order shortly after it expires; this flag resets the requote anchor so the next quote rebuilds the grid even if the mid has not moved beyond `requote_threshold_bps`. ## dYdX-specific considerations ### Short-term order expiry When `expire_time_secs=8`, orders are classified as short-term by the adapter: 1. The adapter checks `8s < max_short_term_secs (40 blocks * ~0.5s = ~20s)`. 2. The order is submitted as short-term with `GoodTilBlock = current_height + N`. 3. The order expires on chain after about eight seconds if not filled. Expiry costs no gas (GTB replay protection handles it on chain), but the indexer still emits an `OrderCanceled` event for each expired order shortly after the expiry block, so the strategy observes the expiry through the normal cancel event path. This is the recommended configuration for market making because: - Short-term orders have lower latency. - Expiry has no on-chain gas cost. - Continuous requoting (driven by the indexer-emitted cancel events when `on_cancel_resubmit=true`) replaces expired orders. See the [order classification](../integrations/dydx.md#order-classification) section in the integration guide for full details. ### Unexpected cancels and `on_cancel_resubmit` The `pending_self_cancels` set distinguishes self-initiated from unexpected cancels: 1. When the strategy calls `cancel_all_orders`, it records all open order IDs in `pending_self_cancels`. 2. When `on_order_canceled` fires: - If the order ID is in `pending_self_cancels`, it is a self-cancel and no action is needed. - Otherwise it was not strategy-initiated (short-term order expiry, self-trade prevention, or a risk limit). Reset `last_quoted_mid` so the next quote triggers a full grid resubmission. This stops the strategy re-quoting unnecessarily during its own cancel waves while still responding to surprises. `on_order_filled` also removes the order from `pending_self_cancels`. If an order fills before the cancel acknowledgement arrives, this prevents stale entries from accumulating. ### Order quantization Price and size quantization for dYdX markets is handled automatically by the adapter's `OrderMessageBuilder`. No manual rounding or conversion is needed. See [Price and size quantization](../integrations/dydx.md#price-and-size-quantization) for details. ### Post-only orders All grid orders are submitted with `post_only=true`. The exchange rejects any order that would cross the spread at match time, so every fill lands at the maker fee rate and the grid never inadvertently lifts its own offers during requote transitions. ## Running and stopping ### Environment setup Credentials load from environment variables or a `.env` file at the project root (loaded automatically via `dotenvy`): ```bash # Direct export export DYDX_PRIVATE_KEY="0x..." export DYDX_WALLET_ADDRESS="dydx1..." ``` ```bash # .env equivalent DYDX_PRIVATE_KEY=0x... DYDX_WALLET_ADDRESS=dydx1... ``` ### Run the example ```bash cargo run --example dydx-grid-mm --package nautilus-dydx --features examples ``` The example targets mainnet. To run against testnet, set the `DYDX_NETWORK` constant near the top of the example to `DydxNetwork::Testnet` (this needs a testnet API trading key) and rebuild. ### Graceful shutdown Press **Ctrl+C** to stop the node. The shutdown sequence: 1. SIGINT received, trader stops, `on_stop` fires. 2. Strategy cancels all orders and closes positions. 3. 5-second grace period (`delay_post_stop_secs`) processes residual events. 4. Clients disconnect, node exits. ## Code walkthrough The `main` function lives at [`crates/adapters/dydx/examples/node_grid_mm.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/dydx/examples/node_grid_mm.rs): ```rust const DYDX_NETWORK: DydxNetwork = DydxNetwork::Mainnet; #[tokio::main] async fn main() -> Result<(), Box> { dotenvy::dotenv().ok(); let network = DYDX_NETWORK; let environment = Environment::Live; let trader_id = TraderId::from("TESTER-001"); let account_id = AccountId::from("DYDX-001"); let node_name = "DYDX-GRID-MM-001".to_string(); let instrument_id = InstrumentId::from("ETH-USD-PERP.DYDX"); let data_config = DydxDataClientConfig { network, ..Default::default() }; let exec_config = DydxExecutionClientConfig { account_id, network, ..Default::default() }; let data_factory = DydxDataClientFactory::new(); let exec_factory = DydxExecutionClientFactory::new(); let log_config = LoggerConfig { stdout_level: LevelFilter::Info, ..Default::default() }; let mut node = LiveNode::builder(trader_id, environment)? .with_name(node_name) .with_logging(log_config) .add_data_client(None, Box::new(data_factory), Box::new(data_config))? .add_exec_client(None, Box::new(exec_factory), Box::new(exec_config))? .with_reconciliation(false) .with_delay_post_stop_secs(5) .build()?; let config = GridMarketMakerConfig::builder() .instrument_id(instrument_id) .max_position(Quantity::from("0.10")) .num_levels(3) .grid_step_bps(100) .skew_factor(0.5) .requote_threshold_bps(10) .expire_time_secs(8) .on_cancel_resubmit(true) .build(); let strategy = GridMarketMaker::new(config); node.add_strategy(strategy)?; node.run().await?; Ok(()) } ``` Configuration points: - **`dotenvy::dotenv().ok()`**: loads `.env` from the project root if present. - **`with_reconciliation(false)`**: disabled for simplicity; enable in production to resume state across restarts. - **`with_delay_post_stop_secs(5)`**: grace period for pending cancel and close events to finalize during shutdown. ### Event flow ```mermaid flowchart TB A[LiveNode starts] --> B[connect: HTTP instruments + WebSocket channels] B --> C[on_start subscribes to quotes] C --> D[on_quote] D --> E{should_requote?} E -->|no| D E -->|yes| F[cancel_all_orders] F --> G[compute grid with skew] G --> H[submit GTD short-term limits] H --> I[on_order_filled] H --> J[on_order_canceled] I --> D J --> D K[on_stop] --> L[cancel_all_orders + close positions] ``` ## Strategy internals The key Rust snippets from `grid_mm.rs` follow. ### Trade size resolution (`on_start`) Trade size resolves from the instrument cache: config value first, then the instrument's `min_quantity`, then `1.0` as a final fallback. ```rust fn on_start(&mut self) -> anyhow::Result<()> { let instrument_id = self.config.instrument_id; let (instrument, size_precision, min_quantity) = { let cache = self.cache(); let instrument = cache .instrument(&instrument_id) .ok_or_else(|| anyhow::anyhow!("Instrument {instrument_id} not found in cache"))?; ( instrument.clone(), instrument.size_precision(), instrument.min_quantity(), ) }; self.price_precision = Some(instrument.price_precision()); self.instrument = Some(instrument); if self.trade_size.is_none() { self.trade_size = Some(min_quantity.unwrap_or_else(|| Quantity::new(1.0, size_precision))); } self.subscribe_quotes(instrument_id, None, None); Ok(()) } ``` ### Quote handler (`on_quote`, abbreviated) ```rust fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> { let mid_f64 = (quote.bid_price.as_f64() + quote.ask_price.as_f64()) / 2.0; let mid = Price::new( mid_f64, self.price_precision .expect("price_precision should be resolved in on_start"), ); if !self.should_requote(mid) { return Ok(()); // Mid hasn't moved enough, keep existing grid } self.cancel_all_orders( instrument_id, None, None, true, // Restrict cancellation to this strategy. None, )?; let (net_position, worst_long, worst_short) = { /* ... */ }; let grid = self.grid_orders(mid, net_position, worst_long, worst_short); if grid.is_empty() { return Ok(()); // Don't advance requote anchor when fully constrained } let (tif, expire_time) = match self.config.expire_time_secs { Some(secs) => { let now_ns = self.clock().timestamp_ns(); let expire_ns = now_ns + secs * 1_000_000_000; (Some(TimeInForce::Gtd), Some(expire_ns)) } None => (None, None), }; for (side, price) in grid { let order = self.order().limit( instrument_id, side, trade_size, price, tif, expire_time, Some(true), // post_only ); self.submit_order(order, None, None)?; } self.last_quoted_mid = Some(mid); Ok(()) } ``` ### Grid pricing (`grid_orders`) Computes geometric grid prices and enforces `max_position` per level: ```rust fn grid_orders( &self, mid: Price, net_position: f64, worst_long: Decimal, worst_short: Decimal, ) -> Vec<(OrderSide, Price)> { let instrument = self .instrument .as_ref() .expect("instrument should be resolved in on_start"); let mid_f64 = mid.as_f64(); let skew_f64 = self.config.skew_factor * net_position; let pct = self.config.grid_step_bps as f64 / 10_000.0; let trade_size = self .trade_size .expect("trade_size should be resolved in on_start") .as_decimal(); let max_pos = self.config.max_position.as_decimal(); let mut projected_long = worst_long; let mut projected_short = worst_short; let mut orders = Vec::new(); for level in 1..=self.config.num_levels { let buy_f64 = mid_f64 * (1.0 - pct).powi(level as i32) - skew_f64; let sell_f64 = mid_f64 * (1.0 + pct).powi(level as i32) - skew_f64; let buy_price = instrument.next_bid_price(buy_f64, 0); let sell_price = instrument.next_ask_price(sell_f64, 0); if let Some(buy_price) = buy_price && projected_long + trade_size <= max_pos { orders.push((OrderSide::Buy, buy_price)); projected_long += trade_size; } if let Some(sell_price) = sell_price && projected_short - trade_size >= -max_pos { orders.push((OrderSide::Sell, sell_price)); projected_short -= trade_size; } } orders } ``` ## What a 35-second mainnet run produces A 35-second mainnet run on `ETH-USD-PERP.DYDX` with the example config (`grid_step_bps=100`, `num_levels=3`, `skew_factor=0.5`, `requote_threshold_bps=10`, `expire_time_secs=8`) captures 47 requote events, 276 order submissions, 67 accepts, and 54 cancels. ETH was trading near 2,281 USD: the price never moved enough to trip the 10 bps requote threshold, so most cycles trigger from the periodic 8-second short-term order expiry rather than from price movement. ![ETH-USD-PERP mid with theoretical grid bands](./assets/grid_market_maker_dydx/panel_a_grid_overlay.png) **Figure 1.** *ETH-USD-PERP mid at every requote with the six theoretical grid bands (3 levels each side, 100 bps step). Mid sits near 2,281 USD; the inner buy and sell levels are at ~2,258 and ~2,304 USD.* ![Order lifetime distribution](./assets/grid_market_maker_dydx/panel_b_order_lifetime.png) **Figure 2.** *Time from `OrderAccepted` to `OrderCanceled` per short-term order, in seconds. The mass near 7-8 seconds matches the `expire_time_secs=8` setting; the smaller cluster below 6 seconds is strategy-initiated cancels during requote transitions.* ![Orders submitted per 250-ms bucket](./assets/grid_market_maker_dydx/panel_c_orders_per_cycle.png) **Figure 3.** *Order submission count per 250-ms bucket, split by side. Each requote cycle places six orders (3 buys + 3 sells); the spacing between bursts is the requote interval.* ![Short-term order timeline](./assets/grid_market_maker_dydx/panel_d_short_term_timeline.png) **Figure 4.** *Theoretical short-term order timeline with `expire_time_secs=8` and 0.5-second blocks. The bottom panel tracks how the chain block height advances; each order's `GoodTilBlock` target is set to ~16 blocks ahead, giving the eight-second expiry.* ### Regenerate the panels After building NautilusTrader from source, run these commands from the repository root: ```bash make sync # Capture a 35-second mainnet run. timeout 35 ./target/release/examples/dydx-grid-mm > /tmp/dydx_main.log 2>&1 DYDX_LOG=/tmp/dydx_main.log \ uv run --project python --no-sync \ python docs/tutorials/assets/grid_market_maker_dydx/render_panels.py ``` ## Monitoring and understanding output ### Log messages | Log message | Meaning | | --------------------------------------------------- | ------------------------------------------------------- | | `Requoting grid: mid=X, last_mid=Y` | Mid moved beyond threshold, refreshing grid. | | `Submit short-term order N` | Order submitted via short-term broadcast path. | | `BatchCancel N short-term orders` | Batch cancel executed for expired/stale orders. | | `benign cancel error, treating as success` | Cancel for an already-filled or expired order (normal). | | `Sequence mismatch detected, will resync and retry` | Cosmos SDK sequence error, auto-recovering. | ### Expected behavior patterns 1. **Startup**: instruments load, WebSocket connects, first quote triggers initial grid. 2. **Steady state**: grid persists across ticks; requotes only when mid moves more than `requote_threshold_bps`. 3. **Fills**: position updates, skew adjusts, the next requote shifts the grid. 4. **Expiry**: short-term orders expire on chain after about eight seconds; the indexer emits a cancel event for each, and the next quote refreshes the grid. 5. **Shutdown**: all orders cancelled, positions closed, WebSocket disconnected. ## Customization tips ### High vs low volatility | Condition | Adjustment | | --------------- | ------------------------------------------------------------------------- | | High volatility | Wider `grid_step_bps` (100-200), fewer `num_levels`, lower `skew_factor`. | | Low volatility | Tighter `grid_step_bps` (10-30), more `num_levels`, higher `skew_factor`. | | Thin liquidity | Increase `requote_threshold_bps` to reduce cancel frequency. | ### Multiple instruments Run separate `GridMarketMaker` instances per instrument. Each instance manages its own grid, position, and cancel state independently: ```rust let btc_config = GridMarketMakerConfig::builder() .instrument_id(InstrumentId::from("BTC-USD-PERP.DYDX")) .max_position(Quantity::from("0.001")) .base( StrategyConfig::builder() .strategy_id(StrategyId::from("GRID_MM-BTC")) .order_id_tag("BTC".to_string()) .build(), ) .grid_step_bps(50) .build(); let eth_config = GridMarketMakerConfig::builder() .instrument_id(InstrumentId::from("ETH-USD-PERP.DYDX")) .max_position(Quantity::from("0.10")) .base( StrategyConfig::builder() .strategy_id(StrategyId::from("GRID_MM-ETH")) .order_id_tag("ETH".to_string()) .build(), ) .grid_step_bps(100) .build(); node.add_strategy(GridMarketMaker::new(btc_config))?; node.add_strategy(GridMarketMaker::new(eth_config))?; ``` ### Mainnet vs testnet toggle The example selects the network from the `DYDX_NETWORK` constant near the top of the file (`DydxNetwork::Mainnet` by default). Change it to `DydxNetwork::Testnet` and rebuild to run against testnet. ## Further reading - [dYdX v4 Integration Guide](../integrations/dydx.md): full adapter reference. - [dYdX Protocol Documentation](https://docs.dydx.xyz/): official protocol docs. - [Order types](https://docs.dydx.xyz/concepts/trading/orders): protocol-level order mechanics. # Hurst/VPIN Directional Strategy (Kraken Futures) Source: https://nautilustrader.io/docs/latest/tutorials/hurst_vpin_kraken/ :::note This is a **Rust-only** tutorial. The strategy, backtest wiring, and tests all live in the compiled core. ::: This tutorial backtests a directional strategy on **PF_XBTUSD**, the USD-margined Bitcoin perpetual on [Kraken Futures](https://futures.kraken.com). The strategy combines a **Hurst-exponent regime filter** with a **VPIN** (Volume-synchronized Probability of Informed Trading) flow signal. Historical trades and quotes come from [Tardis.dev](https://tardis.dev) and replay through the Rust `BacktestEngine`. ## Introduction The strategy combines three components: a **slow regime filter** derived from bars, a **fast informed-flow signal** derived from trades, and a **quote-driven entry** that fires only when both align. - **Hurst exponent on dollar bars.** Sampled on constant-notional (value) bars, following Lopez de Prado (*Advances in Financial Machine Learning*, Chapter 2). A rescaled-range (R/S) estimate above `0.55` indicates persistent, trending behavior; below `0.50` the series is mean-reverting or noise. - **VPIN from trade aggressor flow.** Each completed dollar bar is treated as one volume bucket. The absolute imbalance between aggressive buy and aggressive sell volume, averaged over the last fifty buckets, gives the VPIN level. The signed imbalance gives the net informed direction. - **Quote-driven entry.** Once both signals agree, the strategy opens a position on the next quote tick. Entry timing stays tied to the live top of book rather than to bar closes. Exit is driven by the same ingredients: position is closed when the Hurst estimate decays back through a lower threshold, or when a holding-time cap is reached. The strategy is shipped as [`HurstVpinDirectional`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/strategies/hurst_vpin_directional) in the `nautilus_trading::examples::strategies` module. As with all shipped example strategies, it is intentionally simple and has no alpha advantage. ### Why Kraken Futures Kraken Futures lists perpetuals on Bitcoin and Ether in two forms: - **`PI_` inverse perpetuals**: quoted in USD, margined and settled in the underlying. - **`PF_` linear perpetuals**: quoted in USD, margined and settled in USD (via multi-collateral). The tutorial uses **`PF_XBTUSD`** so the account currency, quote currency, and dollar-bar sampling frame are all USD. ### Why dollar bars and VPIN together VPIN is defined on *volume* buckets rather than *time* buckets. Dollar bars (`VALUE` aggregation in NautilusTrader) close after a fixed notional has traded, so the sampling frame adapts to market activity. Defining each VPIN bucket as one dollar bar keeps both signals on the same clock, and Hurst sampled on the same bars uses the same frame. ## Prerequisites - A working Rust toolchain (see [rustup.rs](https://rustup.rs)). - The NautilusTrader repository cloned and building. - Internet access to download a free Tardis sample (no API key required for the first day of each month). ## Data preparation Kraken Futures is published by Tardis under the historical slug `cryptofacilities`. The first day of each month is available for free without an API key, which is enough for a single-day plumbing check. A full backtest needs at least two sessions to warm the 128-bar Hurst window, which requires a paid Tardis API key (see the tip below). ```bash mkdir -p /tmp/tardis_kraken curl -L -o /tmp/tardis_kraken/PF_XBTUSD_trades.csv.gz \ https://datasets.tardis.dev/v1/cryptofacilities/trades/2024/01/01/PF_XBTUSD.csv.gz curl -L -o /tmp/tardis_kraken/PF_XBTUSD_quotes.csv.gz \ https://datasets.tardis.dev/v1/cryptofacilities/quotes/2024/01/01/PF_XBTUSD.csv.gz ``` The runnable example binary shown later in this tutorial reads from `/tmp/tardis_kraken/` by default, so downloading into that directory up front means `cargo run` works without needing `KRAKEN_TRADES` or `KRAKEN_QUOTES` overrides. :::tip Full historical ranges require a paid Tardis API key. Use the [Tardis download utility](https://docs.tardis.dev/downloadable-csv-files) for bulk fetches once you move beyond single-day samples. ::: The Rust Tardis loader parses `.csv.gz` directly and tags each record with the instrument ID we supply, so no symbology mapping is needed at the strategy level: ```rust use nautilus_model::identifiers::InstrumentId; use nautilus_tardis::csv::load::{load_quotes, load_trades}; let instrument_id = InstrumentId::from("PF_XBTUSD.KRAKEN"); let trades = load_trades( "PF_XBTUSD_trades.csv.gz", Some(1), // price_precision Some(4), // size_precision Some(instrument_id), None, // limit )?; let quotes = load_quotes( "PF_XBTUSD_quotes.csv.gz", Some(1), Some(4), Some(instrument_id), None, )?; ``` Pass the instrument's `price_precision` and `size_precision` explicitly. The loader otherwise infers precision from the first few records, and a sample day without fractional prices can infer `0`, which the matching engine will reject when it sees a quote tick that does not match the instrument's declared precision. ## Instrument definition Since we are loading CSV data directly rather than through the live Kraken adapter, we define `PF_XBTUSD` manually as a [`CryptoPerpetual`](https://github.com/nautechsystems/nautilus_trader/blob/master/crates/model/src/instruments/crypto_perpetual.rs). Linear perpetuals on Kraken Futures are quoted and margined in USD: ```rust use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::CryptoPerpetual, types::{Currency, Price, Quantity}, }; use rust_decimal_macros::dec; let instrument = CryptoPerpetual::builder() .instrument_id(InstrumentId::from("PF_XBTUSD.KRAKEN")) .raw_symbol(Symbol::from("PF_XBTUSD")) .base_currency(Currency::BTC()) // base .quote_currency(Currency::USD()) // quote .settlement_currency(Currency::USD()) // settlement (linear) .is_inverse(false) .price_precision(1) .size_precision(4) .price_increment(Price::from("0.5")) .size_increment(Quantity::from("0.0001")) .margin_init(dec!(0.02)) .margin_maint(dec!(0.01)) .maker_fee(dec!(0.0002)) .taker_fee(dec!(0.0005)) .ts_event(0.into()) .ts_init(0.into()) .build() .unwrap(); ``` Fees and margin are explicit backtest assumptions. Check the [Kraken Futures fee schedule](https://futures.kraken.com/features/fee-schedule) for current rates. ## Dollar-bar sampling NautilusTrader ships all the information-driven bar aggregators from AFML Chapter 2: tick, volume, value (dollar), plus imbalance and runs variants for each. We use plain `VALUE` bars here, which close after a fixed notional has traded on the tape. The bar type is expressed as a string. The `INTERNAL` suffix tells the engine to aggregate inside NautilusTrader from the underlying trade stream (price type `LAST`): ```rust use nautilus_model::data::BarType; let bar_type = BarType::from("PF_XBTUSD.KRAKEN-2000000-VALUE-LAST-INTERNAL"); ``` Each bar closes after **USD 2,000,000** of traded notional. A session typically prints under 150 bars at this size, short of the 128-bar Hurst window, so warming the defaults needs multiple sessions. For a single-day run, either shrink the bar size to **USD 500,000** or drop `hurst_window` and `vpin_window` accordingly. Full multi-day backtests should use the defaults. :::note `VALUE` bars are a *view* onto the trade tape. The backtest engine consumes the same stream that drives VPIN, so there is no double counting. ::: ## Strategy overview The `HurstVpinDirectional` strategy runs three concurrent pipelines that synchronize at bar close: trades feed a bucket accumulator, bar close triggers signal recomputation, and quotes drive entry and timeout checks. ```mermaid flowchart LR subgraph Inputs ["Data streams"] T["TradeTick"] B["Value bar close
(USD 2M notional)"] Q["QuoteTick"] end subgraph State ["Rolling state"] BV["Bucket buy / sell volume"] RET["Log-return window"] IMB["Imbalance window"] end subgraph Signals ["Signals"] H(("Hurst")) V(("VPIN + signed")) end subgraph Gates ["Decision gates"] E{"Flat AND
Hurst >= 0.55 AND
VPIN >= 0.30"} R{"Open AND
Hurst < 0.50"} X{"Open AND
held > max_holding_secs"} end subgraph Orders ["Orders"] Op["Market IOC
side = sign(signed VPIN)"] Cl["Close position"] end T -->|aggressor| BV B -->|log return| RET BV -.->|snapshot + reset
on bar close| IMB RET --> H IMB --> V H --> E V --> E Q -->|tick| E E -->|yes| Op H --> R R -->|yes| Cl Q -->|tick| X X -->|yes| Cl ``` 1. **Per trade**: accumulate aggressive buy and aggressive sell volume for the current dollar-bar bucket using `TradeTick::aggressor_side`. 2. **Per bar** (bucket close): append the bar's log return to the Hurst window, compute the bucket's signed and absolute imbalance, reset the accumulators, re-estimate rolling Hurst and VPIN, clear `exit_cooldown`, and check regime exit. 3. **Per quote**: if flat and both signals agree (Hurst trending, VPIN above threshold, signed imbalance non-zero), open a market IOC order. If already positioned, check the holding-time cap. Regime exit fires from the bar pipeline when Hurst drops below `hurst_exit`. Holding timeout fires from the quote pipeline when the position has been open longer than `max_holding_secs`. ![Signal dashboard during an active trading window](./assets/hurst_vpin_kraken/panel_b_dashboard.png) **Figure 1.** *Signal dashboard for 2024-01-16 14:09-16:15 UTC: close, Hurst, VPIN. Markers sit at actual fill price; dotted connector shows slip against the bar-close line.* ### Hurst estimator The strategy uses classical rescaled-range (R/S) regression. For each lag `k` in `(4, 8, 16, 32)`, the return window is split into non-overlapping chunks of length `k`, each chunk's rescaled range is computed, and the mean R/S is recorded. The slope of `log(R/S)` vs `log(k)` across the lag set gives the Hurst estimate. ![Hurst exponent over the full backtest](./assets/hurst_vpin_kraken/panel_e_hurst_only.png) **Figure 2.** *Rolling Hurst across 14 days of PF_XBTUSD (2024-01-15 to 2024-01-28) with enter 0.55 and exit 0.50 thresholds.* ### VPIN estimator With explicit trade aggressor side available from the venue feed, VPIN collapses to ``` VPIN = mean_k ( |V_B_k - V_S_k| / (V_B_k + V_S_k) ) ``` over the last `k` completed dollar-bar buckets. The signed variant retains the sign of `V_B - V_S` and is used to choose direction. This is more accurate than the bulk-volume classification used in the original Easley/Lopez de Prado formulation, which was only necessary when aggressor side was not directly observable. ![VPIN distribution across the backtest](./assets/hurst_vpin_kraken/panel_d_vpin_hist.png) **Figure 3.** *VPIN distribution across all bars with the 0.30 entry threshold.* ### Configuration | Parameter | Value | Description | | ------------------ | ---------------- | ----------------------------------------------------------------- | | `bar_type` | `2M-VALUE-LAST` | Dollar bars closing every USD 2,000,000 of notional. | | `trade_size` | `0.0100` | 0.0100 XBT per trade (matches instrument precision). | | `hurst_window` | `128` | Rolling window of dollar bar log returns. | | `hurst_lags` | `[4, 8, 16, 32]` | Lag set used in the R/S regression. | | `hurst_enter` | `0.55` | Above this, the regime is treated as trending. | | `hurst_exit` | `0.50` | Below this, open positions are flattened. | | `vpin_window` | `50` | Completed volume buckets averaged for VPIN. | | `vpin_threshold` | `0.30` | Minimum VPIN for flow to be considered informed. | | `max_holding_secs` | `1800` | Seconds a position may be held (default `3600`; overridden here). | Set `hurst_window` and `vpin_window` to values in `[1, 16_384]`. :::tip Dollar-bar size, Hurst lags, and VPIN window are all coupled. Smaller bars give faster reaction but noisier Hurst; larger bars smooth both signals but risk too few samples in a single-day backtest. ::: ## Backtest setup Configure a `BacktestEngine` with a Kraken venue and a USD starting balance: ```rust use nautilus_backtest::{ config::{BacktestEngineConfig, SimulatedVenueConfig}, engine::BacktestEngine, }; use nautilus_model::{ data::Data, enums::{AccountType, BookType, OmsType}, identifiers::Venue, instruments::{Instrument, InstrumentAny}, types::Money, }; let mut engine = BacktestEngine::new(BacktestEngineConfig::default())?; engine.add_venue( SimulatedVenueConfig::builder() .venue(Venue::from("KRAKEN")) .oms_type(OmsType::Netting) .account_type(AccountType::Margin) .book_type(BookType::L1_MBP) .starting_balances(vec![Money::from("100_000 USD")]) .build()?, )?; engine.add_instrument(&InstrumentAny::CryptoPerpetual(instrument))?; ``` Feed the loaded trades and quotes as `Data` enum variants: ```rust let mut data: Vec = trades.into_iter().map(Data::Trade).collect(); data.extend(quotes.into_iter().map(Data::Quote)); engine.add_data(data, None, true, true)?; ``` ### Add the strategy ```rust use nautilus_model::types::Quantity; use nautilus_trading::examples::strategies::{ HurstVpinDirectional, HurstVpinDirectionalConfig, }; let config = HurstVpinDirectionalConfig::builder() .instrument_id(instrument_id) .bar_type(bar_type) .trade_size(Quantity::from("0.0100")) // match instrument size_precision .max_holding_secs(1800) .build(); engine.add_strategy(HurstVpinDirectional::new(config))?; ``` ### Run the backtest ```rust engine.run(None, None, None, false)?; ``` With the default 128/50 windows and USD 2,000,000 bars, a single-day sample stays in warmup for the whole run. The run will show the engine aggregating dollar bars and driving the strategy at trade and quote granularity, but Hurst and VPIN will not emit until the windows are full. To see signal updates and entry/exit logic fire on a single day, shrink the bar size or windows as noted above, or provide two or more sessions of data. A 14-day run (2024-01-15 to 2024-01-28) on this configuration prints 1,224 bars and fires only 2 entries, 1 partial cover, and 1 close. Entries are sparse by design: both `hurst_enter` and `vpin_threshold` must clear on the same quote. The residual size after the partial IOC cover stays open through the rest of the session, which is how a Netting OMS resolves an exit IOC that only fills part of the resting position. The wiring above is shipped as a runnable binary: ```bash cargo run -p nautilus-kraken --features examples \ --example kraken-hurst-vpin-backtest --release ``` By default it reads `PF_XBTUSD_trades.csv.gz` and `PF_XBTUSD_quotes.csv.gz` from `/tmp/tardis_kraken/`. Override with `KRAKEN_TRADES` and `KRAKEN_QUOTES` environment variables. ![Trade detail during the active window](./assets/hurst_vpin_kraken/panel_a_price_regime.png) **Figure 4.** *Close price for 2024-01-16 14:09-16:15 UTC. Teal bands mark `Hurst >= 0.55` bars; gold bands mark periods with an open position. Markers sit at actual fill price; dotted connector shows slip against the bar-close line.* ![Decision space across every bar](./assets/hurst_vpin_kraken/panel_c_decision_scatter.png) **Figure 5.** *Per-bar Hurst vs. VPIN across the backtest, colored by signed VPIN. Shaded quadrant marks the entry-eligible region.* ### Regenerate the panels The backtest strategy logs `Hurst=… VPIN=… signed=… bar_close=…` on every bar close and standard `OrderFilled` events on entries and exits, so the panels above are fully reproducible from the run's stdout. After building NautilusTrader from source, run these commands from the repository root: ```bash make sync RUST_LOG=info cargo run -p nautilus-kraken --features examples \ --example kraken-hurst-vpin-backtest --release > /tmp/backtest.log 2>&1 BACKTEST_LOG=/tmp/backtest.log \ uv run --project python --no-sync \ python docs/tutorials/assets/hurst_vpin_kraken/render_panels.py ``` The renderer uses the shared `nautilus_dark` tearsheet theme and writes static PNGs via Plotly's Kaleido exporter. ## Next steps - **Tune the sampling frame**. Try larger or smaller dollar-bar thresholds. The `VALUE_IMBALANCE` and `VALUE_RUNS` aggregators produce bars that close on information arrival itself, which may be an interesting substitute for constant-dollar sampling. - **Tighten the thresholds**. `hurst_enter`, `hurst_exit`, and `vpin_threshold` all interact: a higher enter threshold makes signals rarer but more specific; a tighter exit shortens average holding time. - **Add a volatility gate**. Overlay a realized-volatility estimator on the same bars to suppress entries during clearly chaotic sessions. - **Go live on Kraken Futures demo**. Once the backtest behaves, drive the same strategy through the Kraken live client factories against [demo-futures.kraken.com](https://demo-futures.kraken.com). A runnable live wiring ships as: ```bash cargo run -p nautilus-kraken --features examples \ --example kraken-hurst-vpin-live ``` Set `KRAKEN_FUTURES_API_KEY` and `KRAKEN_FUTURES_API_SECRET` in the environment before running. ## Further reading - [`HurstVpinDirectional` strategy source](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/trading/src/examples/strategies/hurst_vpin_directional) - [Data concepts: bar types and aggregation](../concepts/data/) - [Tardis integration guide](../integrations/tardis.md) - [Kraken integration guide](../integrations/kraken.md) - [Kraken Futures documentation](https://docs.kraken.com/api/docs/futures-api) - Lopez de Prado, M. (2018). *Advances in Financial Machine Learning*, Wiley. Chapter 2 (information-driven bars) and Chapter 19 (VPIN). # Tutorials Source: https://nautilustrader.io/docs/latest/tutorials/ Step-by-step walkthroughs demonstrating specific features and workflows. :::info Most Python tutorials are Jupytext percent-format files in the docs [tutorials directory](https://github.com/nautechsystems/nautilus_trader/tree/master/docs/tutorials). You can run those directly as scripts or open them as notebooks with Jupytext. Rust tutorials use the commands shown on their pages. ::: :::note - **Latest**: docs built from the `master` branch for stable releases. See . - **Nightly**: docs built from the `nightly` branch for experimental features. See . ::: ## Recommended order New to NautilusTrader? Work through these in sequence: 1. [Quickstart](../getting_started/quickstart) - run your first backtest in five minutes with synthetic data 2. [Backtest (low-level API)](../getting_started/backtest_low_level) - direct `BacktestEngine` usage with real market data and execution algorithms 3. [Backtest (high-level API)](../getting_started/backtest_high_level) - config-driven backtesting with `BacktestNode` and the Parquet data catalog 4. [Loading external data][loading_external_data] - load CSV or other external data into the `ParquetDataCatalog` (how-to guide) 5. [Backtest with FX bar data][backtest_fx_bars] - FX bar backtesting with rollover interest simulation 6. Pick a topic-specific tutorial below ## Backtesting | Tutorial | Description | Data | | :-------------------------------------------------------------------------- | :--------------------------------------------- | :------------ | | [Backtest with FX Bar Data][backtest_fx_bars] | EMA cross on FX bars with rollover simulation. | Bundled | | [Backtest with Order Book Depth Data (Binance)][backtest_orderbook_binance] | Order book imbalance strategy on depth data. | User-provided | | [Backtest with Order Book Depth Data (Bybit)][backtest_orderbook_bybit] | Order book imbalance strategy on depth data. | User-provided | ## Data workflows For task-oriented data recipes, see the [how-to guides](../how_to/): | Guide | Description | Data | | :---------------------------------------------------- | :------------------------------------------------ | :---------------- | | [Loading external data][loading_external_data] | Load external data into the `ParquetDataCatalog`. | User-provided | | [Data catalog with Databento][data_catalog_databento] | Set up a catalog with Databento schemas. | Databento API key | ## Strategy patterns | Tutorial | Description | Data | | :---------------------------------------------------------------------------------- | :-------------------------------------------- | :---------------- | | [Mean Reversion with Proxy FX Data (AX Exchange)](fx_mean_reversion_ax) | Bollinger Band mean reversion on EURUSD-PERP. | TrueFX proxy | | [Gold Perpetual Book Imbalance (AX Exchange)](gold_book_imbalance_ax) | Order book imbalance on XAU-PERP. | Databento API key | | [On-Chain Grid Market Making with Short-Term Orders (dYdX)](grid_market_maker_dydx) | Grid MM on dYdX v4 perpetuals. | User-provided | ## Options | Tutorial | Description | Data | | :---------------------------------------------------------------------- | :--------------------------------------------- | :------- | | [Options Data and Greeks (Bybit)](options_data_bybit) | Stream Greeks and option chain snapshots. | Live API | | [Delta-Neutral Options Strategy (Bybit)](delta_neutral_options_bybit) | Short strangle with perpetual delta hedging. | Live API | | [Delta-Neutral Options Strategy (Derive)](delta_neutral_options_derive) | Derive ETH strangle hedger with premium entry. | Live API | ## Rust | Tutorial | Description | Data | | :------------------------------------------------------------------------------------------ | :--------------------------------------------------- | :------------------ | | [Book Imbalance Backtest (Betfair)](backtest_book_imbalance_betfair) | Book imbalance actor on Betfair L2 data. | User-provided | | [Composite Market Making on Lighter RWA with Databento EQUS NVDA](lighter_rwa_composite_mm) | Signal-skewed MM on NVDA-PERP. | Databento + Lighter | | [Hurst/VPIN Directional Strategy (Kraken Futures)](hurst_vpin_kraken) | Regime-filtered informed-flow strategy on PF_XBTUSD. | Tardis.dev | [backtest_fx_bars]: https://github.com/nautechsystems/nautilus_trader/blob/master/docs/tutorials/backtest_fx_bars.py [backtest_orderbook_binance]: https://github.com/nautechsystems/nautilus_trader/blob/master/docs/tutorials/backtest_orderbook_binance.py [backtest_orderbook_bybit]: https://github.com/nautechsystems/nautilus_trader/blob/master/docs/tutorials/backtest_orderbook_bybit.py [loading_external_data]: https://github.com/nautechsystems/nautilus_trader/blob/master/docs/how_to/loading_external_data.py [data_catalog_databento]: https://github.com/nautechsystems/nautilus_trader/blob/master/docs/how_to/data_catalog_databento.py # Composite Market Making on Lighter RWA with Databento US Equities NVDA Source: https://nautilustrader.io/docs/latest/tutorials/lighter_rwa_composite_mm/ This tutorial runs the shipped [`CompositeMarketMaker`][composite-market-maker] strategy on Lighter's `NVDA-PERP.LIGHTER` RWA market using Databento `NVDA.EQUS` quotes as an external signal. The strategy quotes one post-only bid and one post-only ask around the Lighter mid, then shifts both sides from a normalized Databento residual and the current Lighter inventory. The setup uses a Rust [`LiveNode`][live-node], while the strategy itself runs as the native Rust `CompositeMarketMaker` strategy. If you are new to the Lighter adapter, start with [Get started with Lighter][lighter-get-started] first. That guide isolates the Rust and Python data-client paths before this tutorial adds Databento signal data and live order flow. ## Introduction Lighter lists real-world asset (RWA) perpetuals that trade continuously, including single-name equity markets. See Lighter's [RWA docs] and [market specifications] for current venue details. Databento's [US Equities][Databento US Equities] datasets provide US equity top-of-book data for `NVDA`, with `mbp-1` available through the Nautilus Databento adapter. `CompositeMarketMaker` is a small two-input market maker: - The **target instrument** is the Lighter market to quote: `NVDA-PERP.LIGHTER`. - The **signal instrument** is the Databento reference feed: `NVDA.EQUS`. - The **anchor** is the Lighter mid. - The **signal residual** is `(databento_mid / baseline) - 1.0`. - The **quote shift** is `signal_skew_factor * residual - inventory_skew_factor * net_position`. With no configured baseline, the strategy captures the first observed `NVDA.EQUS` mid as the reference price. The residual starts at zero and measures NVDA's move from that first signal mid, not the Lighter/Databento basis. Set the `SIGNAL_BASELINE` constant in the example source to pin the reference price for deterministic runs. In this setup, the Lighter BBO remains the spread anchor. Databento moves the quote center up or down through the normalized residual. ```mermaid flowchart LR subgraph Databento ["Databento data client"] DQ["NVDA.EQUS QuoteTick
dataset = EQUS.MINI
schema = mbp-1"] DS["signal_mid = (bid + ask) / 2"] DR["residual = signal_mid / baseline - 1"] end subgraph Lighter ["Lighter data + execution clients"] LQ["NVDA-PERP.LIGHTER QuoteTick"] LM["anchor = (bid + ask) / 2"] EX["Post-only limit orders"] end subgraph Strategy ["CompositeMarketMaker"] TH{{"no target orders OR anchor/signal impact
>= requote_threshold_bps"}} CA["cancel_all_orders()"] SK["shift = signal_skew - inventory_skew"] QU["bid = anchor - half_spread + shift
ask = anchor + half_spread + shift"] PO["submit post-only bid/ask"] end DQ --> DS --> DR --> SK LQ --> LM --> TH TH -->|yes| CA --> SK --> QU --> PO --> EX TH -->|no| LQ ``` The focus is the adapter wiring: one engine consumes a direct US equity feed and a crypto-native RWA venue, while order lifecycle, inventory, and quote state stay inside the same event-driven runtime. ## Prerequisites - A Rust toolchain (MSRV 1.98.1 or newer). - A Cargo project with the Nautilus, Lighter, and Databento crates as dependencies (see [Project setup](#project-setup)). - Python 3.12+ to regenerate the rendered panels. - A Databento API key with live access to Databento US Equities Mini (`EQUS.MINI`), the default dataset for the bundled `NVDA.EQUS` route. Higher tiers such as `EQUS.PLUS` need a separate Databento license; select one with `venue_dataset_map` when your account is entitled. - Lighter API credentials (numeric account index, API key index, and API secret) for the configured environment (testnet by default), required only to connect and submit orders. - The Lighter integration guide: [Lighter](../integrations/lighter.md). - The Databento integration guide: [Databento](../integrations/databento.md). The example reads credentials from environment variables and keeps the strategy parameters as editable Rust constants. It defaults to `LighterEnvironment::Testnet`, so follow the [account and API key setup](../integrations/lighter.md#account-and-api-key-setup) and set the testnet Lighter credentials: ```bash export DATABENTO_API_KEY="your-databento-api-key" export LIGHTER_TESTNET_ACCOUNT_INDEX="123456" export LIGHTER_TESTNET_API_KEY_INDEX="4" export LIGHTER_TESTNET_API_SECRET="your-lighter-api-secret" ``` For mainnet, change `LIGHTER_ENVIRONMENT` in the source to `LighterEnvironment::Mainnet` and use the mainnet `LIGHTER_*` credential variables described in the integration guide. Set `DATABENTO_API_KEY` before running the example. ## Project setup The strategy, node, and adapters ship as crates, so you can depend on them from your own Cargo project rather than working inside a NautilusTrader checkout. Add the following to your `Cargo.toml`, pointing every Nautilus dependency at the same `develop` git source so the crates resolve to one consistent version: ```toml [dependencies] nautilus-common = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "master", features = ["live"] } nautilus-core = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "master" } nautilus-databento = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "master", features = ["high-precision", "live"] } nautilus-lighter = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "master", features = ["examples", "high-precision"] } nautilus-live = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "master", features = ["node"] } nautilus-model = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "master", features = ["high-precision"] } nautilus-trading = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "master", features = ["examples", "high-precision"] } tokio = { version = "1", features = ["full"] } ``` The `examples` feature on `nautilus-trading` exposes the `CompositeMarketMaker` strategy, and `high-precision` is required for Lighter's crypto-native pricing. For the general crate layout, feature flags, and the crates.io alternative to the git source, see the Rust [project setup guide][project-setup]. The Databento client also needs a publishers file that maps venues to datasets. Download [`publishers.json`][databento-publishers] from the Databento adapter crate and point `publishers_filepath` at your local copy. The shipped example resolves the same file relative to the checkout, so this step only applies to your own project. ## Why NVDA `NVDA` is a liquid Nasdaq-listed single-name equity, and Lighter maps its RWA perpetual to `NVDA-PERP.LIGHTER`. This pairs a licensed Databento signal with a Lighter traded market: | Role | Instrument ID | Source | Notes | | ----------------- | ------------------- | --------- | ------------------------------------- | | Signal instrument | `NVDA.EQUS` | Databento | EQUS.MINI top-of-book quote updates. | | Target instrument | `NVDA-PERP.LIGHTER` | Lighter | RWA perpetual traded through Lighter. | Subscribing to `NVDA.EQUS` requests top-of-book (`mbp-1`) quotes for `NVDA` from Databento's `EQUS.MINI` dataset by default, delivered as a single `QuoteTick` stream. `EQUS.MINI` is the lowest-cost consolidated US equities tier; richer tiers such as `EQUS.PLUS` need a separate Databento license and can be selected with the client's `venue_dataset_map` (for example `{"EQUS": "EQUS.PLUS"}`) once your account is entitled. The adapter resolves the `EQUS` venue from a publishers file: the example points `DatabentoDataClientConfig` at the `publishers.json` bundled with the Databento adapter. See [Instrument IDs and symbology][databento-symbology] for the mapping rules. The older Databento Equities Basic (`DBEQ.BASIC`) dataset name appears in some grandfathered accounts and historical examples. New Databento subscriptions use the Databento US Equities product line, so this tutorial uses the consolidated `EQUS` venue. Treat the top-of-book feed as a licensed signal proxy for the tutorial wiring, not as a full depth Nasdaq TotalView book. The example starts at `trade_size=0.05`, which aligns with the Lighter NVDA minimum base amount observed during tutorial validation. Check the [market details endpoint] before increasing size or changing instruments. ## Session constraint Lighter RWA markets trade continuously. `NVDA.EQUS` follows the US equity market data session. The first live test should run during the regular cash session (13:30-20:00 UTC, US daylight time), with special handling for holidays and half-days. `CompositeMarketMaker` does not include a built-in session gate or signal-age guard. For production use, add an actor or strategy variant that cancels quotes when the Databento signal goes stale. The tutorial example keeps this explicit instead of hiding it in a custom strategy. ## Example node There are two ways to run this: from a NautilusTrader checkout via the shipped [Lighter NVDA composite market maker example][example-script] binary, or by copying the node wiring below into a `main` in your own project that depends on the crates from [Project setup](#project-setup). A Python counterpart also lives at [`examples/live/lighter/nvda_composite_mm.py`][python-example-script]; it uses the same Rust strategy through PyO3. From a checkout, with the credential variables set, the shipped binary connects the data and execution clients. It defaults to `DRY_RUN = false`, which adds the order-submitting strategy. :::warning This command can submit live orders. Start with the smallest accepted size on a funded test account or a mainnet account sized for loss. Confirm the active instrument ID, account ID, numeric account index, and Lighter credentials before running it. ::: ```bash cargo run --bin lighter-nvda-composite-mm --package nautilus-tutorials --features examples ``` Databento is a multi-venue data client without a fixed venue route, so the engine uses it as the default route for `NVDA.EQUS`. Lighter registers with the `LIGHTER` venue route and receives `NVDA-PERP.LIGHTER` subscriptions. The core of the setup is the three-client node plus `CompositeMarketMaker`: ```rust let lighter_environment = LIGHTER_ENVIRONMENT; let trader_id = TraderId::from(TRADER_ID); let account_id = AccountId::from(ACCOUNT_ID); let instrument_id = InstrumentId::from(INSTRUMENT_ID); let signal_instrument_id = InstrumentId::from(SIGNAL_INSTRUMENT_ID); let api_key = get_env_var("DATABENTO_API_KEY")?; let databento_config = DatabentoDataClientConfig::new(api_key, publishers_filepath, true, true); let lighter_data_config = LighterDataClientConfig::builder() .environment(lighter_environment) .build(); let lighter_exec_config = LighterExecutionClientConfig::builder() .account_id(account_id) .environment(lighter_environment) .build(); let mut strategy_config = CompositeMarketMakerConfig::builder() .instrument_id(instrument_id) .signal_instrument_id(signal_instrument_id) .max_position(max_position) .trade_size(trade_size) .half_spread_bps(HALF_SPREAD_BPS) .inventory_skew_factor(INVENTORY_SKEW_FACTOR) .signal_skew_factor(SIGNAL_SKEW_FACTOR) .requote_threshold_bps(REQUOTE_THRESHOLD_BPS) .on_cancel_resubmit(ON_CANCEL_RESUBMIT) .build(); strategy_config.base.strategy_id = Some(StrategyId::from("NVDA_COMPOSITE_MM-001")); strategy_config.base.order_id_tag = Some("001".to_string()); let mut node = LiveNode::builder(trader_id, Environment::Live)? .with_name("LIGHTER-NVDA-COMPOSITE-MM-001".to_string()) .with_reconciliation(!DRY_RUN) .add_data_client( None, Box::new(DatabentoDataClientFactory::new()), Box::new(databento_config), )? .add_data_client( None, Box::new(LighterDataClientFactory::new()), Box::new(lighter_data_config), )? .add_exec_client( None, Box::new(LighterExecutionClientFactory::new()), Box::new(lighter_exec_config), )? .build()?; if !DRY_RUN { node.add_strategy(CompositeMarketMaker::new(strategy_config))?; } ``` To connect without adding the order-submitting strategy, edit the constant near the top of the example source: ```rust const DRY_RUN: bool = true; ``` Then run the same command: ```bash cargo run --bin lighter-nvda-composite-mm --package nautilus-tutorials --features examples ``` For a testnet smoke run, keep `LIGHTER_ENVIRONMENT` as `LighterEnvironment::Testnet` and use the `LIGHTER_TESTNET_*` credential variables. If the run is outside the Databento US Equities cash session, it can still validate node startup, routing, Lighter data, and the order lifecycle. The Databento residual remains zero until the first `NVDA.EQUS` quote arrives. ## Strategy parameters | Parameter | Value | Description | | ----------------------- | ------------------- | -------------------------------------------------------------- | | `instrument_id` | `NVDA-PERP.LIGHTER` | Lighter RWA perpetual to quote. | | `signal_instrument_id` | `NVDA.EQUS` | Databento US Equities Mini signal feed. | | `trade_size` | `0.05` | Size per bid or ask. | | `max_position` | `0.20` | Hard cap on net Lighter exposure. | | `half_spread_bps` | `25` | Half-spread around the Lighter anchor. | | `inventory_skew_factor` | `2.0` | Price units per unit of net position. | | `signal_skew_factor` | `55.0` | Price units per unit of normalized Databento residual. | | `signal_baseline` | First signal mid | Optional reference price for the Databento residual. | | `requote_threshold_bps` | `5` | Anchor or signal-impact move that triggers cancel and requote. | With a Lighter mid of `207.00` and `half_spread_bps=25`, the unskewed half spread is `0.5175` USD. If Databento is 30 bps above its baseline, a `signal_skew_factor` of `55.0` shifts both sides up by `0.165` USD before inventory skew. A long position of `0.05` with `inventory_skew_factor=2.0` shifts both sides down by `0.10` USD. ## Requote behavior Signal ticks update internal state but do not submit orders by themselves. Until the first Databento quote arrives, the residual is zero. The next Lighter quote tick reads the latest signal residual and checks the quote state. A quote cycle occurs when: - no target orders are open or in-flight; - the Lighter anchor moves by at least `requote_threshold_bps`; or - the price impact of the signal residual change clears the same threshold. The strategy then cancels open orders, reads current net position and pending exposure from the cache, computes one bid and one ask, drops any side that breaches `max_position`, and submits the remaining sides as post-only limits. ## Panels The panels below use deterministic replay data. They show the quoting mechanics and the cash-session constraint. They are not a captured live Lighter fill trace. ![NVDA composite quote center against Databento and Lighter mids](./assets/lighter_rwa_composite_mm/panel_a_reference_overlay.png) **Figure 1.** *Databento `NVDA.EQUS` mid, Lighter `NVDA-PERP.LIGHTER` mid, composite bid, composite ask, and quote center.* ![Databento residual, Lighter basis, and quote-center shift](./assets/lighter_rwa_composite_mm/panel_b_signal_basis.png) **Figure 2.** *Databento residual, Lighter basis, and quote-center shift in bps.* ![Inventory skew terms for the composite market maker](./assets/lighter_rwa_composite_mm/panel_c_inventory_skew.png) **Figure 3.** *Net position, signal shift, inventory adjustment, and total shift for a `0.05` NVDA trade size and `0.20` NVDA position cap.* ![Lighter continuous trading and Databento session clock](./assets/lighter_rwa_composite_mm/panel_d_session_clock.png) **Figure 4.** *Lighter's continuous RWA market clock against the Databento US Equities cash-session signal, with signal age after the regular session.* ## Regenerate the panels After building NautilusTrader from source, run these commands from the repository root: ```bash make sync uv run --project python --no-sync \ python docs/tutorials/assets/lighter_rwa_composite_mm/render_panels.py ``` The renderer writes four PNGs into `docs/tutorials/assets/lighter_rwa_composite_mm/`. It uses the `nautilus_dark` Plotly theme and deterministic replay data so docs builds do not depend on vendor data licenses or live exchange access. ## Extensions The next useful improvement is a signal-age gate. For example, cancel all Lighter orders when the latest `NVDA.EQUS` quote is older than 30 seconds during the cash session, or immediately after the cash session closes. That makes the Databento signal an explicit operating dependency instead of an implicit one. For a pure fair-value strategy, use this tutorial as the client wiring and write a small variant that anchors bid/ask directly on the Databento mid, then checks the Lighter BBO only for post-only and basis limits. [composite-market-maker]: https://github.com/nautechsystems/nautilus_trader/blob/master/crates/trading/src/examples/strategies/composite_market_maker/strategy.rs [live-node]: ../how_to/run_rust_live_trading.md [project-setup]: ../concepts/rust.md#project-setup [lighter-get-started]: ../how_to/get_started_lighter.md [databento-symbology]: ../integrations/databento.md#instrument-ids-and-symbology [databento-publishers]: https://github.com/nautechsystems/nautilus_trader/blob/master/crates/adapters/databento/publishers.json [RWA docs]: https://docs.lighter.xyz/trading/real-world-assets-rwas [market specifications]: https://docs.lighter.xyz/trading/real-world-assets-rwas/market-specifications [market details endpoint]: https://mainnet.zklighter.elliot.ai/api/v1/orderBookDetails [Databento US Equities]: https://databento.com/blog/introducing-databento-us-equities [example-script]: https://github.com/nautechsystems/nautilus_trader/blob/master/examples/tutorials/src/bin/lighter_nvda_composite_mm.rs [python-example-script]: https://github.com/nautechsystems/nautilus_trader/blob/master/examples/live/lighter/nvda_composite_mm.py # Options Data and Greeks (Bybit) Source: https://nautilustrader.io/docs/latest/tutorials/options_data_bybit/ :::note This is a **Rust-only** system tutorial. It uses the Rust `LiveNode` with the Bybit adapter to stream live option Greeks and aggregated chain snapshots. ::: This tutorial connects to Bybit's live options market and consumes Greeks and option chain data through two `DataActor` examples. It covers instrument discovery, venue-provided Greeks subscriptions, and periodic chain snapshots with ATM-relative strike filtering. ## Introduction Bybit publishes Greeks (delta, gamma, vega, theta) and implied volatility alongside every option ticker update. NautilusTrader exposes this data at two levels: - **Per-instrument Greeks**: subscribe to a single option contract and receive an `OptionGreeks` event on every ticker update. - **Option chain snapshots**: subscribe to an entire expiry series and receive periodic `OptionChainSlice` events that aggregate quotes and Greeks across all active strikes. Two example binaries back these patterns: the first subscribes to individual Greeks streams, the second subscribes to an aggregated chain with ATM-relative strike filtering. ```mermaid flowchart LR subgraph BybitAPI ["Bybit V5 public WebSocket"] TKR["Per-contract option ticker"] end subgraph Adapter ["nautilus-bybit data client"] Q["QuoteTick + OptionGreeks per contract"] AGG["Per-series aggregator
(ATM and strike filtering)"] end subgraph Actors ["DataActor implementations"] G["GreeksTester
on_option_greeks()"] C["OptionChainTester
on_option_chain()"] end TKR --> Q Q --> G Q --> AGG AGG -->|interval timer| C ``` ## Prerequisites - A working Rust toolchain ([rustup.rs](https://rustup.rs)). - The NautilusTrader repository cloned and building. - A Bybit API key with read permissions. No trading permissions are needed for data-only use. Create keys at [bybit.com](https://www.bybit.com/app/user/api-management). - Environment variables set for authentication: ```bash export BYBIT_API_KEY="your-api-key" export BYBIT_API_SECRET="your-api-secret" ``` A `.env` file in the repository root also works. The examples load it via `dotenvy`. :::warning Bybit demo trading uses `stream-demo.bybit.com` only for private streams. Public option market data uses the mainnet public stream `wss://stream.bybit.com/v5/public/option`. ::: ## The DataActor pattern A Rust `DataActor` needs three pieces: 1. A struct with a `core: DataActorCore` field plus your own state. 2. The `nautilus_actor!(YourType)` macro plus a `Debug` implementation. 3. A `DataActor` trait implementation with your callbacks. The macro supplies the native runtime wiring required by the blanket `Actor` and `Component` implementations, so you only implement the callbacks you need. Every callback has a default no-op implementation. ## Part 1: per-instrument Greeks The `bybit-greeks-tester` example subscribes to `OptionGreeks` for all BTC CALL options at the nearest expiry and logs each update. ### Actor structure ```rust #[derive(Debug)] struct GreeksTester { core: DataActorCore, client_id: ClientId, subscribed_instruments: Vec, } nautilus_actor!(GreeksTester); impl GreeksTester { fn new(client_id: ClientId) -> Self { Self { core: DataActorCore::new(DataActorConfig { actor_id: Some("GREEKS_TESTER-001".into()), ..Default::default() }), client_id, subscribed_instruments: Vec::new(), } } } ``` The `core` field is required by the macro. The `client_id` identifies which data client to route subscriptions to. The `subscribed_instruments` vector tracks what we subscribed to so we clean up on stop. ### Discovering instruments On start, the actor queries the cache for all option instruments, filters for BTC CALLs that have not expired, and finds the nearest expiry: ```rust fn on_start(&mut self) -> anyhow::Result<()> { let venue = Venue::new("BYBIT"); let underlying_filter = Ustr::from("BTC"); let mut options: Vec<(InstrumentId, f64, u64)> = { let cache = self.cache(); let instruments = cache.instruments(&venue, Some(&underlying_filter)); instruments .iter() .filter_map(|inst| { if inst.option_kind() == Some(OptionKind::Call) { let expiry = inst.expiration_ns()?.as_u64(); let strike = inst.strike_price()?.as_f64(); Some((inst.id(), strike, expiry)) } else { None } }) .collect() }; // cache borrow dropped here let now_ns = self.timestamp_ns().as_u64(); options.retain(|(_, _, exp)| *exp > now_ns); let nearest_expiry = options.iter().map(|(_, _, exp)| *exp).min().unwrap(); options.retain(|(_, _, exp)| *exp == nearest_expiry); options.sort_by(|(_, a, _), (_, b, _)| a.partial_cmp(b).unwrap()); // ...subscribe to each } ``` :::warning Release the cache borrow before calling any subscription methods. The cache uses `Rc>` internally, and subscription methods may need to borrow it. Collect owned data into a local `Vec`, drop the cache reference, then subscribe. ::: ### Subscribing to Greeks After discovering instruments, subscribe to each one: ```rust let client_id = self.client_id; for (instrument_id, _, _) in &options { self.subscribe_option_greeks(*instrument_id, Some(client_id), None); self.subscribed_instruments.push(*instrument_id); } ``` ### Handling updates Each ticker update from Bybit triggers `on_option_greeks` with an `OptionGreeks` event: ```rust fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> { log::info!( "GREEKS | {} | delta={:.4} gamma={:.6} vega={:.4} theta={:.4} rho={:.6} | \ mark_iv={} bid_iv={} ask_iv={} | underlying={} oi={}", greeks.instrument_id, greeks.delta, greeks.gamma, greeks.vega, greeks.theta, greeks.rho, greeks.mark_iv.map_or("-".to_string(), |v| format!("{v:.2}")), greeks.bid_iv.map_or("-".to_string(), |v| format!("{v:.2}")), greeks.ask_iv.map_or("-".to_string(), |v| format!("{v:.2}")), greeks.underlying_price.map_or("-".to_string(), |v| format!("{v:.2}")), greeks.open_interest.map_or("-".to_string(), |v| format!("{v:.1}")), ); Ok(()) } ``` The `OptionGreeks` fields: | Field | Type | Description | | ------------------ | -------------- | ----------------------------------------------- | | `instrument_id` | `InstrumentId` | The option contract. | | `delta` | `f64` | Price sensitivity to underlying. | | `gamma` | `f64` | Delta sensitivity to underlying. | | `vega` | `f64` | Price sensitivity to a 1% change in volatility. | | `theta` | `f64` | Daily time decay. | | `rho` | `f64` | Sensitivity to interest rate changes. | | `mark_iv` | `Option` | Mark price implied volatility. | | `bid_iv` | `Option` | Bid implied volatility. | | `ask_iv` | `Option` | Ask implied volatility. | | `underlying_price` | `Option` | Venue reference price for this expiry. | | `open_interest` | `Option` | Open interest for this contract. | The `delta`, `gamma`, `vega`, `theta`, and `rho` values live on a nested `greeks: OptionGreekValues` struct. `OptionGreeks` implements `Deref`, so `greeks.delta` and friends work as shown above. Bybit does not provide rho; the adapter sets it to `0.0`. ### Cleanup On stop, unsubscribe from all instruments: ```rust fn on_stop(&mut self) -> anyhow::Result<()> { let ids: Vec = self.subscribed_instruments.drain(..).collect(); let client_id = self.client_id; for instrument_id in ids { self.unsubscribe_option_greeks(instrument_id, Some(client_id), None); } log::info!("Unsubscribed from all option greeks"); Ok(()) } ``` ## Part 2: option chain snapshots The `bybit-option-chain` example subscribes to an aggregated option chain and logs periodic snapshots showing calls and puts at each strike with their quotes and Greeks. ### Why use option chains Per-instrument subscriptions give granular control, but monitoring an entire surface means managing individual streams and correlating updates across strikes. An option chain subscription handles this: the `DataEngine` aggregates quotes and Greeks across all strikes in a series and publishes a single `OptionChainSlice` on a timer. This aggregation happens inside NautilusTrader. Bybit publishes per-contract option market data and does not expose a native option chain stream in the V5 public WebSocket docs. ### Chain types **`OptionSeriesId`** identifies a single expiry series: ```rust let series_id = OptionSeriesId::new( Venue::new("BYBIT"), // venue Ustr::from("BTC"), // underlying Ustr::from("USDT"), // settlement currency UnixNanos::from(expiry), // expiration timestamp ); ``` **`StrikeRange`** controls which strikes are active: | Variant | Description | | ------------- | ---------------------------------------------------- | | `Fixed` | A fixed set of strike prices. | | `AtmRelative` | `strikes_above` above and `strikes_below` below ATM. | | `AtmPercent` | All strikes within `pct` of the ATM price. | For dynamic strike ranges, subscriptions are deferred until the ATM price is determined from the venue-provided reference price. ### Subscribing ```rust let strike_range = StrikeRange::AtmRelative { strikes_above: 3, strikes_below: 3, }; let snapshot_interval_ms = Some(5_000); // snapshot every 5 seconds self.subscribe_option_chain( series_id, strike_range, snapshot_interval_ms, Some(client_id), None, // params ); ``` Pass `None` for `snapshot_interval_ms` to use raw mode, where every quote or Greeks update publishes a slice immediately. ### Handling snapshots The `on_option_chain` callback receives an `OptionChainSlice` containing all active strikes with their call and put data: ```rust fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> { log::info!( "OPTION_CHAIN | {} | atm={} | calls={} puts={} | strikes={}", slice.series_id, slice.atm_strike.map_or("-".to_string(), |p| format!("{p}")), slice.call_count(), slice.put_count(), slice.strike_count(), ); for strike in slice.strikes() { let call_info = slice.get_call(&strike).map(|d| { let greeks_str = d.greeks.as_ref().map_or("-".to_string(), |g| { format!( "d={:.3} g={:.5} v={:.2} iv={:.1}%", g.delta, g.gamma, g.vega, g.mark_iv.unwrap_or(0.0) * 100.0, ) }); format!("bid={} ask={} [{}]", d.quote.bid_price, d.quote.ask_price, greeks_str) }); let put_info = slice.get_put(&strike).map(|d| { let greeks_str = d.greeks.as_ref().map_or("-".to_string(), |g| { format!( "d={:.3} g={:.5} v={:.2} iv={:.1}%", g.delta, g.gamma, g.vega, g.mark_iv.unwrap_or(0.0) * 100.0, ) }); format!("bid={} ask={} [{}]", d.quote.bid_price, d.quote.ask_price, greeks_str) }); log::info!( " K={} | CALL: {} | PUT: {}", strike, call_info.unwrap_or_else(|| "-".to_string()), put_info.unwrap_or_else(|| "-".to_string()), ); } Ok(()) } ``` The `OptionChainSlice` fields and methods: | Name | Type / Returns | Description | | ---------------- | --------------------------- | ------------------------------------ | | `series_id` | `OptionSeriesId` | The series this snapshot covers. | | `atm_strike` | `Option` | ATM strike from the reference price. | | `call_count()` | `usize` | Number of call strikes with data. | | `put_count()` | `usize` | Number of put strikes with data. | | `strike_count()` | `usize` | Union of all strikes. | | `strikes()` | `Vec` | Sorted list of all strike prices. | | `get_call(k)` | `Option<&OptionStrikeData>` | Call quote and Greeks at strike `k`. | | `get_put(k)` | `Option<&OptionStrikeData>` | Put quote and Greeks at strike `k`. | Each `OptionStrikeData` contains a `quote: QuoteTick` (bid/ask) and an optional `greeks: Option`. ## Node setup Both examples use the same `LiveNode` pattern. No execution client is needed for data-only use: ```rust #[tokio::main] async fn main() -> Result<(), Box> { dotenvy::dotenv().ok(); let environment = Environment::Live; let trader_id = TraderId::test_default(); let client_id = ClientId::new("BYBIT"); let bybit_config = BybitDataClientConfig { api_key: None, // loaded from BYBIT_API_KEY env var api_secret: None, // loaded from BYBIT_API_SECRET env var product_types: vec![BybitProductType::Option], ..Default::default() }; let client_factory = BybitDataClientFactory::new(); let mut node = LiveNode::builder(trader_id, environment)? .with_name("BYBIT-OPTIONS-001".to_string()) .add_data_client(None, Box::new(client_factory), Box::new(bybit_config))? .with_delay_post_stop_secs(5) .build()?; let actor = GreeksTester::new(client_id); // or OptionChainTester node.add_actor(actor)?; node.run().await?; Ok(()) } ``` Setting `product_types` to `[BybitProductType::Option]` loads only option instruments. Startup blocks while the instrument provider fetches and parses every listed option. ## Running the examples ```bash # Per-instrument Greeks cargo run --example bybit-greeks-tester --package nautilus-bybit --features examples # Option chain snapshots cargo run --example bybit-option-chain --package nautilus-bybit --features examples ``` Stop either example with Ctrl+C. The actor's `on_stop` callback unsubscribes from all streams before shutdown. ## What the examples produce A 30-second mainnet run on April 28 (BTC near 76,800 USDT, expiry 2026-04-28 08:00 UTC) captures **938 Greeks updates** across 22 BTC CALL contracts in the per-instrument tester, plus **5 chain snapshots** covering 7 strikes each in the chain tester. ### Per-instrument Greeks output ``` Found 22 BTC CALL options at nearest expiry (ts=1777359600000000000) Subscribed to option greeks for 22 instruments GREEKS | BTC-28APR26-72000-C-USDT-OPTION.BYBIT | delta=0.4733 gamma=0.000000 vega=0.0000 theta=-0.0000 rho=0.000000 | mark_iv=0.66 bid_iv=0.00 ask_iv=5.00 | underlying=76782.43 oi=0.0 GREEKS | BTC-28APR26-71000-C-USDT-OPTION.BYBIT | delta=0.4733 gamma=0.000000 vega=0.0000 theta=-0.0000 rho=0.000000 | mark_iv=0.74 bid_iv=0.00 ask_iv=5.00 | underlying=76782.43 oi=0.1 GREEKS | BTC-28APR26-73000-C-USDT-OPTION.BYBIT | delta=0.4733 gamma=0.000000 vega=0.0000 theta=-0.0000 rho=0.000000 | mark_iv=0.57 bid_iv=0.00 ask_iv=5.00 | underlying=76782.43 oi=0.0 ``` ### Option chain output ``` OPTION_CHAIN | BYBIT:BTC:USDT:2026-04-28T08:00:00Z | atm=77000 | calls=7 puts=7 | strikes=7 K=75500 | CALL: bid=1210 ask=1430 [d=0.445 g=0.00000 v=0.00 iv=36.2%] | PUT: bid=0 ask=5 [d=0.000 g=0.00000 v=0.00 iv=36.2%] K=76000 | CALL: bid=700 ask=850 [d=0.445 g=0.00000 v=0.00 iv=32.5%] | PUT: bid=0 ask=5 [d=0.000 g=0.00000 v=0.00 iv=32.5%] K=76500 | CALL: bid=265 ask=370 [d=0.442 g=0.00000 v=0.07 iv=29.9%] | PUT: bid=0 ask=5 [d=-0.003 g=0.00000 v=0.07 iv=29.9%] ``` ### Panels ![BTC CALL delta vs strike](./assets/options_data_bybit/panel_a_delta_vs_strike.png) **Figure 1.** *Last delta per BTC CALL strike at the nearest expiry, underlying ~77,000 USDT marked. Delta drops from ~0.45 below the underlying to near zero past the underlying. Bybit's delta on near-zero gamma contracts close to expiry compresses to a step-like profile around ATM.* ![IV smile per strike](./assets/options_data_bybit/panel_b_iv_smile.png) **Figure 2.** *Mark IV per strike for the latest chain snapshot (CALL and PUT overlaid). The smile is symmetric around ATM at 77,000 USDT, with IV dipping from 36% at 75,500 to 30% at 77,000 and rising back to 38% at 78,500.* ![Underlying trajectory and open interest](./assets/options_data_bybit/panel_c_underlying_oi.png) **Figure 3.** *Bybit's underlying price reported in each Greeks update (top) and open interest by strike at the last update (bottom). OI concentrates in the 70,000-76,000 USDT band: at-the-money to slightly out-of-the-money strikes.* ![CALL spread per chain snapshot](./assets/options_data_bybit/panel_d_call_spread.png) **Figure 4.** *Average CALL bid-ask spread per chain snapshot in USDT. Snapshots arrive every five seconds (`snapshot_interval_ms=5000`).* ### Regenerate the panels After building NautilusTrader from source, run these commands from the repository root: ```bash make sync timeout 30 ./target/release/examples/bybit-greeks-tester > /tmp/bybit_greeks.log 2>&1 timeout 30 ./target/release/examples/bybit-option-chain > /tmp/bybit_chain.log 2>&1 GREEKS_LOG=/tmp/bybit_greeks.log CHAIN_LOG=/tmp/bybit_chain.log \ uv run --project python --no-sync \ python docs/tutorials/assets/options_data_bybit/render_panels.py ``` ## Complete source - [`crates/adapters/bybit/examples/node_greeks.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/bybit/examples/node_greeks.rs) - [`crates/adapters/bybit/examples/node_option_chain.rs`](https://github.com/nautechsystems/nautilus_trader/tree/master/crates/adapters/bybit/examples/node_option_chain.rs) ## Next steps - **Combine both patterns**. Use per-instrument Greeks for near-ATM contracts alongside the aggregated chain view in a single actor. Subscribe to Greeks for contracts you want to track individually, and the chain for a surface-level view. - **Add quote and depth subscriptions**. Call `subscribe_quotes` for top-of-book `QuoteTick` updates on individual option contracts. Call `subscribe_book_deltas` when you need the dedicated option orderbook stream. Bybit supports option depths 25 and 100. - **Options execution**. The [delta-neutral strategy tutorial](delta_neutral_options_bybit.md) walks through a short strangle with perpetual hedging, including IV-based order placement via Bybit's `order_iv` parameter. ## See also - [Options](../concepts/options.md): option instrument types, Greeks data types, and chain architecture. - [Bybit integration](../integrations/bybit.md): full Bybit adapter reference including options order parameters and limitations. # Order Book Data Source: https://nautilustrader.io/docs/latest/tutorials/orderbook_data/ Load Bybit order book archives and convert normalized venue rows into NautilusTrader order book deltas for the Binance and Bybit backtest tutorials. ```python from __future__ import annotations import json import tempfile from collections.abc import Iterable from collections.abc import Iterator from decimal import Decimal from os import PathLike from pathlib import Path from zipfile import ZipFile, is_zipfile import pandas as pd from nautilus_trader.model import ( BookAction, BookOrder, CryptoPerpetual, CurrencyPair, OrderBookDelta, OrderSide, Price, Quantity, RecordFlag, ) from nautilus_trader.testkit.providers import TEST_DATA_DIR, TestDataProvider def sample_data_path(name: str) -> Path: """ Return a local path for the bundled sample file at the given relative `name`. Reads from the `test_data/` directory in a source checkout, and downloads to a temporary directory otherwise, so the loaders that only accept a file path work from an installed wheel. """ local = TEST_DATA_DIR / name if local.is_file(): return local cached = Path(tempfile.gettempdir()) / "nautilus_sample_data" / name if not cached.is_file(): cached.parent.mkdir(parents=True, exist_ok=True) cached.write_bytes(TestDataProvider().read(name)) return cached def deltas_from_frame( frame: pd.DataFrame, instrument: CurrencyPair | CryptoPerpetual, ) -> list[OrderBookDelta]: """ Convert loader rows and preserve snapshot and event boundaries. """ if frame.empty: return [] instrument_id = str(instrument.id) if not frame["instrument_id"].eq(instrument_id).all(): raise ValueError(f"Expected only {instrument_id} order book data") rows = list(frame.itertuples()) deltas: list[OrderBookDelta] = [] first = rows[0] first_action = BookAction.from_str(first.action) if int(first.flags) & RecordFlag.F_SNAPSHOT.value and first_action != BookAction.CLEAR: ts = int(first.Index.value) deltas.append( OrderBookDelta( instrument_id=instrument.id, action=BookAction.CLEAR, order=BookOrder( side=OrderSide.from_str(first.side), price=Price.from_decimal_dp( Decimal(str(first.price)), instrument.price_precision, ), size=Quantity.zero(instrument.size_precision), order_id=0, ), flags=RecordFlag.F_SNAPSHOT.value, sequence=int(first.sequence), ts_event=ts, ts_init=ts, ), ) for index, row in enumerate(rows): ts = int(row.Index.value) next_row = rows[index + 1] if index + 1 < len(rows) else None flags = int(row.flags) next_starts_snapshot = next_row is not None and next_row.action == "CLEAR" snapshot_continues = ( next_row is not None and not next_starts_snapshot and flags & RecordFlag.F_SNAPSHOT.value and int(next_row.flags) & RecordFlag.F_SNAPSHOT.value ) event_continues = ( next_row is not None and not next_starts_snapshot and next_row.Index == row.Index and next_row.sequence == row.sequence ) if not snapshot_continues and not event_continues: flags |= RecordFlag.F_LAST.value deltas.append( OrderBookDelta( instrument_id=instrument.id, action=BookAction.from_str(row.action), order=BookOrder( side=OrderSide.from_str(row.side), price=Price.from_decimal_dp( Decimal(str(row.price)), instrument.price_precision, ), size=Quantity.from_decimal_dp( Decimal(str(row.size)), instrument.size_precision, ), order_id=int(row.order_id), ), flags=flags, sequence=int(row.sequence), ts_event=ts, ts_init=ts, ), ) return deltas def load_bybit_order_book_deltas( file_path: str | PathLike[str], nrows: int | None = None, ) -> pd.DataFrame: if not is_zipfile(file_path): raise ValueError("Bybit order book data must be a ZIP archive") rows = [] with ZipFile(file_path) as archive, archive.open(archive.namelist()[0]) as file: for event in _bybit_events(file): if nrows is not None and len(rows) + len(event) > nrows: break rows.extend(event) columns = [ "timestamp", "instrument_id", "action", "side", "price", "size", "order_id", "flags", "sequence", ] frame = pd.DataFrame(rows, columns=columns).set_index("timestamp") return frame.astype({"order_id": int, "flags": int, "sequence": int}) def _bybit_events(lines: Iterable[bytes]) -> Iterator[list[dict[str, object]]]: for line in lines: message = json.loads(line) data = message["data"] timestamp = pd.to_datetime(int(message["ts"]) * 1_000_000, unit="ns", utc=True) snapshot = message["type"] == "snapshot" sides = [(side, data.get(key) or []) for key, side in (("b", "BUY"), ("a", "SELL"))] event = [] if snapshot: side, levels = next(((side, levels) for side, levels in sides if levels), ("BUY", [])) event.append( { "timestamp": timestamp, "instrument_id": f"{data['s']}-LINEAR.BYBIT", "action": "CLEAR", "side": side, "price": levels[0][0] if levels else "0", "size": "0", "order_id": 0, "flags": RecordFlag.F_SNAPSHOT.value, "sequence": data["seq"], }, ) for side, levels in sides: for price, size in levels: if snapshot: action = "ADD" elif Decimal(size) == 0: action = "DELETE" else: action = "UPDATE" event.append( { "timestamp": timestamp, "instrument_id": f"{data['s']}-LINEAR.BYBIT", "action": action, "side": side, "price": price, "size": size, "order_id": 0, "flags": RecordFlag.F_SNAPSHOT.value if snapshot else 0, "sequence": data["seq"], }, ) yield event ``` # Order Book Imbalance Source: https://nautilustrader.io/docs/latest/tutorials/orderbook_imbalance/ Define the reusable order book imbalance strategy used by the Binance and Bybit order book backtest tutorials. ```python from __future__ import annotations from decimal import Decimal from nautilus_trader.config import StrategyConfig from nautilus_trader.model import ( BookType, InstrumentId, OrderBookDeltas, OrderSide, Quantity, TimeInForce, ) from nautilus_trader.trading import Strategy class OrderBookImbalanceConfig(StrategyConfig): def __init__( self, *, instrument_id: str, max_trade_size: str, trigger_min_size: float = 100.0, trigger_imbalance_ratio: float = 0.20, min_seconds_between_triggers: float = 1.0, book_type: str = "L2_MBP", **_kwargs: object, ) -> None: super().__init__() self.instrument_id = instrument_id self.max_trade_size = max_trade_size self.trigger_min_size = trigger_min_size self.trigger_imbalance_ratio = trigger_imbalance_ratio self.min_seconds_between_triggers = min_seconds_between_triggers self.book_type = book_type class OrderBookImbalance(Strategy): def __init__(self, config: OrderBookImbalanceConfig) -> None: if not 0 < config.trigger_imbalance_ratio < 1: raise ValueError("trigger_imbalance_ratio must be between 0 and 1") if config.min_seconds_between_triggers < 0: raise ValueError("min_seconds_between_triggers must be non-negative") super().__init__(config) self._instrument_id = InstrumentId.from_str(config.instrument_id) self._book_type = BookType.from_str(config.book_type) self._max_trade_size = Decimal(config.max_trade_size) self._trigger_min_size = Decimal(str(config.trigger_min_size)) self._trigger_imbalance_ratio = Decimal(str(config.trigger_imbalance_ratio)) self._trigger_interval_ns = int(config.min_seconds_between_triggers * 1_000_000_000) self._instrument = None self._last_trigger_ns: int | None = None def on_start(self) -> None: self._instrument = self.cache.instrument(self._instrument_id) if self._instrument is None: log_msg = f"Could not find instrument for {self._instrument_id}" self.log.error(log_msg) self.stop() return self.subscribe_book_deltas(self._instrument_id, self._book_type, managed=True) def on_book_deltas(self, _deltas: OrderBookDeltas) -> None: book = self.cache.order_book(self._instrument_id) if book is None or not book.spread(): return bid_size = book.best_bid_size() ask_size = book.best_ask_size() if bid_size is None or bid_size <= 0 or ask_size is None or ask_size <= 0: return bid = bid_size.as_decimal() ask = ask_size.as_decimal() smaller = min(bid, ask) larger = max(bid, ask) if larger <= self._trigger_min_size or smaller / larger >= self._trigger_imbalance_ratio: return now = self.clock.timestamp_ns() if ( self._last_trigger_ns is not None and now - self._last_trigger_ns < self._trigger_interval_ns ): return if self.cache.orders_inflight(strategy_id=self.strategy_id): return if bid > ask: side = OrderSide.BUY price = book.best_ask_price() level_size = ask else: side = OrderSide.SELL price = book.best_bid_price() level_size = bid if price is None or self._instrument is None: return self._last_trigger_ns = now order = self.order_factory.limit( instrument_id=self._instrument_id, order_side=side, quantity=Quantity.from_decimal_dp( min(level_size, self._max_trade_size), self._instrument.size_precision, ), price=price, time_in_force=TimeInForce.FOK, post_only=False, ) self.submit_order(order) def on_stop(self) -> None: self.cancel_all_orders(self._instrument_id) self.close_all_positions(self._instrument_id) def on_reset(self) -> None: self._instrument = None self._last_trigger_ns = None ``` # Backtest Accounts and Margin Source: https://nautilustrader.io/docs/latest/concepts/backtesting/accounts-and-margin/ Backtest venues use simulated accounts for balances, margin, and funding settlement. For the full account model and margin formulas, see [Accounting](../accounting.md). ## Funding Backtests settle perpetual funding at funding boundaries from `FundingRateUpdate` data. When an update has `next_funding_ns`, the simulated exchange stores the latest rate, and the backtest clock emits one `FundingSettlement` at that timestamp. Without `next_funding_ns`, the exchange settles only when `ts_event` lands on the `interval` boundary. Updates without a boundary remain strategy data and do not create funding payments. ```mermaid flowchart LR A[FundingRateUpdate] --> B[SimulatedExchange stores latest rate] B --> C[Backtest clock reaches funding boundary] C --> D[FundingSettlement] D --> E[Open positions] E --> F[PositionAdjusted: Funding] E --> G[AccountState] F --> H[Portfolio] G --> H ``` The settlement adjusts the open position and the matching account balance before the portfolio observes the new state. `PositionAdjusted` remains the position accounting event. A positive funding rate debits long positions and credits short positions. The resulting adjustment changes realized PnL, and the matching account balance update records the cash movement. ## Accounts Every backtest venue uses one of three `account_type` values: `CASH`, `MARGIN`, or `BETTING`. The low-level API accepts model types directly: ```python from nautilus_trader.backtest import BacktestEngine from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.model import AccountType from nautilus_trader.model import Money from nautilus_trader.model import OmsType from nautilus_trader.model import Venue engine = BacktestEngine(BacktestEngineConfig()) engine.add_venue( venue=Venue("BINANCE"), oms_type=OmsType.NETTING, account_type=AccountType.CASH, starting_balances=[Money.from_str("10_000 USDT")], ) ``` The high-level API accepts the same enum values but represents starting balances as strings: ```python from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.model import AccountType from nautilus_trader.model import BookType from nautilus_trader.model import OmsType venue = BacktestVenueConfig( name="SIM", oms_type=OmsType.NETTING, account_type=AccountType.CASH, book_type=BookType.L1_MBP, starting_balances=["10_000 USDT"], ) ``` ## Margin models Margin accounts use `LeveragedMarginModel` by default. Pass `StandardMarginModel` when the simulation should reserve the instrument's fixed initial and maintenance margin percentages without reducing them by account leverage. ```python from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.model import AccountType from nautilus_trader.model import BookType from nautilus_trader.model import OmsType from nautilus_trader.model import StandardMarginModel venue = BacktestVenueConfig( name="SIM", oms_type=OmsType.NETTING, account_type=AccountType.MARGIN, book_type=BookType.L1_MBP, starting_balances=["1_000_000 USD"], margin_model=StandardMarginModel(), ) ``` `BacktestVenueConfig` accepts the built-in `StandardMarginModel` and `LeveragedMarginModel` objects directly. The current high-level configuration does not load custom margin models from class-path strings. # Backtest APIs and Repeated Runs Source: https://nautilustrader.io/docs/latest/concepts/backtesting/apis-and-runs/ NautilusTrader provides a **low-level** `BacktestEngine` API for direct control and a **high-level** `BacktestNode` API for catalog-backed, configurable runs. ## Choosing an API level Use the low-level API when: - The data fits in memory or you will stream batches manually. - You want to load data from formats other than a Nautilus Parquet catalog. - You need direct control over venues, instruments, actors, strategies, or execution algorithms. - You want to rerun the same loaded data with selected components changed. Use the high-level API when: - The data lives in a `ParquetDataCatalog`. - The data requires automatic chunked loading. - You want one configuration object to describe and identify a catalog-backed run. - You want each independent run to use a fresh engine. ## Low-level API The low-level API centers on `BacktestEngine`. Create the engine with a `BacktestEngineConfig`, then add venues, instruments, components, and data before calling `run()`: ```python from nautilus_trader.backtest import BacktestEngine from nautilus_trader.config import BacktestEngineConfig engine = BacktestEngine(BacktestEngineConfig()) engine.add_venue(...) engine.add_instrument(instrument) engine.add_strategy(strategy) engine.add_data(data) engine.run() ``` ### Loading data Each `add_data()` call copies its input into an independent stream. The engine orders each stream by replay timestamp and merges all streams chronologically during the run. Adding one batch per instrument does not repeatedly sort a cumulative list: ```python engine.add_data(instrument1_bars) engine.add_data(instrument2_bars) engine.add_data(instrument3_bars) engine.run() ``` Keep the default `sort=True` unless a batching workflow deliberately manages run readiness. A call with `sort=False` marks the engine as not ready to run. Call `sort_data()` before `run()` unless a later `add_data(..., sort=True)` call has restored readiness: ```python engine.add_data(instrument1_bars, sort=False) engine.add_data(instrument2_bars, sort=False) engine.sort_data() engine.run() ``` `sort_data()` marks the independently ordered streams as ready for replay. It is safe to call more than once. The engine copies each input sequence. Clearing or modifying the original Python list after `add_data()` does not change the loaded stream. ### Streaming batches manually Use **streaming mode** when the complete dataset does not fit in memory: ```python engine.add_strategy(strategy) for batch in data_batches: engine.add_data(batch) engine.run(streaming=True) engine.clear_data() engine.end() ``` `run(streaming=True)` pauses when the current data is exhausted. It does not finalize the trader or advance timers beyond that batch. Call `end()` after the final batch to flush timers to the last run boundary, invoke stop handlers, and produce the final result. The low-level API does not expose a generator-based `add_data_iterator()` method. `BacktestNode` provides automatic catalog chunking; direct engine users stream with the loop above. ## High-level API The high-level API centers on `BacktestNode`. Each `BacktestRunConfig` contains: - One or more `BacktestVenueConfig` objects. - One or more `BacktestDataConfig` objects. - An optional `BacktestEngineConfig`. - Optional chunk size, time bounds, exception handling, and disposal settings. Build the node before adding strategies through its run-specific methods: ```python from nautilus_trader.config import BacktestDataConfig from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.backtest import BacktestNode from nautilus_trader.config import BacktestRunConfig from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.model import AccountType from nautilus_trader.model import BookType from nautilus_trader.model import OmsType venue = BacktestVenueConfig( name="SIM", oms_type=OmsType.HEDGING, account_type=AccountType.MARGIN, book_type=BookType.L1_MBP, starting_balances=["1_000_000 USD"], ) data = BacktestDataConfig( data_type="QuoteTick", catalog_path="/data/catalog", instrument_id=instrument_id, ) config = BacktestRunConfig( venues=[venue], data=[data], engine=BacktestEngineConfig(), chunk_size=100_000, ) node = BacktestNode([config]) node.build() node.add_strategy_from_config(config.id, strategy_config) results = node.run() ``` Set `chunk_size` to a value in `[1, 1_000_000]` to enable streaming. Leave it as `None` to load all data at once. `BacktestNode` also provides methods for adding actors and built-in strategies to a built run. ## Shutdown on error Set `BacktestEngineConfig.shutdown_on_error=True` to request a normal shutdown when the Rust logger emits an error record: ```python from nautilus_trader.config import BacktestEngineConfig config = BacktestEngineConfig(shutdown_on_error=True) ``` The backtest loop observes the request, stops the trader and engines, and returns results collected up to that point. It does not abort the process. Error records suppressed by component filters or `bypass_logging=True` still request shutdown. Python `logging.error(...)` calls do not. The trigger resets when a new kernel run starts. For final `on_stop` and command-settling behavior, see [shutdown semantics](execution-flow.md#shutdown-semantics). ## Repeated runs `BacktestEngine.reset()` returns trading state and loaded component state to their initial values. It keeps data, instruments, venues, actors, strategies, and execution algorithms registered. The reset **clears**: - Orders, positions, and account balances. - Component runtime state. - Engine counters and timestamps. The reset **retains**: - Data added through `add_data()`. - Instruments and venue configuration. - Registered actors, strategies, and execution algorithms. Instruments remain loaded because the default backtest cache configuration sets `drop_instruments_on_reset=False`. ### Use fresh nodes for independent runs `BacktestNode` accepts one `BacktestRunConfig`. To run independent configurations, create and dispose one node at a time: ```python configs = [ BacktestRunConfig(...), BacktestRunConfig(...), BacktestRunConfig(...), ] results = [] for config in configs: node = BacktestNode([config]) try: results.extend(node.run()) finally: node.dispose() ``` Build each node and register its strategies before `run()` when they are not supplied by a controller. ### Reuse loaded data for parameter runs Use `reset()` when runs should share loaded data and venue setup: ```python engine = BacktestEngine(BacktestEngineConfig()) engine.add_venue(...) engine.add_instrument(instrument) engine.add_data(data) engine.add_strategy(strategy1) engine.run() engine.reset() engine.run() engine.reset() engine.clear_strategies() engine.add_strategy(strategy2) engine.run() ``` Call `clear_strategies()` before replacing a strategy instance. Use `clear_data()` only when the next run should load a different dataset. # Bar-Based Execution Source: https://nautilustrader.io/docs/latest/concepts/backtesting/bar-execution/ Bar data records the open, high, low, close, and volume for an interval. It does not record when each price occurred within that interval or whether the high preceded the low. Bar-based execution therefore simulates a plausible intrabar path rather than reconstructing the original trades. NautilusTrader converts each execution bar into synthetic market updates for an L1 order book. Resting orders match as those updates move through the bar. ## Bar timestamp convention :::warning For execution simulation, each bar's initialization timestamp (`ts_init`) must represent the **close of the interval**. This prevents the complete bar from becoming visible before it formed. ::: The event timestamp (`ts_event`) may represent the open or close, depending on the data source: - For bars timestamped at the close, set `ts_init` to the same timestamp. - For bars timestamped at the open, set `ts_init = ts_event + interval_ns`. For example, add `60_000_000_000` nanoseconds for one-minute bars. Where an adapter provides a setting such as `bars_timestamp_on_close=True`, prefer that setting so the stored data uses the expected convention. For custom data, populate `ts_event` and `ts_init` before constructing `Bar` objects, encoding Arrow record batches, writing a catalog, or calling `add_data()`. The `BarDataWrangler` consumes explicit timestamp fields and does not expose a `ts_init_delta` argument. Verify the result on a small sample before running a backtest. ## Processing bar data Bar execution applies only when: - The venue has `bar_execution=True`. - The venue uses `BookType.L1_MBP`. - The bar has an external aggregation source. Internally aggregated bars and bars sent to L2 or L3 venues still reach subscribed strategies, but they do not update the matching engine's book or trigger matching. For each applicable bar, the engine: 1. Selects the most granular configured bar type for the instrument. 1. Splits the bar volume across four synthetic updates. 1. Processes the open, high, and low in the configured order, then the close. 1. Matches orders after each synthetic update. 1. Dispatches the complete bar to actors and strategies. Orders already resting at the start of the bar can therefore fill at an intermediate OHLC point. Orders submitted from `on_bar` arrive only after all four points for that bar have been processed. ## OHLC price simulation The engine splits the bar volume evenly across the four price points. It assigns any remainder to the close so the synthetic updates preserve total volume. If one quarter of the volume is below the instrument's minimum `size_increment`, each point uses the minimum increment. The venue's `bar_adaptive_high_low_ordering` option controls the intrabar path: - With `False` (the default), every bar uses `Open -> High -> Low -> Close`. - With `True`, the engine visits the extreme closest to the open first: - If the open is closer to the high, it uses `Open -> High -> Low -> Close`. - If the open is closer to the low, it uses `Open -> Low -> High -> Close`. The adaptive path is a **deterministic heuristic**, not a reconstruction of the actual trade sequence. Its accuracy depends on the market, interval, and data source. An [exploratory EUR/USD analysis](https://gist.github.com/stefansimik/d387e1d9ff784a8973feca0cde51e363) motivates the distance heuristic but does not establish a general accuracy rate. The path matters when both a protective stop and a profit target lie inside the same bar because the first visited level determines which order can fill first. Configure adaptive ordering on the venue: ```python from nautilus_trader.backtest import BacktestEngine from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.model import AccountType from nautilus_trader.model import Money from nautilus_trader.model import OmsType from nautilus_trader.model import Venue engine = BacktestEngine(BacktestEngineConfig()) engine.add_venue( venue=Venue("SIM"), oms_type=OmsType.NETTING, account_type=AccountType.CASH, starting_balances=[Money.from_str("10_000 USDT")], bar_adaptive_high_low_ordering=True, ) ``` ## Order submission timing Bar N's OHLC sequence runs before `on_bar(N)`. Without a latency model, an order submitted from `on_bar` settles immediately against the book left at bar N's close. A latency model delays the order's effective arrival. Once the command reaches its arrival timestamp, the engine can release it from the venue's latency queue in two ways: - Exchange-routed market data for the order's instrument. With bar-only data and no intervening timer events, the first bar at or after the arrival timestamp completes its OHLC sweep before the order settles, so the order sees that bar's close. Quote or trade ticks can release it earlier against the book state they establish. - An unrestricted settlement point, such as a timer, funding-rate settlement, or shutdown drain. These points release all commands due at that time. Market data for another instrument does not release the delayed command against stale book state. ```python from nautilus_trader.execution import StaticLatencyModel engine.add_venue( venue=Venue("SIM"), oms_type=OmsType.NETTING, account_type=AccountType.CASH, starting_balances=[Money.from_str("10_000 USDT")], latency_model=StaticLatencyModel(base_latency_nanos=1_000_000_000), ) ``` :::warning[Next-bar-open fills and look-ahead] The engine does not provide a native next-bar-open fill mode. A strategy can form a signal from a completed prior bar without look-ahead, but the next bar's open is processed before that next bar is dispatched. Using the current bar's open from its `on_bar` callback would introduce look-ahead; using latency with bar-only data normally settles against a later book state, not the next open. ::: ## Internal bar aggregation timing When the data engine aggregates time bars from ticks, a timer closes each bar at the interval boundary. Data with the exact same timestamp may otherwise be processed after that close timer. Set `time_bars_build_delay` in `DataEngineConfig` to delay the timer: ```python from nautilus_trader.config import BacktestEngineConfig from nautilus_trader.config import DataEngineConfig config = BacktestEngineConfig( data_engine=DataEngineConfig( time_bars_build_delay=1, ), ) ``` The value is in microseconds. A small delay, such as one microsecond, lets boundary data arrive before the bar closes. It affects only internally aggregated bars. # Backtest Data and Venues Source: https://nautilustrader.io/docs/latest/concepts/backtesting/data-and-venues/ ## Data Historical data advances the backtest clock, updates simulated market state, and drives strategy callbacks. The venue's `book_type` determines which data can update the matching book, so the configuration must match the available data. Order book data exposes more execution detail than quotes, trades, or bars, but even a recorded book cannot show how a simulated order would have changed the market. NautilusTrader supports the following data in descending order of detail: ```mermaid flowchart LR L3["L3 Order Book
(market-by-order)"] L2["L2 Order Book
(market-by-price)"] L1["L1 Quotes
(top of book)"] T["Trades"] B["Bars"] L3 --> L2 --> L1 --> T --> B style L3 fill:#2d5a3d,color:#fff style L2 fill:#3d6a4d,color:#fff style L1 fill:#4d7a5d,color:#fff style T fill:#5d8a6d,color:#fff style B fill:#6d9a7d,color:#fff ``` More granular data exposes more of the recorded queue and depth, while less granular data requires more simulation assumptions. - **L3 order book data (market-by-order)**: Individual orders at each recorded price level. - **L2 order book data (market-by-price)**: Aggregate size at each recorded price level. - **L1 quote ticks (market-by-price)**: Best bid and ask prices and sizes. - **Trade ticks**: Recorded executions. - **Bars**: Aggregated price and volume over fixed intervals. ### Choosing data: cost vs. accuracy Bar data can be sufficient for early strategy development and is often cheaper and easier to obtain than tick or order book data. It cannot establish intrabar price order, spread, depth, or queue position, so execution-sensitive strategies need more granular validation. :::tip Start with bars to test the core signal when appropriate. Move to quotes, trades, or depth data before relying on results that depend on spread, exact intrabar order, tight exits, or queue position. ::: ## Venues When initializing a venue for backtesting, you must specify its internal order `book_type` for execution processing from the following options: - `L1_MBP`: Level 1 market-by-price (default). Only the top level of the order book is maintained. - `L2_MBP`: Level 2 market-by-price. Order book depth is maintained, with a single order aggregated per price level. - `L3_MBO`: Level 3 market-by-order. Order book depth is maintained, with all individual orders tracked as provided by the data. The `book_type` determines which data updates book state and drives matching. Data that does not apply to the selected book is ignored for book and price updates, but the outer backtest clock still advances. Strategies continue to receive subscribed data through the data engine. Precision validation depends on the matching path; for example, a bar ignored by an L2 or L3 venue returns before executable-bar precision checks. | Data type | L1_MBP | L2_MBP | L3_MBO | | ------------------ | ----------------- | ----------------- | ----------------- | | `QuoteTick` | Updates book | *Ignored* | *Ignored* | | `TradeTick` | Triggers matching | Triggers matching | Triggers matching | | `Bar` | Updates book | *Ignored* | *Ignored* | | `OrderBookDelta` | *Ignored* | Updates book | Updates book | | `OrderBookDeltas` | *Ignored* | Updates book | Updates book | | `OrderBookDepth10` | Updates book | Updates book | Updates book | The granularity of the data must match the specified order `book_type`. Nautilus cannot generate higher granularity data (L2 or L3) from lower-level data such as quotes, trades, or bars. :::warning[L2 and L3 book data] If you specify `L2_MBP` or `L3_MBO` as the venue's `book_type`, quotes and bars will not update the book. Ensure you provide order book delta data, otherwise orders may appear as though they are never filled. ::: :::warning[L1 ignores order book deltas] When using `L1_MBP` (the default), order book deltas are ignored by the matching engine. If you subscribe to order book deltas, set the venue `book_type` to `L2_MBP` or `L3_MBO`. This also applies to sandbox execution, where the matching engine uses the same `book_type` configuration. ::: # Backtest Execution Flow Source: https://nautilustrader.io/docs/latest/concepts/backtesting/execution-flow/ The backtest loop processes market state before strategy callbacks, then settles commands generated at the same timestamp. ## Data and message sequencing In the main backtesting loop, new market data is processed for order execution before being dispatched to actors/strategies via the data engine. ### Main loop flow For each data point the engine runs three phases: 1. **Exchange processes data.** The simulated exchange updates its order book from the incoming market data and iterates the matching engine. This fills any existing orders that now match against the new market state. 1. **Strategy receives data.** The data engine dispatches the data point to actors and strategies via their callbacks (e.g. `on_quote`, `on_bar`). Strategies may submit, cancel, or modify orders during these callbacks. 1. **Settle venues.** The engine drains all queued venue commands and then iterates matching engines to fill newly submitted orders. This loop repeats until no eligible commands remain, so cascading orders (e.g. a hedge submitted from `on_order_filled`) settle within the same timestamp. Earlier latency-delayed commands follow the instrument-scoped rules under [command settling](#command-settling). ```mermaid sequenceDiagram participant BL as Backtest Loop participant Exch as SimulatedExchange participant ME as MatchingEngine participant DE as DataEngine participant Stgy as Strategy BL->>BL: next data point (ts=T) rect rgb(240, 248, 255) note right of BL: Phase 1 - Exchange processes data BL->>Exch: process_quote_tick / process_bar Exch->>ME: update book + iterate() note right of ME: Matches existing orders
against new market state note right of ME: Option expiry cancels open orders
Settlement waits for the expiry timer end rect rgb(245, 255, 245) note right of BL: Phase 2 - Strategy receives data BL->>DE: process(data) DE->>Stgy: on_quote() / on_bar() Stgy-->>Exch: submit_order (queued or immediate) end rect rgb(255, 248, 240) note right of BL: Phase 3 - Settle venues BL->>BL: _process_and_settle_venues(T) BL->>Exch: _drain_commands(T) note right of Exch: Processes queued commands,
adds orders to matching core BL->>ME: _core.iterate(T) note right of ME: Matches newly added orders
against current market state note right of ME: Fills may trigger strategy callbacks
that enqueue further commands,
repeats until no eligible commands BL->>Exch: run simulation modules end ``` The three phases ensure resting orders see the incoming market before newly submitted orders do. Timer events use the same settle mechanism but batch by timestamp: all callbacks at timestamp T execute first, then venues are settled for T before advancing to T+1. For timer behavior used by internally aggregated bars, see [internal bar aggregation timing](bar-execution.md#internal-bar-aggregation-timing). ### Deferred option settlement At an option's expiration timestamp, automatic expiry checks close its market, cancel open orders, and reject new orders. Position settlement waits until all market data at that timestamp has been processed, so settlement sees the latest underlying price available for that timestamp. An explicit `InstrumentClose` with `InstrumentCloseType::ContractExpired` attempts settlement immediately. Streaming batches must keep **all data for a timestamp together**; `BacktestNode` does this automatically. `SimulatedVenueConfig.defer_option_settlement` defaults to `true`. The backtest engine schedules settlement after all market data at the expiry timestamp, without waiting for the next timestamp. When driving `SimulatedExchange` directly, schedule expiry processing after that timestamp's data, or explicitly set `defer_option_settlement` to `false` for immediate settlement. Immediate settlement can use an older underlying price if an update with the same timestamp has yet to be processed. ### Command settling #### Same-cycle commands An order fill can trigger a strategy callback that submits another order, such as a stop-loss from `on_order_filled`. The engine drains venue command queues and processes any commands generated by their events until no command eligible for the current cycle remains. Commands created at that timestamp and already due, including zero-latency and same-tick commands for another instrument, settle within the same cycle. Simulation modules run once, after the command loop completes. #### Latency-delayed commands A `LatencyModel` places each command in the venue's inflight queue with an arrival timestamp. Once a command is due, the settlement point determines whether the engine releases it: | Settlement point | Due commands released | | ----------------------- | -------------------------------------------------------------------------- | | Market data | Same-timestamp commands and older commands for the data's instrument. | | Timer | All commands due at the timer timestamp. | | Funding-rate settlement | All commands due at the funding settlement timestamp. | | Shutdown drain | All commands due as the clock advances through the final inflight arrival. | Market data for another instrument does not activate an older command against stale market state. Commands with a future arrival timestamp remain in the inflight queue. ### Sandbox inbound latency `SandboxExecutionClientConfig.latency_model` accepts a `StaticLatencyModel`, mirroring the existing `fee_model` field. A submit, modify, or cancel is deferred by the model's insert, update, or delete leg before it reaches the matching engine. Venue-generated events (accepts, fills, cancels, expirations) are not delayed, so the model covers the inbound leg only. Without a latency model the client is unchanged and its events take the runner's execution channel as before. ```python from nautilus_trader.adapters.sandbox import SandboxExecutionClientConfig from nautilus_trader.execution import StaticLatencyModel from nautilus_trader.model import Money from nautilus_trader.model import Venue config = SandboxExecutionClientConfig( venue=Venue("BINANCE"), starting_balances=[Money.from_str("10_000 USDT")], latency_model=StaticLatencyModel(base_latency_nanos=1_000_000_000), ) ``` Every event the client emits takes the runner's execution channel exactly as it does without a latency model, in emission order: an order's `OrderSubmitted` precedes its venue events, and a fill from market data precedes the response to any command released after it. A command is applied before any market data processed after its due time, since the client drains its queue ahead of each tick it receives, and the client's clock alert releases a queue no data is flowing to. A command whose latency leg is zero is applied on arrival, unless a command is already due and not yet released, in which case it joins the queue behind it. A cancel-all reaching the venue cancels only orders the venue has received: an order whose submit is still in transit is left alone until the venue processes its submit. In both backtest and sandbox, contingent actions also respect venue receipt. Fills, updates, expirations, and cancellations cannot activate, amend, or cancel a linked order the venue has not yet received. Each submit list arrives as a unit. An OTO child already received by the venue can still activate when its parent fills. A late submit is checked against the current state of its linked orders and may be rejected if a linked order has already closed. Contingent quantity changes skipped before receipt are not replayed when the submit arrives; the order retains its submitted quantity unless another applicable rule changes it. Stopping the client discards anything still in flight. A discarded submit, modify, or targeted cancel is rejected (`OrderRejected`, `OrderModifyRejected`, `OrderCancelRejected`) so its order does not stay `SUBMITTED` or pending forever; the sandbox generates no order status reports, so nothing else would resolve it. A discarded `CancelAllOrders` is dropped, since the strategy marks no order `PENDING_CANCEL` for it and so there is no pending state to release. ### Shutdown semantics `BacktestEngine::end()` is separate from the `shutdown_on_error` configuration in [backtest APIs and repeated runs](apis-and-runs.md#shutdown-on-error). It invokes each strategy's `on_stop` handler, drains and settles any commands it emits (e.g. `close_all_positions`, `cancel_all_orders`), then stops the engines. - `on_stop` commands use normal venue queuing and latency. They do not get priority over earlier inflight commands. - If a pre-stop order reaches the venue before an `on_stop` cancel, it may still fill. A later reduce-only close can then reject if the fill changed net exposure. - Strategies that need deterministic flattening should enter an exit-only state before stopping and avoid new opening orders while cancel and close commands are in-flight. - Strategy event handlers do not fire for the resulting events: the strategy is already `Stopped`, so `OrderFilled` and similar events log but bypass `on_order_filled` and friends. Logic that reacts to fills must run before `on_stop` returns. - Simulation modules do not re-run at shutdown. `SimulationModule::process` is once per timestamp; re-invoking would double-apply side effects like FX rollover interest. - A `LatencyModel` adds its configured delay to trailing commands (those emitted on the final data tick or in `on_stop`). The shutdown path advances the engine clock to the latest inflight arrival timestamp so those commands still settle before the engines stop. ## Timer-only backtests The backtest engine supports runs with timers but no market data. This is useful for scheduled operations or testing timer-based logic. Timers fire in chronological order. ## Deterministic trade IDs The simulated exchange (used by both backtest and sandbox execution) emits a deterministic `TradeId` for each generated fill. The ID is formatted as `T-{hash:016x}-{count:03d}`, where the 16-character hex is an FNV-1a hash of `(venue, raw_id, ts_init)` and the trailing counter distinguishes multiple fills at the same `ts_init` (e.g. several legs of a bar-driven fill). Deterministic trade IDs have these properties: - **Deterministic across runs**: the same replayed data produces the same `TradeId` every time, so downstream dedup and golden-output comparisons stay stable. - **Collision-safe across resets**: `ts_init` is pinned in backtest data and monotonic in live/sandbox, so a `BacktestEngine.reset()` (or an in-memory `IdsGenerator` reset in a sandbox with persisted orders) cannot mint a `TradeId` that collides with one already in the cache. - **Bounded length**: the hash keeps the identifier under the 36-character `TradeId` cap regardless of venue name length. The `use_random_ids` venue flag still governs `VenueOrderId` and `PositionId` generation, but `TradeId` is always deterministic and is not affected by the flag. # Fill Models Source: https://nautilustrader.io/docs/latest/concepts/backtesting/fill-models/ Historical data cannot show how a simulated order would have interacted with other market participants. A **fill model** controls the assumptions NautilusTrader makes about limit-order eligibility, one-tick slippage, and optional synthetic liquidity. ## Behavior by book type With L2 or L3 data, the recorded book supplies price levels and sizes. The matching engine walks those levels, and `prob_fill_on_limit` can model whether a touched limit order fills. `prob_slippage` does not apply because the book itself determines price impact. With an L1 book, including books updated from quotes, trades, or bars: - `prob_fill_on_limit` controls whether a limit order fills when its price is touched. - `prob_slippage` is evaluated for every fill, regardless of order type or liquidity side. - A successful slippage draw moves the fill one tick against the order direction. - A model may provide a synthetic L2 book to represent liquidity beyond the best bid and ask. For example, with `prob_slippage=0.5`, each BUY fill has a 50% chance of moving one tick higher. Set `random_seed` when a run must reproduce the model's random draws. If a venue does not specify a fill model, it uses `DefaultFillModel` with `prob_fill_on_limit=1.0` and `prob_slippage=0.0`. The model therefore considers a touched limit fill-eligible, and L1 fills do not receive probabilistic one-tick slippage by default. This does not disable the matching engine's separate residual-fill rule for eligible market-style orders. :::warning Historical order book data remains immutable after a fill. With `liquidity_consumption=False`, the same displayed size can support more than one simulated order in an iteration. Set `liquidity_consumption=True` to track consumed size per level until fresh data arrives. See [order book immutability](fill-prices-and-matching.md#order-book-immutability). ::: ## Available models | Model | Liquidity behavior | | ---------------------------- | ------------------------------------------------------- | | `DefaultFillModel` | Uses the matching engine's recorded book. | | `BestPriceFillModel` | Provides unlimited size at the best bid and ask. | | `OneTickSlippageFillModel` | Provides unlimited size one tick beyond the best price. | | `ProbabilisticFillModel` | Chooses the best price or one tick worse. | | `TwoTierFillModel` | Places 10 units at best, then the rest one tick worse. | | `ThreeTierFillModel` | Places 50, 30, and 20 units across three levels. | | `LimitOrderPartialFillModel` | Places 5 units at best, then the rest one tick worse. | | `SizeAwareFillModel` | Changes the book shape at an order size of 10 units. | | `CompetitionAwareFillModel` | Exposes a configurable fraction of 1,000 units at best. | | `VolumeSensitiveFillModel` | Places 25% of its internal volume at best. | | `MarketHoursFillModel` | Uses a normal or one-tick-wider synthetic spread. | The tier sizes are model constants expressed in instrument quantity units. Confirm that they suit the scale of the instrument before using a tiered model. `CompetitionAwareFillModel` accepts `liquidity_factor` values in `[0.0, 1.0]`, defaults to `0.3`, and clamps the calculated size to at least one instrument quantity unit. The current Python bindings do not expose the state setters for `VolumeSensitiveFillModel` or `MarketHoursFillModel`. From Python, they retain their initial values of 1,000 recent-volume units and normal-liquidity mode. ## Configuration Pass a built-in model object directly to `BacktestVenueConfig`: ```python from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.execution import DefaultFillModel from nautilus_trader.model import AccountType from nautilus_trader.model import BookType from nautilus_trader.model import OmsType venue = BacktestVenueConfig( name="SIM", oms_type=OmsType.NETTING, account_type=AccountType.CASH, book_type=BookType.L1_MBP, starting_balances=["100_000 USD"], fill_model=DefaultFillModel( prob_fill_on_limit=0.2, prob_slippage=0.5, random_seed=42, ), ) ``` Synthetic book models use the same constructor parameters: ```python from nautilus_trader.execution import ThreeTierFillModel venue = BacktestVenueConfig( name="SIM", oms_type=OmsType.NETTING, account_type=AccountType.CASH, book_type=BookType.L1_MBP, starting_balances=["100_000 USD"], fill_model=ThreeTierFillModel( prob_fill_on_limit=1.0, prob_slippage=0.0, random_seed=42, ), ) ``` The current high-level venue configuration accepts built-in fill models. It does not load fill models from import-path configuration objects. The low-level `BacktestEngine.add_venue()` method also accepts a custom Python object. It must implement: - `is_limit_filled() -> bool` - `is_slipped() -> bool` It may also implement: - `fill_limit_inside_spread() -> bool` - `get_orderbook_for_fill_simulation(instrument, order, best_bid, best_ask) -> OrderBook | None` Subclassing `nautilus_trader.execution.FillModel` supplies default implementations for these methods. This custom-object protocol applies to the low-level engine only. ## Probabilistic parameters ### `prob_fill_on_limit` (default: `1.0`) This value controls whether a limit order fills when the market touches, but does not cross, its price: - `0.0`: Never fill on touch. - `0.5`: Fill on half of eligible touches on average. - `1.0`: Always fill on touch. Crossing the limit price is a separate matching condition. For explicit queue-volume tracking, see [queue position tracking](trade-execution.md#queue-position-tracking). ### `prob_slippage` (default: `0.0`) For L1 books, this value controls a one-tick adverse move on each fill: - `0.0`: Never add model slippage. - `0.5`: Add one tick on half of fills on average. - `1.0`: Add one tick to every fill. The draw applies to maker and taker fills. It does not apply to L2 or L3 books. ## Synthetic order books Before determining a fill, the matching engine asks the model for an optional synthetic order book. If the model returns a book, the engine fills against its levels. If it returns `None`, the engine uses the recorded book. :::warning[Synthetic book consumption] Per-level `liquidity_consumption` tracking does not apply to a synthetic model book. A custom model must represent any desired consumption behavior in the books it returns. ::: # Fill Prices and Matching Source: https://nautilustrader.io/docs/latest/concepts/backtesting/fill-prices-and-matching/ The backtest matching engine treats recorded order book and trade data as immutable. Simulated fills do not edit the historical book. This preserves the replayed market while requiring explicit assumptions about whether the same displayed liquidity can fill more than one simulated order. The engine provides two controls for those assumptions: - `liquidity_consumption=True` tracks displayed size consumed at each price level. - A fixed fill-model `random_seed` makes that model's probabilistic decisions repeatable. It does not configure randomness or execution ordering outside that model. ## Fill price determination Fill prices depend on order type, liquidity side, book type, and the market state that caused the match. ### L2 and L3 books With depth data, market-style orders walk crossed book levels. A limit-style order receives crossed book prices while acting as a taker and uses its limit price when acting as a maker. | Order type | Fill behavior | | ---------------------- | ----------------------------------------------------------------- | | `MARKET` | Walks crossed book levels as a taker. | | `MARKET_TO_LIMIT` | Walks the book, then rests the remainder at its first fill price. | | `LIMIT` | Uses crossed levels as a taker or the limit price as a maker. | | `STOP_MARKET` | Walks crossed levels after triggering. | | `STOP_LIMIT` | Uses the limit-style rule after triggering. | | `MARKET_IF_TOUCHED` | Walks crossed levels after triggering. | | `LIMIT_IF_TOUCHED` | Uses the limit-style rule after triggering. | | `TRAILING_STOP_MARKET` | Walks crossed levels after activation and triggering. | | `TRAILING_STOP_LIMIT` | Uses the limit-style rule after activation and triggering. | A depth order can fill partially when the available crossed size is smaller than its remaining quantity. ### L1 books With an L1 book, the recorded market exposes only the best bid and ask: | Order class | Fill behavior | | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | | `MARKET`, `MARKET_IF_TOUCHED`, `STOP_MARKET`, and `TRAILING_STOP_MARKET` | Use the market or trigger-price rule, then fill any residual one tick worse. | | `MARKET_TO_LIMIT` | Uses the best opposite quote, then rests the remainder at the first fill price. | | Limit-style taker | Uses the best crossed quote, bounded by the limit price. | | Limit-style maker | Uses the limit price when matched by a trade or market move. | The one-tick residual fill applies after the eligible market-style orders exhaust displayed L1 size. Price protection can prevent that residual fill if it would cross the configured boundary. This deterministic residual rule is separate from probabilistic fill-model slippage. Trade-driven matching has one additional rule. If a trade provides fill evidence at a price absent from the book, the engine fills at the order's limit price and caps the quantity at the trade size. See [trade-based execution](trade-execution.md#trade-driven-matching). Fill models can change prices or provide a synthetic depth book. See [fill models](fill-models.md). ### Triggered market-order fills with bars Bar execution distinguishes a gap from an intrabar move for `STOP_MARKET`, `MARKET_IF_TOUCHED`, and `TRAILING_STOP_MARKET` orders. If the bar opens beyond the trigger, the order fills at the simulated market price. For example, a SELL stop at 100 can fill at 90 when the next bar opens at 90. If the bar opens before the trigger and a later high, low, or close moves through it, the engine uses the trigger price. For example: 1. A SELL stop has a trigger at 100. 1. The bar opens at 102. 1. The low reaches 98. 1. The order fills at 100. This rule assumes a continuous move through the trigger. Use quote, trade, or order book data when the strategy requires more precise gap and path behavior. ## Price protection **Price protection** limits how far `MARKET` and `STOP_MARKET` orders can walk the book. Configure the offset as a number of instrument price increments: ```python from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.model import AccountType from nautilus_trader.model import BookType from nautilus_trader.model import OmsType venue = BacktestVenueConfig( name="SIM", oms_type=OmsType.NETTING, account_type=AccountType.MARGIN, book_type=BookType.L2_MBP, starting_balances=["100_000 USDT"], price_protection_points=100, ) ``` The engine calculates the boundary at fill time: - BUY: `ask + (points * price_increment)` - SELL: `bid - (points * price_increment)` For an instrument with a 0.01 price increment, 100 points allow a BUY to fill at most 1.00 above the current ask. Levels beyond the boundary are excluded, so the order can remain partially filled. A market order gets its boundary when processed. A stop-market order gets its boundary when triggered, using the bid or ask at that time. Set `price_protection_points=0` to disable protection. ## Order book immutability A simulated fill never decrements the historical book. By default, each matching iteration can use the full recorded size: ```python from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.model import AccountType from nautilus_trader.model import BookType from nautilus_trader.model import OmsType venue = BacktestVenueConfig( name="SIM", oms_type=OmsType.NETTING, account_type=AccountType.CASH, book_type=BookType.L1_MBP, starting_balances=["100_000 USD"], liquidity_consumption=True, ) ``` With `liquidity_consumption=True`, the engine records the original and consumed size for each price level. Available size is `original_size - consumed`. A fresh book update at that level resets the record to the new displayed size. ### L1 passive fills When an L1 market moves through a passive limit: | `liquidity_consumption` | Remaining-quantity behavior | | ----------------------- | --------------------------------------------------------------------- | | `False` | Fill the complete remaining order at its limit price. | | `True` | Fill only the unconsumed displayed size and leave the remainder open. | For example: 1. The ask is 100.10 for 50 units. 1. A BUY LIMIT for 1,000 units rests at 100.05. 1. The next ask is 100.00 for 30 units. 1. With consumption enabled, 30 units fill and 970 remain open. 1. Later updates can provide more fresh size. ### Trade consumption A trade can provide executable size at a price absent from the current book. With consumption enabled, trade-driven fills share that size instead of letting every order consume the full trade. For L2 and L3 books, the triggering trade may already have consumed displayed depth. The engine accounts for that volume before triggered orders use the remaining level. It skips this adjustment when a newer book update already reflects the trade. Synthetic books returned by a fill model do not use per-level consumption tracking. The model must represent its own liquidity assumptions. ### Limitations Consumption tracking estimates **available size**, not **order priority**. Set `queue_position=True` with book and trade data for displayed-queue tracking, or use `prob_fill_on_limit` for a probabilistic approximation. Trade-driven fills are also opportunistic: a print proves that liquidity existed momentarily, not that it remained available after the recorded trade. ## Precision requirements Prices and quantities must use the instrument's configured `price_precision` and `size_precision`. The outcome of a mismatch depends on where it enters the matching engine: | Input | Validated fields | Mismatch outcome | | ---------------- | ---------------------------------------------------- | ------------------------------------------------------------ | | `QuoteTick` | Bid and ask prices and sizes | Log a warning and skip the tick. | | `TradeTick` | Price and size | Log a warning and skip the tick. | | Executable `Bar` | Open, high, low, close, and volume | Log a warning and skip the bar. | | New order | Quantity, display quantity, price, and trigger price | Reject the order. | | Order update | Quantity, price, and trigger price | Reject the modification. | | Generated fill | Fill price and quantity | Normalize when compatible; otherwise warn and skip the fill. | The engine logs an error after 20 consecutive market-data precision mismatches. With `shutdown_on_error=True`, that error can request a normal backtest shutdown. A valid quote, trade, or executable bar resets the consecutive-mismatch count. `Bar.volume` must use the instrument's quantity units and size precision. Convert provider-specific quote-volume fields before creating the bar. Use the instrument factories to construct compatible values: ```python price = instrument.make_price(raw_price) quantity = instrument.make_qty(raw_quantity) ``` Also verify that the instrument definition matches the data source and that custom loaders preserve the source precision. # Backtesting Source: https://nautilustrader.io/docs/latest/concepts/backtesting/ Backtesting simulates trading against historical data using the same core system components used in live trading: built-in engines, the `Cache`, the [MessageBus](../message_bus.md), `Portfolio`, [Actors](../actors.md), [Strategies](../strategies.md), [Execution Algorithms](../execution/algorithms.md), and user-defined modules. A `BacktestEngine` processes a stream of historical data. When the stream is exhausted, the engine produces results and performance metrics for analysis. NautilusTrader offers two API levels for backtesting: | API level | Use when | | ---------- | ----------------------------------------------------------------------- | | High-level | You want `BacktestNode`, config objects, data catalogs, and batch runs. | | Low-level | You want direct `BacktestEngine` control and manual component setup. | The pages in this section describe the current Rust backtest engine and its Python package-root API. ## Reading guide The generated sidebar may sort these pages alphabetically. Use this order when reading the section end to end: | Step | Page | Use it for | | ---- | ------------------------------------------------------- | ----------------------------------------------- | | 1 | [APIs and repeated runs](apis-and-runs.md) | Choose API level, load data, and run batches. | | 2 | [Data and venues](data-and-venues.md) | Match data granularity with venue `book_type`. | | 3 | [Execution flow](execution-flow.md) | Understand sequencing, timers, and trade IDs. | | 4 | [Fill prices and matching](fill-prices-and-matching.md) | Understand deterministic matching behavior. | | 5 | [Trade execution](trade-execution.md) | Use trade ticks, aggressor sides, and queues. | | 6 | [Bar execution](bar-execution.md) | Use bars, OHLC sequencing, and bar timing. | | 7 | [Fill models](fill-models.md) | Configure slippage and probabilistic fills. | | 8 | [Accounts and margin](accounts-and-margin.md) | Configure funding, balances, and margin models. | ## Simulation modules [Simulation modules](simulation-modules.md) describes module configuration, lifecycle, failure handling, and built-in FX rollover and CFD swap behavior. ## Related guides - [Strategies](../strategies.md): Develop strategies to backtest. - [Visualization](../visualization.md): Generate tearsheets from backtest results. - [Reports](../reports.md): Analyze backtest performance data. # Simulation Modules Source: https://nautilustrader.io/docs/latest/concepts/backtesting/simulation-modules/ This page describes simulation module configuration, lifecycle, and failure handling. The [behavioral model design](../behavioral_models.md) explains the enum and shared-handle representations used below. Simulation modules use the enum and handle forms at different configuration boundaries: | Boundary | Stored form | Accepted implementations | | ---------------------------------------------- | ------------------------ | ------------------------------------- | | Declarative `BacktestVenueConfig` | `SimulationModuleAny` | Built-ins and language bridges. | | `SimulatedVenueConfig` and `SimulatedExchange` | `SimulationModuleHandle` | Any linked Rust trait implementation. | :::warning[Shared module state] `SimulationModuleHandle` owns an `Rc`, so cloning a handle shares the module and its state. Cloning a built-in enum value copies its state, while cloning a Python bridge retains the same Python object. Venues or runs that require isolated state therefore use distinct module instances, including distinct Python objects. ::: ## Lifecycle The exchange runs each module through this lifecycle: 1. `pre_process` runs before the exchange processes each supported market data item. 1. `process` runs for each module in order against the same read-only exchange snapshot after commands have settled for the timestamp. Processing stops at the first failure, and the exchange applies no adjustments from that timestamp. 1. For each completed result in order, the exchange applies its batch as ordered `Money` adjustments, then calls that module's `acknowledge` exactly once with the corresponding outcomes, including for an empty batch. ## Failure handling - Failures from `pre_process`, `process`, `acknowledge`, or `reset` leave the exchange in an error state until every module resets successfully. This prevents a failed acknowledgement from replaying adjustments that the account may already contain. - Diagnostic failures return to the engine with the module index and hook name without changing the exchange error state. ## Python modules The `process` hook for a Python `SimulationModule` subclass receives an owned `SimulationModuleContext` snapshot containing: - The venue. - The optional base currency. - The instruments. - The order books. - The open positions. The bridge does not expose mutable cache or matching-engine state. Python exceptions retain the hook name as they propagate through the exchange and `BacktestEngine.run`. ## Linked native types Linked native PyO3 types can register an extractor for their Python class. The extractor resolves an object for imperative `BacktestEngine.add_venue` configuration. Python configuration resolves modules as follows: | Configuration path | Accepted objects | Stored form | Native extractor behavior | | -------------------------- | ----------------------------------------------------- | ------------------------ | -------------------------- | | `BacktestEngine.add_venue` | Built-ins, linked native types, and Python subclasses | `SimulationModuleHandle` | Matches the exact type. | | `BacktestVenueConfig` | Built-ins and Python subclasses | `SimulationModuleAny` | Does not consult registry. | An unrelated class with the same name does not select a registered extractor. Extractor registration does not create a runtime ABI for trait objects across a `cdylib` boundary. ## Built-in modules The built-in FX rollover and CFD swap modules use the completed-batch acknowledgement flow. CFD swap rates are per-instrument signed daily `Decimal` fractions of settlement notional, with separate long and short values, a configurable UTC rollover time, and a configurable triple-roll weekday. For a single-currency account, the module converts the adjustment to the account base currency at the cached mid exchange rate. The CFD swap module defers the whole batch when any of these inputs is missing: - A matching engine. - A settlement price. - An exchange rate. The module logs one warning per booking date, instrument, and failure kind before quieter retries. Perpetual funding remains part of `SimulatedExchange` and is not a simulation module. # Trade-Based Execution Source: https://nautilustrader.io/docs/latest/concepts/backtesting/trade-execution/ Trade ticks trigger matching by default when a venue has `trade_execution=True`. A trade provides evidence that liquidity traded at its price, so it can fill resting orders on the passive side. Set `trade_execution=False` to use trades as strategy data without treating them as execution liquidity for ordinary resting orders: ```python from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.model import AccountType from nautilus_trader.model import BookType from nautilus_trader.model import OmsType venue = BacktestVenueConfig( name="SIM", oms_type=OmsType.NETTING, account_type=AccountType.CASH, book_type=BookType.L1_MBP, starting_balances=["100_000 USD"], trade_execution=False, ) ``` When trade execution is disabled, behavior depends on the venue's book type: - With L1 data, accepted trade ticks update the L1 book but skip matching and maintenance. Later quote ticks or executable bars drive that work. - With L2 or L3 data, accepted trade ticks advance `LastPrice` and run trailing-stop maintenance for all trigger types. They can trigger `LastPrice` stop orders, which fill against existing book liquidity. The tick does not match resting limits or trigger stop orders that use other trigger types. It also runs enabled GTD expiry and instrument-expiration checks. ## Trade-driven matching The engine temporarily moves its matching references to the trade price: - A `SELL` trade can match resting BUY orders. - A `BUY` trade can match resting SELL orders. - A `NO_AGGRESSOR` trade can affect both sides because the passive side is unknown. The historical order book remains unchanged. Only the matching core's transient bid, ask, and last prices move for the iteration. ### Fill determination When a trade triggers a limit fill: 1. If the book contains crossed liquidity, the engine fills against those book levels. 1. If the book does not represent the trade price, the engine can create a trade-driven fill at the order's limit price. 1. A trade-driven fill is capped at `min(order.leaves_qty, trade.size)`. With `liquidity_consumption=False`, the same trade size can support more than one order during an iteration. With `liquidity_consumption=True`, trade-driven fills share a consumption counter, so their total cannot exceed the unconsumed trade size. For example, a `SELL` trade at 100.00 can fill a BUY LIMIT at 100.05. If no book level represents that fill, the engine uses 100.05 rather than granting the better trade price. ### Matching-state restoration After the iteration, the engine restores matching references from the available market baseline: - With L2 or L3 data, the depth book remains the independent source of bid and ask state. - With an L1 quote baseline, the non-aggressor side is restored from the latest quote. - With trade-only L1 data, there is no quote baseline to restore, so the latest trade continues to define the available top-of-book state. This distinction matters when interpreting a stream of trades without quotes. Repeated trades can move the simulated L1 state, but quote-backed L1 matching does not progressively discard the non-aggressor side of the latest quote. ## Aggressor sides The **aggressor** is the participant that crossed the spread: - `SELL`: A seller hit the bid. The trade can fill a resting BUY order. - `BUY`: A buyer lifted the ask. The trade can fill a resting SELL order. - `NO_AGGRESSOR`: The data does not identify the aggressor. The engine considers both sides where the feature requires a side. A trade with aggressor side `BUY` provides evidence for passive `SELL` orders, not `BUY` orders. A trade with aggressor side `SELL` provides evidence for passive `BUY` orders, not `SELL` orders. ## Combining book and trade data Book updates establish the spread and visible depth. Trade ticks provide execution evidence between those updates. This is useful when depth snapshots are throttled and a trade occurs at a price that the latest snapshot does not contain. Use the two feeds with care: - A trade must have the opposite aggressor side to fill a resting order. - A book update can cross an order independently of a trade. - A fill at a missing trade-price level uses the trade-driven quantity cap. - With consumption enabled, the engine accounts for trade volume already removed from an L2 or L3 book before triggered orders consume the remaining depth. ## Queue position tracking Set `queue_position=True` with `trade_execution=True` to track displayed quantity ahead of each LIMIT order: ```python venue = BacktestVenueConfig( name="SIM", oms_type=OmsType.NETTING, account_type=AccountType.MARGIN, book_type=BookType.L2_MBP, starting_balances=["100_000 USD"], trade_execution=True, queue_position=True, ) ``` Sandbox paper trading uses the same matching-engine flags. Pass them on `SandboxExecutionClientConfig` (defaults remain off, matching current sandbox behavior): ```python from nautilus_trader.adapters.sandbox import SandboxExecutionClientConfig from nautilus_trader.model import BookType from nautilus_trader.model import Money from nautilus_trader.model import Venue config = SandboxExecutionClientConfig( venue=Venue("BINANCE"), starting_balances=[Money.from_str("10_000 USDT")], book_type=BookType.L2_MBP, trade_execution=True, queue_position=True, liquidity_consumption=True, ) ``` The sandbox `venue` must match the data client's instrument venue, and the strategy must subscribe to trades (and L2/L3 deltas when using depth). ### Queue lifecycle 1. On acceptance, a LIMIT order snapshots same-side displayed size at its price. 1. Correct-side trades at that price reduce the quantity ahead. 1. The order becomes fill-eligible when the quantity ahead reaches zero. 1. Only trade volume beyond the cleared queue is available to fill on that tick. For example: 1. The bid at 100.00 contains 100 units. 1. A BUY LIMIT for 50 units joins with 100 units ahead. 1. A `SELL` trade for 80 units reduces the queue ahead to 20. 1. A `SELL` trade for 30 units clears the queue and leaves 10 units available to fill. 1. The next correct-side trade can fill the remaining order quantity. ### Book changes For L2 books and aggregate L3 updates: - A DELETE clears the price level and its queue. - An UPDATE caps quantity ahead at the level's new displayed size. - A completed book snapshot rebases each tracked queue position against the new visible quantity at its price: quantity ahead is capped at the snapshot size, while newly added liquidity does not move an existing simulated order further back. Snapshot batches may start with a `F_SNAPSHOT` clear and finish with a later `F_LAST` delta. - A `BookDepth10` replacement applies the same rebase rule after the full depth replacement. For L3 MBO books: - A per-order DELETE advances the queue by that order's remaining tracked size. - A size decrease advances the queue by the difference. - A size increase keeps the larger order ahead. - A price change removes the book order from the tracked queue. - A completed book snapshot retains only surviving tracked order IDs ahead, each capped at its previous quantity. Changing a simulated order's price resets its queue position at the new level. A quantity-only change retains the progress already made. ### L1 queue tracking With `BookType.L1_MBP`, trade ticks reduce quantity ahead while quotes provide price-move and displayed-size evidence: - A move away through the order's price clears the queue. - A move toward the order preserves the queue. - A return to a previously visible level caps quantity ahead at the new displayed size. - An order behind the BBO remains pending until a quote reaches its price or a trade crosses it. ### Limitations - Queue tracking applies only to `LIMIT` orders. - Each simulated order has an independent queue estimate. - The initial estimate is limited to book state visible at acceptance. - Historical data cannot reveal hidden orders or every venue-specific priority rule. :::warning[Unknown aggressor side] `NO_AGGRESSOR` trades reduce queues on both sides. This can clear a queue and fill an order earlier than reality, so it is optimistic from the strategy's execution perspective. ::: # Bar Source: https://nautilustrader.io/docs/latest/concepts/data/bar/ `Bar` represents OHLCV price and volume data for a specific `BarType`. A venue or data provider can supply bars, or NautilusTrader can aggregate them from quote ticks, trade ticks, or smaller bars. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ---------- | ----------- | ----------- | ---------------- | ------------------------------------------------ | | `bar_type` | `BarType` | `BarType` | Required | Instrument, aggregation, price type, and source. | | `open` | `Price` | `Price` | Required | First price in the bar interval. | | `high` | `Price` | `Price` | Required | Highest price in the bar interval. | | `low` | `Price` | `Price` | Required | Lowest price in the bar interval. | | `close` | `Price` | `Price` | Required | Last price in the bar interval. | | `volume` | `Quantity` | `Quantity` | Required | Traded volume or tick-volume proxy. | | `ts_event` | `UnixNanos` | `int` | Required | Bar event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## Behavior - `high` must be greater than or equal to `open`, `low`, and `close`. - `low` must be less than or equal to `open` and `close`. - `bar_type` determines whether a bar is internal or external. - Composite bar types use `@` syntax to identify the source bar type. :::warning[Bar timestamps for execution simulation] For execution simulation, `ts_init` must represent the close of the bar interval, which prevents the complete bar from becoming visible before it formed. See [bar timestamp convention](../backtesting/bar-execution.md#bar-timestamp-convention). ::: ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::{Bar, BarType}, types::{Price, Quantity}, }; let bar = Bar::new( BarType::from("AUD/USD.SIM-1-MINUTE-LAST-EXTERNAL"), Price::from("0.65000"), Price::from("0.65010"), Price::from("0.64990"), Price::from("0.65005"), Quantity::from("1000000"), UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); ``` ```python tab="Python" from nautilus_trader.model import Bar from nautilus_trader.model import BarType from nautilus_trader.model import Price from nautilus_trader.model import Quantity bar = Bar( bar_type=BarType.from_str("AUD/USD.SIM-1-MINUTE-LAST-EXTERNAL"), open=Price.from_str("0.65000"), high=Price.from_str("0.65010"), low=Price.from_str("0.64990"), close=Price.from_str("0.65005"), volume=Quantity.from_int(1_000_000), ts_event=1_000_000_000, ts_init=1_000_000_100, ) ``` ## Related guides - [Bars and aggregation](index.md#bars-and-aggregation) covers aggregation methods. - [Bar types](index.md#bar-types) explains `BarType` string syntax. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # FundingRateUpdate Source: https://nautilustrader.io/docs/latest/concepts/data/funding_rate_update/ `FundingRateUpdate` represents the funding rate for a perpetual swap instrument. It can also include the funding interval and next funding timestamp when the venue publishes them. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------- | -------------- | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Perpetual instrument for the rate. | | `rate` | `Decimal` | `Decimal` | Required | Current funding rate. | | `interval` | `Option` | `int \| None` | `None` | Funding interval in minutes. | | `next_funding_ns` | `Option` | `int \| None` | `None` | Next funding timestamp in nanoseconds. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## Behavior - Funding rates are cached by instrument when received. - Equality and hashing use instrument ID, rate, interval, and next funding time. - Funding rates are reference data and do not imply a payment was applied. - Use `interval` and `next_funding_ns` only when the venue publishes them. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{data::FundingRateUpdate, identifiers::InstrumentId}; use rust_decimal::Decimal; let funding = FundingRateUpdate::new( InstrumentId::from("BTCUSDT-PERP.BINANCE"), Decimal::new(1, 4), Some(480), Some(UnixNanos::from(1_000_008_000)), UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); ``` ```python tab="Python" from decimal import Decimal from nautilus_trader.model import FundingRateUpdate from nautilus_trader.model import InstrumentId funding = FundingRateUpdate( instrument_id=InstrumentId.from_str("BTCUSDT-PERP.BINANCE"), rate=Decimal("0.0001"), ts_event=1_000_000_000, ts_init=1_000_000_100, interval=480, next_funding_ns=1_000_008_000, ) ``` ## Related guides - [MarkPriceUpdate](mark_price_update.md) covers mark prices for derivatives. - [IndexPriceUpdate](index_price_update.md) covers index reference prices. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # Data Source: https://nautilustrader.io/docs/latest/concepts/data/ NautilusTrader supports granular order book data, quotes, trades, bars, reference prices, and custom data. This overview links to the built-in types and explains the concepts shared across backtesting, sandbox, and live environments. ## Built-in data types Each main built-in market data type has a dedicated guide to its fields, behavior, and construction. | Data type | Category | Description | | --------------------------------------------- | -------------------- | ---------------------------------------------------- | | [`OrderBookDelta`](order_book_delta.md) | Order book | Single incremental order book change. | | [`OrderBookDeltas`](order_book_deltas.md) | Order book | Batch of related order book deltas. | | [`OrderBookDepth10`](order_book_depth10.md) | Order book | Fixed top 10 bid and ask levels. | | [`QuoteTick`](quote_tick.md) | Top-of-book | Best bid and ask prices and sizes. | | [`TradeTick`](trade_tick.md) | Trades | Single venue trade or match event. | | [`Bar`](bar.md) | Aggregation | OHLCV bar for a specific `BarType`. | | [`MarkPriceUpdate`](mark_price_update.md) | Derivative reference | Mark price for a derivatives instrument. | | [`IndexPriceUpdate`](index_price_update.md) | Derivative reference | Index price used by a derivatives market. | | [`FundingRateUpdate`](funding_rate_update.md) | Derivative reference | Funding rate and next funding metadata. | | [`OptionGreeks`](option_greeks.md) | Options | Venue-provided Greeks and implied volatility. | | [`InstrumentStatus`](instrument_status.md) | Instrument event | Trading, quoting, and halt status changes. | | [`InstrumentClose`](instrument_close.md) | Instrument event | Close, settlement, or other venue close price event. | When data flows over the message bus, topic-addressable data stays under the `data` root. Live streams use `data....`; the data pipeline path uses `data.pipeline....`. See [Message Bus](../message_bus.md#topic-hierarchy) for the topic hierarchy. ## Order books A Rust `OrderBook` maintains state for one instrument in backtesting and live trading. NautilusTrader supports these book types: - `L3_MBO`: Level 3 market-by-order (MBO) data, keyed by order ID at every price level. - `L2_MBP`: Level 2 market-by-price (MBP) data, aggregated by price level. - `L1_MBP`: Level 1 market-by-price (MBP) top-of-book data, also known as best bid and offer (BBO). Quote, trade, and bar data (`QuoteTick`, `TradeTick`, and `Bar`) can also drive `L1_MBP` books in backtests. ### Delta flags and event boundaries Each `OrderBookDelta` carries a `flags` field using `RecordFlag` bitmask values to signal event boundaries to the `DataEngine`: - `F_LAST`: Marks the final delta in a logical event group. When `buffer_deltas` is enabled, the `DataEngine` accumulates deltas and only publishes to subscribers when it encounters `F_LAST`. Every event group **must** end with a delta that has `F_LAST` set. - `F_SNAPSHOT`: Marks deltas that belong to a snapshot (as opposed to an incremental update). Snapshot sequences begin with a `Clear` action followed by `Add` deltas reconstructing the full book state. The last delta in a snapshot has both `F_SNAPSHOT | F_LAST` set. :::warning[Missing F_LAST stalls buffered consumers] A missing `F_LAST` on the final delta in an event group causes buffered consumers to accumulate deltas indefinitely without publishing. This applies to incremental updates and snapshots alike, including empty book snapshots where only a `Clear` delta is emitted. ::: ## Instruments All market data belongs to an instrument. The instrument definition supplies the identity, precision, price and size increments, limits, currencies, and contract semantics that make the data meaningful. See [Instruments](../instruments/) for the instrument taxonomy and per-type guides. ## Bars and aggregation ### Introduction to bars A **bar**, also known as a candle, candlestick, or kline, summarizes price and volume over an interval: - Opening price - Highest price - Lowest price - Closing price - Traded volume (or ticks as a volume proxy) An **aggregation method** defines how NautilusTrader groups input data into bars. ### Purpose of data aggregation Aggregation converts granular market data into bars that: - Supply inputs for technical indicators and strategies. - Match the time resolution a strategy needs. - Reduce storage and processing compared with high-frequency order book data. ### Aggregation methods NautilusTrader supports these aggregation methods: | Name | Description | Category | | :----------------- | :-------------------------------------------------------- | :---------- | | `TICK` | Number of ticks. | Threshold | | `TICK_IMBALANCE` | Buy/sell imbalance of ticks. | Threshold | | `TICK_RUNS` | Sequential buy/sell runs of ticks. | Information | | `VOLUME` | Traded volume. | Threshold | | `VOLUME_IMBALANCE` | Buy/sell imbalance of traded volume. | Threshold | | `VOLUME_RUNS` | Sequential buy/sell runs of traded volume. | Information | | `VALUE` | Notional trade value, also known as dollar bars. | Threshold | | `VALUE_IMBALANCE` | Buy/sell imbalance of notional trade value. | Threshold | | `VALUE_RUNS` | Sequential buy/sell runs of notional trade value. | Information | | `RENKO` | Fixed price movements, with brick size measured in ticks. | Price | | `MILLISECOND` | Time intervals with millisecond granularity. | Time | | `SECOND` | Time intervals with second granularity. | Time | | `MINUTE` | Time intervals with minute granularity. | Time | | `HOUR` | Time intervals with hour granularity. | Time | | `DAY` | Time intervals with day granularity. | Time | | `WEEK` | Time intervals with week granularity. | Time | | `MONTH` | Time intervals with month granularity. | Time | | `YEAR` | Time intervals with year granularity. | Time | The threshold, information, and time categories follow the `BarSpecification` predicates. `RENKO` is price-driven and has no matching predicate. The broader information-driven concept below includes both imbalance and runs bars. ### Information-driven bars Information-driven bars adapt their sampling frequency to market activity rather than using fixed intervals. They are based on the concept of **aggressor side** (whether the trade initiator was a buyer or seller) and come in two families: **imbalance** and **runs**. **Imbalance bars** close when the *net* buy/sell activity reaches a threshold. Each trade contributes a signed value: positive for buyer-initiated trades and negative for seller-initiated trades. The bar closes when the absolute imbalance reaches the configured step. This means that opposing trades cancel each other out, so imbalance bars form more slowly in balanced markets and faster during directional moves. **Runs bars** close when *consecutive* activity from the same aggressor side reaches a threshold. Unlike imbalance bars, runs bars reset their counter when the aggressor side changes. This makes them sensitive to sustained one-sided pressure rather than net imbalance. Both families have three variants based on what is measured: | Variant | Imbalance | Runs | What is measured | | :------ | :----------------- | :------------ | :---------------------------- | | Tick | `TICK_IMBALANCE` | `TICK_RUNS` | Number of trades. | | Volume | `VOLUME_IMBALANCE` | `VOLUME_RUNS` | Traded quantity. | | Value | `VALUE_IMBALANCE` | `VALUE_RUNS` | Price multiplied by quantity. | Information-driven bars require `TradeTick` data because they need the `aggressor_side` field to classify each trade. They cannot be aggregated from `QuoteTick` data alone. ### Types of aggregation NautilusTrader supports three aggregation inputs: | Input | Result | Price type | Syntax requirement | | ------------- | ----------------------------------------- | ---------------------- | ------------------- | | `TradeTick` | Trade-to-bar aggregation. | `LAST` | No `@` source. | | `QuoteTick` | Quote-to-bar aggregation. | `BID`, `ASK`, or `MID` | No `@` source. | | Smaller `Bar` | Bar-to-bar aggregation into a larger bar. | Target bar price type. | Source follows `@`. | ### Bar types `BarType` identifies a bar by: - **Instrument ID** (`InstrumentId`): The instrument for the bar. - **Bar specification** (`BarSpecification`): - `step`: The interval or frequency. - `aggregation`: The aggregation method. - `price_type`: The price basis, such as bid, ask, mid, or last. - **Aggregation source** (`AggregationSource`): Whether NautilusTrader or an external venue or data provider aggregated the bar. The Rust/PyO3 `BarSpecification` validates fixed-subunit time aggregations so bars align cleanly with their parent clock or calendar unit: - `MILLISECOND` steps must divide 1000 and be less than 1000. - `SECOND` and `MINUTE` steps must divide 60 and be less than 60. - `HOUR` steps must divide 24 and be less than 24. - `MONTH` steps must divide 12 and may equal 12. Except for `12-MONTH`, use the next larger aggregation when the step equals a parent unit, such as `1-HOUR` instead of `60-MINUTE`. In this model, `DAY`, `WEEK`, `YEAR`, threshold, information-driven, and `RENKO` bars are not restricted by this fixed-subunit rule. Time aggregations must also convert to a duration and nanosecond interval, so an oversized `DAY`, `WEEK`, or `YEAR` step is rejected. Bar types can also be classified as either *standard* or *composite*: - **Standard**: Generated from granular market data, such as quote ticks or trade ticks. - **Composite**: Derived from a finer-grained bar type, such as 5-minute bars aggregated from 1-minute bars. ### Aggregation sources Bar data aggregation can be either *internal* or *external*: - `INTERNAL`: NautilusTrader aggregates the bar. - `EXTERNAL`: A venue or data provider aggregates the bar. For bar-to-bar aggregation, the target is always `INTERNAL`. The source can be `INTERNAL` or `EXTERNAL`. ### Defining bar types with string syntax #### Standard bars Define a standard bar type with: `{instrument_id}-{step}-{aggregation}-{price_type}-{INTERNAL | EXTERNAL}` This example defines 5-minute AAPL trade bars that NautilusTrader aggregates locally: ```python bar_type = BarType.from_str("AAPL.XNAS-5-MINUTE-LAST-INTERNAL") ``` #### Composite bars Define a composite bar type with: `{instrument_id}-{step}-{aggregation}-{price_type}-INTERNAL@{step}-{aggregation}-{INTERNAL | EXTERNAL}` - The derived bar type must use an `INTERNAL` aggregation source (since this is how the bar is aggregated). - The sampled bar type must be finer-grained than the derived bar type. - The sampled instrument ID is inferred to match that of the derived bar type. - Composite bars can be aggregated *from* `INTERNAL` or `EXTERNAL` aggregation sources. This example defines internal 5-minute AAPL trade bars aggregated from external 1-minute bars: ```python bar_type = BarType.from_str("AAPL.XNAS-5-MINUTE-LAST-INTERNAL@1-MINUTE-EXTERNAL") ``` ### Aggregation syntax examples The `BarType` string format encodes both the target bar type and, optionally, the source data type: ```text {instrument_id}-{step}-{aggregation}-{price_type}-{source}@{step}-{aggregation}-{source} ``` The part after `@` applies only to bar-to-bar aggregation: - **Without `@`**: Aggregate from `TradeTick` objects for `LAST`, or `QuoteTick` objects for `BID`, `ASK`, or `MID`. - **With `@`**: Aggregate from existing `Bar` objects of the specified source type. #### Trade-to-bar example ```python def on_start(self) -> None: # LAST selects TradeTick data as the source bar_type = BarType.from_str("6EH4.XCME-50-VOLUME-LAST-INTERNAL") start = self.clock.utc_now() - timedelta(days=30) # Deliver historical bars to on_historical_bars self.request_bars(bar_type, start=start) # Deliver live bars to on_bar self.subscribe_bars(bar_type) ``` #### Quote-to-bar example ```python def on_start(self) -> None: # Create 1-minute bars from QuoteTick ask prices bar_type_ask = BarType.from_str("6EH4.XCME-1-MINUTE-ASK-INTERNAL") # Create 1-minute bars from QuoteTick bid prices bar_type_bid = BarType.from_str("6EH4.XCME-1-MINUTE-BID-INTERNAL") # Create 1-minute bars from QuoteTick mid prices bar_type_mid = BarType.from_str("6EH4.XCME-1-MINUTE-MID-INTERNAL") start = self.clock.utc_now() - timedelta(days=30) self.request_bars(bar_type_ask, start=start) self.subscribe_bars(bar_type_ask) ``` #### Bar-to-bar example ```python def on_start(self) -> None: # Create 5-minute bars from 1-minute Bar objects # Format: target_bar_type@source_bar_type # The price type appears only on the target side bar_type = BarType.from_str("6EH4.XCME-5-MINUTE-LAST-INTERNAL@1-MINUTE-EXTERNAL") start = self.clock.utc_now() - timedelta(days=30) self.request_bars(bar_type, start=start) # Deliver live updates to on_bar self.subscribe_bars(bar_type) ``` #### Advanced bar-to-bar example Build longer aggregation chains from bars that NautilusTrader has already aggregated: ```python # Create 1-minute bars from TradeTick objects primary_bar_type = BarType.from_str("6EH4.XCME-1-MINUTE-LAST-INTERNAL") # Create 5-minute bars from the 1-minute bars intermediate_bar_type = BarType.from_str("6EH4.XCME-5-MINUTE-LAST-INTERNAL@1-MINUTE-INTERNAL") # Create hourly bars from the 5-minute bars hourly_bar_type = BarType.from_str("6EH4.XCME-1-HOUR-LAST-INTERNAL@5-MINUTE-INTERNAL") ``` ### Working with bars: request vs. subscribe NautilusTrader provides two operations for working with bars: | Method | Purpose | Delivery handler | | ------------------ | ----------------------- | ---------------------- | | `request_bars()` | Fetch historical bars. | `on_historical_bars()` | | `subscribe_bars()` | Subscribe to live bars. | `on_bar()` | `subscribe_bars()` expects the instrument for the `BarType` in the cache. The same precondition applies to other live market data subscriptions. These methods work together in a typical workflow: 1. `request_bars()` loads historical data to initialize indicators or strategy state. 1. `subscribe_bars()` continues the stream with live bars. The request returns a correlation ID. Historical data arrives through `on_historical_bars()` as a `Sequence[Bar]`; live data arrives through `on_bar()` one bar at a time. ```python from collections.abc import Sequence def on_start(self) -> None: bar_type = BarType.from_str("6EH4.XCME-5-MINUTE-LAST-INTERNAL") start = self.clock.utc_now() - timedelta(days=30) # Register indicators before requesting history self.register_indicator_for_bars(bar_type, self.my_indicator) self.request_bars(bar_type, start=start) self.subscribe_bars(bar_type) def on_historical_bars(self, bars: Sequence[Bar]) -> None: for bar in bars: self.log.info(f"Historical bar: {bar}") def on_bar(self, bar): # Process individual bars from subscribe_bars() pass ``` ### Register indicators before requesting data Register indicators before requesting historical data so they receive those updates. ```python start = self.clock.utc_now() - timedelta(days=30) # Correct order self.register_indicator_for_bars(bar_type, self.ema) self.request_bars(bar_type, start=start) # Incorrect order: the indicator misses historical updates self.request_bars(bar_type, start=start) self.register_indicator_for_bars(bar_type, self.ema) ``` ### Performance considerations Bar aggregators track OHLC prices with the fixed-point `Price` type. The aggregation method determines the additional work for each update: - **Time bars** accumulate OHLCV state per update and use a timer to emit bars. - **Threshold bars** (tick, volume, value) add a counter or accumulator check per update. Volume and value bars may split a single large trade across multiple bars when it exceeds the remaining threshold. - **Information-driven bars** (imbalance, runs) track aggressor side and signed accumulation. - **Renko bars** are price-driven and can emit several bars from one large price move. - **Composite bars** process an aggregated source bar instead of each underlying tick. ### Time bar configuration Time bar behavior is controlled through `DataEngineConfig`. The following options apply to all time-based aggregation from milliseconds through years: | Option | Type | Default | Description | | :---------------------------------- | :-------------------------- | :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `time_bars_interval_type` | `BarIntervalType` or `str` | `LEFT_OPEN` | `LEFT_OPEN`: start excluded, end included. `RIGHT_OPEN`: start included, end excluded. The Python constructor also accepts `"left-open"` and `"right-open"`. | | `time_bars_timestamp_on_close` | `bool` | `True` | When `True`, `ts_event` is the bar close time. When `False`, `ts_event` is the bar open time. | | `time_bars_skip_first_non_full_bar` | `bool` | `False` | Skip emitting a bar when aggregation starts mid-interval, avoiding partial bars on startup. | | `time_bars_build_with_no_updates` | `bool` | `True` | When `True`, bars are emitted even if no market updates arrived during the interval. | | `time_bars_origin_offset` | `dict[BarAggregation, int]` | `{}` | Maps aggregation types to nanosecond offsets that shift bar alignment. | | `time_bars_build_delay` | `int` | `0` | Delay in microseconds before building a bar. Useful in backtests to ensure data at bar boundary timestamps is processed before the timer fires. | For example, an offset of `34_200_000_000_000` nanoseconds for `BarAggregation.DAY` aligns daily bar boundaries to 09:30 UTC. ```python from nautilus_trader.config import DataEngineConfig config = DataEngineConfig( time_bars_timestamp_on_close=True, time_bars_build_with_no_updates=False, time_bars_skip_first_non_full_bar=True, ) ``` ## Timestamps Many market data, order, and event objects carry two timestamps: - `ts_event`: UNIX timestamp in nanoseconds when the event occurred. - `ts_init`: UNIX timestamp in nanoseconds when NautilusTrader initialized the object. ### Typical meanings | Event type | `ts_event` | `ts_init` | | ----------------- | -------------------------------------- | ------------------------------------------------ | | `TradeTick` | Trade time at the venue. | Local object initialization time. | | `QuoteTick` | Quote time at the venue. | Local object initialization time. | | `OrderBookDelta` | Book update time at the venue. | Local object initialization time. | | `Bar` | Configured bar open or close boundary. | Local aggregation or object initialization time. | | `DefiData` | Block or pool event time. | Object initialization time from the chain data. | | `OrderFilled` | Fill time at the venue. | Local fill event initialization time. | | `OrderCanceled` | Cancellation time at the venue. | Local cancellation event initialization time. | | Custom news event | Publication time. | Local object initialization time. | | Custom event | Time defined by the custom event. | Local object initialization time. | :::note `ts_init` means initialization time, not always receipt time. Commands and internally generated events also use it even though NautilusTrader does not receive them from an external source. ::: ### Latency analysis The difference `ts_init - ts_event` measures observed delay only when the clocks that produced both timestamps are synchronized. Otherwise, the result also includes clock offset and cannot represent system latency by itself. ### Environment-specific behavior #### Backtesting environment - Data is ordered by `ts_init` using a stable sort. - DeFi data (`DefiData`) breaks `ts_init` ties by on-chain position (block number, transaction index, log index) so events from the same block replay in canonical chain order. - This ordering gives backtests deterministic replay. #### Live trading environment Live trading processes data as it arrives. For venue-sourced data, `ts_event` records the external event time, while `ts_init` usually records local object initialization after receipt. ### Other notes and considerations - For data from external sources, `ts_init` is usually the local receipt or normalization time, but clock skew means it is not guaranteed to be greater than or equal to `ts_event`. - For data created within NautilusTrader, `ts_init` and `ts_event` can match. - Some types with `ts_init` do not have `ts_event` because: - The initialization of an object happens at the same time as the event itself. - The concept of an external event time does not apply. #### Persisted data The `ts_init` field preserves the original initialization timestamp. For venue data this is typically receipt time; for internally created data it is the creation time of that object. ## Data flow From the `DataEngine` onward, data follows the same pathway regardless of [environment context](../architecture.md#environment-contexts) (backtest, sandbox, live). In live and sandbox modes a venue adapter creates a normalized data object and sends it through a channel; in backtests the engine feeds data directly. Either way the `DataEngine` stores it in the `Cache` (for cached types) and publishes it on the `MessageBus` to subscribed handlers. For a step-by-step trace with a sequence diagram, see [Data flow: life of a quote tick](../architecture.md#data-flow-life-of-a-quote-tick). See [Custom data](#custom-data) to define and publish another data type. ## Loading data Convert external records into NautilusTrader model objects before adding them to a backtest or writing them to a catalog. The conversion path depends on the source: - Use an adapter loader when the repository provides one for that source format. - Construct the target model type directly when working from normalized rows or a DataFrame. - Use the PyO3 data wranglers for Arrow IPC streams that already follow the NautilusTrader schema. ### Data loaders Data loaders are specific to a source format. For example, Binance order book CSV data differs from [Databento Binary Encoding (DBN)](https://databento.com/docs/knowledge-base/new-users/dbn-encoding/getting-started-with-dbn). For example, `load_binance_order_book_deltas(...)` reads Binance depth CSV files into a normalized DataFrame. Convert those rows into `OrderBookDelta` objects with the instrument's price and size precision. See the [tutorials](../../tutorials/) for complete catalog and backtest workflows. ### Arrow IPC wranglers The PyO3 persistence module provides these wranglers for schema-compatible Arrow IPC streams: | Wrangler | Constructor identity | Return type | | ------------------------------ | ---------------------------------- | ------------------------ | | `OrderBookDeltaDataWrangler` | Instrument ID and both precisions. | `list[OrderBookDelta]` | | `OrderBookDepth10DataWrangler` | Instrument ID and both precisions. | `list[OrderBookDepth10]` | | `QuoteTickDataWrangler` | Instrument ID and both precisions. | `list[QuoteTick]` | | `TradeTickDataWrangler` | Instrument ID and both precisions. | `list[TradeTick]` | | `BarDataWrangler` | Bar type and both precisions. | `list[Bar]` | Each constructor takes the identity as a string, followed by `price_precision` and `size_precision`. Pass the complete Arrow IPC stream as `bytes` to `process_record_batch_bytes(...)`. ### Fixed-point precision and raw values NautilusTrader uses fixed-point arithmetic for `Price` and `Quantity`. Raw values must match the scale for their declared precision. #### Raw value requirements When constructing `Price` or `Quantity` with `from_raw()`, use a raw value from: - The `.raw` field of an existing value, such as `price.raw`. - NautilusTrader fixed-point conversion functions. - Values from Nautilus-produced Arrow data. :::warning[Unvalidated raw values] For a precision below `FIXED_PRECISION`, the raw value must be divisible by `10^(FIXED_PRECISION - precision)`. Construction does not reject an invalid multiple, which can produce an incorrect value. ::: #### Legacy raw value correction Older catalog writers could introduce floating-point errors by calculating raw values with `int(value * FIXED_SCALAR)`. Arrow decoding corrects affected price and quantity values to the nearest valid scale multiple for their precision while leaving sentinel values unchanged. These catalogs therefore remain readable without migration. The correction adds a small amount of work during Arrow decoding. ### Transformation pipeline 1. A source-specific loader reads raw data. 1. The conversion normalizes field names, timestamps, enums, and precision. 1. Model constructors validate and create NautilusTrader data objects. 1. The application passes those objects to a backtest or catalog writer. ```mermaid flowchart LR raw["Raw source data"] loader["Source loader"] normalize["Normalize fields and precision"] model["NautilusTrader model objects"] consumer["Backtest or catalog"] raw --> loader loader --> normalize normalize --> model model --> consumer ``` The conversion must preserve exact price and quantity values. Build `Price` and `Quantity` from decimal or string input rather than routing discrete values through binary floating point. ## Data catalog The data catalog stores NautilusTrader data in [Parquet](https://parquet.apache.org) files for backtesting, live trading, and research. ### Overview and architecture `ParquetDataCatalog` is the Python interface to the Rust catalog and DataFusion query engine. The Rust model and persistence crates define the Arrow schemas for built-in data. Registered custom data supplies its schema and encode/decode handlers at runtime. Parquet provides compressed columnar storage and cross-language access. The catalog stores these files under one root without requiring a separate database service. A local path or object-store URI selects the storage backend. ### Initializing Pass a local path or URI as the first constructor argument: ```python from pathlib import Path from nautilus_trader.persistence import ParquetDataCatalog CATALOG_PATH = Path.cwd() / "catalog" catalog = ParquetDataCatalog(str(CATALOG_PATH)) ``` ### Filesystem protocols and storage options The catalog accepts the storage protocols supported by its Rust object-store backend. #### Supported filesystem protocols | Storage | URI schemes | Common option keys | | -------------------- | ------------------ | ------------------------------------------------------------------------- | | Local filesystem | Plain path, `file` | None. | | Amazon S3 | `s3` | `region`, `access_key_id`, `secret_access_key`, `endpoint_url`. | | Google Cloud Storage | `gs`, `gcs` | `service_account_path`, `service_account_key`, `application_credentials`. | | Azure Blob Storage | `az`, `abfs` | `account_name`, `account_key`, `sas_token`. | | HTTP or WebDAV | `http`, `https` | None. | Pass credentials and other backend settings through `storage_options`: ```python catalog = ParquetDataCatalog( "s3://my-bucket/nautilus-data/", storage_options={ "access_key_id": "your-key", "secret_access_key": "your-secret", "region": "us-east-1", }, ) azure_catalog = ParquetDataCatalog( "abfs://container@account.dfs.core.windows.net/nautilus-data/", storage_options={"account_key": "your-account-key"}, ) ``` ### Writing data Use the writer for the concrete data type. Instrument definitions and custom data have separate writers. ```python catalog.write_instruments([instrument]) catalog.write_quote_ticks(quote_ticks) catalog.write_trade_ticks( trade_ticks, start=1704067200000000000, end=1704153600000000000, ) catalog.write_bars(bars, skip_disjoint_check=True) ``` The built-in market-data writers are: - `write_quote_ticks` - `write_trade_ticks` - `write_order_book_deltas` - `write_order_book_depths` - `write_bars` - `write_mark_price_updates` - `write_index_price_updates` - `write_option_greeks` Each writer accepts optional `start` and `end` overrides as UNIX nanoseconds. The data in one call must have one identity, such as one instrument ID or bar type, and must be ordered by `ts_init`. ### File naming and data organization The catalog names files from their timestamp range with the pattern `{start_timestamp}_{end_timestamp}.parquet`. It converts each ISO 8601 timestamp to a filename-safe form by replacing `:` and `.` with `-`. Built-in data is organized in directories by data type and identifier. For instrument IDs and bar types, the catalog removes `/` and replaces `^` with `_` when creating the URI-safe directory name: ```text catalog/ ├── data/ │ ├── quotes/ │ │ └── EURUSD.SIM/ │ │ └── 2024-01-01T00-00-00-000000000Z_2024-01-01T23-59-59-999999999Z.parquet │ └── trades/ │ └── BTCUSD.BINANCE/ │ └── 2024-01-01T00-00-00-000000000Z_2024-01-01T23-59-59-999999999Z.parquet ``` Custom data uses `data/custom//` with optional identifier path segments. :::warning[Overlapping writes] By default, overlapping writes raise an `OSError` to maintain data integrity. Set `skip_disjoint_check=True` only when the overlap is intentional. ::: ### Reading data Use a typed query when the expected return type is known. `start` and `end` are UNIX nanoseconds: ```python quotes = catalog.query_quote_ticks( identifiers=["EUR/USD.SIM"], start=1704067200000000000, end=1704153600000000000, ) trades = catalog.query_trade_ticks( identifiers=["BTC/USD.BINANCE"], start=1704067200000000000, end=1704153600000000000, ) ``` ### `BacktestDataConfig`: backtest data `BacktestDataConfig` defines the catalog data that a `BacktestNode` loads for one run. #### Core parameters - `data_type` is one of `QuoteTick`, `TradeTick`, `Bar`, `OrderBookDelta`, `OrderBookDepth10`, `MarkPriceUpdate`, `IndexPriceUpdate`, `FundingRateUpdate`, `InstrumentStatus`, `OptionGreeks`, or `InstrumentClose`. - `catalog_path` identifies the catalog root. - One of `instrument_id`, `instrument_ids`, or `bar_types` is required. - `start_time` and `end_time` are optional UNIX nanosecond bounds. - `filter_expr` is an optional DataFusion SQL predicate. - `catalog_fs_protocol` prefixes `catalog_path` for remote storage. - `catalog_fs_rust_storage_options` supplies the Rust backend options. If it is unset, `BacktestNode` falls back to `catalog_fs_storage_options`. - For bars, `bar_spec` combines with the instrument ID to select an `EXTERNAL` bar type. Explicit `bar_types` can select internal, external, or composite bars. - `optimize_file_loading` registers whole directories when possible. #### Basic usage examples ```python from nautilus_trader.config import BacktestDataConfig from nautilus_trader.model import BarAggregation from nautilus_trader.model import BarSpecification from nautilus_trader.model import InstrumentId from nautilus_trader.model import PriceType quote_data = BacktestDataConfig( data_type="QuoteTick", catalog_path="/path/to/catalog", instrument_id=InstrumentId.from_str("EUR/USD.SIM"), start_time=1704067200000000000, end_time=1704153600000000000, ) trade_data = BacktestDataConfig( data_type="TradeTick", catalog_path="/path/to/catalog", instrument_ids=[ InstrumentId.from_str("BTC/USD.BINANCE"), InstrumentId.from_str("ETH/USD.BINANCE"), ], ) bar_data = BacktestDataConfig( data_type="Bar", catalog_path="/path/to/catalog", instrument_id=InstrumentId.from_str("AAPL.NASDAQ"), bar_spec=BarSpecification(5, BarAggregation.MINUTE, PriceType.LAST), ) ``` This bar config selects `AAPL.NASDAQ-5-MINUTE-LAST-EXTERNAL`. #### Cloud storage and filtering ```python book_data = BacktestDataConfig( data_type="OrderBookDelta", catalog_path="my-bucket/nautilus-data", catalog_fs_protocol="s3", catalog_fs_rust_storage_options={ "access_key_id": "your-access-key", "secret_access_key": "your-secret-key", "region": "us-east-1", }, instrument_id=InstrumentId.from_str("BTC/USD.COINBASE"), filter_expr="ts_init >= 1704067200000000000", ) ``` #### Integration with BacktestRunConfig Pass the data configurations to `BacktestRunConfig`: ```python from nautilus_trader.config import BacktestDataConfig from nautilus_trader.config import BacktestRunConfig from nautilus_trader.config import BacktestVenueConfig from nautilus_trader.model import AccountType from nautilus_trader.model import BookType from nautilus_trader.model import InstrumentId from nautilus_trader.model import OmsType data_configs = [ BacktestDataConfig( data_type="QuoteTick", catalog_path="/path/to/catalog", instrument_id=InstrumentId.from_str("EUR/USD.SIM"), ), ] run_config = BacktestRunConfig( venues=[ BacktestVenueConfig( name="SIM", oms_type=OmsType.HEDGING, account_type=AccountType.MARGIN, book_type=BookType.L1_MBP, starting_balances=["1_000_000 USD"], ), ], data=data_configs, start=1704067200000000000, end=1704153600000000000, ) ``` #### Data loading process When a backtest runs, the `BacktestNode` processes each `BacktestDataConfig`: 1. Create a `ParquetDataCatalog` from the configuration. 1. Load the required instrument definitions while building the engine. 1. Build and run a DataFusion query from the configuration fields. 1. Sort merged data by `ts_init` and add it to the backtest engine. ### Direct catalog access Use `ParquetDataCatalog` to query or write a catalog directly. Use `BacktestDataConfig` when a `BacktestNode` should load catalog data for a run. `LiveNodeConfig` has no counterpart for loading catalog data; request historical data through a configured data client or query the catalog directly. Its `streaming` field configures feather writing only. ### Querying and filtering The generic query takes a catalog directory name such as `quotes`, `trades`, or `bars`. Use it when you need the `files` or `optimize_file_loading` controls: ```python catalog.query( data_type="quotes", identifiers=["EUR/USD.SIM"], start=1704067200000000000, end=1704153600000000000, where_clause="ts_event <= ts_init", files=None, ) ``` Typed methods such as `query_quote_ticks`, `query_trade_ticks`, and `query_bars` return the concrete model type. `query_custom_data` resolves custom decoders through the runtime registry. `query`, the typed market-data query methods, and `query_custom_data` use UNIX nanosecond time bounds and accept a DataFusion SQL `where_clause`. :::warning[Time-zone database mismatch] With the current `Cargo.lock`, DataFusion SQL temporal functions resolve named time zones with the transitive `chrono-tz` 0.10.4 database (IANA 2025b). Rust core time-zone operations use Jiff 0.2.35 with its bundled IANA 2026c database. Zone results can differ when zone rules change or historical data is corrected after 2025b until DataFusion migrates. If RustSec files unmaintained advisories for `chrono` or `chrono-tz`, maintain matching documented ignores in `.cargo/audit.toml` and `deny.toml` until DataFusion migrates. ::: ### Catalog operations Catalog operations rename, consolidate, or delete data files. #### Reset file names Reset Parquet file names to match their content timestamps so filename-based filtering remains accurate. `reset_all_file_names()` processes the entire catalog; `reset_data_file_names(...)` targets a data path. Supply an instrument ID for data types partitioned by instrument. Without one, the operation recursively reads the type directory and moves the renamed files into that directory. ```python catalog.reset_all_file_names() catalog.reset_data_file_names("quotes", "EUR/USD.SIM") catalog.reset_data_file_names("trades", "BTC/USD.BINANCE") ``` #### Consolidate catalog Combine small Parquet files to reduce file count and query overhead. With no bounds, `consolidate_catalog()` processes each leaf data directory in the catalog. `consolidate_data(...)` operates on one directory; supply an instrument ID for data types partitioned by instrument. ```python catalog.consolidate_catalog() catalog.consolidate_catalog( start=1704067200000000000, end=1704153600000000000, ensure_contiguous_files=True, ) catalog.consolidate_data( "quotes", instrument_id="EUR/USD.SIM", start=1704067200000000000, end=1706745600000000000, ) ``` #### Consolidate catalog by period Split data files into fixed periods. Durations and time bounds use nanoseconds. Both methods accept optional bounds. Supply an identifier to the data-type method for data partitioned by instrument. The catalog-wide method processes quotes, trades, order book deltas, order book depths, bars, index prices, mark prices, instrument closes, and registered custom types. It logs a warning and skips other types. ```python DAY_NS = 86_400_000_000_000 HOUR_NS = 3_600_000_000_000 catalog.consolidate_catalog_by_period(period_nanos=DAY_NS) catalog.consolidate_catalog_by_period( period_nanos=HOUR_NS, start=1704067200000000000, end=1704153600000000000, ) catalog.consolidate_data_by_period( type_name="quotes", identifier="EUR/USD.SIM", period_nanos=HOUR_NS, ) catalog.consolidate_data_by_period( type_name="trades", identifier="EUR/USD.SIM", period_nanos=HOUR_NS, start=1704067200000000000, end=1706745600000000000, ) ``` #### Delete data range Delete data within a time range, optionally limited to one data type and instrument. Omitting `start` extends the range to the beginning; omitting `end` extends it to the end. For `delete_data_range(...)`, omitting both bounds removes all matching data. Supply an instrument ID for data partitioned by instrument. `delete_data_range(...)` supports quotes, trades, bars, order book deltas, order book depth 10, and registered custom types. Pass `order_book_depth10` for order book depth 10 and `custom/` for custom data, such as `custom/MarketTickPython`. `delete_catalog_range(...)` continues after unsupported directories, logs a warning, and leaves their data unchanged. It also skips order book depth directories because their stored path name differs from the direct method's type name. Use `delete_data_range(...)` when you need to confirm that the requested type is supported. ```python catalog.delete_catalog_range( start=1704067200000000000, end=1704153600000000000, ) catalog.delete_catalog_range(end=1704067200000000000) catalog.delete_data_range( type_name="quotes", instrument_id="BTC/USD.BINANCE", ) catalog.delete_data_range( type_name="trades", instrument_id="EUR/USD.SIM", start=1704067200000000000, end=1706745600000000000, ) ``` :::danger[Permanent data removal] Delete operations cannot be undone. The catalog splits partially overlapping files to preserve data outside the range. ::: ### Feather streaming and conversion The Python API exposes `StreamingFeatherWriter` for direct streaming and accepts `StreamingConfig` through `BacktestEngineConfig` when running a `BacktestNode`. The node owns the writer lifecycle and writes each run below `/backtest/`. Use `ParquetDataCatalog.convert_stream_to_data()` to convert a completed Feather stream to Parquet. ## Data migrations The `nautilus_model` crate defines the internal data format. NautilusTrader serializes these models as Arrow record batches and stores them in Parquet files. Use the migration utilities when changing [precision modes](../../getting_started/installation.md#precision-mode) or schemas. ### Migration tools The `nautilus_persistence` crate provides two utilities: #### `to-json` `to-json` converts Parquet files to JSON and preserves their metadata: - Creates two files: - `.json`: Deserialized data. - `.metadata.json`: Schema metadata and row group configuration. - Automatically detects data type from filename: - `OrderBookDelta`: File name contains `deltas` or `order_book_delta`. - `QuoteTick`: File name contains `quotes` or `quote_tick`. - `TradeTick`: File name contains `trades` or `trade_tick`. - `Bar`: File name contains `bars`. #### `to-parquet` `to-parquet` converts JSON back to Parquet: - Reads both the data JSON and metadata JSON files. - Preserves row group sizes from original metadata. - Uses ZSTD compression. - Creates `.parquet`. ### Migration process These examples use trade data. Run each command from `crates/persistence`. #### Migrating from standard-precision (64-bit) to high-precision (128-bit) Convert a standard-precision schema to a high-precision schema: :::note For catalogs that used the `Int64` and `UInt64` Arrow data types for prices and sizes, build the initial `to-json` conversion from [commit `e284162`](https://github.com/nautechsystems/nautilus_trader/commit/e284162cf27a3222115aeb5d10d599c8cf09cf50). ::: 1. Convert standard-precision Parquet to JSON: ```bash cargo run --features python --bin to-json -- trades.parquet ``` This creates `trades.json` and `trades.metadata.json`. 1. Convert the JSON to high-precision Parquet: ```bash cargo run --features "python high-precision" --bin to-parquet -- trades.json ``` This creates `trades.parquet` with the high-precision schema. #### Migrating schema changes Convert data from one schema version to another: 1. Convert the old-schema Parquet file to JSON: For a high-precision source, replace `--features python` with `--features "python high-precision"`. ```bash cargo run --features python --bin to-json -- trades.parquet ``` This creates `trades.json` and `trades.metadata.json`. 1. Switch to the new schema version: ```bash git checkout ``` 1. Convert the JSON to Parquet with the new schema: ```bash cargo run --features "python high-precision" --bin to-parquet -- trades.json ``` This creates `trades.parquet` with the new schema. ### Best practices - Test migrations with a small dataset first. - Back up the original files. - Verify data integrity after migration. - Perform migrations in a staging environment before applying them to production data. ## Custom data Custom payloads use `DataType` for identity and routing and `CustomData` as the common wrapper. Pure Python payloads can use the fallback wrapper without registration for in-process routing. Register a type before reconstructing it from JSON or using Arrow, Parquet, or Feather persistence. Same-binary Rust types can register native handlers; live-only Rust types may omit Arrow support. Every Python payload wrapped in `CustomData`, including an unregistered in-process payload, must expose `ts_event` and `ts_init` as UNIX nanosecond timestamps. See [Custom data](../custom_data.md) for the registry, wrapper, and persistence architecture. ### Pure Python catalog example A Python class used with the catalog supplies timestamps, JSON callbacks, an Arrow schema, and Arrow batch callbacks. Register it once during startup: ```python import json from dataclasses import asdict from dataclasses import dataclass from typing import ClassVar import pyarrow as pa from nautilus_trader.model import CustomData from nautilus_trader.model import DataType from nautilus_trader.model import register_custom_data_class from nautilus_trader.persistence import ParquetDataCatalog @dataclass class MarketTickPython: _schema: ClassVar[pa.Schema] = pa.schema( { "symbol": pa.string(), "price": pa.float64(), "volume": pa.int64(), "ts_event": pa.uint64(), "ts_init": pa.uint64(), }, ) symbol: str = "" price: float = 0.0 volume: int = 0 ts_event: int = 0 ts_init: int = 0 @classmethod def type_name_static(cls) -> str: return cls.__name__ def to_json(self) -> str: return json.dumps(asdict(self)) @classmethod def from_json(cls, data: dict) -> "MarketTickPython": return cls(**data) def encode_record_batch_py(self, items: list) -> pa.RecordBatch: return pa.RecordBatch.from_pylist( [asdict(item) for item in items], schema=self._schema, ) @classmethod def decode_record_batch_py( cls, metadata: dict, batch: pa.RecordBatch, ) -> list["MarketTickPython"]: return [cls(**row) for row in batch.to_pylist()] register_custom_data_class(MarketTickPython) catalog = ParquetDataCatalog("/path/to/catalog") data_type = DataType("MarketTickPython", metadata={"exchange": "NASDAQ"}) wrapped = [ CustomData( data_type, MarketTickPython(ts_event=1, ts_init=1, symbol="AAPL", price=150.5, volume=1000), ), ] catalog.write_custom_data(wrapped) result = catalog.query_custom_data("MarketTickPython") ticks = [item.data for item in result] ``` The registered Arrow schema must contain `ts_init`, which the catalog uses for time filtering. Custom writes must be in ascending `ts_init` order. `BacktestDataConfig` accepts built-in catalog data types, not arbitrary custom types. To replay the queried `CustomData` wrappers, add them to a configured `BacktestEngine` directly: ```python engine.add_data(result) ``` `BacktestEngine.add_data` sorts by `ts_init` by default. Pass `sort=False` only when the input is already in the required replay order. ### Publishing and subscribing Actors and strategies publish and receive the `CustomData` wrapper: ```python from nautilus_trader.model import CustomData from nautilus_trader.model import DataType data_type = DataType("MarketTickPython", metadata={"exchange": "NASDAQ"}) custom = CustomData(data_type, MarketTickPython(ts_event=1, ts_init=1)) self.subscribe_data(data_type) self.publish_data(data_type, custom) def on_data(self, data: CustomData) -> None: if data.data_type == data_type: tick = data.data ``` `publish_data` derives the message-bus topic from its `data_type` argument, including that argument's metadata. This argument can override the `CustomData` wrapper's own `data_type`; use the same value for both unless the override is intentional. `on_data` receives all subscribed custom data, so inspect `data_type` before using `.data`. With no `client_id`, `subscribe_data` installs only the local message-bus subscription. Supplying a `client_id` also sends the subscription request to that data client. ### Cache storage The general `Cache` stores serialized bytes under application-defined keys. After registering the payload type, an actor or strategy can round-trip the complete `CustomData` wrapper: ```python cache_key = "market_tick:AAPL" self.cache.add(cache_key, custom.to_json_bytes()) cached = self.cache.get(cache_key) if cached is not None: restored = CustomData.from_json_bytes(cached) ``` ### Publishing and receiving signal data A **signal** is a named custom-data message whose Python value is converted to a string. Publish and subscribe from an actor or strategy: ```python self.subscribe_signal("signal_name") self.publish_signal("signal_name", value, ts_event) def on_signal(self, signal): print("Signal", signal) ``` If `ts_event` is zero, `publish_signal` uses the current clock time. Signal messages use the custom data pipeline internally, while `subscribe_signal` dispatches them to `on_signal`. ## Related guides - [Custom data](../custom_data.md): Runtime registration, wrappers, routing, and persistence. - [Instruments](../instruments/): Financial instruments referenced by data. - [Options](../options.md): Option instruments, chain subscriptions, and strike filtering. - [Greeks](../greeks.md): Venue-provided and locally computed option Greeks. - [Cache](../cache.md): Data storage and retrieval. - [Adapters](../adapters.md): Data sources and connectivity. # IndexPriceUpdate Source: https://nautilustrader.io/docs/latest/concepts/data/index_price_update/ `IndexPriceUpdate` represents an external index price used by a derivatives market. Venues often use index prices to calculate mark prices, funding, or settlement values. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------- | -------------- | -------------- | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Instrument for the index price. | | `value` | `Price` | `Price` | Required | Current index price. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## Behavior - Index prices are cached by instrument when received. - Index prices are reference data and do not imply a trade occurred. - Perpetual and futures venues may publish both mark and index prices. - The catalog stores index prices with instrument ID and price precision metadata. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::IndexPriceUpdate, identifiers::InstrumentId, types::Price, }; let index = IndexPriceUpdate::new( InstrumentId::from("BTCUSDT-PERP.BINANCE"), Price::from("64995.50"), UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); ``` ```python tab="Python" from nautilus_trader.model import IndexPriceUpdate from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price index = IndexPriceUpdate( instrument_id=InstrumentId.from_str("BTCUSDT-PERP.BINANCE"), value=Price.from_str("64995.50"), ts_event=1_000_000_000, ts_init=1_000_000_100, ) ``` ## Related guides - [MarkPriceUpdate](mark_price_update.md) covers mark prices. - [FundingRateUpdate](funding_rate_update.md) covers perpetual funding metadata. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # InstrumentClose Source: https://nautilustrader.io/docs/latest/concepts/data/instrument_close/ `InstrumentClose` represents a closing price event for an instrument at a venue. It covers end-of-session closes and contract-expiry close events. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------- | --------------------- | --------------------- | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Instrument being closed. | | `close_price` | `Price` | `Price` | Required | Closing or settlement price. | | `close_type` | `InstrumentCloseType` | `InstrumentCloseType` | Required | `END_OF_SESSION` or `CONTRACT_EXPIRED`. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## Behavior - End-of-session closes provide session-level close prices. - Contract-expiry closes mark expiration events for dated contracts. - The close price is reference data; it does not imply a trade occurred at that price. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::InstrumentClose, enums::InstrumentCloseType, identifiers::InstrumentId, types::Price, }; let close = InstrumentClose::new( InstrumentId::from("ESM4.XCME"), Price::from("5325.25"), InstrumentCloseType::EndOfSession, UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentClose from nautilus_trader.model import InstrumentCloseType from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price close = InstrumentClose( instrument_id=InstrumentId.from_str("ESM4.XCME"), close_price=Price.from_str("5325.25"), close_type=InstrumentCloseType.END_OF_SESSION, ts_event=1_000_000_000, ts_init=1_000_000_100, ) ``` ## Related guides - [InstrumentStatus](instrument_status.md) covers instrument status events. - [Instruments](../instruments/) covers instrument definitions. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # InstrumentStatus Source: https://nautilustrader.io/docs/latest/concepts/data/instrument_status/ `InstrumentStatus` represents a change in an instrument's trading state. It captures venue status events such as pre-open, trading, halt, pause, close, and short-selling restriction changes. ## Fields | Field | Rust type | Python type | Required/default | Notes | | -------------------------- | -------------------- | -------------------- | ---------------- | ----------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Instrument whose status changed. | | `action` | `MarketStatusAction` | `MarketStatusAction` | Required | Venue status action. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | | `reason` | `Option` | `str \| None` | `None` | Cause of the status change when provided. | | `trading_event` | `Option` | `str \| None` | `None` | Venue event label when provided. | | `is_trading` | `Option` | `bool \| None` | `None` | Whether trading is enabled when known. | | `is_quoting` | `Option` | `bool \| None` | `None` | Whether quoting is enabled when known. | | `is_short_sell_restricted` | `Option` | `bool \| None` | `None` | Short-sell restriction state when known. | ## Behavior - Optional booleans allow adapters to preserve venue-provided state without guessing. - `action` gives the normalized high-level status even when venue-specific details are also stored in `reason` or `trading_event`. - Strategies can handle status updates through `on_instrument_status(...)`. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::InstrumentStatus, enums::MarketStatusAction, identifiers::InstrumentId, }; use ustr::Ustr; let status = InstrumentStatus::new( InstrumentId::from("AAPL.XNAS"), MarketStatusAction::Trading, UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), Some(Ustr::from("Normal trading")), Some(Ustr::from("MARKET_OPEN")), Some(true), Some(true), Some(false), ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import InstrumentStatus from nautilus_trader.model import MarketStatusAction status = InstrumentStatus( instrument_id=InstrumentId.from_str("AAPL.XNAS"), action=MarketStatusAction.TRADING, ts_event=1_000_000_000, ts_init=1_000_000_100, reason="Normal trading", trading_event="MARKET_OPEN", is_trading=True, is_quoting=True, is_short_sell_restricted=False, ) ``` ## Related guides - [InstrumentClose](instrument_close.md) covers instrument close price events. - [Instruments](../instruments/) covers instrument definitions. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # MarkPriceUpdate Source: https://nautilustrader.io/docs/latest/concepts/data/mark_price_update/ `MarkPriceUpdate` represents an instrument's mark price. Derivatives venues commonly use mark prices for margining, liquidation checks, and unrealized PnL calculations. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------- | -------------- | -------------- | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Instrument for the mark price. | | `value` | `Price` | `Price` | Required | Current mark price. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## Behavior - Mark prices are cached by instrument when received. - Backtests can feed mark prices to align margin and PnL behavior with venues that publish reference prices separately from trades. - The catalog stores mark prices with instrument ID and price precision metadata. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::MarkPriceUpdate, identifiers::InstrumentId, types::Price, }; let mark = MarkPriceUpdate::new( InstrumentId::from("BTCUSDT-PERP.BINANCE"), Price::from("65000.10"), UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import MarkPriceUpdate from nautilus_trader.model import Price mark = MarkPriceUpdate( instrument_id=InstrumentId.from_str("BTCUSDT-PERP.BINANCE"), value=Price.from_str("65000.10"), ts_event=1_000_000_000, ts_init=1_000_000_100, ) ``` ## Related guides - [IndexPriceUpdate](index_price_update.md) covers the index reference price. - [FundingRateUpdate](funding_rate_update.md) covers perpetual funding metadata. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # OptionGreeks Source: https://nautilustrader.io/docs/latest/concepts/data/option_greeks/ `OptionGreeks` represents venue-provided option sensitivities and implied volatility for one option instrument. As a native `Data` enum variant, it can be recorded, replayed, and queried through the catalog. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ------------------ | ------------------- | ------------------ | ---------------- | ------------------------------------------ | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Option instrument for the Greeks. | | `convention` | `GreeksConvention` | `GreeksConvention` | Default | Numeraire convention for the values. | | `greeks` | `OptionGreekValues` | Separate floats | Required | Delta, gamma, vega, theta, and rho. | | `mark_iv` | `Option` | `float \| None` | `None` | Mark implied volatility. | | `bid_iv` | `Option` | `float \| None` | `None` | Bid implied volatility. | | `ask_iv` | `Option` | `float \| None` | `None` | Ask implied volatility. | | `underlying_price` | `Option` | `float \| None` | `None` | Underlying price used for the calculation. | | `open_interest` | `Option` | `float \| None` | `None` | Open interest when published. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## Behavior - `OptionGreeks` dereferences to its core `OptionGreekValues` on the Rust surface. - The Python constructor accepts `delta`, `gamma`, `vega`, `theta`, and optional `rho` as separate float arguments. - Option chain subscriptions use `underlying_price` and deltas to resolve ATM and delta-based strike windows. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::{OptionGreekValues, OptionGreeks}, enums::GreeksConvention, identifiers::InstrumentId, }; let greeks = OptionGreeks { instrument_id: InstrumentId::from("BTC-20240628-65000-C.DERIBIT"), convention: GreeksConvention::PriceAdjusted, greeks: OptionGreekValues { delta: 0.51, gamma: 0.0002, vega: 12.5, theta: -3.2, rho: 0.1, }, mark_iv: Some(0.55), bid_iv: Some(0.54), ask_iv: Some(0.56), underlying_price: Some(65_000.0), open_interest: Some(120.0), ts_event: UnixNanos::from(1_000_000_000), ts_init: UnixNanos::from(1_000_000_100), }; ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import OptionGreeks greeks = OptionGreeks( instrument_id=InstrumentId.from_str("BTC-20240628-65000-C.DERIBIT"), delta=0.51, gamma=0.0002, vega=12.5, theta=-3.2, rho=0.1, mark_iv=0.55, bid_iv=0.54, ask_iv=0.56, underlying_price=65_000.0, open_interest=120.0, ts_event=1_000_000_000, ts_init=1_000_000_100, ) ``` ## Related guides - [Greeks](../greeks.md) covers venue-provided and locally computed Greeks. - [Options](../options.md#optiongreeks-data-type) covers option chain subscriptions. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # OrderBookDelta Source: https://nautilustrader.io/docs/latest/concepts/data/order_book_delta/ `OrderBookDelta` represents one change to an order book. It is the most granular built-in book data type and supports the book types NautilusTrader uses for incremental updates: - `L3_MBO`: Level 3 market-by-order (MBO) data. - `L2_MBP`: Level 2 market-by-price (MBP) data. - `L1_MBP`: Level 1 market-by-price (MBP) top-of-book data. The source feed and target `BookType` determine which granularity a delta carries. Use it when a venue or data provider publishes incremental book changes and Nautilus should maintain the book state locally. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------- | -------------- | -------------- | ---------------- | ------------------------------------------ | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Instrument whose book is changing. | | `action` | `BookAction` | `BookAction` | Required | `ADD`, `UPDATE`, `DELETE`, or `CLEAR`. | | `order` | `BookOrder` | `BookOrder` | Required | Price, size, side, and order ID payload. | | `flags` | `u8` | `int` | Required | `RecordFlag` bit field for event metadata. | | `sequence` | `u64` | `int` | Required | Venue sequence number, or zero if absent. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## BookOrder fields The `order` field contains the `BookOrder` payload for the delta. | Field | Rust type | Python type | Notes | | ---------- | ------------------- | ------------------- | ------------------------------------ | | `side` | `Option` | `OrderSide \| None` | Order side. | | `price` | `Price` | `Price` | Order price. | | `size` | `Quantity` | `Quantity` | Order size. | | `order_id` | `OrderId` (`u64`) | `int` | Order ID carried by the source feed. | The null/default order uses `None` for its side, with zero price, zero size, and zero order ID. ## BookAction variants | Rust variant | Python variant | Value | Meaning | | -------------------- | -------------- | ----- | -------------------------------------- | | `BookAction::Add` | `ADD` | `1` | Adds an order to the book. | | `BookAction::Update` | `UPDATE` | `2` | Updates an existing order in the book. | | `BookAction::Delete` | `DELETE` | `3` | Deletes an existing order in the book. | | `BookAction::Clear` | `CLEAR` | `4` | Clears the order book state. | ## Behavior - `ADD` and `UPDATE` deltas require a positive order size. - `CLEAR` deltas reset book state and use a null book order. - `flags` carries event boundary and snapshot metadata. See [Delta flags and event boundaries](index.md#delta-flags-and-event-boundaries). - Use `OrderBookDelta::clear(...)` in Rust or `OrderBookDelta.clear(...)` in Python to construct clear deltas. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::{BookOrder, OrderBookDelta}, enums::{BookAction, OrderSide, RecordFlag}, identifiers::InstrumentId, types::{Price, Quantity}, }; let delta = OrderBookDelta::new( InstrumentId::from("ETHUSDT-PERP.BINANCE"), BookAction::Add, BookOrder::new( OrderSide::Buy, Price::from("2500.10"), Quantity::from("3.5"), 12_345, ), RecordFlag::F_LAST as u8, 42, UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import BookAction from nautilus_trader.model import BookOrder from nautilus_trader.model import OrderBookDelta from nautilus_trader.model import OrderSide from nautilus_trader.model import RecordFlag delta = OrderBookDelta( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), action=BookAction.ADD, order=BookOrder( OrderSide.BUY, Price.from_str("2500.10"), Quantity.from_str("3.5"), 12_345, ), flags=RecordFlag.F_LAST, sequence=42, ts_event=1_000_000_000, ts_init=1_000_000_100, ) ``` ## Related guides - [OrderBookDeltas](order_book_deltas.md) covers batching deltas. - [Order books](../order_book.md) explains book types and local book state. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # OrderBookDeltas Source: https://nautilustrader.io/docs/latest/concepts/data/order_book_deltas/ `OrderBookDeltas` groups a non-empty batch of `OrderBookDelta` records from one logical book event. It reduces per-message overhead when an adapter receives or produces several changes at once. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------- | --------------------- | ---------------------- | ---------------- | ------------------------------------ | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Instrument whose book is changing. | | `deltas` | `Vec` | `list[OrderBookDelta]` | Required | Non-empty batch of deltas. | | `flags` | `u8` | `int` | From last delta | Last delta flags. | | `sequence` | `u64` | `int` | From last delta | Last delta sequence number. | | `ts_event` | `UnixNanos` | `int` | From last delta | Last delta event timestamp. | | `ts_init` | `UnixNanos` | `int` | From last delta | Last delta initialization timestamp. | ## Behavior - The batch must contain at least one delta. - Every delta's `instrument_id` must match the batch `instrument_id`. - The batch metadata mirrors the final delta. - The final delta should carry `F_LAST` when it closes a logical event group. See [Delta flags and event boundaries](index.md#delta-flags-and-event-boundaries). - Snapshot batches usually begin with a `CLEAR` delta and end with `F_SNAPSHOT | F_LAST`. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::{BookOrder, OrderBookDelta, OrderBookDeltas}, enums::{BookAction, OrderSide, RecordFlag}, identifiers::InstrumentId, types::{Price, Quantity}, }; let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE"); let bid = OrderBookDelta::new( instrument_id, BookAction::Add, BookOrder::new(OrderSide::Buy, Price::from("2500.10"), Quantity::from("3.5"), 1), 0, 41, UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); let ask = OrderBookDelta::new( instrument_id, BookAction::Add, BookOrder::new(OrderSide::Sell, Price::from("2500.20"), Quantity::from("2.0"), 2), RecordFlag::F_LAST as u8, 42, UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); let deltas = OrderBookDeltas::new(instrument_id, vec![bid, ask]); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import BookAction from nautilus_trader.model import BookOrder from nautilus_trader.model import OrderBookDelta from nautilus_trader.model import OrderBookDeltas from nautilus_trader.model import OrderSide from nautilus_trader.model import RecordFlag instrument_id = InstrumentId.from_str("ETHUSDT-PERP.BINANCE") bid = OrderBookDelta( instrument_id=instrument_id, action=BookAction.ADD, order=BookOrder( OrderSide.BUY, Price.from_str("2500.10"), Quantity.from_str("3.5"), 1, ), flags=0, sequence=41, ts_event=1_000_000_000, ts_init=1_000_000_100, ) ask = OrderBookDelta( instrument_id=instrument_id, action=BookAction.ADD, order=BookOrder( OrderSide.SELL, Price.from_str("2500.20"), Quantity.from_str("2.0"), 2, ), flags=RecordFlag.F_LAST, sequence=42, ts_event=1_000_000_000, ts_init=1_000_000_100, ) deltas = OrderBookDeltas(instrument_id, [bid, ask]) ``` ## Related guides - [OrderBookDelta](order_book_delta.md) covers the contained update type. - [Order books](../order_book.md) explains supported order book state. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # OrderBookDepth10 Source: https://nautilustrader.io/docs/latest/concepts/data/order_book_depth10/ `OrderBookDepth10` represents a fixed-depth book update with up to 10 bid levels and 10 ask levels. Use it when a venue publishes a self-contained depth snapshot rather than incremental deltas. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------- | ----------------- | ----------------- | ---------------- | ------------------------------------------ | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Instrument whose book is represented. | | `bids` | `[BookOrder; 10]` | `list[BookOrder]` | Required | Exactly 10 bid levels. | | `asks` | `[BookOrder; 10]` | `list[BookOrder]` | Required | Exactly 10 ask levels. | | `bid_counts` | `[u32; 10]` | `list[int]` | Required | Number of bid orders at each level. | | `ask_counts` | `[u32; 10]` | `list[int]` | Required | Number of ask orders at each level. | | `flags` | `u8` | `int` | Required | `RecordFlag` bit field for event metadata. | | `sequence` | `u64` | `int` | Required | Venue sequence number, or zero if absent. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## Behavior - Rust and PyO3 Python constructors require exactly 10 bid levels, 10 ask levels, 10 bid counts, and 10 ask counts. - Use null or default book orders with zero counts for unavailable levels. - This type is not interchangeable with incremental `OrderBookDelta` streams. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::{BookOrder, OrderBookDepth10, DEPTH10_LEN}, enums::OrderSide, identifiers::InstrumentId, types::{Price, Quantity}, }; let mut bids = [BookOrder::default(); DEPTH10_LEN]; let mut asks = [BookOrder::default(); DEPTH10_LEN]; bids[0] = BookOrder::new(OrderSide::Buy, Price::from("2500.10"), Quantity::from("3.5"), 1); asks[0] = BookOrder::new(OrderSide::Sell, Price::from("2500.20"), Quantity::from("2.0"), 2); let depth = OrderBookDepth10::new( InstrumentId::from("ETHUSDT-PERP.BINANCE"), bids, asks, [1; DEPTH10_LEN], [1; DEPTH10_LEN], 0, 42, UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import BookOrder from nautilus_trader.model import OrderBookDepth10 from nautilus_trader.model import OrderSide bids = [ BookOrder( OrderSide.BUY, Price.from_str(f"{2500.10 - i * 0.10:.2f}"), Quantity.from_str("3.5"), i + 1, ) for i in range(10) ] asks = [ BookOrder( OrderSide.SELL, Price.from_str(f"{2500.20 + i * 0.10:.2f}"), Quantity.from_str("2.0"), i + 11, ) for i in range(10) ] depth = OrderBookDepth10( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), bids=bids, asks=asks, bid_counts=[1] * 10, ask_counts=[1] * 10, flags=0, sequence=42, ts_event=1_000_000_000, ts_init=1_000_000_100, ) ``` ## Related guides - [QuoteTick](quote_tick.md) covers top-of-book data derived from depth. - [Order books](index.md#order-books) explains order book state. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # QuoteTick Source: https://nautilustrader.io/docs/latest/concepts/data/quote_tick/ `QuoteTick` represents the top-of-book bid and ask for one instrument. It carries the best available bid and ask prices and sizes at a specific event time. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------- | -------------- | -------------- | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Instrument for the quote. | | `bid_price` | `Price` | `Price` | Required | Best bid price. | | `ask_price` | `Price` | `Price` | Required | Best ask price. | | `bid_size` | `Quantity` | `Quantity` | Required | Quantity available at the best bid. | | `ask_size` | `Quantity` | `Quantity` | Required | Quantity available at the best ask. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## Behavior - Bid and ask prices must use the same precision. - Bid and ask sizes must use the same precision. - `extract_price(...)` returns the `BID`, `ASK`, or `MID` price and `extract_size(...)` returns the matching size; any other price type is an error. - `MID` results carry one extra digit of precision, capped at `FIXED_PRECISION`. - Quote bars can use `BID`, `ASK`, or `MID` price types. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::QuoteTick, identifiers::InstrumentId, types::{Price, Quantity}, }; let quote = QuoteTick::new( InstrumentId::from("AUD/USD.SIM"), Price::from("0.65000"), Price::from("0.65002"), Quantity::from("1000000"), Quantity::from("1200000"), UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import QuoteTick quote = QuoteTick( instrument_id=InstrumentId.from_str("AUD/USD.SIM"), bid_price=Price.from_str("0.65000"), ask_price=Price.from_str("0.65002"), bid_size=Quantity.from_int(1_000_000), ask_size=Quantity.from_int(1_200_000), ts_event=1_000_000_000, ts_init=1_000_000_100, ) ``` ## Related guides - [OrderBookDepth10](order_book_depth10.md) covers fixed-depth snapshots with top levels. - [Bars and aggregation](index.md#bars-and-aggregation) covers quote-to-bar aggregation. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # TradeTick Source: https://nautilustrader.io/docs/latest/concepts/data/trade_tick/ `TradeTick` represents one executed trade or match event from a venue. It carries the traded price, size, aggressor side, and venue trade identifier. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ---------------- | --------------- | --------------- | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Instrument for the trade. | | `price` | `Price` | `Price` | Required | Executed price. | | `size` | `Quantity` | `Quantity` | Required | Executed quantity. | | `aggressor_side` | `AggressorSide` | `AggressorSide` | Required | `BUY`, `SELL`, or `NO_AGGRESSOR`. | | `trade_id` | `TradeId` | `TradeId` | Required | Venue-assigned match ID. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | ## Behavior - `size` must be positive. - Information-driven bars require `TradeTick` data because they use `aggressor_side`. - Trade bars use `LAST` price type. - `trade_id` should be stable for the venue event when the venue provides one. - Parsing and deserialization accept the deprecated `BUYER`/`SELLER` values; string output is always canonical `BUY`/`SELL`. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ data::TradeTick, enums::AggressorSide, identifiers::{InstrumentId, TradeId}, types::{Price, Quantity}, }; let trade = TradeTick::new( InstrumentId::from("BTCUSDT.BINANCE"), Price::from("65000.10"), Quantity::from("0.25"), AggressorSide::Buy, TradeId::from("123456789"), UnixNanos::from(1_000_000_000), UnixNanos::from(1_000_000_100), ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import TradeId from nautilus_trader.model import TradeTick from nautilus_trader.model import AggressorSide trade = TradeTick( instrument_id=InstrumentId.from_str("BTCUSDT.BINANCE"), price=Price.from_str("65000.10"), size=Quantity.from_str("0.25"), aggressor_side=AggressorSide.BUY, trade_id=TradeId("123456789"), ts_event=1_000_000_000, ts_init=1_000_000_100, ) ``` ## Related guides - [Bar](bar.md) covers trade-to-bar aggregation. - [Information-driven bars](index.md#information-driven-bars) explains aggressor-side use. - [Python API reference](/docs/python-api-latest/model/data.html) lists Python members. # AccountState Source: https://nautilustrader.io/docs/latest/concepts/events/account_state/ `AccountState` carries a snapshot of an account's balances and margins. The system publishes it when the venue reports an account update through the execution client, or when the `Portfolio` recalculates account state after a position update (for margin accounts with `calculate_account_state` enabled). The `Portfolio` subscribes to these events internally to maintain exposure and balance tracking. The `is_reported` flag distinguishes venue-reported snapshots from system-calculated ones. ## Fields | Field | Python type | Required/default | Description | | --------------- | ---------------------- | ---------------- | ------------------------------------------------------------------------- | | `account_id` | `AccountId` | Required | The account ID (with the venue). | | `account_type` | `AccountType` | Required | The account type (`CASH`, `MARGIN`, `BETTING`, or `WALLET`). | | `base_currency` | `Currency` or `None` | `None` | The account base currency (`None` for multi-currency accounts). | | `is_reported` | `bool` | Required | If the state is reported from the exchange (otherwise system-calculated). | | `balances` | `list[AccountBalance]` | Required | The account balances (may be empty). | | `margins` | `list[MarginBalance]` | Required | The margin balances (may be empty). | | `event_id` | `UUID4` | Required | The event ID. | | `ts_event` | `int` | Required | UNIX timestamp (nanoseconds) when the event occurred. | | `ts_init` | `int` | Required | UNIX timestamp (nanoseconds) when the object was initialized. | | `info` | `dict` | `None` | Venue-specific account data with no typed field (empty dict when unset). | ## Example Account state is normally consumed through the `Portfolio` rather than a dedicated handler: ```python from nautilus_trader.model import Venue # Account state is tracked by the portfolio; query it by venue account = self.portfolio.account(venue=Venue("BINANCE")) self.log.info(f"Account state: {account}") ``` The result is detached from the Portfolio. Mutating the returned account does not change the authoritative account held by the engine. ## Related guides - [Events](index.md) - Event categories and dispatch. - [Accounting](../accounting.md) - Account types, balances, and margin models. - [Portfolio](../portfolio.md) - How account state feeds exposure and balance tracking. # Events Source: https://nautilustrader.io/docs/latest/concepts/events/ NautilusTrader models execution, position, account, and time changes as events. The `MessageBus` routes these events to interested components and, where supported, to strategy handlers. This guide covers the event types, their dispatch, and how order fills and corrections produce position events. ## Event categories | Category | Examples | Origin | | -------- | ----------------------------------------------- | ------------------------------- | | Order | `OrderAccepted`, `OrderFilled`, `OrderCanceled` | Execution pipeline | | Position | `PositionOpened`, `PositionAdjusted` | Fills and accounting changes | | Account | `AccountState` | `ExecutionClient` / `Portfolio` | | Time | `TimeEvent` | `Clock` (timers and alerts) | ## Handler dispatch When an event reaches a strategy, the system calls handlers in a fixed order. The **specific handler** runs before the **aggregate handler**, so you can handle events at either granularity or use both. A strategy passes events to its handlers only while it is running; events that arrive before start or after stop are not dispatched. Python data actors do not expose order event callbacks or the raw message bus. Use signals to send derived values from a strategy to a data actor. See [Actors: order event handling](../actors.md#order-event-handling). ### Order events 1. Specific handler (for example, `on_order_filled`). 1. `on_order_event` (receives all order events). ### Position events For the position lifecycle events dispatched to strategies: 1. Specific handler (for example, `on_position_opened`). 1. `on_position_event` (receives all dispatched position lifecycle events). ### Time events Timers and alerts produce `TimeEvent` objects. Pass a `callback` when calling `set_timer` or `set_time_alert` to direct events to your own method. If you omit the callback, a callback previously registered under the same name is used when present; otherwise the event is delivered to `on_time_event`. ## Order events Order events initialize an order, change its state, or correct its fill history. The execution pipeline applies them to the order and cache, then publishes them on the `MessageBus`. The table below shows the primary transitions; partially filled, external, and triggered orders support additional transitions documented in the full [order state flow](../orders/index.md#order-state-flow). | Event | Primary transition | Handler | | ------------------------------------------------- | --------------------------------------------- | -------------------------- | | [`OrderInitialized`](order_initialized.md) | Create or materialize order | `on_order_initialized` | | [`OrderDenied`](order_denied.md) | Initialized -> Denied | `on_order_denied` | | [`OrderEmulated`](order_emulated.md) | Initialized -> Emulated | `on_order_emulated` | | [`OrderReleased`](order_released.md) | Emulated -> Released | `on_order_released` | | [`OrderSubmitted`](order_submitted.md) | Initialized/Released -> Submitted | `on_order_submitted` | | [`OrderAccepted`](order_accepted.md) | Submitted -> Accepted | `on_order_accepted` | | [`OrderRejected`](order_rejected.md) | Submitted -> Rejected | `on_order_rejected` | | [`OrderTriggered`](order_triggered.md) | Accepted -> Triggered | `on_order_triggered` | | [`OrderPendingUpdate`](order_pending_update.md) | Accepted -> PendingUpdate | `on_order_pending_update` | | [`OrderPendingCancel`](order_pending_cancel.md) | Accepted -> PendingCancel | `on_order_pending_cancel` | | [`OrderUpdated`](order_updated.md) | PendingUpdate -> previous status | `on_order_updated` | | [`OrderModifyRejected`](order_modify_rejected.md) | PendingUpdate -> previous status | `on_order_modify_rejected` | | [`OrderCancelRejected`](order_cancel_rejected.md) | PendingCancel -> previous status | `on_order_cancel_rejected` | | [`OrderCanceled`](order_canceled.md) | PendingCancel/Accepted -> Canceled | `on_order_canceled` | | [`OrderExpired`](order_expired.md) | Accepted -> Expired | `on_order_expired` | | [`OrderFilled`](order_filled.md) | Accepted -> Filled/PartiallyFilled | `on_order_filled` | | [`OrderFillVoided`](order_fill_voided.md) | Correct known fill; otherwise assert terminal | `on_order_fill_voided` | ### Common Python order event fields Every concrete Python order event exposes these fields: | Field | Description | | ----------------- | ------------------------------------------------------------ | | `trader_id` | Trader instance identifier. | | `strategy_id` | Strategy associated with the order. | | `instrument_id` | Instrument for the order. | | `client_order_id` | Client-assigned order identifier. | | `event_id` | Unique event identifier. | | `ts_event` | UNIX timestamp (nanoseconds) when the event occurred. | | `ts_init` | UNIX timestamp (nanoseconds) when the event was initialized. | | `causation_id` | Source event or report which caused this event, if known. | Each order event page lists its type-specific fields. These include `venue_order_id`, `account_id`, and `reconciliation` only on the Python event classes that expose them. For example, [`OrderFilled`](order_filled.md) adds `last_qty`, `last_px`, `trade_id`, and `commission`. [`OrderFillVoided`](order_fill_voided.md) identifies the corrected trade and carries its cumulative voided quantity. ## Position events Position lifecycle events describe cached position changes caused by fills and fill corrections. The `ExecutionEngine` processes each `OrderFilled`, updates or creates a position, and emits the corresponding lifecycle event. When an `OrderFillVoided` corrects a locally applied fill, it rebuilds each affected cached position from its effective fill history. It does not emit an opposite fill. After publishing the correction, the engine emits `PositionChanged` for a corrected position that remains open or `PositionClosed` for one that is closed. An order-only correction does not produce a position event. | Event | When it fires | Handler | | ---------------------------------------- | ---------------------------------------------- | --------------------- | | [`PositionOpened`](position_opened.md) | A fill creates a new position. | `on_position_opened` | | [`PositionChanged`](position_changed.md) | A fill or correction changes an open position. | `on_position_changed` | | [`PositionClosed`](position_closed.md) | A fill or correction leaves quantity at zero. | `on_position_closed` | :::warning[PositionAdjusted never reaches a position handler] [`PositionAdjusted`](../positions.md#position-adjustments) records quantity or realized PnL changes outside normal fills, such as base-currency commissions and funding. The `ExecutionEngine` publishes it, but neither `on_position_event` nor any specific handler receives it. Inspect `position.adjustments()` for the recorded history. ::: ### From fill to position: the causal chain The following diagram shows how a single `OrderFilled` event produces a position event, the link between order management and position tracking. ```mermaid sequenceDiagram participant Venue as Venue participant EE as ExecutionEngine participant Cache as Cache participant Strategy as Strategy Venue-->>EE: OrderFilled EE->>EE: apply fill to order EE->>Cache: update order state EE->>EE: determine position ID alt No existing position EE->>Cache: add new Position EE->>Strategy: PositionOpened else Position open, not closed by fill EE->>Cache: update Position EE->>Strategy: PositionChanged else Fill closes the position EE->>Cache: update Position EE->>Strategy: PositionClosed end ``` Step by step: 1. **Fill arrives.** The `ExecutionEngine` receives an `OrderFilled` event through the execution pipeline. 2. **Order state updates.** The engine applies the fill to the order object and writes the updated order to the `Cache`. 3. **Position ID resolved.** The engine determines which position this fill belongs to, based on OMS type and strategy configuration. 4. **Position created or updated.** Three outcomes: - **No position exists** for this ID: the engine creates a `Position` from the fill, adds it to the `Cache`, and emits `PositionOpened`. - **Position exists and remains open** after the fill: the engine applies the fill to the position, updates the `Cache`, and emits `PositionChanged`. - **Position exists and closes** (quantity reaches zero): the engine applies the fill, updates the `Cache`, and emits `PositionClosed`. 5. **Flip case.** When a fill reverses the position, for example a sell of 15 against a long 10, the engine splits the fill into two parts: one that closes the original position (`PositionClosed`) and one that opens the new position (`PositionOpened`). ### Position event fields The three position lifecycle event classes share a core field set and expose additional fields as the position develops. A check mark means the Python class exposes the field; a dash means the field is absent from that class. | Field | Opened | Changed | Closed | Description | | ------------------ | ------ | ------- | ------ | -------------------------------------------- | | `trader_id` | ✓ | ✓ | ✓ | Trader instance identifier. | | `strategy_id` | ✓ | ✓ | ✓ | Strategy that owns the position. | | `instrument_id` | ✓ | ✓ | ✓ | Instrument for the position. | | `position_id` | ✓ | ✓ | ✓ | Unique position identifier. | | `account_id` | ✓ | ✓ | ✓ | Account the position belongs to. | | `opening_order_id` | ✓ | ✓ | ✓ | Order that opened the position. | | `closing_order_id` | - | - | ✓ | Order that closed the position. | | `entry` | ✓ | ✓ | ✓ | Side of the opening fill. | | `side` | ✓ | ✓ | ✓ | Current position side. | | `signed_qty` | ✓ | ✓ | ✓ | Signed quantity (negative=short). | | `quantity` | ✓ | ✓ | ✓ | Unsigned position quantity. | | `peak_quantity` | - | ✓ | ✓ | Largest quantity held. | | `peak_qty` | - | ✓ | ✓ | Compatibility alias for `peak_quantity`. | | `last_qty` | ✓ | ✓ | ✓ | Quantity of the fill or correction. | | `last_px` | ✓ | ✓ | ✓ | Price of the fill or correction. | | `currency` | ✓ | ✓ | ✓ | Position quote currency. | | `avg_px_open` | ✓ | ✓ | ✓ | Average entry price. | | `avg_px_close` | - | ✓ | ✓ | Average exit price, if available. | | `realized_return` | - | ✓ | ✓ | Realized return as a ratio. | | `realized_pnl` | ✓ | ✓ | ✓ | Current-cycle realized PnL in cost currency. | | `unrealized_pnl` | - | ✓ | ✓ | Set to zero by the engine. | | `duration` | - | - | ✓ | Time held in nanoseconds. | | `ts_opened` | - | ✓ | ✓ | Timestamp when position opened. | | `ts_closed` | - | - | ✓ | Timestamp when position closed. | | `event_id` | ✓ | ✓ | ✓ | Unique event identifier. | | `ts_event` | ✓ | ✓ | ✓ | Timestamp of the triggering event. | | `ts_init` | ✓ | ✓ | ✓ | Timestamp when event was created. | ### Tracing orders to positions The `Cache` provides methods to navigate between orders and positions: ```python # From a position, find all orders that contributed fills orders = self.cache.orders_for_position(position.id) # From an order, find the position it belongs to position = self.cache.position_for_order(order.client_order_id) # The opening order is stored directly on the position opening_order_id = position.opening_order_id ``` ## Account events `AccountState` events represent balance and margin snapshots. They fire when: - The venue reports an account update (via the execution client). - The `Portfolio` recalculates account state after a position update (for margin accounts with `calculate_account_state` enabled). Account state contains balances, margins, account type, and base currency. The `Portfolio` subscribes to these events internally to maintain exposure and balance tracking. See [`AccountState`](account_state.md) for the full field list. ## Related guides - [Orders](../orders/) - Order types and state machine. - [Positions](../positions.md) - Position lifecycle and PnL. - [Execution](../execution/) - Execution flow and risk checks. - [Strategies](../strategies.md) - Handler implementations in strategies. - [Architecture](../architecture.md) - Data and execution flow patterns. # OrderAccepted Source: https://nautilustrader.io/docs/latest/concepts/events/order_accepted/ `OrderAccepted` represents an order having been accepted by the trading venue. The `ExecutionEngine` applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. It fires when the venue acknowledges the order as received and valid (often a FIX `NEW` OrdStatus). Typical transition: `SUBMITTED` -> `ACCEPTED`. Handler: `on_order_accepted`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderAccepted` carries: | Field | Python type | Required/default | Description | | ---------------- | -------------- | ---------------- | -------------------------------------- | | `venue_order_id` | `VenueOrderId` | Required | The venue-assigned order identifier. | | `account_id` | `AccountId` | Required | The account associated with the order. | | `reconciliation` | `bool` | Required | If generated during reconciliation. | ## Example Reading the event in a strategy handler: ```python def on_order_accepted(self, event: OrderAccepted) -> None: self.log.info( f"Order {event.client_order_id} accepted as {event.venue_order_id}", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # OrderCancelRejected Source: https://nautilustrader.io/docs/latest/concepts/events/order_cancel_rejected/ `OrderCancelRejected` represents a `CancelOrder` command having been rejected by the trading venue. The `ExecutionEngine` applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. It fires when the venue rejects a cancel request. Typical transition: `PENDING_CANCEL` -> previous status (for example `ACCEPTED`). Handler: `on_order_cancel_rejected`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderCancelRejected` carries: | Field | Python type | Required/default | Description | | ---------------- | ------------------------ | ---------------- | ------------------------------------------------ | | `reason` | `str` | Required | The order cancel rejection reason. | | `venue_order_id` | `VenueOrderId` or `None` | `None` | The venue-assigned order identifier, if known. | | `account_id` | `AccountId` or `None` | `None` | The account associated with the order, if known. | | `reconciliation` | `bool` | Required | If generated during reconciliation. | ## Example Reading the event in a strategy handler: ```python def on_order_cancel_rejected(self, event: OrderCancelRejected) -> None: self.log.warning( f"Cancel rejected for {event.client_order_id}: {event.reason}", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # OrderCanceled Source: https://nautilustrader.io/docs/latest/concepts/events/order_canceled/ `OrderCanceled` records an order reaching the terminal `CANCELED` state. The execution pipeline applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. It can come from a trading venue, simulated matching engine, local order emulator, or reconciliation. Reconciliation can create it from a venue report or from a local timeout or missing-order policy. Typical transitions: `PENDING_CANCEL`/`ACCEPTED` -> `CANCELED`. External and recovery paths also allow `INITIALIZED`, `EMULATED`, `RELEASED`, `SUBMITTED`, `PENDING_UPDATE`, `TRIGGERED`, or `PARTIALLY_FILLED` -> `CANCELED`. A re-close can append `CANCELED` while the order is already canceled when a late fill arrived after its earlier cancellation. Handler: `on_order_canceled`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderCanceled` carries: | Field | Python type | Required/default | Description | | ---------------- | ------------------------ | ---------------- | ------------------------------------------------------------------------------ | | `venue_order_id` | `VenueOrderId` or `None` | `None` | The venue-assigned order identifier, if known. | | `account_id` | `AccountId` or `None` | `None` | The account associated with the order, if known. | | `reason` | `str` or `None` | `None` | The cancellation reason supplied by the venue. | | `reconciliation` | `bool` | Required | If reconciliation generated the event; this does not imply venue confirmation. | ## Example Reading the event in a strategy handler: ```python def on_order_canceled(self, event: OrderCanceled) -> None: self.log.info(f"Order {event.client_order_id} canceled") ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. - [Execution policies](../execution/policies.md#terminal-reconciliation-provenance) - Venue evidence and synthetic terminal policies. # OrderDenied Source: https://nautilustrader.io/docs/latest/concepts/events/order_denied/ `OrderDenied` represents an order having been denied by the Nautilus system. The execution pipeline applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. It fires when an otherwise valid order cannot be submitted, for example due to a risk limit or an unsupported feature. The risk engine, execution engine, execution algorithms, and execution clients can all deny an order. Typical transition: `INITIALIZED` -> `DENIED`. Handler: `on_order_denied`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderDenied` carries: | Field | Python type | Required/default | Description | | -------- | ----------- | ---------------- | ------------------------------------------------------------------------ | | `reason` | `str` | Required | The standardized denied reason code, with an optional diagnostic suffix. | ## Example Reading the event in a strategy handler: ```python def on_order_denied(self, event: OrderDenied) -> None: self.log.warning(f"Order {event.client_order_id} denied: {event.reason}") ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Execution](../execution/) - Risk checks and the pre-trade pipeline. - [Order denied reasons](../execution/index.md#order-denied-reasons) - The standardized code set and message forms. - [Orders](../orders/) - Order types and the state machine. # OrderEmulated Source: https://nautilustrader.io/docs/latest/concepts/events/order_emulated/ `OrderEmulated` records that the `OrderEmulator` has taken an order under local emulation. The emulator applies the event to the order, updates the `Cache`, and publishes it on the `MessageBus`. Typical transition: `INITIALIZED` -> `EMULATED`. Handler: `on_order_emulated`. ## Fields `OrderEmulated` exposes only the [common Python order event fields](index.md#common-python-order-event-fields). ## Example Reading the event in a strategy handler: ```python def on_order_emulated(self, event: OrderEmulated) -> None: self.log.info(f"Order {event.client_order_id} is now emulated locally") ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Emulated orders](../orders/emulated.md) - The local emulation lifecycle. # OrderExpired Source: https://nautilustrader.io/docs/latest/concepts/events/order_expired/ `OrderExpired` records that an order has expired. The execution pipeline applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. It can come from a trading venue, simulated matching engine, or reconciliation, for example when a GTD order reaches its expiry. Typical transition: `ACCEPTED` -> `EXPIRED`. Handler: `on_order_expired`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderExpired` carries: | Field | Python type | Required/default | Description | | ---------------- | ------------------------ | ---------------- | ------------------------------------------------ | | `venue_order_id` | `VenueOrderId` or `None` | `None` | The venue-assigned order identifier, if known. | | `account_id` | `AccountId` or `None` | `None` | The account associated with the order, if known. | | `reconciliation` | `bool` | Required | If generated during reconciliation. | ## Example Reading the event in a strategy handler: ```python def on_order_expired(self, event: OrderExpired) -> None: self.log.info(f"Order {event.client_order_id} expired") ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # OrderFillVoided Source: https://nautilustrader.io/docs/latest/concepts/events/order_fill_voided/ `OrderFillVoided` records that all or part of a previously reported fill no longer has economic effect. The `ExecutionEngine` applies every successful correction to the order. When the referenced fill exists locally, it also rebuilds each affected position and refreshes portfolio position and PnL caches before publishing the correction. Venue adapters refresh account balances from their authoritative account endpoints. Strategies receive `OrderFillVoided` after the corrected cache state is available. When a cached position changes, the engine then publishes `PositionChanged` if it remains open or `PositionClosed` if it is closed. A successful order-only correction does not produce a position event. A **correction** is not an opposite-side fill. It retains the original trade identity so replay, reconciliation, and strategy audit history describe the venue action directly. Handler: `on_order_fill_voided`. ## Contract `voided_qty` and `commission_voided` are cumulative for the referenced `trade_id`. Quantity corrections cannot decrease. For a locally applied fill, fee corrections also cannot decrease, and a later revision may increase either value or change `is_reopened` at the same quantity. Duplicate, stale, and over-void corrections are rejected. Whether the referenced `OrderFilled` is already in the local order history determines how Nautilus interprets the correction: | Fill is local | `is_reopened` | Outcome | | ------------- | ------------- | --------------------------------------------------------------------- | | Yes | `false` | Apply; corrected quantity does not become working. | | Yes | `true` | Apply; corrected quantity becomes working, subject to terminal rules. | | No | `false` | Apply; whole order becomes terminal with zero leaves. | | No | `true` | Reject. | An unapplied non-reopened correction is an **order-level terminal assertion**. This remains true when `voided_qty` is less than the order quantity: the value records the ineffective fill quantity, not working leaves. The event must match the order identity, cannot exceed the order quantity, and cannot void a non-zero commission. Nautilus does not reverse position or account exposure without a local fill. ### Adapter requirements - Publish and persist the referenced `OrderFilled` before a reopened correction or any partial correction that should leave the order executable. Replay enforces the same ordering as live processing. - Emit a correction without its referenced fill only when the whole order is authoritatively terminal. - Do not rely on a later working `OrderStatusReport` to repair event ordering. Continuous reconciliation ignores fill decreases in working reports without explicit void evidence, `VOIDED` does not reopen, and snapshot reconciliation derives corrections only from retained fills. ### Status behavior with a local fill The corrected quantity does not become executable by default: - A filled order becomes terminal `VOIDED`, even when some effective filled quantity survives. - A partially filled order preserves the remainder that was already working. Its status derives from the surviving effective fills and its leaves exclude the non-reopened void quantity. - A canceled or expired order keeps its terminal status. - A correction with `is_reopened=true` also returns the corrected quantity to working leaves. The order derives `ACCEPTED` when no effective fill remains or `PARTIALLY_FILLED` when some quantity survives. `VOIDED` is terminal regardless of the correction path. Later fills, cancels, updates, corrections, and working status reports do not reopen it. :::warning[Upgrade consumers before they read corrected data] The schemas append this event and status without changing existing records. Older v2 readers do not recognize the new values, so upgrade consumers before they read corrected streams or catalog data. ::: ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderFillVoided` carries: | Field | Python type | Required/default | Description | | ------------------- | -------------------------- | ---------------- | ------------------------------------------------------- | | `venue_order_id` | `VenueOrderId` | Required | Venue-assigned order identifier. | | `account_id` | `AccountId` | Required | Account associated with the original fill. | | `correction_id` | `str` | Required | Identity for this correction revision. | | `trade_id` | `TradeId` | Required | Original venue trade ID. | | `voided_qty` | `Quantity` | Required | Cumulative ineffective quantity for the trade. | | `commission_voided` | `Money` or `None` | `None` | Cumulative fee correction for the trade. | | `order_side` | `OrderSide` | Required | Side of the original fill. | | `order_type` | `OrderType` | Required | Type of the original order. | | `last_px` | `Price` | Required | Price of the original fill. | | `currency` | `Currency` | Required | Currency of the original fill price. | | `liquidity_side` | `LiquiditySide` | Required | Liquidity side of the original fill. | | `position_id` | `PositionId` or `None` | `None` | Position ID associated with the original fill. | | `reason` | `str` or `None` | `None` | Venue or reconciliation reason for the correction. | | `info` | `dict[str, str]` or `None` | `None` | Additional venue correction metadata. | | `is_reopened` | `bool` | `False` | Whether the venue proves the order is executable again. | | `reconciliation` | `bool` | Required | If generated during reconciliation. | ## Example ```python def on_order_fill_voided(self, event: OrderFillVoided) -> None: self.log.warning( f"Corrected {event.trade_id}: voided={event.voided_qty} reopened={event.is_reopened}", ) ``` ## Related guides - [Execution](../execution/) - Correction application and publication order. - [OrderFilled](order_filled.md) - The original fill event. - [Orders](../orders/) - Order status and state flow. # OrderFilled Source: https://nautilustrader.io/docs/latest/concepts/events/order_filled/ `OrderFilled` records a partial or full execution against an order. The `ExecutionEngine` applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. Fills from live execution, reconciliation, and simulated matching drive the position lifecycle events. Typical transition: `ACCEPTED` -> `FILLED` / `PARTIALLY_FILLED`. Handler: `on_order_filled`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderFilled` carries: | Field | Python type | Required/default | Description | | ---------------- | -------------------------- | ---------------- | ------------------------------------------------------------------------ | | `venue_order_id` | `VenueOrderId` | Required | The venue-assigned order identifier. | | `account_id` | `AccountId` | Required | The account associated with the fill. | | `trade_id` | `TradeId` | Required | The trade match ID assigned by the venue. | | `position_id` | `PositionId` or `None` | `None` | The position ID associated with the fill. | | `order_side` | `OrderSide` | Required | The execution order side. | | `order_type` | `OrderType` | Required | The execution order type. | | `last_qty` | `Quantity` | Required | The fill quantity for this execution. | | `last_px` | `Price` | Required | The fill price for this execution, not the average price. | | `currency` | `Currency` | Required | The currency of the fill price. | | `commission` | `Money` or `None` | `None` | The fill commission, if reported. | | `liquidity_side` | `LiquiditySide` | Required | The execution liquidity side (`MAKER`, `TAKER`, or `NO_LIQUIDITY_SIDE`). | | `info` | `dict[str, str]` or `None` | `None` | Additional venue-specific or adapter-specific fill metadata. | | `reconciliation` | `bool` | Required | If the event was generated during reconciliation. | ## Example Reading the event in a strategy handler: ```python def on_order_filled(self, event: OrderFilled) -> None: self.log.info( f"Filled {event.last_qty} @ {event.last_px} " f"({event.liquidity_side}) commission={event.commission}", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Positions](../positions.md) - Positions created and modified from fills. - [Orders](../orders/) - Order types and the state machine. # OrderInitialized Source: https://nautilustrader.io/docs/latest/concepts/events/order_initialized/ `OrderInitialized` is the seed event for a new order. It carries enough information to send the order over the wire and reconstruct it with the same properties. The execution pipeline stores the order in the `Cache` and publishes the event on the `MessageBus`. The event seeds both locally created orders and external orders materialized during reconciliation. Handler: `on_order_initialized`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderInitialized` carries: | Field | Python type | Required/default | Description | | ----------------------- | ------------------------------- | ---------------- | --------------------------------------------------- | | `order_side` | `OrderSide` | Required | The order side. | | `order_type` | `OrderType` | Required | The order type. | | `quantity` | `Quantity` | Required | The order quantity. | | `time_in_force` | `TimeInForce` | Required | The order time in force. | | `post_only` | `bool` | Required | If the order only provides liquidity. | | `reduce_only` | `bool` | Required | If the order carries the reduce-only instruction. | | `quote_quantity` | `bool` | Required | If quantity is denominated in the quote currency. | | `reconciliation` | `bool` | Required | If the event was generated during reconciliation. | | `price` | `Price` or `None` | `None` | The limit price. | | `activation_price` | `Price` or `None` | `None` | The activation price for a trailing-stop order. | | `trigger_price` | `Price` or `None` | `None` | The stop trigger price. | | `trigger_type` | `TriggerType` or `None` | `None` | The trigger type. | | `limit_offset` | `Decimal` or `None` | `None` | The trailing offset for the limit price. | | `trailing_offset` | `Decimal` or `None` | `None` | The trailing offset for the trigger price. | | `trailing_offset_type` | `TrailingOffsetType` or `None` | `None` | The trailing offset type. | | `expire_time` | `int` or `None` | `None` | The UNIX expiration timestamp in nanoseconds. | | `display_qty` | `Quantity` or `None` | `None` | The quantity displayed on the public book. | | `emulation_trigger` | `TriggerType` or `None` | `None` | The market price trigger for local emulation. | | `trigger_instrument_id` | `InstrumentId` or `None` | `None` | The instrument that supplies the emulation trigger. | | `contingency_type` | `ContingencyType` or `None` | `None` | The order contingency type. | | `order_list_id` | `OrderListId` or `None` | `None` | The associated order list ID. | | `linked_order_ids` | `list[ClientOrderId]` or `None` | `None` | The linked client order IDs. | | `parent_order_id` | `ClientOrderId` or `None` | `None` | The parent client order ID. | | `exec_algorithm_id` | `ExecAlgorithmId` or `None` | `None` | The execution algorithm ID. | | `exec_algorithm_params` | `dict[str, str]` or `None` | `None` | The execution algorithm parameters. | | `exec_spawn_id` | `ClientOrderId` or `None` | `None` | The spawning primary client order ID. | | `tags` | `list[str]` or `None` | `None` | Custom user tags. | ## Example Reading the event in a strategy handler: ```python def on_order_initialized(self, event: OrderInitialized) -> None: self.log.info( f"Initialized {event.order_type} {event.order_side} {event.quantity} {event.instrument_id}", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # OrderModifyRejected Source: https://nautilustrader.io/docs/latest/concepts/events/order_modify_rejected/ `OrderModifyRejected` records that a `ModifyOrder` command was rejected. The `ExecutionEngine` applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. A trading venue, simulated matching engine, or local risk control can reject the request. Typical transition: `PENDING_UPDATE` -> previous status (for example `ACCEPTED`). Handler: `on_order_modify_rejected`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderModifyRejected` carries: | Field | Python type | Required/default | Description | | ---------------- | ------------------------ | ---------------- | ------------------------------------------------ | | `reason` | `str` | Required | The order update rejection reason. | | `venue_order_id` | `VenueOrderId` or `None` | `None` | The venue-assigned order identifier, if known. | | `account_id` | `AccountId` or `None` | `None` | The account associated with the order, if known. | | `reconciliation` | `bool` | Required | If generated during reconciliation. | ## Example Reading the event in a strategy handler: ```python def on_order_modify_rejected(self, event: OrderModifyRejected) -> None: self.log.warning( f"Modify rejected for {event.client_order_id}: {event.reason}", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # OrderPendingCancel Source: https://nautilustrader.io/docs/latest/concepts/events/order_pending_cancel/ `OrderPendingCancel` represents a `CancelOrder` command having been sent to the trading venue. The `ExecutionEngine` applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. It fires when the system dispatches a cancel request and awaits venue acknowledgement. Typical transition: `ACCEPTED` -> `PENDING_CANCEL`. Handler: `on_order_pending_cancel`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderPendingCancel` carries: | Field | Python type | Required/default | Description | | ---------------- | ------------------------ | ---------------- | ------------------------------------------------ | | `venue_order_id` | `VenueOrderId` or `None` | `None` | The venue-assigned order identifier, if known. | | `account_id` | `AccountId` or `None` | Required | The account associated with the order, if known. | | `reconciliation` | `bool` | Required | If generated during reconciliation. | ## Example Reading the event in a strategy handler: ```python def on_order_pending_cancel(self, event: OrderPendingCancel) -> None: self.log.info(f"Cancel pending for {event.client_order_id}") ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # OrderPendingUpdate Source: https://nautilustrader.io/docs/latest/concepts/events/order_pending_update/ `OrderPendingUpdate` represents a `ModifyOrder` command having been sent to the trading venue. The `ExecutionEngine` applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. It fires when the system dispatches a modify request and awaits venue acknowledgement. Typical transition: `ACCEPTED` -> `PENDING_UPDATE`. Handler: `on_order_pending_update`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderPendingUpdate` carries: | Field | Python type | Required/default | Description | | ---------------- | ------------------------ | ---------------- | ------------------------------------------------ | | `venue_order_id` | `VenueOrderId` or `None` | `None` | The venue-assigned order identifier, if known. | | `account_id` | `AccountId` or `None` | Required | The account associated with the order, if known. | | `reconciliation` | `bool` | Required | If generated during reconciliation. | ## Example Reading the event in a strategy handler: ```python def on_order_pending_update(self, event: OrderPendingUpdate) -> None: self.log.info(f"Modify pending for {event.client_order_id}") ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # OrderRejected Source: https://nautilustrader.io/docs/latest/concepts/events/order_rejected/ `OrderRejected` records an order reaching the terminal `REJECTED` state. The `ExecutionEngine` applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. It normally comes from an explicit venue rejection. Reconciliation can also create it from a venue report or after a local timeout or missing-order policy expires. Typical transition: `SUBMITTED` -> `REJECTED`. External and reconciliation paths also allow `INITIALIZED`, `ACCEPTED`, `PENDING_UPDATE`, `PENDING_CANCEL`, or `TRIGGERED` -> `REJECTED`. Handler: `on_order_rejected`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderRejected` carries: | Field | Python type | Required/default | Description | | ---------------- | ----------- | ---------------- | ------------------------------------------------------------------------------ | | `account_id` | `AccountId` | Required | The account associated with the order. | | `reason` | `str` | Required | The venue reason or local reconciliation policy reason. | | `due_post_only` | `bool` | `False` | If rejected because it was post-only and would execute immediately as a taker. | | `reconciliation` | `bool` | Required | If reconciliation generated the event; this does not imply venue confirmation. | ## Example Reading the event in a strategy handler: ```python def on_order_rejected(self, event: OrderRejected) -> None: self.log.warning(f"Order {event.client_order_id} rejected: {event.reason}") ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. - [Execution policies](../execution/policies.md#terminal-reconciliation-provenance) - Venue evidence and synthetic terminal policies. # OrderReleased Source: https://nautilustrader.io/docs/latest/concepts/events/order_released/ `OrderReleased` records that the `OrderEmulator` has released an order after its trigger condition was met. The emulator applies the event to the order, updates the `Cache`, and publishes it on the `MessageBus` before routing the order onward. Typical transition: `EMULATED` -> `RELEASED`. Handler: `on_order_released`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderReleased` carries: | Field | Python type | Required/default | Description | | ---------------- | ----------- | ---------------- | ----------------------------------------------------- | | `released_price` | `Price` | Required | The price which released the order from the emulator. | ## Example Reading the event in a strategy handler: ```python def on_order_released(self, event: OrderReleased) -> None: self.log.info( f"Order {event.client_order_id} released at {event.released_price}", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Emulated orders](../orders/emulated.md) - The local emulation lifecycle. # OrderSubmitted Source: https://nautilustrader.io/docs/latest/concepts/events/order_submitted/ `OrderSubmitted` represents an order having been submitted by the system to the trading venue. The `ExecutionEngine` applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. It fires when the system sends the order to the venue and awaits acknowledgement. Typical transition: `INITIALIZED` / `RELEASED` -> `SUBMITTED`. Handler: `on_order_submitted`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderSubmitted` carries: | Field | Python type | Required/default | Description | | ------------ | ----------- | ---------------- | -------------------------------------- | | `account_id` | `AccountId` | Required | The account associated with the order. | ## Example Reading the event in a strategy handler: ```python def on_order_submitted(self, event: OrderSubmitted) -> None: self.log.info(f"Order {event.client_order_id} submitted ({event.account_id})") ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # OrderTriggered Source: https://nautilustrader.io/docs/latest/concepts/events/order_triggered/ `OrderTriggered` records that a limit-style conditional order has triggered. The execution pipeline applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. A trading venue, simulated matching engine, or reconciliation can report the trigger for a `StopLimit`, `LimitIfTouched`, or `TrailingStopLimit` order. Typical transition: `ACCEPTED` -> `TRIGGERED`. Handler: `on_order_triggered`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderTriggered` carries: | Field | Python type | Required/default | Description | | ---------------- | ------------------------ | ---------------- | ------------------------------------------------ | | `venue_order_id` | `VenueOrderId` or `None` | `None` | The venue-assigned order identifier, if known. | | `account_id` | `AccountId` or `None` | `None` | The account associated with the order, if known. | | `reconciliation` | `bool` | Required | If generated during reconciliation. | ## Example Reading the event in a strategy handler: ```python def on_order_triggered(self, event: OrderTriggered) -> None: self.log.info(f"Order {event.client_order_id} triggered") ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # OrderUpdated Source: https://nautilustrader.io/docs/latest/concepts/events/order_updated/ `OrderUpdated` records a change to an order's quantity, price, trigger price, or calculated protection price. The execution pipeline applies it to the order, updates the `Cache`, and publishes it on the `MessageBus`. The change can come from a trading venue, simulated matching engine, local order emulator, or reconciliation. Typical transition: `PENDING_UPDATE` -> previous status (for example `ACCEPTED`). Handler: `on_order_updated`. ## Fields Beyond the [common Python order event fields](index.md#common-python-order-event-fields), `OrderUpdated` carries: | Field | Python type | Required/default | Description | | ------------------- | ------------------------ | ---------------- | ----------------------------------------------------------- | | `venue_order_id` | `VenueOrderId` or `None` | `None` | The venue-assigned order identifier, if known. | | `account_id` | `AccountId` or `None` | `None` | The account associated with the order, if known. | | `quantity` | `Quantity` | Required | The order's current quantity. | | `price` | `Price` or `None` | `None` | The order's current price. | | `trigger_price` | `Price` or `None` | `None` | The order's current trigger price. | | `protection_price` | `Price` or `None` | `None` | The order's calculated protection price. | | `is_quote_quantity` | `bool` | `False` | If the order quantity is denominated in the quote currency. | | `reconciliation` | `bool` | Required | If generated during reconciliation. | ## Example Reading the event in a strategy handler: ```python def on_order_updated(self, event: OrderUpdated) -> None: self.log.info( f"Order {event.client_order_id} updated: qty={event.quantity} price={event.price}", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the common order event fields. - [Orders](../orders/) - Order types and the state machine. # PositionChanged Source: https://nautilustrader.io/docs/latest/concepts/events/position_changed/ `PositionChanged` records an update that leaves a position open. The `ExecutionEngine` emits it when a fill changes an open position without closing it, or when a fill correction leaves the corrected position open. See [From fill to position](index.md#from-fill-to-position-the-causal-chain). Handler: `on_position_changed`. ## Fields See [Position event fields](index.md#position-event-fields) for the complete field matrix. In addition to the opening snapshot fields, `PositionChanged` exposes: | Field | Python type | Description | | ----------------- | ----------------- | ------------------------------------------------------------ | | `peak_quantity` | `Quantity` | The largest directional quantity reached by the position. | | `peak_qty` | `Quantity` | Compatibility alias for `peak_quantity`. | | `avg_px_close` | `float` or `None` | The average close price so far, if any quantity has closed. | | `realized_return` | `float` | The realized return for the position. | | `realized_pnl` | `Money` or `None` | The realized PnL, if available. | | `unrealized_pnl` | `Money` | Set to zero by the engine, not a mark-to-market calculation. | | `ts_opened` | `int` | UNIX timestamp (nanoseconds) when the position opened. | ## Example Reading the event in a strategy handler: ```python def on_position_changed(self, event: PositionChanged) -> None: self.log.info( f"Changed {event.instrument_id} to {event.signed_qty} (realized={event.realized_pnl})", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the fill-to-position chain. - [Positions](../positions.md) - Position lifecycle, aggregation, and PnL. - [Orders](../orders/) - Orders whose fills open and close positions. # PositionClosed Source: https://nautilustrader.io/docs/latest/concepts/events/position_closed/ `PositionClosed` records the final snapshot of a position. The `ExecutionEngine` emits it when a fill flattens the position or a fill correction leaves the corrected position closed. See [From fill to position](index.md#from-fill-to-position-the-causal-chain). Handler: `on_position_closed`. ## Fields See [Position event fields](index.md#position-event-fields) for the complete field matrix. The fields that describe the close and final result are: | Field | Python type | Description | | ------------------ | ------------------------- | ----------------------------------------------------------- | | `closing_order_id` | `ClientOrderId` or `None` | The client order ID that closed the position, if available. | | `peak_quantity` | `Quantity` | The largest directional quantity reached by the position. | | `peak_qty` | `Quantity` | Compatibility alias for `peak_quantity`. | | `avg_px_close` | `float` or `None` | The average close price, if available. | | `realized_return` | `float` | The final realized return for the position. | | `realized_pnl` | `Money` or `None` | The final realized PnL, if available. | | `unrealized_pnl` | `Money` | Set to zero by the engine. | | `duration` | `int` | The total open duration in nanoseconds. | | `ts_opened` | `int` | UNIX timestamp (nanoseconds) when the position opened. | | `ts_closed` | `int` or `None` | UNIX timestamp (nanoseconds) when the position closed. | On close, `side` is `FLAT` and `unrealized_pnl` is zero. ## Example Reading the event in a strategy handler: ```python def on_position_closed(self, event: PositionClosed) -> None: self.log.info( f"Closed {event.instrument_id}: realized={event.realized_pnl} " f"return={event.realized_return}", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the fill-to-position chain. - [Positions](../positions.md) - Position lifecycle, aggregation, and PnL. - [Orders](../orders/) - Orders whose fills open and close positions. # PositionOpened Source: https://nautilustrader.io/docs/latest/concepts/events/position_opened/ `PositionOpened` records the opening snapshot of a new position. The `ExecutionEngine` emits it when a fill creates the position (see [From fill to position](index.md#from-fill-to-position-the-causal-chain)). Handler: `on_position_opened`. ## Fields See [Position event fields](index.md#position-event-fields) for the complete field matrix. `PositionOpened` contains the opening snapshot; later events expose additional aggregate and lifecycle fields. Its main state fields are: | Field | Python type | Description | | -------------- | ----------------- | -------------------------------------------------- | | `entry` | `OrderSide` | The entry order side that opened the position. | | `side` | `PositionSide` | The current position side (`LONG` or `SHORT`). | | `signed_qty` | `float` | The signed position quantity. | | `quantity` | `Quantity` | The current open quantity. | | `last_qty` | `Quantity` | The quantity of the fill that opened the position. | | `last_px` | `Price` | The price of the fill that opened the position. | | `currency` | `Currency` | The position quote currency. | | `avg_px_open` | `float` | The average open price. | | `realized_pnl` | `Money` or `None` | The current cycle's realized PnL in cost currency. | At opening, realized PnL subtracts the opening fill's commission when that commission is denominated in the instrument's [cost currency](../positions.md#currency-considerations). Otherwise, it is zero. A reopened `NETTING` position starts a new cycle; realized PnL from the prior cycle remains in the [closed position snapshot](../positions.md#position-snapshotting). ## Example Reading the event in a strategy handler: ```python def on_position_opened(self, event: PositionOpened) -> None: self.log.info( f"Opened {event.side} {event.quantity} {event.instrument_id} @ {event.avg_px_open}", ) ``` ## Related guides - [Events](index.md) - Event categories, dispatch, and the fill-to-position chain. - [Positions](../positions.md) - Position lifecycle, aggregation, and PnL. - [Orders](../orders/) - Orders whose fills open and close positions. # Execution Algorithms Source: https://nautilustrader.io/docs/latest/concepts/execution/algorithms/ An `ExecutionAlgorithm` receives primary orders selected by `exec_algorithm_id` and can split them into smaller spawned orders. NautilusTrader supports custom algorithms and includes a native Rust TWAP implementation. Use this page to configure TWAP, write an algorithm, and manage spawned orders. For the component and routing model, see [Execution](index.md#execution-flow). ## TWAP (time-weighted average price) TWAP spreads a primary order across regular intervals to reduce the market impact of submitting the full quantity at once. To register the native algorithm with an initialized `BacktestEngine`: ```python from nautilus_trader.model import ExecAlgorithmId from nautilus_trader.config import ExecutionAlgorithmConfig engine.add_native_exec_algorithm( "TwapAlgorithm", ExecutionAlgorithmConfig(exec_algorithm_id=ExecAlgorithmId("TWAP")), ) ``` Orders routed to TWAP require these string-valued `exec_algorithm_params`: | Key | Meaning | | --------------- | ------------------------------------------------------- | | `horizon_secs` | Horizon used with the interval to determine the slices. | | `interval_secs` | Time between slices. | Both values must parse as positive numbers, and `horizon_secs` must be at least `interval_secs`. The algorithm submits the first slice immediately and the remaining slices at the configured interval. TWAP denies the primary order before submission when the order type, instrument, or schedule is unsupported or invalid. ## Custom execution algorithms To define a Python execution algorithm, subclass `ExecutionAlgorithm` and implement `on_order(...)`: ```python from nautilus_trader.model import ExecAlgorithmId from nautilus_trader.trading import ExecutionAlgorithm from nautilus_trader.config import ExecutionAlgorithmConfig class MyExecutionAlgorithm(ExecutionAlgorithm): def __init__(self) -> None: super().__init__( ExecutionAlgorithmConfig(exec_algorithm_id=ExecAlgorithmId("MY-ALGO")), ) def on_order(self, order) -> None: ... ``` Python execution algorithms provide cache and portfolio access, a clock for timers, signals, and methods for spawning orders. After registration, the message bus routes an order to the algorithm whose `ExecAlgorithmId` matches the order's `exec_algorithm_id`. The optional `exec_algorithm_params` field is a `Mapping[str, str]`. Override `on_order_list(...)` to handle a list as a unit; its default implementation passes each order to `on_order(...)`. Validate required `exec_algorithm_params` keys and parse their string values before executing an order. Call `deny_order(...)` with a standardized [reason code](index.md#order-denied-reasons), such as `VALIDATION_FAILED: horizon_secs not found in exec_algorithm_params`, when the order cannot be executed. An order received by an execution algorithm is the **primary order**. Use these methods to create **spawned orders**: - `spawn_market(...)`: Creates a `MARKET` order. - `spawn_market_to_limit(...)`: Creates a `MARKET_TO_LIMIT` order. - `spawn_limit(...)`: Creates a `LIMIT` order. Each method takes the primary order as its first argument. By default, the method reduces the primary order quantity by the spawned `quantity`. Pass `reduce_primary=False` to keep the primary quantity unchanged. :::warning When `reduce_primary=True`, the spawned quantity must not exceed the primary order's `leaves_qty` (remaining unfilled quantity). ::: If a spawned order is denied, rejected, canceled, expired, or refused before submission, its unfilled proportion is restored in the primary order's quantity units while the primary remains local. This also applies when a venue converts a quote-quantity spawn to base quantity. Once primary submission is handed off, its quantity is committed and is not changed by a later spawn outcome. A late fill on a canceled spawn re-deducts the corresponding restored quantity while the primary remains locally mutable; if that quantity was already reused by a later spawn, the excess is netted from that spawn's own restoration instead. Converted quote-quantity spawns calculate the remaining unfilled quantity from cumulative fills and round it down to the primary's quantity precision. The total deduction does not depend on how many fill events report the filled quantity. If a [fill is voided](../events/order_fill_voided.md) after any spawn's unfilled quantity was restored, the correction returns the additional unfilled quantity while the primary remains local. It first offsets any late-fill quantity that could not be deducted from the primary. An execution algorithm can keep spawning orders, submit the remaining primary order, or do both. The built-in TWAP algorithm submits the remaining primary order on the final interval. ## Spawned orders Every spawned order sets `exec_spawn_id` to the primary order's `client_order_id`. Its own `client_order_id` follows this pattern: ```text {exec_spawn_id}-E{spawn_sequence} ``` For example, the first order spawned from `O-20230404-001-000` has the ID `O-20230404-001-000-E1`. :::note The primary and spawned terminology distinguishes execution slicing from parent and child contingent-order relationships. ::: ## Execution algorithm order queries The `Cache` provides two primary queries: - `orders_for_exec_algorithm(...)`: Returns orders for an algorithm, with optional venue, instrument, strategy, account, and side filters. - `orders_for_exec_spawn(...)`: Returns the primary order and its spawned orders for a primary `ClientOrderId`. ## Related guides - [Execution](index.md): Component routing, OMS behavior, risk checks, and command outcomes. - [Execution policies](policies.md): Order-state and command-delivery boundaries. - [Orders](../orders/): Order types, instructions, and state transitions. # Execution Source: https://nautilustrader.io/docs/latest/concepts/execution/ NautilusTrader coordinates order submission, risk checks, venue execution, reconciliation, and position updates across multiple strategies and venues. This page explains the components and message flows that support execution. Use the execution guides according to the question you need to answer: | Question | Guide | | ---------------------------------------------------- | ------------------------------------------------------------------ | | Which components handle an order command? | This page: [Execution flow](#execution-flow). | | Which statuses and transitions can an order have? | [Orders: order state flow](../orders/index.md#order-state-flow). | | Which policies govern venue-boundary execution? | [Execution policies](policies.md). | | How do execution algorithms split and manage orders? | [Execution algorithms](algorithms.md). | | How does live state recover and remain consistent? | [Execution reconciliation](reconciliation.md). | | How do I configure a live node? | [Live node configuration](../../how_to/configure_live_trading.md). | | How does a live node schedule and monitor execution? | [Live trading](../live.md). | The main execution-related components include: - `Strategy` - `ExecutionAlgorithm` - `OrderEmulator` - `RiskEngine` - `ExecutionEngine` - `ExecutionClient` ## Execution flow A `Strategy` builds on data actor capabilities and adds methods for managing orders and execution: - `submit_order(...)` - `submit_order_list(...)` - `modify_order(...)` - `cancel_order(...)` - `cancel_orders(...)` - `cancel_all_orders(...)` - `close_position(...)` - `close_all_positions(...)` - `query_account(...)` - `query_order(...)` These methods send point-to-point execution commands over the message bus. Order creation also publishes events such as `OrderInitialized`. Commands follow different routes: - `submit_order(...)` routes to `OrderEmulator` for emulated orders, to an `ExecutionAlgorithm` when `exec_algorithm_id` is set, and to the `RiskEngine` otherwise. - `submit_order_list(...)` follows the same branching behavior based on emulation and `exec_algorithm_id`. - `modify_order(...)` routes to the `OrderEmulator` for emulated orders, to an `ExecutionAlgorithm` when the order has an `exec_algorithm_id` and is still active within the local system, and to the `RiskEngine` otherwise. - Cancel and query commands can route directly to the `OrderEmulator`, `ExecutionAlgorithm`, or `ExecutionEngine`, depending on the command and order state. New orders typically enter one of these paths: `Strategy` -> `OrderEmulator` or `ExecutionAlgorithm` or `RiskEngine` The downstream flow is: `OrderEmulator` -> `ExecutionAlgorithm` or `ExecutionEngine` `ExecutionAlgorithm` -> `RiskEngine` -> `ExecutionEngine` -> `ExecutionClient` ```mermaid flowchart LR strategy[Strategy] emulator[OrderEmulator] algo[ExecutionAlgorithm] risk[RiskEngine] engine[ExecutionEngine] client[ExecutionClient] strategy --> emulator strategy --> algo strategy --> risk strategy --> engine emulator -. OrderReleased .-> risk emulator --> algo emulator --> engine algo --> risk risk <--> engine engine <--> client ``` Execution paths branch by emulation and algorithm routing before reaching the execution engine and client. ## Order management system (OMS) An **order management system (OMS)** type determines how orders map to positions for an instrument. Strategies and venues, whether simulated or live, each use an OMS type defined by the `OmsType` enum. The `OmsType` enum has three variants: - `UNSPECIFIED`: The strategy uses the venue's OMS type. - `NETTING`: Positions combine into one position per instrument and strategy. - `HEDGING`: Multiple positions per instrument and strategy can remain open. When the strategy and venue OMS types differ, the `ExecutionEngine` assigns or overrides `position_id` values on `OrderFilled` events. A **virtual position** exists in NautilusTrader but not as a separate venue position. | Strategy OMS | Venue OMS | Result | | ------------ | --------- | ------------------------------------------------------------------- | | `NETTING` | `NETTING` | One position per instrument and strategy. | | `HEDGING` | `HEDGING` | Multiple positions per instrument and strategy. | | `NETTING` | `HEDGING` | One virtual position across the venue positions. | | `HEDGING` | `NETTING` | Multiple virtual positions against the venue's single net position. | If a fill resolves to a cached position for a different instrument, the `ExecutionEngine` logs an error and drops the fill. The order remains non-terminal so a subsequent valid fill can be applied. For reductions of inherited inventory, see [Reducing external positions](reconciliation.md#reducing-external-positions). ### OMS configuration When a strategy omits `oms_type` or uses `UNSPECIFIED`, the `ExecutionEngine` follows the venue's OMS type without overriding venue `position_id` values. Configure a backtest venue with the OMS type used by the venue being modeled. Venue position modes may require adapter-specific configuration. For example, see [Binance Futures hedge mode](../../integrations/binance.md#futures-hedge-mode). ### Custom position IDs and NETTING Custom position IDs are only valid under `HEDGING` OMS. `NETTING` has one position per instrument and strategy, with a deterministic ID of the form `{instrument_id}-{strategy_id}`. The `ExecutionEngine` enforces this at submit time. If the effective OMS resolves to `NETTING` and `submit_order` (or `submit_order_list`) is called with a `position_id` that does not match `{instrument_id}-{strategy_id}`, the order is denied with an `OrderDenied` event explaining the mismatch. This rule still permits the common closing idiom: `Strategy.close_position(position)` forwards `position.id`, which under `NETTING` is exactly the deterministic ID, so it is accepted. To label or partition positions with arbitrary IDs, configure the strategy with `oms_type=HEDGING`. For `submit_order_list`, the engine additionally denies any mixed-instrument list when a `position_id` is supplied, regardless of OMS. A position belongs to a single instrument, so the combination is rejected with an explicit `OrderDenied` reason. See [Order lists](../orders/advanced.md#order-lists) for the broader set of mixed-instrument caveats. ### Position replay across NETTING cycles Under `NETTING` the engine reuses one position ID across close and reopen cycles, so a position's replay log can accumulate every fill ever applied to that ID. The `ExecutionEngineConfig.carry_replay_events_on_reopen` option controls whether that log survives a reopen: | `carry_replay_events_on_reopen` | Behavior | | ------------------------------- | -------------------------------------------------------------- | | `False` (default) | Keeps only current-cycle state, bounding the per-fill cost. | | `True` | Keeps earlier fills correctable while position state can grow. | Live trading pins the option `True`: `LiveExecutionEngineConfig` always carries the replay log, so a venue [`OrderFillVoided`](../events/order_fill_voided.md) referencing an earlier cycle still resolves. The simulated venue never emits fill voids, so backtests take the bounded default. Enable it explicitly for a custom or external execution client that can correct a fill from a prior cycle; without the carried log the engine finds no matching position fragment and rejects the correction. Realized-PnL snapshots follow the correction. A fill void that reaches an earlier cycle rebuilds the position across the cycle boundary, moving the boundaries its archived snapshots describe, so the engine settles those snapshots into the corrected history's own closed cycles and realized PnL counts each cycle once. A void confined to the current cycle leaves the archive intact. See [Position snapshotting](../positions.md#position-snapshotting). ## Risk engine The `RiskEngine` is a component of every Nautilus system, including backtest, sandbox, and live environments. It sits on the submit and modify path, and it also receives order events such as `OrderReleased` from the `OrderEmulator`. Cancel and query commands route directly to other execution components and do not pass through the `RiskEngine`. Unless bypassed in `RiskEngineConfig`, the engine validates: - Price and trigger-price precision for the instrument. - Positive prices, unless the instrument allows negative prices (options, futures spreads, option spreads, and spot commodities). - Quantity precision and base-quantity minimum and maximum bounds. - GTD orders have not already expired. - `reduce_only` orders do not increase the referenced position. - Engine-level `max_notional_per_order` limits and the instrument's `min_notional` and `max_notional` fields. - Cash-account balance impact for non-margin accounts. - Submit and modify rate limits. - Trading-state restrictions (`ACTIVE`, `HALTED`, `REDUCING`). If a submit-time risk check fails, the system generates an `OrderDenied` event with a standardized [reason code](#order-denied-reasons). If a modify-time risk check fails, it generates an `OrderModifyRejected` event. ### Whole-position conditional exits Some execution clients support conditional exits whose venue determines the closing quantity from the open position when the trigger fires. Nautilus orders still carry a placeholder quantity for local validation. The `full_position_exit_venues` setting on `RiskEngineConfig` and `LiveRiskEngineConfig` identifies venues whose execution clients enforce these semantics. It defaults to empty. An order qualifies for the placeholder exemption only when all of these conditions hold: - The order is submitted individually, not in an order list. - Its venue is listed in `full_position_exit_venues`. - It uses a supported futures or perpetual instrument. - It is a `StopMarket` or `MarketIfTouched` order with a trigger price and `close_position=true`. - It has a positive placeholder quantity and sets `reduce_only=true`. - The command, order, and linked cached position use the same instrument and position ID. - The linked position is open, the order side closes it, and the placeholder quantity does not exceed the position quantity. For a qualifying exit, the risk engine treats checks as follows: | Risk check | Treatment | | --------------------------------------------------------------------- | ------------------------------------------- | | Quantity precision and positivity | Enforced. | | Price and trigger-price precision and positivity | Enforced. | | GTD expiration, trading-state restrictions, and submission rate limit | Enforced. | | Position exposure, margin, and balance | Treated as position-reducing. | | Instrument minimum and maximum quantity | Skipped for the placeholder quantity. | | Instrument minimum and maximum notional | Skipped for the placeholder notional. | | Configured `max_notional_per_order` | Skipped for the placeholder notional. | | Non-qualifying orders | All ordinary risk checks continue to apply. | Only allowlist a venue when its downstream execution client enforces whole-position closing. See [Binance Futures close-position orders](../../integrations/binance.md#close-position) for a supported configuration. :::warning The simulated exchange does not interpret `close_position` or replace the placeholder with the open position quantity. Leave simulated backtest venues out of `full_position_exit_venues`; model a backtest exit with an explicit quantity and `reduce_only` instead. ::: ### Trading state The states become progressively more restrictive: | State | Numeric value | Permitted commands | | ---------- | ------------: | ---------------------------------------------------------------------------- | | `ACTIVE` | 1 | Submit, modify, cancel, and query commands operate normally. | | `REDUCING` | 2 | Eligible individual reduce-only submissions, cancels, and queries. | | `HALTED` | 3 | Cancels and queries only. New submissions and modifications are not allowed. | In `REDUCING`, an individual `SubmitOrder` is eligible only when the order sets `reduce_only=true`, the command and order identify the same instrument, and the supplied position ID matches the order's cached open position. The order side must oppose the position, and the submitted quantity must not exceed the cached position quantity. Order lists and modifications are denied. The risk engine applies these rules before forwarding commands to execution. :::warning[Bypassing trading-state checks] When `RiskEngineConfig.bypass` is enabled, trading state is not enforced. Execution clients still follow the [reduce-only send-or-reject contract](../adapters.md#reduce-only-execution-contract). ::: See the [`RiskEngineConfig` API reference](/docs/python-api-latest/config.html#nautilus_trader.risk.RiskEngineConfig) for configuration details. ## Execution algorithms An `ExecutionAlgorithm` receives primary orders selected by `exec_algorithm_id` and can split them into smaller spawned orders. NautilusTrader supports custom algorithms and includes a native Rust TWAP implementation. See [Execution algorithms](algorithms.md) for TWAP configuration, custom algorithms, spawned-order behavior, and cache queries. ## Cancel-all routing `Strategy.cancel_all_orders(...)` supports strategy-scoped and broad cancellation: | `strategy_only` | Strategy output | Scope | Downstream routing | | --------------- | ------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------- | | `True` | One `CancelOrder` per matching order. | Matching orders associated with the calling strategy. | Each order follows its normal cancel route. | | `False` | One root `CancelAllOrders`, even without local matches. | Matching orders for one resolved execution client and account. | The execution engine creates the required children. | Broad mode delegates before the strategy inspects its cache. The command therefore reaches the resolved execution client even when NautilusTrader has no matching local order. This allows a venue bulk-cancel endpoint to remove an order that exists at the venue but is missing from the local cache. When the adapter provides such an endpoint, the single command can also reduce cancel request volume. For a local execution client, the `ExecutionEngine` resolves the root command to exactly one client in this order: 1. The explicit `client_id`, when it identifies a registered local client. 1. The client registered for the instrument's venue. 1. The default execution client. The engine then creates fresh child commands for the selected client and its account: - One `CancelAllOrders` for the execution client, covering matching venue orders. - One `CancelAllOrders` for the `OrderEmulator`, covering matching emulated orders. - One `CancelOrder` per eligible active-local execution-algorithm order. ```mermaid flowchart LR call[Strategy.cancel_all_orders] scope{strategy_only?} exact[Exact CancelOrder per matching strategy order] root[One root CancelAllOrders] external{External client?} pass[Pass through unchanged] resolve[Resolve one explicit, venue, or default client and account] venue[One venue CancelAllOrders] emulator[One emulator CancelAllOrders] algo[Exact CancelOrder per eligible algorithm order] call --> scope scope -->|True| exact scope -->|False| root root --> external external -->|Yes| pass external -->|No| resolve resolve --> venue resolve --> emulator resolve --> algo ``` Broad mode selects one client before fan-out; it never broadcasts across all execution clients. Call `cancel_all_orders(...)` once per client to cancel across several clients. Every child has a new command ID, copies the root parameters, correlates to the root operation, and records the root command as its cause. Instrument and optional side filters apply to every local route. The selected execution account also bounds matching-engine cancellation, including orders in `SUBMITTED` and other cancelable in-flight states. Client ownership applies to local emulated and execution-algorithm orders: - Orders already assigned to another client remain untouched. - When the root omits `client_id`, matching unassigned orders are claimed by the client selected by the engine before local cancellation. - When the root supplies `client_id`, unassigned orders remain untouched because the engine cannot infer that they belong to the explicit client. - An emulated order matches its traded instrument, even when another instrument supplies its trigger. An explicitly configured external execution client receives the original root command unchanged. The external client owns any fan-out needed behind that boundary. ## Command outcomes Execution commands distinguish definitive local failures, definitive venue results, and unknown live outcomes. An unknown outcome remains in flight for stream updates, polling, queries, or reconciliation. Retry exhaustion can later apply a synthetic terminal reconciliation event. See [Execution policies](policies.md#command-outcomes) for the evidence classes, delivery and retry limits, persistence boundary, and terminal reconciliation provenance. See [Runtime checks](reconciliation.md#runtime-checks) for the continuous reconciliation procedure. ## Order denied reasons A local denial (`OrderDenied`) carries a standardized `CATEGORY_CONDITION` reason code and may include a diagnostic suffix. Only the leading code is canonical. Messages use these forms: - `CODE` when the denial needs no diagnostic suffix. - `CODE: value` for one typed value or a free-text diagnostic. - `CODE: key=value, key=value` when multiple typed values need disambiguation. - `CODE: value; free text` when one typed value precedes a free-text diagnostic. The table covers local denials emitted by execution algorithms and clients as well as the risk and execution engines. These codes are the source of truth for locally denied orders. Venue-confirmed `OrderRejected` events instead carry the venue-provided meaning, while synthetic reconciliation rejections use the reasons documented under [Terminal reconciliation provenance](policies.md#terminal-reconciliation-provenance). Adapters remove protocol wrappers and bound untrusted venue text before emission without replacing it with a standardized local denial code. Price and quantity checks can also emit these code-led reasons on `OrderModifyRejected`: - `PRICE_PRECISION_EXCEEDS_MAXIMUM` - `PRICE_NOT_POSITIVE` - `QUANTITY_PRECISION_EXCEEDS_MAXIMUM` - `QUANTITY_EXCEEDS_MAXIMUM` - `QUANTITY_BELOW_MINIMUM` For price reasons, `field` is `PRICE` or `TRIGGER_PRICE` and names the rejected command field. Other modification rejection reasons remain free-form; `OrderDeniedCode` does not classify them. `OrderRejected.due_post_only` is `true` only when venue evidence proves that a post-only order would cross or immediately match. Other venue rejections leave it `false`. | Code | Description | | ------------------------------------------------ | ------------------------------------------------------------------------------------- | | `PRICE_PRECISION_EXCEEDS_MAXIMUM` | The price precision exceeds the instrument maximum. | | `PRICE_NOT_POSITIVE` | The price is not positive. | | `QUANTITY_PRECISION_EXCEEDS_MAXIMUM` | The quantity precision exceeds the instrument maximum. | | `QUANTITY_CONVERSION_FAILED` | The order quantity could not be converted for risk checks. | | `QUANTITY_EXCEEDS_MAXIMUM` | The effective order quantity exceeds the instrument maximum. | | `QUANTITY_BELOW_MINIMUM` | The effective order quantity is below the instrument minimum. | | `INVALID_MAX_NOTIONAL_PER_ORDER` | The configured maximum notional per order is invalid. | | `MISSING_EXPIRE_TIME` | A GTD order is missing its expire time. | | `EXPIRE_TIME_IN_PAST` | The order's expire time is in the past. | | `MISSING_TRAILING_OFFSET_TYPE` | The order is missing a required trailing offset type. | | `UNSUPPORTED_TRAILING_OFFSET_TYPE` | The order's trailing offset type is not supported. | | `MISSING_TRIGGER_TYPE` | The order is missing a required trigger type. | | `MISSING_TRAILING_OFFSET` | The order is missing a required trailing offset. | | `INSTRUMENT_NOT_FOUND` | The instrument was not found in the cache. | | `POSITION_NOT_FOUND` | The position for a reduce-only order was not found. | | `MARKET_PRICE_UNAVAILABLE` | No market price is available for the order risk check. | | `TRAILING_STOP_CALCULATION_FAILED` | The trailing stop trigger price could not be calculated. | | `NOTIONAL_CALCULATION_FAILED` | The order notional value could not be calculated. | | `NOTIONAL_BELOW_MINIMUM` | The order notional is below the instrument minimum. | | `NOTIONAL_EXCEEDS_MAXIMUM` | The order notional exceeds the instrument maximum. | | `NOTIONAL_EXCEEDS_MAX_PER_ORDER` | The order notional exceeds the configured maximum per order. | | `NOTIONAL_EXCEEDS_FREE_BALANCE` | The order notional exceeds the account free balance. | | `INITIAL_MARGIN_CALCULATION_FAILED` | The order initial margin could not be calculated. | | `INITIAL_MARGIN_EXCEEDS_FREE_BALANCE` | The order initial margin exceeds the account free balance. | | `BETTING_BALANCE_LOCKED_CALCULATION_FAILED` | The balance to lock for the betting order could not be calculated. | | `CUMULATIVE_NOTIONAL_EXCEEDS_FREE_BALANCE` | The cumulative order notional exceeds the account free balance. | | `CUMULATIVE_INITIAL_MARGIN_CALCULATION_FAILED` | The cumulative initial margin could not be calculated. | | `CUMULATIVE_INITIAL_MARGIN_EXCEEDS_FREE_BALANCE` | The cumulative initial margin exceeds the account free balance. | | `REDUCE_ONLY_WOULD_INCREASE_POSITION` | A reduce-only order would increase the position. | | `ORDER_LIST_INCOMPLETE` | The order list is missing orders in the cache. | | `ORDER_LIST_DENIED` | The order was denied because its order list failed risk checks. | | `TRADING_HALTED` | Trading is halted; new submissions and modifications are denied. | | `TRADING_STATE_REDUCING` | Trading is reducing; only eligible reduce-only submissions are permitted. | | `RATE_LIMIT_EXCEEDED` | The order submission rate limit was exceeded. | | `STREAM_RECONCILING` | The execution stream is unavailable or recovering; retry after recovery. | | `NO_EXECUTION_CLIENT` | No execution client was found for the routed command. | | `CLIENT_VENUE_MISMATCH` | The execution client does not handle the order venue. | | `SUBMIT_FAILED` | Submitting the order to the execution client failed. | | `INVALID_CLIENT_ORDER_ID` | The client order ID is invalid for the venue. | | `INVALID_POSITION_ID` | The supplied position ID is invalid for the order submission. | | `UNSUPPORTED_ORDER_LIST` | The venue does not support the requested order list. | | `UNSUPPORTED_ORDER_TYPE` | The order type is not supported. | | `UNSUPPORTED_REDUCE_ONLY` | The execution client or venue does not support the requested reduce-only instruction. | | `UNSUPPORTED_TIME_IN_FORCE` | The order's time in force is not supported. | | `UNSUPPORTED_TP_SL` | The venue does not support the requested take-profit/stop-loss parameters. | | `VALIDATION_FAILED` | The order failed validation before submission. | ## Own order books When `manage_own_order_books` is enabled, the `ExecutionEngine` maintains a market-by-order (MBO/L3) view of your working orders for each instrument. Strategies can subtract these orders from the public book to estimate net available liquidity. See [Own order book](../order_book.md#own-order-book) for lifecycle, queries, filtering, and auditing. ### Safe cancellation queries When querying an own order book for cancellation candidates, exclude `PENDING_CANCEL` from the `status` filter. :::warning Including `PENDING_CANCEL` can issue duplicate cancel requests and repeatedly select orders that already await confirmation. ::: ## Overfills An **overfill** occurs when an order's cumulative filled quantity exceeds its original quantity. For example, fills totaling 110 units overfill a 100-unit order by 10 units. ### How overfills occur The engine observes an overfill when reported quantities exceed the order quantity. This can represent a genuine venue result, duplicate delivery under different trade IDs, or inconsistent venue reporting. Quantity alone does not identify the cause. Live fills can arrive through two channels: - Real-time fill events arriving via WebSocket. - Periodic reconciliation polling the venue for fill history and position status. Stable `trade_id` values let the engine deduplicate the same fill across both channels. If the logical fill arrives with different IDs, the engine treats the reports as distinct. See [Continuous reconciliation](../../how_to/configure_live_trading.md#continuous-reconciliation) for configuration details. ### System behavior The `ExecutionEngine` checks for potential overfills before applying each fill event by comparing the order's current `filled_qty` plus the incoming `last_qty` against the original `quantity`. The `allow_overfills` configuration option (default: `False`) controls how overfills are handled: | `allow_overfills` | Behavior | | ----------------- | -------------------------------------------------------------------------- | | `False` | Logs and rejects the fill, preserving the order's current state. | | `True` | Logs a warning, applies the fill, and tracks the excess in `overfill_qty`. | When overfills are allowed, the order's `overfill_qty` field tracks the excess quantity. The order transitions to `FILLED` status and `leaves_qty` is clamped to zero. ### Duplicate fill detection The `Order` model enforces one applied fill per `trade_id`. `Order.apply()` returns an error when the same ID already exists on the order. #### Core engine path Before applying a fill, the `ExecutionEngine` calls `Order.is_duplicate_fill()`, which compares: - `trade_id` - `order_side` - `last_px` - `last_qty` An exact match is skipped with a warning. If the `trade_id` matches but another field differs, the four-field check does not classify the fill as an exact duplicate. `Order.apply()` then rejects the reused ID, and the engine logs and drops the fill. #### Reconciliation path The reconciliation path checks `trade_id` before generating an `OrderFilled` event. It drops a report when that ID already exists on the order, regardless of its price or quantity. Synthetic and inferred reconciliation fills use deterministic IDs. Replaying the same inputs after a restart therefore produces the same `trade_id` and is deduplicated. ### Configuration For live trading, enable overfill tolerance in the `LiveExecutionEngineConfig`: ```python from nautilus_trader.config import LiveExecutionEngineConfig config = LiveExecutionEngineConfig( allow_overfills=True, ) ``` :::warning Choose this setting from the venue's execution contract. The default `False` protects local state but can leave a discrepancy after a legitimate venue overfill. `True` applies the excess quantity and is not a substitute for duplicate-fill detection. Use [execution reconciliation](reconciliation.md) to detect discrepancies. ::: ## Fill corrections Some venues can later reduce or invalidate a fill. Nautilus records this as an [`OrderFillVoided`](../events/order_fill_voided.md) event, never as an opposite-side fill. The event identifies the original trade and carries the cumulative voided quantity and fee correction. The execution engine rebuilds the affected order and positions and refreshes portfolio position and PnL caches before publishing the correction to strategies and execution algorithms. Adapters that support fill corrections request an authoritative account refresh after a void. Adapters must publish the referenced fill before a reopened correction or a partial correction that leaves the order executable. Without a local fill, a non-reopened correction makes the whole order terminal, even when `voided_qty` is less than the order quantity. A later working status report does not reopen `VOIDED`. See the complete [`OrderFillVoided` contract](../events/order_fill_voided.md#contract). ### How voided fills occur A void is a venue action on a trade it already reported. The causes recur across asset classes: - **Erroneous execution review**: the venue nullifies a print that is substantially inconsistent with the market at the time of execution, or one caused by an exchange system fault. - **Settlement failure**: a matched trade fails to settle, so the fill never takes economic effect. - **Event invalidation**: the underlying event is abandoned or a competitor is withdrawn, so matched positions carry no exposure. - **Post-trade restatement**: the venue restates the quantity or fees of a trade during clearing. The event does not restate the fill price, so a venue price adjustment is not expressible as a single correction. A break reaches the client differently by venue. FIX venues signal one through [`ExecType <150>`](https://www.onixs.biz/fix-dictionary/5.0.sp2/tagnum_150.html) values `H` (trade cancel) and `G` (trade correct). Venues that notify out of band leave the break to surface through [execution reconciliation](reconciliation.md). ### Venue references Each venue publishes the conditions under which it acts: | Venue | Mechanism | Reference | | ---------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Nasdaq | Clearly erroneous transactions (Rule 11890). | [Clearly erroneous transactions policy](https://www.nasdaqtrader.com/Trader.aspx?id=ClearlyErroneous). | | NYSE | Clearly erroneous executions (Rule 7.10). | [Clearly erroneous execution review](https://www.nyse.com/trade/cee). | | Cboe US equities | Clearly erroneous executions (BZX Rule 11.17). | [Clearly erroneous execution form](https://www.cboe.com/us/equities/trading/cee_form/). | | CME Group | Trade cancellations and price adjustments (Rule 588). | [CME rulebook chapter 5](https://www.cmegroup.com/rulebook/CME/I/5/5.pdf). | | Betfair | Voided bets, reported as cumulative size voided (`sv`). | [Void bets on the Stream API](https://support.developer.betfair.com/hc/en-us/articles/360000391492-How-are-void-bets-treated-by-the-Stream-API). | | Polymarket | `FAILED` trade status after an on-chain revert or reorg. | [User channel](https://docs.polymarket.com/developers/CLOB/websocket/user-channel). | Nautilus adapters emit `OrderFillVoided` where the venue publishes the void on a stream the adapter consumes: [Betfair](../../integrations/betfair.md#voided-fills) from the order change message `sv` field, and [Polymarket](../../integrations/polymarket.md#trades) from the user channel trade status. ## Related guides - [Events](../events/): Order and position event types and dispatch. - [Execution algorithms](algorithms.md): TWAP, custom algorithms, and spawned orders. - [Execution policies](policies.md): Delivery, state, persistence, and recovery boundaries. - [Execution reconciliation](reconciliation.md): Live state recovery and runtime consistency checks. - [Order book](../order_book.md): Public and own order book behavior. - [Orders](../orders/): Order types and management. - [Positions](../positions.md): Position tracking from executions. - [Strategies](../strategies.md): Order submission from strategies. # Execution Policies Source: https://nautilustrader.io/docs/latest/concepts/execution/policies/ NautilusTrader coordinates local state with trading venues across a distributed boundary. This page defines the policies that govern order commands, order events, persistence, and reconciliation, including their documented behavior and known limits. Use it when interpreting an order state, implementing an execution adapter, or designing live recovery procedures. For the component and routing model, see [Execution](index.md). For every order status and the primary state transitions, see [Orders](../orders/index.md#order-state-flow). ## Policy summary | Boundary | Behavior | Limit | | ----------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Order state | Each applied event must satisfy the order state machine and identity checks. | A status alone does not identify whether venue evidence or reconciliation produced it. | | Command outcome | Adapters distinguish definitive local failures, definitive venue results, and unknown outcomes. | A transport result does not necessarily prove what the venue did. | | Command delivery | Adapters retry state-changing commands only when repeating the same request is safe. | NautilusTrader does not guarantee exactly-once delivery across the venue boundary. | | Event application | Order identity and transition checks reject invalid events; fills reject a repeated `trade_id`. | No blanket exactly-once guarantee applies to every event type or across lost retained state. | | Persistence before send | The cache enqueues the order and resolved execution-client origin before calling the client. | The built-in cache backends do not wait for durable storage before the client can send. | | Bounded recovery | Reconciliation recovers reported order state without applying unsupported fill economics. | Partial history does not prove historical position economics, realized PnL, or average price. | | Terminal policy | Reconciliation may resolve missing or timed-out orders after configured retries. | A policy resolution is not a venue-confirmed rejection or cancellation. | ## Order state Orders are event sourced. Every order starts with `OrderInitialized`, and `OrderCore::apply` validates the event against the order identity and the transition allowed from its present status. It rejects an invalid transition or a repeated fill `trade_id` before changing the order, then appends each accepted event to the order's event history. The [order state flow](../orders/index.md#order-state-flow) shows the primary lifecycle. The model also accepts selected recovery and real-world edge cases, including fills received while a command is pending and late fills for canceled orders. Each event page under [Events](../events/) documents its fields and typical transition. ### Ordering Within one live node, the runner handles each selected message branch to completion before it selects another. This serializes kernel-side order mutation. The order appends each accepted event in application order and does not reorder its history by event timestamp. When several channels are ready, the runner's priority determines which it handles next. Events from independent adapter tasks or venues can therefore interleave, and event timestamps do not define a global FIFO order. See [Dispatch priority and overload behavior](../live.md#dispatch-priority-and-overload-behavior). ### Duplicate application NautilusTrader does not use `event_id` as a universal order-level deduplication key and does not guarantee exactly-once application for every order event. Its narrower protections are: - An order rejects a second fill with the same `trade_id`. - The execution engine prevents the same `trade_id` from being applied again to the target position. - Fill voids use the original `trade_id` and reject duplicate, stale, conflicting, or excessive cumulative corrections. - Other repeated lifecycle events must still pass the state transition. Some state-preserving updates and repeated pending requests are valid events and can be appended again. These checks depend on the order and position evidence retained in the cache. Restored state keeps its earlier trade IDs and event history available after restart. If that state and the required venue history are absent, NautilusTrader cannot infer exactly-once application from the missing evidence. Reports that describe one logical fill with different trade IDs remain distinct and are subject to the normal overfill and integrity checks. The optional [event store](../event_sourcing.md) has a separate boundary. Its capture adapter deduplicates repeated dispatches of one message identity within a bounded recent window, and replay applies each stored sequence entry once. This does not make an uncommitted capture durable or make venue delivery exactly once. ## Command outcomes Execution commands resolve according to the evidence available: | Evidence | Meaning | Result | | ------------------------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | Definitive local failure | Validation proves that the command was not sent. | Denies a submit or rejects a modify or cancel when the failure is attributable to that command. | | Definitive venue result | The matching engine or venue explicitly confirms the outcome. | Applies the corresponding accepted, updated, canceled, or rejected event. | | Unknown live outcome | The command may have reached the venue, but no result is known. | Keeps the command in flight for stream updates, polling, queries, or reconciliation. | The failure event depends on the command and when the failure becomes definitive: | Command | Event | Meaning | | ----------------------------------- | --------------------- | ---------------------------------------------------------------------- | | Submit or submit order list | `OrderDenied` | Local checks prevent submission; no `OrderSubmitted` event is emitted. | | Submit or submit order list | `OrderRejected` | The submit entered execution and was later proven unsuccessful. | | Modify | `OrderModifyRejected` | The requested modification was proven unsuccessful. | | Cancel, cancel-all, or batch cancel | `OrderCancelRejected` | The requested cancellation was proven unsuccessful. | For modify or cancel preparation, NautilusTrader emits the matching rejection only when the failure is attributable to that command and proves it was not sent. Otherwise, it logs the failure without inventing an outcome. A successful batch response can still contain definitive per-order failures. A whole-request failure without per-order evidence does not prove that every child command failed. :::warning[Unknown live outcomes] Transport errors, timeouts, disconnects, task cancellation, exhausted adapter request retries, missing acknowledgements, and parse failures after transmission usually leave the venue outcome unknown. HTTP status codes and rate limits are definitive only when venue-specific semantics prove that the command was not accepted. ::: An **in-flight order** is awaiting resolution: - `SUBMITTED`: Initial submission awaiting acceptance or rejection. - `PENDING_UPDATE`: Modification awaiting confirmation. - `PENDING_CANCEL`: Cancellation awaiting confirmation. ### Delivery and retry limits A request can reach a venue even when its response is lost. NautilusTrader therefore does not make a broad exactly-once delivery claim for submit, modify, or cancel commands. An adapter may retry a state-changing command only when the venue protocol makes repetition safe, such as through stable request identity and duplicate detection or idempotent semantics for the same target. Otherwise, the adapter sends once and uses stream updates, queries, polling, or reconciliation to resolve an unknown outcome. Retryability and command outcome are separate. A failure can be safe to retry while still leaving the earlier attempt ambiguous. Once an attempt may have reached the venue, a later failure remains ambiguous unless authoritative evidence resolves the same semantic command. ## Persistence before transport Creating a client order ID, publishing `OrderInitialized`, and sending `SubmitOrder` are in-process actions. They do not by themselves confirm durable storage. For a submission handled by the built-in execution engine: 1. The order exists in the cache with its `ClientOrderId` and `OrderInitialized` event before the final execution-client call. 1. The engine selects and validates the execution client. 1. The cache enqueues the resolved order-to-client origin for persistence, then updates the in-memory origin index. 1. The engine calls the selected `ExecutionClient`. 1. The cache backend processes its queued writes independently of venue transport and acknowledgement. The enqueue steps fail before the client call when the cache backend cannot accept them. Successful enqueue does not mean the backing store has committed the order or origin. The built-in Redis and PostgreSQL cache backends process these writes asynchronously. The final step is only the call into the adapter's `ExecutionClient`. The adapter owns the later wire send and maps venue responses or stream updates to order events. Neither a successful client call nor an `OrderSubmitted` event proves venue acceptance. :::warning[Persistence gap] A process failure can therefore occur after the venue receives an order but before the local order and origin become durable. Startup reconciliation can recover that order when the venue reports it, but incomplete venue history can leave the node without enough evidence to reconstruct the full execution history. ::: The optional [event store](../event_sourcing.md) also captures asynchronously and does not gate message dispatch on durable commit. Live restart continues to use restored cache state plus venue reconciliation. ## Reconciliation authority The cached order event stream is the source of local derived order state. During live recovery, adapter reports provide the venue evidence used to align that state. Reconciliation applies the reports in order-status, fill, and position phases so position checks build on the reconciled order and fill state. Recovery means restoring available cached state, reconciling available venue reports, and holding the strategy-start barrier until startup reconciliation finishes. It does not prove that the venue returned complete history or that every unknown command outcome was resolved. An explicitly bounded report set changes NETTING position and portfolio economics only when the reports are complete and coherent, retained state is compatible, and replay matches one authoritative position report. Otherwise, NautilusTrader updates the reported order state without applying the unsupported fill to a position or portfolio. See [Bounded history safety](reconciliation.md#bounded-history-safety). Reports for orders absent from the cache can create external orders. Active claims assign an external order to a strategy; unclaimed orders use the `EXTERNAL` strategy. See [External order creation](reconciliation.md#external-order-creation). External orders and fills still participate in position tracking and portfolio calculations when their evidence passes the same reconciliation rules. The bounded-history safeguards apply whether or not a strategy claims the activity. Startup reconciliation runs before trader components start. A startup failure stops the node from starting unless a documented compatibility path handles that specific condition. ### Terminal reconciliation provenance The `reconciliation` field identifies an event generated through reconciliation. It does not by itself distinguish a venue status report from a local policy resolution: | Evidence path | Prior status | Terminal event | Available event provenance | | ------------------------------------------------------------------- | ----------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------- | | Explicit venue status report | Any transition allowed by the model | `OrderRejected`/`OrderCanceled` | `reconciliation=true`; a rejection keeps the reported reason, or `UNKNOWN` when none is reported. | | In-flight retry exhaustion | `SUBMITTED` | `OrderRejected` | `reconciliation=true`, reason `INFLIGHT_TIMEOUT`. | | In-flight retry exhaustion | `PENDING_UPDATE`/`PENDING_CANCEL` | `OrderCanceled` | `reconciliation=true`; the event has no reason field. | | Full-history order remains missing after retries and targeted query | `SUBMITTED`/`ACCEPTED` | `OrderRejected` | `reconciliation=true`, reason `NOT_FOUND_AT_VENUE`. | | Full-history order remains missing after retries and targeted query | `PARTIALLY_FILLED` | `OrderCanceled` | `reconciliation=true`; the event has no reason field. | :::warning[Local terminal state is not venue confirmation] The first row is backed by an explicit venue status. The remaining rows restore a terminal local state after an operator-configured retry policy expires. They do not prove that the venue rejected the submit or canceled the working order. ::: `OrderCanceled` has no reason field, so the event alone cannot distinguish a venue-reported cancellation from the two synthetic reconciliation paths. Consumers that require that distinction must preserve the associated reconciliation inputs and operational logs. The optional event store captures raw venue reports when enabled, but no `OrderCanceled` field carries the policy reason. See [Runtime checks](reconciliation.md#runtime-checks) for query coordination, recent-order protection, and missing-order behavior. ## Related guides - [Execution](index.md): Component roles, routing, OMS behavior, and risk checks. - [Execution algorithms](algorithms.md): TWAP, custom algorithms, and spawned orders. - [Orders](../orders/): Order types, statuses, and the primary state flow. - [Events](../events/): Event fields, dispatch, and order-to-position effects. - [Execution reconciliation](reconciliation.md): Startup recovery, continuous checks, and reconciliation invariants. - [Live trading](../live.md): Node lifecycle, dispatch policy, metrics, and shutdown. - [Configure a live trading node](../../how_to/configure_live_trading.md): Live execution settings. # Execution Reconciliation Source: https://nautilustrader.io/docs/latest/concepts/execution/reconciliation/ Execution reconciliation aligns the venue's actual order and position state with the system's internal event-sourced state. Use this guide to understand startup state recovery and the continuous checks that detect runtime discrepancies. Unresolved live command outcomes are one source of state divergence. For how Nautilus classifies local failures, definitive results, and unknown outcomes, see [Command outcomes](policies.md#command-outcomes). For the complete node lifecycle, see [Live trading](../live.md). For the available settings and recommended values, see [Configure a live trading node](../../how_to/configure_live_trading.md#executionengine-configuration). ## Reconciliation model Live execution reconciles local state against venue reports. Backtesting controls both order execution and the resulting state, so it does not need venue reconciliation. Two scenarios: - **Cached state exists**: report data generates missing events to align the state. - **No cached state**: all orders and positions at the venue are generated from scratch. :::tip Persist all execution events to the cache database. This reduces reliance on venue history and gives reconciliation the retained order and position state needed to interpret short history windows. ::: ### Component responsibilities `LiveNode` owns the `ExecutionManager` and schedules recurring reconciliation. The manager tracks activity, retries, and fill identities, interprets cached state, and prepares reconciliation events. `ExecutionEngine` applies events to orders and positions and handles individual execution reports. The UML diagram shows ownership and dependencies. A filled diamond denotes ownership; dashed arrows point from a caller to a component it uses. The kernel owns the engine and shared cache; it is omitted here to focus on reconciliation. ```mermaid classDiagram direction LR namespace nautilus_live { class LiveNode class ExecutionManager } namespace nautilus_execution { class ExecutionEngine } namespace nautilus_common { class ExecutionClient { <> } class Cache } LiveNode *-- ExecutionManager : owns LiveNode ..> ExecutionClient : requests recurring reports LiveNode ..> ExecutionEngine : dispatches through kernel ExecutionManager ..> ExecutionClient : polls reports for standalone checks ExecutionManager ..> ExecutionEngine : applies startup events ExecutionManager ..> Cache : reads state and registers external orders ExecutionEngine ..> ExecutionClient : routes commands and requests reports ExecutionEngine ..> Cache : updates orders and positions ``` The live client facade shares one adapter instance between the node and engine. Pending report requests can retain client borrows while the event loop handles other work. Instrument updates are deferred until those borrows are released, then flushed on request completion or cancellation. Within `nautilus-live`, the source modules divide these responsibilities as follows: | Module | Responsibility | | ----------------------------- | ------------------------------------------------------------------------ | | `node/mod.rs` | Node lifecycle, event loop, and event dispatch. | | `node/reconciliation.rs` | Recurring report tasks, deadlines, cancellation, and result handling. | | `execution/manager.rs` | Reconciliation state, decisions, and individual reconciliation checks. | | `execution/reconciliation.rs` | Shared types, state-independent decisions, and targeted report requests. | The separate `nautilus_execution::reconciliation` module supplies report-to-event and arithmetic operations shared with the execution engine. At startup, the manager publishes raw reports, applies order and fill events, verifies historical fill application, and then evaluates positions against the updated cache. During continuous position checks, the node coordinates authoritative fill queries and dispatch before asking the manager to generate synthetic events. Activity revisions detect local changes during requests or callbacks; applying authoritative fills defers synthetic reconciliation until a fresh position report. The manager remains available without the `node` feature. Standalone callers can use its individual polling methods and apply the returned events themselves. Standalone position polling directly returns synthetic discrepancy events; the node adds the authoritative-fill recovery sequence. ### Execution-client origins An **execution-client origin** is a write-once binding between an order and the client responsible for its execution. An origin is recorded: - From an explicit client on submission, or from the final client selected after routing and venue validation and before transport. - When non-synthetic external orders are materialized during startup reconciliation, from the reporting mass-status client. - When external orders are materialized from runtime venue reports and the report's account matches exactly one registered client that handles the instrument venue. An origin may be absent for: - Cache data written before resolved origins were persisted. - External orders whose runtime report does not identify exactly one registered client by account and instrument venue. - Synthetic reconciliation orders. The built-in cache backends enqueue a resolved origin for persistence before transport. Their writes remain asynchronous, so enqueue order does not guarantee that the origin is durable before the order reaches the client. At startup, each client's mass status is checked against the cached origins: an order the client reports is expected to be bound to that same client. A missing origin logs an aggregated warning and remains compatible with existing cache data. A conflicting origin logs an aggregated deprecation warning and reconciles for compatibility. A future release rejects the conflict as a startup error. See the origin rows in [Startup reconciliation](#startup-reconciliation). This is separate from [external order claims](#external-order-creation), which attribute venue-sourced orders to a *strategy*. The execution-client origin records which *client* an order belongs to. ### Reconciliation reports The execution engine consumes four reconciliation report variants from live adapters. Each variant has a different normal role when its matching order is absent from the cache. Explicitly bounded history can instead use [order-only fill projection](#order-only-fill-projection). | Variant | Purpose | Missing-order action | | ---------------------- | ------------------------ | ---------------------------------------------------- | | `OrderStatusReport` | Order state update. | Creates an order and infers any reported fill. | | `FillReport` | Standalone fill. | Creates a market order, then applies fill metadata. | | `OrderWithFills` | Order state plus fills. | Creates an order, applies fills, and infers residue. | | `PositionStatusReport` | Venue position snapshot. | Logs the report; positions remain fill-derived. | #### When to use each variant Adapters choose the variant that matches the venue event: - Use `OrderStatusReport` for order lifecycle updates when fill details arrive on a separate stream. - Use `FillReport` for a venue-initiated closure that has a fill but no user-level order. Hyperliquid liquidations follow this pattern. - Use `OrderWithFills` when one venue event contains both an order status and its fills. Binance Futures uses this for exchange-generated ADL, liquidation, and settlement orders. ### Order-only fill projection During startup reconciliation, a bounded historical report can prove an order's status and filled quantity without proving that its fill belongs in the current position lifecycle. The engine then projects the `OrderFilled` event onto the order only. The order reaches the exact reported state, while the fill does not create or change a position and does not update portfolio economics. This projection applies only to reconciliation recovery. Raw reports remain available, and an authoritative position report can reconcile the current venue position separately. See [Bounded history safety](#bounded-history-safety) for the required evidence. ### External order creation When a report references an order that is absent from the cache, the engine creates an **external order**. This covers venue-initiated ADL, liquidation, or settlement, orders placed by another process, and orders not yet observed locally. The naming distinguishes configuration intent from live ownership state: - `external_order_instrument_ids` is the serializable strategy configuration intent. It names the instruments whose external orders should be assigned to the strategy when it is registered. - An **external order claim** is an active cache entry that maps one `InstrumentId` to one `StrategyId`. The code uses `external_order_claims` for the collection of these live entries. Live strategy registration materializes the configured instrument IDs with `register_external_order_claims`. This operation is additive and strict: it rejects a repeated instrument or any instrument that already has a claim, including a claim for the same strategy. The strategy method `set_external_order_instrument_ids(...)` delegates to the cache operation `set_external_order_claims`. This operation treats its input as the strategy's complete desired active set. It can retain or release that strategy's existing claims and acquire unclaimed instruments, but it cannot take a claim from another strategy. Validation covers the complete input before changing the cache, so a conflict leaves every existing claim unchanged. The `ExecutionManager` and `ExecutionEngine` read the same canonical claim map from the cache when they process external reports. They assign an external order to: - The strategy identified by the active claim for the report's instrument. - The `EXTERNAL` strategy as a default fallback. An active-claim update is therefore visible to both components without a coordination message. The claim present when an external order is created determines the assignment. Existing cached orders keep their assigned `StrategyId`; changing a claim does not reassign them. Transferring an instrument between strategies requires the current owner to release it before the new owner claims it. There is no atomic handoff across strategies. A report processed between the release and acquisition has no active claim and is assigned to `EXTERNAL`. Cache resets preserve active claims so registered routing remains configured, while retiring a strategy clears its claims. The external order uses the report's `client_order_id` when present and otherwise derives one from the `venue_order_id`. The engine adds the order to the cache, registers its venue order ID, and emits the applicable `OrderAccepted`, `OrderFilled`, `OrderCanceled`, or `OrderExpired` events. Positions then update through the normal event pipeline. See [Claiming external orders](../strategies.md#claiming-external-orders) for strategy configuration and runtime updates. ### Reducing external positions A strategy can use reduce-only fills to reduce inherited `EXTERNAL` inventory under NETTING. #### Position selection Existing cached position links remain authoritative. Without a cached link, a reduce-only fill uses the strategy's own open position when available. If that position is absent or closed, the engine looks for positions that meet all of these conditions: - Belong to `EXTERNAL` and use NETTING. - Are open on the opposite side of the fill. - Match the fill's instrument and account. The engine selects a fallback only when **exactly one** position matches. The fill quantity must not exceed that position's quantity, though the order's remaining quantity can be larger. If no safe fallback exists, an otherwise valid fill updates the order but neither opens nor updates a position. #### Ownership and events After a successful reduction, the engine links the order to the external position so subsequent fills use the same target. The position retains `EXTERNAL` ownership: - `OrderFilled` keeps the reducing strategy's ID and identifies the external position. - `PositionChanged` and `PositionClosed` use the `EXTERNAL` strategy's event topic. #### Linked reduction checks When applying position economics, each linked reduction must match the external position's account and reduce its open quantity without flipping or reopening it. If a fill violates these checks, the engine rejects it **before changing the order or position**. [Order-only fill projection](#order-only-fill-projection) bypasses these reduction checks because it repairs order history without changing the position. ## Reconciliation configuration Unless `reconciliation` is set to false, the live node runs startup reconciliation for each execution client. The `reconciliation_lookback_mins` parameter controls how far back it requests history through the execution engine. Startup enablement and polling intervals belong to the node's `LiveExecutionEngineConfig`; the manager receives the thresholds, retry limits, filters, and lookbacks used to make reconciliation decisions. :::tip Leave `reconciliation_lookback_mins` unset to use the adapter's documented default. Many adapters request the maximum execution history the venue provides, while others use a bounded default to match venue retention and request limits. See the integration guide for the selected venue. ::: :::warning A bounded history window can begin after the fill that opened a position. When an adapter declares the lower bound in its mass status, the engine applies historical fill economics only when the bounded report set and retained state prove a coherent position transition. Adapters that do not declare the bound use the compatibility fill-adjustment path, which can generate synthetic events with information loss. Some venues also filter or drop older execution data. ::: Each strategy can configure `external_order_instrument_ids` as its intent to claim venue-sourced external orders and materialized reconciliation activity for specific instruments. Live strategy registration materializes that intent as active claims, which the strategy can replace at runtime. This lets a strategy resume managing open orders and positions when no cached state exists. Unclaimed external orders use strategy ID `EXTERNAL` with tag `VENUE`. Unclaimed orders generated during position reconciliation use strategy ID `EXTERNAL` with tag `RECONCILIATION`. Claimed orders and fills use the claiming strategy ID and have no external/reconciliation tag, so the strategy can continue managing the recovered state. :::tip To detect unclaimed external orders in your strategy, check `order.strategy_id.value == "EXTERNAL"`. Ownership does not exclude these orders from position tracking or portfolio calculations. Historical fills still follow the [bounded history safety](#bounded-history-safety) rules when applicable. ::: For all live trading options, see the `LiveExecutionEngineConfig` [API reference](/docs/python-api-latest/config.html#nautilus_trader.live.LiveExecutionEngineConfig). ### Instrument availability Adapters parse reconciliation reports using the instrument, so every instrument a report references must already be loaded. Adapters do not fetch missing instruments from the venue during reconciliation. Instrument scope comes from the adapter's provider config rather than the engine. `InstrumentProviderConfig.load_ids` decides which instruments the adapter holds, while `reconciliation_instrument_ids` filters reports only after the adapter has produced them. Reports for instruments outside an explicit `load_ids` scope are expected: they are dropped at debug level, so a node scoped to one instrument stays quiet about the rest of the venue. An in-scope instrument that does not resolve means something is wrong, whether it was named in `load_ids` or covered by `load_all=True`, and the outcome depends on what the report describes: - An open order or position report fails reconciliation, so the system does not start. A live position that cannot be priced is never silently dropped. - A closed or historical record logs a warning instead of aborting startup. When the adapter declares a bounded history, the record also marks the report set incomplete, applying the [bounded history safety](#bounded-history-safety) rules. Expiries routinely retire instruments that older fills still reference. ## Reconciliation procedure All adapter execution clients follow the same reconciliation procedure, calling three methods to produce an execution mass status: - `generate_order_status_reports` - `generate_fill_reports` - `generate_position_status_reports` ```mermaid flowchart TD Start[Startup Reconciliation] --> Fetch[Fetch venue reports
orders, fills, positions] Fetch --> Dedup[Deduplicate reports
log warnings for duplicates] Dedup --> Orders[Order Reconciliation
align order states, generate missing events] Orders --> Fills[Fill Reconciliation
verify fills, generate missing OrderFilled events] Fills --> Pos[Position Reconciliation
compare net positions per instrument] Pos --> Match{Positions
match venue?} Match -->|Yes| Done[Reconciliation complete
system ready for trading] Match -->|No| Gen[Generate missing orders
strategy: EXTERNAL, tag: RECONCILIATION] Gen --> Done ``` These reports represent external reality. The procedure processes them in the order shown so each position check builds on reconciled order and fill state. ### Mass-status history contract An `ExecutionMassStatus` can declare the provenance of its historical reports: - `lookback_start=None` means that the adapter has not declared an explicit lower time bound. - `lookback_start=Some(timestamp)` means that historical order and fill reports exclude venue activity before that timestamp. - `reports_complete=true` means that every order, fill, and position source needed to interpret the bounded history completed and all required records were mapped successfully. An adapter can still return authoritative active orders when a historical source fails. It marks the mass status incomplete so the engine can recover those orders without treating the partial history as proof of position or portfolio economics. ### Report deduplication - Deduplicates order reports within the batch and logs warnings. - Logs duplicate trade IDs as warnings for investigation. ### Order reconciliation - Generates and applies events to move orders from cached state to current state. - Generates external order events for unrecognized client order IDs or reports missing a client order ID. ### Fill reconciliation - Infers `OrderFilled` events for missing trade reports. - Verifies fill report data consistency with tolerance-based price and commission comparisons. ### Position reconciliation - Matches the net position per account and instrument against venue position reports using instrument precision. - Generates external order events when order reconciliation leaves a position that differs from the venue. - When `generate_missing_orders` is enabled (default: True), generates orders with strategy ID `EXTERNAL` and tag `RECONCILIATION` to align discrepancies. - Logs a warning when NETTING ownership is split across multiple strategies for the same account and instrument, since venue position reports are account-level net positions. When generating reconciliation orders, the engine uses this price hierarchy: 1. **Calculated reconciliation price** (preferred): targets the correct average position. 1. **Market mid-price**: uses the current bid-ask midpoint. 1. **Current position average**: uses the existing position's average price. 1. **MARKET order** (last resort): used only when no price data exists (no positions, no market data). The engine uses LIMIT orders when a price can be determined (cases 1-3) to preserve PnL accuracy and skips zero quantity differences after precision rounding. ### Fill adjustment without an explicit report bound For compatibility, a mass status without an explicit `lookback_start` follows the existing fill adjustment path. The engine can analyze zero-crossings, remove closed lifecycles, and generate a synthetic fill when the reported fills do not explain the current venue position. When `generate_missing_orders` is disabled, the engine still processes raw venue order reports. It filters completed lifecycles when the current lifecycle explains the venue position, but it does not add or replace synthetic reports to align a fill window with the venue position or materialize an order for a fill group that has no order report. Adapters that apply a history cutoff should declare it through the [mass-status history contract](#mass-status-history-contract) instead of relying on this inference. ### Bounded history safety For explicitly bounded NETTING history without a venue position ID, the engine applies historical fills to positions and the portfolio only when all of the following evidence agrees: - The report set is complete, and each fill has coherent account, instrument, order, side, and strategy ownership. - Retained fills are excluded, and any cached predecessor is an unambiguous NETTING position for the same account, instrument, and strategy. - A reduce-only fill has a sufficient opposite-side predecessor. - Fill intervals are ordered without overlapping or equal timestamp boundaries that make their sequence ambiguous. - Replaying the fills from retained state matches one unambiguous authoritative position report, including an explicit flat report. If any condition fails, the engine projects the affected historical fill onto its order only. This preserves the reported order status and filled quantity without opening, closing, or changing a position and without publishing fill economics to the portfolio. Raw reconciliation reports remain available, and position reconciliation can align an authoritative current position separately. Reports with an explicit `venue_position_id` follow the position-specific reconciliation path and do not require NETTING lifecycle inference. ### Failure handling - An adapter can preserve successful report legs after an individual source failure. Explicitly bounded mass statuses must mark the result incomplete, which makes unsupported historical fills order-only. - Fill reports arriving before order status reports are deferred until order state is available. #### Commission failures An adapter fill commission that cannot be calculated or represented fails the report request under the [adapter contract](../../developer_guide/adapters.md#commission-failure-handling). The adapter does not drop that fill or replace its commission with zero or a generic formula. Startup stops before applying that client's mass status. When the engine asks the responsible execution client to calculate an inferred-fill commission, a failure defers the inferred quantity and dependent terminal transition until a later reconciliation cycle succeeds. Valid explicit fills from the same report set can still apply. For an external order, the engine resolves the commission before adding the order to the cache or publishing its initial event, so a failure defers the entire external order. An unavailable responsible execution client has the same fail-closed result. An inferred-fill commission failure while applying an otherwise successful mass status does not stop startup. The unresolved work remains pending for a later reconciliation cycle. If startup reconciliation fails for any other reason, the system logs an error and does not start. ## Common reconciliation scenarios The tables below cover startup reconciliation (mass status) and runtime checks (in-flight order checks, open-order polls, own-books audits). ### Startup reconciliation | Scenario | Description | System behavior | | -------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | **Order state discrepancy** | Local state differs from venue (e.g., local `SUBMITTED`, venue `REJECTED`). | Updates local order to match venue state, emits missing events. | | **Missed fills** | Complete venue history contains a fill the engine missed. | Generates the missing `OrderFilled` event and applies its economics. | | **Multiple fills** | A complete, coherent report set contains several fills for an order. | Reconstructs the reported fill history in event order. | | **Incomplete bounded history** | A required order, fill, or position source failed or could not be mapped. | Recovers order state but projects historical fills without position or portfolio effects. | | **Ambiguous bounded lifecycle** | The bounded reports do not prove one coherent NETTING position transition. | Preserves order state and leaves current position alignment to position reconciliation. | | **External orders** | Orders exist on venue but not in local cache. | Creates unclaimed orders with strategy ID `EXTERNAL` and tag `VENUE`. | | **Missing client origin** | A cached order in the mass status has no recorded execution-client origin. | Logs one aggregated warning with a count and sample IDs; reconciles against the reporting client. | | **Conflicting client origin** | A cached order's origin differs from the client that supplied the report. | Logs one aggregated deprecation warning; reconciliation proceeds during the compatibility period. | | **Partially filled then canceled** | Order partially filled then canceled by venue. | Updates state to `CANCELED`, preserves fill history. | | **Different fill data** | Venue reports different fill price/commission than cached. | Preserves cached data, logs discrepancies. | | **Filtered orders** | Orders marked for filtering via config. | Skips based on `filtered_client_order_ids` or instrument filters. | | **Unresolved instrument** | A report references an in-scope instrument the adapter has not loaded. | Fails startup for open order and position reports; warns and marks bounded history incomplete otherwise. | | **Fill commission failure** | An adapter cannot represent a required fill commission while building reports. | Fails mass-status generation and stops startup before applying that client's reports. | | **Inferred-fill commission failure** | The responsible execution client cannot calculate a required commission. | Defers inferred work; an external order remains absent, while valid explicit fills can still apply. | | **Duplicate order reports** | Multiple orders share the same identifier. | Deduplicates with warning logged. | | **Position quantity mismatch (long)** | Internal long position differs from venue (e.g., 100 vs 150). | Generates BUY LIMIT with calculated price when `generate_missing_orders=True`. | | **Position quantity mismatch (short)** | Internal short position differs from venue (e.g., -100 vs -150). | Generates SELL LIMIT with calculated price when `generate_missing_orders=True`. | | **Position reduction** | Venue position smaller than internal (e.g., internal 150 long, venue 100 long). | Generates opposite-side LIMIT order with calculated price. | | **Position side flip** | Internal position opposite of venue (e.g., internal 100 long, venue 50 short). | Generates LIMIT order to close internal and open external position. | | **Internal reconciliation orders** | Orders generated to align position discrepancies. | Uses a claim when configured; otherwise `EXTERNAL` + `RECONCILIATION`. | ### Runtime checks Continuous reconciliation starts after startup reconciliation completes. It: - Monitors in-flight orders for delays exceeding a configured threshold. - Reconciles open orders with the venue at configured intervals. - Checks position status with the venue at configured intervals. - Audits internal *own* order books against the venue's public books. The loop waits for startup reconciliation to finish before starting periodic checks. The `reconciliation_startup_delay_secs` parameter adds a further delay *after* startup reconciliation completes, giving the system time to stabilize. | Scenario | Description | System behavior | | ----------------------------------- | --------------------------------------------------------- | ----------------------------------------------- | | **In-flight submit timeout** | `SUBMITTED` remains unconfirmed beyond retry exhaustion. | Resolves to `REJECTED` with `INFLIGHT_TIMEOUT`. | | **In-flight cancel/update timeout** | `PENDING_CANCEL` or `PENDING_UPDATE` exceeds the retries. | Resolves to `CANCELED` through reconciliation. | | **Open orders check discrepancy** | Periodic poll detects a venue state change. | Confirms status and applies transitions. | | **Position check discrepancy** | Periodic poll detects a position mismatch. | Generates reconciliation events when eligible. | | **Commission construction failure** | A required fill commission cannot be represented. | Defers the affected work to a later cycle. | | **Own books audit mismatch** | Own order books diverge from venue public books. | Audits and logs inconsistencies. | The in-flight checker produces the submit and cancel/update timeout results after exhausting the configured retries. [Terminal reconciliation provenance](policies.md#terminal-reconciliation-provenance) distinguishes these local policy resolutions from venue-reported outcomes. A missing open-order report does not by itself prove a pending modify or cancel outcome, so the consistency checks below leave those states unresolved until another check can determine the venue state. **Order consistency checks** (when cache state differs from venue state): :::info[Full-history checks] The *Not found* rows apply only in full-history mode (`open_check_open_only=False`); open-only mode is the default. ::: | Cache status | Venue status | Resolution | Rationale | | ------------------ | ------------ | ------------ | ------------------------------------------------------------------- | | `SUBMITTED` | *Not found* | `REJECTED` | Order never confirmed by venue (e.g., lost during network error). | | `ACCEPTED` | *Not found* | `REJECTED` | Order doesn't exist at venue, likely was never successfully placed. | | `ACCEPTED` | `CANCELED` | `CANCELED` | Venue canceled the order (user action or venue-initiated). | | `ACCEPTED` | `EXPIRED` | `EXPIRED` | Order reached GTD expiration at venue. | | `ACCEPTED` | `REJECTED` | `REJECTED` | Venue rejected after initial acceptance (rare but possible). | | `PENDING_UPDATE` | *Not found* | *Unresolved* | Modification outcome remains unknown. | | `PENDING_CANCEL` | *Not found* | *Unresolved* | Cancellation outcome remains unknown. | | `PARTIALLY_FILLED` | `CANCELED` | `CANCELED` | Order canceled at venue with fills preserved. | | `PARTIALLY_FILLED` | *Not found* | `CANCELED` | Order doesn't exist but had fills (reconciles fill history). | **Runtime reconciliation caveats:** - **Open-only mode**: venue "open orders" endpoints exclude closed orders by design, making it impossible to distinguish missing orders from recently closed ones. Pending cancel/update orders remain unresolved when a missing-order check cannot prove the final venue state. - **Recent order protection**: the engine skips reconciliation for orders whose last event falls within the `open_check_threshold_ms` window. This prevents false positives from race conditions where the venue is still processing. - **Targeted query safeguard**: before applying a terminal "not found" resolution, the engine issues a single-order query to the venue. This catches false negatives from bulk query limitations or timing delays. - **Position report failures**: if a venue position query fails, the engine skips cached positions for that venue during the cycle instead of treating missing reports as flat. - **Completed orders**: `FILLED` orders that are "not found" at the venue are silently ignored. Venues commonly drop completed orders from their query results. **Retry coordination.** The in-flight loop increments its own per-order retry count against `inflight_check_retries` and mirrors that value into missing-order tracking. The open-order loop increments the missing-order count against `open_check_missing_retries`. Each loop applies its own limit; neither setting automatically overrides the other. When the open-order loop exhausts retries, the engine issues one targeted `GenerateOrderStatusReport` probe before applying a terminal state or leaving an ambiguous pending cancel/update unresolved. If the venue returns the order, reconciliation proceeds and missing-order tracking clears. If a pending state remains unresolved, the engine also resets the in-flight count before checking again after the configured threshold. Position checks use separate retry counters per instrument and account. A successful position match clears the counter, while repeated unresolved discrepancies stop active reconciliation for that pair until the discrepancy clears. **Single-order query throttling.** The engine caps single-order queries per cycle via `max_single_order_queries_per_cycle`. Remaining orders are deferred to the next cycle. `single_order_query_delay_ms` spaces out consecutive queries to avoid rate limits. This handles bulk query failures across hundreds of orders without overwhelming the venue API. ## Common reconciliation issues - **Missing trade reports**: Some venues filter out older trades. Increase `reconciliation_lookback_mins` or persist all events locally. Explicitly bounded adapters mark incomplete history so unsupported fills do not change positions or portfolio economics. - **Position mismatches**: External orders that predate the lookback window cause position drift. Increase the window, restore retained state, or let an authoritative position report reconcile the current quantity. Flatten the account only as a deliberate operational recovery step. - **Split NETTING ownership**: Multiple strategies can hold cached positions for the same account and instrument, but venues report a single account-level net position. Prefer one claiming strategy per NETTING account/instrument pair when resuming external state. - **Duplicate order IDs**: Deduplicated with warnings logged. Frequent duplicates may indicate venue data integrity issues. - **Unresolved instruments**: A report references an instrument the adapter never loaded. Add it to `load_ids` or set `load_all=True`. Reports outside an explicit `load_ids` scope are dropped by design and need no action. - **Precision differences**: Small decimal differences are handled using instrument precision. Large discrepancies may indicate missing orders. - **Out-of-order reports**: Fill reports arriving before order status reports are deferred until order state is available. :::tip For persistent issues, inspect the venue reports and cached ownership before dropping state or flattening an account. ::: ## Reconciliation invariants The reconciliation path preserves these invariants for the reports and positions it processes: 1. **Order state**: authoritative reports recover the exact order status and filled quantity even when bounded history cannot support economic replay. 1. **Evidence-gated economics**: an explicitly bounded historical fill changes a NETTING position and portfolio only when complete, coherent evidence proves the transition. 1. **Position quantity**: reconciled positions match authoritative venue reports within instrument precision. 1. **Price and PnL integrity**: applied or generated economic fills use reported or calculated prices that preserve the reconciled average entry price and unrealized PnL. 1. **ID determinism**: synthetic `trade_id` and `venue_order_id` values are deterministic functions of the logical event, so replay deduplicates them across restarts. Incomplete or ambiguous bounded history therefore does not claim to reconstruct historical average entry price or realized PnL. It recovers the order record and leaves unsupported historical economics unapplied. ## Fill adjustment scenarios without an explicit bound These scenarios apply when the mass status does not declare a `lookback_start`: | Scenario | Description | System behavior | | ----------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | | **Complete lifecycle** | All fills from opening to current state are captured. | No adjustment. | | **Incomplete single lifecycle** | Reports miss opening fills, with no zero-crossings. | Adds a synthetic opening fill with calculated price. | | **Multiple lifecycles, current matches** | Zero-crossings separate earlier and current lifecycles. | Filters out old lifecycles and retains the current one. | | **Multiple lifecycles, current mismatch** | The current lifecycle differs from the venue position. | Replaces it with one synthetic fill. | | **Flat position** | The venue reports flat regardless of fill history. | Makes no adjustment. | | **No fills** | The report set contains no fills. | Returns the empty fill set. | Concepts: - **Zero-crossing**: position quantity crosses through zero (FLAT), marking a lifecycle boundary. - **Lifecycle**: a sequence of fills between zero-crossings representing one open-close cycle. - **Synthetic fill**: a calculated fill report representing missing activity, priced to achieve the correct average position. - **Tolerance**: position matching uses configurable price tolerance (default 0.0001 = 0.01%) to absorb minor calculation differences. ## Bounded history scenarios | Scenario | Evidence | Economic fill behavior | | ---------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------- | | **Complete coherent sequence** | Ordered fills replay to the one authoritative position report. | Applies the fills normally. | | **Isolated reduce-only close** | No sufficient correlated predecessor exists. | Updates the order only. | | **Correlated cached predecessor** | Same account, instrument, and strategy with sufficient quantity. | Applies the fill normally. | | **Unrelated or undersized position** | Cached state cannot fully support the transition. | Leaves the cached position unchanged and updates the order only. | | **Incomplete report source** | A required order, fill, or position leg failed or did not map. | Updates affected historical orders only. | | **Ambiguous fill ordering** | Fill intervals overlap or share a boundary timestamp. | Updates affected historical orders only. | | **Missing or ambiguous position report** | No single authoritative NETTING report proves the final quantity. | Updates affected historical orders only. | | **Explicit venue position identity** | Reports carry a `venue_position_id`. | Uses the position-specific reconciliation path. | ## Related guides - [Live trading](../live.md) - Node lifecycle, configuration, metrics, and shutdown. - [Configure a live trading node](../../how_to/configure_live_trading.md) - Node and engine configuration. - [Adapters](../adapters.md) - Venue connectivity. - [Execution](index.md) - Command outcomes and execution flow. - [Execution policies](policies.md) - Command evidence, persistence, and recovery boundaries. # Betting Instrument Source: https://nautilustrader.io/docs/latest/concepts/instruments/betting_instrument/ `BettingInstrument` represents one selection in a sports or gaming market. It carries event, competition, market, and selection metadata so Nautilus can treat the selection as an instrument with prices, sizes, limits, margins, and fees. Examples include Betfair match-odds selections and handicap market selections. ## Fields | Field | Rust type | Python type | Required/default | Notes | | -------------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native or generated venue symbol. | | `event_type_id` | `u64` | `int` | Required | Event type identifier. | | `event_type_name` | `Ustr` | `str` | Required | Event type name, such as a sport. | | `competition_id` | `u64` | `int` | Required | Competition identifier. | | `competition_name` | `Ustr` | `str` | Required | Competition name. | | `event_id` | `u64` | `int` | Required | Event identifier. | | `event_name` | `Ustr` | `str` | Required | Event name. | | `event_country_code` | `Ustr` | `str` | Required | Event country code. | | `event_open_date` | `UnixNanos` | `int` | Required | Event open time. | | `betting_type` | `Ustr` | `str` | Required | Betting type published by the venue. | | `market_id` | `Ustr` | `str` | Required | Market identifier. | | `market_name` | `Ustr` | `str` | Required | Market name. | | `market_type` | `Ustr` | `str` | Required | Market type, such as match odds. | | `market_start_time` | `UnixNanos` | `int` | Required | Market start time. | | `selection_id` | `u64` | `int` | Required | Selection or runner identifier. | | `selection_name` | `Ustr` | `str` | Required | Selection or runner name. | | `selection_handicap` | `f64` | `float` | Required | Handicap value for handicap markets. | | `currency` | `Currency` | `Currency` | Required | Quote and settlement currency. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Price step, often set by a tick scheme. | | `size_increment` | `Quantity` | `Quantity` | Required | Minimum size step. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `1` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `1` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `BettingInstrument` has asset class `Alternative` and instrument class `SportsBetting`. - Each selection or runner is modeled as its own instrument. - Betting instruments commonly use a registered tick scheme for valid odds steps. - Margin defaults to one because staking a bet typically reserves the full stake. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::BettingInstrument, types::{Currency, Money, Price, Quantity}, }; use rust_decimal_macros::dec; use ustr::Ustr; let event_open: Timestamp = "2022-02-07T23:30:00Z".parse().unwrap(); let market_start: Timestamp = "2022-02-07T23:30:00Z".parse().unwrap(); let selection = BettingInstrument::builder() .instrument_id(InstrumentId::from("1-123456789-50214.BETFAIR")) .raw_symbol(Symbol::from("1-123456789-50214")) .event_type_id(6423) .event_type_name(Ustr::from("American Football")) .competition_id(12_282_733) .competition_name(Ustr::from("NFL")) .event_id(29_678_534) .event_name(Ustr::from("NFL")) .event_country_code(Ustr::from("GB")) .event_open_date(UnixNanos::from(event_open)) .betting_type(Ustr::from("ODDS")) .market_id(Ustr::from("1-123456789")) .market_name(Ustr::from("AFC Conference Winner")) .market_type(Ustr::from("SPECIAL")) .market_start_time(UnixNanos::from(market_start)) .selection_id(50214) .selection_name(Ustr::from("Kansas City Chiefs")) .selection_handicap(0.0) .currency(Currency::from("GBP")) .price_precision(2) .size_precision(2) .price_increment(Price::from("0.01")) .size_increment(Quantity::from("0.01")) .max_quantity(Quantity::from("1000")) .min_quantity(Quantity::from("1")) .max_notional(Money::from("10000 GBP")) .min_notional(Money::from("10 GBP")) .max_price(Price::from("100.00")) .min_price(Price::from("1.00")) .margin_init(dec!(1)) .margin_maint(dec!(1)) .maker_fee(dec!(0)) .taker_fee(dec!(0)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" import pandas as pd from nautilus_trader.model import BettingInstrument from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Money from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol from nautilus_trader.model import Venue GBP = Currency.from_str("GBP") selection = BettingInstrument( instrument_id=InstrumentId(Symbol("1-123456789-50214"), Venue("BETFAIR")), raw_symbol=Symbol("1-123456789-50214"), event_type_id=6423, event_type_name="American Football", competition_id=12282733, competition_name="NFL", event_id=29678534, event_name="NFL", event_country_code="GB", event_open_date=pd.Timestamp("2022-02-07 23:30:00+00:00").value, betting_type="ODDS", market_id="1-123456789", market_name="AFC Conference Winner", market_type="SPECIAL", market_start_time=pd.Timestamp("2022-02-07 23:30:00+00:00").value, selection_id=50214, selection_name="Kansas City Chiefs", selection_handicap=0.0, currency=GBP, price_precision=2, size_precision=2, price_increment=Price.from_str("0.01"), size_increment=Quantity.from_str("0.01"), min_notional=Money(1, GBP), ts_event=0, ts_init=0, ) ``` ## Adapters Representative adapters that create or consume `BettingInstrument` instruments include: - [Betfair](../../integrations/betfair.md) for sports betting markets. ## Related guides - [Accounting](../accounting.md) covers betting account behavior. - [Data](../data/) explains market data that references instruments. # Binary Option Source: https://nautilustrader.io/docs/latest/concepts/instruments/binary_option/ `BinaryOption` represents a binary outcome instrument that settles to a fixed payoff based on whether a condition is true. It can model prediction markets, binary options, or venue-specific yes/no contracts. Examples include prediction market outcomes and binary event contracts. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ----------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `asset_class` | `AssetClass` | `AssetClass` | Required | Asset class of the outcome market. | | `currency` | `Currency` | `Currency` | Required | Quote and settlement currency. | | `activation_ns` | `UnixNanos` | `int` | Required | Contract activation timestamp. | | `expiration_ns` | `UnixNanos` | `int` | Required | Contract expiration timestamp. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `event_id` | `Option` | `str \| None` | `None` | Venue-scoped parent event identifier. | | `outcome` | `Option` | `str \| None` | `None` | Outcome label when the venue provides it. | | `description` | `Option` | `str \| None` | `None` | Human-readable market description. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `BinaryOption` has instrument class `BinaryOption`. - It is never inverse and uses a multiplier and lot size of one. - Many venues quote binary outcomes between zero and one, but the venue defines the allowed price range and tick size. - `outcome` and `description` provide human-readable context for the contract. - `event_id` identifies the event containing the instrument's market, scoped to its venue. - Instruments sharing an `event_id` belong to the same event. That does not imply shared collateral, mutually exclusive outcomes, or identical settlement rules. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ enums::AssetClass, identifiers::{InstrumentId, Symbol, Venue}, instruments::BinaryOption, types::{Currency, Price, Quantity}, }; use rust_decimal_macros::dec; use ustr::Ustr; let raw_symbol = Symbol::from( "0x12a0cb60174abc437bf1178367c72d11f069e1a3add20b148fb0ab4279b772b2-92544998123698303655208967887569360731013655782348975589292031774495159624905", ); let expiration: Timestamp = "2024-01-01T00:00:00Z".parse().unwrap(); let yes_outcome = BinaryOption::builder() .instrument_id(InstrumentId::new(raw_symbol, Venue::from("POLYMARKET"))) .raw_symbol(raw_symbol) .asset_class(AssetClass::Alternative) .currency(Currency::from("USDC")) .activation_ns(UnixNanos::default()) .expiration_ns(UnixNanos::from(expiration)) .price_precision(3) .size_precision(2) .price_increment(Price::from("0.001")) .size_increment(Quantity::from("0.01")) .outcome(Ustr::from("Yes")) .description(Ustr::from("Will the outcome of this market be 'Yes'?")) .min_quantity(Quantity::from("5")) .maker_fee(dec!(0)) .taker_fee(dec!(0)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from decimal import Decimal import pandas as pd from nautilus_trader.model import AssetClass from nautilus_trader.model import BinaryOption from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol from nautilus_trader.model import Venue raw_symbol = Symbol( "0x12a0cb60174abc437bf1178367c72d11f069e1a3add20b148fb0ab4279b772b2-92544998123698303655208967887569360731013655782348975589292031774495159624905", ) price_increment = Price.from_str("0.001") size_increment = Quantity.from_str("0.01") yes_outcome = BinaryOption( instrument_id=InstrumentId(raw_symbol, Venue("POLYMARKET")), raw_symbol=raw_symbol, asset_class=AssetClass.ALTERNATIVE, currency=Currency.from_str("USDC"), activation_ns=0, expiration_ns=pd.Timestamp("2024-01-01", tz="UTC").value, price_precision=price_increment.precision, size_precision=size_increment.precision, price_increment=price_increment, size_increment=size_increment, min_quantity=Quantity.from_int(5), maker_fee=Decimal(0), taker_fee=Decimal(0), outcome="Yes", description="Will the outcome of this market be 'Yes'?", ts_event=0, ts_init=0, ) ``` ## Adapters Representative adapters that create or consume `BinaryOption` instruments include: - [Hyperliquid](../../integrations/hyperliquid.md) for binary and prediction-style markets. - [OKX](../../integrations/okx.md) for venue-defined binary outcome products. - [Polymarket](../../integrations/polymarket.md) for prediction market outcomes. ## Related guides - [Order Book](../order_book.md) covers binary market order book behavior. - [Data](../data/) explains market data that references instruments. # CFD Source: https://nautilustrader.io/docs/latest/concepts/instruments/cfd/ `Cfd` represents a contract for difference that tracks an underlying asset without transferring ownership of the underlying. The venue defines the quote currency, precision, increments, limits, margins, and fees. Examples include CFD contracts on FX, equities, indexes, and commodities. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `asset_class` | `AssetClass` | `AssetClass` | Required | Asset class of the underlying. | | `base_currency` | `Option` | `Currency \| None` | `None` | Base currency when the CFD tracks one. | | `quote_currency` | `Currency` | `Currency` | Required | Currency used to quote and value prices. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `lot_size` | `Option` | `Quantity \| None` | `None` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `Cfd` has instrument class `Cfd`. - It is never inverse and uses a multiplier of one. - It has no activation timestamp, expiration timestamp, strike, or option kind. - When a venue offers both a cash instrument and a CFD on the same underlying, use `Cfd` only for the CFD contract and the matching cash type for the underlying market. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ enums::AssetClass, identifiers::{InstrumentId, Symbol}, instruments::Cfd, types::{Currency, Price, Quantity}, }; use rust_decimal_macros::dec; let audusd = Cfd::builder() .instrument_id(InstrumentId::from("AUDUSD.OANDA")) .raw_symbol(Symbol::from("AUD/USD")) .asset_class(AssetClass::FX) .base_currency(Currency::from("AUD")) .quote_currency(Currency::from("USD")) .price_precision(5) .size_precision(0) .price_increment(Price::from("0.00001")) .size_increment(Quantity::from("1")) .lot_size(Quantity::from("1000")) .margin_init(dec!(0.03)) .margin_maint(dec!(0.03)) .maker_fee(dec!(0.00002)) .taker_fee(dec!(0.00002)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from decimal import Decimal from nautilus_trader.model import AssetClass from nautilus_trader.model import Cfd from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol audusd = Cfd( instrument_id=InstrumentId.from_str("AUDUSD.OANDA"), raw_symbol=Symbol("AUD/USD"), asset_class=AssetClass.FX, quote_currency=Currency.from_str("USD"), price_precision=5, price_increment=Price.from_str("0.00001"), size_precision=0, size_increment=Quantity.from_int(1), ts_event=0, ts_init=0, base_currency=Currency.from_str("AUD"), lot_size=Quantity.from_int(1000), margin_init=Decimal("0.03"), margin_maint=Decimal("0.03"), maker_fee=Decimal("0.00002"), taker_fee=Decimal("0.00002"), ) ``` ## Adapters Representative adapters that create or consume `Cfd` instruments include: - [Interactive Brokers](../../integrations/interactive_brokers.md) for CFD contracts. ## Related guides - [Currency Pair](currency_pair.md) covers cash FX and crypto spot pairs. - [Commodity](commodity.md) covers spot commodity instruments. # Commodity Source: https://nautilustrader.io/docs/latest/concepts/instruments/commodity/ `Commodity` represents a spot commodity market such as gold, silver, oil, or another physical asset quoted in a currency. It models a spot market, not a dated futures contract. Examples include `XAUUSD.IDEALPRO` and venue-specific commodity cash symbols. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `asset_class` | `AssetClass` | `AssetClass` | Required | Commodity asset classification. | | `quote_currency` | `Currency` | `Currency` | Required | Currency used to price the commodity. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | | `lot_size` | `Option` | `Quantity \| None` | `None` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `Commodity` has instrument class `Spot`. - It allows negative prices: spot markets such as electricity or oil can trade below zero, and the `RiskEngine` accepts negative prices on both order submission and modification. - It is never inverse, and its cost currency is the quote currency. - It has no activation timestamp, expiry, strike, option kind, or settlement currency field. - Use `FuturesContract` for dated exchange-traded commodity futures. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ enums::AssetClass, identifiers::{InstrumentId, Symbol}, instruments::Commodity, types::{Currency, Price, Quantity}, }; let gold = Commodity::builder() .instrument_id(InstrumentId::from("GOLD.COMEX")) .raw_symbol(Symbol::from("GOLD")) .asset_class(AssetClass::Commodity) .quote_currency(Currency::from("USD")) .price_precision(2) .size_precision(0) .price_increment(Price::from("0.01")) .size_increment(Quantity::from("1")) .lot_size(Quantity::from("1")) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from nautilus_trader.model import AssetClass from nautilus_trader.model import Commodity from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol gold = Commodity( instrument_id=InstrumentId.from_str("GOLD.COMEX"), raw_symbol=Symbol("GOLD"), asset_class=AssetClass.COMMODITY, quote_currency=Currency.from_str("USD"), price_precision=2, price_increment=Price.from_str("0.01"), size_precision=0, size_increment=Quantity.from_int(1), ts_event=0, ts_init=0, lot_size=Quantity.from_int(1), ) ``` ## Adapters Representative adapters that create or consume `Commodity` instruments include: - [Interactive Brokers](../../integrations/interactive_brokers.md) for spot commodity and metal contracts. ## Related guides - [Futures Contract](futures_contract.md) covers dated futures on commodity underlyings. - [Data](../data/) explains market data that references instruments. # Crypto Future Source: https://nautilustrader.io/docs/latest/concepts/instruments/crypto_future/ `CryptoFuture` represents a dated crypto futures contract. It tracks a crypto underlying, quotes in a quote currency, settles in a settlement currency, and expires at a fixed timestamp. Examples include dated BTC or ETH futures on crypto derivatives venues. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `underlying` | `Currency` | `Currency` | Required | Crypto asset the contract tracks. | | `quote_currency` | `Currency` | `Currency` | Required | Currency used to quote the price. | | `settlement_currency` | `Currency` | `Currency` | Required | Currency used to settle PnL and fees. | | `is_inverse` | `bool` | `bool` | Required | True when sizing/costing is inverse. | | `activation_ns` | `UnixNanos` | `int` | Required | Contract activation timestamp. | | `expiration_ns` | `UnixNanos` | `int` | Required | Contract expiration timestamp. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `multiplier` | `Quantity` | `Quantity` | `1` | Contract multiplier. | | `lot_size` | `Quantity` | `Quantity` | `1` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `CryptoFuture` has asset class `Cryptocurrency` and instrument class `Future`. - Use `CryptoPerpetual` for crypto derivatives with no expiration. The currency set determines the settlement style: - **Linear**: typically sets `is_inverse=False` and settles in the quote currency. - **Inverse**: sets `is_inverse=True` and typically settles in the underlying currency. - **Quanto**: settles in a third currency that differs from both underlying and quote. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::CryptoFuture, types::{Currency, Money, Price, Quantity}, }; let activation: Timestamp = "2024-01-08T00:00:00Z".parse().unwrap(); let expiration: Timestamp = "2024-03-29T00:00:00Z".parse().unwrap(); let btcusdt_future = CryptoFuture::builder() .instrument_id(InstrumentId::from("BTCUSDT-240329.BINANCE")) .raw_symbol(Symbol::from("BTCUSDT-240329")) .underlying(Currency::from("BTC")) .quote_currency(Currency::from("USDT")) .settlement_currency(Currency::from("USDT")) .is_inverse(false) .activation_ns(UnixNanos::from(activation)) .expiration_ns(UnixNanos::from(expiration)) .price_precision(2) .size_precision(6) .price_increment(Price::from("0.01")) .size_increment(Quantity::from("0.000001")) .max_quantity(Quantity::from("9000.0")) .min_quantity(Quantity::from("0.000001")) .min_notional(Money::from("10.00 USDT")) .max_price(Price::from("1000000.00")) .min_price(Price::from("0.01")) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" import pandas as pd from nautilus_trader.model import CryptoFuture from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Money from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol BTC = Currency.from_str("BTC") USDT = Currency.from_str("USDT") btcusdt_future = CryptoFuture( instrument_id=InstrumentId.from_str("BTCUSDT-240329.BINANCE"), raw_symbol=Symbol("BTCUSDT-240329"), underlying=BTC, quote_currency=USDT, settlement_currency=USDT, is_inverse=False, activation_ns=pd.Timestamp("2024-01-08", tz="UTC").value, expiration_ns=pd.Timestamp("2024-03-29", tz="UTC").value, price_precision=2, size_precision=6, price_increment=Price.from_str("0.01"), size_increment=Quantity.from_str("0.000001"), max_quantity=Quantity.from_str("9000"), min_quantity=Quantity.from_str("0.000001"), min_notional=Money(10.00, USDT), max_price=Price.from_str("1000000.00"), min_price=Price.from_str("0.01"), ts_event=0, ts_init=0, ) ``` ## Adapters Representative adapters that create or consume `CryptoFuture` instruments include: - [BitMEX](../../integrations/bitmex.md) for inverse and linear dated futures. - [Bybit](../../integrations/bybit.md) for crypto futures markets. - [Deribit](../../integrations/deribit.md) for dated crypto futures. - [OKX](../../integrations/okx.md) for dated crypto futures. - [Tardis](../../integrations/tardis.md) for crypto futures metadata. ## Related guides - [Crypto Perpetual](crypto_perpetual.md) covers perpetual crypto futures. - [Futures Contract](futures_contract.md) covers non-crypto futures contracts. # Crypto Futures Spread Source: https://nautilustrader.io/docs/latest/concepts/instruments/crypto_futures_spread/ `CryptoFuturesSpread` represents an exchange-defined spread strategy over crypto futures. The venue publishes the strategy as a single instrument with its own symbol, strategy type, precision, increments, and expiration. Examples include listed crypto futures calendar spreads. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `underlying` | `Currency` | `Currency` | Required | Crypto asset the strategy tracks. | | `quote_currency` | `Currency` | `Currency` | Required | Currency used to quote the price. | | `settlement_currency` | `Currency` | `Currency` | Required | Currency used to settle PnL and fees. | | `is_inverse` | `bool` | `bool` | Required | True when sizing/costing is inverse. | | `strategy_type` | `Ustr` | `str` | Required | Venue strategy type, such as calendar. | | `activation_ns` | `UnixNanos` | `int` | Required | Strategy activation timestamp. | | `expiration_ns` | `UnixNanos` | `int` | Required | Strategy expiration timestamp. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `multiplier` | `Quantity` | `Quantity` | `1` | Strategy multiplier. | | `lot_size` | `Quantity` | `Quantity` | `1` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `CryptoFuturesSpread` has asset class `Cryptocurrency` and instrument class `FuturesSpread`. - The venue publishes the spread as a single tradable instrument. - The strategy can be linear, inverse, or quanto, depending on the currency set. - Spread prices can be zero or negative, and the `RiskEngine` accepts non-positive prices for this instrument class. - Store venue-specific leg details in `info` when the adapter provides them. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::CryptoFuturesSpread, types::{Currency, Price, Quantity}, }; use rust_decimal_macros::dec; use ustr::Ustr; let activation: Timestamp = "2026-05-12T00:00:00Z".parse().unwrap(); let expiration: Timestamp = "2026-05-19T08:00:00Z".parse().unwrap(); let btc_spread = CryptoFuturesSpread::builder() .instrument_id(InstrumentId::from("BTC-FS-19MAY26_PERP.DERIBIT")) .raw_symbol(Symbol::from("BTC-FS-19MAY26_PERP")) .underlying(Currency::from("BTC")) .quote_currency(Currency::from("USD")) .settlement_currency(Currency::from("BTC")) .is_inverse(false) .strategy_type(Ustr::from("FS")) .activation_ns(UnixNanos::from(activation)) .expiration_ns(UnixNanos::from(expiration)) .price_precision(1) .size_precision(0) .price_increment(Price::from("0.5")) .size_increment(Quantity::from("1")) .multiplier(Quantity::from("10")) .lot_size(Quantity::from("1")) .min_quantity(Quantity::from("1")) .maker_fee(dec!(0.0003)) .taker_fee(dec!(0.0003)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from decimal import Decimal import pandas as pd from nautilus_trader.model import CryptoFuturesSpread from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol BTC = Currency.from_str("BTC") USD = Currency.from_str("USD") btc_spread = CryptoFuturesSpread( instrument_id=InstrumentId.from_str("BTC-FS-19MAY26_PERP.DERIBIT"), raw_symbol=Symbol("BTC-FS-19MAY26_PERP"), underlying=BTC, quote_currency=USD, settlement_currency=BTC, is_inverse=False, strategy_type="FS", activation_ns=pd.Timestamp("2026-05-12T00:00:00", tz="UTC").value, expiration_ns=pd.Timestamp("2026-05-19T08:00:00", tz="UTC").value, price_precision=1, size_precision=0, price_increment=Price.from_str("0.5"), size_increment=Quantity.from_int(1), multiplier=Quantity.from_int(10), lot_size=Quantity.from_int(1), min_quantity=Quantity.from_int(1), maker_fee=Decimal("0.0003"), taker_fee=Decimal("0.0003"), ts_event=0, ts_init=0, ) ``` ## Adapters Representative adapters that create or consume `CryptoFuturesSpread` instruments include: - [Deribit](../../integrations/deribit.md) for crypto futures combos. - [OKX](../../integrations/okx.md) for crypto futures spread markets. ## Related guides - [Crypto Future](crypto_future.md) covers single-leg dated crypto futures. - [Futures Spread](futures_spread.md) covers non-crypto futures spreads. # Crypto Option Source: https://nautilustrader.io/docs/latest/concepts/instruments/crypto_option/ `CryptoOption` represents a put or call option on a crypto underlying. It defines the option kind, strike price, activation time, expiration time, quote currency, settlement currency, and contract sizing. Examples include BTC and ETH options on crypto derivatives venues. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `underlying` | `Currency` | `Currency` | Required | Crypto asset the option tracks. | | `quote_currency` | `Currency` | `Currency` | Required | Currency used to quote the premium. | | `settlement_currency` | `Currency` | `Currency` | Required | Currency used to settle PnL and fees. | | `is_inverse` | `bool` | `bool` | Required | True when sizing/costing is inverse. | | `option_kind` | `OptionKind` | `OptionKind` | Required | Put or call. | | `strike_price` | `Price` | `Price` | Required | Option strike price. | | `activation_ns` | `UnixNanos` | `int` | Required | Contract activation timestamp. | | `expiration_ns` | `UnixNanos` | `int` | Required | Contract expiration timestamp. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `multiplier` | `Quantity` | `Quantity` | `1` | Contract multiplier. | | `lot_size` | `Quantity` | `Quantity` | `1` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `1` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `CryptoOption` has asset class `Cryptocurrency` and instrument class `Option`. - The option kind and strike price define the payoff shape. - The contract can be linear, inverse, or quanto, depending on the currency set. - Use `OptionContract` for non-crypto listed options. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ enums::OptionKind, identifiers::{InstrumentId, Symbol}, instruments::CryptoOption, types::{Currency, Money, Price, Quantity}, }; use rust_decimal_macros::dec; let activation: Timestamp = "2022-12-22T00:00:00Z".parse().unwrap(); let expiration: Timestamp = "2023-01-13T08:00:00Z".parse().unwrap(); let btc_option = CryptoOption::builder() .instrument_id(InstrumentId::from("BTC-13JAN23-16000-P.DERIBIT")) .raw_symbol(Symbol::from("BTC-13JAN23-16000-P")) .underlying(Currency::from("BTC")) .quote_currency(Currency::from("USD")) .settlement_currency(Currency::from("BTC")) .is_inverse(false) .option_kind(OptionKind::Put) .strike_price(Price::from("16000.00")) .activation_ns(UnixNanos::from(activation)) .expiration_ns(UnixNanos::from(expiration)) .price_precision(2) .size_precision(1) .price_increment(Price::from("0.01")) .size_increment(Quantity::from("0.1")) .multiplier(Quantity::from("1")) .lot_size(Quantity::from("1")) .max_quantity(Quantity::from("9000")) .min_quantity(Quantity::from("0.1")) .min_notional(Money::from("10.00 USD")) .margin_init(dec!(0)) .margin_maint(dec!(0)) .maker_fee(dec!(0.0003)) .taker_fee(dec!(0.0003)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from decimal import Decimal import pandas as pd from nautilus_trader.model import CryptoOption from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Money from nautilus_trader.model import OptionKind from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol BTC = Currency.from_str("BTC") USD = Currency.from_str("USD") btc_option = CryptoOption( instrument_id=InstrumentId.from_str("BTC-13JAN23-16000-P.DERIBIT"), raw_symbol=Symbol("BTC-13JAN23-16000-P"), underlying=BTC, quote_currency=USD, settlement_currency=BTC, is_inverse=False, option_kind=OptionKind.PUT, strike_price=Price.from_str("16000.00"), activation_ns=pd.Timestamp("2022-12-22", tz="UTC").value, expiration_ns=pd.Timestamp("2023-01-13T08:00:00", tz="UTC").value, price_precision=2, size_precision=1, price_increment=Price.from_str("0.01"), size_increment=Quantity.from_str("0.1"), max_quantity=Quantity.from_str("9000"), min_quantity=Quantity.from_str("0.1"), min_notional=Money(10.00, USD), margin_init=Decimal(0), margin_maint=Decimal(0), maker_fee=Decimal("0.0003"), taker_fee=Decimal("0.0003"), ts_event=0, ts_init=0, ) ``` ## Adapters Representative adapters that create or consume `CryptoOption` instruments include: - [Bybit](../../integrations/bybit.md) for crypto options. - [Deribit](../../integrations/deribit.md) for crypto options. - [OKX](../../integrations/okx.md) for crypto options. - [Tardis](../../integrations/tardis.md) for crypto option metadata. ## Related guides - [Options](../options.md) covers option data, Greeks, and chain subscriptions. - [Crypto Option Spread](crypto_option_spread.md) covers exchange-defined crypto option spreads. # Crypto Option Spread Source: https://nautilustrader.io/docs/latest/concepts/instruments/crypto_option_spread/ `CryptoOptionSpread` represents an exchange-defined strategy over crypto options. The venue publishes the strategy as one instrument with its own symbol, strategy type, precision, increments, and expiration. Examples include listed BTC or ETH option combos on crypto derivatives venues. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `underlying` | `Currency` | `Currency` | Required | Crypto asset the strategy tracks. | | `quote_currency` | `Currency` | `Currency` | Required | Currency used to quote the premium. | | `settlement_currency` | `Currency` | `Currency` | Required | Currency used to settle PnL and fees. | | `is_inverse` | `bool` | `bool` | Required | True when sizing/costing is inverse. | | `strategy_type` | `Ustr` | `str` | Required | Venue strategy type, such as vertical. | | `activation_ns` | `UnixNanos` | `int` | Required | Strategy activation timestamp. | | `expiration_ns` | `UnixNanos` | `int` | Required | Strategy expiration timestamp. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `multiplier` | `Quantity` | `Quantity` | `1` | Strategy multiplier. | | `lot_size` | `Quantity` | `Quantity` | `1` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `CryptoOptionSpread` has asset class `Cryptocurrency` and instrument class `OptionSpread`. - The venue publishes the spread as a single tradable instrument. - The strategy can be linear, inverse, or quanto, depending on the currency set. - Spread prices can be zero or negative, and the `RiskEngine` accepts non-positive prices for this instrument class. - Store venue-specific leg details in `info` when the adapter provides them. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::CryptoOptionSpread, types::{Currency, Price, Quantity}, }; use rust_decimal_macros::dec; use ustr::Ustr; let activation: Timestamp = "2026-05-12T00:00:00Z".parse().unwrap(); let expiration: Timestamp = "2026-05-19T08:00:00Z".parse().unwrap(); let btc_spread = CryptoOptionSpread::builder() .instrument_id(InstrumentId::from("BTC-CS-19MAY26-70000_75000.DERIBIT")) .raw_symbol(Symbol::from("BTC-CS-19MAY26-70000_75000")) .underlying(Currency::from("BTC")) .quote_currency(Currency::from("USD")) .settlement_currency(Currency::from("BTC")) .is_inverse(false) .strategy_type(Ustr::from("CS")) .activation_ns(UnixNanos::from(activation)) .expiration_ns(UnixNanos::from(expiration)) .price_precision(4) .size_precision(1) .price_increment(Price::from("0.0001")) .size_increment(Quantity::from("0.1")) .multiplier(Quantity::from("1")) .min_quantity(Quantity::from("0.1")) .maker_fee(dec!(0.0003)) .taker_fee(dec!(0.0003)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from decimal import Decimal import pandas as pd from nautilus_trader.model import CryptoOptionSpread from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol BTC = Currency.from_str("BTC") USD = Currency.from_str("USD") btc_spread = CryptoOptionSpread( instrument_id=InstrumentId.from_str("BTC-CS-19MAY26-70000_75000.DERIBIT"), raw_symbol=Symbol("BTC-CS-19MAY26-70000_75000"), underlying=BTC, quote_currency=USD, settlement_currency=BTC, is_inverse=False, strategy_type="CS", activation_ns=pd.Timestamp("2026-05-12T00:00:00", tz="UTC").value, expiration_ns=pd.Timestamp("2026-05-19T08:00:00", tz="UTC").value, price_precision=4, size_precision=1, price_increment=Price.from_str("0.0001"), size_increment=Quantity.from_str("0.1"), min_quantity=Quantity.from_str("0.1"), maker_fee=Decimal("0.0003"), taker_fee=Decimal("0.0003"), ts_event=0, ts_init=0, ) ``` ## Adapters Representative adapters that create or consume `CryptoOptionSpread` instruments include: - [Deribit](../../integrations/deribit.md) for crypto option combos. - [OKX](../../integrations/okx.md) for crypto option spread markets. ## Related guides - [Crypto Option](crypto_option.md) covers single-leg crypto options. - [Option Spread](option_spread.md) covers non-crypto option spreads. # Crypto Perpetual Source: https://nautilustrader.io/docs/latest/concepts/instruments/crypto_perpetual/ `CryptoPerpetual` represents a crypto perpetual futures contract, also known as a perpetual swap. It has no expiry, tracks a crypto base asset, and settles in a crypto, stablecoin, or other venue-defined settlement currency. Examples include `ETHUSDT-PERP.BINANCE`, `XBTUSD.BITMEX`, and `BTC-USD-SWAP.OKX`. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `base_currency` | `Currency` | `Currency` | Required | Base crypto asset. | | `quote_currency` | `Currency` | `Currency` | Required | Price quote currency. | | `settlement_currency` | `Currency` | `Currency` | Required | Currency used to settle PnL and fees. | | `is_inverse` | `bool` | `bool` | Required | True when sizing/costing is inverse. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | | `multiplier` | `Quantity` | `Quantity` | `1` | Contract multiplier. | | `lot_size` | `Quantity` | `Quantity` | `1` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `CryptoPerpetual` has asset class `Cryptocurrency` and instrument class `Swap`. - It has no activation or expiration timestamp. The currency set determines the settlement style: - **Linear**: typically sets `is_inverse=False` and settles in the quote currency. - **Inverse**: sets `is_inverse=True` and typically settles in the base currency. - **Quanto**: settles in a third currency that differs from both base and quote. The cost currency follows from that style: base for inverse contracts, settlement for quanto contracts, and quote otherwise. :::note Funding payments are not fields on the instrument. They arrive as data, such as `FundingRateUpdate`, and reference the instrument ID. ::: ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::{CryptoPerpetual, InstrumentAny}, types::{Currency, Money, Price, Quantity}, }; use rust_decimal_macros::dec; let ethusdt_perp = CryptoPerpetual::builder() .instrument_id(InstrumentId::from("ETHUSDT-PERP.BINANCE")) .raw_symbol(Symbol::from("ETHUSDT")) .base_currency(Currency::from("ETH")) .quote_currency(Currency::from("USDT")) .settlement_currency(Currency::from("USDT")) .is_inverse(false) .price_precision(2) .size_precision(3) .price_increment(Price::from("0.01")) .size_increment(Quantity::from("0.001")) .max_quantity(Quantity::from("10000.000")) .min_quantity(Quantity::from("0.001")) .min_notional(Money::from("10.00 USDT")) .max_price(Price::from("15000.00")) .min_price(Price::from("1.00")) .margin_init(dec!(1.0)) .margin_maint(dec!(0.35)) .maker_fee(dec!(0.0002)) .taker_fee(dec!(0.0004)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); let instrument = InstrumentAny::CryptoPerpetual(ethusdt_perp); ``` ```python tab="Python" from decimal import Decimal from nautilus_trader.model import CryptoPerpetual from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Money from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol ETH = Currency.from_str("ETH") USDT = Currency.from_str("USDT") ethusdt_perp = CryptoPerpetual( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), raw_symbol=Symbol("ETHUSDT"), base_currency=ETH, quote_currency=USDT, settlement_currency=USDT, is_inverse=False, price_precision=2, size_precision=3, price_increment=Price.from_str("0.01"), size_increment=Quantity.from_str("0.001"), ts_event=0, ts_init=0, max_quantity=Quantity.from_str("10000.000"), min_quantity=Quantity.from_str("0.001"), min_notional=Money(10.00, USDT), max_price=Price.from_str("15000.00"), min_price=Price.from_str("1.00"), margin_init=Decimal("1.0"), margin_maint=Decimal("0.35"), maker_fee=Decimal("0.0002"), taker_fee=Decimal("0.0004"), ) ``` ## Adapters Representative adapters that create or consume `CryptoPerpetual` instruments include: - [Binance](../../integrations/binance.md) for USD-M and COIN-M perpetual futures. - [BitMEX](../../integrations/bitmex.md) for inverse and linear perpetual contracts. - [Bybit](../../integrations/bybit.md) for linear and inverse perpetual products. - [dYdX](../../integrations/dydx.md) for perpetual markets. - [Hyperliquid](../../integrations/hyperliquid.md) for perpetual markets. - [Kraken](../../integrations/kraken.md) for futures venue perpetual markets. - [OKX](../../integrations/okx.md) for swap markets. - [Tardis](../../integrations/tardis.md) for crypto perpetual metadata. ## Related guides - [Data](../data/) covers mark prices, index prices, and funding rate updates. - [Options](../options.md) covers option-specific instrument types. - [Execution](../execution/) explains precision and notional checks before orders reach a venue. # Currency Pair Source: https://nautilustrader.io/docs/latest/concepts/instruments/currency_pair/ `CurrencyPair` represents a spot or cash market quoted as `BASE/QUOTE`. The base currency is the asset being bought or sold, and the quote currency prices one unit of the base. Nautilus uses this type for fiat FX pairs and crypto spot pairs. Examples include `EUR/USD.SIM`, `BTCUSDT.BINANCE`, and `ETH/USD.KRAKEN`. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `base_currency` | `Currency` | `Currency` | Required | Asset bought or sold. | | `quote_currency` | `Currency` | `Currency` | Required | Currency used to price the base asset. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | | `multiplier` | `Quantity` | `Quantity` | `1` | Contract multiplier. | | `lot_size` | `Option` | `Quantity \| None` | `None` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `CurrencyPair` has instrument class `Spot`. - Its asset class is `Cryptocurrency` when either currency has crypto type; otherwise, it is `FX`. - It has no expiration, strike price, option kind, or derivative underlying field. - It is never inverse. The settlement currency and cost currency are the quote currency. - Use this type for both fiat FX pairs and crypto spot pairs. :::note Use the specific derivative type for dated futures, swaps, and options. This keeps cost currency, settlement currency, expiration, and notional calculations aligned with the venue, including when the symbol looks like a pair. ::: ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::CurrencyPair, types::{Currency, Money, Price, Quantity}, }; use rust_decimal_macros::dec; let btcusdt = CurrencyPair::builder() .instrument_id(InstrumentId::from("BTCUSDT.BINANCE")) .raw_symbol(Symbol::from("BTCUSDT")) .base_currency(Currency::from("BTC")) .quote_currency(Currency::from("USDT")) .price_precision(2) .size_precision(6) .price_increment(Price::from("0.01")) .size_increment(Quantity::from("0.000001")) .min_quantity(Quantity::from("0.000001")) .min_notional(Money::from("10.00 USDT")) .max_price(Price::from("1000000.00")) .min_price(Price::from("0.01")) .margin_init(dec!(0.001)) .margin_maint(dec!(0.001)) .maker_fee(dec!(0.001)) .taker_fee(dec!(0.001)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from decimal import Decimal from nautilus_trader.model import Currency from nautilus_trader.model import CurrencyPair from nautilus_trader.model import InstrumentId from nautilus_trader.model import Money from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol BTC = Currency.from_str("BTC") USDT = Currency.from_str("USDT") btcusdt = CurrencyPair( instrument_id=InstrumentId.from_str("BTCUSDT.BINANCE"), raw_symbol=Symbol("BTCUSDT"), base_currency=BTC, quote_currency=USDT, price_precision=2, size_precision=6, price_increment=Price.from_str("0.01"), size_increment=Quantity.from_str("0.000001"), ts_event=0, ts_init=0, min_quantity=Quantity.from_str("0.000001"), min_notional=Money(10.00, USDT), max_price=Price.from_str("1000000.00"), min_price=Price.from_str("0.01"), margin_init=Decimal("0.001"), margin_maint=Decimal("0.001"), maker_fee=Decimal("0.001"), taker_fee=Decimal("0.001"), ) ``` ## Adapters Representative adapters that create or consume `CurrencyPair` instruments include: - [Binance](../../integrations/binance.md) for spot markets. - [Kraken](../../integrations/kraken.md) for spot markets. - [OKX](../../integrations/okx.md) for spot markets. - [Tardis](../../integrations/tardis.md) for spot metadata. - [Interactive Brokers](../../integrations/interactive_brokers.md) for FX cash contracts. - [Hyperliquid](../../integrations/hyperliquid.md) for spot assets. ## Related guides - [Data](../data/) explains market data that references instruments. - [Execution](../execution/) explains order checks that use instrument precision. - [Value types](../value_types.md) explains `Price`, `Quantity`, and `Money`. # Equity Source: https://nautilustrader.io/docs/latest/concepts/instruments/equity/ `Equity` represents a listed share, ETF, or similar cash-market security. Nautilus uses this type for instruments that trade in whole units, quote in one currency, and have no contract expiry. Examples include `AAPL.XNAS`, `MSFT.XNAS`, and venue-specific ETF symbols. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `currency` | `Currency` | `Currency` | Required | Quote and settlement currency. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `lot_size` | `Option` | `Quantity \| None` | `None` | Board lot or whole-share lot size. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | | `isin` | `Option` | `str \| None` | `None` | International Securities ID when known. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `Equity` has asset class `Equity` and instrument class `Spot`. - Quantity precision is always zero, so orders use whole-share quantities. - The multiplier and size increment are one. - It has no base currency, expiry, strike, option kind, or inverse costing flag. - Use price limits only when the venue publishes them. :::warning `Equity` fixes size precision at zero. The `RiskEngine` denies any order whose quantity precision exceeds the instrument size precision, so fractional-share quantities are rejected. ::: ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::Equity, types::{Currency, Price, Quantity}, }; use ustr::Ustr; let aapl = Equity::builder() .instrument_id(InstrumentId::from("AAPL.XNAS")) .raw_symbol(Symbol::from("AAPL")) .isin(Ustr::from("US0378331005")) .currency(Currency::from("USD")) .price_precision(2) .price_increment(Price::from("0.01")) .lot_size(Quantity::from("100")) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from nautilus_trader.model import Currency from nautilus_trader.model import Equity from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol aapl = Equity( instrument_id=InstrumentId.from_str("AAPL.XNAS"), raw_symbol=Symbol("AAPL"), currency=Currency.from_str("USD"), price_precision=2, price_increment=Price.from_str("0.01"), ts_event=0, ts_init=0, isin="US0378331005", lot_size=Quantity.from_int(100), ) ``` ## Adapters Representative adapters that create or consume `Equity` instruments include: - [Databento](../../integrations/databento.md) for listed US equities and ETFs. - [Interactive Brokers](../../integrations/interactive_brokers.md) for listed equity contracts. ## Related guides - [Data](../data/) explains market data that references instruments. - [Value types](../value_types.md) explains `Price`, `Quantity`, and `Money`. # Futures Contract Source: https://nautilustrader.io/docs/latest/concepts/instruments/futures_contract/ `FuturesContract` represents a dated, exchange-traded futures contract with a defined underlying, activation time, expiration time, currency, multiplier, and lot size. Examples include equity index futures, commodity futures, interest-rate futures, and currency futures. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `asset_class` | `AssetClass` | `AssetClass` | Required | Asset class of the underlying. | | `exchange` | `Option` | `str \| None` | `None` | Exchange MIC or venue code when known. | | `underlying` | `Ustr` | `str` | Required | Underlying asset, index, or product. | | `activation_ns` | `UnixNanos` | `int` | Required | Contract activation timestamp. | | `expiration_ns` | `UnixNanos` | `int` | Required | Contract expiration timestamp. | | `currency` | `Currency` | `Currency` | Required | Quote and settlement currency. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_precision` | `u8` | `int` | Fixed `0` | Futures trade in whole contracts. | | `size_increment` | `Quantity` | `Quantity` | Fixed `1` | Minimum contract size step. | | `multiplier` | `Quantity` | `Quantity` | Required | Contract multiplier. | | `lot_size` | `Quantity` | `Quantity` | Required | Rounded lot or contract lot size. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `1` | Minimum order quantity. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `FuturesContract` has instrument class `Future`. - It is never inverse. Cost, settlement, and quote currency use `currency`. - It trades in whole contracts with size precision `0` and size increment `1`. - Use `CryptoFuture` for dated crypto futures where the underlying and settlement currencies can differ. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ enums::AssetClass, identifiers::{InstrumentId, Symbol}, instruments::FuturesContract, types::{Currency, Price, Quantity}, }; use ustr::Ustr; let activation: Timestamp = "2021-09-10T00:00:00Z".parse().unwrap(); let expiration: Timestamp = "2021-12-17T00:00:00Z".parse().unwrap(); let esz21 = FuturesContract::builder() .instrument_id(InstrumentId::from("ESZ21.GLBX")) .raw_symbol(Symbol::from("ESZ21")) .asset_class(AssetClass::Index) .exchange(Ustr::from("XCME")) .underlying(Ustr::from("ES")) .activation_ns(UnixNanos::from(activation)) .expiration_ns(UnixNanos::from(expiration)) .currency(Currency::from("USD")) .price_precision(2) .price_increment(Price::from("0.25")) .multiplier(Quantity::from("1")) .lot_size(Quantity::from("1")) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" import pandas as pd from nautilus_trader.model import AssetClass from nautilus_trader.model import Currency from nautilus_trader.model import FuturesContract from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol esz21 = FuturesContract( instrument_id=InstrumentId.from_str("ESZ21.GLBX"), raw_symbol=Symbol("ESZ21"), asset_class=AssetClass.INDEX, underlying="ES", activation_ns=pd.Timestamp("2021-09-10", tz="UTC").value, expiration_ns=pd.Timestamp("2021-12-17", tz="UTC").value, currency=Currency.from_str("USD"), price_precision=2, price_increment=Price.from_str("0.25"), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), ts_event=0, ts_init=0, exchange="XCME", ) ``` ## Adapters Representative adapters that create or consume `FuturesContract` instruments include: - [Databento](../../integrations/databento.md) for futures reference data and market data. - [Interactive Brokers](../../integrations/interactive_brokers.md) for listed futures contracts. ## Related guides - [Continuous Futures](../continuous_futures.md) covers roll-adjusted futures series. - [Crypto Future](crypto_future.md) covers dated crypto futures contracts. # Futures Spread Source: https://nautilustrader.io/docs/latest/concepts/instruments/futures_spread/ `FuturesSpread` represents an exchange-defined futures strategy with more than one leg, such as a calendar spread or inter-commodity spread. The venue defines the strategy, symbol, tick size, and expiry. Examples include listed futures calendar spreads and exchange-supported spread markets. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ----------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `asset_class` | `AssetClass` | `AssetClass` | Required | Asset class of the underlying strategy. | | `exchange` | `Option` | `str \| None` | `None` | Exchange MIC or venue code when known. | | `underlying` | `Ustr` | `str` | Required | Underlying product or product family. | | `strategy_type` | `Ustr` | `str` | Required | Venue strategy type, such as calendar. | | `activation_ns` | `UnixNanos` | `int` | Required | Strategy activation timestamp. | | `expiration_ns` | `UnixNanos` | `int` | Required | Strategy expiration timestamp. | | `currency` | `Currency` | `Currency` | Required | Quote and settlement currency. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_precision` | `u8` | `int` | Fixed `0` | Futures spreads trade in whole contracts. | | `size_increment` | `Quantity` | `Quantity` | Fixed `1` | Minimum contract size step. | | `multiplier` | `Quantity` | `Quantity` | Required | Strategy multiplier. | | `lot_size` | `Quantity` | `Quantity` | Required | Rounded lot or contract lot size. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `1` | Minimum order quantity. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `FuturesSpread` has instrument class `FuturesSpread`. - The venue publishes the spread as a single tradable instrument. - It trades in whole contracts with size precision `0` and size increment `1`. - Spread prices can be zero or negative, and the `RiskEngine` accepts non-positive prices for this instrument class. - Use leg data from the adapter metadata when a strategy needs venue-specific leg details. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ enums::AssetClass, identifiers::{InstrumentId, Symbol}, instruments::FuturesSpread, types::{Currency, Price, Quantity}, }; use ustr::Ustr; let activation: Timestamp = "2022-06-21T13:30:00Z".parse().unwrap(); let expiration: Timestamp = "2024-06-21T13:30:00Z".parse().unwrap(); let es_spread = FuturesSpread::builder() .instrument_id(InstrumentId::from("ESM4-ESU4.GLBX")) .raw_symbol(Symbol::from("ESM4-ESU4")) .asset_class(AssetClass::Index) .exchange(Ustr::from("XCME")) .underlying(Ustr::from("ES")) .strategy_type(Ustr::from("EQ")) .activation_ns(UnixNanos::from(activation)) .expiration_ns(UnixNanos::from(expiration)) .currency(Currency::from("USD")) .price_precision(2) .price_increment(Price::from("0.01")) .multiplier(Quantity::from("1")) .lot_size(Quantity::from("1")) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" import pandas as pd from nautilus_trader.model import AssetClass from nautilus_trader.model import Currency from nautilus_trader.model import FuturesSpread from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol es_spread = FuturesSpread( instrument_id=InstrumentId.from_str("ESM4-ESU4.GLBX"), raw_symbol=Symbol("ESM4-ESU4"), asset_class=AssetClass.INDEX, underlying="ES", strategy_type="EQ", activation_ns=pd.Timestamp("2022-06-21T13:30:00", tz="UTC").value, expiration_ns=pd.Timestamp("2024-06-21T13:30:00", tz="UTC").value, currency=Currency.from_str("USD"), price_precision=2, price_increment=Price.from_str("0.01"), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), ts_event=0, ts_init=0, exchange="XCME", ) ``` ## Adapters Representative adapters that create or consume `FuturesSpread` instruments include: - [Databento](../../integrations/databento.md) for listed futures spread markets. - [Interactive Brokers](../../integrations/interactive_brokers.md) for exchange-defined futures strategies. ## Related guides - [Futures Contract](futures_contract.md) covers single-leg futures. - [Continuous Futures](../continuous_futures.md) covers roll-adjusted futures series. # Instruments Source: https://nautilustrader.io/docs/latest/concepts/instruments/ An instrument represents the specification for a tradable asset, contract, or local synthetic market. Market data, orders, positions, accounting, portfolio calculations, and adapter symbology all refer back to an `InstrumentId` and its instrument definition. NautilusTrader exposes the same instrument model to Rust and Python users. Rust examples use `nautilus_model`; Python examples use `nautilus_trader.model`. ## Instrument types | Instrument type | `InstrumentClass` | Description | Typical adapters | | ------------------------------------------------- | ----------------- | ---------------------------------------------------- | ------------------------------- | | [`Equity`](equity.md) | `SPOT` | Listed share or ETF traded on a cash market. | Databento, Interactive Brokers. | | [`CurrencyPair`](currency_pair.md) | `SPOT` | Fiat FX or crypto spot pair in base/quote form. | Binance, Kraken, OKX, Tardis. | | [`Commodity`](commodity.md) | `SPOT` | Spot commodity such as gold or oil. | Interactive Brokers. | | [`Cfd`](cfd.md) | `CFD` | Contract for difference tracking an underlying. | Interactive Brokers. | | [`IndexInstrument`](index_instrument.md) | `SPOT` | Reference index, not directly tradable. | Interactive Brokers. | | [`TokenizedAsset`](tokenized_asset.md) | `SPOT` | Tokenized asset on a crypto venue. | Kraken. | | [`FuturesContract`](futures_contract.md) | `FUTURE` | Dated futures contract. | Databento, Interactive Brokers. | | [`FuturesSpread`](futures_spread.md) | `FUTURES_SPREAD` | Exchange defined futures strategy with several legs. | Databento, Interactive Brokers. | | [`CryptoFuture`](crypto_future.md) | `FUTURE` | Dated crypto futures contract. | BitMEX, Bybit, Deribit, OKX. | | [`CryptoFuturesSpread`](crypto_futures_spread.md) | `FUTURES_SPREAD` | Exchange defined crypto futures spread. | Deribit, OKX. | | [`CryptoPerpetual`](crypto_perpetual.md) | `SWAP` | Crypto perpetual futures contract. | Binance, BitMEX, Bybit, dYdX. | | [`PerpetualContract`](perpetual_contract.md) | `SWAP` | Perpetual futures contract across asset classes. | Architect AX, Binance. | | [`OptionContract`](option_contract.md) | `OPTION` | Exchange traded put or call option. | Databento, Interactive Brokers. | | [`OptionSpread`](option_spread.md) | `OPTION_SPREAD` | Exchange defined options strategy with several legs. | Databento, Interactive Brokers. | | [`CryptoOption`](crypto_option.md) | `OPTION` | Option on a crypto underlying. | Bybit, Deribit, OKX, Tardis. | | [`CryptoOptionSpread`](crypto_option_spread.md) | `OPTION_SPREAD` | Exchange defined crypto option spread. | Deribit, OKX. | | [`BinaryOption`](binary_option.md) | `BINARY_OPTION` | Binary instrument that settles to 0 or 1. | Hyperliquid, OKX, Polymarket. | | [`BettingInstrument`](betting_instrument.md) | `SPORTS_BETTING` | Sports or gaming market selection. | Betfair. | | [`SyntheticInstrument`](synthetic_instrument.md) | n/a | Formula derived local instrument. | Local only. | ## Taxonomy NautilusTrader groups instruments by the market structure they represent: ```mermaid flowchart TD I[Instrument Types] I --> Spot I --> Derivatives I --> Other Spot --> Equity Spot --> CurrencyPair Spot --> Commodity Spot --> IndexInstrument Spot --> TokenizedAsset Derivatives --> Futures Derivatives --> Options Derivatives --> Swaps Derivatives --> Cfd Futures --> FuturesContract Futures --> FuturesSpread Futures --> CryptoFuture Futures --> CryptoFuturesSpread Options --> OptionContract Options --> OptionSpread Options --> CryptoOption Options --> CryptoOptionSpread Options --> BinaryOption Swaps --> CryptoPerpetual Swaps --> PerpetualContract Other --> BettingInstrument Other --> SyntheticInstrument ``` ## Common fields Most concrete instruments share the same core shape. Individual type pages list the complete constructor and struct fields for that type. | Field | Meaning | | ----------------- | ------------------------------------------------------------------- | | `id` | Nautilus `InstrumentId`, formed from a symbol and venue. | | `raw_symbol` | Native venue symbol before Nautilus normalization. | | `price_precision` | Configured number of decimal places for price values. | | `size_precision` | Configured number of decimal places for quantity values. | | `price_increment` | Smallest valid price step. | | `size_increment` | Smallest valid quantity step. | | `multiplier` | Contract multiplier used in notional and PnL calculations. | | `lot_size` | Rounded lot or board size when the venue publishes one. | | `margin_init` | Initial margin rate as a decimal fraction of notional value. | | `margin_maint` | Maintenance margin rate as a decimal fraction of notional value. | | `maker_fee` | Maker fee rate. Negative values represent rebates. | | `taker_fee` | Taker fee rate. Negative values represent rebates. | | `max_quantity` | Maximum order quantity when known. | | `min_quantity` | Minimum order quantity when known. | | `max_notional` | Maximum order notional value when known. | | `min_notional` | Minimum order notional value when known. | | `max_price` | Maximum valid quote or order price when known. | | `min_price` | Minimum valid quote or order price when known. | | `tick_scheme` | Registered variable tick scheme name where the type supports one. | | `info` | Adapter metadata preserved from the venue or data source. | | `ts_event` | UNIX nanosecond timestamp for when the definition event occurred. | | `ts_init` | UNIX nanosecond timestamp for when Nautilus initialized the object. | ## Symbology Every instrument has a unique `InstrumentId` made from a Nautilus symbol and venue, separated by a period. The separate `raw_symbol` field preserves the venue's native symbol. For example, Binance Futures represents the Ethereum perpetual contract as: ```text ETHUSDT-PERP.BINANCE ``` Native symbols should be unique for a venue, but this is not guaranteed by every exchange. The Nautilus `{symbol}.{venue}` pair must be unique inside a system. :::warning The instrument definition must match the market data and venue order semantics. An incorrect instrument can truncate prices or quantities, calculate notional values with the wrong currency, or make a backtest accept prices a live venue would reject. ::: ## Rust and Python surfaces Rust users work with the `nautilus_model` instrument structs and `InstrumentAny`: ```rust use nautilus_model::instruments::{CurrencyPair, InstrumentAny}; ``` Python users normally work with instrument classes from `nautilus_trader.model`: ```python from nautilus_trader.model import CurrencyPair ``` Both surfaces represent the same instrument contract: identity, precision, increments, currencies, limits, margins, fees, metadata, and timestamps. ## Loading instruments Generic test instruments can be instantiated through the `TestInstrumentProvider`: ```python from nautilus_trader.testkit.providers import TestInstrumentProvider audusd = TestInstrumentProvider.default_fx_ccy("AUD/USD") ``` Live integration adapters expose `InstrumentProvider` objects that cache instrument definitions. Use `InstrumentProviderConfig(load_all=True)` where the integration supports it, or `load_ids` to load a known set of instruments. Order submission requires the matching instrument definition to exist in the central cache. ## Finding instruments Strategies and actors retrieve instruments from the central cache: ```rust tab="Rust" use nautilus_model::identifiers::InstrumentId; let instrument_id = InstrumentId::from("ETHUSDT-PERP.BINANCE"); let instrument = cache.instrument(&instrument_id); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId instrument_id = InstrumentId.from_str("ETHUSDT-PERP.BINANCE") instrument = self.cache.instrument(instrument_id) ``` It is also possible to subscribe to one instrument or all instruments for a venue: ```python self.subscribe_instrument(instrument_id) self.subscribe_instruments(venue) ``` When the `DataEngine` receives an instrument update, it passes the object to the `on_instrument()` handler. ## Precision For order validation, `price_precision` and `size_precision` set the maximum number of decimal places that the `RiskEngine` accepts. `price_increment` and `size_increment` record the corresponding minimum steps. | Field | Constrains | Example | | ----------------- | ------------------------------------ | ----------------- | | `price_precision` | Order prices, trigger prices, fills. | `2` -> `50000.01` | | `size_precision` | Order quantities and fill sizes. | `5` -> `1.00001` | The price increment precision must match `price_precision`, and the size increment precision must match `size_precision`. For example, `price_precision=2` pairs with `price_increment=Price(0.01, 2)`. Use the instrument factory methods to round values to the configured precision: ```python instrument = self.cache.instrument(instrument_id) price = instrument.make_price(0.90500) quantity = instrument.make_qty(150) ``` These methods round to the corresponding increment precision, which instrument construction requires to match the declared precision. They do not ensure that the result is a multiple of an increment such as `0.25`. :::warning The `RiskEngine` does not round values automatically. If you create a `Price` with 5 decimal places for an instrument that supports 2, the order is denied. Use `instrument.make_price()` and `instrument.make_qty()` to round explicitly. The `RiskEngine` also does not validate increment multiples, so ensure that prices and quantities match the venue steps before submission. ::: ## Limits, margins, and fees Venue and adapter definitions can include optional limits: - `max_quantity` and `min_quantity`. - `max_notional` and `min_notional`. - `max_price` and `min_price`. Margin models use `margin_init` and `margin_maint` to calculate initial and maintenance margin. Maker and taker fee rates apply to commission calculations. Nautilus uses one fee-rate convention across adapters and backtesting: - Positive fee rates represent commissions. - Negative fee rates represent rebates. For deeper accounting behavior, see [Accounting](../accounting.md). ## Metadata The `info` field preserves raw or adapter-specific metadata as a JSON-serializable dictionary. Use it when the venue publishes useful details that do not belong in the unified Nautilus instrument API. ## Related guides - [Data](../data/) covers market data types that reference instruments. - [Orders](../orders/) covers order fields that reference instruments. - [Synthetics](../synthetics.md) covers local formula-derived instruments. - [Python API Reference](/docs/python-api-latest/model/instruments.html) lists Python constructors and members. # Index Instrument Source: https://nautilustrader.io/docs/latest/concepts/instruments/index_instrument/ `IndexInstrument` represents a reference index such as an equity index, volatility index, or benchmark price series. It carries precision and increment metadata so Nautilus can store and route prices consistently, but it is not a directly tradable contract. Examples include `SPX.XCBO`, `VIX.XCBO`, and venue-specific reference indexes. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ---------------- | -------------- | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `currency` | `Currency` | `Currency` | Required | Reference currency for quoted values. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for quantities. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `IndexInstrument` has asset class `Index` and instrument class `Spot`. - It has no limits, margins, fees, contract multiplier, expiry, or settlement currency. - Use option or futures types for tradable derivatives whose underlyings are indexes. :::warning `IndexInstrument` is a reference instrument, not a tradable contract. Do not submit orders against it; trade the corresponding futures or option instrument instead. ::: ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::IndexInstrument, types::{Currency, Price, Quantity}, }; let spx = IndexInstrument::builder() .instrument_id(InstrumentId::from("SPX.XCBO")) .raw_symbol(Symbol::from("SPX")) .currency(Currency::from("USD")) .price_precision(2) .size_precision(0) .price_increment(Price::from("0.01")) .size_increment(Quantity::from("1")) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from nautilus_trader.model import Currency from nautilus_trader.model import IndexInstrument from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol spx = IndexInstrument( instrument_id=InstrumentId.from_str("SPX.XCBO"), raw_symbol=Symbol("SPX"), currency=Currency.from_str("USD"), price_precision=2, size_precision=0, price_increment=Price.from_str("0.01"), size_increment=Quantity.from_str("1"), ts_event=0, ts_init=0, ) ``` ## Adapters The [Interactive Brokers](../../integrations/interactive_brokers.md) adapter creates `IndexInstrument` definitions for reference indexes. ## Related guides - [Option Contract](option_contract.md) covers listed options on index underlyings. - [Futures Contract](futures_contract.md) covers index futures. # Option Contract Source: https://nautilustrader.io/docs/latest/concepts/instruments/option_contract/ `OptionContract` represents a listed put or call option on a non-crypto underlying. It defines the option kind, strike price, activation time, expiration time, currency, multiplier, and lot size. Examples include equity options, index options, and futures options. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `asset_class` | `AssetClass` | `AssetClass` | Required | Asset class of the underlying. | | `exchange` | `Option` | `str \| None` | `None` | Exchange MIC or venue code when known. | | `underlying` | `Ustr` | `str` | Required | Underlying asset, future, or index. | | `option_kind` | `OptionKind` | `OptionKind` | Required | Put or call. | | `strike_price` | `Price` | `Price` | Required | Option strike price. | | `activation_ns` | `UnixNanos` | `int` | Required | Contract activation timestamp. | | `expiration_ns` | `UnixNanos` | `int` | Required | Contract expiration timestamp. | | `currency` | `Currency` | `Currency` | Required | Premium quote and settlement currency. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_precision` | `u8` | `int` | Fixed `0` | Options trade in whole contracts. | | `size_increment` | `Quantity` | `Quantity` | Fixed `1` | Minimum contract size step. | | `multiplier` | `Quantity` | `Quantity` | Required | Contract multiplier. | | `lot_size` | `Quantity` | `Quantity` | Required | Rounded lot or contract lot size. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `1` | Minimum order quantity. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `OptionContract` has instrument class `Option`. - It trades in whole contracts with size precision `0` and size increment `1`. - The option kind and strike price define the payoff shape. - Use `CryptoOption` for options where the underlying and settlement are crypto currencies. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ enums::{AssetClass, OptionKind}, identifiers::{InstrumentId, Symbol}, instruments::OptionContract, types::{Currency, Price, Quantity}, }; use ustr::Ustr; let activation: Timestamp = "2021-09-17T00:00:00Z".parse().unwrap(); let expiration: Timestamp = "2021-12-17T00:00:00Z".parse().unwrap(); let aapl_call = OptionContract::builder() .instrument_id(InstrumentId::from("AAPL211217C00150000.OPRA")) .raw_symbol(Symbol::from("AAPL211217C00150000")) .asset_class(AssetClass::Equity) .exchange(Ustr::from("GMNI")) .underlying(Ustr::from("AAPL")) .option_kind(OptionKind::Call) .strike_price(Price::from("150.00")) .currency(Currency::from("USD")) .activation_ns(UnixNanos::from(activation)) .expiration_ns(UnixNanos::from(expiration)) .price_precision(2) .price_increment(Price::from("0.01")) .multiplier(Quantity::from("100")) .lot_size(Quantity::from("1")) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" import pandas as pd from nautilus_trader.model import AssetClass from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import OptionContract from nautilus_trader.model import OptionKind from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol aapl_call = OptionContract( instrument_id=InstrumentId.from_str("AAPL211217C00150000.OPRA"), raw_symbol=Symbol("AAPL211217C00150000"), asset_class=AssetClass.EQUITY, underlying="AAPL", option_kind=OptionKind.CALL, strike_price=Price.from_str("150.00"), currency=Currency.from_str("USD"), activation_ns=pd.Timestamp("2021-09-17", tz="UTC").value, expiration_ns=pd.Timestamp("2021-12-17", tz="UTC").value, price_precision=2, price_increment=Price.from_str("0.01"), multiplier=Quantity.from_int(100), lot_size=Quantity.from_int(1), ts_event=0, ts_init=0, exchange="GMNI", ) ``` ## Adapters Representative adapters that create or consume `OptionContract` instruments include: - [Databento](../../integrations/databento.md) for listed options data. - [Interactive Brokers](../../integrations/interactive_brokers.md) for listed option contracts. ## Related guides - [Options](../options.md) covers option data, Greeks, and chain subscriptions. - [Crypto Option](crypto_option.md) covers crypto option contracts. # Option Spread Source: https://nautilustrader.io/docs/latest/concepts/instruments/option_spread/ `OptionSpread` represents an exchange-defined options strategy with more than one leg. The venue publishes the strategy as a single instrument with its own symbol, tick size, expiration, and execution rules. Examples include listed vertical spreads, calendar spreads, and other option strategies. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `asset_class` | `AssetClass` | `AssetClass` | Required | Asset class of the underlying strategy. | | `exchange` | `Option` | `str \| None` | `None` | Exchange MIC or venue code when known. | | `underlying` | `Ustr` | `str` | Required | Underlying asset, future, or index. | | `strategy_type` | `Ustr` | `str` | Required | Venue strategy type, such as vertical. | | `activation_ns` | `UnixNanos` | `int` | Required | Strategy activation timestamp. | | `expiration_ns` | `UnixNanos` | `int` | Required | Strategy expiration timestamp. | | `currency` | `Currency` | `Currency` | Required | Premium quote and settlement currency. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_precision` | `u8` | `int` | Fixed `0` | Option spreads trade in whole contracts. | | `size_increment` | `Quantity` | `Quantity` | Fixed `1` | Minimum contract size step. | | `multiplier` | `Quantity` | `Quantity` | Required | Strategy multiplier. | | `lot_size` | `Quantity` | `Quantity` | Required | Rounded lot or contract lot size. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `1` | Minimum order quantity. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `OptionSpread` has instrument class `OptionSpread`. - The venue publishes the spread as a single tradable instrument. - It trades in whole contracts with size precision `0` and size increment `1`. - Spread prices can be zero or negative, and the `RiskEngine` accepts non-positive prices for this instrument class. - Store venue-specific leg details in `info` when the adapter provides them. ## Example ```rust tab="Rust" use jiff::Timestamp; use nautilus_core::UnixNanos; use nautilus_model::{ enums::AssetClass, identifiers::{InstrumentId, Symbol}, instruments::OptionSpread, types::{Currency, Price, Quantity}, }; use ustr::Ustr; let activation: Timestamp = "2023-11-06T20:54:07Z".parse().unwrap(); let expiration: Timestamp = "2024-02-23T22:59:00Z".parse().unwrap(); let sr3_spread = OptionSpread::builder() .instrument_id(InstrumentId::from("UD:U$: GN 2534559.GLBX")) .raw_symbol(Symbol::from("UD:U$: GN 2534559")) .asset_class(AssetClass::FX) .exchange(Ustr::from("XCME")) .underlying(Ustr::from("SR3")) .strategy_type(Ustr::from("GN")) .activation_ns(UnixNanos::from(activation)) .expiration_ns(UnixNanos::from(expiration)) .currency(Currency::from("USD")) .price_precision(2) .price_increment(Price::from("0.01")) .multiplier(Quantity::from("1")) .lot_size(Quantity::from("1")) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" import pandas as pd from nautilus_trader.model import AssetClass from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import OptionSpread from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol sr3_spread = OptionSpread( instrument_id=InstrumentId.from_str("UD:U$: GN 2534559.GLBX"), raw_symbol=Symbol("UD:U$: GN 2534559"), asset_class=AssetClass.FX, underlying="SR3", strategy_type="GN", activation_ns=pd.Timestamp("2023-11-06T20:54:07", tz="UTC").value, expiration_ns=pd.Timestamp("2024-02-23T22:59:00", tz="UTC").value, currency=Currency.from_str("USD"), price_precision=2, price_increment=Price.from_str("0.01"), multiplier=Quantity.from_int(1), lot_size=Quantity.from_int(1), ts_event=0, ts_init=0, exchange="XCME", ) ``` ## Adapters Representative adapters that create or consume `OptionSpread` instruments include: - [Databento](../../integrations/databento.md) for listed option spread markets. - [Interactive Brokers](../../integrations/interactive_brokers.md) for exchange-defined option strategies. ## Related guides - [Option Contract](option_contract.md) covers single-leg option contracts. - [Options](../options.md) covers option data, Greeks, and chain subscriptions. # Perpetual Contract Source: https://nautilustrader.io/docs/latest/concepts/instruments/perpetual_contract/ `PerpetualContract` represents a generic perpetual futures contract across asset classes. Use it when a venue exposes a perpetual swap that is not specifically modeled as `CryptoPerpetual`. Examples include non-crypto perpetual contracts and venue-specific synthetic swaps. ## Fields | Field | Rust type | Python type | Required/default | Notes | | --------------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `underlying` | `Ustr` | `str` | Required | Underlying asset or reference market. | | `asset_class` | `AssetClass` | `AssetClass` | Required | Asset class of the underlying. | | `base_currency` | `Option` | `Currency \| None` | `None` | Base currency, required for inverse. | | `quote_currency` | `Currency` | `Currency` | Required | Currency used to quote the price. | | `settlement_currency` | `Currency` | `Currency` | Required | Currency used to settle PnL and fees. | | `is_inverse` | `bool` | `bool` | Required | True when sizing/costing is inverse. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `multiplier` | `Quantity` | `Quantity` | `1` | Contract multiplier. | | `lot_size` | `Quantity` | `Quantity` | `1` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `PerpetualContract` has instrument class `Swap`. - It has no activation timestamp or expiration timestamp. - Inverse contracts require a base currency. - Linear contracts typically settle in the quote currency. - Use `CryptoPerpetual` for crypto perpetuals where the base asset is a currency. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ enums::AssetClass, identifiers::{InstrumentId, Symbol}, instruments::PerpetualContract, types::{Currency, Price, Quantity}, }; use rust_decimal_macros::dec; use ustr::Ustr; let eurusd_perp = PerpetualContract::builder() .instrument_id(InstrumentId::from("EURUSD-PERP.AX")) .raw_symbol(Symbol::from("EURUSD-PERP")) .underlying(Ustr::from("EURUSD")) .asset_class(AssetClass::FX) .base_currency(Currency::from("EUR")) .quote_currency(Currency::from("USD")) .settlement_currency(Currency::from("USD")) .is_inverse(false) .price_precision(5) .size_precision(0) .price_increment(Price::from("0.00001")) .size_increment(Quantity::from("1")) .margin_init(dec!(0.03)) .margin_maint(dec!(0.03)) .maker_fee(dec!(0.00002)) .taker_fee(dec!(0.00002)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from decimal import Decimal from nautilus_trader.model import AssetClass from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import PerpetualContract from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol eurusd_perp = PerpetualContract( instrument_id=InstrumentId.from_str("EURUSD-PERP.AX"), raw_symbol=Symbol("EURUSD-PERP"), underlying="EURUSD", asset_class=AssetClass.FX, quote_currency=Currency.from_str("USD"), settlement_currency=Currency.from_str("USD"), is_inverse=False, price_precision=5, size_precision=0, price_increment=Price.from_str("0.00001"), size_increment=Quantity.from_int(1), ts_event=0, ts_init=0, base_currency=Currency.from_str("EUR"), margin_init=Decimal("0.03"), margin_maint=Decimal("0.03"), maker_fee=Decimal("0.00002"), taker_fee=Decimal("0.00002"), ) ``` ## Adapters Representative adapters that create or consume `PerpetualContract` instruments include: - [Architect AX](../../integrations/architect_ax.md) for venue-defined perpetual contracts. - [Binance](../../integrations/binance.md) for USD-M TradFi perpetual contracts. ## Related guides - [Crypto Perpetual](crypto_perpetual.md) covers crypto perpetual futures. - [Data](../data/) covers mark prices, index prices, and funding rate updates. # Synthetic Instrument Source: https://nautilustrader.io/docs/latest/concepts/instruments/synthetic_instrument/ `SyntheticInstrument` represents a local instrument whose price comes from a formula over other instruments. It is useful for spreads, baskets, ratios, and other derived prices that should appear in the system as an instrument. Examples include `(BTC.BINANCE + LTC.BINANCE) / 2.0` and ratio-style pairs built from component instrument prices. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------- | -------------------- | ---------------- | ------------------------------------------- | | `symbol` | `Symbol` | `Symbol` | Required | Synthetic symbol used with venue `SYNTH`. | | `id` | `InstrumentId` | `InstrumentId` | Derived | Instrument ID formed from `symbol.SYNTH`. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for synthetic price. | | `price_increment` | `Price` | `Price` | Derived | Smallest price step from precision. | | `components` | `Vec` | `list[InstrumentId]` | Required | Component instruments used by the formula. | | `formula` | `String` | `str` | Required | Numeric expression over component IDs. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | *Note: Python constructs the instrument ID from `symbol` and the `SYNTH` venue. Rust stores the same value as `id`.* ## Behavior - `SyntheticInstrument` is local to Nautilus and does not represent a venue orderable market. - It always uses the synthetic venue `SYNTH`. - The formula must compile against the supplied component identifiers before the object is valid. - It has no venue limits, margins, fees, order book, or adapter-specific metadata. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ identifiers::{InstrumentId, Symbol}, instruments::SyntheticInstrument, }; let synthetic = SyntheticInstrument::builder() .symbol(Symbol::from("BTC-LTC")) .price_precision(2) .components(vec![ InstrumentId::from("BTC.BINANCE"), InstrumentId::from("LTC.BINANCE"), ]) .formula("(BTC.BINANCE + LTC.BINANCE) / 2.0") .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import Symbol from nautilus_trader.model import SyntheticInstrument synthetic = SyntheticInstrument( symbol=Symbol("BTC-LTC"), price_precision=2, components=[ InstrumentId.from_str("BTC.BINANCE"), InstrumentId.from_str("LTC.BINANCE"), ], formula="(BTC.BINANCE + LTC.BINANCE) / 2.0", ts_event=0, ts_init=0, ) ``` ## Adapters `SyntheticInstrument` is local only. It derives prices from component instruments that may come from any adapter already loaded into the system. ## Related guides - [Synthetics](../synthetics.md) covers formula-derived instruments and synthetic bars. - [Data](../data/) explains market data that references instruments. # Tokenized Asset Source: https://nautilustrader.io/docs/latest/concepts/instruments/tokenized_asset/ `TokenizedAsset` represents a spot-like token that tracks another asset on a crypto venue. Use it for tokenized equities, tokenized funds, or similar instruments where the trading venue exposes a token but the economic reference is an external asset. Examples include tokenized stock or ETF symbols on crypto venues. ## Fields | Field | Rust type | Python type | Required/default | Notes | | ----------------- | ------------------ | ------------------ | ---------------- | ---------------------------------------- | | `instrument_id` | `InstrumentId` | `InstrumentId` | Required | Stored as `id` in Rust. | | `raw_symbol` | `Symbol` | `Symbol` | Required | Native venue symbol. | | `asset_class` | `AssetClass` | `AssetClass` | Required | Economic asset classification. | | `base_currency` | `Currency` | `Currency` | Required | Tokenized asset or base token. | | `quote_currency` | `Currency` | `Currency` | Required | Currency used to price the token. | | `price_precision` | `u8` | `int` | Required | Decimal places allowed for prices. | | `size_precision` | `u8` | `int` | Required | Decimal places allowed for order sizes. | | `price_increment` | `Price` | `Price` | Required | Smallest valid price step. | | `size_increment` | `Quantity` | `Quantity` | Required | Smallest valid size step. | | `ts_event` | `UnixNanos` | `int` | Required | Event timestamp in nanoseconds. | | `ts_init` | `UnixNanos` | `int` | Required | Initialization timestamp in nanoseconds. | | `isin` | `Option` | `str \| None` | `None` | International Securities ID when known. | | `multiplier` | `Quantity` | `Quantity` | `1` | Contract multiplier. | | `lot_size` | `Option` | `Quantity \| None` | `None` | Rounded lot or board size. | | `max_quantity` | `Option` | `Quantity \| None` | `None` | Maximum order quantity. | | `min_quantity` | `Option` | `Quantity \| None` | `None` | Minimum order quantity. | | `max_notional` | `Option` | `Money \| None` | `None` | Maximum order notional value. | | `min_notional` | `Option` | `Money \| None` | `None` | Minimum order notional value. | | `max_price` | `Option` | `Price \| None` | `None` | Maximum valid quote or order price. | | `min_price` | `Option` | `Price \| None` | `None` | Minimum valid quote or order price. | | `margin_init` | `Option` | `Decimal \| None` | `0` | Initial margin rate. | | `margin_maint` | `Option` | `Decimal \| None` | `0` | Maintenance margin rate. | | `maker_fee` | `Option` | `Decimal \| None` | `0` | Maker fee rate. Negative values rebate. | | `taker_fee` | `Option` | `Decimal \| None` | `0` | Taker fee rate. Negative values rebate. | | `tick_scheme` | `Option` | `str \| None` | `None` | Registered variable tick scheme name. | | `info` | `Option` | `dict \| None` | `None` | Adapter metadata. | *Note: Python constructors use `instrument_id`; Rust stores the same value as `id`.* ## Behavior - `TokenizedAsset` has instrument class `Spot`. - It is never inverse, and its cost currency is the quote currency. - It can carry an `isin` when the token references a listed security. - It has no activation timestamp, expiry, strike, or option kind. ## Example ```rust tab="Rust" use nautilus_core::UnixNanos; use nautilus_model::{ enums::AssetClass, identifiers::{InstrumentId, Symbol}, instruments::TokenizedAsset, types::{Currency, Price, Quantity}, }; use rust_decimal_macros::dec; let aaplx = TokenizedAsset::builder() .instrument_id(InstrumentId::from("AAPLx/USD.KRAKEN")) .raw_symbol(Symbol::from("AAPLxUSD")) .asset_class(AssetClass::Equity) .base_currency(Currency::get_or_create_crypto("AAPLx")) .quote_currency(Currency::from("USD")) .price_precision(2) .size_precision(4) .price_increment(Price::from("0.01")) .size_increment(Quantity::from("0.0001")) .min_quantity(Quantity::from("0.0001")) .maker_fee(dec!(-0.0002)) .taker_fee(dec!(0.001)) .ts_event(UnixNanos::default()) .ts_init(UnixNanos::default()) .build() .unwrap(); ``` ```python tab="Python" from decimal import Decimal from nautilus_trader.model import AssetClass from nautilus_trader.model import Currency from nautilus_trader.model import InstrumentId from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import Symbol from nautilus_trader.model import TokenizedAsset aaplx = TokenizedAsset( instrument_id=InstrumentId.from_str("AAPLx/USD.KRAKEN"), raw_symbol=Symbol("AAPLxUSD"), asset_class=AssetClass.EQUITY, base_currency=Currency.from_str("AAPLx"), quote_currency=Currency.from_str("USD"), price_precision=2, size_precision=4, price_increment=Price.from_str("0.01"), size_increment=Quantity.from_str("0.0001"), ts_event=0, ts_init=0, min_quantity=Quantity.from_str("0.0001"), maker_fee=Decimal("-0.0002"), taker_fee=Decimal("0.001"), ) ``` ## Adapters Representative adapters that create or consume `TokenizedAsset` instruments include: - [Kraken](../../integrations/kraken.md) for tokenized assets where the venue exposes them. ## Related guides - [Currency Pair](currency_pair.md) covers ordinary crypto spot pairs. - [Equity](equity.md) covers listed cash equities. # Advanced orders Source: https://nautilustrader.io/docs/latest/concepts/orders/advanced/ Order lists group related orders, while contingency metadata describes how fills, cancellations, or updates should affect linked orders. The component that handles the list determines the behavior: the backtest matching engine, local order emulator, live adapter and venue, or strategy code. :::warning An `OrderList` or `ContingencyType` does not guarantee that every live adapter or venue implements the relationship. Check the target integration before relying on native contingency behavior. ::: ## Order lists An order list groups contingent orders or a larger batch under one `order_list_id`. Orders in the list do not need a contingency relationship; their own metadata defines any relationship. Production constructors require every order in a list to use the same venue. Orders may target different instruments at that venue, such as pairs, calendar spreads, or multi-leg strategies. The list takes its representative `instrument_id` from the first order; consumers that need the actual instrument must resolve each order individually. Caveats for mixed-instrument lists: - **Pre-trade checks**: Price precision, quantity precision, and GTD expiry use each order's own instrument. - **Cumulative risk check**: Free balance, notional bounds, position-reducing exposure, and market data use the list's representative instrument. For a mixed list, this produces a single-instrument bound rather than per-instrument accuracy. - **Cache lookups**: `cache.order_lists(instrument_id=...)` filters against the representative `instrument_id`; lists containing other instruments will not match queries for those other instruments. - **Position IDs**: The execution engine denies mixed-instrument lists when a `position_id` is supplied (a position belongs to a single instrument, regardless of OMS). - **Adapter batching**: `submit_order_list` implementations vary. Some iterate orders per leg and resolve each order's own `instrument_id` against the venue API; others still build the batch request around the list's representative `instrument_id` and will misroute non-first orders. Treat mixed-instrument lists as adapter-specific; verify the target adapter's behavior before relying on it. Backtesting and strategy-managed routing avoid relying on an adapter's mixed-instrument batch behavior. ## Contingency types - **OTO (One-Triggers-Other)**: A parent order releases one or more child orders after a configured fill condition. - **OCO (One-Cancels-Other)**: A fill in one linked order requests cancellation of the others. - **OUO (One-Updates-Other)**: A fill in one linked order requests a quantity update for the others. :::info These types correspond to FIX [`ContingencyType <1385>`](https://www.onixs.biz/fix-dictionary/5.0.sp2/tagnum_1385.html). ::: ### Strategy-managed contingencies Enable `StrategyConfig.manage_contingent_orders` to manage open OTO, OCO, and OUO relationships for orders that are not active local. The strategy sends the resulting cancel and quantity-update commands through the normal execution path before it calls the specific and aggregate user order-event handlers. The `OrderEmulator` always owns active-local orders. Enabling strategy management therefore does not make the strategy and emulator manage the same order. The option does not add native venue support or submit a non-active-local OTO child: it manages non-active-local orders that are already open. ### One-Triggers-Other (OTO) An OTO relationship has two parts: 1. The parent order enters its execution path. 1. One or more child orders reference the parent and wait for the configured release condition. The handler determines where the children wait. The backtest engine can hold them locally, while a live adapter may send native venue instructions, submit all legs, reject the list, or require the strategy to manage the relationship. #### Child sizing Before the parent's first fill, strategy management propagates parent quantity updates to open, non-active-local OTO children. After filling starts, each parent event starts the child target at the parent's cumulative filled quantity. For an execution spawn, this quantity includes fills from every order in the spawn. For a parent linked to a position, the manager then adjusts the target in order: 1. For a non-spread parent with a reduce-only child, cap the total target at the child's filled quantity plus the current [commission-adjusted](../positions.md#base-currency-commissions) position quantity. This keeps the child's remaining quantity within the open position. 1. Round the total target down to a multiple of the child instrument's size increment. 1. When configured, treat a rounded target below the child instrument's minimum quantity as zero. The calculation does not round position or account state. A remaining position too small to meet the child instrument's size increment and optional minimum quantity stays open without reduce-only child coverage. Spread parents skip the position cap because the execution engine does not create positions for them. Non-reduce-only children also skip the cap. Both still use the child instrument's size rules. #### Required sizing state When a fill event, cached parent, or filled execution-spawn sibling identifies a position, sizing requires: - The parent and child instruments in the cache. - A positive size increment for the child instrument. - The linked position in the cache for a non-spread parent with a reduce-only child. - Matching fill-event and cached position IDs when both are present. A fill-event position ID that conflicts with cached ownership stops processing for that parent event. Other missing sizing state leaves the affected child unchanged, and processing continues with the remaining linked children. #### Child lifecycle Parent events apply the validated target according to the child and parent state: | Condition | Action | | -------------------------------------------------------- | ---------------------------------------------------------- | | Managed child has a different positive target | Update its total quantity. | | Target is zero; parent or execution spawn remains active | Keep the child unchanged and wait for executable quantity. | | Target is zero; parent or execution spawn closes | Cancel the child. | | Child fills meet or exceed the positive target | Cancel any remaining quantity. | | Active-local child reaches an executable positive target | The active-local emulator submits it once. | A child fill or update does not recalculate the target immediately. The next parent event refreshes it. #### Trigger models | Trigger model | Backtest release condition | | ------------- | ------------------------------------------------------------------------- | | **Partial** | Release children after the parent's first partial fill. | | **Full** | Release children after the parent's cumulative fill reaches its quantity. | :::info The default `BacktestVenueConfig` mode is `OtoTriggerMode.PARTIAL`. Set `oto_trigger_mode` to `OtoTriggerMode.FULL` to wait for a complete fill. This setting controls release timing; it does not promise pro rata child sizing. Verify child quantities when the parent fills partially. ::: #### Enforcing a full-fill trigger in strategy code If the execution context does not provide the required full-fill behavior: 1. Submit the parent order without contingent children. 1. Handle `OrderFilled` events for the parent. 1. Confirm the parent has reached `FILLED` status. 1. Submit the stop-loss, take-profit, or other child orders. :::warning Full-fill release leaves a partially filled position without its contingent exits until the parent finishes. Partial release reduces that delay, but the current backtest mode does not guarantee that child quantities track each partial fill. Check quantities and adapter behavior before treating a child as complete protection. ::: ### One-Cancels-Other (OCO) In backtest local matching, a full or partial fill in one OCO order causes a best-effort request to cancel its open siblings. The local order manager applies this behavior only while a sibling remains active local. With strategy management enabled, the strategy requests cancellation for open, non-active-local siblings. Otherwise, the adapter or venue determines cancellation behavior. Another sibling can fill before cancellation completes. ### One-Updates-Other (OUO) #### Updates after a sibling fill In backtest local matching, a fill in one OUO order uses that order's remaining quantity as the target for each open sibling: - If the target is zero or the sibling's filled quantity already meets the target, cancel the sibling. - Otherwise, update the sibling's quantity when needed. This behavior suits equal-sized peers and does not preserve a ratio between unequal starting quantities. With strategy management enabled, the strategy applies the same update or cancellation behavior to open, non-active-local siblings. Otherwise, live behavior depends on adapter and venue support. #### Backtest reduce-only resizing With reduce-only enforcement enabled, a fill can resize resting reduce-only orders to the available position quantity, subject to parent caps. When contingent-order support is also enabled, a resized OUO order propagates its remaining quantity to siblings that are: - Open and not active local. - Passive orders resting on the same instrument's book. Siblings do not need to be `reduce_only`. Each sibling's quantity update follows these rules: - Add the sibling's prior fills to the propagated remaining quantity to obtain its total quantity. - Apply the sibling's own cached parent's filled-quantity cap, when available. - Never reduce the total below the sibling's prior fills. The order already being filled retains its active fill loop's quantity rules. This propagation does not trigger matching itself. #### Backtest cancellation at zero capacity With reduce-only enforcement and contingent-order support enabled: - When the reduce-only order has no remaining capacity, cancel it and its eligible siblings without resizing the siblings. This also covers siblings whose acceptance event is still awaiting delivery. - When a sibling exhausts only its own parent allowance, resize it to its filled quantity, then cancel it. ## Constructing contingent orders Use `OrderFactory.bracket` to construct a bracket's contingency metadata. In Rust, `self.order().create_list(...)` assigns a fresh `order_list_id` to an existing group of orders. Python code instead passes a plain list to `self.submit_order_list(...)`, which creates an `OrderList` when needed. These grouping paths do not create parent or linked-order relationships. The current model enforces only part of the remaining consistency: - A contingent order must have at least one `linked_order_id`. - A child identifies its parent through `parent_order_id`. - Rust `create_list` requires a non-empty list whose orders use one venue. - `OrderList.validate` checks for non-empty, unique client order IDs when a strategy submits the list. - `OrderList.validate` does not verify shared `order_list_id` values, parent references, or other cross-field relationships. Modification, cancellation, and rejection behavior depends on the component managing the contingency. Do not assume a parent update or cancellation cascades in every live integration. :::warning Handle `OrderDenied` and `OrderRejected` events for every leg. Adapter or venue failures can affect legs independently and leave a position without its intended protection. ::: ## Bracket orders Bracket orders combine an entry with take-profit and stop-loss children. By default, `OrderFactory.bracket` creates a `MARKET` entry, a `LIMIT` take-profit, and a `STOP_MARKET` stop-loss. It marks the entry with an `OTO` contingency, marks both exits `reduce_only`, and links the exits with an `OUO` contingency. The default `LIMIT` take-profit is also `post_only`. The factory creates the orders and their relationship metadata. The execution context determines whether children wait locally, use a native venue instruction, enter the venue with the parent, or require manual strategy handling. Create brackets with [`OrderFactory`](/docs/python-api-latest/common.html#nautilus_trader.common.OrderFactory), which also supports different entry and exit types, trigger settings, and execution instructions. In the following example we bracket a *Market* entry to BUY 10 ETHUSDT-PERP contracts with a take-profit *Limit* at 3,300 USDT and a stop-loss *Stop-Market* triggering at 2,800 USDT. The entry defaults to `MARKET`, the take-profit to `LIMIT`, and the stop-loss to `STOP_MARKET`; the take-profit and stop-loss legs are `reduce_only` and linked with the `OUO` contingency: ```rust tab="Rust" use nautilus_model::{ enums::OrderSide, identifiers::InstrumentId, types::{Price, Quantity}, }; // `bracket()` returns a `bon` builder; finalize with `.call()`. // The result is a `Vec` ordered as [entry, stop-loss, take-profit]. let orders = self .order() .bracket() .instrument_id(InstrumentId::from("ETHUSDT-PERP.BINANCE")) .order_side(OrderSide::Buy) .quantity(Quantity::from(10)) .tp_price(Price::from("3300.00")) // take-profit LIMIT (default) .sl_trigger_price(Price::from("2800.00")) // stop-loss STOP_MARKET (default) .call(); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import OrderSide from nautilus_trader.model import Price from nautilus_trader.model import Quantity orders = self.order_factory.bracket( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), order_side=OrderSide.BUY, quantity=Quantity.from_int(10), tp_price=Price.from_str("3300.00"), # <-- take-profit LIMIT (default) sl_trigger_price=Price.from_str("2800.00"), # <-- stop-loss STOP_MARKET (default) ) ``` :::warning Some venues reserve margin for bracket legs. Check the venue's margin rules and handle a child rejection after the entry fills. ::: ## Related guides - [Orders](index.md) - Order concepts, execution instructions, and the order factory. - [Emulated orders](emulated.md) - Emulating order types on venues without native support. - [Execution](../execution/) - Order execution and fill handling. # Emulated orders Source: https://nautilustrader.io/docs/latest/concepts/orders/emulated/ Emulation lets you use order types even when your trading venue does not natively support them. The `OrderEmulator` monitors the market data selected by `emulation_trigger`. When the local order matches its release condition, the emulator transforms it into a `MARKET` or `LIMIT` order and sends that order through the normal risk and execution path. For example, an emulated `STOP_LIMIT` becomes a `LIMIT` order after its stop price triggers. ## Submitting an order for emulation Set `emulation_trigger` on an order constructor or `OrderFactory` method. The local emulator accepts these values: | Trigger type | Market data used | | ------------ | -------------------------------------------------- | | `DEFAULT` | Quotes, with the same local behavior as `BID_ASK`. | | `BID_ASK` | Best bid and ask quotes. | | `LAST_PRICE` | Trades. | Leave `emulation_trigger` as `None` to disable local emulation and submit through the normal pathway. Other `TriggerType` values describe trigger methods that some venues support, but the local `OrderEmulator` does not accept them as `emulation_trigger` values. :::warning The emulator cancels an order submitted with any other `emulation_trigger` value, and logs the unsupported trigger type as an error. ::: The choice of trigger type determines how emulation behaves: - For stop orders, the emulator compares the trigger price with the selected market data. - For trailing-stop orders, it updates the trailing trigger from that market data. - For emulated `LIMIT` orders, it compares the limit price with that market data and releases a `MARKET` order when matched. ## Technical details The same `OrderEmulator` component manages supported emulated order types in all [environment contexts](../architecture.md#environment-contexts). :::note NautilusTrader does not configure a fixed count limit for emulated orders. Available memory and the cost of market data processing provide practical limits. ::: ## Lifecycle An emulated order progresses through these stages: 1. A `Strategy` submits it through `submit_order`. 1. The `RiskEngine` applies pre-trade checks and may deny it. 1. The `OrderEmulator` holds and monitors it locally. 1. A matching market update transforms it into a `MARKET` or `LIMIT` order and releases it. 1. The `RiskEngine` checks the released order again before venue submission. :::note Emulated orders pass through the normal risk controls. A strategy can modify or cancel them, and a cancel-all request includes them. ::: :::info An emulated order retains its client order ID when transformed, so cache queries continue to use the same ID. ::: ### Held emulated orders While the `OrderEmulator` holds an order: - It caches the original `SubmitOrder` command. - It processes the order in a local matching core. - It subscribes to the required quotes or trades if no matching subscription exists. - It accepts strategy modifications and market-driven updates until release or cancellation. ### Released emulated orders When market data matches an emulated order, release performs these actions: - It transforms the order into a `MARKET` or `LIMIT` order through another `OrderInitialized` event. - It sets the order's `emulation_trigger` to `None` so components no longer treat it as emulated. - It sends the transformed order and original `SubmitOrder` command back through the `RiskEngine`. - If the risk engine does not deny it, the `ExecutionEngine` routes it to an `ExecutionClient`. ## Order types that can be emulated The released type depends on the original emulated order type: | Order type for emulation | Can emulate | Released type | | ------------------------ | ----------- | ------------- | | `MARKET` | - | N/A | | `MARKET_TO_LIMIT` | - | N/A | | `LIMIT` | ✓ | `MARKET` | | `STOP_MARKET` | ✓ | `MARKET` | | `STOP_LIMIT` | ✓ | `LIMIT` | | `MARKET_IF_TOUCHED` | ✓ | `MARKET` | | `LIMIT_IF_TOUCHED` | ✓ | `LIMIT` | | `TRAILING_STOP_MARKET` | ✓ | `MARKET` | | `TRAILING_STOP_LIMIT` | ✓ | `LIMIT` | ## Querying Use the cache or the order object to query emulation status. ### Through the cache The `Cache` provides these methods: - `self.cache.orders_emulated(...)` returns all emulated orders that match its filters. - `self.cache.is_order_emulated(...)` checks one client order ID. - `self.cache.orders_emulated_count(...)` returns the number of matching emulated orders. See the full [API reference](/docs/python-api-latest/cache.html) for additional details. ### Direct order queries Use `order.is_emulated` to query an order object directly. A `False` value means the order was released or was never emulated. :::warning Do not hold a local reference to an emulated order. The order object transforms when the emulated order is *released*. Use the `Cache` instead. ::: ## Persistence and recovery On startup, the `OrderEmulator` reactivates emulated orders that the configured cache database restored into the cache. This preserves their state across restarts. ## Best practices When working with emulated orders: 1. Query the `Cache` instead of storing local order references. 1. Account for the order type changing on release. 1. Handle a denial at either the initial or release-time risk check. ## Related guides - [Orders](index.md) - Order concepts, execution instructions, and the order factory. - [Advanced orders](advanced.md) - Order lists, contingency types, and bracket orders. - [Strategies](../strategies.md) - Order management from strategies. # Orders Source: https://nautilustrader.io/docs/latest/concepts/orders/ NautilusTrader provides a common model for order types, execution instructions, and contingency relationships across trading venues. ## Overview All order types derive from two fundamentals: *Market* and *Limit* orders. *Market* orders seek immediate execution at the best available price. Non-marketable *Limit* orders rest in the order book at a specified price until matched, while marketable *Limit* orders can take liquidity. NautilusTrader supports nine order types (the `OrderType` enum values), summarized under [Order types](#order-types) with a dedicated guide for each. :::info NautilusTrader provides a unified API, but order and instruction support varies by venue and adapter. An adapter may deny an unsupported request before submission, or the venue may reject it. Check the target integration's capabilities before relying on an option. ::: ### Terminology - An order is **aggressive** if its type is `MARKET` or it executes as a marketable order and takes liquidity. - An order is **passive** if it rests without taking liquidity. - An order is **active local** if it remains within the local system boundary in one of these non-terminal statuses: - `INITIALIZED` - `EMULATED` - `RELEASED` - An order is **in-flight** when at one of the following statuses: - `SUBMITTED` - `PENDING_UPDATE` - `PENDING_CANCEL` - An order is **open** when at one of the following (non-terminal) statuses: - `ACCEPTED` - `TRIGGERED` - `PENDING_UPDATE` - `PENDING_CANCEL` - `PARTIALLY_FILLED` - An order is **closed** when at one of the following (terminal) statuses: - `DENIED` - `REJECTED` - `CANCELED` - `EXPIRED` - `FILLED` - `VOIDED` These groups overlap, so open and closed are not opposites. `PENDING_UPDATE` and `PENDING_CANCEL` are both open and in-flight: the order is working at the venue while a modify or cancel request is outstanding. Four statuses are neither open nor closed: `INITIALIZED`, `EMULATED`, and `RELEASED` are active local, and `SUBMITTED` is in-flight until the venue acknowledges the order. :::warning[Open and closed are not complements] Test for a finished order with `is_closed`, never by negating `is_open`. An order at one of the four statuses above is not open, but it is not finished either. Every order is `SUBMITTED` immediately after submission, so code which treats "not open" as done abandons orders the venue is still processing. Use `is_inflight` for the awaiting-venue case. ::: ### Order state flow The following diagram illustrates the order lifecycle and primary state transitions. Each status appears once, so `PENDING_UPDATE` and `PENDING_CANCEL` are drawn under In-Flight although they are also open: ```mermaid flowchart TB subgraph local ["Active Local"] Initialized Emulated Released end subgraph flight ["In-Flight"] Submitted PendingUpdate PendingCancel end subgraph open ["Open (on venue)"] Accepted Triggered PartiallyFilled end subgraph closed ["Closed (terminal)"] Denied Rejected Canceled Expired Filled Voided end Initialized -->|"Emulation trigger"| Emulated Initialized -->|"Submit"| Submitted Initialized -->|"System denied"| Denied Emulated -->|"Triggered locally"| Released Released --> Submitted Submitted -->|"Venue ACK"| Accepted Submitted --> Rejected Accepted -->|"Stop hit"| Triggered Accepted --> PartiallyFilled Triggered --> PartiallyFilled PartiallyFilled -->|"More fills"| PartiallyFilled Accepted --> PendingUpdate Accepted --> PendingCancel PartiallyFilled --> PendingUpdate PartiallyFilled --> PendingCancel PendingUpdate --> Accepted PendingCancel --> Canceled Accepted --> Filled Triggered --> Filled PartiallyFilled --> Filled Filled -->|"Fill correction"| Voided Filled -->|"Explicit reopened correction"| Accepted Filled -->|"Reopened correction with surviving fill"| PartiallyFilled PartiallyFilled --> Canceled Accepted --> Expired ``` The diagram shows the primary transitions, while the order model validates the complete transition set for recovery and venue edge cases. An order status describes local state, not the evidence that produced it. See [Execution policies](../execution/policies.md) for command outcome classes, event provenance, delivery limits, and reconciliation policy. ### Order status definitions | Status | Description | | ------------------ | ---------------------------------------------------------------------------------------------- | | `INITIALIZED` | Order is instantiated within the Nautilus system. | | `DENIED` | Order was denied by Nautilus for being invalid, unprocessable, or exceeding a risk limit. | | `EMULATED` | Order is being emulated by the `OrderEmulator` component. | | `RELEASED` | Order was released from the `OrderEmulator` component. | | `SUBMITTED` | Order was submitted to the venue (awaiting acknowledgement). | | `ACCEPTED` | Order was acknowledged by the venue as received and valid (may now be working). | | `REJECTED` | Order is terminal as rejected; `reconciliation` and `reason` provide the available provenance. | | `CANCELED` | Order is terminal as canceled; status alone does not identify venue, local, or policy cause. | | `EXPIRED` | Order reached its GTD expiration (terminal). | | `TRIGGERED` | A stop-limit, trailing-stop-limit, or limit-if-touched order triggered on the venue. | | `PENDING_UPDATE` | Order is pending a modification request on the venue. | | `PENDING_CANCEL` | Order is pending a cancellation request on the venue. | | `PARTIALLY_FILLED` | Order has been partially filled on the venue. | | `FILLED` | Order has been completely filled (terminal). | | `VOIDED` | Order is terminal after an authoritative fill correction. | ## Execution instructions Execution instructions specify conditions and restrictions on how a venue processes an order. Support varies by venue and adapter. ### Time in force Time in force specifies how long an order remains active before any unfilled quantity is canceled. - `GTC` (Good Till Cancel): The order remains active until canceled by the trader or the venue. - `IOC` (Immediate or Cancel / Fill and Kill): The order executes immediately, with any unfilled portion canceled. - `FOK` (Fill or Kill): The order executes immediately in full or not at all. - `GTD` (Good Till Date): The order remains active until a specified expiration date and time. - `DAY` (Good for session/day): The order remains active until the end of the current trading session. - `AT_THE_OPEN` (OPG): The order is only active at the open of the trading session. - `AT_THE_CLOSE`: The order is only active at the close of the trading session. ### Expire time Use `expire_time` with `GTD` to specify when the order expires and leaves the venue's order book or order management system. ### Post-only An order marked `post_only` may provide liquidity but must not take it. A venue normally rejects or cancels the order if it would execute immediately. Market makers can use this instruction to target maker fees. ### Reduce-only An order marked `reduce_only` may reduce an existing position but must not increase exposure or open a position while flat. Exact behavior varies by venue. The Nautilus `SimulatedExchange` applies these rules: - It cancels the order when the associated position becomes flat. - It reduces the order quantity as the associated position shrinks. ### Display quantity The `display_qty` specifies how much of an order is visible on the limit order book. An order with a smaller displayed quantity than its total quantity is commonly called an iceberg order. A display quantity of zero makes the order hidden when the venue supports that behavior. ### Trigger type The trigger type, also known as a [trigger method](https://www.interactivebrokers.com/en/software/tws/usersguidebook/configuretws/Modify%20the%20Stop%20Trigger%20Method.htm), specifies the market price used to trigger a conditional order. An absent trigger type is represented by `None` and is invalid for an order that requires one. - `DEFAULT`: Uses the venue's default trigger type. - `LAST_PRICE`: Uses the last traded price. - `BID_ASK`: Uses the ask for BUY orders and the bid for SELL orders. - `DOUBLE_LAST`: Requires two consecutive matching last prices. - `DOUBLE_BID_ASK`: Requires two consecutive matching bid or ask prices, based on the order side. - `LAST_OR_BID_ASK`: Uses either the last price or the side-appropriate bid or ask. - `MID_POINT`: Uses the midpoint between the bid and ask. - `MARK_PRICE`: Uses the venue's mark price for the instrument. - `INDEX_PRICE`: Uses the venue's index price for the instrument. ### Trailing offset type The trailing offset type specifies how a trailing order calculates its trigger offset from the applicable market price. An absent trailing offset type is represented by `None` and is invalid for a trailing order. - `PRICE`: Uses a price difference. - `BASIS_POINTS`: Uses a percentage difference in basis points, where 100 basis points equals 1%. - `TICKS`: Uses a number of ticks. - `PRICE_TIER`: Uses a venue-specific price tier. ### Contingent orders Contingency relationships can hold child orders until a parent activates or fills, cancel linked orders, or reduce their quantities. See [Advanced orders](advanced.md) for the available models and their constraints. ## Order factory Use the built-in `OrderFactory` to create orders. Each Python `Strategy` exposes one as `self.order_factory`; the Rust strategy API exposes it through `self.order()`. The factory assigns the trader and strategy IDs, generates client order and initialization IDs when needed, records the initial timestamp, and applies defaults for the selected order type. The examples in these guides create orders from a `Strategy` context. See the [`OrderFactory` API reference](/docs/python-api-latest/common.html#nautilus_trader.common.OrderFactory) for further details. ## Order types NautilusTrader supports the following order types. Each links to a dedicated guide with a code example; optional parameters are marked with a comment showing the default value. | Order type | Category | Description | | ------------------------------------------------- | -------------------- | --------------------------------------------------------------------------- | | [`MARKET`](market.md) | Aggressive | Trades the quantity immediately at the best available price. | | [`LIMIT`](limit.md) | Passive | Rests in the book and trades only at the limit price or better. | | [`STOP_MARKET`](stop_market.md) | Conditional | Once the trigger price is hit, places a *Market* order. | | [`STOP_LIMIT`](stop_limit.md) | Conditional | Once the trigger price is hit, places a *Limit* order at the set price. | | [`MARKET_TO_LIMIT`](market_to_limit.md) | Hybrid | Submits as *Market*; any remainder rests as a *Limit* at the fill price. | | [`MARKET_IF_TOUCHED`](market_if_touched.md) | Conditional | Once the trigger price is touched, places a *Market* order. | | [`LIMIT_IF_TOUCHED`](limit_if_touched.md) | Conditional | Once the trigger price is touched, places a *Limit* order at the set price. | | [`TRAILING_STOP_MARKET`](trailing_stop_market.md) | Conditional trailing | Trails the trigger by an offset, then places a *Market* order. | | [`TRAILING_STOP_LIMIT`](trailing_stop_limit.md) | Conditional trailing | Trails the trigger by an offset, then places a *Limit* order. | ### FIX OrdType mapping Each type maps to the nearest FIX 5.0 SP2 [`OrdType <40>`](https://www.onixs.biz/fix-dictionary/5.0.sp2/tagnum_40.html) value, where the protocol defines one: | Order type | FIX `OrdType <40>` | | -------------------- | ------------------------------------ | | Market | `1` (Market) | | Limit | `2` (Limit) | | Stop-Market | `3` (Stop) | | Stop-Limit | `4` (Stop Limit) | | Market-To-Limit | `K` (Market With Left Over as Limit) | | Market-If-Touched | `J` (Market If Touched) | | Limit-If-Touched | no dedicated value † | | Trailing-Stop-Market | `3` (Stop) + trailing peg | | Trailing-Stop-Limit | `4` (Stop Limit) + trailing peg | † FIX defines no dedicated `OrdType` for *Limit-If-Touched*; it is commonly sent as `4` (Stop Limit) with a favorable trigger. Trailing stops likewise have no dedicated value and are modeled as `3`/`4` plus trailing peg fields. ## Advanced orders Orders can be grouped into lists and linked with contingency relationships (OTO, OCO, OUO), and bracket orders attach take-profit and stop-loss children to an entry. See the [Advanced orders](advanced.md) guide for order lists, contingency types, validation rules, and brackets. ## Emulated orders NautilusTrader can locally emulate order types that a venue does not natively support, using only `MARKET` and `LIMIT` orders for actual execution. See the [Emulated orders](emulated.md) guide for the emulation lifecycle, supported types, querying, and best practices. ## Related guides - [Events](../events/) - Order events, position events, and handler dispatch. - [Execution](../execution/) - Order execution and fill handling. - [Positions](../positions.md) - Positions created from order fills. - [Strategies](../strategies.md) - Order management from strategies. # Limit Source: https://nautilustrader.io/docs/latest/concepts/orders/limit/ `FIX OrdType <40>=2` A *Limit* order rests on the limit order book at a specified price and executes only at that price or better. ## Use cases Use a *Limit* order to control the execution price and, when appropriate, provide liquidity. Common uses include market making, scaling into or out of a position at chosen levels, and targeting maker fees with `post_only`. The order cannot fill worse than its limit price, but it may remain unfilled or fill only partially. ## Example In the following example we create a *Limit* order on the Binance Futures Crypto exchange to SELL 20 ETHUSDT-PERP Perpetual Futures contracts at a limit price of 5000 USDT, as a market maker. ```rust tab="Rust" use nautilus_model::{ enums::{OrderSide, TimeInForce}, identifiers::InstrumentId, types::{Price, Quantity}, }; let order = self.order().limit( InstrumentId::from("ETHUSDT-PERP.BINANCE"), OrderSide::Sell, Quantity::from(20), Price::from("5000.00"), Some(TimeInForce::Gtc), // optional (default GTC) None, // expire_time Some(true), // post_only (default false) Some(false), // reduce_only (default false) None, // quote_quantity (default false) None, // display_qty (default full display) None, // emulation_trigger None, // trigger_instrument_id None, // exec_algorithm_id None, // exec_algorithm_params None, // tags None, // client_order_id ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import LimitOrder from nautilus_trader.model import OrderSide from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import TimeInForce order: LimitOrder = self.order_factory.limit( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), order_side=OrderSide.SELL, quantity=Quantity.from_int(20), price=Price.from_str("5_000.00"), time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) post_only=True, # <-- optional (default False) reduce_only=False, # <-- optional (default False) display_qty=None, # <-- optional (default None which indicates full display) tags=None, # <-- optional (default None) ) ``` See the [`LimitOrder` API reference](/docs/python-api-latest/model/orders.html#nautilus_trader.model.LimitOrder) for further details. ## Related guides - [Orders](index.md) - Order concepts, execution instructions, and the order factory. - [Emulated orders](emulated.md) - Emulating *Limit* orders, released as *Market* orders on trigger. - [Execution](../execution/) - How orders reach the venue and fills are handled. # Limit-If-Touched Source: https://nautilustrader.io/docs/latest/concepts/orders/limit_if_touched/ `FIX OrdType <40>` no dedicated value (commonly `4` Stop Limit with a favorable trigger) A *Limit-If-Touched* order releases a *Limit* order at the specified price when its trigger price is reached. ## Use cases Use a *Limit-If-Touched* order to activate a price-protected order only after a trigger is touched, for example to place a take-profit *Limit* order as price approaches a target instead of resting it early. As with a *Stop-Limit*, the order may not fill if the market moves through the limit after the trigger. ## Example The following example creates a *Limit-If-Touched* order to BUY 5 BTCUSDT-PERP perpetual futures contracts on Binance Futures at a limit price of 30,100 USDT once the market reaches 30,150 USDT. The order expires one hour after creation: ```rust tab="Rust" use nautilus_core::DurationNanos; use nautilus_model::{ enums::{OrderSide, TimeInForce, TriggerType}, identifiers::InstrumentId, types::{Price, Quantity}, }; use ustr::Ustr; let expire_time = self.clock().timestamp_ns() + DurationNanos::from_mins(60); let order = self.order().limit_if_touched( InstrumentId::from("BTCUSDT-PERP.BINANCE"), OrderSide::Buy, Quantity::from(5), Price::from("30100"), Price::from("30150"), Some(TriggerType::LastPrice), // optional (default DEFAULT) Some(TimeInForce::Gtd), // optional (default GTC) Some(expire_time), // one hour from now Some(true), // post_only (default false) Some(false), // reduce_only (default false) None, // quote_quantity (default false) None, // display_qty None, // emulation_trigger None, // trigger_instrument_id None, // exec_algorithm_id None, // exec_algorithm_params Some(vec![Ustr::from("TAKE_PROFIT")]), // tags None, // client_order_id ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import LimitIfTouchedOrder from nautilus_trader.model import OrderSide from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import TimeInForce from nautilus_trader.model import TriggerType order: LimitIfTouchedOrder = self.order_factory.limit_if_touched( instrument_id=InstrumentId.from_str("BTCUSDT-PERP.BINANCE"), order_side=OrderSide.BUY, quantity=Quantity.from_int(5), price=Price.from_str("30_100"), trigger_price=Price.from_str("30_150"), trigger_type=TriggerType.LAST_PRICE, # <-- optional (default DEFAULT) time_in_force=TimeInForce.GTD, # <-- optional (default GTC) expire_time=self.clock.timestamp_ns() + 3_600_000_000_000, post_only=True, # <-- optional (default False) reduce_only=False, # <-- optional (default False) tags=["TAKE_PROFIT"], # <-- optional (default None) ) ``` See the [`LimitIfTouchedOrder` API reference](/docs/python-api-latest/model/orders.html#nautilus_trader.model.LimitIfTouchedOrder) for further details. ## Related guides - [Orders](index.md#trigger-type) - Trigger types and other execution instructions. - [Emulated orders](emulated.md) - Emulating conditional orders on venues without native support. - [Execution](../execution/) - How orders reach the venue and fills are handled. # Market Source: https://nautilustrader.io/docs/latest/concepts/orders/market/ `FIX OrdType <40>=1` A *Market* order instructs the venue to trade a quantity immediately at the best available price. It can also carry time in force and reduce-only instructions. ## Use cases Use a *Market* order when prompt execution matters more than the exact price, such as for urgent risk reduction or entry into a liquid, fast-moving market. A *Market* order has no price protection: it can incur spread costs and slippage, and the venue can still reject it or leave it unfilled when no market is available. ## Example In the following example we create a *Market* order on the Interactive Brokers [IdealPro](https://ibkr.info/node/1708) Forex ECN to BUY 100,000 AUD using USD: ```rust tab="Rust" use nautilus_model::{ enums::{OrderSide, TimeInForce}, identifiers::InstrumentId, types::Quantity, }; use ustr::Ustr; let order = self.order().market( InstrumentId::from("AUD/USD.IDEALPRO"), OrderSide::Buy, Quantity::from(100_000), Some(TimeInForce::Ioc), // optional (default GTC) Some(false), // reduce_only (default false) None, // quote_quantity (default false) None, // exec_algorithm_id None, // exec_algorithm_params Some(vec![Ustr::from("ENTRY")]), // tags None, // client_order_id (auto-generated if None) ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import MarketOrder from nautilus_trader.model import OrderSide from nautilus_trader.model import Quantity from nautilus_trader.model import TimeInForce order: MarketOrder = self.order_factory.market( instrument_id=InstrumentId.from_str("AUD/USD.IDEALPRO"), order_side=OrderSide.BUY, quantity=Quantity.from_int(100_000), time_in_force=TimeInForce.IOC, # <-- optional (default GTC) reduce_only=False, # <-- optional (default False) tags=["ENTRY"], # <-- optional (default None) ) ``` See the [`MarketOrder` API reference](/docs/python-api-latest/model/orders.html#nautilus_trader.model.MarketOrder) for further details. ## Related guides - [Orders](index.md) - Order concepts, execution instructions, and the order factory. - [Execution](../execution/) - How orders reach the venue and fills are handled. # Market-If-Touched Source: https://nautilustrader.io/docs/latest/concepts/orders/market_if_touched/ `FIX OrdType <40>=J` (Market If Touched) A *Market-If-Touched* order releases a *Market* order when its trigger price is reached. Traders often use it to enter on a pullback or take profit: a SELL order against a LONG position or a BUY order against a SHORT position. ## Use cases Use a *Market-If-Touched* order to prioritize execution when a target price is touched. It triggers in the opposite market direction from a stop order, such as buying below or selling above the current market. The touch price is not a guaranteed fill price, and the released *Market* order can slip, be rejected, or remain unfilled. ## Example In the following example we create a *Market-If-Touched* order on the Binance Futures exchange to SELL 10 ETHUSDT-PERP Perpetual Futures contracts at a trigger price of 10,000 USDT, active until further notice: ```rust tab="Rust" use nautilus_model::{ enums::{OrderSide, TimeInForce, TriggerType}, identifiers::InstrumentId, types::{Price, Quantity}, }; use ustr::Ustr; let order = self.order().market_if_touched( InstrumentId::from("ETHUSDT-PERP.BINANCE"), OrderSide::Sell, Quantity::from(10), Price::from("10000.00"), Some(TriggerType::LastPrice), // optional (default DEFAULT) Some(TimeInForce::Gtc), // optional (default GTC) None, // expire_time Some(false), // reduce_only (default false) None, // quote_quantity (default false) None, // emulation_trigger None, // trigger_instrument_id None, // exec_algorithm_id None, // exec_algorithm_params Some(vec![Ustr::from("ENTRY")]), // tags None, // client_order_id ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import MarketIfTouchedOrder from nautilus_trader.model import OrderSide from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import TimeInForce from nautilus_trader.model import TriggerType order: MarketIfTouchedOrder = self.order_factory.market_if_touched( instrument_id=InstrumentId.from_str("ETHUSDT-PERP.BINANCE"), order_side=OrderSide.SELL, quantity=Quantity.from_int(10), trigger_price=Price.from_str("10_000.00"), trigger_type=TriggerType.LAST_PRICE, # <-- optional (default DEFAULT) time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) reduce_only=False, # <-- optional (default False) tags=["ENTRY"], # <-- optional (default None) ) ``` See the [`MarketIfTouchedOrder` API reference](/docs/python-api-latest/model/orders.html#nautilus_trader.model.MarketIfTouchedOrder) for further details. ## Related guides - [Orders](index.md#trigger-type) - Trigger types and other execution instructions. - [Emulated orders](emulated.md) - Emulating conditional orders on venues without native support. - [Execution](../execution/) - How orders reach the venue and fills are handled. # Market-To-Limit Source: https://nautilustrader.io/docs/latest/concepts/orders/market_to_limit/ `FIX OrdType <40>=K` (Market With Left Over as Limit) A *Market-To-Limit* order submits as a *Market* order. After the first fill, any unfilled quantity rests as a *Limit* order at that fill price. ## Use cases Use a *Market-To-Limit* order to take liquidity at the best available price without sweeping deeper levels. This can suit thin books or larger orders where limiting further market impact matters. Any remainder can stay unfilled if the market moves away from the first fill price. ## Example In the following example we create a *Market-To-Limit* order on the Interactive Brokers [IdealPro](https://ibkr.info/node/1708) Forex ECN to BUY 200,000 USD using JPY: ```rust tab="Rust" use nautilus_model::{ enums::{OrderSide, TimeInForce}, identifiers::InstrumentId, types::Quantity, }; let order = self.order().market_to_limit( InstrumentId::from("USD/JPY.IDEALPRO"), OrderSide::Buy, Quantity::from(200_000), Some(TimeInForce::Gtc), // optional (default GTC) None, // expire_time Some(false), // reduce_only (default false) None, // quote_quantity (default false) None, // display_qty (default full display) None, // exec_algorithm_id None, // exec_algorithm_params None, // tags None, // client_order_id ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import MarketToLimitOrder from nautilus_trader.model import OrderSide from nautilus_trader.model import Quantity from nautilus_trader.model import TimeInForce order: MarketToLimitOrder = self.order_factory.market_to_limit( instrument_id=InstrumentId.from_str("USD/JPY.IDEALPRO"), order_side=OrderSide.BUY, quantity=Quantity.from_int(200_000), time_in_force=TimeInForce.GTC, # <-- optional (default GTC) reduce_only=False, # <-- optional (default False) display_qty=None, # <-- optional (default None which indicates full display) tags=None, # <-- optional (default None) ) ``` See the [`MarketToLimitOrder` API reference](/docs/python-api-latest/model/orders.html#nautilus_trader.model.MarketToLimitOrder) for further details. ## Related guides - [Orders](index.md) - Order concepts, execution instructions, and the order factory. - [Execution](../execution/) - How orders reach the venue and fills are handled. # Stop-Limit Source: https://nautilustrader.io/docs/latest/concepts/orders/stop_limit/ `FIX OrdType <40>=4` (Stop Limit) A *Stop-Limit* order releases a *Limit* order at the specified price when its trigger price is reached. ## Use cases Use a *Stop-Limit* order when a stop trigger must also enforce a worst acceptable fill price, such as for a price-protected exit or breakout entry. If the market gaps through both the trigger and limit, the order may not fill and can leave a position unprotected. ## Example The following example creates a *Stop-Limit* order on the Currenex FX ECN to BUY 50,000 GBP at a limit price of 1.3000 USD once the market reaches 1.30010 USD. The order expires one hour after creation: ```rust tab="Rust" use nautilus_core::DurationNanos; use nautilus_model::{ enums::{OrderSide, TimeInForce, TriggerType}, identifiers::InstrumentId, types::{Price, Quantity}, }; let expire_time = self.clock().timestamp_ns() + DurationNanos::from_mins(60); let order = self.order().stop_limit( InstrumentId::from("GBP/USD.CURRENEX"), OrderSide::Buy, Quantity::from(50_000), Price::from("1.30000"), Price::from("1.30010"), Some(TriggerType::BidAsk), // optional (default DEFAULT) Some(TimeInForce::Gtd), // optional (default GTC) Some(expire_time), // one hour from now Some(true), // post_only (default false) Some(false), // reduce_only (default false) None, // quote_quantity (default false) None, // display_qty None, // emulation_trigger None, // trigger_instrument_id None, // exec_algorithm_id None, // exec_algorithm_params None, // tags None, // client_order_id ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import OrderSide from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import StopLimitOrder from nautilus_trader.model import TimeInForce from nautilus_trader.model import TriggerType order: StopLimitOrder = self.order_factory.stop_limit( instrument_id=InstrumentId.from_str("GBP/USD.CURRENEX"), order_side=OrderSide.BUY, quantity=Quantity.from_int(50_000), price=Price.from_str("1.30000"), trigger_price=Price.from_str("1.30010"), trigger_type=TriggerType.BID_ASK, # <-- optional (default DEFAULT) time_in_force=TimeInForce.GTD, # <-- optional (default GTC) expire_time=self.clock.timestamp_ns() + 3_600_000_000_000, post_only=True, # <-- optional (default False) reduce_only=False, # <-- optional (default False) tags=None, # <-- optional (default None) ) ``` See the [`StopLimitOrder` API reference](/docs/python-api-latest/model/orders.html#nautilus_trader.model.StopLimitOrder) for further details. ## Related guides - [Orders](index.md#trigger-type) - Trigger types and other execution instructions. - [Emulated orders](emulated.md) - Emulating conditional orders on venues without native support. - [Execution](../execution/) - How orders reach the venue and fills are handled. # Stop-Market Source: https://nautilustrader.io/docs/latest/concepts/orders/stop_market/ `FIX OrdType <40>=3` (Stop) A *Stop-Market* order releases a *Market* order when its trigger price is reached. It is often used as a stop-loss: a SELL order against a LONG position or a BUY order against a SHORT position. ## Use cases Use a *Stop-Market* order to prioritize execution after a price level is breached, such as for a protective stop-loss or breakout entry. The trigger price is not a guaranteed fill price: a fast or gapping market can produce substantial slippage, and the released order can still be rejected or remain unfilled when no market is available. A *Stop-Limit* provides price protection instead but may not fill. ## Example In the following example we create a *Stop-Market* order on the Binance Spot/Margin exchange to SELL 1 BTC at a trigger price of 100,000 USDT, active until further notice: ```rust tab="Rust" use nautilus_model::{ enums::{OrderSide, TimeInForce, TriggerType}, identifiers::InstrumentId, types::{Price, Quantity}, }; let order = self.order().stop_market( InstrumentId::from("BTCUSDT.BINANCE"), OrderSide::Sell, Quantity::from(1), Price::from("100000"), Some(TriggerType::LastPrice), // optional (default DEFAULT) Some(TimeInForce::Gtc), // optional (default GTC) None, // expire_time Some(false), // reduce_only (default false) None, // quote_quantity (default false) None, // display_qty None, // emulation_trigger None, // trigger_instrument_id None, // exec_algorithm_id None, // exec_algorithm_params None, // tags None, // client_order_id ); ``` ```python tab="Python" from nautilus_trader.model import InstrumentId from nautilus_trader.model import OrderSide from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import StopMarketOrder from nautilus_trader.model import TimeInForce from nautilus_trader.model import TriggerType order: StopMarketOrder = self.order_factory.stop_market( instrument_id=InstrumentId.from_str("BTCUSDT.BINANCE"), order_side=OrderSide.SELL, quantity=Quantity.from_int(1), trigger_price=Price.from_int(100_000), trigger_type=TriggerType.LAST_PRICE, # <-- optional (default DEFAULT) time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) reduce_only=False, # <-- optional (default False) tags=None, # <-- optional (default None) ) ``` See the [`StopMarketOrder` API reference](/docs/python-api-latest/model/orders.html#nautilus_trader.model.StopMarketOrder) for further details. ## Related guides - [Orders](index.md#trigger-type) - Trigger types and other execution instructions. - [Emulated orders](emulated.md) - Emulating conditional orders on venues without native support. - [Execution](../execution/) - How orders reach the venue and fills are handled. # Trailing-Stop-Limit Source: https://nautilustrader.io/docs/latest/concepts/orders/trailing_stop_limit/ `FIX OrdType <40>=4` (Stop Limit) + trailing peg A *Trailing-Stop-Limit* order keeps its stop trigger a fixed offset from the specified market price as the market moves favorably. It releases a *Limit* order when triggered, and the limit price also updates with the market until then. ## Use cases Use a *Trailing-Stop-Limit* order for dynamic trailing protection with a worst acceptable fill price. As with a *Stop-Limit*, the released *Limit* order may not fill during a fast reversal and can leave the position open. ## Example In the following example we create a *Trailing-Stop-Limit* order on the Currenex FX ECN to BUY 1,250,000 AUD using USD at a limit price of 0.71000 USD, activating at 0.72000 USD then trailing at a stop offset of 0.00100 USD away from the current ask price, active until further notice: ```rust tab="Rust" use nautilus_model::{ enums::{OrderSide, TimeInForce, TrailingOffsetType, TriggerType}, identifiers::InstrumentId, types::{Price, Quantity}, }; use rust_decimal_macros::dec; use ustr::Ustr; let order = self.order().trailing_stop_limit( InstrumentId::from("AUD/USD.CURRENEX"), OrderSide::Buy, Quantity::from(1_250_000), Price::from("0.71000"), // limit price dec!(0.00050), // limit_offset dec!(0.00100), // trailing_offset Some(TrailingOffsetType::Price), // optional (default PRICE) Some(Price::from("0.72000")), // activation_price None, // trigger_price (materializes from the offset on the first trail) Some(TriggerType::BidAsk), // optional (default DEFAULT) Some(TimeInForce::Gtc), // optional (default GTC) None, // expire_time Some(false), // post_only (default false) Some(true), // reduce_only (default false) None, // quote_quantity (default false) None, // display_qty None, // emulation_trigger None, // trigger_instrument_id None, // exec_algorithm_id None, // exec_algorithm_params Some(vec![Ustr::from("TRAILING_STOP")]), // tags None, // client_order_id ); ``` ```python tab="Python" from decimal import Decimal from nautilus_trader.model import InstrumentId from nautilus_trader.model import OrderSide from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import TimeInForce from nautilus_trader.model import TrailingOffsetType from nautilus_trader.model import TrailingStopLimitOrder from nautilus_trader.model import TriggerType order: TrailingStopLimitOrder = self.order_factory.trailing_stop_limit( instrument_id=InstrumentId.from_str("AUD/USD.CURRENEX"), order_side=OrderSide.BUY, quantity=Quantity.from_int(1_250_000), price=Price.from_str("0.71000"), activation_price=Price.from_str("0.72000"), trigger_type=TriggerType.BID_ASK, # <-- optional (default DEFAULT) limit_offset=Decimal("0.00050"), trailing_offset=Decimal("0.00100"), trailing_offset_type=TrailingOffsetType.PRICE, time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) reduce_only=True, # <-- optional (default False) tags=["TRAILING_STOP"], # <-- optional (default None) ) ``` :::info If both `activation_price` and `trigger_price` are omitted, the order activates immediately at the current market and its trigger price materializes from `trailing_offset` on the first update. ::: See the [`TrailingStopLimitOrder` API reference](/docs/python-api-latest/model/orders.html#nautilus_trader.model.TrailingStopLimitOrder) for further details. ## Related guides - [Orders](index.md#trailing-offset-type) - Trigger and trailing offset types. - [Emulated orders](emulated.md) - Emulating trailing stops on venues without native support. - [Execution](../execution/) - How orders reach the venue and fills are handled. # Trailing-Stop-Market Source: https://nautilustrader.io/docs/latest/concepts/orders/trailing_stop_market/ `FIX OrdType <40>=3` (Stop) + trailing peg A *Trailing-Stop-Market* order keeps its stop trigger a fixed offset from the specified market price as the market moves favorably. It releases a *Market* order when triggered. ## Use cases Use a *Trailing-Stop-Market* order to protect gains while allowing a position to continue through favorable moves. A tight offset can trigger on ordinary volatility, while a wide offset can give back more profit. The released *Market* order can also slip, be rejected, or remain unfilled on a sharp reversal. ## Example In the following example we create a *Trailing-Stop-Market* order on the Binance Futures exchange to SELL 10 ETHUSD-PERP COIN_M margined Perpetual Futures Contracts. It activates at a price of 5,000 USD, then trails at an offset of 1% (in basis points) from the current last traded price: ```rust tab="Rust" use nautilus_model::{ enums::{OrderSide, TimeInForce, TrailingOffsetType, TriggerType}, identifiers::InstrumentId, types::{Price, Quantity}, }; use rust_decimal::Decimal; use ustr::Ustr; let order = self.order().trailing_stop_market( InstrumentId::from("ETHUSD-PERP.BINANCE"), OrderSide::Sell, Quantity::from(10), Decimal::from(100), // trailing_offset Some(TrailingOffsetType::BasisPoints), // optional (default PRICE) Some(Price::from("5000")), // activation_price None, // trigger_price (materializes from the offset on the first trail) Some(TriggerType::LastPrice), // optional (default DEFAULT) Some(TimeInForce::Gtc), // optional (default GTC) None, // expire_time Some(true), // reduce_only (default false) None, // quote_quantity (default false) None, // display_qty None, // emulation_trigger None, // trigger_instrument_id None, // exec_algorithm_id None, // exec_algorithm_params Some(vec![Ustr::from("TRAILING_STOP-1")]), // tags None, // client_order_id ); ``` ```python tab="Python" from decimal import Decimal from nautilus_trader.model import InstrumentId from nautilus_trader.model import OrderSide from nautilus_trader.model import Price from nautilus_trader.model import Quantity from nautilus_trader.model import TimeInForce from nautilus_trader.model import TrailingOffsetType from nautilus_trader.model import TrailingStopMarketOrder from nautilus_trader.model import TriggerType order: TrailingStopMarketOrder = self.order_factory.trailing_stop_market( instrument_id=InstrumentId.from_str("ETHUSD-PERP.BINANCE"), order_side=OrderSide.SELL, quantity=Quantity.from_int(10), activation_price=Price.from_str("5_000"), trigger_type=TriggerType.LAST_PRICE, # <-- optional (default DEFAULT) trailing_offset=Decimal(100), trailing_offset_type=TrailingOffsetType.BASIS_POINTS, time_in_force=TimeInForce.GTC, # <-- optional (default GTC) expire_time=None, # <-- optional (default None) reduce_only=True, # <-- optional (default False) tags=["TRAILING_STOP-1"], # <-- optional (default None) ) ``` :::info If both `activation_price` and `trigger_price` are omitted, the order activates immediately at the current market and its trigger price materializes from `trailing_offset` on the first update. ::: See the [`TrailingStopMarketOrder` API reference](/docs/python-api-latest/model/orders.html#nautilus_trader.model.TrailingStopMarketOrder) for further details. ## Related guides - [Orders](index.md#trailing-offset-type) - Trigger and trailing offset types. - [Emulated orders](emulated.md) - Emulating trailing stops on venues without native support. - [Execution](../execution/) - How orders reach the venue and fills are handled.