# 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 Rust-native 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 execution semantics and time model apply 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.
## Evaluation areas
- [Architecture](concepts/architecture): event-driven design, core components, environment
contexts, fail-fast behavior, and recovery model.
- [Backtesting](concepts/backtesting): historical simulation APIs, data-loading contracts,
streaming workflows, and post-run analysis.
- [Live trading](concepts/live): live command outcomes, execution reconciliation, startup
recovery, and operational caveats.
- [Integrations](integrations): supported venues and data providers, adapter status, and
adapter implementation goals.
- [Developer guide](developer_guide): Rust and Python internals, testing standards,
adapter specs, and release process.
}>
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.md).
## 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`:
| Account type | Typical use case | What the engine locks |
| ------------ | ------------------------------------------------ | ------------------------------------------------------------------------- |
| Cash | Spot trading (e.g., BTC/USDT, stocks) | Notional value for every position a pending order would open. |
| 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. |
### Cash accounts
Cash accounts settle trades in full; there is no leverage and therefore no
concept of margin. Locked balances reflect the notional reserved for pending
orders.
### 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.
**Key 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.
:::
### Betting accounts
Betting accounts are specialised 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.
## 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 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 helper | When to use |
| --------------------------------------- | ------------------------------------------------------------------------------ |
| `AccountBalance::from_total_and_locked` | Venue reports total and locked; `free` is derived and clamped to `[0, total]`. |
| `AccountBalance::from_total_and_free` | Venue reports total and free; `locked` is derived and clamped. |
| `AccountBalance::new` | All three values are already known and consistent (tests, pass‑through). |
The helpers clamp the derived field to `[0, total]` when `total >= 0`, so
transient overshoots from venue rounding never leave the account in a broken
state.
## 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.
:::note
`MarginAccount.apply()` **replaces** both stores from the incoming event. It does
not merge with prior state. 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. The balances list is likewise replaced.
:::
## 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 of the value you want | Use |
| -------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Per‑instrument margin (isolated) | `margin(id)` / `margin_init(id)` / `margin_maint(id)` |
| Account‑wide margin for one collateral | `margin_for_currency(ccy)` / `margin_init_for_currency(ccy)` / `margin_maint_for_currency(ccy)` |
| Combined total across both scopes | `total_margin_init(ccy)` / `total_margin_maint(ccy)` |
Point queries return `None` when the entry is absent; total queries always
return a `Money` (zero for the currency if nothing matches).
:::note
The names below are the Python / Cython API on `MarginAccount`. Rust strategies
using the `nautilus-model` crate call `account_margin(¤cy)`,
`account_initial_margin(¤cy)`, `account_maintenance_margin(¤cy)`,
`total_initial_margin(currency)`, and `total_maintenance_margin(currency)`: the
same split by `Option`, with different method names.
:::
### Per-instrument queries (`MarginAccount`)
- `margin(instrument_id) -> MarginBalance | None`
- `margin_init(instrument_id) -> Money | None`
- `margin_maint(instrument_id) -> Money | None`
- `margins() -> dict[InstrumentId, MarginBalance]` (all per-instrument entries)
- `margins_init() -> dict[InstrumentId, Money]`
- `margins_maint() -> 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`)
- `margin_for_currency(currency) -> MarginBalance | None`
- `margin_init_for_currency(currency) -> Money | None`
- `margin_maint_for_currency(currency) -> Money | None`
- `account_margins() -> dict[Currency, MarginBalance]` (all account-wide entries)
- `account_margins_init() -> dict[Currency, Money]`
- `account_margins_maint() -> dict[Currency, Money]`
### Totals (`MarginAccount`)
These sum across per-instrument and account-wide entries for a given currency:
- `total_margin_init(currency) -> Money`
- `total_margin_maint(currency) -> Money`
Useful when a strategy trades on a venue where both scopes may appear (for
example, isolated positions alongside cross-margin collateral).
### Clearing account-wide entries
- `clear_account_margin(currency)` removes the account-wide entry for a given
collateral currency and triggers a balance recalculation. The counterpart for
per-instrument entries is `clear_margin(instrument_id)`.
These are system methods; adapter code calls them implicitly via
`MarginAccount.apply()`. Strategies should not need them directly.
### Portfolio-level queries
Margin queries:
- `portfolio.margins_init(venue=..., account_id=...) -> dict[InstrumentId, Money]`
- `portfolio.margins_maint(venue=..., account_id=...) -> dict[InstrumentId, Money]`
These mirror `MarginAccount.margins_init` / `margins_maint` and return only the
per-instrument entries. For account-wide data on cross-margin venues, query the
account directly via `portfolio.account(venue).margin_init_for_currency(ccy)`.
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=...) -> dict[Currency, Money]`
- `portfolio.realized_pnls(venue=..., account_id=...) -> dict[Currency, Money]`
- `portfolio.total_pnls(venue=..., account_id=...) -> dict[Currency, Money]`
- `portfolio.net_exposures(venue=..., account_id=...) -> dict[Currency, Money]`
- `portfolio.mark_values(venue=..., account_id=...) -> dict[Currency, Money]`
- `portfolio.equity(venue=..., account_id=...) -> dict[Currency, Money]`
- `portfolio.missing_price_instruments(venue) -> list[InstrumentId]`
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.margin_init_for_currency(USDC)
usdc_total = margin_account.total_margin_init(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
`_account_margins` or `_margins` without going through a model.
### Overview
Different venues treat leverage differently:
- **Traditional brokers** (Interactive Brokers, TD Ameritrade): 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, or implement a
[custom model](#custom-models).
### HEDGING-mode netting
Under `OmsType.HEDGING` each fill opens its own `Position`, so an account can
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 for the
same fill sequence; 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. Override programmatically:
```python
from nautilus_trader.backtest.models import LeveragedMarginModel
from nautilus_trader.backtest.models import StandardMarginModel
from nautilus_trader.test_kit.stubs.execution import TestExecStubs
account = TestExecStubs.margin_account()
# Traditional broker behavior
account.set_margin_model(StandardMarginModel())
# Or the leveraged model (default)
account.set_margin_model(LeveragedMarginModel())
```
### 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 $10,000 account: the standard model blocks the trade; the leveraged model
allows it.
### Custom models
Subclass `MarginModel` and receive configuration through `MarginModelConfig`:
```python
from decimal import Decimal
from nautilus_trader.backtest.config import MarginModelConfig
from nautilus_trader.backtest.models import MarginModel
from nautilus_trader.model.objects import Money
class RiskAdjustedMarginModel(MarginModel):
def __init__(self, config: MarginModelConfig) -> None:
self.risk_multiplier = Decimal(str(config.config.get("risk_multiplier", 1.0)))
self.use_leverage = config.config.get("use_leverage", False)
def calculate_margin_init(self, instrument, quantity, price, leverage, use_quote_for_inverse=False):
notional = instrument.notional_value(quantity, price, use_quote_for_inverse)
if self.use_leverage:
adjusted = notional.as_decimal() / leverage
else:
adjusted = notional.as_decimal()
margin = adjusted * instrument.margin_init * self.risk_multiplier
return Money(margin, instrument.quote_currency)
def calculate_margin_maint(self, instrument, side, quantity, price, leverage, use_quote_for_inverse=False):
return self.calculate_margin_init(instrument, quantity, price, leverage, use_quote_for_inverse)
```
For backtest-wide configuration of the margin model via `BacktestVenueConfig`
and `MarginModelConfig`, see the margin-models section of
[Backtesting](backtesting.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 helpers 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` |
:::note
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.md): starting balances, `MarginModelConfig`, 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.
# Actors
Source: https://nautilustrader.io/docs/latest/concepts/actors/
An `Actor` receives data, handles events, and manages state. The `Strategy` class extends Actor
with order management capabilities.
**Key capabilities**:
- Data subscription and requests (market data, custom data).
- Event handling and publishing.
- Timers and alerts.
- Cache and portfolio access.
- Logging.
## Basic example
Actors support configuration through a pattern similar to strategies.
```python
from nautilus_trader.config import ActorConfig
from nautilus_trader.model import InstrumentId
from nautilus_trader.model import Bar, BarType
from nautilus_trader.common.actor import Actor
class MyActorConfig(ActorConfig):
instrument_id: InstrumentId # example value: "ETHUSDT-PERP.BINANCE"
bar_type: BarType # example value: "ETHUSDT-PERP.BINANCE-15-MINUTE[LAST]-INTERNAL"
lookback_period: int = 10
class MyActor(Actor):
def __init__(self, config: MyActorConfig) -> None:
super().__init__(config)
# Custom state variables
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
Actors can receive an `ActorConfig` subclass. The base config may include an `actor_id`;
if supplied, the actor registers with that ID. If omitted, the system derives a runtime
actor ID.
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
For Rust actors, generated or assigned runtime IDs live on the actor core rather than being
written back into `DataActorConfig`. This differs from Python bridge paths which may copy
inherited config fields into runtime state when a Python object is created from an importable
config.
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 follow a defined state machine through their lifecycle:
```mermaid
stateDiagram-v2
[*] --> PRE_INITIALIZED
PRE_INITIALIZED --> READY : register()
READY --> STARTING : start()
STARTING --> RUNNING : on_start()
RUNNING --> STOPPING : stop()
STOPPING --> STOPPED : on_stop()
STOPPED --> RUNNING : resume()
RUNNING --> DEGRADING : degrade()
DEGRADING --> DEGRADED : on_degrade()
DEGRADED --> RUNNING : resume()
RUNNING --> FAULTING : fault()
FAULTING --> FAULTED : on_fault()
RUNNING --> DISPOSED : dispose()
```
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 (cancel timers, clean up resources). |
| `on_resume()` | Actor is resuming from a stopped state. |
| `on_reset()` | Reset indicators and internal state (called between backtest runs). |
| `on_degrade()` | Actor is entering a degraded state (partial functionality). |
| `on_fault()` | Actor has encountered a fault. |
| `on_dispose()` | Actor is being disposed (final cleanup). |
## Timers and alerts
Actors have access to a clock for scheduling:
```python
def on_start(self) -> None:
# Set a recurring timer with a callback (fires every 5 seconds)
self.clock.set_timer(
"my_timer",
timedelta(seconds=5),
callback=self._on_timer,
)
# Set a one-time alert with a callback
self.clock.set_time_alert(
"my_alert",
self.clock.utc_now() + timedelta(minutes=1),
callback=self._on_alert,
)
def on_stop(self) -> None:
# Cancel timers to prevent resource leaks across stop/resume cycles
self.clock.cancel_timer("my_timer")
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. If you
omit the callback, the event is delivered to `on_event` instead.
## System access
Actors have access to core system components:
| Property | Description |
|-------------------|----------------------------------------------------------|
| `self.cache` | Shared state for instruments, orders, positions, etc. |
| `self.portfolio` | Portfolio state and calculations. |
| `self.clock` | Current time and timer/alert scheduling. |
| `self.log` | Structured logging. |
| `self.msgbus` | Publish/subscribe to custom messages. |
For custom messaging between components, see the [Message Bus](message_bus.md) guide.
## Data handling and callbacks
The system uses different callback handlers depending on whether data is historical or real-time.
Understanding the relationship between data *requests/subscriptions* and their handlers is key.
### Historical vs real-time data
The system distinguishes between two data flows:
1. **Historical data** (from *requests*):
- Obtained through methods like `request_bars()`, `request_quote_ticks()`, etc.
- Processed through the `on_historical_data()` handler.
- Used for initial data loading and historical analysis.
2. **Real-time data** (from *subscriptions*):
- Obtained through methods like `subscribe_bars()`, `subscribe_quote_ticks()`, etc.
- Processed through specific handlers like `on_bar()`, `on_quote_tick()`, etc.
- Used for live data processing.
### Callback handlers
Different data operations map to these handlers:
| Operation | Category | Handler | Purpose |
|--------------------------------------|------------|--------------------------|---------------------------------------------------|
| `subscribe_data()` | Real‑time | `on_data()` | Live data updates. |
| `subscribe_instrument()` | Real‑time | `on_instrument()` | Live instrument definition updates. |
| `subscribe_instruments()` | Real‑time | `on_instrument()` | Live instrument definition updates (for venue). |
| `subscribe_order_book_deltas()` | Real‑time | `on_order_book_deltas()` | Live order book deltas. |
| `subscribe_order_book_depth()` | Real‑time | `on_order_book_depth()` | Live order book depth snapshots. |
| `subscribe_order_book_at_interval()` | Real‑time | `on_order_book()` | Live order book snapshots at intervals. |
| `subscribe_quote_ticks()` | Real‑time | `on_quote_tick()` | Live quote updates. |
| `subscribe_trade_ticks()` | Real‑time | `on_trade_tick()` | Live trade updates. |
| `subscribe_mark_prices()` | Real‑time | `on_mark_price()` | Live mark price updates. |
| `subscribe_index_prices()` | Real‑time | `on_index_price()` | Live index price updates. |
| `subscribe_bars()` | Real‑time | `on_bar()` | Live bar updates. |
| `subscribe_funding_rates()` | Real‑time | `on_funding_rate()` | Live funding rate updates. |
| `subscribe_instrument_status()` | Real‑time | `on_instrument_status()` | Live instrument status updates. |
| `subscribe_instrument_close()` | Real‑time | `on_instrument_close()` | Live instrument close updates. |
| `subscribe_option_greeks()` | Real‑time | `on_option_greeks()` | Live option greeks updates. |
| `subscribe_option_chain()` | Real‑time | `on_option_chain()` | Live option chain slice snapshots. |
| `subscribe_order_fills()` | Real‑time | `on_order_filled()` | Live order fill events for an instrument. |
| `subscribe_order_cancels()` | Real‑time | `on_order_canceled()` | Live order cancel events for an instrument. |
| `request_data()` | Historical | `on_historical_data()` | Historical data processing. |
| `request_order_book_deltas()` | Historical | `on_historical_data()` | Historical order book deltas. |
| `request_order_book_depth()` | Historical | `on_historical_data()` | Historical order book depth. |
| `request_order_book_snapshot()` | Historical | `on_historical_data()` | Historical order book snapshot. |
| `request_instrument()` | Historical | `on_instrument()` | Instrument definition. |
| `request_instruments()` | Historical | `on_instrument()` | Instrument definitions. |
| `request_quote_ticks()` | Historical | `on_historical_data()` | Historical quotes processing. |
| `request_trade_ticks()` | Historical | `on_historical_data()` | Historical trades processing. |
| `request_bars()` | Historical | `on_historical_data()` | Historical bars processing. |
| `request_aggregated_bars()` | Historical | `on_historical_data()` | Historical aggregated bars (on‑the‑fly). |
| `request_funding_rates()` | Historical | `on_historical_data()` | Historical funding rates processing. |
### Example
This example shows both historical and real-time data handling:
```python
from nautilus_trader.common.actor import Actor
from nautilus_trader.config import ActorConfig
from nautilus_trader.core.data import Data
from nautilus_trader.model import Bar, BarType
from nautilus_trader.model import ClientId, InstrumentId
class MyActorConfig(ActorConfig):
instrument_id: InstrumentId # example value: "AAPL.XNAS"
bar_type: BarType # example value: "AAPL.XNAS-1-MINUTE-LAST-EXTERNAL"
class MyActor(Actor):
def __init__(self, config: MyActorConfig) -> None:
super().__init__(config)
self.bar_type = config.bar_type
def on_start(self) -> None:
# Request historical data - will be processed by on_historical_data() handler
self.request_bars(
bar_type=self.bar_type,
# Many optional parameters
start=None, # pd.Timestamp | None
end=None, # pd.Timestamp | None
callback=None, # Callable[[UUID4], None] | None
update_catalog_mode=None, # UpdateCatalogMode | None
params=None, # dict[str, Any] | None
)
# Subscribe to real-time data - will be processed by on_bar() handler
self.subscribe_bars(
bar_type=self.bar_type,
# Many optional parameters
client_id=None, # ClientId, optional
params=None, # dict[str, Any], optional
)
def on_historical_data(self, data: Data) -> None:
# Handle historical data (from requests)
if isinstance(data, Bar):
self.log.info(f"Received historical bar: {data}")
def on_bar(self, bar: Bar) -> None:
# Handle real-time bar updates (from subscriptions)
self.log.info(f"Received real-time bar: {bar}")
```
When `validate_data_sequence=True`, subscribe to live bars via the `request_bars()`
`callback` (rather than a separate `subscribe_bars()` call) so the stream starts only
after history has loaded; see [Working with bars: request vs. subscribe](data.md#working-with-bars-request-vs-subscribe).
Separating historical and real-time handlers lets you apply different processing logic
based on context. For example:
- Use historical data to initialize indicators or establish baseline metrics.
- Process real-time data differently for live trading decisions.
- Apply different validation or logging for historical vs real-time data.
:::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_data()`
as the data might be coming from a request rather than a subscription.
:::
## Order fill subscriptions
Actors can subscribe to order fill events for specific instruments using `subscribe_order_fills()`.
This is useful for monitoring trading activity, fill analysis, or tracking execution quality.
When subscribed, the handler `on_order_filled()` receives all fills for the specified instrument,
regardless of which strategy or component generated the original order.
### Example
```python
from nautilus_trader.common.actor import Actor
from nautilus_trader.config import ActorConfig
from nautilus_trader.model import InstrumentId
from nautilus_trader.model.events import OrderFilled
class MyActorConfig(ActorConfig):
instrument_id: InstrumentId # example value: "ETHUSDT-PERP.BINANCE"
class FillMonitorActor(Actor):
def __init__(self, config: MyActorConfig) -> None:
super().__init__(config)
self.fill_count = 0
self.total_volume = 0.0
def on_start(self) -> None:
# Subscribe to all fills for the instrument
self.subscribe_order_fills(self.config.instrument_id)
def on_order_filled(self, event: OrderFilled) -> None:
# Handle order fill events
self.fill_count += 1
self.total_volume += float(event.last_qty)
self.log.info(
f"Fill received: {event.order_side} {event.last_qty} @ {event.last_px}, "
f"Total fills: {self.fill_count}, Volume: {self.total_volume}"
)
def on_stop(self) -> None:
# Unsubscribe from fills
self.unsubscribe_order_fills(self.config.instrument_id)
```
:::note
Order fill subscriptions use the message bus only and do not involve the data engine.
The `on_order_filled()` handler receives events only while the actor is running.
:::
## Order cancel subscriptions
Actors can subscribe to order cancel events for specific instruments using `subscribe_order_cancels()`.
This is useful for monitoring cancellations or tracking order lifecycle events.
When subscribed, the handler `on_order_canceled()` receives all cancels for the specified instrument,
regardless of which strategy or component generated the original order.
### Example
```python
from nautilus_trader.common.actor import Actor
from nautilus_trader.config import ActorConfig
from nautilus_trader.model import InstrumentId
from nautilus_trader.model.events import OrderCanceled
class MyActorConfig(ActorConfig):
instrument_id: InstrumentId # example value: "ETHUSDT-PERP.BINANCE"
class CancelMonitorActor(Actor):
def __init__(self, config: MyActorConfig) -> None:
super().__init__(config)
self.cancel_count = 0
def on_start(self) -> None:
# Subscribe to all cancels for the instrument
self.subscribe_order_cancels(self.config.instrument_id)
def on_order_canceled(self, event: OrderCanceled) -> None:
# Handle order cancel events
self.cancel_count += 1
self.log.info(
f"Cancel received: {event.client_order_id}, "
f"Total cancels: {self.cancel_count}"
)
def on_stop(self) -> None:
# Unsubscribe from cancels
self.unsubscribe_order_cancels(self.config.instrument_id)
```
:::note
Order cancel subscriptions use the message bus only and do not involve the data engine.
The `on_order_canceled()` handler receives events only while the actor is running.
:::
## Related guides
- [Strategies](strategies.md) - Strategies extend actors with order management capabilities.
- [Data](data.md) - 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 integrate data providers and trading venues into NautilusTrader.
They can be found in the top-level `adapters` subpackage.
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. |
## Instrument providers
Instrument providers parse venue API responses into Nautilus `Instrument` objects.
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
Here is an example of discovering the current instruments for the Binance Futures testnet:
```python
import asyncio
import os
from nautilus_trader.adapters.binance.common.enums import BinanceAccountType
from nautilus_trader.adapters.binance.common.enums import BinanceEnvironment
from nautilus_trader.adapters.binance import get_cached_binance_http_client
from nautilus_trader.adapters.binance.futures.providers import BinanceFuturesInstrumentProvider
from nautilus_trader.common.component import LiveClock
async def main():
clock = LiveClock()
client = get_cached_binance_http_client(
clock=clock,
account_type=BinanceAccountType.USDT_FUTURES,
api_key=os.getenv("BINANCE_FUTURES_TESTNET_API_KEY"),
api_secret=os.getenv("BINANCE_FUTURES_TESTNET_API_SECRET"),
environment=BinanceEnvironment.TESTNET,
)
provider = BinanceFuturesInstrumentProvider(
client=client,
account_type=BinanceAccountType.USDT_FUTURES,
)
await provider.load_all_async()
# Access loaded instruments
instruments = provider.list_all()
print(f"Loaded {len(instruments)} instruments")
if __name__ == "__main__":
asyncio.run(main())
```
### Live trading
Each integration handles this differently. An `InstrumentProvider` within a `TradingNode`
generally offers two loading behaviors:
- Load all instruments on start:
```python
from nautilus_trader.config import InstrumentProviderConfig
InstrumentProviderConfig(load_all=True)
```
- Load only the instruments specified in configuration:
```python
InstrumentProviderConfig(load_ids=["BTCUSDT-PERP.BINANCE", "ETHUSDT-PERP.BINANCE"])
```
Subscriptions do not load instruments by themselves. Before a strategy subscribes to
live data, configure the provider to load the instrument at startup or request the
instrument explicitly and wait until it reaches the cache.
## 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 nautilus_trader.model import Instrument, InstrumentId
from nautilus_trader.trading.strategy 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: Instrument) -> None:
self.log.info(f"Received instrument: {instrument.id}")
def on_historical_data(self, data) -> None:
self.log.info(f"Received historical data: {data}")
```
### Subscribing to data
For real-time data, use subscription methods:
```python
def on_start(self) -> None:
# Assumes the instrument has already been loaded into the cache
# Subscribe to live trade updates
self.subscribe_trade_ticks(InstrumentId.from_str("BTCUSDT-PERP.BINANCE"))
# Subscribe to live bars
self.subscribe_bars(BarType.from_str("BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL"))
def on_trade_tick(self, tick: TradeTick) -> None:
self.log.info(f"Trade: {tick}")
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.
Key responsibilities:
- Submit, modify, and cancel orders.
- Process fills and execution reports.
- Reconcile order state with the venue.
- Handle account and position updates.
The `ExecutionEngine` routes commands to the appropriate
execution client based on the order's venue. See the [Execution](execution.md) guide for details
on 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.md) - Order execution through adapters.
- [Data](data.md) - Market data provided by adapters.
# Architecture
Source: https://nautilustrader.io/docs/latest/concepts/architecture/
This guide covers the architectural principles and structure of NautilusTrader:
- Design philosophy and quality attributes.
- Core components and how they interact.
- Environment contexts (backtest, sandbox, live).
- Framework organization and code structure.
:::note
Throughout the documentation, the term *"Nautilus system boundary"* refers to operations within
the runtime of a single Nautilus node (also known as a "trader instance").
:::
## Design philosophy
The major architectural techniques and design patterns employed by NautilusTrader are:
- [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) (Pub/Sub, Req/Rep, point-to-point)
- [Ports and adapters](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software))
- [Crash-only design](#crash-only-design)
These techniques help achieve certain architectural quality attributes.
### Quality attributes
Architectural decisions are often a trade-off between competing priorities.
The following quality attributes guide design and architectural decisions,
roughly in order of weighting.
- Reliability
- Performance
- Modularity
- Testability
- Maintainability
- Deployability
### Assurance-driven engineering
NautilusTrader is incrementally adopting a high-assurance mindset: critical code
paths should carry executable invariants that verify behaviour matches the
business requirements. Practically this means we:
- Identify the components whose failure has the highest blast radius (core
domain types, risk and execution flows) and write down their invariants in
plain language.
- Codify those invariants as executable checks (unit tests, property tests,
fuzzers, static assertions) that run in CI, keeping the feedback loop light.
- Prefer zero-cost safety techniques built into Rust (ownership, `Result`
surfaces, `panic = abort`) and add targeted formal tools only where they pay
for themselves.
- Track “assurance debt” alongside feature work so new integrations extend the
safety net rather than bypass it.
This approach preserves the platform’s delivery cadence while giving
high-stakes flows the additional scrutiny they need.
Further reading: [High Assurance Rust](https://highassurance.rs/).
### Crash-only design
NautilusTrader draws inspiration from [crash-only design](https://en.wikipedia.org/wiki/Crash-only_software)
principles, particularly for handling unrecoverable faults. The core insight is that systems which
can recover cleanly from crashes are more robust than those with separate (and rarely tested)
graceful shutdown paths.
Key principles:
- **Unified recovery path** - Startup and crash recovery share the same code path, ensuring it is well-tested.
- **Externalized state** - Critical state is meant to be persisted externally when configured, reducing data-loss risk; durability depends on the backing store.
- **Fast restart** - The system is designed to restart quickly after a crash, minimizing downtime.
- **Idempotent operations** - Operations are designed to be safely retried after restart.
- **Fail-fast for unrecoverable errors** - Data corruption or invariant violations trigger immediate termination rather than attempting to continue in a compromised state.
:::note
The system does provide graceful shutdown flows (`stop`, `dispose`) for normal operation. These
tear down clients, persist state, and flush writers. The crash-only philosophy applies specifically
to *unrecoverable faults* where attempting graceful cleanup could cause further damage.
:::
This design complements the [fail-fast policy](#data-integrity-and-fail-fast-policy), where
unrecoverable errors result in immediate process termination.
**References:**
- [Crash-Only Software](https://www.usenix.org/conference/hotos-ix/crash-only-software) - Candea & Fox, HotOS 2003 (original research paper)
- [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's blog
- [Crash-only software: More than meets the eye](https://lwn.net/Articles/191059/) - LWN.net article
- [Recovery-Oriented Computing (ROC) Project](http://roc.cs.berkeley.edu/) - UC Berkeley/Stanford research
### Data integrity and fail-fast policy
NautilusTrader prioritizes data integrity over availability for trading operations. The system employs
a strict fail-fast policy for arithmetic operations and data handling to prevent silent data corruption
that could lead to incorrect trading decisions.
#### Fail-fast principles
The system will fail fast (panic or return an error) when encountering:
- 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.
Rationale:
In trading systems, corrupt data is worse than no data. A single incorrect price, timestamp, or quantity
can cascade through the system, resulting in:
- Incorrect position sizing or risk calculations.
- Orders placed at wrong prices.
- Backtests producing misleading results.
- Silent financial losses.
By crashing immediately on invalid data, NautilusTrader aims to provide:
1. **No silent corruption** - The fail-fast policy is intended to prevent invalid data from propagating; this relies on checks covering the inputs.
2. **Immediate feedback** - Issues are discovered during development and testing, not in production.
3. **Audit trail** - Crash logs clearly identify the source of invalid data.
4. **Deterministic behavior** - With deterministic ordering and configuration, the same invalid input should trigger the same failure; nondeterministic sources can vary outcomes.
#### When fail-fast applies
Panics are used for:
- Programmer errors (logic bugs, incorrect API usage).
- Data that violates fundamental invariants (negative timestamps, NaN prices).
- Arithmetic that would silently produce incorrect results.
Results or Options are used for:
- Expected runtime failures (network errors, file I/O).
- Business logic validation (order constraints, risk limits).
- User input validation.
- Library APIs exposed to downstream crates where callers need explicit error handling without relying on panics for control flow.
#### Example scenarios
```rust
// CORRECT: Panics on overflow - prevents data corruption
let total_ns = timestamp1 + timestamp2; // Panics if result > u64::MAX
// CORRECT: Rejects NaN during deserialization
let price = serde_json::from_str("NaN"); // Error: "must be finite"
// CORRECT: Explicit overflow handling when needed
let total_ns = timestamp1.checked_add(timestamp2)?; // Returns Option
```
This policy is implemented throughout the core types (`UnixNanos`, `Price`, `Quantity`, etc.)
and helps NautilusTrader maintain strong data correctness for production trading.
In production deployments, the system is typically configured with `panic = abort` in release builds,
ensuring that any panic results in a clean process termination that can be handled by process supervisors
or orchestration systems. This aligns with the [crash-only design](#crash-only-design) principle, where unrecoverable errors
lead to immediate restart rather than attempting to continue in a potentially corrupted state.
## System architecture
The NautilusTrader codebase is actually both a framework for composing trading
systems, and a set of default system implementations which can operate in various
[environment contexts](#environment-contexts).

### Core components
Several core components work together to form the trading system:
#### `NautilusKernel`
The central orchestration component responsible for:
- Initializing and managing all system components.
- Configuring the messaging infrastructure.
- Maintaining environment-specific behaviors.
- Coordinating shared resources and lifecycle management.
- Providing a unified entry point for system operations.
#### `MessageBus`
The backbone of inter-component communication, implementing:
- **Publish/Subscribe patterns**: For broadcasting events and data to multiple consumers.
- **Request/Response communication**: For operations requiring acknowledgment.
- **Command/Event messaging**: For triggering actions and notifying state changes.
- **Optional state persistence**: Using Redis for durability and restart capabilities.
#### `Cache`
High-performance in-memory storage system that:
- Stores instruments, accounts, orders, positions, and more.
- Provides performant fetching capabilities for trading components.
- Maintains consistent state across the system.
- Supports both read and write operations with optimized access patterns.
#### `DataEngine`
Processes and routes market data throughout the system:
- Handles multiple data types (quotes, trades, bars, order books, custom data, and more).
- Routes data to appropriate consumers based on subscriptions.
- Manages data flow from external sources to internal components.
#### `ExecutionEngine`
Manages order lifecycle and execution:
- Routes trading commands to the appropriate adapter clients.
- Tracks order and position states.
- Coordinates with risk management systems.
- Handles execution reports and fills from venues.
- Handles reconciliation of external execution state.
#### `RiskEngine`
Provides risk management:
- Pre-trade risk checks and validation.
- Position and exposure monitoring.
- Real-time risk calculations.
- Configurable risk rules and limits.
### Environment contexts
An environment context in NautilusTrader defines the type of data and trading venue you work with.
Understanding these contexts matters for backtesting, development, and live trading.
Here are the available environments you can work with:
- `Backtest`: Historical data with simulated venues.
- `Sandbox`: Real-time data with simulated venues.
- `Live`: Real-time data with live venues (paper trading or real accounts).
### Common core
The platform has been designed to share as much common code between backtest, sandbox and live trading systems as possible.
This is formalized in the `system` subpackage, where you will find the `NautilusKernel` class,
providing a common core system 'kernel'.
The *ports and adapters* architectural style enables modular components to be integrated into the
core system, providing various hooks for user-defined or custom component implementations.
### Data and execution flow patterns
Understanding how data and execution flow through the system helps when working with the platform.
#### Data flow: life of a quote tick
The following trace shows every step a `QuoteTick` takes from the network to your
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 tip 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_tick(quote)
```
**Step by step:**
1. **Adapter receives raw data.** A venue-specific `DataClient` (e.g. Binance, Bybit)
receives a WebSocket message, parses it, and constructs a `QuoteTick`.
2. **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.
3. **DataEngine processes the event.** The channel receiver routes the event to
`DataEngine::process_data`, which dispatches to `handle_quote`.
4. **Cache stores the quote.** `handle_quote` writes the quote into the `Cache`
via `cache.add_quote(quote)`, making it available to any component through
`self.cache.quote_tick(instrument_id)`.
5. **MessageBus publishes.** The engine publishes the quote on a topic derived
from the instrument ID (e.g. `data.quotes.BINANCE.BTCUSDT-PERP`). The
`MessageBus` finds all handlers subscribed to that topic.
6. **Strategy handler fires.** Each subscribed strategy's `on_quote_tick(quote)`
runs on the single-threaded kernel. The quote is already in the cache before
the handler executes, so `self.cache.quote_tick(instrument_id)` returns the
same quote.
:::tip
For quotes, trades, and bars the cache-then-publish order means your strategy
handler can always read the latest value from the cache. Order book deltas and
depth snapshots are published directly; book state is maintained separately
through `BookUpdater` subscriptions.
:::
#### Execution flow: life of an order
When a strategy submits an order, it flows through validation, routing, and back
again as execution events:
```mermaid
sequenceDiagram
participant Strategy as Strategy
participant RE as RiskEngine
participant EE as ExecutionEngine
participant EC as ExecutionClient
participant Venue as Venue
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->>Strategy: on_order_accepted(event)
Venue-->>EC: OrderFilled
EC->>EE: OrderFilled event
EE->>Strategy: on_order_filled(event)
```
1. **Strategy creates a command.** The strategy calls `self.submit_order(order)`.
2. **RiskEngine validates.** Pre-trade checks run (position limits, notional
limits, order rate). If a check fails the strategy receives `OrderDenied`
and the order never reaches the venue.
3. **ExecutionEngine routes.** The command is routed to the `ExecutionClient`
for the target venue.
4. **ExecutionClient submits.** The adapter sends the order to the venue over
REST or WebSocket.
5. **Events flow back.** The venue responds with acknowledgments and fills.
Each event (Accepted, Filled, Canceled, Rejected, Expired) flows back 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
All components follow a finite state machine pattern. The `ComponentState` enum defines both stable states and transitional states:
```mermaid
stateDiagram-v2
[*] --> PRE_INITIALIZED
PRE_INITIALIZED --> READY : register()
READY --> STARTING : start()
STARTING --> RUNNING
RUNNING --> STOPPING : stop()
STOPPING --> STOPPED
STOPPED --> STARTING : start()
STOPPED --> RESETTING : reset()
RESETTING --> READY
RUNNING --> RESUMING : resume()
RESUMING --> RUNNING
RUNNING --> DEGRADING : degrade()
DEGRADING --> DEGRADED
DEGRADED --> STOPPING : stop()
DEGRADED --> FAULTING : fault()
RUNNING --> FAULTING : fault()
FAULTING --> FAULTED
STOPPED --> DISPOSING : dispose()
FAULTED --> DISPOSING : dispose()
DISPOSING --> DISPOSED
DISPOSED --> [*]
```
**Stable states:**
- **PRE_INITIALIZED**: Component is instantiated but not yet ready to fulfill its specification.
- **READY**: Component is configured and able to be started.
- **RUNNING**: Component is operating normally and can fulfill its specification.
- **STOPPED**: Component has successfully stopped.
- **DEGRADED**: Component has degraded and may not meet its full specification.
- **FAULTED**: Component has shut down due to a detected fault.
- **DISPOSED**: Component has shut down and released all of its resources.
**Transitional states:**
- **STARTING**: Component is executing its actions on `start`.
- **STOPPING**: Component is executing its actions on `stop`.
- **RESUMING**: Component is being started again after its initial start.
- **RESETTING**: Component is executing its actions on `reset`.
- **DISPOSING**: Component is executing its actions on `dispose`.
- **DEGRADING**: Component is executing its actions on `degrade`.
- **FAULTING**: Component is executing its actions on `fault`.
Transitional states are brief intermediate states that occur during state transitions. Components should not remain in transitional states for extended periods.
#### Actor vs Component traits
At the Rust implementation level, the system distinguishes between two complementary traits:
```mermaid
classDiagram
class Actor {
<>
+id() Ustr
+handle(message)
}
class Component {
<>
+component_id() ComponentId
+state() ComponentState
+register()
+start()
+stop()
+reset()
+dispose()
}
class ActorRegistry {
+insert(actor)
+get(id) ActorRef
}
class ComponentRegistry {
+insert(component)
+get(id) ComponentRef
}
Actor <|.. Throttler : implements
Actor <|.. Strategy : implements
Component <|.. Strategy : implements
Component <|.. DataEngine : implements
Component <|.. ExecutionEngine : implements
ActorRegistry --> Actor : manages
ComponentRegistry --> Component : manages
class Throttler {
Actor only
}
class Strategy {
Actor + Component
}
class DataEngine {
Component only
}
class ExecutionEngine {
Component only
}
```
**`Actor` trait** - Message dispatch:
- Provides the `handle` method for receiving messages dispatched through the actor registry.
- Enables type-safe lookup and message dispatch by actor ID.
- Used by components that need to receive targeted messages (strategies, throttlers).
**`Component` trait** - Lifecycle management:
- Manages state transitions (`start`, `stop`, `reset`, `dispose`).
- Provides registration with the system kernel (`register`).
- Tracks component state via the finite state machine described above.
- Used by all system components that need lifecycle management.
:::note
All components can publish and subscribe to messages via the `MessageBus` directly - this is independent of the `Actor` trait. The `Actor` trait specifically enables the registry-based message dispatch pattern where messages are routed to a specific actor by ID.
:::
This separation allows:
- **Actor-only**: Lightweight message handlers without lifecycle (e.g., `Throttler`).
- **Component-only**: System infrastructure with lifecycle but using direct MessageBus pub/sub (e.g., `DataEngine`, `ExecutionEngine`).
- **Both traits**: Trading strategies that need lifecycle management AND targeted message dispatch.
The traits are managed by separate registries to support their different access patterns - lifecycle methods are called sequentially, while message handlers may be invoked re-entrantly during callbacks.
### Messaging
For modularity and loose coupling, an efficient `MessageBus` passes messages (data, commands, and events) between components.
#### Threading model
Within a node, the *kernel* consumes and dispatches messages on a single thread. The kernel encompasses:
- The `MessageBus` and actor callback dispatch.
- Strategy logic and order management.
- Risk engine checks and execution coordination.
- Cache reads and writes.
This single-threaded core provides deterministic event ordering and helps maintain backtest-live parity,
though live inputs and latency can still cause behavioral differences. Components consume messages
synchronously in a pattern *similar* to the [actor model](https://en.wikipedia.org/wiki/Actor_model).
:::note
Of interest is the LMAX exchange architecture, which achieves award winning performance running on
a single thread. You can read about their *disruptor* pattern based architecture in [this interesting article](https://martinfowler.com/articles/lmax.html) by Martin Fowler.
:::
Background services use separate threads or async runtimes:
- **Network I/O** - WebSocket connections, REST clients, and async data feeds.
- **Persistence** - DataFusion queries and database operations via multi-threaded Tokio runtime.
- **Adapters** - Async adapter operations via thread pool executors.
These services communicate results back to the kernel via the `MessageBus`. The bus itself is thread-local,
so each thread has its own instance, with cross-thread communication occurring through channels that
ultimately deliver events to the single-threaded core.
## Framework organization
The codebase organizes into layers of abstraction, grouped into logical subpackages
of cohesive concepts. You can navigate to the documentation for each subpackage
from the left nav menu.
### Core / low-level
- `core`: Constants, functions and low-level components used throughout the framework.
- `common`: Common parts for assembling the frameworks various components.
- `network`: Low-level base components for networking clients.
- `serialization`: Serialization base components and serializer implementations.
- `model`: Defines a rich trading domain model.
### Components
- `accounting`: Different account types and account management machinery.
- `adapters`: Integration adapters for the platform including brokers and exchanges.
- `analysis`: Components relating to trading performance statistics and analysis.
- `cache`: Provides common caching infrastructure.
- `data`: The data stack and data tooling for the platform.
- `execution`: The execution stack for the platform.
- `indicators`: A set of efficient indicators and analyzers.
- `persistence`: Data storage, cataloging and retrieval, mainly to support backtesting.
- `portfolio`: Portfolio management functionality.
- `risk`: Risk specific components and tooling.
- `trading`: Trading domain specific components and tooling.
### System implementations
- `backtest`: Backtesting componentry as well as a backtest engine and node implementations.
- `live`: Live engine and client implementations as well as a node for live trading.
- `system`: The core system kernel common between `backtest`, `sandbox`, `live` [environment contexts](#environment-contexts).
## Code structure
The foundation of the codebase is the `crates` directory, containing a collection of Rust crates including a C foreign function interface (FFI) generated by `cbindgen`.
The bulk of the production code resides in the `nautilus_trader` directory, which contains a collection of Python/Cython subpackages and modules.
Python bindings for the Rust core are provided by statically linking the Rust libraries to the C extension modules generated by Cython at compile time (effectively extending the CPython API).
### Dependency flow
```mermaid
flowchart TB
subgraph trader["nautilus_trader Python / Cython"]
end
subgraph core["crates Rust"]
end
trader -->|"C API"| core
```
### Rust crates
The `crates/` directory contains the Rust implementation organized into focused crates with clear dependency boundaries.
Feature flags control optional functionality - for example, `streaming` enables persistence for catalog-based data streaming,
and `cloud` enables cloud storage backends (S3, Azure, GCP).
Dependency flow (arrows point to dependencies):
```mermaid
flowchart BT
subgraph Foundation
core
model
common
system
trading
end
subgraph Infrastructure
serialization
network
cryptography
persistence
end
subgraph Engines
data
execution
portfolio
risk
end
subgraph Runtime
live
backtest
end
adapters
pyo3
model --> core
common --> core
common --> model
system --> common
trading --> common
serialization --> model
network --> common
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
```
**Crate categories:**
| Category | Crates | Purpose |
|----------------|-----------------------------------------------------------|----------------------------------------------------------|
| Foundation | `core`, `model`, `common`, `system`, `trading` | Primitives, domain model, kernel, actor & strategy base. |
| Engines | `data`, `execution`, `portfolio`, `risk` | Core trading engine components. |
| Infrastructure | `serialization`, `network`, `cryptography`, `persistence` | Encoding, networking, signing, storage. |
| Runtime | `live`, `backtest` | Environment‑specific node implementations. |
| External | `adapters/*` | Venue and data integrations. |
| Bindings | `pyo3` | Python bindings. |
**Feature flags:**
| Feature | Crates | Effect |
|-------------|----------------------------|------------------------------------------------------------|
| `streaming` | `data`, `system`, `live` | Enables `persistence` dependency for catalog streaming. |
| `cloud` | `persistence` | Enables cloud storage backends (S3, Azure, GCP, HTTP). |
| `python` | most crates | Enables PyO3 bindings (auto‑enables `streaming`, `cloud`). |
| `defi` | `common`, `model`, `data` | Enables DeFi/blockchain data types. |
:::note
Both Rust and Cython are build dependencies. The binary wheels produced from a build do not require
Rust or Cython to be installed at runtime.
:::
### Type safety
The platform design prioritizes software correctness and safety.
The Rust codebase under `crates/` relies on the `rustc` compiler's guarantees for safe code.
Any `unsafe` blocks are explicit opt-outs where we must uphold the required invariants ourselves
(see the Rust section of the [Developer Guide](../developer_guide/rust.md)); overall memory and type safety
depend on those invariants holding.
Cython provides type safety at the C level at both compile time, and runtime:
:::info
If you pass an argument with an invalid type to a Cython implemented module with typed parameters,
then you will receive a `TypeError` at runtime.
:::
If a function or method's parameter is not explicitly typed to accept `None`, passing `None` as an
argument will result in a `ValueError` at runtime.
:::warning
The above exceptions are not explicitly documented to prevent excessive bloating of the docstrings.
:::
### Errors and exceptions
The documentation aims to cover all possible exceptions that NautilusTrader code
can raise, and the conditions that trigger them.
:::warning
There may be other undocumented exceptions which can be raised by Python's standard
library, or from third party library dependencies.
:::
### Processes and threads
:::warning[One node per process]
Running multiple `TradingNode` or `BacktestNode` instances **concurrently** in the same process is not supported due to global singleton state:
- **Backtest force-stop flag** - The `_FORCE_STOP` global flag is shared across all engines in the process.
- **Logger mode and timestamps** - The logging subsystem uses global state; backtests flip between static and real-time modes.
- **Runtime singletons** - Global Tokio runtime, callback registries, and other `OnceLock` instances are process-wide.
**Sequential execution** of multiple nodes (one after another with proper disposal between runs) is fully supported and used in the test suite.
For production deployments, add multiple strategies to a **single TradingNode** within a process.
For parallel execution or workload isolation, run each node in its own separate process.
:::
## Related guides
- [Overview](overview.md) - High-level introduction to NautilusTrader.
- [Message Bus](message_bus.md) - Core messaging infrastructure.
# Backtesting
Source: https://nautilustrader.io/docs/latest/concepts/backtesting/
Backtesting simulates trading using a specific system implementation. The system comprises the
built-in engines, `Cache`, [MessageBus](message_bus.md), `Portfolio`, [Actors](actors.md),
[Strategies](strategies.md), [Execution Algorithms](execution.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:
- **High-level API**: Uses a `BacktestNode` and configuration objects (`BacktestEngine`s are used internally).
- **Low-level API**: Uses a `BacktestEngine` directly with more "manual" setup.
## Choosing an API level
Consider using the **low-level** API when:
- Your entire data stream can be processed within the available machine resources (e.g., RAM).
- You prefer not to store data in the Nautilus-specific Parquet format.
- You have a specific need or preference to retain raw data in its original format (e.g., CSV, binary, etc.).
- You require fine-grained control over the `BacktestEngine`, such as the ability to re-run backtests on identical datasets while swapping out components (e.g., actors or strategies) or adjusting parameter configurations.
Consider using the **high-level** API when:
- Your data stream exceeds available memory, requiring streaming data in batches.
- You want the performance and convenience of the `ParquetDataCatalog` for storing data in the Nautilus-specific Parquet format.
- You value the flexibility and functionality of passing configuration objects to define and manage multiple backtest runs across various engines simultaneously.
## Low-level API
The low-level API centers around a `BacktestEngine`, where inputs are initialized and added manually via a Python script.
An instantiated `BacktestEngine` can accept the following:
- Lists of `Data` objects, which are automatically sorted into monotonic order based on `ts_init`.
- Multiple venues, manually initialized.
- Multiple actors, manually initialized and added.
- Multiple execution algorithms, manually initialized and added.
This approach offers detailed control over the backtesting process, allowing you to manually configure each component.
### Loading large datasets efficiently
When working with large amounts of data across multiple instruments, the way you load data
can significantly impact performance.
#### The performance consideration
By default, `BacktestEngine.add_data()` sorts the entire data stream (existing data + newly
added data) on each call when `sort=True` (the default). This means:
- First call with 1M bars: sorts 1M bars.
- Second call with 1M bars: sorts 2M bars.
- Third call with 1M bars: sorts 3M bars.
- And so on...
This repeated sorting of increasingly large datasets can become a bottleneck when loading
data for multiple instruments.
#### Optimization strategies
**Strategy 1: Defer sorting until the end (recommended for multiple instruments)**
```python
from nautilus_trader.backtest.engine import BacktestEngine
engine = BacktestEngine()
# Setup venue and instruments
engine.add_venue(...)
engine.add_instrument(instrument1)
engine.add_instrument(instrument2)
engine.add_instrument(instrument3)
# Load all data WITHOUT sorting on each call
engine.add_data(instrument1_bars, sort=False)
engine.add_data(instrument2_bars, sort=False)
engine.add_data(instrument3_bars, sort=False)
# Sort once at the end - much more efficient!
engine.sort_data()
# Now run your backtest
engine.add_strategy(strategy)
engine.run()
```
**Strategy 2: Collect and add in a single batch**
```python
# Collect all data first
all_bars = []
all_bars.extend(instrument1_bars)
all_bars.extend(instrument2_bars)
all_bars.extend(instrument3_bars)
# Add once with sorting
engine.add_data(all_bars, sort=True)
```
**Strategy 3: Use streaming API for very large datasets**
For datasets that don't fit in memory, there are two streaming approaches:
**Automatic chunking** - supply a generator that yields batches. The engine pulls chunks
lazily during a single `run()` call:
```python
def data_generator():
# Yield chunks of data (each chunk is a list of Data objects)
yield load_chunk_1()
yield load_chunk_2()
yield load_chunk_3()
engine.add_data_iterator(
data_name="my_data_stream",
generator=data_generator(),
)
engine.run() # Chunks are consumed on-demand
```
**Manual chunking** - load and run each batch yourself. This is the pattern
used internally by `BacktestNode` and gives full control over batch boundaries:
```python
engine.add_strategy(strategy)
for batch in data_batches:
engine.add_data(batch)
engine.run(streaming=True)
engine.clear_data()
engine.end() # Finalize: flushes remaining timers, stops engines, produces results
```
:::note
In streaming mode, timer advancement stops when data exhausts for each batch. Timers scheduled
past the last data point (e.g. bar aggregation intervals) are deferred until more data arrives
or `end()` is called, which flushes up to the `end` boundary from the last `run()` call.
:::
:::tip[Performance impact]
For a backtest with 10 instruments, each with 1M bars:
- Sorting on each call: ~10 sorts of increasing size (1M, 2M, 3M, ... 10M bars).
- Sorting once at the end: 1 sort of 10M bars.
The deferred sorting approach can be **significantly faster** for large datasets.
:::
### Data loading contract
The `BacktestEngine` enforces important invariants to ensure data integrity:
**Requirements:**
- All data must be sorted before calling `run()`.
- When using `sort=False`, you **must** call `sort_data()` before running.
- The engine validates this and raises `RuntimeError` if unsorted data is detected.
- Calling `sort_data()` multiple times is safe (idempotent).
**Safety guarantees:**
- Data lists are always copied internally to prevent external mutations from affecting engine state.
- You can safely clear or modify data lists after passing them to `add_data()`.
- Adding data with `sort=True` makes it immediately available for backtesting.
This design ensures data integrity while enabling performance optimizations for large datasets.
## 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
```
`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.
## High-level API
The high-level API centers around a `BacktestNode`, which orchestrates the management of multiple `BacktestEngine` instances,
each defined by a `BacktestRunConfig`. Multiple configurations can be bundled into a list and processed by the node in one run.
Each `BacktestRunConfig` object consists of the following:
- A list of `BacktestDataConfig` objects.
- A list of `BacktestVenueConfig` objects.
- A list of `ImportableActorConfig` objects.
- A list of `ImportableStrategyConfig` objects.
- A list of `ImportableExecAlgorithmConfig` objects.
- An optional `ImportableControllerConfig` object.
- An optional `BacktestEngineConfig` object, with a default configuration if not specified.
## Shutdown on error
Set `BacktestEngineConfig.shutdown_on_error=True` so that a Rust error log ends the
backtest run. The Rust logger records the first `log::error!` emitted after the kernel
starts, and the kernel converts that trigger into a `ShutdownSystem` command the next time
the backtest loop checks for shutdown.
The shutdown request follows the normal backtest stop path. It stops the trader and
engines, then returns the backtest results collected up to the shutdown point. It does not
abort the process.
```python
from nautilus_trader.backtest import BacktestEngineConfig
config = BacktestEngineConfig(shutdown_on_error=True)
```
Error logs suppressed by component filters or `bypass_logging=True` still request shutdown.
The trigger is cleared and re-armed when a new kernel run starts, so a process can run
another backtest without reinitializing the logging system. Shutdown-on-error observes Rust
`log` records, not Python `logging.error(...)` calls.
## Repeated runs
When conducting multiple backtest runs, it's important to understand how components reset to avoid unexpected behavior.
### BacktestEngine.reset()
The `.reset()` method returns engine state and loaded component state to their **initial value**.
It keeps loaded components, data, instruments, and venues registered.
**What gets reset:**
- All trading state (orders, positions, account balances).
- Loaded actors, strategies, and execution algorithms are reset in place.
- Engine counters and timestamps.
**What persists:**
- Data added via `.add_data()` (use `.clear_data()` to remove).
- Instruments (must match the persisted data).
- Venue configurations.
- Loaded actors, strategies, and execution algorithms.
**Instrument handling:**
For `BacktestEngine`, instruments persist across resets by default (because data persists and instruments must match data).
This is configured via `CacheConfig.drop_instruments_on_reset=False` in the default `BacktestEngineConfig`.
### Approaches for multiple backtest runs
There are two main approaches for running multiple backtests:
#### 1. Use BacktestNode (recommended for production)
The high-level API is designed for multiple backtest runs with different configurations:
```python
from nautilus_trader.backtest.node import BacktestNode
from nautilus_trader.config import BacktestRunConfig
# Define multiple run configurations
configs = [
BacktestRunConfig(...), # Run 1
BacktestRunConfig(...), # Run 2
BacktestRunConfig(...), # Run 3
]
# Execute all runs
node = BacktestNode(configs=configs)
results = node.run()
```
Each run gets a fresh engine with clean state - no reset() needed.
#### 2. Use BacktestEngine.reset()
For fine-grained control with the low-level API:
```python
from nautilus_trader.backtest.engine import BacktestEngine
engine = BacktestEngine()
# Setup once
engine.add_venue(...)
engine.add_instrument(ETHUSDT)
engine.add_data(data)
# Run 1
engine.add_strategy(strategy1)
engine.run()
# Reset and run 2 with the same loaded strategy
engine.reset()
engine.run()
# Reset and run 3 with a different strategy
engine.reset()
engine.clear_strategies()
engine.add_strategy(strategy2)
engine.run()
```
:::note
Instruments and data persist across resets by default for `BacktestEngine`, making parameter optimizations straightforward.
:::
:::tip[Best practices]
- **For production backtesting:** Use `BacktestNode` with configuration objects.
- **For parameter optimizations:** Use `BacktestEngine.reset()` to keep data and instruments,
then call `clear_strategies()` before adding a replacement strategy instance.
- **For quick experiments:** Either approach works - choose based on individual use case.
:::
## Data
Data provided for backtesting drives the execution flow. Since a variety of data types can be used,
it's crucial that your venue configurations align with the data being provided for backtesting.
Mismatches between data and configuration can lead to unexpected behavior during execution.
NautilusTrader is primarily designed and optimized for order book data, which provides
a complete representation of every price level or order in the market, reflecting the real-time behavior of a trading venue.
This provides the greatest execution granularity and realism. However, if granular order book data is either not
available or necessary, then the platform has the capability of processing market data in the following 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
```
1. **Order Book Data/Deltas (L3 market-by-order)**:
- Full market depth with visibility of all individual orders.
2. **Order Book Data/Deltas (L2 market-by-price)**:
- Market depth visibility across all price levels.
3. **Quote Ticks (L1 market-by-price)**:
- Top of book only - best bid and ask prices and sizes.
4. **Trade Ticks**:
- Actual executed trades.
5. **Bars**:
- Aggregated trading activity over fixed time intervals (e.g., 1-minute, 1-hour, 1-day).
### Choosing data: cost vs. accuracy
For many trading strategies, bar data (e.g., 1-minute) can be sufficient for backtesting and strategy development. This is
particularly important because bar data is typically much more accessible and cost-effective compared to tick or order book data.
Given this practical reality, Nautilus is designed to support bar-based backtesting with advanced features
that maximize simulation accuracy, even when working with lower granularity data.
:::tip
For some trading strategies, it can be practical to start development with bar data to validate core trading ideas.
If the strategy looks promising, but is more sensitive to precise execution timing (e.g., requires fills at specific prices
between OHLC levels, or uses tight take-profit/stop-loss levels), you can then invest in higher granularity data
for more accurate validation.
:::
## 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 types the matching engine uses to update book
state and drive execution. Data types not applicable for a given `book_type` are
ignored for book and price updates, though precision validation still applies and
the engine clock still advances. Strategies always receive all subscribed data via
the data engine regardless of `book_type`.
| 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 |
:::note
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
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
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.
:::
## Execution
### 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:
- **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.
- **Strategy receives data.** The data engine dispatches the data point to actors
and strategies via their callbacks (e.g. `on_quote_tick`, `on_bar`). Strategies
may submit, cancel, or modify orders during these callbacks.
- **Settle venues.** The engine drains all queued venue commands and then iterates
matching engines to fill newly submitted orders. This loop repeats until no
pending commands remain, so cascading orders (e.g. a hedge submitted from
`on_order_filled`) settle within the same timestamp.
```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
end
rect rgb(245, 255, 245)
note right of BL: Phase 2 - Strategy receives data
BL->>DE: process(data)
DE->>Stgy: on_quote_tick() / 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 pending commands
BL->>Exch: run simulation modules
BL->>Exch: check instrument expirations
end
```
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.
#### Command settling
When an order fill triggers a strategy callback that submits additional orders (e.g., a stop-loss submitted
in `on_order_filled`), those cascading commands are settled within the same timestamp/event cycle. The engine
repeatedly drains venue command queues and any newly generated commands until no commands remain pending
for the current timestamp. Simulation modules are run only once per cycle, after all commands have settled.
When a `LatencyModel` is configured, commands are placed in the venue's inflight queue with a future
timestamp derived from the simulated latency. The settle loop considers inflight commands that are due
at the current timestamp as pending, so zero-latency or same-tick latency configurations still settle
correctly. Commands with future timestamps are deferred and processed when the engine reaches that time.
#### Shutdown semantics
`BacktestEngine::end()` 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 queueing 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.
### Fill modeling philosophy
NautilusTrader treats historical order book and trade data as **immutable** during backtesting. What happened in the market is preserved exactly as recorded. Fills never modify the underlying book state.
This addresses a gap in academic literature: most research focuses on live market dynamics where the book actually evolves. Historical backtesting with frozen snapshots is a distinct engineering problem: how do we simulate realistic fills against data that doesn't change in response to our orders?
**Design choices:**
- **Immutable historical data**: Order book and trade data are never modified.
- **Optional consumption tracking**: When `liquidity_consumption=True`, the engine tracks consumed liquidity per price level to prevent duplicate fills. See [Order book immutability](#order-book-immutability) for configuration.
- **Reproducible results**: A fixed `random_seed` pins the probabilistic fill model's PRNG. Same-process reruns are expected to match; cross-process reruns may differ in rare cases due to hash-ordering effects outside the fill model.
### Fill price determination
The matching engine determines fill prices based on order type, book type, and market state.
#### L2/L3 order book data
With full order book depth, fills are determined by actual book simulation:
| Order Type | Fill Price |
| ---------------------- | ----------------------------------------------------------- |
| `MARKET` | Walks the book, filling at each price level (taker). |
| `MARKET_TO_LIMIT` | Walks the book, filling at each price level (taker). |
| `LIMIT` | Order's limit price when matched (maker). |
| `STOP_MARKET` | Walks the book when triggered. |
| `STOP_LIMIT` | Order's limit price when triggered and matched. |
| `MARKET_IF_TOUCHED` | Walks the book when triggered. |
| `LIMIT_IF_TOUCHED` | Order's limit price when triggered. |
| `TRAILING_STOP_MARKET` | Walks the book when activated and triggered. |
| `TRAILING_STOP_LIMIT` | Order's limit price when activated, triggered, and matched. |
With L2/L3 data, market-type orders may partially fill across multiple price levels if insufficient liquidity exists at the top of book.
Limit-type orders act as resting orders after triggering and may remain unfilled if the market doesn't reach the limit price.
`MARKET_TO_LIMIT` fills as a taker first, then rests any remaining quantity as a limit order at its first fill price.
#### L1 order book data (quotes, trades, bars)
With only top-of-book data, the same book simulation is used with a single-level book:
| Order Type | BUY Fill Price | SELL Fill Price |
| ---------------------- | -------------- | --------------- |
| `MARKET` | Best ask | Best bid |
| `MARKET_TO_LIMIT` | Best ask | Best bid |
| `LIMIT` | Limit price | Limit price |
| `STOP_MARKET` | Best ask | Best bid |
| `STOP_LIMIT` | Limit price | Limit price |
| `MARKET_IF_TOUCHED` | Best ask | Best bid |
| `LIMIT_IF_TOUCHED` | Limit price | Limit price |
| `TRAILING_STOP_MARKET` | Best ask | Best bid |
| `TRAILING_STOP_LIMIT` | Limit price | Limit price |
With L1 data, the simulated book has a single price level. Orders fill against the available size at that level. If an order has remaining quantity after exhausting top-of-book liquidity, market and marketable limit-style orders will slip one tick to fill the residual.
For bar data specifically, `STOP_MARKET` and `TRAILING_STOP_MARKET` orders may fill at the trigger price rather than best ask/bid when the bar moves through the trigger during its high/low processing. See [Stop order fill behavior with bar data](#stop-order-fill-behavior-with-bar-data) for details.
:::note
Fill models can alter these fill prices. See the [Fill models](#fill-models) section for details on configuring execution simulation.
:::
#### Order type semantics
- **Market execution**: Fill at current market price (bid/ask). This models real exchange behavior where these orders execute at the best available price after triggering. Exception: with bar data, `STOP_MARKET` and `TRAILING_STOP_MARKET` orders triggered during H/L processing fill at the trigger price (see below).
- **Limit execution**: Fill at the order's limit price when matched. Provides price guarantee but may not fill if the market doesn't reach the limit.
#### Stop order fill behavior with bar data
When backtesting with bar data only (no tick data), the matching engine distinguishes between two scenarios for `STOP_MARKET` and `TRAILING_STOP_MARKET` orders:
**Gap scenario** (bar opens past trigger):
When a bar's open price gaps past the trigger price, the stop triggers immediately and fills at the market price (the open). This models real exchange behavior where stop-market orders provide no price guarantee during gaps.
Example - SELL `STOP_MARKET` with trigger at 100:
- Previous bar closes at 105.
- Next bar opens at 90 (overnight gap down).
- Stop triggers at open and fills at 90.
**Move-through scenario** (bar moves through trigger):
When a bar opens normally and then its high or low moves through the trigger price, the stop fills at the trigger price. Since we only have OHLC data, we assume the market moved smoothly through the trigger and the order would have filled there.
Example - SELL `STOP_MARKET` with trigger at 100:
- Bar opens at 102 (no gap).
- Bar low reaches 98, moving through trigger at 100.
- Stop fills at 100 (the trigger price).
This behavior caps potential slippage during orderly market moves while still modeling gap slippage accurately. For tick-level precision, use quote or trade tick data instead of bars.
### Price protection
Price protection defines an exchange-calculated price boundary that prevents marketable orders from
executing at excessively aggressive prices. This models exchanges like Binance and CME that implement
protection mechanisms for market and stop-market orders.
**Configuration:**
```python
from nautilus_trader.backtest.config import BacktestVenueConfig
venue_config = BacktestVenueConfig(
name="BINANCE",
oms_type="NETTING",
account_type="MARGIN",
starting_balances=["100_000 USDT"],
price_protection_points=100, # 100 points = 1.00 offset for 2-decimal instruments
)
```
**How it works:**
The matching engine calculates the protection boundary from the current best bid/ask at fill time:
- **BUY orders**: `protection_price = ask + (points × price_increment)`
- **SELL orders**: `protection_price = bid - (points × price_increment)`
The engine filters out fills beyond the protection boundary. For example, with `price_protection_points=100`
on an instrument with `price_increment=0.01`:
- Best ask is 1001.00.
- Protection price = 1001.00 + (100 × 0.01) = 1002.00.
- A BUY market order fills only at prices ≤ 1002.00.
- Liquidity at 1003.00 or higher is filtered, leaving the order partially filled.
**Trigger-time semantics:**
The engine computes protection at fill time, not order submission time:
- **Market orders**: Protection computed immediately when the order processes.
- **Stop-market orders**: Protection computed when the stop triggers, using the bid/ask at that moment.
This design allows stop orders to be submitted even when the opposite side of the book is empty,
since the engine computes protection later when the stop triggers.
**Order types affected:**
- `MARKET`
- `STOP_MARKET`
Limit orders are unaffected since they already define a price boundary.
:::note
Set `price_protection_points=0` to disable price protection (default behavior).
:::
### Slippage and spread handling
When backtesting with different types of data, Nautilus implements specific handling for slippage and spread simulation:
For L2 (market-by-price) or L3 (market-by-order) data, slippage is simulated with high accuracy by:
- Filling orders against actual order book levels.
- Matching available size at each price level sequentially.
- Maintaining realistic order book depth impact (per order fill).
For L1 data types (e.g., L1 order book, trades, quotes, bars), slippage is handled through the `FillModel`:
**Per-fill slippage** (`prob_slippage`):
- Applies to each fill when using an L1 book with a configured `FillModel`.
- Affects all order types (market, limit, stop, etc.).
- When triggered, moves the fill price one tick against the order direction.
- Example: With `prob_slippage=0.5`, a BUY order has 50% chance of filling one tick above the best ask.
:::note
When backtesting with bar data, be aware that the reduced granularity of price information affects the slippage mechanism.
For the most realistic backtesting results, consider using higher granularity data sources such as L2 or L3 order book data when available.
:::
#### How simulation varies by data type
The behavior of the `FillModel` adapts based on the order book type being used:
**L2/L3 order book data**
With full order book depth, the `FillModel` focuses purely on simulating queue position for limit orders through `prob_fill_on_limit`.
The order book itself handles slippage naturally based on available liquidity at each price level.
- `prob_fill_on_limit` is active - simulates queue position.
- `prob_slippage` is not used - real order book depth determines price impact.
:::warning
The historical order book is immutable during backtesting. Book depth is **not** decremented after fills.
By default (`liquidity_consumption=False`), the same liquidity can be consumed repeatedly within an iteration.
Enable `liquidity_consumption=True` to track consumed liquidity per price level. Consumption resets when fresh
data arrives at that level. See [Order book immutability](#order-book-immutability) for details.
:::
**L1 order book data**
With only best bid/ask prices available, the `FillModel` provides additional simulation:
- `prob_fill_on_limit` is active - simulates queue position.
- `prob_slippage` is active - simulates basic price impact since we lack real depth information.
**Bar/Quote/Trade data**
When using less granular data, the same behaviors apply as L1:
- `prob_fill_on_limit` is active - simulates queue position.
- `prob_slippage` is active - simulates basic price impact.
#### Important considerations
- **Partial fills**: With L2/L3 data, fills are limited to available liquidity at each price level. With L1 data, the full order quantity fills at the single available level.
- **Consumption tracking**: See [Order book immutability](#order-book-immutability) for details on preventing duplicate fills.
### Order book immutability
Historical order book data is immutable during backtesting. When your order fills against book liquidity,
the book state remains unchanged. This preserves historical data integrity.
The matching engine can optionally use **per-level consumption tracking** to prevent duplicate fills while
allowing fills when fresh liquidity arrives. This behavior is controlled by the `liquidity_consumption`
configuration option.
**Configuration:**
```python
from nautilus_trader.backtest.config import BacktestVenueConfig
venue_config = BacktestVenueConfig(
name="SIM",
oms_type="NETTING",
account_type="CASH",
starting_balances=["100_000 USD"],
liquidity_consumption=True, # Enable consumption tracking (default: False)
)
```
- `liquidity_consumption=False` (default): Each iteration fills against the full book liquidity independently.
Simpler behavior, assumes you're a small participant whose orders don't meaningfully impact available liquidity.
- `liquidity_consumption=True`: Tracks consumed liquidity per price level. Prevents the same
displayed liquidity from generating multiple fills. Resets when fresh data arrives at that level.
**How consumption tracking works (when enabled):**
For each price level, the engine maintains:
- `original_size`: The book's quantity when tracking began.
- `consumed`: How much has been filled against this level.
When processing a fill:
1. Check if the book's current size at this level matches `original_size`
2. If different (fresh data arrived), reset the entry: `original_size = current_size`, `consumed = 0`
3. Calculate `available = original_size - consumed`
4. After filling, increment `consumed` by the fill quantity
**Example:**
1. Order book shows 100 units at ask 100.00. Engine tracks: `(original=100, consumed=0)`.
2. Your BUY order fills 30 units. Engine updates: `(original=100, consumed=30)`. Available = 70.
3. Another BUY order attempts 50 units. Available = 70, so it fills 50. `(original=100, consumed=80)`.
4. A delta updates ask 100.00 to 120 units. Engine resets: `(original=120, consumed=0)`.
5. New orders can now fill against the fresh 120 units.
**Passive limit order fills on L1 data:**
With L1 data (quotes, trades, bars), the book has only a single price level per side. When the market
moves through a passive (MAKER) limit order's price, the engine must decide how to handle remaining
order quantity after exhausting displayed liquidity.
| `liquidity_consumption` | Behavior when market moves through passive limit |
| ----------------------- | ----------------------------------------------------------------------------------------------- |
| `False` (default) | Fill entire order at limit price. Assumes market movement implies sufficient liquidity existed. |
| `True` | Fill only against displayed liquidity. Order remains open for subsequent fills. |
**Example scenario** (`liquidity_consumption=True`):
1. Quote shows ask 100.10 with 50 units.
2. You place BUY LIMIT at 100.05 for 1000 units (passive, resting below ask).
3. Next quote shows ask 100.00 with 30 units (market moved through your limit).
4. Order fills 30 units against displayed liquidity. 970 units remain open.
5. Next quote shows ask 99.95 with 200 units.
6. Order fills another 200 units. 770 units remain open.
7. Fills continue as fresh liquidity arrives at crossed price levels.
This behavior provides conservative fill simulation: your order only fills against liquidity
actually observed in the data, rather than inferring liquidity from price movements.
**Trade tick liquidity:**
Trade ticks provide evidence of executable liquidity at the trade price. When a trade occurs at a price level
not reflected in the current book, the engine can use the trade quantity as available liquidity, subject to
the same consumption tracking rules (when enabled).
**Trade consumption seeding:**
When using L2/L3 book data and a trade tick triggers order matching (e.g., triggering a resting stop order),
the trade itself consumed liquidity from the book. Before simulating fills for triggered orders, the engine
pre-seeds the consumption maps with the trade's consumed volume. This prevents triggered orders from filling
against liquidity that the triggering trade already consumed. This seeding is skipped for L1 books, where the
trade tick has already updated the single top-of-book level directly.
For example, if the book has 10 units at the best ask and a BUY trade of size 8 triggers a stop market BUY
for 5 units, the stop order sees only 2 units remaining at best ask (10 - 8) and must fill the remaining
3 units at the next price level. Without this seeding, the stop would incorrectly fill all 5 units at the
best ask price.
The engine uses a timestamp guard to avoid double-counting: if the book's most recent update (`ts_last`)
is newer than the trade's event time (`ts_event`), seeding is skipped. This handles exchanges like Binance
where depth deltas arrive before the corresponding trade tick, so the book already reflects the consumed
liquidity, so additional seeding would over-penalize fills.
:::note
As the `FillModel` continues to evolve, future versions may introduce more sophisticated simulation of order execution dynamics, including:
- Variable slippage based on order size.
- More complex queue position modeling.
:::
#### Known limitations
**No queue position within a level**: Consumption tracking determines *how much* liquidity remains at a level,
but doesn't model *where* your order sits in the queue relative to other participants. Use `prob_fill_on_limit`
to simulate queue position probabilistically.
**Trade-driven fills are opportunistic**: When trade ticks indicate liquidity at a price not in the book,
the engine uses this as fill evidence. However, this represents liquidity that existed momentarily and may
not reflect sustained availability.
### Trade-based execution
Trade tick data triggers order fills by default (`trade_execution=True`). A trade tick indicates that liquidity
was accessed at the trade price, allowing resting limit orders to match. This mirrors the default behavior
for bar data (`bar_execution=True`).
Advanced users who want to isolate execution to L1 book data only (quotes or order book updates) can disable
trade-based execution:
```python
venue_config = BacktestVenueConfig(
name="SIM",
oms_type="NETTING",
account_type="CASH",
starting_balances=["100_000 USD"],
trade_execution=False, # Disable trade-based fills
)
```
When `trade_execution=False` or `bar_execution=False`, the respective data types skip order matching
and maintenance operations (GTD order expiry, trailing stop activation, instrument expiration checks).
Quote ticks always trigger maintenance, so this is typically acceptable when using multiple data types.
The matching engine uses a "transient override" mechanism: during the matching process, it temporarily adjusts
the matching core's Best Bid (for BUYER trades) or Best Ask (for SELLER trades) toward the trade price. This allows
resting orders on the passive side to cross the spread and fill. Note: the underlying order book data is never
modified (it remains immutable); only the matching core's internal price references are adjusted.
**Fill determination:**
When a trade tick triggers order matching, the engine determines fills as follows:
1. **Book reflects trade price**: If the order book has liquidity at the trade price, fills use book depth (standard behavior).
2. **Book doesn't reflect trade price**: If the book's liquidity is at a different price, the engine uses a "trade-driven fill" at the order's limit price, capped to `min(order.leaves_qty, trade.size)`.
This ensures that when a trade prints through the spread but the book hasn't updated, fills are bounded by what the trade tick actually evidences. When `liquidity_consumption=False` (default), the same trade size can fill multiple orders within an iteration. When `liquidity_consumption=True`, consumption tracking applies to trade-driven fills as well. Repeated fills at the same trade price will be bounded by consumed liquidity until fresh data arrives.
**Restoration behavior:**
After matching, the core's bid/ask are only restored to their original values if the trade price improved them
(moved them away from the spread):
- **SELLER trade**: Ask is restored only if trade price was below the original ask.
- **BUYER trade**: Bid is restored only if trade price was above the original bid.
If the trade price didn't improve the quote (e.g., a SELLER trade at or above the ask), the core retains
the trade price. This means repeated trades at or beyond the spread can progressively move the core's bid/ask.
**Fill price:**
- **SELLER trade at P**: The engine sets the core's Best Ask to P (if P < current ask). Resting BUY LIMIT orders at P or higher will fill at their limit price (if book doesn't have that level) or at book prices (if book does).
- **BUYER trade at P**: The engine sets the core's Best Bid to P (if P > current bid). Resting SELL LIMIT orders at P or lower will fill at their limit price (if book doesn't have that level) or at book prices (if book does).
This conservative approach ensures fills occur at the order's limit price rather than potentially better trade prices. For example, a BUY LIMIT at 100.05 triggered by a SELLER trade at 100.00 will fill at 100.05, not 100.00.
:::tip
Combine trade data with book or quote data for best results: book/quote data establishes the baseline spread,
while trade ticks trigger execution for orders that might be inside the spread or ahead of the quote updates.
:::
#### Understanding trade tick aggressor sides
A common source of confusion is the `aggressor_side` field on trade ticks:
- **SELLER trade**: A seller aggressed, selling into the bid. This provides evidence of fill-able liquidity for **BUY** orders at the trade price.
- **BUYER trade**: A buyer aggressed, buying from the ask. This provides evidence of fill-able liquidity for **SELL** orders at the trade price.
In other words, trade ticks trigger fills for orders on the **opposite** side of the aggressor. A SELLER trade at 100.00 can fill your resting BUY LIMIT at 100.00, but cannot fill your SELL LIMIT, since the trade already represents someone else selling.
#### Combining L2 book data with trade ticks
When using L2 order book data (e.g., 100ms throttled depth snapshots) combined with trade tick data:
1. **Book updates establish the spread**: Each book delta/snapshot updates the matching engine's view of available liquidity at each price level.
2. **Trade ticks provide execution evidence**: Trade ticks indicate that liquidity was accessed at a specific price, potentially between book snapshots.
3. **Fill quantity determination**: When a trade triggers a fill:
- If the book already reflects liquidity at the trade price, fills use book depth
- If the trade price is inside the spread (not in the current book), fills are capped by `min(order.leaves_qty, trade.size)`
4. **Timing considerations**: With throttled book data (e.g., 100ms), the book may lag behind trades. A trade at a price not yet reflected in the book will use trade-driven fill logic.
**Common misconception**: Users sometimes expect every trade tick to trigger fills. Remember:
- Only trades on the **opposite** side can fill your orders.
- SELLER trades -> potential BUY fills.
- BUYER trades -> potential SELL fills.
- Book UPDATE events move the market but only trigger fills if prices cross your order.
#### Queue position tracking
When `queue_position=True` is enabled alongside `trade_execution=True`, the matching engine simulates
queue position for limit orders. This provides more realistic fill behavior by tracking how many
orders are "ahead" of your order at a given price level.
**How it works:**
1. **Order placement**: When a LIMIT order is accepted, the engine snapshots the current same-side
book depth at the order's price level. This represents the orders ahead in the queue.
2. **Trade ticks**: When trade ticks occur at the order's price level, the "quantity ahead" is
decremented by the trade size. Only trades on the correct side affect the queue (BUYER trades
decrement queue for SELL orders, SELLER trades decrement queue for BUY orders). Trades with
`NO_AGGRESSOR` (common in historical datasets lacking aggressor metadata) affect both sides.
This is pessimistic but prevents orders from stalling indefinitely.
3. **Fill eligibility**: The order becomes eligible to fill only when the quantity ahead reaches zero.
On the tick that clears the queue, only the excess volume (trade size minus queue ahead) is
available for fill, preventing overfill.
4. **Price level DELETE**: If the order book level is deleted (BookAction.DELETE), the queue clears
immediately, making the order fill-eligible. UPDATE actions are ignored (queue unchanged).
5. **Order modification**: If the order is modified (price or quantity change), the queue position
resets. The order moves to the back of the queue at its new price level.
**Configuration:**
```python
from nautilus_trader.backtest.config import BacktestVenueConfig
venue_config = BacktestVenueConfig(
name="SIM",
oms_type="NETTING",
account_type="MARGIN",
starting_balances=["100_000 USD"],
trade_execution=True, # Required for queue_position
queue_position=True, # Enable queue position tracking
)
```
**Example scenario:**
1. Order book shows 100 units at bid 100.00.
2. You place a BUY LIMIT at 100.00 for 50 units. Queue ahead = 100.
3. SELLER trade of 80 units at 100.00 -> queue ahead = 20. No fill yet.
4. SELLER trade of 30 units at 100.00 -> queue clears with 10 excess. Fill = 10 units.
5. Next SELLER trade of 50 units -> fill remaining 40 units.
**Limitations:**
- Only applies to `LIMIT` orders. Stop-limit and limit-if-touched orders are not tracked in this implementation.
- Queue position is per-order, not shared across multiple orders at the same price.
- The queue snapshot is based on book state at order acceptance time.
- Trades with `NO_AGGRESSOR` decrement queue for both sides, which may cause orders to fill sooner than in reality (pessimistic for queue estimation, but prevents stalling).
**L1 quote-based mode:**
When using `BookType.L1_MBP` (top-of-book quotes only), queue position tracking uses
trade ticks to decrement the queue (the same mechanism as L2/L3), while quote ticks
handle price-move detection and deferred snapshot resolution.
- **Trade ticks**: Trades at the order's price level decrement the queue ahead by the trade
size, identical to L2/L3 behavior. Only trades on the correct aggressor side affect the
queue (SELLER trades decrement queue for BUY orders, BUYER trades for SELL orders).
- **Price moves away**: If the bid drops below a BUY order's price (or ask rises above a
SELL order's price), the order's price level has been "crossed" and the queue clears to zero,
making the order fill-eligible on the next matching trade.
- **Price moves toward**: If the bid rises (or ask drops), the level at the order's price was
not consumed, so queue positions are preserved.
- **Price returns to a level**: When the price returns after moving away, the queue ahead is
capped at the new displayed size if it was previously larger.
- **Orders behind BBO (pending)**: When a limit order is placed behind the best bid/ask
(e.g., BUY below best bid), the queue snapshot is deferred because L1 data has no visible
depth at that level. Fills are blocked until the BBO reaches the order's price, at which
point the queue is snapshotted from the displayed size. Pending orders are also resolved
when trades cross through their price level.
L1 mode uses the same configuration: set `queue_position=True` with `book_type=BookType.L1_MBP`.
This provides a lightweight alternative to full L2/L3 data when only top-of-book quotes are
available.
:::note
Queue position tracking provides a heuristic simulation of queue dynamics. Real exchange queue
behavior depends on many factors (order priority rules, hidden orders, etc.) that cannot be
perfectly reconstructed from historical data.
:::
### Bar-based execution
Bar data provides a summary of market activity with four key prices for each time period (assuming bars are aggregated by trades):
- **Open**: opening price (first trade)
- **High**: highest price traded
- **Low**: lowest price traded
- **Close**: closing price (last trade)
While this gives us an overview of price movement, we lose some important information that we'd have with more granular data:
- We don't know in what order the market hit the high and low prices.
- We can't see exactly when prices changed within the time period.
- We don't know the actual sequence of trades that occurred.
This is why Nautilus processes bar data through a system that attempts to maintain
the most realistic yet conservative market behavior possible, despite these limitations.
At its core, the platform always maintains an order book simulation - even when you provide less
granular data such as quotes, trades, or bars (although the simulation will only have a top level book).
:::warning
When using bars for execution simulation (enabled by default with `bar_execution=True` in venue configurations),
Nautilus strictly expects the initialization timestamp (`ts_init`) of each bar to represent its **closing time**.
This ensures accurate chronological processing, prevents look-ahead bias, and aligns market updates (Open -> High -> Low -> Close) with the moment the bar is complete.
The event timestamp (`ts_event`) can represent either the open or close time of the bar:
- If `ts_event` is at the **close**, ensure `ts_init_delta=0` when processing bars (default).
- If `ts_event` is at the **open**, set `ts_init_delta` equal to the bar's duration to shift `ts_init` to the close.
:::
#### Bar timestamp convention
If your data source provides bars timestamped at the **opening time** (common in some providers), you need to ensure `ts_init` is set to the closing time for correct execution simulation. There are two approaches:
**Approach 1: Adjust data timestamps (recommended)**
- Use adapter-specific configurations like `bars_timestamp_on_close=True` (e.g., for Bybit or Databento adapters) to handle this automatically during data ingestion.
- For custom data, manually shift the timestamps by the bar duration before loading (e.g., add 1 minute for `1-MINUTE` bars).
- This approach is clearest because the data itself reflects the close time.
**Approach 2: Use `ts_init_delta` parameter**
- When calling `BarDataWrangler.process()`, set `ts_init_delta` to the bar's duration in nanoseconds (e.g., `60_000_000_000` for 1-minute bars).
- The wrangler computes `ts_init = ts_event + ts_init_delta`, shifting execution timing to the close.
- Use this when you cannot or prefer not to modify source data timestamps.
Always verify your data's timestamp convention with a small sample to avoid simulation inaccuracies. Incorrect timestamp handling can lead to look-ahead bias and unrealistic backtest results.
#### Processing bar data
Even when you provide bar data, Nautilus maintains an internal order book for each instrument, as a real venue would.
1. **Time processing**:
- Nautilus has a specific way of handling the timing of bar data *for execution* that's crucial for accurate simulation.
- The initialization timestamp (`ts_init`) is used for execution timing and must represent the close time of the bar. This approach is most logical because it represents the moment when the bar is fully formed and its aggregation is complete.
- The event timestamp (`ts_event`) represents when the data event occurred and may differ from `ts_init` depending on your data source:
- If your bars are timestamped at the **close** (the recommended default), use `ts_init_delta=0` in `BarDataWrangler` so that `ts_init = ts_event`.
- If your bars are timestamped at the **open**, set `ts_init_delta` to the bar's duration in nanoseconds (e.g., 60_000_000_000 for 1-minute bars) to shift `ts_init` to the close time.
- The platform ensures all events happen in the correct sequence based on `ts_init`, preventing any possibility of look-ahead bias in your backtests.
:::note[Exceptions for bar execution]
Bars will **not** be processed for execution (and will not update the order book) in the following cases:
- **Internally aggregated bars**: Bars with `AggregationSource.INTERNAL` are skipped to avoid processing bars that are derived from already-processed tick data.
- **Non-L1 book types**: When the venue's `book_type` is configured as `L2_MBP` or `L3_MBO`, bar data is ignored for execution processing, as bars are derived from top-of-book prices only.
In these cases, bars will still be received by strategies for analytics and decision-making, but they won't trigger order matching or update the simulated order book.
:::
2. **Price processing**:
- The platform converts each bar's OHLC prices into a sequence of market updates.
- By default, updates follow the order: Open -> High -> Low -> Close (configurable via `bar_adaptive_high_low_ordering`).
- If you provide multiple timeframes (like both 1-minute and 5-minute bars), the platform uses the more granular data for highest accuracy.
3. **Executions**:
- When you place orders, they interact with the simulated order book as they would on a real venue.
- For MARKET orders, execution happens at the current simulated market price plus any configured latency.
- For LIMIT orders working in the market, they'll execute if any of the bar's prices reach or cross your limit price (see below).
- The matching engine continuously processes orders as OHLC prices move, rather than waiting for complete bars.
#### OHLC prices simulation
During backtest execution, each bar is converted into a sequence of four price points:
1. Opening price
2. High price *(Order between High/Low is configurable. See `bar_adaptive_high_low_ordering` below.)*
3. Low price
4. Closing price
The trading volume for that bar is **split evenly** among these four points (25% each), with any
remainder added to the closing price trade to preserve total volume. In marginal cases, if the
bar's volume divided by 4 is less than the instrument's minimum `size_increment`, we use the
minimum `size_increment` per price point to ensure valid market activity (e.g., 1 contract for
CME group exchanges).
How these price points are sequenced can be controlled via the `bar_adaptive_high_low_ordering` parameter when configuring a venue.
Nautilus supports two modes of bar processing:
1. **Fixed ordering** (`bar_adaptive_high_low_ordering=False`, default)
- Processes every bar in a fixed sequence: `Open -> High -> Low -> Close`.
- Simple and deterministic approach.
2. **Adaptive ordering** (`bar_adaptive_high_low_ordering=True`)
- Uses bar structure to estimate likely price path:
- If Open is closer to High: processes as `Open -> High -> Low -> Close`.
- If Open is closer to Low: processes as `Open -> Low -> High -> Close`.
- [Research](https://gist.github.com/stefansimik/d387e1d9ff784a8973feca0cde51e363) shows this approach achieves ~75-85% accuracy in predicting correct High/Low sequence (compared to statistical ~50% accuracy with fixed ordering).
- This is particularly important when both take-profit and stop-loss levels occur within the same bar - as the sequence determines which order fills first.
Here's how to configure adaptive bar ordering for a venue, including account setup:
```python
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.model.enums import OmsType, AccountType
from nautilus_trader.model import Money, Currency
# Initialize the backtest engine
engine = BacktestEngine()
# Add a venue with adaptive bar ordering and required account settings
engine.add_venue(
venue=venue, # Your Venue identifier, e.g., Venue("BINANCE")
oms_type=OmsType.NETTING,
account_type=AccountType.CASH,
starting_balances=[Money(10_000, Currency.from_str("USDT"))],
bar_adaptive_high_low_ordering=True, # Enable adaptive ordering of High/Low bar prices
)
```
#### Order submission timing
Bar N's OHLC sequence processes before `on_bar(N)` fires. Without a `LatencyModel`,
an order submitted from `on_bar` settles immediately and matches against the current
book, whose top reflects bar N's close.
Attach a `LatencyModel` to the venue to defer the order's effective arrival. With
bar-only data and no intervening timer events, the order settles after the next bar's
OHLC sweep, so the fill price is that bar's close (or a later bar's close if latency
exceeds the bar interval). Finer-grained data (quotes, trades) or timer-driven
settlement between bars can drain the order earlier, against the book as it stands at
that point:
```python
from nautilus_trader.backtest.models import LatencyModel
engine.add_venue(
venue=venue,
oms_type=OmsType.NETTING,
account_type=AccountType.CASH,
starting_balances=[Money(10_000, Currency.from_str("USDT"))],
latency_model=LatencyModel(base_latency_nanos=1_000_000_000), # 1 second
)
```
:::note
A native "next-bar-open" execution mode is not provided. A bar's `ts_init` is its
close timestamp, so the open price is only known once the bar arrives. Filling at
that open from a signal generated on the prior bar would require look-ahead.
:::
### Internal bar aggregation timing
When aggregating time bars internally from tick data, the data engine uses timers to close bars at
interval boundaries. A timing edge case occurs when data arrives at the exact bar close timestamp: the
timer may fire before processing boundary data.
Configure `time_bars_build_delay` in `DataEngineConfig` to delay bar close timers:
```python
from nautilus_trader.config import BacktestEngineConfig
from nautilus_trader.data.config import DataEngineConfig
config = BacktestEngineConfig(
data_engine=DataEngineConfig(
time_bars_build_delay=1, # Microseconds
),
)
```
:::tip
A small delay (1 microsecond) ensures boundary data is processed before the bar closes.
Useful when tick data clusters at round interval timestamps.
:::
:::note
Only affects internally aggregated bars (`AggregationSource.INTERNAL`).
:::
### Timer-only backtests
The backtest engine supports running with timers but no market data. This is useful for scheduled
operations or testing timer-based logic. Timers fire in chronological order, and timer callbacks
can dynamically add data via `add_data_iterator()` which will be processed in sequence.
:::warning
Data added by timer callbacks at the exact start time should have timestamps **after** the start time.
The engine reads the first data point before processing start-time timers, so dynamically added data
with timestamps at or before the start time may not be processed in the expected order.
:::
### Fill models
Fill models simulate order execution dynamics during backtesting. They address a fundamental challenge:
*even with perfect historical market data, we can't fully simulate how orders may have interacted
with other market participants in real-time*.
The base `FillModel` provides probabilistic parameters for queue position and slippage simulation.
Subclasses can override `get_orderbook_for_fill_simulation()` to generate synthetic order books
for more sophisticated liquidity modeling.
#### Available fill models
| Model | Description | Use Case |
| ---------------------------- | ------------------------------------------------------- | -------------------------------------------- |
| `FillModel` | Base model with probabilistic fill/slippage parameters. | Simple queue position and slippage. |
| `BestPriceFillModel` | Fills at best price with unlimited liquidity. | Testing basic strategy logic optimistically. |
| `OneTickSlippageFillModel` | Forces exactly one tick of slippage on all orders. | Conservative slippage testing. |
| `TwoTierFillModel` | 10 contracts at best price, remainder one tick worse. | Basic market depth simulation. |
| `ThreeTierFillModel` | 50/30/20 contracts across three price levels. | More realistic depth simulation. |
| `ProbabilisticFillModel` | 50% chance best price, 50% chance one tick slippage. | Randomized execution quality. |
| `SizeAwareFillModel` | Different execution based on order size (≤10 vs >10). | Size‑dependent market impact. |
| `LimitOrderPartialFillModel` | Max 5 contracts fill per price touch. | Queue position via partial fills. |
| `MarketHoursFillModel` | Wider spreads during low liquidity periods. | Session‑aware execution. |
| `VolumeSensitiveFillModel` | Liquidity based on recent trading volume. | Volume‑adaptive depth. |
| `CompetitionAwareFillModel` | Only percentage of visible liquidity available. | Multi‑participant competition. |
#### Configuring fill models
**Using the base FillModel with probabilistic parameters:**
```python
from nautilus_trader.backtest.config import BacktestVenueConfig
from nautilus_trader.backtest.config import ImportableFillModelConfig
venue_config = BacktestVenueConfig(
name="SIM",
oms_type="NETTING",
account_type="CASH",
starting_balances=["100_000 USD"],
fill_model=ImportableFillModelConfig(
fill_model_path="nautilus_trader.backtest.models:FillModel",
config_path="nautilus_trader.backtest.config:FillModelConfig",
config={
"prob_fill_on_limit": 0.2, # Chance a limit order fills when price matches
"prob_slippage": 0.5, # Chance of 1-tick slippage (L1 data only)
"random_seed": 42, # Optional: Set for reproducible results
},
),
)
```
**Using an order book simulation model:**
```python
from nautilus_trader.backtest.config import BacktestVenueConfig
from nautilus_trader.backtest.config import ImportableFillModelConfig
venue_config = BacktestVenueConfig(
name="SIM",
oms_type="NETTING",
account_type="CASH",
starting_balances=["100_000 USD"],
fill_model=ImportableFillModelConfig(
fill_model_path="nautilus_trader.backtest.models:ThreeTierFillModel",
),
)
```
#### Probabilistic parameters (base FillModel)
**prob_fill_on_limit** (default: `1.0`)
Simulates queue position by controlling the probability of a limit order filling when its price level is touched (but not crossed).
- `0.0`: Never fills at touch (back of queue).
- `0.5`: 50% chance of filling (middle of queue).
- `1.0`: Always fills at touch (front of queue).
**prob_slippage** (default: `0.0`)
Simulates price slippage on each fill. Only applies to L1 data types (quotes, trades, bars) where real depth is unavailable. Affects all order types when executing as takers.
- `0.0`: No slippage (fills at best price).
- `0.5`: 50% chance of one tick slippage per fill.
- `1.0`: Always slips one tick.
#### Order book simulation models
These models override the `get_orderbook_for_fill_simulation()` method to generate synthetic order books
representing expected market liquidity. The matching engine fills orders against this simulated book.
**How it works:**
1. Before processing a fill, the matching engine calls `get_orderbook_for_fill_simulation()`.
2. If the model returns a synthetic order book, fills execute against that book's liquidity.
3. If the model returns `None`, standard fill logic applies.
:::note
When a custom fill model provides a simulated order book, the `liquidity_consumption` tracking is **not** applied.
Custom fill models are expected to manage their own liquidity simulation within the returned order book.
Liquidity consumption tracking only affects the built-in fill logic (when `get_orderbook_for_fill_simulation()` returns `None`).
:::
**Example: ThreeTierFillModel**
This model creates a book with liquidity distributed across three price levels:
- 50 contracts at best price
- 30 contracts one tick worse
- 20 contracts two ticks worse
A 100-contract market order would fill partially at each level, experiencing realistic price impact.
**Creating custom fill models:**
```python
from nautilus_trader.backtest.models import FillModel
from nautilus_trader.model.book import OrderBook, BookOrder
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.core.rust.model import BookType
class MyCustomFillModel(FillModel):
def get_orderbook_for_fill_simulation(
self,
instrument,
order,
best_bid,
best_ask,
):
book = OrderBook(
instrument_id=instrument.id,
book_type=BookType.L2_MBP,
)
# Add custom liquidity based on your market model
# ...
return book
```
### Precision requirements and invariants
The matching engine enforces strict precision invariants to ensure data integrity throughout the fill pipeline.
All prices and quantities must match the instrument's configured precision (`price_precision` and `size_precision`).
Mismatches raise a `RuntimeError` immediately, preventing silent corruption of fill quantities.
| Data/Operation | Field | Required Precision | Validation Location |
| -------------- | ------------------------------ | ---------------------------- | --------------------------- |
| `QuoteTick` | `bid_price`, `ask_price` | `instrument.price_precision` | `process_quote_tick` |
| `QuoteTick` | `bid_size`, `ask_size` | `instrument.size_precision` | `process_quote_tick` |
| `TradeTick` | `price` | `instrument.price_precision` | `process_trade_tick` |
| `TradeTick` | `size` | `instrument.size_precision` | `process_trade_tick` |
| `Bar` | `open`, `high`, `low`, `close` | `instrument.price_precision` | `process_bar` |
| `Bar` | `volume` (base units) | `instrument.size_precision` | `process_bar` |
| `Order` | `quantity` | `instrument.size_precision` | `process_order` |
| `Order` | `price` | `instrument.price_precision` | `process_order` |
| `Order` | `trigger_price` | `instrument.price_precision` | `process_order` |
| `Order` | `activation_price`\* | `instrument.price_precision` | `process_order` |
| Order update | `quantity` | `instrument.size_precision` | `update_order` |
| Order update | `price`, `trigger_price` | `instrument.price_precision` | `update_order` |
| Fill | `fill_qty` | `instrument.size_precision` | `apply_fills`, `fill_order` |
| Fill | `fill_px` | `instrument.price_precision` | `apply_fills` |
\*`activation_price` is immutable after order submission.
:::warning
`Bar.volume` must be in **base currency units**. Some data providers report quote-currency volume;
convert to base units before loading (divide by price or use provider-specific fields).
:::
:::tip
If you encounter a precision mismatch error, align your data to the instrument:
```python
# Align price/quantity to instrument precision
price = instrument.make_price(raw_price)
qty = instrument.make_qty(raw_qty)
```
Also verify that:
1. The instrument definition matches your data source's precision.
2. Data was not inadvertently rounded or truncated during loading.
3. Custom data loaders preserve the original precision metadata.
:::
## Accounts
Every backtest venue is attached with one of three `account_type` values:
`CASH`, `MARGIN`, or `BETTING`. For the full data model, query API, and margin
model reference, see [Accounting](accounting.md).
Example of adding a `CASH` account for a backtest venue:
```python
from nautilus_trader.adapters.binance import BINANCE_VENUE
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.model.currencies import USDT
from nautilus_trader.model.enums import OmsType, AccountType
from nautilus_trader.model import Money, Currency
# Initialize the backtest engine
engine = BacktestEngine()
# Add a CASH account for the venue
engine.add_venue(
venue=BINANCE_VENUE, # Create or reference a Venue identifier
oms_type=OmsType.NETTING,
account_type=AccountType.CASH,
starting_balances=[Money(10_000, USDT)],
)
```
## Margin models
Margin models determine how the simulated exchange reserves collateral for
orders and positions in backtest runs. The model types (`StandardMarginModel`
vs `LeveragedMarginModel`), their formulas, the default behavior, and custom
model authoring are covered in the dedicated
[Accounting](accounting.md#margin-models) guide.
This section covers only the backtest-specific configuration.
### Backtest venue configuration
Specify the margin model on `BacktestVenueConfig` via `MarginModelConfig`:
```python
from nautilus_trader.backtest.config import BacktestVenueConfig
from nautilus_trader.backtest.config import MarginModelConfig
venue_config = BacktestVenueConfig(
name="SIM",
oms_type="NETTING",
account_type="MARGIN",
starting_balances=["1_000_000 USD"],
margin_model=MarginModelConfig(model_type="standard"), # Options: 'standard', 'leveraged'
)
```
Available `model_type` values:
- `"leveraged"`: margin reduced by leverage (default).
- `"standard"`: fixed percentages (traditional brokers).
- Fully-qualified class path for a custom model:
`"my_package.my_module:MyMarginModel"`.
### High-level backtest API
When using the high-level API, attach the margin model in the same way:
```python
from nautilus_trader.backtest.config import BacktestVenueConfig
from nautilus_trader.backtest.config import MarginModelConfig
from nautilus_trader.config import BacktestRunConfig
venue_config = BacktestVenueConfig(
name="SIM",
oms_type="NETTING",
account_type="MARGIN",
starting_balances=["1_000_000 USD"],
margin_model=MarginModelConfig(
model_type="standard", # Traditional broker simulation
),
)
config = BacktestRunConfig(
venues=[venue_config],
# ... other config
)
```
Custom model with parameters:
```python
margin_model=MarginModelConfig(
model_type="my_package.my_module:CustomMarginModel",
config={
"risk_multiplier": 1.5,
"use_leverage": False,
"volatility_threshold": 0.02,
},
)
```
The model is applied to the simulated exchange during backtest execution.
## Trade ID derivation
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).
**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.
## Related guides
- [Strategies](strategies.md) - Develop strategies to backtest.
- [Visualization](visualization.md) - Generate tearsheets from backtest results.
- [Reports](reports.md) - Analyze backtest performance data.
# Cache
Source: https://nautilustrader.io/docs/latest/concepts/cache/
The `Cache` is a central in-memory database that stores and manages all trading-related data,
from market data to order history to custom calculations.
The Cache serves multiple purposes:
1. **Stores market data**:
- Stores recent market history (e.g., order books, quotes, trades, bars).
- Gives you access to both current and historical market data for your strategy.
2. **Tracks trading data**:
- Maintains complete `Order` history and current execution state.
- Tracks all `Position`s and `Account` information.
- Stores `Instrument` definitions and `Currency` information.
3. **Stores custom data**:
- You can store any user-defined objects or data in the `Cache` for later use.
- Enables data sharing between different strategies.
## How caching works
**Built-in types**:
- The system automatically adds data to the `Cache` as it flows through.
- In live contexts, the engine applies updates asynchronously, so you might see a brief delay between an event and its appearance in the `Cache`.
- For quotes, trades, and bars the `DataEngine` writes to the `Cache` before publishing to subscribers, so the latest value is available in the cache by the time your handler runs. Order book deltas and depth snapshots are published directly without a cache write; book state is maintained separately through `BookUpdater` subscriptions:
```mermaid
flowchart LR
data[Data]
engine[DataEngine]
cache[Cache]
callback["Strategy callback: on_quote_tick(...)"]
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, you can access the `Cache` through `self.cache`. Here’s a typical example:
:::note
Within a `Strategy` class, `self` refers to the strategy instance.
:::
```python
def on_bar(self, bar: Bar) -> None:
# Current bar is provided in the parameter 'bar'
# Get historical bars from Cache
last_bar = self.cache.bar(self.bar_type, index=0) # Last bar (practically the same as the 'bar' parameter)
previous_bar = self.cache.bar(self.bar_type, index=1) # Previous bar
third_last_bar = self.cache.bar(self.bar_type, index=2) # Third last bar
# Get current position information
if self.last_position_opened_id is not None:
position = self.cache.position(self.last_position_opened_id)
if position.is_open:
# Check position details
current_pnl = position.unrealized_pnl
# Get all open orders for our instrument
open_orders = self.cache.orders_open(instrument_id=self.instrument_id)
```
## Configuration
Use the `CacheConfig` class to configure the `Cache` behavior and capacity.
You can provide this configuration either to a `BacktestEngine` or a `TradingNode`, depending on your [environment context](architecture.md#environment-contexts).
Here's a basic example of configuring the `Cache`:
```python
from nautilus_trader.config import CacheConfig, BacktestEngineConfig, TradingNodeConfig
# 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 = TradingNodeConfig(
cache=CacheConfig(
tick_capacity=10_000,
bar_capacity=5_000,
),
)
```
:::tip
By default, the `Cache` keeps the last 10,000 bars for each bar type and 10,000 trade ticks per instrument.
These limits provide a good balance between memory usage and data availability. Increase them if your strategy needs more historical data.
:::
### 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 separate capacity. For example, if you're using 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
For persistence between system restarts, you can configure a database backend.
`CacheConfig` controls cache behavior only. Connection settings belong to the concrete cache
database technology config, such as `RedisCacheConfig` or `PostgresCacheConfig`.
When is it useful to use persistence?
- **Long-running systems**: If you want your data to survive system restarts, upgrading, or unexpected failures, having a database configuration helps to pick up exactly where you left off.
- **Historical insights**: When you need to preserve past trading data for detailed post-analysis or audits.
- **Multi-node or distributed setups**: If multiple services or nodes need to access the same
state, a persistent store helps ensure shared and consistent data.
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?;
```
## Using the cache
### Accessing market data
The `Cache` provides a full interface for accessing order books, quotes, trades, and bars.
All market data in the cache uses reverse indexing, so the most recent entry sits at index 0.
#### Bar access
```python
# Get a list of all cached bars for a bar type
bars = self.cache.bars(bar_type) # Returns list[Bar] or an empty list if no bars found
# Get the most recent bar
latest_bar = self.cache.bar(bar_type) # Returns Bar or None if no such object exists
# Get a specific historical bar by index (0 = most recent)
second_last_bar = self.cache.bar(bar_type, index=1) # Returns Bar or None if no such object exists
# Check if bars exist and get count
bar_count = self.cache.bar_count(bar_type) # Returns number of bars in cache for the specified bar type
has_bars = self.cache.has_bars(bar_type) # Returns bool indicating if any bars exist for the specified bar type
```
#### Quote ticks
```python
# Get quotes
quotes = self.cache.quote_ticks(instrument_id) # Returns list[QuoteTick] or an empty list if no quotes found
latest_quote = self.cache.quote_tick(instrument_id) # Returns QuoteTick or None if no such object exists
second_last_quote = self.cache.quote_tick(instrument_id, index=1) # Returns QuoteTick or None if no such object exists
# Check quote availability
quote_count = self.cache.quote_tick_count(instrument_id) # Returns the number of quotes in cache for this instrument
has_quotes = self.cache.has_quote_ticks(instrument_id) # Returns bool indicating if any quotes exist for this instrument
```
#### Trade ticks
```python
# Get trades
trades = self.cache.trade_ticks(instrument_id) # Returns list[TradeTick] or an empty list if no trades found
latest_trade = self.cache.trade_tick(instrument_id) # Returns TradeTick or None if no such object exists
second_last_trade = self.cache.trade_tick(instrument_id, index=1) # Returns TradeTick or None if no such object exists
# Check trade availability
trade_count = self.cache.trade_tick_count(instrument_id) # Returns the number of trades in cache for this instrument
has_trades = self.cache.has_trade_ticks(instrument_id) # Returns bool indicating if any trades exist
```
#### Order book
```python
# Get current order book
book = self.cache.order_book(instrument_id) # Returns OrderBook or None if no such object exists
# Check if order book exists
has_book = self.cache.has_order_book(instrument_id) # Returns bool indicating if an order book exists
# Get count of order book updates
update_count = self.cache.book_update_count(instrument_id) # Returns the number of updates received
```
#### Price access
```python
from nautilus_trader.core.rust.model import PriceType
# Get 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.core.rust.model import PriceType, AggregationSource
# 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
class MarketDataStrategy(Strategy):
def on_start(self):
# Subscribe to 1-minute bars
self.bar_type = BarType.from_str(f"{self.instrument_id}-1-MINUTE-LAST-EXTERNAL") # example of instrument_id = "EUR/USD.FXCM"
self.subscribe_bars(self.bar_type)
def on_bar(self, bar: Bar) -> None:
bars = self.cache.bars(self.bar_type)[:3]
if len(bars) < 3: # Wait until we have at least 3 bars
return
# Access last 3 bars for analysis
current_bar = bars[0] # Most recent bar
prev_bar = bars[1] # Second to last bar
prev_prev_bar = bars[2] # Third to last bar
# Get latest quote and trade
latest_quote = self.cache.quote_tick(self.instrument_id)
latest_trade = self.cache.trade_tick(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 all trading objects within the system, including:
- Orders
- Positions
- Accounts
- Instruments
#### Orders
You can access and query orders through multiple methods, with flexible filtering options by venue, strategy, instrument, and 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` maintains a record of all positions and offers 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 and currencies
##### 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 filtered instruments
venue_instruments = self.cache.instruments(venue=venue) # Instruments for a specific venue
instruments_by_underlying = self.cache.instruments(underlying="ES") # Instruments by underlying
# 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 every per-instrument
map (order book, quotes, trades, mark/index/funding prices, instrument status, 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.
```python
class HousekeepingStrategy(Strategy):
def on_start(self) -> None:
# Drop instruments that are no longer in the watchlist.
for instrument_id in self.cache.instrument_ids(venue=self.venue):
if instrument_id not in self.watchlist:
self.cache.purge_instrument(instrument_id)
```
:::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
`LiveExecEngineConfig` schedules the bulk purges on a timer. Set the interval to enable
the loop and the buffer or lookback to control how recent entries are protected. The
following defaults work well for most live sessions:
```python
from nautilus_trader.config import LiveExecEngineConfig
exec_engine = LiveExecEngineConfig(
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 60-minute buffer keeps recent activity available for reconciliation while still
trimming long-tail growth. Tune these down for HFT sessions and up if you need longer
historical lookbacks for analytics. 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` can also store and retrieve custom data types in addition to built-in market data and trading objects.
Use it to share any user-defined data between system components, primarily actors and strategies.
#### Basic storage and retrieval
```python
# Call this code inside Strategy methods (`self` refers to Strategy)
# Store data
self.cache.add(key="my_key", value=b"some binary data")
# Retrieve data
stored_data = self.cache.get("my_key") # Returns bytes or None
```
For more complex use cases, the `Cache` can store custom data objects that inherit from the `nautilus_trader.core.Data` base class.
:::warning
The `Cache` is not designed to be a full database replacement. For large datasets or complex querying needs, consider using a dedicated database system.
:::
## Best practices and common questions
### Cache vs. portfolio usage
The `Cache` and `Portfolio` components serve different but complementary purposes in NautilusTrader:
**Cache**:
- Maintains the historical knowledge and current state of the trading system.
- Updates immediately when local state changes (for example, initializing an order before submission).
- Updates asynchronously as external events occur (for example, when an order fills).
- Provides a complete history of trading activity and market data.
- Keeps every event the strategy receives in the cache.
**Portfolio**:
- Aggregates position, exposure, and account information.
- Provides current state without history.
**Example**:
```python
class MyStrategy(Strategy):
def on_position_changed(self, event: PositionEvent) -> None:
# Use Cache when you need historical perspective
position_history = self.cache.position_snapshots(event.position_id)
# Use Portfolio when you need current real-time state
current_exposure = self.portfolio.net_exposure(event.instrument_id)
```
### Cache vs. strategy variables
Choosing between storing data in the `Cache` versus strategy variables depends on your specific needs:
**Cache storage**:
- Use for data that needs to be shared between strategies.
- Best for data that needs to persist between system restarts.
- Acts as a central database accessible to all components.
- Ideal for state that needs to survive strategy resets.
**Strategy variables**:
- Use for strategy-specific calculations.
- Better for temporary values and intermediate results.
- Provides faster access and better encapsulation.
- Best for data that only your strategy needs.
**Example**:
The following example shows how you might store data in the `Cache` so multiple strategies can access the same information.
```python
import pickle
class MyStrategy(Strategy):
def on_start(self):
# Prepare data you want to share with other strategies
shared_data = {
"last_reset": self.clock.timestamp_ns(),
"trading_enabled": True,
# Include any other fields that you want other strategies to read
}
# Store it in the cache with a descriptive key
# This way, multiple strategies can call self.cache.get("shared_strategy_info")
# to retrieve the same data
self.cache.add("shared_strategy_info", pickle.dumps(shared_data))
```
Another strategy can retrieve the cached data as follows:
```python
import pickle
class AnotherStrategy(Strategy):
def on_start(self):
# Load the shared data from the same key
data_bytes = self.cache.get("shared_strategy_info")
if data_bytes is not None:
shared_data = pickle.loads(data_bytes)
self.log.info(f"Shared data retrieved: {shared_data}")
```
## Related guides
- [Data](data.md) - 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 structs throughout the platform.
Each component (data clients, execution clients, engines, strategies) has a
dedicated config struct that controls its behavior.
## Design principles
### Defaults resolve at the config boundary
Config structs carry concrete values for fields that always have a sensible default.
Timeouts, retry counts, backoff delays, and heartbeat intervals are plain types like
`u64` or `u32` with defaults baked in. Downstream code receives resolved values and
does not repeat defaulting logic.
### Option means semantic absence, not "use default"
`Option` fields appear only when `None` carries real meaning: a feature is off,
a lookback window is unbounded, or a value is inherited from the environment at
runtime. If a field always resolves to a concrete value, it is not wrapped in `Option`.
This distinction makes config semantics visible in the type. A plain `u64` field
always has a value. An `Option` field might be absent, and the code that consumes
it will branch on that.
### Single source of truth for defaults
Each config struct uses `bon::Builder` to define defaults in one place via
`#[builder(default = value)]` annotations. The `Default` impl delegates to the builder
(`Self::builder().build()`), so there is no second copy of default values that could
drift out of sync.
### Config decoding fails on unknown fields
Config decoding fails fast on unknown fields. Nautilus treats extra keys as bugs, not
as harmless input. This catches misspellings, stale names after config renames, and
copy-paste mistakes before a node or client starts with the wrong settings.
## Python configs
Python config classes (msgspec structs) accept `None` for optional parameters.
For plain `T` fields, `None` means "use the default." For `Option` fields,
`None` preserves the field's optional meaning (disabled, unbounded, etc.).
All Python config classes inherit from `NautilusConfig`, which sets
`forbid_unknown_fields=True` on the underlying `msgspec.Struct`. Unknown keys now raise
`msgspec.ValidationError` during decoding.
```python
from nautilus_trader.adapters.bybit.config import BybitDataClientConfig
# All defaults: 60s timeout, 3 retries, etc.
config = BybitDataClientConfig()
# Override just the timeout
config = BybitDataClientConfig(http_timeout_secs=30)
# Disable instrument status polling
config = BybitDataClientConfig(instrument_status_poll_secs=None)
```
## Rust configs
All config structs derive [`bon::Builder`](https://bon-rs.com), which generates
a type-safe builder with compile-time checks for required fields. Fields with
`#[builder(default = value)]` can be omitted from the builder call and will
use their declared default. Three equivalent ways to construct a config:
Rust config structs that deserialize with Serde also set
`#[serde(deny_unknown_fields)]`. Unknown keys now fail deserialization instead of being
ignored.
```rust
// Builder: only set what differs from defaults
let config = BybitDataClientConfig::builder()
.http_timeout_secs(30)
.build();
// Struct literal with default spread
let config = BybitDataClientConfig {
http_timeout_secs: 30,
..Default::default()
};
// Full defaults
let config = BybitDataClientConfig::default();
```
All three produce identical results for unspecified fields.
## Common config fields
Most adapter configs share a common set of fields:
| Field | 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` | varies | WebSocket keepalive interval. |
| `recv_window_ms` | `u64` | varies | Signed request expiry window. |
| `update_instruments_interval_mins` | varies | varies | Periodic instrument refresh. |
Adapter-specific fields (rate limits, polling intervals, margin modes) are
documented in each adapter's integration guide.
## Engine configs
Engine configs (`LiveExecEngineConfig`, `DataEngineConfig`, etc.) follow the same
pattern. Fields like `reconciliation`, `inflight_check_interval_ms`, and
`open_check_threshold_ms` are plain types with builder defaults. Genuinely optional
features use `Option`:
```python
from nautilus_trader.config import LiveExecEngineConfig
config = LiveExecEngineConfig(
reconciliation=True,
open_check_interval_secs=30.0, # Enable open order polling
open_check_lookback_mins=60, # Look back 60 minutes
# position_check_interval_secs=None # Disabled by default
)
```
# 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; the continuous series stays active by
rolling to the next contract at a transition point and shifting historical prices into the new
contract's frame so the resulting series has no roll-induced jumps.
Nautilus models a continuous future as a target `BarType` plus an explicit list of roll
transitions supplied in request or subscribe params. The data engine walks per-contract segments,
computes the cumulative price adjustment per segment, and feeds 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 | Most recent contract |
| `FORWARD_SPREAD` | Additive | First contract |
| `BACKWARD_RATIO` | Multiplicative | Most recent contract |
| `FORWARD_RATIO` | Multiplicative | First contract |
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": ContinuousFutureAdjustmentType.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",
}
```
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 the active portion of the transition table:
- `last_post_instrument_id` caps the upper end at the first transition whose `post_instrument_id`
matches. Backward modes use this as the anchor (cumulative adjustment for the anchor segment
is zero); forward modes use it to cap how far later contracts accumulate.
- `first_pre_instrument_id` caps the lower end at the first transition whose `pre_instrument_id`
matches. Forward modes use this as the anchor; backward modes use it to cap how far earlier
contracts accumulate.
This lets callers pass a wide transition table while anchoring the adjusted series to a specific
contract on either side.
## Validation
The Rust request path (`crates/data/src/engine/requests.rs`) and the Cython request and
subscription paths (`engine.pyx::_continuous_future_validate_transitions`) validate transition
params before allocating any aggregator:
- `continuous_future_adjustment_mode` must parse as a valid `ContinuousFutureAdjustmentType`.
- `continuous_future_transitions` must be a list or tuple of dict 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`.
On validation failure the Rust request returns an error before it allocates child segment state.
The Cython request handler calls `_abort_request` to drop workflow state it had begun to set up;
the Cython subscription path logs a specific error and returns.
## 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, the Rust request path and the Cython 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 --> OuterReq[Outer loop: segments]
SubPath --> OuterSub[Outer loop: segments + time alerts]
OuterReq -->|per segment| SubReq[Inner request for segment contract]
OuterSub -->|per segment| LiveSub[Inner subscribe for segment contract]
SubReq --> Agg[(Primary aggregator BarBuilder.set_adjustment)]
LiveSub --> Agg2[(Live aggregator BarBuilder.set_adjustment)]
Agg -->|Rust request path| ReqAgg[(Request-scoped aggregator chain)]
Agg -->|Cython request path| Chain[Cython chain aggregators]
Agg2 -->|adjusted bars| MsgBus[(msgbus: data.bars.*)]
Chain -->|final bars| PipelineBus[(Cython msgbus: data.pipeline.bars.*)]
```
The design has two entry points, one outer loop shape (walk the segments), two ways to fetch
per-segment data (historical sub-requests or live sub-subscriptions), and one adjustment
mechanism (`BarBuilder.set_adjustment` at each segment boundary).
## 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 and subscription paths return the next segment starting at `cursor_ns`, clamped to
`end_ns`.
## Request flow
The request path mirrors `_handle_long_request` one level up: each iteration fires one inner
request for one segment's worth of data, and the inner's completion callback advances the cursor.
```mermaid
sequenceDiagram
participant User
participant Engine as DataEngine
participant Agg as Primary aggregator
participant Client as DataClient
User->>Engine: request(RequestBars w/ transitions)
Engine->>Agg: init aggregators, set cursor
loop one iteration per segment
Engine->>Agg: BarBuilder.set_adjustment(offset, mode)
Engine->>Client: inner Request_ for segment contract
Client-->>Engine: DataResponse
Engine->>Agg: route child response through request-scoped aggregation
Engine->>Engine: advance cursor
end
Engine->>User: terminal parent response
```
If the caller sets `time_range_generator` and `durations_seconds` in params, the inner request
inherits them and itself becomes a long request that chunks the segment's time range into N
further sub-sub-requests. The outer continuous-future loop ignores the inner chunking: each
inner request still emits exactly one combined response back, which triggers the outer loop's
next segment.
### Chain aggregators
If the caller sets `bar_types = (bar_type_1, bar_type_2)` for multi-level internal aggregation,
the setup creates all aggregators keyed by `parent.id`. The Rust request path routes segment
source responses into the primary continuous target, then forwards emitted bars to matching
request-scoped downstream aggregators. The Cython path wires pipeline topics between levels so
the chain walks up automatically. Only the primary builder has `set_adjustment` called on it;
higher levels re-aggregate already adjusted data in both paths.
## 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),
activates the next segment (resolves the new source, applies the new offset, subscribes), and
re-arms the timer for the next 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)"]
```
## BarBuilder adjustment
The builder applies adjustment **at ingress** on every `update(price, ...)` and
`update_bar(bar, ...)` call. The running OHLC state always sits in the adjusted (common) frame,
so a mid-bar adjustment change only affects subsequent prices. No partial-bar buffering is
needed.
```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` only cares about the ratio-vs-spread distinction to decide add-or-multiply.
The engine collapses direction information into the sign and magnitude of the cumulative offset
before calling `set_adjustment`. The `reset()` method clears per-bar OHLCV state for the next
bar in the series but intentionally preserves the adjustment configuration: rolls happen far
less often than bar resets, so the adjustment is treated as segment-scoped state.
## 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. This is the intentional policy: rewriting
the running OHLC at every roll would require buffering raw input per segment, which adds cost
without changing the adjusted result for the common case where the adjusted segment is built
seamlessly across the boundary.
## 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 goes through `float` in the hot path (`price_as_f64 * ratio` then
`price_new`). For high-precision instruments the rounding can shift the resulting raw by 1
ULP versus the equivalent `Decimal` multiplication. Spread mode is exact because it works
directly on `PriceRaw` (int64/int128).
# Custom Data
Source: https://nautilustrader.io/docs/latest/concepts/custom_data/
Nautilus Trader supports custom data authored in Python and Rust, and moves
that data through the same runtime, persistence, and query pipeline used
by the rest of the platform.
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 | Example | Registration path | Encode/decode path | Wrapper backend |
|-------------------|----------------------------------------------------|---------------------------------------------------------|---------------------------------|---------------------------|
| Pure Python | `@customdataclass_pyo3` class | `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 native extractor | 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 Global DataRegistry
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/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
### `DataRegistry`
`crates/model/src/data/registry.rs` is the central runtime registry module for
custom data in the main process. Registration uses atomic `DashMap::entry()` so
that concurrent `register_*` and `ensure_*` calls do not race.
The module contains several `OnceLock`-initialized `DashMap` singletons:
- 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, Nautilus 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` exposes value semantics: `__eq__` and
`__repr__` are implemented (equality uses the Rust `PartialEq` logic).
Instances are intentionally unhashable so that equality remains consistent with
the inner 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 (e.g. for `to_json_bytes` / `from_json_bytes`, SQL
cache, or Redis), `CustomData` uses a single canonical envelope so that
deserialization 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 receive only this value in
`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
`DataRegistry` 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` (used only for catalog pathing, not for routing or
equality).
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` affects only the storage path under
`data/custom//`.
Custom-data storage and queries use `DataType`, not just the bare Rust/Python
class name. This allows the same logical type to be stored under different
metadata or identifiers while still decoding 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[Main-process DataRegistry]
J --> L
```
### Pure Python registration
When Python code calls `register_custom_data_class(MyType)`:
1. The type is registered in the Python serialization layer for JSON and Arrow
support.
2. Rust registers a Python extractor that wraps Python instances as
`PythonCustomDataWrapper`.
3. Rust registers Arrow schema/encode/decode callbacks in `DataRegistry`.
This path is flexible and user-friendly, but Arrow encoding and reconstruction
rely on Python callbacks.
### Same-binary Rust registration
For Rust types defined inside Nautilus:
1. `#[custom_data]` or `#[custom_data(pyo3)]` generates the necessary trait,
JSON, and Arrow implementations.
2. `ensure_custom_data_registered::()` inserts native schema/encoder/decoder
handlers into `DataRegistry`.
3. For PyO3-exposed types, a native extractor can convert Python instances back
into the concrete Rust type rather than a Python fallback wrapper.
This path stays fully native in Rust for encode/decode.
### Registration precedence
`register_custom_data_class(...)` resolves types in the following order:
1. Same-binary native Rust registration.
2. Pure Python fallback registration.
That ordering preserves the fastest available path for types already known
natively by the main binary.
## 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`.
- Calls Python methods for JSON and Arrow-related operations under the GIL.
This is the fallback path when the main process does not have a native Rust
representation for the type.
### Native same-binary Rust payload
For Rust types compiled into Nautilus, the inner payload is the concrete Rust
type itself 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 Nautilus 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. Extracts `type_name`, `metadata`, and `identifier` from `DataType`.
2. Looks up the Arrow encoder in `DataRegistry`.
3. Encodes the values to a `RecordBatch`.
4. Appends a `data_type` column containing the persisted `DataType`.
5. Attaches `type_name` and metadata to the Arrow schema.
6. 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.
2. Extracts `type_name` from schema metadata.
3. Asks `DataRegistry` for the registered decoder.
4. Decodes the `RecordBatch` into `Vec`.
5. Reconstructs `CustomData` with the original `DataType`.
This makes custom-data query resolution symmetric with write-time registration.
When converting a Feather stream to Parquet (e.g. after a backtest), the
custom-data branch decodes batches and writes them via
`write_custom_data_batch` so that custom data written through the Feather
writer is correctly converted to Parquet.
## The Arrow C FFI bridge
Pure Python custom data cannot provide native Rust Arrow encode logic directly.
For those types, Nautilus uses the Arrow C FFI interface to pass `RecordBatch`
data between Python and Rust without serialization overhead.
```mermaid
sequenceDiagram
participant R as Rust encoder
participant P as Python custom class
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.
2. Rust calls `encode_record_batch_py(...)` on the Python class.
3. Python converts objects to a `pyarrow.RecordBatch`.
4. Python exports the batch via `_export_to_c` into Arrow C FFI structs.
5. 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.
2. Python imports the batch via `RecordBatch._import_from_c`.
3. Python calls `decode_record_batch_py(metadata, batch)` on the class.
4. 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 Python class using
`from_dict` or `from_json`.
In all cases the caller receives the same outer `CustomData` wrapper at the
PyO3 API boundary.
## Runtime integration
Custom data is not only a persistence feature. It also participates in Nautilus
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 other data families.
## SQL cache and database integration
The SQL cache/database layer also supports `CustomData`.
Current behavior:
- 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.
## Cython custom data
The Cython `@customdataclass` system is separate from this architecture.
This document describes the PyO3 custom-data system:
- PyO3 `CustomData`.
- Dynamic runtime registration.
- Arrow/Parquet persistence.
- Native Rust execution paths.
## Practical implications
This architecture gives Nautilus two important properties:
1. Python-first extensibility for users who only want to write Python.
2. Native Rust performance for built-in or compiled custom types.
The result is one conceptual custom-data system with two backends, rather than
separate feature silos for Python-only and Rust-only data types.
# Data
Source: https://nautilustrader.io/docs/latest/concepts/data/
Common built-in data types include:
- `OrderBookDelta` (L1/L2/L3): Represents the most granular order book updates.
- `OrderBookDeltas` (L1/L2/L3): Batches multiple order book deltas for more efficient processing.
- `OrderBookDepth10`: Aggregated order book snapshot (up to 10 levels per bid and ask side).
- `QuoteTick`: Represents the best bid and ask prices along with their sizes at the top-of-book.
- `TradeTick`: A single trade/match event between counterparties.
- `Bar`: OHLCV (Open, High, Low, Close, Volume) bar/candle, aggregated using a specified *aggregation method*.
- `MarkPriceUpdate`: The current mark price for an instrument (typically used in derivatives trading).
- `IndexPriceUpdate`: The index price for an instrument (underlying price used for mark price calculations).
- `FundingRateUpdate`: The funding rate for perpetual contracts (periodic payments between long and short positions).
- `InstrumentStatus`: An instrument-level status event.
- `InstrumentClose`: The closing price of an instrument.
See the API reference for the full set of built-in data classes and wrappers.
NautilusTrader operates primarily on granular order book data for the highest realism
in execution simulations. Backtests can also run on any supported market data type,
depending on the desired simulation fidelity.
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 high-performance order book implemented in Rust is available to maintain order book state based on provided data.
`OrderBook` instances are maintained per instrument for both backtesting and live trading, with the following book types available:
- `L3_MBO`: **Market by order (MBO)** or L3 data, uses every order book event at every price level, keyed by order ID.
- `L2_MBP`: **Market by price (MBP)** or L2 data, aggregates order book events by price level.
- `L1_MBP`: **Market by price (MBP)** or L1 data, also known as best bid and offer (BBO), captures only top-level updates.
:::note
Top-of-book data, such as `QuoteTick`, `TradeTick` and `Bar`, can also be used for backtesting, with markets operating on `L1_MBP` book types.
:::
### 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
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) is a data structure that represents
price and volume information over a specific period, including:
- Opening price
- Highest price
- Lowest price
- Closing price
- Traded volume (or ticks as a volume proxy)
The system generates bars using an *aggregation method* that groups data by specific criteria.
### Purpose of data aggregation
Data aggregation in NautilusTrader transforms granular market data into structured bars or candles for several reasons:
- To provide data for technical indicators and strategy development.
- Because time-aggregated data (like minute bars) are often sufficient for many strategies.
- To reduce costs compared to high-frequency L1/L2/L3 market data.
### Aggregation methods
The platform implements various aggregation methods:
| Name | Description | Category |
|:-------------------|:---------------------------------------------------------------------------|:-------------|
| `TICK` | Aggregation of a number of ticks. | Threshold |
| `TICK_IMBALANCE` | Aggregation of the buy/sell imbalance of ticks. | Threshold |
| `TICK_RUNS` | Aggregation of sequential buy/sell runs of ticks. | Information |
| `VOLUME` | Aggregation of traded volume. | Threshold |
| `VOLUME_IMBALANCE` | Aggregation of the buy/sell imbalance of traded volume. | Threshold |
| `VOLUME_RUNS` | Aggregation of sequential runs of buy/sell traded volume. | Information |
| `VALUE` | Aggregation of the notional value of trades (also known as "Dollar bars"). | Threshold |
| `VALUE_IMBALANCE` | Aggregation of the buy/sell imbalance of trading by notional value. | Threshold |
| `VALUE_RUNS` | Aggregation of sequential buy/sell runs of trading by notional value. | Information |
| `RENKO` | Aggregation based on fixed price movements (brick size in ticks). | Threshold |
| `MILLISECOND` | Aggregation of time intervals with millisecond granularity. | Time |
| `SECOND` | Aggregation of time intervals with second granularity. | Time |
| `MINUTE` | Aggregation of time intervals with minute granularity. | Time |
| `HOUR` | Aggregation of time intervals with hour granularity. | Time |
| `DAY` | Aggregation of time intervals with day granularity. | Time |
| `WEEK` | Aggregation of time intervals with week granularity. | Time |
| `MONTH` | Aggregation of time intervals with month granularity. | Time |
| `YEAR` | Aggregation of time intervals with year granularity. | Time |
### 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. The bar closes
when the absolute imbalance reaches the configured step. This means that opposing trades cancel each
other out, so imbalance bars tend to 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 (each trade counts as 1) |
| Volume | `VOLUME_IMBALANCE` | `VOLUME_RUNS` | Traded volume (quantity) |
| Value | `VALUE_IMBALANCE` | `VALUE_RUNS` | Notional value (price x quantity) |
:::note
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 implements three distinct data aggregation methods:
1. **Trade-to-bar aggregation**: Creates bars from `TradeTick` objects (executed trades)
- Use case: For strategies analyzing execution prices or when working directly with trade data.
- Always uses the `LAST` price type in the bar specification.
2. **Quote-to-bar aggregation**: Creates bars from `QuoteTick` objects (bid/ask prices)
- Use case: For strategies focusing on bid/ask spreads or market depth analysis.
- Uses `BID`, `ASK`, or `MID` price types in the bar specification.
3. **Bar-to-bar aggregation**: Creates larger-timeframe `Bar` objects from smaller-timeframe `Bar` objects
- Use case: For resampling existing smaller timeframe bars (1-minute) into larger timeframes (5-minute, hourly).
- Always requires the `@` symbol in the specification.
### Bar types
NautilusTrader defines a unique *bar type* (`BarType` class) based on the following components:
- **Instrument ID** (`InstrumentId`): Specifies the particular instrument for the bar.
- **Bar Specification** (`BarSpecification`):
- `step`: Defines the interval or frequency of each bar.
- `aggregation`: Specifies the method used for data aggregation (see the above table).
- `price_type`: Indicates the price basis of the bar (e.g., bid, ask, mid, last).
- **Aggregation Source** (`AggregationSource`): Indicates whether the bar was aggregated internally (within Nautilus)
or externally (by a trading venue or data provider).
:::note
`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; and `MONTH` steps must divide 12 and be less than 12. Use the next larger
aggregation when the step equals a parent unit, such as `1-HOUR` instead of `60-MINUTE`.
`DAY`, `WEEK`, `YEAR`, threshold, information, and `RENKO` bars are not restricted by this
fixed-subunit rule.
A future version will allow advanced users to override this validation for arbitrary bar periods
that do not align to clock or calendar boundaries.
:::
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 higher-granularity bar type through subsampling (like 5-MINUTE bars aggregate from 1-MINUTE bars).
### Aggregation sources
Bar data aggregation can be either *internal* or *external*:
- `INTERNAL`: The bar is aggregated inside the local Nautilus system boundary.
- `EXTERNAL`: The bar is aggregated outside the local Nautilus system boundary (typically by a trading venue or data provider).
For bar-to-bar aggregation, the target bar type is always `INTERNAL` (since you're doing the aggregation within NautilusTrader),
but the source bars can be either `INTERNAL` or `EXTERNAL`, i.e., you can aggregate externally provided bars or already
aggregated internal bars.
### Defining bar types with *string syntax*
#### Standard bars
You can define standard bar types from strings using the following convention:
`{instrument_id}-{step}-{aggregation}-{price_type}-{INTERNAL | EXTERNAL}`
For example, to define a `BarType` for AAPL trades (last price) on Nasdaq (XNAS) using a 5-minute interval
aggregated from trades locally by Nautilus:
```python
bar_type = BarType.from_str("AAPL.XNAS-5-MINUTE-LAST-INTERNAL")
```
#### Composite bars
Composite bars are derived by aggregating higher-granularity bars into the desired bar type. To define a composite bar,
use this convention:
`{instrument_id}-{step}-{aggregation}-{price_type}-INTERNAL@{step}-{aggregation}-{INTERNAL | EXTERNAL}`
**Notes**:
- The derived bar type must use an `INTERNAL` aggregation source (since this is how the bar is aggregated).
- The sampled bar type must have a higher granularity 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.
For example, to define a `BarType` for AAPL trades (last price) on Nasdaq (XNAS) using a 5-minute interval
aggregated locally by Nautilus, from 1-minute interval bars aggregated externally:
```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:
```
{instrument_id}-{step}-{aggregation}-{price_type}-{source}@{step}-{aggregation}-{source}
```
The part after the `@` symbol is optional and only used for bar-to-bar aggregation:
- **Without `@`**: Aggregates from `TradeTick` objects (when price_type is `LAST`) or `QuoteTick` objects (when price_type is `BID`, `ASK`, or `MID`).
- **With `@`**: Aggregates from existing `Bar` objects (specifying the source bar type).
#### Trade-to-bar example
```python
def on_start(self) -> None:
# Define a bar type for aggregating from TradeTick objects
# Uses price_type=LAST which indicates TradeTick data as source
bar_type = BarType.from_str("6EH4.XCME-50-VOLUME-LAST-INTERNAL")
start = self.clock.utc_now() - timedelta(days=30)
# Request historical data (will receive bars in on_historical_data handler)
self.request_bars(bar_type, start=start)
# Subscribe to live data (will receive bars in on_bar handler)
self.subscribe_bars(bar_type)
```
#### Quote-to-bar example
```python
def on_start(self) -> None:
# Create 1-minute bars from ASK prices (in QuoteTick objects)
bar_type_ask = BarType.from_str("6EH4.XCME-1-MINUTE-ASK-INTERNAL")
# Create 1-minute bars from BID prices (in QuoteTick objects)
bar_type_bid = BarType.from_str("6EH4.XCME-1-MINUTE-BID-INTERNAL")
# Create 1-minute bars from MID prices (middle between ASK and BID prices in QuoteTick objects)
bar_type_mid = BarType.from_str("6EH4.XCME-1-MINUTE-MID-INTERNAL")
start = self.clock.utc_now() - timedelta(days=30)
# Request historical data and subscribe to live data
self.request_bars(bar_type_ask, start=start) # Historical bars processed in on_historical_data
self.subscribe_bars(bar_type_ask) # Live bars processed in on_bar
```
#### Bar-to-bar example
```python
def on_start(self) -> None:
# Create 5-minute bars from 1-minute bars (Bar objects)
# Format: target_bar_type@source_bar_type
# Note: price type (LAST) is only needed on the left target side, not on the source side
bar_type = BarType.from_str("6EH4.XCME-5-MINUTE-LAST-INTERNAL@1-MINUTE-EXTERNAL")
start = self.clock.utc_now() - timedelta(days=30)
# Request historical data by providing the dependency-ordered aggregation chain
self.request_aggregated_bars([bar_type], start=start)
# Subscribe to live updates (processed in on_bar(...) handler)
self.subscribe_bars(bar_type)
```
#### Advanced bar-to-bar example
You can create complex aggregation chains where you aggregate from already aggregated bars:
```python
# First create 1-minute bars from TradeTick objects (LAST indicates TradeTick source)
primary_bar_type = BarType.from_str("6EH4.XCME-1-MINUTE-LAST-INTERNAL")
# Then create 5-minute bars from 1-minute bars
# Note the @1-MINUTE-INTERNAL part identifying the source bars
intermediate_bar_type = BarType.from_str("6EH4.XCME-5-MINUTE-LAST-INTERNAL@1-MINUTE-INTERNAL")
# Then create hourly bars from 5-minute bars
# Note the @5-MINUTE-INTERNAL part identifying the source 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 distinct operations for working with bars:
- **`request_bars()`**: Fetches historical data for a standard `BarType`, processed by the
`on_historical_data()` handler.
- **`request_aggregated_bars()`**: Fetches historical data for a dependency-ordered list of bar
types, building internal bars on the fly.
- **`subscribe_bars()`**: Establishes a real-time data feed processed by the `on_bar()` handler.
It expects the instrument for the `BarType` to already be loaded in the cache.
The same cache precondition applies to quote, trade, order book, and other live
subscriptions.
These methods work together in a typical workflow:
1. First, `request_bars()` loads historical data to initialize indicators or state of strategy with past market behavior.
2. Then, `subscribe_bars()` ensures the strategy continues receiving new bars as they form in real-time.
:::tip[Request and subscribe ordering]
When `validate_data_sequence=True` (common with live adapters such as Interactive Brokers),
calling `subscribe_bars()` immediately after `request_bars()` can cause a race condition:
live bars arriving before the historical batch may cause the validator to discard older
warmup bars. To avoid it, pass a `callback` to `request_bars()` and subscribe from inside
it, as shown in the example below.
:::
Example usage in `on_start()`:
```python
def on_start(self) -> None:
# Define bar type
bar_type = BarType.from_str("6EH4.XCME-5-MINUTE-LAST-INTERNAL")
start = self.clock.utc_now() - timedelta(days=30)
# Register indicators before requesting history so they receive historical updates too
self.register_indicator_for_bars(bar_type, self.my_indicator)
# Request historical data to initialize indicators
# These bars will be delivered to the on_historical_data(...) handler in strategy
# Subscribe to real-time bars as a callback to the request so the live stream
# only starts once history is loaded (see tip above)
# New live bars will be delivered to the on_bar(...) handler in strategy
self.request_bars(
bar_type,
start=start,
callback=lambda _: self.subscribe_bars(bar_type),
)
```
Required handlers in your strategy to receive the data:
```python
def on_historical_data(self, data):
# Processes historical Data objects from request_bars() or request_aggregated_bars()
# Note: indicators registered with register_indicator_for_bars
# are updated automatically with historical data
pass
def on_bar(self, bar):
# Processes individual bars in real-time from subscribe_bars()
# Indicators registered with this bar type will update automatically and they will be updated before this handler is called
pass
```
### Historical data requests with aggregation
When requesting historical bars for backtesting or initializing indicators, use
`request_bars()` for standard bar types and `request_aggregated_bars()` for
on-the-fly aggregation:
```python
start = self.clock.utc_now() - timedelta(days=30)
# Request raw 1-minute bars (aggregated from TradeTick objects as indicated by LAST price type)
self.request_bars(
BarType.from_str("6EH4.XCME-1-MINUTE-LAST-EXTERNAL"),
start=start,
)
# Request bars that are aggregated from historical trade ticks
self.request_aggregated_bars(
[BarType.from_str("6EH4.XCME-100-VOLUME-LAST-INTERNAL")],
start=start,
)
# Request 5-minute bars aggregated from 1-minute bars
self.request_aggregated_bars(
[BarType.from_str("6EH4.XCME-5-MINUTE-LAST-INTERNAL@1-MINUTE-EXTERNAL")],
start=start,
)
```
### Common pitfalls
**Register indicators before requesting data**: Ensure indicators are registered before requesting historical data so they get updated properly.
```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
self.request_bars(bar_type, start=start) # Indicator won't receive historical data
self.register_indicator_for_bars(bar_type, self.ema)
```
### Performance considerations
Bar aggregators track OHLC prices via the fixed-point `Price` type. Threshold comparisons for
tick and volume aggregators, including their imbalance and runs variants, use integer arithmetic,
while value-based aggregators (value, value imbalance, and value runs) currently use `f64` for
notional value and signed accumulation (these are being migrated to fixed-point integer arithmetic). The choice of aggregation method has a modest impact on per-update
overhead:
- **Time bars** are the most efficient for high-throughput data. The aggregator accumulates
OHLCV state per update; bar emission is driven by a timer rather than per-tick logic.
- **Threshold bars** (tick, volume, value) add a lightweight 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) require tracking aggressor side and signed
accumulation per update. The overhead is slightly higher than threshold bars but still minimal.
- **Renko bars** are price-driven and can emit multiple bars from a single large price move.
Otherwise the per-update cost is comparable to threshold bars.
- **Composite bars** (bar-to-bar) are the most efficient way to produce higher-timeframe bars
when lower-timeframe bars are already available, as each input bar represents an already
aggregated period rather than a single tick.
### Time bar configuration
Time bar behavior is controlled through `DataEngineConfig`. The following options
apply to all time-based aggregation (millisecond through year):
| Option | Type | Default | Description |
|:------------------------------------|:-------|:--------------|:------------------------------------------------------------------------------------------------------------------------------------------------|
| `time_bars_interval_type` | `str` | `"left-open"` | `"left-open"`: start excluded, end included. `"right-open"`: start included, end excluded. |
| `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` | `None` | Maps `BarAggregation` types to `pd.Timedelta` or `pd.DateOffset` values for shifting bar alignment (e.g., align to 09:30 market open). |
| `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. |
```python
from nautilus_trader.data.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
The platform uses two fundamental timestamp fields that appear across many objects, including market data, orders, and events.
These timestamps serve distinct purposes and help maintain precise timing information throughout the system:
- `ts_event`: UNIX timestamp (nanoseconds) representing when an event actually occurred.
- `ts_init`: UNIX timestamp (nanoseconds) representing when Nautilus created the internal object representing that event.
### Examples
| **Event Type** | **`ts_event`** | **`ts_init`** |
| -----------------| ------------------------------------------------------| --------------|
| `TradeTick` | Time when trade occurred at the exchange. | Time when Nautilus received the trade data. |
| `QuoteTick` | Time when quote occurred at the exchange. | Time when Nautilus received the quote data. |
| `OrderBookDelta` | Time when order book update occurred at the exchange. | Time when Nautilus received the order book update. |
| `Bar` | Time of the bar's closing (exact minute/hour). | Time when Nautilus generated (for internal bars) or received the bar data (for external bars). |
| `DefiData` | Time the block or pool event occurred. | Time when Nautilus created the object from the chain data. |
| `OrderFilled` | Time when order was filled at the exchange. | Time when Nautilus received and processed the fill confirmation. |
| `OrderCanceled` | Time when cancellation was processed at the exchange. | Time when Nautilus received and processed the cancellation confirmation. |
| `NewsEvent` | Time when the news was published. | Time when the event object was created (if internal event) or received (if external event) in Nautilus. |
| Custom event | Time when event conditions actually occurred. | Time when the event object was created (if internal event) or received (if external event) in Nautilus. |
:::note
The `ts_init` field represents a more general concept than "time of reception" for events.
It denotes the timestamp when an object, such as a data point or command, was initialized within Nautilus.
This distinction is important because `ts_init` is not exclusive to "received events". It applies to any internal
initialization process.
For example, the `ts_init` field is also used for commands, where the concept of reception does not apply.
This broader definition ensures consistent handling of initialization timestamps across various object types in the system.
:::
### Latency analysis
The dual timestamp system enables latency analysis within the platform:
- Latency can be calculated as `ts_init - ts_event`.
- This difference represents total system latency, including network transmission time, processing overhead, and any queueing delays.
- It's important to remember that the clocks producing these timestamps are likely not synchronized.
### 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 behavior ensures deterministic processing order and simulates realistic system behavior, including latencies.
#### Live trading environment
- The system processes data as it arrives to minimize latency and enable real-time decisions.
- For venue-sourced data, `ts_init` is typically when Nautilus creates the local object after receiving the update.
- `ts_event` reflects the time the event occurred externally, enabling accurate comparisons between external event timing and system reception.
- We can use the difference between `ts_init` and `ts_event` to detect network or processing delays.
### 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 Nautilus, `ts_init` and `ts_event` can be the same because the object is initialized at the same time the event happens.
- Not every type with a `ts_init` field necessarily has a `ts_event` field. This reflects cases where:
- 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).
For users who need more flexibility, the platform also supports the creation of custom data types.
For details on how to implement user-defined data types, see the [Custom Data](#custom-data) section below.
## Loading data
NautilusTrader supports data loading and conversion for three main use cases:
- Providing data for a `BacktestEngine` to run backtests.
- Persisting the Nautilus-specific Parquet format for the data catalog via `ParquetDataCatalog.write_data(...)` to be later used with a `BacktestNode`.
- For research purposes (to ensure data is consistent between research and backtesting).
Regardless of the destination, the process remains the same: converting diverse external data formats into Nautilus data structures.
To achieve this, two main components are necessary:
- A type of DataLoader (normally specific per raw source/format) which can read the data and return a `pd.DataFrame` with the correct schema for the desired Nautilus object.
- A type of DataWrangler (specific per data type) which takes this `pd.DataFrame` and returns a `list[Data]` of Nautilus objects.
### Data loaders
Data loader components are typically specific for the raw source/format and per integration. For instance, Binance order book data is stored in its raw CSV file form with
an entirely different format to [Databento Binary Encoding (DBN)](https://databento.com/docs/knowledge-base/new-users/dbn-encoding/getting-started-with-dbn) files.
### Data wranglers
Data wranglers are implemented per specific Nautilus data type, and can be found in the `nautilus_trader.persistence.wranglers` module.
Common v1 wranglers include:
- `OrderBookDeltaDataWrangler`
- `QuoteTickDataWrangler`
- `TradeTickDataWrangler`
- `BarDataWrangler`
For Arrow v2 / PyO3 workflows, the v2 module also provides `OrderBookDepth10DataWranglerV2`.
:::warning
There are a number of **DataWrangler v2** components, which will take a `pd.DataFrame` typically
with a different fixed width Nautilus Arrow v2 schema, and output PyO3 Nautilus objects which are only compatible with the new version
of the Nautilus core, currently in development.
**These PyO3 data objects are not compatible where v1 legacy Cython objects are expected (e.g., adding directly to a `BacktestEngine`).**
:::
### Fixed-point precision and raw values
NautilusTrader uses fixed-point arithmetic for `Price` and `Quantity` types for precise financial calculations without floating-point errors. Understanding how raw values work is essential when creating data or working with catalogs.
#### Raw value requirements
When constructing `Price` or `Quantity` using `from_raw()`, the raw value **must** be a valid multiple of the scale factor for the given precision. Valid raw values should come from:
- Accessing the `.raw` field of an existing value (e.g., `price.raw`).
- Using the Nautilus fixed-point conversion functions.
- Values from Nautilus-produced Arrow data.
:::warning
Raw values that are not valid multiples will cause a panic. The raw value must be divisible by `10^(FIXED_PRECISION - precision)` where `FIXED_PRECISION` is 9 (standard mode) or 16 (high-precision mode).
:::
#### Automatic raw value correction
Catalog data can contain raw values with floating-point precision errors.
This happens when raw values are produced with `int(value * FIXED_SCALAR)`
instead of precision-aware conversion:
```python
int(value * FIXED_SCALAR) # Introduces floating-point errors
round(value * 10**precision) * scale # Correct precision-aware conversion
```
For example, `int(0.67068 * 1e9)` produces `670680000000001` instead of
the expected `670680000000000`.
The Arrow decode path automatically corrects these values by rounding to
the nearest valid multiple, so affected catalogs work without data migration.
:::note
This correction adds a small amount of overhead during data decoding.
:::
### Transformation pipeline
**Process flow**:
1. Raw data (e.g., CSV) is input into the pipeline.
2. DataLoader processes the raw data and converts it into a `pd.DataFrame`.
3. DataWrangler further processes the `pd.DataFrame` to generate a list of Nautilus objects.
4. The Nautilus `list[Data]` is the output of the data loading process.
The following diagram illustrates how raw data is transformed into Nautilus data structures:
```mermaid
flowchart LR
raw["Raw data (CSV)"]
loader[DataLoader]
wrangler[DataWrangler]
output["Nautilus list[Data]"]
raw --> loader
loader -->|"pd.DataFrame"| wrangler
wrangler --> output
```
Concretely, this would involve:
- `BinanceOrderBookDeltaDataLoader.load(...)` which reads CSV files provided by Binance from disk, and returns a `pd.DataFrame`.
- `OrderBookDeltaDataWrangler.process(...)` which takes the `pd.DataFrame` and returns `list[OrderBookDelta]`.
The following example shows how to accomplish the above in Python:
```python
from nautilus_trader import TEST_DATA_DIR
from nautilus_trader.adapters.binance.loaders import BinanceOrderBookDeltaDataLoader
from nautilus_trader.persistence.wranglers import OrderBookDeltaDataWrangler
from nautilus_trader.test_kit.providers import TestInstrumentProvider
# Load raw data
data_path = TEST_DATA_DIR / "binance" / "btcusdt-depth-snap.csv"
df = BinanceOrderBookDeltaDataLoader.load(data_path)
# Set up a wrangler
instrument = TestInstrumentProvider.btcusdt_binance()
wrangler = OrderBookDeltaDataWrangler(instrument)
# Process to a list `OrderBookDelta` Nautilus objects
deltas = wrangler.process(df)
```
## Data catalog
The data catalog is a central store for Nautilus data, persisted in the [Parquet](https://parquet.apache.org) file format. It is the primary data management system for both backtesting and live trading scenarios, providing efficient storage, retrieval, and streaming capabilities for market data.
### Overview and architecture
The NautilusTrader data catalog is built on a dual-backend architecture that combines the performance of Rust with the flexibility of Python:
**Core components:**
- **ParquetDataCatalog**: The main Python interface for data operations.
- **Rust backend**: High-performance query engine for core data types (`OrderBookDelta`,
`OrderBookDeltas`, `OrderBookDepth10`, `QuoteTick`, `TradeTick`, `Bar`,
`MarkPriceUpdate`) and registered same-binary Rust custom data.
- **PyArrow backend**: Flexible fallback for custom data types and advanced filtering.
- **fsspec integration**: Support for local and cloud storage (S3, GCS, Azure, etc.).
**Key benefits**:
- **Performance**: Rust backend provides optimized query performance for core market data types.
- **Flexibility**: PyArrow backend handles custom data types and complex filtering scenarios.
- **Scalability**: Efficient compression and columnar storage reduce storage costs and improve I/O performance.
- **Cloud native**: Built-in support for cloud storage providers through fsspec.
- **No dependencies**: Self-contained solution requiring no external databases or services.
**Storage format advantages:**
- Superior compression ratio and read performance compared to CSV/JSON/HDF5.
- Columnar storage enables efficient filtering and aggregation.
- Schema evolution support for data model changes.
- Cross-language compatibility (Python, Rust, Java, C++, etc.).
The Arrow schemas used for the Parquet format are defined in two places: the Rust `model` and `persistence` crates for core market data types, and the Python `serialization/arrow/schema.py` module for additional types.
### Initializing
The data catalog can be initialized from a `NAUTILUS_PATH` environment variable, or by explicitly passing in a path like object.
:::note[NAUTILUS_PATH environment variable]
The `NAUTILUS_PATH` environment variable should point to the **root** directory containing your Nautilus data. The catalog will automatically append `/catalog` to this path.
For example:
- If `NAUTILUS_PATH=/home/user/trading_data`.
- Then the catalog will be located at `/home/user/trading_data/catalog`.
This is a common pattern when using `ParquetDataCatalog.from_env()` - make sure your `NAUTILUS_PATH` points to the parent directory, not the catalog directory itself.
:::
The following example shows how to initialize a data catalog where there is pre-existing data already written to disk at the given path.
```python
from pathlib import Path
from nautilus_trader.persistence.catalog import ParquetDataCatalog
CATALOG_PATH = Path.cwd() / "catalog"
# Create a new catalog instance
catalog = ParquetDataCatalog(CATALOG_PATH)
# Alternative: Environment-based initialization
catalog = ParquetDataCatalog.from_env() # Uses NAUTILUS_PATH environment variable
```
### Filesystem protocols and storage options
The catalog supports multiple filesystem protocols through fsspec integration, working across local and cloud storage systems.
#### Supported filesystem protocols
**Local filesystem (`file`):**
```python
catalog = ParquetDataCatalog(
path="/path/to/catalog",
fs_protocol="file", # Default protocol
)
```
**Amazon S3 (`s3`):**
```python
catalog = ParquetDataCatalog(
path="s3://my-bucket/nautilus-data/",
fs_protocol="s3",
fs_storage_options={
"key": "your-access-key-id",
"secret": "your-secret-access-key",
"endpoint_url": "https://s3.amazonaws.com", # Optional custom endpoint
}
)
```
**Google Cloud Storage (`gcs`):**
```python
catalog = ParquetDataCatalog(
path="gcs://my-bucket/nautilus-data/",
fs_protocol="gcs",
fs_storage_options={
"project": "my-project-id",
"token": "/path/to/service-account.json", # Or "cloud" for default credentials
}
)
```
**Azure Blob Storage :**
`abfs` protocol
```python
catalog = ParquetDataCatalog(
path="abfs://container@account.dfs.core.windows.net/nautilus-data/",
fs_protocol="abfs",
fs_storage_options={
"account_name": "your-storage-account",
"account_key": "your-account-key",
# Or use SAS token: "sas_token": "your-sas-token"
}
)
```
`az` protocol
```python
catalog = ParquetDataCatalog(
path="az://container/nautilus-data/",
fs_protocol="az",
fs_storage_options={
"account_name": "your-storage-account",
"account_key": "your-account-key",
# Or use SAS token: "sas_token": "your-sas-token"
}
)
```
#### URI-based initialization
For convenience, you can use URI strings that automatically parse protocol and storage options:
```python
# Local filesystem
catalog = ParquetDataCatalog.from_uri("/path/to/catalog")
# S3 bucket
catalog = ParquetDataCatalog.from_uri("s3://my-bucket/nautilus-data/")
# With storage options
catalog = ParquetDataCatalog.from_uri(
"s3://my-bucket/nautilus-data/",
fs_storage_options={
"access_key_id": "your-key",
"secret_access_key": "your-secret"
}
)
```
### Writing data
Store data in the catalog using the `write_data()` method. All Nautilus built-in `Data` objects are supported, and any data which inherits from `Data` can be written.
```python
# Write a list of data objects
catalog.write_data(quote_ticks)
# Write with custom timestamp range
catalog.write_data(
trade_ticks,
start=1704067200000000000, # Optional start timestamp override (UNIX nanoseconds)
end=1704153600000000000, # Optional end timestamp override (UNIX nanoseconds)
)
# Skip disjoint check for overlapping data
catalog.write_data(bars, skip_disjoint_check=True)
```
### File naming and data organization
The catalog automatically generates filenames based on the timestamp range of the data being
written. Files are named using the pattern `{start_timestamp}_{end_timestamp}.parquet`, where
each timestamp is an ISO 8601 value converted to a filename-safe form by replacing `:` and `.`
with `-`.
Data is organized in directories by data type and identifier
(instrument ID, bar type, or custom identifier). Identifiers are made URI-safe by removing `/`:
```
catalog/
├── data/
│ ├── quote_ticks/
│ │ └── EURUSD.SIM/
│ │ └── 2024-01-01T00-00-00-000000000Z_2024-01-01T23-59-59-999999999Z.parquet
│ └── trade_ticks/
│ └── BTCUSD.BINANCE/
│ └── 2024-01-01T00-00-00-000000000Z_2024-01-01T23-59-59-999999999Z.parquet
```
**Rust backend data types (enhanced performance):**
The following data types use optimized Rust implementations:
- `OrderBookDelta`.
- `OrderBookDeltas`.
- `OrderBookDepth10`.
- `QuoteTick`.
- `TradeTick`.
- `Bar`.
- `MarkPriceUpdate`.
:::warning
By default, overlapping writes raise a `ValueError` to maintain data integrity.
Use `skip_disjoint_check=True` in `write_data()` to bypass this check when needed.
:::
### Reading data
Use the `query()` method to read data back from the catalog:
```python
from nautilus_trader.model import QuoteTick, TradeTick
# Query quote ticks for a specific instrument and time range
quotes = catalog.query(
data_cls=QuoteTick,
identifiers=["EUR/USD.SIM"],
start="2024-01-01T00:00:00Z",
end="2024-01-02T00:00:00Z"
)
# Query trade ticks for a specific instrument and time range
trades = catalog.query(
data_cls=TradeTick,
identifiers=["BTC/USD.BINANCE"],
start="2024-01-01",
end="2024-01-02",
)
```
### `BacktestDataConfig` - data specification for backtests
The `BacktestDataConfig` class is the primary mechanism for specifying data requirements before a backtest starts. It defines what data should be loaded from the catalog and how it should be filtered and processed during the backtest execution.
#### Core parameters
**Required parameters:**
- `catalog_path`: Path to the data catalog directory.
- `data_cls`: The data type class (e.g., QuoteTick, TradeTick, OrderBookDelta, Bar).
**Optional parameters:**
- `catalog_fs_protocol`: Filesystem protocol ('file', 's3', 'gcs', etc.).
- `catalog_fs_storage_options`: Storage-specific options (credentials, region, etc.).
- `catalog_fs_rust_storage_options`: Storage-specific options for the Rust backend.
- `instrument_id`: Specific instrument to load data for.
- `instrument_ids`: List of instruments (alternative to single instrument_id).
- `start_time`: Start time for data filtering (ISO string or UNIX nanoseconds).
- `end_time`: End time for data filtering (ISO string or UNIX nanoseconds).
- `filter_expr`: Additional PyArrow filter expressions.
- `client_id`: Client ID for custom data types.
- `metadata`: Additional metadata for data queries.
- `bar_spec`: Bar specification for bar data (e.g., `"1-MINUTE-LAST"`). When combined
with `instrument_id` or `instrument_ids`, this builds `...-EXTERNAL` bar identifiers.
- `bar_types`: Explicit list of full bar types. Use this for `INTERNAL` bars or composite bars.
- `optimize_file_loading`: Load directories instead of individual files when supported.
#### Basic usage examples
**Loading quote ticks:**
```python
from nautilus_trader.config import BacktestDataConfig
from nautilus_trader.model import QuoteTick, InstrumentId
data_config = BacktestDataConfig(
catalog_path="/path/to/catalog",
data_cls=QuoteTick,
instrument_id=InstrumentId.from_str("EUR/USD.SIM"),
start_time="2024-01-01T00:00:00Z",
end_time="2024-01-02T00:00:00Z",
)
```
**Loading multiple instruments:**
```python
data_config = BacktestDataConfig(
catalog_path="/path/to/catalog",
data_cls=TradeTick,
instrument_ids=["BTC/USD.BINANCE", "ETH/USD.BINANCE"],
start_time="2024-01-01T00:00:00Z",
end_time="2024-01-02T00:00:00Z",
)
```
**Loading Bar Data:**
```python
data_config = BacktestDataConfig(
catalog_path="/path/to/catalog",
data_cls=Bar,
instrument_id=InstrumentId.from_str("AAPL.NASDAQ"),
bar_spec="5-MINUTE-LAST", # Loads AAPL.NASDAQ-5-MINUTE-LAST-EXTERNAL
start_time="2024-01-01",
end_time="2024-01-31",
)
```
#### Advanced configuration examples
**Cloud Storage with Custom Filtering:**
```python
data_config = BacktestDataConfig(
catalog_path="s3://my-bucket/nautilus-data/",
catalog_fs_protocol="s3",
catalog_fs_storage_options={
"key": "your-access-key",
"secret": "your-secret-key",
"region": "us-east-1"
},
data_cls=OrderBookDelta,
instrument_id=InstrumentId.from_str("BTC/USD.COINBASE"),
start_time="2024-01-01T09:30:00Z",
end_time="2024-01-01T16:00:00Z",
)
```
**Custom Data with Client ID:**
```python
data_config = BacktestDataConfig(
catalog_path="/path/to/catalog",
data_cls="my_package.data.NewsEventData",
client_id="NewsClient",
metadata={"source": "reuters", "category": "earnings"},
start_time="2024-01-01",
end_time="2024-01-31",
)
```
#### Integration with BacktestRunConfig
The `BacktestDataConfig` objects are integrated into the backtesting framework through `BacktestRunConfig`:
```python
from nautilus_trader.config import BacktestRunConfig, BacktestVenueConfig
# Define multiple data configurations
data_configs = [
BacktestDataConfig(
catalog_path="/path/to/catalog",
data_cls=QuoteTick,
instrument_id="EUR/USD.SIM",
start_time="2024-01-01",
end_time="2024-01-02",
),
BacktestDataConfig(
catalog_path="/path/to/catalog",
data_cls=TradeTick,
instrument_id="EUR/USD.SIM",
start_time="2024-01-01",
end_time="2024-01-02",
),
]
# Create backtest run configuration
run_config = BacktestRunConfig(
venues=[BacktestVenueConfig(name="SIM", oms_type="HEDGING")],
data=data_configs, # List of data configurations
start="2024-01-01T00:00:00Z",
end="2024-01-02T00:00:00Z",
)
```
#### Data loading process
When a backtest runs, the `BacktestNode` processes each `BacktestDataConfig`:
1. **Catalog Loading**: Creates a `ParquetDataCatalog` instance from the config.
2. **Query Construction**: Builds query parameters from config attributes.
3. **Data Retrieval**: Executes catalog queries using the appropriate backend.
4. **Instrument Loading**: Loads instrument definitions if needed.
5. **Engine Integration**: Adds data to the backtest engine with proper sorting.
The system automatically handles:
- Instrument ID resolution and validation.
- Data type validation and conversion.
- Memory-efficient streaming for large datasets.
- Error handling and logging.
### DataCatalogConfig - on-the-fly data loading
The `DataCatalogConfig` class provides configuration for on-the-fly data loading scenarios, particularly useful for backtests where the number of possible instruments is vast,
Unlike `BacktestDataConfig` which pre-specifies data for backtests, `DataCatalogConfig` enables flexible catalog access during runtime.
Catalogs defined this way can also be used for requesting historical data.
#### Core parameters
**Required Parameters:**
- `path`: Path to the data catalog directory.
**Optional Parameters:**
- `fs_protocol`: Filesystem protocol ('file', 's3', 'gcs', 'azure', etc.).
- `fs_storage_options`: Protocol-specific storage options.
- `fs_rust_storage_options`: Protocol-specific storage options for the Rust backend.
- `name`: Optional name identifier for the catalog configuration.
#### Basic usage examples
**Local Catalog Configuration:**
```python
from nautilus_trader.persistence.config import DataCatalogConfig
catalog_config = DataCatalogConfig(
path="/path/to/catalog",
fs_protocol="file",
name="local_market_data"
)
# Convert to catalog instance
catalog = catalog_config.as_catalog()
```
**Cloud storage configuration:**
```python
catalog_config = DataCatalogConfig(
path="s3://my-bucket/market-data/",
fs_protocol="s3",
fs_storage_options={
"key": "your-access-key",
"secret": "your-secret-key",
"region": "us-west-2",
"endpoint_url": "https://s3.us-west-2.amazonaws.com"
},
name="cloud_market_data"
)
```
#### Integration with live trading
`DataCatalogConfig` is commonly used in live trading configurations for historical data access:
```python
from nautilus_trader.config import TradingNodeConfig
from nautilus_trader.persistence.config import DataCatalogConfig
# Configure catalog for live system
catalog_config = DataCatalogConfig(
path="/data/nautilus/catalog",
fs_protocol="file",
name="historical_data"
)
# Use in trading node configuration
node_config = TradingNodeConfig(
# ... other configurations
catalogs=[catalog_config], # Enable historical data access
)
```
#### Streaming configuration
For streaming data to catalogs during live trading or backtesting, use `StreamingConfig`:
```python
from nautilus_trader.persistence.config import StreamingConfig, RotationMode
import pandas as pd
streaming_config = StreamingConfig(
catalog_path="/path/to/streaming/catalog",
fs_protocol="file",
flush_interval_ms=1000, # Flush every second
replace_existing=False,
rotation_mode=RotationMode.INTERVAL,
rotation_interval=pd.Timedelta(hours=1),
max_file_size=1024 * 1024 * 100, # 100MB max file size
)
```
#### Use cases
**Historical Data Analysis:**
- Load historical data during live trading for strategy calculations.
- Access reference data for instrument lookups.
- Retrieve past performance metrics.
**Dynamic data loading:**
- Load data based on runtime conditions.
- Implement custom data loading strategies.
- Support multiple catalog sources.
**Research and development:**
- Interactive data exploration in Jupyter notebooks.
- Ad-hoc analysis and backtesting.
- Data quality validation and monitoring.
### Query system and dual backend architecture
The catalog's query system uses a dual-backend architecture that selects the query engine based on data type and query parameters.
#### Backend selection logic
**Rust backend (high performance):**
- **Supported Types**: OrderBookDelta, OrderBookDeltas, OrderBookDepth10, QuoteTick, TradeTick, Bar, MarkPriceUpdate.
- **Conditions**: Used when `files` parameter is None (automatic file discovery).
- **Benefits**: Optimized performance, memory efficiency, native Arrow integration.
Registered same-binary Rust custom data types can also use this path.
**PyArrow backend (flexible):**
- **Supported Types**: All data types including custom data classes.
- **Conditions**: Used for custom data types or when `files` parameter is specified.
- **Benefits**: Advanced filtering, custom data support, complex query expressions.
#### Query methods and parameters
**Core query parameters:**
```python
catalog.query(
data_cls=QuoteTick, # Data type to query
identifiers=["EUR/USD.SIM"], # Instrument identifiers
start="2024-01-01T00:00:00Z", # Start time (various formats supported)
end="2024-01-02T00:00:00Z", # End time
files=None, # Leave unset for automatic file discovery
)
```
- `where=` passes a DataFusion SQL predicate to Rust-backed queries.
- `filter_expr=` passes a parsed PyArrow dataset expression to PyArrow-backed queries.
**Time format support:**
- ISO 8601 strings: `"2024-01-01T00:00:00Z"`.
- UNIX nanoseconds: `1704067200000000000` (or ISO format: `"2024-01-01T00:00:00Z"`).
- Pandas Timestamps: `pd.Timestamp("2024-01-01", tz="UTC")`.
- Python datetime objects (timezone-aware recommended).
**Filtering notes:**
- Use `where=` for Rust-backed built-in market data queries.
- Use `filter_expr=` for PyArrow-backed queries, including custom data and queries
forced onto the PyArrow path with `files=`.
### Catalog operations
The catalog provides several operation functions for maintaining and organizing data files. These operations help optimize storage, improve query performance, and maintain data integrity.
#### Reset file names
Reset parquet file names to match their actual content timestamps. This ensures filename-based filtering works correctly.
**Reset all files in catalog:**
```python
# Reset all parquet files in the catalog
catalog.reset_all_file_names()
```
**Reset specific data type:**
```python
# Reset filenames for all quote tick files
catalog.reset_data_file_names(QuoteTick)
# Reset filenames for specific instrument's trade files
catalog.reset_data_file_names(TradeTick, "BTC/USD.BINANCE")
```
#### Consolidate catalog
Combine multiple small parquet files into larger files to improve query performance and reduce storage overhead.
**Consolidate entire catalog:**
```python
# Consolidate all files in the catalog
catalog.consolidate_catalog()
# Consolidate files within a specific time range
catalog.consolidate_catalog(
start="2024-01-01T00:00:00Z",
end="2024-01-02T00:00:00Z",
ensure_contiguous_files=True
)
```
**Consolidate specific data type:**
```python
# Consolidate all quote tick files
catalog.consolidate_data(QuoteTick)
# Consolidate specific instrument's files
catalog.consolidate_data(
TradeTick,
identifier="BTC/USD.BINANCE",
start="2024-01-01",
end="2024-01-31"
)
```
#### Consolidate catalog by period
Split data files into fixed time periods for standardized file organization.
**Consolidate entire catalog by period:**
```python
import pandas as pd
# Consolidate all files by 1-day periods
catalog.consolidate_catalog_by_period(
period=pd.Timedelta(days=1)
)
# Consolidate by 1-hour periods within time range
catalog.consolidate_catalog_by_period(
period=pd.Timedelta(hours=1),
start="2024-01-01T00:00:00Z",
end="2024-01-02T00:00:00Z"
)
```
**Consolidate specific data by period:**
```python
# Consolidate quote data by 4-hour periods
catalog.consolidate_data_by_period(
data_cls=QuoteTick,
period=pd.Timedelta(hours=4)
)
# Consolidate specific instrument by 30-minute periods
catalog.consolidate_data_by_period(
data_cls=TradeTick,
identifier="EUR/USD.SIM",
period=pd.Timedelta(minutes=30),
start="2024-01-01",
end="2024-01-31"
)
```
#### Delete data range
Remove data within a specified time range for specific data types and instruments. This operation permanently deletes data and handles file intersections intelligently.
**Delete entire catalog range:**
```python
# Delete all data within a time range across the entire catalog
catalog.delete_catalog_range(
start="2024-01-01T00:00:00Z",
end="2024-01-02T00:00:00Z"
)
# Delete all data from the beginning up to a specific time
catalog.delete_catalog_range(end="2024-01-01T00:00:00Z")
```
**Delete specific data type:**
```python
# Delete all quote tick data for a specific instrument
catalog.delete_data_range(
data_cls=QuoteTick,
identifier="BTC/USD.BINANCE"
)
# Delete trade data within a specific time range
catalog.delete_data_range(
data_cls=TradeTick,
identifier="EUR/USD.SIM",
start="2024-01-01T00:00:00Z",
end="2024-01-31T23:59:59Z"
)
```
:::warning
Delete operations permanently remove data and cannot be undone. Files that partially overlap the deletion range are split to preserve data outside the range.
:::
### Feather streaming and conversion
The catalog supports streaming data to temporary feather files during backtests, which can then be converted to permanent parquet format for efficient querying.
**Example: option greeks streaming**
```python
from option_trader.greeks import GreeksData
from nautilus_trader.persistence.config import StreamingConfig
# 1. Configure streaming for custom data
streaming = StreamingConfig(
catalog_path=catalog.path,
include_types=[GreeksData],
flush_interval_ms=1000,
)
# 2. Run backtest with streaming enabled
engine_config = BacktestEngineConfig(streaming=streaming)
results = node.run()
# 3. Convert streamed data to permanent catalog
catalog.convert_stream_to_data(
results[0].instance_id,
GreeksData,
)
# 4. Query converted data
greeks_data = catalog.query(
data_cls=GreeksData,
start="2024-01-01",
end="2024-01-31",
where="delta > 0.5",
)
```
### Catalog summary
The NautilusTrader data catalog provides market data management:
**Core features**:
- **Dual Backend**: Rust performance + Python flexibility.
- **Multi-Protocol**: Local, S3, GCS, Azure storage.
- **Streaming**: Feather -> Parquet conversion pipeline.
- **Operations**: Reset file names, consolidate data, period-based organization.
**Key use cases**:
- **Backtesting**: Pre-configured data loading via BacktestDataConfig.
- **Live Trading**: On-demand data access via DataCatalogConfig.
- **Maintenance**: File consolidation and organization operations.
- **Research**: Interactive querying and analysis.
## Data migrations
NautilusTrader defines an internal data format specified in the `nautilus_model` crate.
These models are serialized into Arrow record batches and written to Parquet files.
Nautilus backtesting is most efficient when using these Nautilus-format Parquet files.
However, migrating the data model between [precision modes](../getting_started/installation.md#precision-mode) and schema changes can be challenging.
This guide explains how to handle data migrations using our utility tools.
### Migration tools
The `nautilus_persistence` crate provides two key utilities:
#### `to_json`
Converts Parquet files to JSON while preserving metadata:
- Creates two files:
- `.json`: Contains the deserialized data
- `.metadata.json`: Contains schema metadata and row group configuration
- Automatically detects data type from filename:
- `OrderBookDelta` (contains "deltas" or "order_book_delta")
- `QuoteTick` (contains "quotes" or "quote_tick")
- `TradeTick` (contains "trades" or "trade_tick")
- `Bar` (contains "bars")
#### `to_parquet`
Converts JSON back to Parquet format:
- Reads both the data JSON and metadata JSON files.
- Preserves row group sizes from original metadata.
- Uses ZSTD compression.
- Creates `.parquet`.
### Migration process
The following migration examples both use trades data (you can also migrate the other data types in the same way).
All commands should be run from the root of the `persistence` crate directory.
#### Migrating from standard-precision (64-bit) to high-precision (128-bit)
This example describes a scenario where you want to migrate from standard-precision schema to high-precision schema.
:::note
If you're migrating from a catalog that used the `Int64` and `UInt64` Arrow data types for prices and sizes,
be sure to check out commit [e284162](https://github.com/nautechsystems/nautilus_trader/commit/e284162cf27a3222115aeb5d10d599c8cf09cf50)
**before** compiling the code that writes the initial JSON.
:::
**1. Convert from standard-precision Parquet to JSON**:
```bash
cargo run --bin to_json trades.parquet
```
This will create `trades.json` and `trades.metadata.json` files.
**2. Convert from JSON to high-precision Parquet**:
Add the `--features high-precision` flag to write data as high-precision (128-bit) schema Parquet.
```bash
cargo run --features high-precision --bin to_parquet trades.json
```
This will create a `trades.parquet` file with high-precision schema data.
#### Migrating schema changes
This example describes a scenario where you want to migrate from one schema version to another.
**1. Convert from old schema Parquet to JSON**:
Add the `--features high-precision` flag if the source data uses a high-precision (128-bit) schema.
```bash
cargo run --bin to_json trades.parquet
```
This will create `trades.json` and `trades.metadata.json` files.
**2. Switch to new schema version**:
```bash
git checkout
```
**3. Convert from JSON back to new schema Parquet**:
```bash
cargo run --features high-precision --bin to_parquet trades.json
```
This will create a `trades.parquet` file with the new schema.
### Best practices
- Always test migrations with a small dataset first.
- Maintain backups of original files.
- Verify data integrity after migration.
- Perform migrations in a staging environment before applying them to production data.
## Custom data
Due to the modular nature of the Nautilus design, it is possible to set up systems
with very flexible data streams, including custom user-defined data types. This
guide covers some possible use cases for this functionality.
It's possible to create custom data types within the Nautilus system. First you
will need to define your data by subclassing from `Data`.
:::info
As `Data` holds no state, it is not strictly necessary to call `super().__init__()`.
:::
```python
from nautilus_trader.core import Data
class MyDataPoint(Data):
"""
This is an example of a user-defined data class, inheriting from the base class `Data`.
The fields `label`, `x`, `y`, and `z` in this class are examples of arbitrary user data.
"""
def __init__(
self,
label: str,
x: int,
y: int,
z: int,
ts_event: int,
ts_init: int,
) -> None:
self.label = label
self.x = x
self.y = y
self.z = z
self._ts_event = ts_event
self._ts_init = ts_init
@property
def ts_event(self) -> int:
"""
UNIX timestamp (nanoseconds) when the data event occurred.
Returns
-------
int
"""
return self._ts_event
@property
def ts_init(self) -> int:
"""
UNIX timestamp (nanoseconds) when the object was initialized.
Returns
-------
int
"""
return self._ts_init
```
The `Data` abstract base class acts as a contract within the system and requires two properties
for all types of data: `ts_event` and `ts_init`. These represent the UNIX nanosecond timestamps
for when the event occurred and when the object was initialized, respectively.
The recommended approach to satisfy the contract is to assign `ts_event` and `ts_init`
to backing fields, and then implement the `@property` for each as shown above
(for completeness, the docstrings are copied from the `Data` base class).
:::info
These timestamps enable Nautilus to correctly order data streams for backtests
using monotonically increasing `ts_init` UNIX nanoseconds.
:::
We can now work with this data type for backtesting and live trading. For instance,
we could now create an adapter which is able to parse and create objects of this
type - and send them back to the `DataEngine` for consumption by subscribers.
You can publish a custom data type within your actor/strategy using the message bus
in the following way:
```python
self.publish_data(
DataType(MyDataPoint, metadata={"some_optional_category": 1}),
MyDataPoint(...),
)
```
The `metadata` dictionary optionally adds more granular information that is used in the
topic name to publish data with the message bus.
Extra metadata information can also be passed to a `BacktestDataConfig` configuration object in order to
enrich and describe custom data objects used in a backtesting context:
```python
from nautilus_trader.config import BacktestDataConfig
data_config = BacktestDataConfig(
catalog_path=str(catalog.path),
data_cls=MyDataPoint,
metadata={"some_optional_category": 1},
)
```
You can subscribe to custom data types within your actor/strategy in the following way:
```python
self.subscribe_data(
data_type=DataType(MyDataPoint,
metadata={"some_optional_category": 1}),
client_id=ClientId("MY_ADAPTER"),
)
```
The `client_id` provides an identifier to route the data subscription to a specific client.
This will result in your actor/strategy passing these received `MyDataPoint`
objects to your `on_data` method. You will need to check the type, as this
method acts as a flexible handler for all custom data.
```python
def on_data(self, data: Data) -> None:
# First check the type of data
if isinstance(data, MyDataPoint):
# Do something with the data
```
### Publishing and receiving signal data
Here is an example of publishing and receiving signal data using the `MessageBus` from an actor or strategy.
A signal is an automatically generated custom data identified by a name containing only one value of a basic type
(str, float, int, bool or bytes).
```python
self.publish_signal("signal_name", value, ts_event)
self.subscribe_signal("signal_name")
def on_signal(self, signal):
print("Signal", signal)
```
### Option greeks example
This example demonstrates how to create a custom data type for option Greeks, specifically the delta.
By following these steps, you can create custom data types, subscribe to them, publish them, and store
them in the `Cache` or `ParquetDataCatalog` for efficient retrieval.
```python
import msgspec
from nautilus_trader.core import Data
from nautilus_trader.core.datetime import unix_nanos_to_iso8601
from nautilus_trader.model import DataType
from nautilus_trader.serialization.base import register_serializable_type
from nautilus_trader.serialization.arrow.serializer import register_arrow
import pyarrow as pa
from nautilus_trader.model import InstrumentId
from nautilus_trader.core.datetime import dt_to_unix_nanos, unix_nanos_to_dt, format_iso8601
class GreeksData(Data):
def __init__(
self, instrument_id: InstrumentId = InstrumentId.from_str("ES.GLBX"),
ts_event: int = 0,
ts_init: int = 0,
delta: float = 0.0,
) -> None:
self.instrument_id = instrument_id
self._ts_event = ts_event
self._ts_init = ts_init
self.delta = delta
def __repr__(self):
return (f"GreeksData(ts_init={unix_nanos_to_iso8601(self._ts_init)}, instrument_id={self.instrument_id}, delta={self.delta:.2f})")
@property
def ts_event(self):
return self._ts_event
@property
def ts_init(self):
return self._ts_init
def to_dict(self):
return {
"instrument_id": self.instrument_id.value,
"ts_event": self._ts_event,
"ts_init": self._ts_init,
"delta": self.delta,
}
@classmethod
def from_dict(cls, data: dict):
return GreeksData(InstrumentId.from_str(data["instrument_id"]), data["ts_event"], data["ts_init"], data["delta"])
def to_bytes(self):
return msgspec.msgpack.encode(self.to_dict())
@classmethod
def from_bytes(cls, data: bytes):
return cls.from_dict(msgspec.msgpack.decode(data))
def to_catalog(self):
return pa.RecordBatch.from_pylist([self.to_dict()], schema=GreeksData.schema())
@classmethod
def from_catalog(cls, table: pa.Table):
return [GreeksData.from_dict(d) for d in table.to_pylist()]
@classmethod
def schema(cls):
return pa.schema(
{
"instrument_id": pa.string(),
"ts_event": pa.int64(),
"ts_init": pa.int64(),
"delta": pa.float64(),
}
)
```
#### Publishing and receiving data
Here is an example of publishing and receiving data using the `MessageBus` from an actor or strategy:
```python
register_serializable_type(GreeksData, GreeksData.to_dict, GreeksData.from_dict)
def publish_greeks(self, greeks_data: GreeksData):
self.publish_data(DataType(GreeksData), greeks_data)
def subscribe_to_greeks(self):
self.subscribe_data(DataType(GreeksData))
def on_data(self, data):
if isinstance(data, GreeksData):
print("Data", data)
```
#### Writing and reading data using the cache
Here is an example of writing and reading data using the `Cache` from an actor or strategy:
```python
def greeks_key(instrument_id: InstrumentId):
return f"{instrument_id}_GREEKS"
def cache_greeks(self, greeks_data: GreeksData):
self.cache.add(greeks_key(greeks_data.instrument_id), greeks_data.to_bytes())
def greeks_from_cache(self, instrument_id: InstrumentId):
return GreeksData.from_bytes(self.cache.get(greeks_key(instrument_id)))
```
#### Writing and reading data using a catalog
For streaming custom data to feather files or writing it to parquet files in a catalog
(`register_arrow` needs to be used):
```python
register_arrow(GreeksData, GreeksData.schema(), GreeksData.to_catalog, GreeksData.from_catalog)
from nautilus_trader.persistence.catalog import ParquetDataCatalog
catalog = ParquetDataCatalog('.')
catalog.write_data([GreeksData()])
```
### Creating a custom data class automatically
The `@customdataclass` decorator enables the creation of a custom data class with default
implementations for all the features described above.
Each method can also be overridden if needed. Here is an example of its usage:
```python
from nautilus_trader.model.custom import customdataclass
@customdataclass
class GreeksTestData(Data):
instrument_id: InstrumentId = InstrumentId.from_str("ES.GLBX")
delta: float = 0.0
GreeksTestData(
instrument_id=InstrumentId.from_str("CL.GLBX"),
delta=1000.0,
ts_event=1,
ts_init=2,
)
```
#### Python-only custom data with the PyO3 catalog
To use custom data with the Rust-backed catalog (`ParquetDataCatalog` from `nautilus_pyo3`), use the
`@customdataclass_pyo3()` decorator instead of `@customdataclass`. This adds the methods the Rust catalog
expects (JSON and Arrow IPC serialization). After defining your class, register it once. You can pass either
the **type** (recommended) or a **sample instance**:
```python
from nautilus_trader.core.nautilus_pyo3 import ParquetDataCatalog
from nautilus_trader.core.nautilus_pyo3.model import CustomData
from nautilus_trader.core.nautilus_pyo3.model import DataType
from nautilus_trader.core.nautilus_pyo3.model import register_custom_data_class
from nautilus_trader.model.custom import customdataclass_pyo3
@customdataclass_pyo3()
class MarketTickPython:
symbol: str = ""
price: float = 0.0
volume: int = 0
# Register by type (no instance needed; call once, e.g. at startup)
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("MarketTickPython")
ticks = [item.data for item in result]
```
See `nautilus_trader.model.custom.customdataclass_pyo3` for details.
#### Custom data type stub
For better IDE code suggestions, you can create a `.pyi`
stub file with the proper constructor signature for your custom data types as well as type hints for attributes.
This is particularly useful when the constructor is dynamically generated at runtime, as it allows the IDE to recognize
and provide suggestions for the class's methods and attributes.
For instance, if you have a custom data class defined in `greeks.py`, you can create a corresponding `greeks.pyi` file
with the following constructor signature:
```python
from nautilus_trader.core import Data
from nautilus_trader.model import InstrumentId
class GreeksData(Data):
instrument_id: InstrumentId
delta: float
def __init__(
self,
ts_event: int = 0,
ts_init: int = 0,
instrument_id: InstrumentId = InstrumentId.from_str("ES.GLBX"),
delta: float = 0.0,
) -> GreeksData: ...
```
## Related guides
- [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.
# DST
Source: https://nautilustrader.io/docs/latest/concepts/dst/
**Deterministic simulation testing (DST)** runs NautilusTrader under a seed-controlled runtime so that
timing-sensitive execution behavior is bitwise reproducible from a single integer. This guide
explains what DST is, how NautilusTrader supports it, what guarantees the support provides, and
where those guarantees stop.
The goal is a published contract that external users and auditors can verify: the determinism
NautilusTrader claims is backed by source-level evidence and enforced at commit time by a pre-
commit hook that runs in continuous integration.
## Introduction
### What DST is
DST is a testing technique for concurrent systems. A single seed fully determines an execution,
including task scheduling, timer firings, and random values. Two runs with the same seed, binary,
and configuration produce identical observable behavior. When a property fails, the seed is the
reproduction: the same seed replays the failure every time.
Scheduling decisions in an async runtime come from ambient process state: task wake order,
timer resolution, thread scheduling, hash seeds. None of that is controlled by the test harness,
which is why a race that surfaces once in CI is usually hard to reproduce on demand. DST
replaces those ambient sources with a seeded pseudorandom sequence, so the interleaving is a
function of the seed.
FoundationDB applied the pattern to a production distributed database starting around 2009;
in the Rust ecosystem, [madsim](https://github.com/madsim-rs/madsim) intercepts `tokio`
primitives to provide a deterministic scheduler.
The bugs DST targets are the ones that escape unit, integration, property, and acceptance
testing: channel wakeup ordering, drain races at shutdown, startup sequencing, reconciliation
ordering, recovery-path correctness. All involve interleavings that other test layers cannot
exhaustively cover but a deterministic scheduler can explore systematically.
### What this guide covers
NautilusTrader's DST support has two halves:
- **The contract**: what the runtime guarantees under seed-controlled execution, and under which
conditions.
- **The enforcement**: the source-level seams that implement the contract and the pre-commit
hook that keeps them in place.
## Goals
- **Seed-reproducible execution** for the in-scope portion of the NautilusTrader runtime.
- **Honest scope**. The contract lists what is covered and what is not. No silent fallbacks to
real wall-clock time or unseeded RNG; conditions that weaken the guarantee are enumerated.
- **Enforcement in source**. A pre-commit hook fails commits that add banned patterns to the DST
path, so the contract stays true without relying on reviewer attention.
- **Minimum necessary instrumentation**. The seams route time, task scheduling, and randomness
through a deterministic source only where the contract requires it; everything else runs
unchanged.
## Approach
`madsim` determinizes only the `tokio` primitives that route through its aliased submodules
(`time`, `task`, `runtime`, `signal`). Wall-clock reads, monotonic reads, RNG draws, hash
iteration, and `select!` polling bypass `tokio` entirely and need their own seams. Layer 1
swaps the aliased submodules for `madsim`; Layer 2 supplies the seams.
### Layer 1: runtime swap
Under the `simulation` Cargo feature on `nautilus-common`, four `tokio` submodules are routed
through `madsim` when `RUSTFLAGS="--cfg madsim"` is set:
- `time` (timers, intervals, monotonic `Instant`).
- `task` (spawning and joining async tasks).
- `runtime` (the runtime builder and handle).
- `signal` (process signals such as `ctrl_c`; re-export is available, call-site adoption is
partial as noted under [scope boundaries](#signal-handling)).
These re-exports live in `nautilus_common::live::dst`. DST-path call sites for `time`, `task`,
and `runtime` import from this module rather than directly from `tokio`, so toggling the feature
switches the async runtime in one place for the primitives that are fully routed. Under normal
builds, the re-exports resolve to real `tokio`. Under `simulation` + `cfg(madsim)`, they resolve
to `madsim`'s deterministic counterparts.
Everything else that `tokio` provides (`sync`, `io`, `select!` as a macro, `fs`, `net`) uses real
`tokio` unconditionally. Transitive crates (`tokio-tungstenite`, `tokio-rustls`, `reqwest`) are
unaffected.
### Layer 2: nondeterminism substitution
Nondeterminism outside the async runtime is redirected through explicit seams:
- **Wall-clock reads** go through `nautilus_core::time::duration_since_unix_epoch`. Under
simulation this routes to `madsim::time::TimeHandle::try_current()`, preserving Unix-epoch
semantics for order and fill timestamps. When called outside a madsim runtime (plain
`#[rstest]` test bodies), it falls back to `SystemTime::now()`, which under `cfg(madsim)` is
libc-intercepted to the same real syscall a normal build would use. Production paths under
simulation always run inside a runtime, so they continue to receive virtual time.
- **Monotonic reads** go through `nautilus_common::live::dst::time::Instant`. The type resolves
to `tokio::time::Instant` on normal builds (for compatibility with `tokio::test(start_paused)`
test helpers) and `madsim::time::Instant` under simulation.
- **Network-local monotonic reads** go through `nautilus_network::dst::time`. The crate sits
below `nautilus-common` in the dependency graph and exposes a local re-export module with the
same semantics.
- **Hash iteration order** in the reconciliation manager and the order matching engine uses
`IndexMap` and `IndexSet` rather than `AHashMap` and `AHashSet`. `AHash` randomizes its hasher
per process; insertion-order iteration is needed where order drives downstream event
publication or the sequence in which a seeded `FillModel` RNG is consumed.
- **`tokio::select!` polling order** uses the `biased;` modifier at every production site on the
DST path. Unbiased `select!` polls branches in an order chosen by an unintercepted RNG.
## Determinism contract
Under the conditions below, a run identified by `(seed, binary hash, configuration hash)` on the
same platform produces bitwise-identical:
1. Scheduling order of async tasks.
2. Timer firings (virtual monotonic and virtual wall-clock).
3. RNG output from `madsim::rand`.
4. Channel delivery order on `tokio` primitives.
### Required conditions
The contract holds only when all of the following are true:
1. The `simulation` Cargo feature is active and `RUSTFLAGS="--cfg madsim"` is set. Both are
required. The feature activates the deterministic runtime; the cfg flag activates `madsim`'s
libc-level `clock_gettime` and `getrandom` intercepts. One without the other silently falls
back to real `tokio` and breaks determinism without an error.
2. Every `tokio::select!` call site on the DST path uses the `biased;` modifier.
3. Monotonic time reads route through the DST seam (either `nautilus_common::live::dst::time` or
`nautilus_network::dst::time`), not `std::time::Instant::now` directly.
4. Wall-clock time reads route through `nautilus_core::time::duration_since_unix_epoch`.
5. Randomness routes through `madsim::rand`. `rand::thread_rng`, `rand::rng()`, `fastrand`,
`getrandom`, and `OsRng` are not intercepted.
6. Iteration-order-sensitive collections use `IndexMap` or `IndexSet`, not `AHashMap` or
`AHashSet`.
7. `tokio::task::LocalSet` construction is cfg-gated out under simulation. `madsim` does not
provide `LocalSet`; `spawn_local` works without it.
8. `tokio::task::spawn_blocking` call sites are cfg-gated or removed. A blocking call escapes
the deterministic scheduler.
## Static enforcement
Static enforcement has two layers:
- Clippy policy in `clippy.toml` and `[workspace.lints.clippy]` blocks APIs that are invalid
across the workspace DST contract: direct `getrandom::{fill,u32,u64}` calls and
`tokio::task::LocalSet`.
- A pre-commit hook named `check-dst-conventions` enforces scoped, path-aware, and cfg-aware
structural checks that Clippy cannot express cleanly.
The hook lives at `.pre-commit-hooks/check_dst_conventions.sh`, runs as part of the standard
pre-commit suite, and runs in continuous integration. It covers the 16 in-scope workspace crates
and fails the commit when it detects any of:
- Raw `std::time::Instant::now()`, `SystemTime::now()`, or `chrono::Utc::now()` reads,
including bare forms when the enclosing file imports the type from `std::time`, or from
`chrono` for `Utc`.
- Raw RNG usage (`rand::thread_rng`, `rand::rng()`, `fastrand::`, `getrandom::`, `OsRng`)
or `Uuid::new_v4()` without cfg gating.
- `tokio::select!` blocks missing `biased;` within the first three lines.
- `std::thread::spawn`, `std::thread::Builder::new`, or `tokio::task::spawn_blocking` calls that
lack a preceding `#[cfg(test)]`, `#[cfg(not(madsim))]`, or
`#[cfg(not(all(feature = "simulation", madsim)))]` attribute.
- `AHashMap` or `AHashSet` in iteration-order-sensitive files on the DST path. The full set
of files is under audit; enforcement currently covers `crates/live/src/manager.rs` and
`crates/execution/src/matching_engine/engine.rs`, and expands as further files are reviewed.
- Direct `tokio::net::TcpStream::connect` / `tokio::net::TcpListener::bind` reaches that
bypass `nautilus_network::net`. The seam re-exports `tokio::net` types under normal builds
and swaps to `turmoil::net` under the `turmoil` feature, so all TCP entry points share a
single cfg-gated swap point.
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, progress reporting in the
DeFi module).
Test files, files under `tests/`, `python/`, and `ffi/` directories, and lines inside an inline
`#[cfg(test)]` module are excluded because they are not part of the DST path.
### In-scope crates
The hook applies to the 16 workspace crates in the transitive closure of `nautilus-live`:
- `analysis`, `common`, `core`, `cryptography`, `data`, `execution`, `indicators`, `live`,
`model`, `network`, `persistence`, `portfolio`, `risk`, `serialization`, `system`, `trading`.
Adapter crates and infrastructure crates (Redis, Postgres) are out of scope. Their DST
suitability requires a separate audit before they enter the DST path.
## Network seed soaks
The `nautilus-network` Turmoil tests use two layers:
- Fixed-seed tests run in the nightly test suite. These cover connect, reconnect, partition,
close during reconnect, close during backoff, and repeated server-drop scenarios with
reproducible seeds.
- An ignored reconnect soak sweeps Turmoil seeds until stopped, or until
`NAUTILUS_TURMOIL_SOAK_COUNT` seeds have run. Each seed runs the Tungstenite WebSocket backend
first and the Sockudo backend second when `transport-sockudo` is enabled, so both backends see
the same schedule search path.
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
```
The soak uses deterministic seed sweep, random node order, randomized link latency, repeated
server-side drops, reconnect state cycling, and exact application-message order checks. It does
not enable Turmoil `fail_rate`: for TCP, that breaks links without a retransmit model, which
would overstate the client delivery contract for an order-preservation test.
These Turmoil tests run against the simulated network and are not gated to Linux. Several real
localhost socket and WebSocket unit tests use `target_os = "linux"` for CI stability, so macOS
local runs do not exercise that host TCP coverage. Use macOS for the Turmoil seed sweep and use
Linux CI, or a Linux workstation, before treating the full network test set as covered.
## Implementation notes
Concrete changes the DST audit produced in this repository. Use this as the starting point
when investigating whether a code path is on the DST path and how it routes today.
### Iteration-order seams
Production sites where `AHashMap` / `AHashSet` flipped to `IndexMap` / `IndexSet` because
the iteration order is observable on the DST path:
- **Matching engine** (`crates/execution/src/matching_engine/engine.rs`): nine fields
(`execution_bar_types`, `execution_bar_deltas`, `account_ids`, `cached_filled_qty`,
`bid_consumption`, `ask_consumption`, `queue_ahead`, `queue_excess`, `queue_pending`).
Iterated removes use `.shift_remove()`. Closes
[#3914](https://github.com/nautechsystems/nautilus_trader/issues/3914).
- **Reconciliation manager** (`crates/live/src/manager.rs`): hook-enforced, plus
`ReconciliationResult.orders` and `ReconciliationResult.fills`.
- **Account trait** (`crates/model/src/accounts/`): `balances`, `balances_total`,
`balances_free`, `balances_locked`, `starting_balances` returns. Storage fields on
`BaseAccount` and `MarginAccount` are `IndexMap`.
- **Position events** (`crates/model/src/position.rs`): `Position::commissions` flipped
to `IndexMap` (consumed via `.values()` in `events/position/snapshot.rs`).
- **Portfolio aggregation** (`crates/portfolio/src/portfolio.rs`): `unrealized_pnls`,
`realized_pnls`, `net_positions` storage; `accumulate_mark_values` builds
`IndexMap`.
- **Data engine** (`crates/data/src/engine/`): `book_snapshot_counts`, `bar_aggregators`,
`BookSnapshotInfos`. Iterated removes use `.shift_remove()`.
- **Execution engine** (`crates/execution/src/engine/`): `ExecutionEngine.clients`, plus
the `client_ids` / `venues` accumulators in `get_clients_for_orders()`.
- **Trading algorithm** (`crates/trading/src/algorithm/core.rs`):
`strategy_event_handlers` (drives ordered `msgbus::unsubscribe_*` fan-out).
- **Analyzer** (`crates/analysis/src/analyzer.rs`): `account_balances`,
`account_balances_starting`.
- **Cache API** (`crates/common/src/cache/mod.rs`): `get_orders_for_ids` and
`get_positions_for_ids` sort their `Vec` returns by `client_order_id` / `position_id`
before returning. Storage stays on `AHashSet` (set semantics).
Remaining `AHashMap` / `AHashSet` sites in the in-scope crates are lookup-only, behind
concurrent shared-ownership wrappers (`Arc`, `AtomicMap`), or feed into
commutative aggregation. Any new in-scope site that drives observable iteration order
is a regression that the per-area audit guards against.
### Time seams
`Instant::now` / `SystemTime::now` call sites that remain on the DST path are either
inside `#[cfg(test)]`, file-allowlisted in the hook, or carry an inline `// dst-ok`
marker with a reason:
- `crates/common/src/testing.rs:81,108` `wait_until` / `wait_until_async`
- `crates/execution/src/engine/mod.rs:822,847` init log timing
- `crates/common/src/cache/mod.rs:569,904,3895` log and audit timing (file-allowlisted)
- `crates/model/src/defi/reporting.rs:59,123` progress logging (file-allowlisted)
- `crates/core/src/time.rs` seam definition site (file-allowlisted)
`chrono::Utc::now` is hook-banned in the in-scope crates. The remaining call sites are
the logging bridge and writer (scoped out under "Logging runs on real OS threads"). The
`crates/core/src/datetime.rs::is_within_last_24_hours` helper used to reach
`chrono::Utc::now` from non-logging paths; it now routes through
`nautilus_core::time::nanos_since_unix_epoch()` and compares in `u64` nanos directly.
### Randomness seams
Production RNG sites on the DST path:
- `crates/core/src/uuid.rs::UUID4::new()` routes through `madsim::rand::thread_rng()`
when called inside a madsim runtime under simulation, falling back to `rand::rng()`
outside one (and on normal builds). Production paths under simulation always run
inside a runtime, so they consume seeded bytes; plain `#[rstest]` tests under
`cfg(madsim)` use the host RNG. Reachable from order and event factories in
`nautilus-common` and `nautilus-risk`.
- `crates/execution/src/models/fill.rs::default_std_rng()` routes the same way. Called
from `ProbabilisticFillState::new()` when no seed is provided. With a seed,
`StdRng::seed_from_u64` is deterministic by construction.
- `crates/execution/src/matching_engine/ids_generator.rs:167,179` uses
`nautilus_core::UUID4::new()` for the `use_random_ids` path. The default ID scheme
(`{venue}-{raw_id}-{count}`) is deterministic without it.
Allowed-with-marker: `crates/network/src/backoff.rs:105` for reconnect jitter,
`// dst-ok` (transport layer).
### Tokio submodule split
`madsim` aliases `time`, `task`, `runtime`, and `signal`. Other tokio submodules
(`sync`, `io`, `select!`, `fs`, `net`) stay on real tokio under simulation. Extending
the swap further would require rebuilding `tokio-tungstenite`, `tokio-rustls`, and
`reqwest` against shimmed `tokio::net::TcpStream`, which the audit ruled out as too
invasive.
In-scope sites that touch real `tokio::net` / `tokio::io` directly:
- `crates/network/src/net.rs:37` re-exports `tokio::net::{TcpListener, TcpStream}`
- `crates/network/src/socket/client.rs:46,356` `tokio::io::{AsyncReadExt, AsyncWriteExt}`
- `crates/network/src/tls.rs:22` `tokio::io::{AsyncRead, AsyncWrite}`
- `crates/network/src/websocket/types.rs:26,29` aliases `MaybeTlsStream`
These run on real sockets even under simulation. Channel delivery order on
`tokio::sync` stays deterministic because the sender and receiver tasks are scheduled
by the madsim executor even though the channel implementation is real.
### 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 (e.g. 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 init 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` (eight tests).
- `crates/common/src/logging/macros.rs::tests` (two tests).
`logger.rs::tests::sim_tests::test_init_under_madsim_skips_writer_thread_and_forces_bypass`
runs under simulation and pins the gated behaviour.
## Scope boundaries
The contract is deliberately narrow. The following weakenings are explicit, not oversights.
### Python is not in DST scope
DST runs under a native Rust test harness. No Python interpreter starts during a DST run. The
PyO3 bindings under `crates/*/src/python/`, the `ffi/` directories, and the Python packages
under `nautilus_trader/` are excluded from the contract as a policy, not as a weakness. Any
code reachable only from Python call paths is out of scope; any Rust path reachable from the
native DST harness must satisfy the contract even if the same type is also exported to Python.
The `check-dst-conventions` hook encodes this policy by skipping `/python/` and `/ffi/` paths
in the in-scope crates. Clock, RNG, and threading call sites behind those paths do not apply
to the contract.
The primary objective of DST is reliability of the Rust engine itself: the order lifecycle,
reconciliation, matching, risk, and execution state machines. Deterministic replay of user
strategies is a later, secondary goal that becomes available once strategies are authored in
Rust or run through a Rust-native test harness. In the meantime, a Python strategy that calls
`time.time()`, issues arbitrary network requests, or relies on thread scheduling can vary its
command stream between runs; the Rust core will process the varying stream deterministically,
but end-to-end replay from a Python entry point is not guaranteed.
### Platform-scoped
`madsim`'s libc overrides for `clock_gettime` and `getrandom` are platform-specific.
Cross-platform bitwise reproducibility is not claimed. A seed that reproduces a failure on Linux
x86_64 may not reproduce on macOS aarch64.
### Non-aliased dependencies escape silently
Any dependency that reaches the OS through a non-aliased path (direct `libc` calls, `std::net`
bypass, crates using `fastrand` or `OsRng`) escapes the simulator without raising an error. The
in-scope crates have been audited; adapter crates and infrastructure crates require their own
audits before entering the DST path.
### Transport-layer I/O is not simulated
`tokio-tungstenite`, `tokio-rustls`, `reqwest`, `redis`, and `sqlx` use real `tokio` internally.
Under simulation, WebSocket and HTTP I/O run on real networking. This is intentional: the
initial target is order lifecycle determinism, not transport fault injection. Transport-layer
determinism would require per-crate `madsim` shims that do not exist at this time.
Test modules that drive real localhost sockets (`crates/network/src/socket/client.rs::tests`,
`::rust_tests`; `crates/network/src/websocket/client.rs::tests`, `::rust_tests`;
`crates/network/tests/websocket_proxy.rs`) are cfg-gated out under
`all(feature = "simulation", madsim)` because their production code paths reach
`dst::time::*` (madsim time primitives), which panic when called from a
`#[tokio::test]` runtime. The retry test modules (`crates/network/src/retry.rs::tests`,
`::proptest_tests`) run under simulation: each test attribute is `cfg_attr`-swapped
between `#[tokio::test(start_paused = true)]` and `#[madsim::test]`, time reads and
sleeps route through `crate::dst::time`, and explicit virtual-time advances go
through a `cfg`-gated `advance_clock` helper so the same body covers both runtimes.
### Signal handling
`nautilus_common::live::dst::signal` exposes a routed `ctrl_c` re-export. The
`crates/live/src/node.rs` run loop routes through it, so node shutdown driven by `ctrl_c` is
injectable from test code under `cfg(madsim)` via `madsim::runtime::Handle::send_ctrl_c`.
Adapter-bin entry points still call `tokio::signal::ctrl_c` directly and remain scoped out.
### 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
Adapter crates are out of scope for the initial DST contract. Each adapter has its own set of
`chrono::Utc::now`, `SystemTime::now`, `Uuid::new_v4`, and transport-layer call sites. An
adapter that enters the DST path must be audited for direct clock, RNG, and transport usage
before its behavior can be covered by the contract.
### Process-global lazy state consumes RNG bytes on first call
The contract holds within a single run-of-the-runtime. A few process-global lazy
initialisations consume RNG bytes on first call, which matters for harnesses that
run two seeded executions inside one process and compare traces.
- The `Ustr::from()` interner allocates and seeds its internal map on first use.
- `ahash::RandomState` seeds itself via `getrandom` (which `madsim` hooks under
`cfg(madsim)`) on first instance creation.
Single-runtime tests (one body invocation per seed, fresh process) are unaffected:
the consumption is part of the seeded execution and reproduces deterministically.
Same-seed equality frameworks that invoke the body twice in one process to diff
the traces will see drift between runs. Run 1 pays the lazy-init cost and consumes
RNG bytes; run 2 inherits the warm state and starts at a different offset in the
RNG sequence.
Workaround: pre-warm process-global state outside the runtime before the
comparison, for example by calling `Ustr::from("")` and constructing one
`ahash::RandomState` once at process start. Both runs then start with the warmed
state and consume the RNG sequence identically.
### Adapter factories no longer expose `Rc>`
Commit `f0ea66da15` ("Standardize adapter cache access via `CacheView`") replaced
the mutable `Rc>` parameter on `DataClientFactory::create` and
`ExecutionClientFactory::create` with a `CacheView`. `CacheView` exposes
`borrow()` for read access but does not expose the inner `Rc` handle.
This blocks DST-style harness factories that need to construct an
`OrderMatchingEngine` inline inside the factory, because
`OrderMatchingEngine::new` still takes `Rc>` and there is no
public accessor to recover that handle from a `CacheView`.
Workarounds:
- Pin the harness consumer to a commit before `f0ea66da15`.
- Refactor the harness so it builds the `OrderMatchingEngine` outside the
factory (where the kernel `Cache` handle is still reachable) and passes the
constructed engine into the client.
A longer-term escape hatch would be either an accessor on `CacheView` for the
inner handle or an alternative `OrderMatchingEngine` constructor that accepts a
`CacheView`. Neither is in the tree today.
## 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's unique value is in the intersection of async concurrency and state-machine correctness.
Bugs such as "a message at shutdown is dropped under a specific wakeup ordering" or "a
reconciliation event is lost when iteration order reverses" are the target class. For anything
else, the pre-existing test layers are the right tool.
## Status
As of the current state of this repository:
- Layer 1 (runtime swap) is implemented. `nautilus_common::live::dst` exposes routed re-exports
for `time`, `task`, `runtime`, and `signal`. Production call sites for `time`, `task`, and
`runtime` route through the seam; signal call-site adoption is partial (see "Signal handling"
under "Scope boundaries").
- Layer 2 (nondeterminism substitution) is implemented across the 16 in-scope crates. Seams
exist for wall-clock time, monotonic time, randomness, and iteration order. The audit
closures and remaining allowed call sites are enumerated under "Implementation notes".
- Static enforcement via `check-dst-conventions` is active in pre-commit and CI. The hook
covers the load-bearing conditions; the `// dst-ok` marker convention permits per-line
exceptions when justified.
- Build-and-test smoke gate under `cfg(madsim)` runs via the `dst` workflow
(`.github/workflows/dst.yml`, invokes `make cargo-test-sim`). It compiles the in-scope
crates with `--features simulation` and runs every test that is sim-compatible today.
Crates that consume `nautilus-model` types (`nautilus-common`, `nautilus-execution`)
also run a second leg with `--features "simulation,high-precision"` so the seam-routed
code paths are exercised under both fixed-point widths (`QuantityRaw` / `PriceRaw` as
`u64` vs `u128`).
- All of `nautilus-common`. This leg compiles with `nautilus-core/simulation`
propagated, so the explicit `wall_clock_now` cfg branch is selected for every
test in the suite. Plain `#[rstest]` tests run outside a madsim runtime and route
through the seam's `SystemTime::now()` fallback (the same path madsim's libc shim
takes outside a runtime). The `live::dst::tests::test_dst_wall_clock_advances_with_virtual_time`
test in this leg uses `#[madsim::test]` and asserts that `nanos_since_unix_epoch`
advances with `madsim::time::sleep`, so virtual wall-clock behavior is validated
end-to-end on the common leg.
- All of `nautilus-network` (transport-bound test modules are gated out at the
source). Includes the seam pinning tests for sleep / timeout virtual time and the
rate-limiter, plus the retry suites that exercise backoff timing under virtual time.
- All of `nautilus-execution`. The matching engine, fill model, and execution-engine
state machines run under the deterministic scheduler with the seeded RNG.
- The cross-crate seam pinning tests in `nautilus-core` (`wall_clock_now` virtual
time). Each leg runs with its own crate's `--features simulation` and uses
`#[madsim::test]` where applicable, so the explicit cfg branches and virtual time
are both validated.
Together this catches drift in the cfg-gated DST seams and exercises the in-scope
state machines under the deterministic scheduler; it does not yet exercise
determinism end-to-end.
- End-to-end runtime verification (same-seed diff over an in-scope code path) is out of
scope for this repository. The structural conditions (Rule 1 to Rule 6) are enforced;
the claim that a seed reproduces identical observable behavior across runs is plausible
from the seam design but is not yet verified by a regression gate.
## Further reading
- `.pre-commit-hooks/check_dst_conventions.sh` defines the six enforcement rules in full and
documents the `// dst-ok` marker convention.
- External references: the [FoundationDB testing
philosophy](https://apple.github.io/foundationdb/testing.html), the [TigerBeetle simulation
testing blog posts](https://tigerbeetle.com/blog/), and the
[madsim repository](https://github.com/madsim-rs/madsim) for the deterministic runtime.
# 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 the concepts here as the design contract for
current development, and use the crate README for current API details.
:::
## 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`.
The store does not replace the data catalog. Market-data observations remain in the Feather
streaming catalog. The event store records the command stream, raw reports, generated events, and
metadata needed to replay how the engine reacted to that world.
## 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, before downstream handlers observe the
message. That placement matters because any handler that can mutate state must see only messages
that the event store has already accepted.
```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"]
```
**The operational steps are**:
- The producer publishes or sends a state-affecting message.
- The bus capture tap builds an event-store entry before downstream handlers run.
- The writer assigns the next `seq`, writes a batch, and advances the high-watermark after the
backend acknowledges durability.
- Handlers run after the captured entry has reached the writer boundary.
- Readers scan sealed or running backends without exposing append operations.
The writer uses a bounded channel. If the writer stalls past its configured threshold, Nautilus
halts instead of dropping entries or allowing unaudited state changes.
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. The capture adapter dedups on the
registered message identity (event id, command id), so each logical message becomes exactly one
entry and replay never applies the same event twice.
## Lifecycle options
`EventStoreConfig` remains 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 registry. Callers can
use lifecycle options to:
- Supply a custom encoder registry before the bus tap starts capture.
- Supply a backend opener that returns any `EventStore` implementation for the new run.
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 time when that ordering detail matters.
- `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`.
The current secondary indices support 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
Nautilus records three identity levels so readers can answer scope, lineage, and message identity
questions.
- `correlation_id`: the logical workflow or chain. A component `intent_id` is recorded in this field
at the dispatch boundary.
- `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
Intent["Component intent_id"] --> Correlation["correlation_id"]
Command["SubmitOrder command_id"] --> Event["OrderAccepted event_id"]
Event --> Fill["OrderFilled event_id"]
Correlation --> Command
Correlation --> Event
Correlation --> Fill
Command -. "causation_id" .-> Event
Event -. "causation_id" .-> Fill
```
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`
- `crate_versions`
- `feature_flags`
- Adapter versions
- Configuration identity:
- `config_hash`
- Registered components
- Optional 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"]
```
Operationally:
- `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. A clean tail without `RunEnded` seals as `CrashedRecovered`, a tail ending in `RunEnded`
seals as `Ended`, and a hash mismatch, gap, or structural corruption seals as `Quarantined`.
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 modes
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 can load `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.
:::
## Data marker sidecar
:::note
The marker sidecar shipped in `crates/event_store/src/markers/`. It is opt-in via
`EventStoreConfig.data_markers` (`crates/system/src/event_store.rs`) 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, beside the event-store run, 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, or say anything about runs where marker capture
was disabled.
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.
The stable contract is the marker schema, opt-in capture and reader primitives, gap-free marker
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.
These APIs **do not**:
- Open live venue clients
- Run strategies or actors
- Re-run reconciliation
- Delete files
- Replay the clock registration/cancel lifecycle
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.
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
It **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.
## Snapshot-anchored recovery
Cache snapshots are owned by the cache. The event store stores only the snapshot anchor: the
high-watermark at snapshot time plus a content-addressed reference to the snapshot 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 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 last entry actually on disk,
not 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 usable restore point remains.
## 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.
## 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, so a run whose restore path is broken cannot verify clean.
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 -- /path/to/run.redb
```
Clean output looks like:
```text
clean run_id=1700000000-cafe0001 status=Ended high_watermark=3 entries_scanned=3
```
Corrupt output includes `quarantine=not-performed`:
```text
corrupt run_id=1700000000-cafe0001 status=Ended high_watermark=3 entries_scanned=3 findings=1 quarantine=not-performed
- hash mismatch at seq 2
```
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.
:::note
The verifier reports corruption but does not mutate run files. Quarantine is an operator or supervisor policy.
:::
## Operational use today
Current alpha use is focused on local inspection and verification of run files.
Verify a run after copying or restoring it:
```bash
cargo run -p nautilus-event-store --bin verify -- ./event_store/trader-001/1700000000-cafe0001.redb
```
Increase the verifier 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(())
}
```
The verifier is read-only inspection. It reports corruption without changing the run file, so
quarantine decisions remain outside this command path.
## Relationship to DST
The event store and deterministic simulation testing (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 when
identified by:
- `seed`
- `binary_hash`
- `config_hash`
- `schema_version`
- `log`
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.
# Events
Source: https://nautilustrader.io/docs/latest/concepts/events/
Nautilus is event-driven: every state change in the system is represented
by an event object that flows through the `MessageBus` to strategy and actor
handlers. This guide covers the event types, how they are dispatched, and how
order fills produce position events.
## Event categories
| Category | Examples | Origin |
|----------|-------------------------------------------------|---------------------------------|
| Order | `OrderAccepted`, `OrderFilled`, `OrderCanceled` | `ExecutionEngine` (from venue) |
| Position | `PositionOpened`, `PositionChanged` | `ExecutionEngine` (from fills) |
| 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
priority order. The first matching handler runs, then the next level, so you
can handle events at whatever granularity you need.
### Order events
1. Specific handler (e.g. `on_order_filled`)
2. `on_order_event` (receives all order events)
3. `on_event` (receives everything)
### Position events
1. Specific handler (e.g. `on_position_opened`)
2. `on_position_event` (receives all position events)
3. `on_event` (receives everything)
### 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, the event is delivered to `on_event` instead.
## Order events
Each order event corresponds to a state transition in the
[order state machine](orders/index.md#order-state-flow). The `ExecutionEngine`
applies the event to the order, updates the `Cache`, and publishes it on the
`MessageBus`. The table below shows the primary transitions; partially filled
and triggered orders support additional transitions documented in the full
[order state flow](orders/index.md#order-state-flow).
| Event | Primary transition | Handler |
|------------------------|-------------------------------------|----------------------------|
| `OrderInitialized` | (created locally) | `on_order_initialized` |
| `OrderDenied` | Initialized -> Denied | `on_order_denied` |
| `OrderEmulated` | Initialized -> Emulated | `on_order_emulated` |
| `OrderReleased` | Emulated -> Released | `on_order_released` |
| `OrderSubmitted` | Initialized/Released -> Submitted | `on_order_submitted` |
| `OrderAccepted` | Submitted -> Accepted | `on_order_accepted` |
| `OrderRejected` | Submitted -> Rejected | `on_order_rejected` |
| `OrderTriggered` | Accepted -> Triggered | `on_order_triggered` |
| `OrderPendingUpdate` | Accepted -> PendingUpdate | `on_order_pending_update` |
| `OrderPendingCancel` | Accepted -> PendingCancel | `on_order_pending_cancel` |
| `OrderUpdated` | PendingUpdate -> Accepted | `on_order_updated` |
| `OrderModifyRejected` | PendingUpdate -> Accepted | `on_order_modify_rejected` |
| `OrderCancelRejected` | PendingCancel -> Accepted | `on_order_cancel_rejected` |
| `OrderCanceled` | PendingCancel/Accepted -> Canceled | `on_order_canceled` |
| `OrderExpired` | Accepted -> Expired | `on_order_expired` |
| `OrderFilled` | Accepted -> Filled/PartiallyFilled | `on_order_filled` |
### Common order event fields
All order events share these fields:
| Field | Description |
|--------------------|------------------------------------------|
| `trader_id` | Trader instance identifier. |
| `strategy_id` | Strategy that submitted the order. |
| `instrument_id` | Instrument for the order. |
| `client_order_id` | Client‑assigned order identifier. |
| `venue_order_id` | Venue‑assigned order identifier. |
| `account_id` | Account the order belongs to. |
| `reconciliation` | Whether generated during reconciliation. |
| `event_id` | Unique event identifier. |
| `ts_event` | Timestamp when the event occurred. |
| `ts_init` | Timestamp when the event was created. |
Individual events add type-specific fields (e.g. `OrderFilled` adds
`last_qty`, `last_px`, `trade_id`, `commission`). See the API reference
for the full field list per event type.
:::tip
Override `on_order_event` to handle all order events in one place. The specific
handlers fire first, so you can mix both approaches.
:::
## Position events
Position events are a direct consequence of fill events. The `ExecutionEngine`
processes each `OrderFilled`, updates or creates a position, and emits the
corresponding position event.
| Event | When it fires | Handler |
|---------------------|-------------------------------------------|-----------------------|
| `PositionOpened` | First fill creates a new position. | `on_position_opened` |
| `PositionChanged` | Subsequent fill changes quantity or side. | `on_position_changed` |
| `PositionClosed` | Fill reduces quantity to zero. | `on_position_closed` |
### From fill to position: the causal chain
The following diagram shows how a single `OrderFilled` event produces a
position event. This is the key 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
from the venue adapter.
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 (e.g. long 10 filled
sell 15), 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
| 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_qty` | - | ✓ | ✓ | Largest quantity held. |
| `last_qty` | ✓ | ✓ | ✓ | Quantity of the last fill. |
| `last_px` | ✓ | ✓ | ✓ | Price of the last fill. |
| `currency` | ✓ | ✓ | ✓ | Settlement currency. |
| `avg_px_open` | ✓ | ✓ | ✓ | Average entry price. |
| `avg_px_close` | - | ✓ | ✓ | Average exit price. |
| `realized_return` | - | ✓ | ✓ | Realized return as a ratio. |
| `realized_pnl` | - | ✓ | ✓ | Realized profit and loss. |
| `unrealized_pnl` | - | ✓ | ✓ | Unrealized profit and loss. |
| `duration_ns` | - | - | ✓ | 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 fill. |
| `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.
## Event subscriptions
Beyond strategy handlers, actors can subscribe to specific event streams for
instruments they do not trade. These subscriptions use the `MessageBus`
directly and do not involve the `DataEngine`.
| Method | Handler | Receives |
|------------------------------|-----------------------|--------------------------------|
| `subscribe_order_fills()` | `on_order_filled()` | All fills for an instrument. |
| `subscribe_order_cancels()` | `on_order_canceled()` | All cancels for an instrument. |
These are useful for monitoring actors that track execution quality or fill
rates across strategies without participating in order management.
For details and examples, see
[Order fill subscriptions](actors.md#order-fill-subscriptions) and
[Order cancel subscriptions](actors.md#order-cancel-subscriptions).
## Related guides
- [Orders](orders/) - Order types and state machine.
- [Positions](positions.md) - Position lifecycle and PnL.
- [Execution](execution.md) - Execution flow and risk checks.
- [Strategies](strategies.md) - Handler implementations in strategies.
- [Architecture](architecture.md) - Data and execution flow patterns.
# Execution
Source: https://nautilustrader.io/docs/latest/concepts/execution/
NautilusTrader can handle trade execution and order management for multiple strategies and venues
simultaneously (per instance). Several interacting components are involved in execution, making it
important to understand the possible flows of execution messages (commands and events).
The main execution-related components include:
- `Strategy`
- `ExecAlgorithm` (execution algorithms)
- `OrderEmulator`
- `RiskEngine`
- `ExecutionEngine` or `LiveExecutionEngine`
- `ExecutionClient` or `LiveExecutionClient`
## Execution flow
The `Strategy` base class inherits from `Actor` and contains all common data methods.
It also provides methods for managing orders and trade 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 create the necessary execution commands and send them on the message bus to the
relevant components (point-to-point). They also publish events such as `OrderInitialized`.
There is not a single linear path for every command:
- `submit_order(...)` routes to `OrderEmulator` for emulated orders, to an `ExecAlgorithm` 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 and to the `RiskEngine`
otherwise.
- Cancel and query commands can route directly to the `OrderEmulator`, `ExecAlgorithm`, or
`ExecutionEngine`, depending on the command and order state.
For new order submission, the typical flow looks like this:
`Strategy` -> `OrderEmulator` or `ExecAlgorithm` or `RiskEngine`
From there, the downstream flow is typically:
`OrderEmulator` -> `ExecAlgorithm` or `ExecutionEngine`
`ExecAlgorithm` -> `RiskEngine` -> `ExecutionEngine` -> `ExecutionClient`
This diagram illustrates message flow (commands and events) across the Nautilus execution components.
```mermaid
flowchart LR
strategy[Strategy]
emulator[OrderEmulator]
algo[ExecAlgorithm]
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
```
## Order denied reasons
A local denial (`OrderDenied`) carries a standardized `CATEGORY_CONDITION` reason code followed by
`key=value` context, for example `QUANTITY_EXCEEDS_MAXIMUM: effective_quantity=15, max_quantity=10`.
The table covers local denials emitted by the risk and execution engines. These codes are the
source of truth for locally-denied orders; venue rejections (`OrderRejected`) pass through the
venue's own text unchanged.
| Code | Description |
| ------------------------------------- | --------------------------------------------------------------------------------- |
| `CLIENT_VENUE_MISMATCH` | The execution client does not handle the order venue. |
| `CUM_MARGIN_EXCEEDS_FREE_BALANCE` | The cumulative initial margin exceeds the account free balance. |
| `CUM_NOTIONAL_EXCEEDS_FREE_BALANCE` | The cumulative order notional exceeds the account free balance. |
| `EXPIRE_TIME_IN_PAST` | The order's expire time is in the past. |
| `INSTRUMENT_NOT_FOUND` | The instrument was not found in the cache. |
| `INVALID_CLIENT_ORDER_ID` | The client order ID is invalid for the venue. |
| `INVALID_MAX_NOTIONAL_PER_ORDER` | The configured maximum notional per order is invalid. |
| `INVALID_ORDER_SIDE` | The order side is invalid for this operation. |
| `INVALID_POSITION_ID` | The supplied position ID is invalid for the order submission. |
| `MARGIN_EXCEEDS_FREE_BALANCE` | The order initial margin exceeds the account free balance. |
| `MISSING_EXPIRE_TIME` | A GTD order is missing its expire time. |
| `MISSING_TRAILING_OFFSET` | The order is missing a required trailing offset. |
| `MISSING_TRAILING_OFFSET_TYPE` | The order is missing a required trailing offset type. |
| `MISSING_TRIGGER_TYPE` | The order is missing a required trigger type. |
| `NOTIONAL_BELOW_MINIMUM` | The order notional is below the instrument minimum. |
| `NOTIONAL_EXCEEDS_FREE_BALANCE` | The order notional exceeds the account free balance. |
| `NOTIONAL_EXCEEDS_MAXIMUM` | The order notional exceeds the instrument maximum. |
| `NOTIONAL_EXCEEDS_MAX_PER_ORDER` | The order notional exceeds the configured maximum per order. |
| `NO_EXECUTION_CLIENT` | No execution client was found for the routed command. |
| `ORDER_LIST_DENIED` | The order was denied because its order list failed risk checks. |
| `ORDER_LIST_INCOMPLETE` | The order list is missing orders in the cache. |
| `POSITION_NOT_FOUND` | The position for a reduce‑only order was not found. |
| `QUANTITY_BELOW_MINIMUM` | The effective order quantity is below the instrument minimum. |
| `QUANTITY_CONVERSION_FAILED` | The order quantity could not be converted for risk checks. |
| `QUANTITY_EXCEEDS_MAXIMUM` | The effective order quantity exceeds the instrument maximum. |
| `RATE_LIMIT_EXCEEDED` | The order submission rate limit was exceeded. |
| `REDUCE_ONLY_WOULD_INCREASE_POSITION` | A reduce‑only order would increase the position. |
| `STREAM_RECONCILING` | A post‑reconnect stream reconciliation is in progress; retry once it completes. |
| `SUBMIT_FAILED` | Submitting the order to the execution client failed. |
| `TRADING_HALTED` | Trading is halted; new orders are denied. |
| `TRADING_STATE_REDUCING` | Trading is reducing; the order would increase exposure. |
| `TRAILING_STOP_CALC_FAILED` | The trailing stop trigger price could not be calculated. |
| `UNSUPPORTED_ORDER_LIST` | The venue does not support the requested order list. |
| `UNSUPPORTED_ORDER_TYPE` | The order type is not supported by the venue. |
| `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. |
| `UNSUPPORTED_TRAILING_OFFSET_TYPE` | The order's trailing offset type is not supported. |
| `VALIDATION_FAILED` | The order failed adapter validation before submission. |
## Order Management System (OMS)
An order management system (OMS) type refers to the method used for assigning orders to positions and tracking those positions for an instrument.
OMS types apply to both strategies and venues (simulated and real). Even if a venue doesn't explicitly
state the method in use, an OMS type is always in effect. The OMS type for a component can be specified
using the `OmsType` enum.
The `OmsType` enum has three variants:
- `UNSPECIFIED`: The OMS type defaults based on where it is applied (details below)
- `NETTING`: Positions are combined into a single position per instrument ID
- `HEDGING`: Multiple positions per instrument ID are supported (both long and short)
The table below describes different configuration combinations and their applicable scenarios.
When the strategy and venue OMS types differ, the `ExecutionEngine` handles this by overriding or assigning `position_id` values for received `OrderFilled` events.
A "virtual position" refers to a position ID that exists within the Nautilus system but not on the venue in
reality.
| Strategy OMS | Venue OMS | Description |
|:-------------|:----------|:------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `NETTING` | `NETTING` | The strategy uses the venue's native OMS type, with a single position ID per instrument ID. |
| `HEDGING` | `HEDGING` | The strategy uses the venue's native OMS type, with multiple position IDs per instrument ID (both `LONG` and `SHORT`). |
| `NETTING` | `HEDGING` | The strategy **overrides** the venue's native OMS type. The venue tracks multiple positions per instrument ID, but Nautilus maintains a single position ID. |
| `HEDGING` | `NETTING` | The strategy **overrides** the venue's native OMS type. The venue tracks a single position per instrument ID, but Nautilus maintains multiple position IDs. |
:::note
Configuring OMS types separately for strategies and venues increases platform complexity but allows
for a wide range of trading styles and preferences (see below).
:::
OMS config examples:
- Most cryptocurrency exchanges use a `NETTING` OMS type, representing a single position per market. It may be desirable for a trader to track multiple "virtual" positions for a strategy.
- Some FX ECNs or brokers use a `HEDGING` OMS type, tracking multiple positions both `LONG` and `SHORT`. The trader may only care about the NET position per currency pair.
:::info
Nautilus does not yet support venue-side hedging modes such as Binance `BOTH` vs. `LONG/SHORT` where the venue nets per direction.
It is advised to keep Binance account configurations as `BOTH` so that a single position is netted.
:::
### OMS configuration
If a strategy OMS type is not explicitly set using the `oms_type` configuration option,
it will default to `UNSPECIFIED`. This means the `ExecutionEngine` will not override any venue `position_id`s,
and the OMS type will follow the venue's OMS type.
:::tip
When configuring a backtest, you can specify the `oms_type` for the venue. For accuracy, match this with the OMS type used by the venue.
:::
### Custom position IDs and NETTING
Custom position IDs are only valid under `HEDGING` OMS. Under `NETTING` there is by
definition a single position per (instrument, strategy), and the engine assigns it 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.
## 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 specifically bypassed in the `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 min/max bounds.
- GTD orders have not already expired.
- `reduce_only` orders do not increase the referenced position.
- Engine-level `max_notional_per_order` limits and instrument `max_notional` limits.
- 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.
### Trading state
Additionally, the current trading state of a Nautilus system affects order flow.
The `TradingState` enum has three variants:
- `ACTIVE`: Submit and modify commands operate normally.
- `HALTED`: New submit and modify commands are denied. Cancels still pass through.
- `REDUCING`: Cancels are allowed, and only submit or modify commands that do not increase
exposure are accepted.
See the [`RiskEngineConfig` API Reference](/docs/python-api-latest/config.html#nautilus_trader.risk.config.RiskEngineConfig) for further details.
## Execution algorithms
The platform supports custom execution algorithm components and provides built-in algorithms
such as TWAP (Time-Weighted Average Price).
### TWAP (Time-Weighted Average Price)
The TWAP algorithm spreads execution evenly over a specified time horizon. It receives a
primary order representing the total size and direction, then spawns smaller child orders
executed at regular intervals.
This reduces the market impact of the full order size by spreading trade volume over time.
The algorithm will immediately submit the first order, with the final order submitted being the
primary order at the end of the horizon period.
Using the TWAP algorithm as an example (found in
`nautilus_trader/examples/algorithms/twap.py`), this example demonstrates how to initialize and
register a TWAP execution algorithm directly with a `BacktestEngine` (assuming an engine is
already initialized):
```python
from nautilus_trader.examples.algorithms.twap import TWAPExecAlgorithm
# `engine` is an initialized BacktestEngine instance
exec_algorithm = TWAPExecAlgorithm()
engine.add_exec_algorithm(exec_algorithm)
```
For this particular algorithm, two parameters must be specified:
- `horizon_secs`
- `interval_secs`
The `horizon_secs` parameter determines the time period over which the algorithm will execute, while
the `interval_secs` parameter sets the time between individual order executions. These parameters
determine how a primary order is split into a series of spawned orders.
```python
from decimal import Decimal
from nautilus_trader.model.data import BarType
from nautilus_trader.test_kit.providers import TestInstrumentProvider
from nautilus_trader.examples.strategies.ema_cross_twap import EMACrossTWAP, EMACrossTWAPConfig
# Configure your strategy
config = EMACrossTWAPConfig(
instrument_id=TestInstrumentProvider.ethusdt_binance().id,
bar_type=BarType.from_str("ETHUSDT.BINANCE-250-TICK-LAST-INTERNAL"),
trade_size=Decimal("0.05"),
fast_ema_period=10,
slow_ema_period=20,
twap_horizon_secs=10.0, # execution algorithm parameter (total horizon in seconds)
twap_interval_secs=2.5, # execution algorithm parameter (seconds between orders)
)
# Instantiate your strategy
strategy = EMACrossTWAP(config=config)
```
Alternatively, you can specify these parameters dynamically per order, determining them based on
actual market conditions. In this case, the strategy configuration parameters could be provided to
an execution model which determines the horizon and interval.
:::info
There is no limit to the number of execution algorithm parameters you can create. The parameters
must be a dictionary with string keys and primitive values (values that can be serialized
over the wire, such as ints, floats, and strings).
:::
### Writing execution algorithms
To build a custom execution algorithm, define a class that inherits from `ExecAlgorithm`.
An execution algorithm is a type of `Actor`, so it's capable of the following:
- Request and subscribe to data.
- Access the `Cache`.
- Set time alerts and/or timers using a `Clock`.
Additionally it can:
- Access the central `Portfolio`.
- Spawn secondary orders from a received primary (original) order.
Once an execution algorithm is registered, and the system is running, it will receive orders off the
messages bus which are addressed to its `ExecAlgorithmId` via the `exec_algorithm_id` order parameter.
The order may also carry the `exec_algorithm_params` being a `dict[str, Any]`.
:::warning
Because of the flexibility of the `exec_algorithm_params` dictionary, it's important to thoroughly
validate all of the key value pairs for correct operation of the algorithm (for starters that the
dictionary is not `None` and all necessary parameters actually exist).
:::
Received orders will arrive via the following `on_order(...)` method. These received orders are
known as "primary" (original) orders when being handled by an execution algorithm.
```python
from nautilus_trader.model.orders.base import Order
def on_order(self, order: Order) -> None:
# Handle the order here
```
When the algorithm is ready to spawn a secondary order, it can use one of the following methods:
- `spawn_market(...)` (spawns a `MARKET` order)
- `spawn_market_to_limit(...)` (spawns a `MARKET_TO_LIMIT` order)
- `spawn_limit(...)` (spawns a `LIMIT` order)
:::note
Additional order types will be implemented in future versions, as the need arises.
:::
Each of these methods takes the primary (original) `Order` as the first argument. By default, the
primary order quantity is reduced by the spawned `quantity`. This can be disabled by passing
`reduce_primary=False`.
:::warning
When `reduce_primary=True`, the spawned quantity must not exceed the primary order's `leaves_qty`
(remaining unfilled quantity).
:::
:::note
If a spawned order is denied or rejected before acceptance, the deducted quantity is automatically
restored to the primary order. Once accepted by the venue, the reduction is considered committed.
:::
An execution algorithm can keep spawning secondary orders, submit the remaining primary order, or
do both depending on its design. The built-in TWAP example submits the remaining primary order on
the final interval.
### Spawned orders
All secondary orders spawned from an execution algorithm will carry a `exec_spawn_id` which is
the `ClientOrderId` of the primary (original) order, and whose `client_order_id`
derives from this original identifier with the following convention:
- `exec_spawn_id` (primary order `client_order_id` value)
- `spawn_sequence` (the sequence number for the spawned order)
```
{exec_spawn_id}-E{spawn_sequence}
```
e.g. `O-20230404-001-000-E1` (for the first spawned order)
:::note
The "primary" and "secondary" / "spawn" terminology was specifically chosen to avoid conflict
or confusion with the "parent" and "child" contingent orders terminology (an execution algorithm may also deal with contingent orders).
:::
### Managing execution algorithm orders
The `Cache` provides several methods to aid in managing (keeping track of) the activity of
an execution algorithm. Calling the below method will return all execution algorithm orders
for the given query filters.
```python
def orders_for_exec_algorithm(
self,
exec_algorithm_id: ExecAlgorithmId,
venue: Venue | None = None,
instrument_id: InstrumentId | None = None,
strategy_id: StrategyId | None = None,
side: OrderSide = OrderSide.NO_ORDER_SIDE,
account_id: AccountId | None = None,
) -> list[Order]:
```
As well as more specifically querying the orders for a certain execution series/spawn.
Calling the below method will return all orders for the given `exec_spawn_id` (if found).
```python
def orders_for_exec_spawn(self, exec_spawn_id: ClientOrderId) -> list[Order]:
```
:::note
This also includes the primary (original) order.
:::
## Own order books
Own order books are L3 order books that track only your own (user) orders organized by price level, maintained separately from the venue's public order books.
### Purpose
Own order books serve several purposes:
- Monitor the state of your orders within the venue's public book in real-time.
- Validate order placement by checking available liquidity at price levels before submission.
- Help prevent self-trading by identifying price levels where your own orders already exist.
- Support advanced order management strategies that depend on queue position.
- Enable reconciliation between internal state and venue state during live trading.
### Lifecycle
Own order books are maintained per instrument and automatically updated as orders transition through their lifecycle.
Orders are added when submitted or accepted, updated when modified, and removed when filled, canceled, rejected, or expired.
Only orders with prices can be represented in own order books. Market orders and other order types without explicit prices are excluded since they cannot be positioned at specific price levels.
### Safe cancellation queries
When querying own order books for orders to cancel, use a `status` filter that **excludes** `PENDING_CANCEL` to avoid processing orders already being cancelled.
:::warning
Including `PENDING_CANCEL` in status filters can cause:
- Duplicate cancel attempts on the same order.
- Inflated open order counts (orders in `PENDING_CANCEL` remain "open" until confirmed canceled).
- Order state explosion when multiple strategies attempt to cancel the same orders.
:::
The optional `accepted_buffer_ns` many methods expose is a time-based guard that only returns
orders whose `ts_accepted` is at least that many nanoseconds in the past. When
`accepted_buffer_ns > 0`, you must also provide `ts_now`. Orders that have not yet been accepted
by the venue still have `ts_accepted = 0`, so they are included once the buffer window elapses.
To exclude those inflight orders you must pair the buffer with an explicit status filter
(for example, restrict to `ACCEPTED` / `PARTIALLY_FILLED`).
### Auditing
During live trading, own order books can be periodically audited against the cache's open and
inflight order indexes to ensure consistency. The audit verifies that closed orders are removed and
that inflight orders (submitted but not yet accepted) remain tracked during venue latency windows.
The audit interval can be configured using the `own_books_audit_interval_secs` parameter in live
trading configurations.
## Overfills
An overfill occurs when the cumulative fill quantity for an order exceeds the original order quantity.
For example, an order for 100 units that receives fills totaling 110 units has an overfill of 10 units.
### How overfills occur
Overfills can result from two fundamentally different causes:
- Duplicate fill events (a network/messaging issue).
- Genuine overfills at the matching engine (a real execution outcome).
**Genuine overfills at the matching engine**
In some cases, the matching engine actually executes more quantity than the order requested.
This is a real execution outcome, not a duplicate event:
- **Matching engine race conditions**: In fast markets with high concurrency, an order may match
against multiple counterparties nearly simultaneously before being fully removed from the book.
- **Minimum lot size constraints**: If an order's remaining quantity falls below the venue's minimum
tradeable lot, some matching engines fill the minimum lot anyway rather than leaving an untradeable remainder.
- **DEX/AMM mechanics**: Decentralized exchanges using automated market makers may have execution
mechanics where actual fill quantities differ slightly from requested due to price impact calculations.
- **Multi-fill atomicity**: Some venues do not guarantee atomic fill quantities across partial
executions, allowing aggregate fills to exceed the original order quantity.
**Duplicate fill events**
Separate from genuine overfills, the same fill event may be delivered multiple times:
- WebSocket reconnection replaying previously received events.
- The venue's internal retry or delivery guarantee mechanisms.
- API timing issues in the venue's execution reporting.
The system handles duplicate events via `trade_id` deduplication (see below), but duplicates with
different `trade_id` values require overfill handling.
**Race conditions with reconciliation**
During live trading, the system maintains state through two parallel channels:
- Real-time fill events arriving via WebSocket.
- Periodic reconciliation polling the venue for fill history and position status.
If the same fill arrives through both channels with different identifiers before deduplication
can occur, both may be applied to the order. This is particularly likely during:
- System startup when reconciliation runs while WebSocket connections are establishing.
- Network instability causing reconnections mid-fill.
- High-frequency trading where fills arrive faster than reconciliation cycles.
The likelihood of reconciliation race conditions increases when:
- **Thresholds are reduced**: The `open_check_threshold_ms` and `inflight_check_threshold_ms` settings
(both default to 5,000 ms) define how long the engine waits before acting on discrepancies.
Reducing these below the round-trip latency to your venue increases the chance of processing
a fill via reconciliation before the real-time event arrives (or vice versa).
- **Reconciliation frequency is increased**: Setting `open_check_interval_secs` or
`position_check_interval_secs` to aggressive values (e.g., 1-2 seconds) increases how often
the system polls the venue, creating more opportunities for race conditions with real-time
events.
- **Startup delay is reduced**: The `reconciliation_startup_delay_secs` setting (default 10 seconds)
provides time for WebSocket connections to stabilize before continuous reconciliation begins.
Reducing this increases the chance of duplicate fills during the startup window.
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 that each `trade_id` can only be applied once. Inside `Order.apply()`,
a hard check raises an error if the incoming fill's `trade_id` already exists on the order.
This is the invariant that prevents double-counting executions.
**Core engine path (backtest and real-time event processing)**
In the core `ExecutionEngine` (used for backtests and processing real-time fill events), before
calling `apply()`, the engine checks `Order.is_duplicate_fill()` which compares:
- `trade_id`
- `order_side`
- `last_px`
- `last_qty`
If all fields match an existing fill exactly, the event is skipped gracefully with a warning log.
This avoids raising an error for benign exact replays (e.g., from WebSocket reconnection).
If the `trade_id` matches but other fields differ ("noisy replay"), the 4-field check passes
but `Order.apply()` will raise an error due to the duplicate `trade_id`. The engine catches
this error, logs the exception with full context, and drops the fill - it does not crash.
**Live reconciliation sanitizer**
During live reconciliation, `LiveExecutionEngine` pre-filters on `trade_id` alone *before*
generating fill events. This check runs before the 4-field check described above. If a fill
report arrives with a `trade_id` that already exists on the order, it is skipped regardless
of whether the price or quantity differs. When the data does differ, a warning is logged to
alert operators to potential venue data quality issues.
This pre-filtering ensures that "noisy duplicates" from venue replays or reconciliation races
are filtered out before they can trigger model integrity errors. If a venue legitimately needs
to correct fill data, it should use proper execution report semantics rather than resending
with the same `trade_id`.
Reconciliation-generated `trade_id` values are deterministic hashes of the reconciliation fill
inputs, so a restart that replays reconciliation produces the same `trade_id` and is deduped by
this sanitizer rather than being treated as a new fill.
### Configuration
For live trading, enable overfill tolerance in the `LiveExecEngineConfig`:
```python
from nautilus_trader.live.config import LiveExecEngineConfig
config = LiveExecEngineConfig(
allow_overfills=True, # Log warning instead of rejecting
)
```
:::tip
Enable `allow_overfills=True` when trading on venues known to emit duplicate fills or when
position reconciliation races with exchange fill events are expected. Monitor the logs for
overfill warnings to identify patterns that may require venue-specific handling.
:::
:::warning
When `allow_overfills=False` (the default), rejected fills may cause position discrepancies
between the system and the venue. Use the [reconciliation](live.md#execution-reconciliation)
features to detect and resolve such discrepancies.
:::
## Reconciliation reports
The execution engine consumes four reconciliation report variants emitted by
adapters in live trading. Each variant has a different role and a different
fallback when the matching order is not yet in the local cache.
| Variant | Use case | Order missing from cache |
|------------------------|--------------------------------------------------------------|---------------------------------------|
| `OrderStatusReport` | Standalone order state update. | External order created from the report; if status is `PartiallyFilled`/`Filled`, an inferred fill is synthesised from `avg_px`/`filled_qty`. |
| `FillReport` | Standalone execution. | External order is created from the fill (`OrderType::Market`, quantity `last_qty`); the real fill is then applied so its `trade_id` and `commission` are preserved. |
| `OrderWithFills` | Order status update bundled with the fills that produced it. | External order created without an inferred fill; the supplied fills are applied first; any residual gap between `report.filled_qty` and the sum of supplied `last_qty`s is closed with an inferred fill. |
| `PositionStatusReport` | Position snapshot from the venue. | Logged; positions are derived from fills, not bootstrapped here. |
### When to use each variant
Adapters choose the variant based on what the venue's wire format actually
delivers for a given event:
- Use `OrderStatusReport` for ordinary order lifecycle updates (Accepted,
PartiallyFilled, Canceled, Expired) where fill detail arrives separately on
a different stream.
- Use `FillReport` for venues that only surface a fill for venue-initiated
closures and never open a user-level order (the canonical example is
Hyperliquid liquidations: the user receives a `userFills` entry with
`liquidation` metadata but no entry on the orders stream).
- Use `OrderWithFills` when a single venue event maps to both a status update
and one or more fills, and the adapter has both available at the same
point in time. Bundling lets the engine apply real fill metadata
(`trade_id`, `commission`) and only synthesise an inferred fill for the
residual quantity. Binance Futures uses this for exchange-generated ADL,
liquidation, and settlement orders via
`dispatch_exchange_generated_fill`.
### External order creation
When a report references an order that is not in the cache (a
venue-initiated ADL / liquidation / settlement, an order placed by a
different process, or an order that has not yet been observed locally), the
engine creates an *external order* and routes ownership to:
- The strategy that has claimed the instrument via
`register_external_order_claims`, or
- The `EXTERNAL` strategy as a default fallback.
The external order's `client_order_id` is taken from the report when
present, otherwise derived from the `venue_order_id`. The order is added to
the cache, the venue order ID index is registered, and the engine emits the
appropriate lifecycle events (`OrderAccepted`, `OrderFilled`,
`OrderCanceled`, `OrderExpired`) so positions update through the normal
event pipeline.
This means a Hyperliquid liquidation that arrives as a single `FillReport`
and a Binance ADL that arrives as a bundled `OrderWithFills` both update
the local position without any strategy-side handling.
## Related guides
- [Events](events.md) - Order and position event types and dispatch.
- [Orders](orders/) - Order types and management.
- [Positions](positions.md) - Position tracking from executions.
- [Strategies](strategies.md) - Order submission from strategies.
# Greeks
Source: https://nautilustrader.io/docs/latest/concepts/greeks/
Nautilus provides two paths for working with option Greeks
(sensitivities of option prices to changes in market variables):
1. **Venue-provided Greeks (Rust/PyO3)**: real-time Greeks streamed from venues
like Deribit, Bybit, and OKX via the `OptionGreeks` data type and the option
chain aggregation system.
2. **Local Greeks calculator (Cython/Python and Rust/PyO3)**: the `GreeksCalculator`
class computes Black-Scholes Greeks from cached market data, with support for
portfolio aggregation, shock scenarios, and beta weighting.
Either path works independently or together. 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 (shocks, beta weighting, percent Greeks).
## Venue-provided Greeks (Rust/PyO3)
### OptionGreeks
The `OptionGreeks` type represents venue-provided sensitivities for a single option
contract. It is a Rust-native type exposed to Python via 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` | Sensitivity to a 1% change in implied volatility. |
| `theta` | `float` | Daily time decay (dV/dt / 365.25). |
| `rho` | `float` | Sensitivity to a change in interest rate. |
| `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 (not custom data). Writing and querying
use the standard catalog API:
```python
catalog.write_data(greeks) # greeks: list[OptionGreeks]
greeks = catalog.query(data_cls=OptionGreeks)
```
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, and `delta` supports delta-based
strike selection through `StrikeRange.delta(target, tolerance)`.
### Core schema versus custom data
The native `OptionGreeks` fields are the canonical core schema: the five standard Greeks
(`delta`, `gamma`, `vega`, `theta`, `rho`) plus implied volatility, underlying price, open
interest, and convention. These field names are stable.
There is no single complete Greeks shape, so venue- or model-specific values such as
`vanna`, `volga`, `charm`, calibration inputs, or surface metadata belong in
[custom data](custom_data.md) rather than the native type. Optional venue fields are
nullable. Fields required to interpret the values, such as `convention`, are non-nullable
and carry defaults.
### Underlying Rust types
The core Rust implementation lives in `crates/model/src/data/greeks.rs`:
- `OptionGreekValues`: a plain struct with `delta`, `gamma`, `vega`, `theta`, `rho`
fields. Implements `Add` and `Mul` for aggregation.
- `OptionGreeks` (in `crates/model/src/data/option_chain.rs`): wraps
`OptionGreekValues` with `instrument_id`, `convention`, implied volatility fields, and
timestamps. Implements `Deref` so you can access Greeks
fields directly.
- `HasGreeks` trait: provides a `greeks()` method returning `OptionGreekValues`.
Implemented by both `OptionGreekValues` and `OptionGreeks`.
### Black-Scholes functions (Rust/PyO3)
Low-level pricing functions exposed to Python from `crates/model/src/data/greeks.rs`:
```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 vol estimate (faster convergence)
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)
```
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 calculators
### GreeksCalculator
The legacy Cython `GreeksCalculator` class in `nautilus_trader/model/greeks.pyx` computes
Black-Scholes Greeks from cached market data. A PyO3 calculator is also exposed from
`nautilus_trader.common.GreeksCalculator` for the v2 runtime surface.
Both calculators use the cache and clock and are accessible from actors or strategies.
```python
from nautilus_trader.model.greeks import GreeksCalculator # legacy Cython
# v2 PyO3: 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
)
# Both surfaces return GreeksData or None while market data is warming up.
```
Both calculators:
1. Look up the instrument and its underlying in the cache.
2. Retrieve current prices (MID preferred, LAST as fallback).
3. Look up yield curves from the cache (falls back to `flat_interest_rate`).
4. Imply volatility from the market price using `imply_vol_and_greeks`.
5. Return a `GreeksData` object with all computed values.
Missing prices return `None`, which lets strategies treat warm-up as a normal no-op path.
The v2 PyO3 surface still raises a Python exception for setup errors such as a missing
instrument definition.
For non-option instruments (futures, equities), the calculator returns a `GreeksData`
with `delta=1` (or beta-weighted delta) and no gamma/vega/theta.
**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% absolute vol increase
time_to_expiry_shock=1/365, # roll forward one day
)
```
**Volatility update**: refine implied vol from a cached starting point for faster
convergence:
```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
)
```
**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: pnl, price, delta, gamma, vega, theta
```
Filters:
- `underlyings`: list of symbol prefixes (e.g., `["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 (LONG, SHORT).
- `greeks_filter`: callable that accepts `PortfolioGreeks` per position; return
`True` to include.
### GreeksData
On the legacy Python surface, `GreeksData` is a Python custom data class
(`@customdataclass`) that carries the full context of a single instrument's Greeks
computation. It extends `Data` and supports Arrow serialization, cache storage, and
catalog persistence. The v2/PyO3 surface exposes the same core fields from Rust.
| Field | Type | Description |
|---------------------|-----------------|--------------------------------------------------------|
| `instrument_id` | `InstrumentId` | The instrument. |
| `is_call` | `bool` | True for call, False for put. |
| `strike` | `float` | Strike price. |
| `expiry` | `int` | Expiry date as YYYYMMDD integer. |
| `expiry_in_days` | `int` | Days to expiry. |
| `expiry_in_years` | `float` | Years to expiry (days / 365.25). |
| `multiplier` | `float` | Contract multiplier. |
| `quantity` | `float` | Position quantity (always 1 from `instrument_greeks`). |
| `underlying_price` | `float` | Underlying price used in calculation. |
| `interest_rate` | `float` | Interest rate used. |
| `cost_of_carry` | `float` | Cost of carry (r - dividend yield; 0 for futures). |
| `vol` | `float` | Implied volatility. |
| `pnl` | `float` | PnL relative to position entry (if position provided). |
| `price` | `float` | Model price. |
| `delta` | `float` | Delta. |
| `gamma` | `float` | Gamma. |
| `vega` | `float` | Vega (dV / 1% vol change). |
| `theta` | `float` | Theta (daily decay). |
| `itm_prob` | `float` | In‑the‑money probability. |
`GreeksData` scales to portfolio level via its `to_portfolio_greeks()` method, which
multiplies all values by the contract `multiplier`. The `*` operator applies position
quantity:
```python
position_greeks = signed_qty * instrument_greeks # returns PortfolioGreeks
```
### PortfolioGreeks
`PortfolioGreeks` is the aggregated result from `portfolio_greeks()`. It supports
addition (`+`) for combining positions and scalar multiplication (`*`) for scaling:
| Field | Type | Description |
|---------|---------|------------------------|
| `pnl` | `float` | Aggregate PnL. |
| `price` | `float` | Aggregate model value. |
| `delta` | `float` | Portfolio delta. |
| `gamma` | `float` | Portfolio gamma. |
| `vega` | `float` | Portfolio vega. |
| `theta` | `float` | Portfolio theta. |
### YieldCurveData
`YieldCurveData` stores an interest rate or dividend yield curve. The `GreeksCalculator`
looks up curves from the cache by currency code (for interest rates) or by underlying
instrument ID (for dividend yields).
```python
from nautilus_trader.model.greeks_data import YieldCurveData
import numpy as np
curve = YieldCurveData(
ts_event=0,
ts_init=0,
curve_name="USD",
tenors=np.array([0.25, 0.5, 1.0, 2.0]),
interest_rates=np.array([0.04, 0.042, 0.045, 0.048]),
)
# Callable: interpolates rate for a given tenor
rate = curve(0.75) # quadratic interpolation
```
## Choosing between the two paths
| Criterion | Venue‑provided (`OptionGreeks`) | Local calculator (`GreeksCalculator`) |
|------------------------------|----------------------------------------|------------------------------------------|
| Computation | Done by the venue | Local Black‑Scholes |
| Latency | Arrives with market data | Computed on demand |
| Venues | Deribit, Bybit, OKX | Any venue with option instruments |
| Shock scenarios | Not supported | Spot, vol, and time shocks |
| Portfolio aggregation | Manual (iterate `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 |
| Greeks available | delta, gamma, vega, theta, rho, IV, OI | delta, gamma, vega, theta, itm_prob, vol |
| Data type | `OptionGreeks` (Rust/PyO3) | `GreeksData` / `PortfolioGreeks` |
## Greek definitions
For reference, the Greeks that Nautilus computes:
| Greek | Symbol | Definition |
|------------|--------|-------------------------------------------------------------------------------|
| Delta | `d` | First derivative of option price with respect to underlying price (dV/dS). |
| Gamma | `g` | Second derivative of option price with respect to underlying price (d2V/dS2). |
| Vega | `v` | Sensitivity to a 1 percentage point change in implied volatility (dV/dVol). |
| Theta | `t` | Daily time decay: change in option price per calendar day (dV/dt / 365.25). |
| Rho | `r` | Sensitivity to a change in the risk‑free interest rate (dV/dr). |
| 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.md) - 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.
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
}
/>
:::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/
NautilusTrader deploys backtested strategies to live markets with no code changes.
The same actors, strategies, and execution algorithms run against both the backtest
engine and a live trading node.
:::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.**
:::
## Configuration
For how config structs handle defaults, `T` vs `Option` semantics, and
builder patterns, see the [Configuration](configuration.md) concept guide.
For step-by-step setup of `TradingNodeConfig`, execution engine options, strategy
configuration, and multi-venue wiring, see the
[Configure a live trading node](../how_to/configure_live_trading.md) how-to guide.
## Live execution policies
### Order command outcome policy
Live command outcomes are one of:
- Confirmed by the venue.
- Definitively rejected by the venue or refused by the venue API.
- Denied locally by Nautilus before a submit leaves the system boundary.
- Logged as unresolved while Nautilus checks the venue for the final state.
Rejection events only appear for definitive command outcomes, not for ambiguous failures:
- `OrderRejected`.
- `OrderModifyRejected`.
- `OrderCancelRejected`.
Before an order enters `SUBMITTED`, Nautilus can deny a submit locally. These failures appear
as `OrderDenied`, no `OrderSubmitted` event is emitted.
After a submit reaches the venue API, Nautilus emits `OrderRejected` when the order
response proves that the venue did not accept the order. This includes structured venue
submit rejects and API refusals where venue semantics guarantee non‑acceptance, such as
HTTP 400, 401, 403, and 429 status responses. A status code is definitive
only when venue‑specific semantics prove non‑acceptance.
| Command type | Rejection event | When it appears |
|--------------------------------------|-----------------------|-------------------------------------------------------------------|
| Submit and submit order list | `OrderRejected` | A venue or API response proves an order was not accepted. |
| Modify | `OrderModifyRejected` | The venue returns a command‑specific modify reject. |
| Cancel, cancel‑all, and batch‑cancel | `OrderCancelRejected` | The venue returns a command‑specific or per‑order cancel reject. |
A successful response can still contain per‑order failure fields, and those fields are
definitive command outcomes. A whole‑request failure without per‑order results remains
unresolved unless the target command is proven refused.
Other local validation failures are reported differently:
- Cancel, modify, cancel‑all, and batch‑cancel commands that fail local checks log warnings
and do not produce rejection events.
:::note[Ambiguous outcomes]
These failures leave the venue outcome unknown:
- Transport errors, WebSocket send failures, request timeouts, and disconnects.
- Canceled local tasks, missing acknowledgements, and server errors.
- Parse failures after a request may have reached the venue.
- Whole‑batch failures without per‑order venue results.
- In‑flight retry exhaustion for `PENDING_UPDATE` and `PENDING_CANCEL`.
- Rate limits, except create‑order API refusals treated as definitive submit rejections.
When the outcome is unknown, Nautilus logs the failure, keeps the order in its current
in‑flight state, and waits for WebSocket updates, open‑order polling, in‑flight checks, or
startup reconciliation to resolve the state.
:::
:::note[Terminology]
An **in‑flight order** is one awaiting venue acknowledgement:
- `SUBMITTED` - initial submission, awaiting accept/reject.
- `PENDING_UPDATE` - modification requested, awaiting confirmation.
- `PENDING_CANCEL` - cancellation requested, awaiting confirmation.
These orders are monitored by WebSocket updates, open‑order polling, in‑flight checks, and
startup reconciliation.
:::
For never‑acknowledged submits, the `LiveExecutionEngine` in‑flight check queries the venue.
If the order stays unconfirmed beyond `inflight_check_retries`, the engine resolves it to
`REJECTED`. Pending cancel and update outcomes remain unresolved until venue reconciliation
confirms their final state.
See the Runtime checks table below.
## Execution reconciliation
Execution reconciliation aligns the venue's actual order and position state with the
system's internal state built from events. Only the `LiveExecutionEngine` performs
reconciliation, since backtesting controls both sides.
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 allows full recovery even with short lookback windows.
:::
### Reconciliation configuration
Unless `reconciliation` is set to false, the execution engine reconciles state for each
venue at startup. The `reconciliation_lookback_mins` parameter controls how far back the
engine requests history.
:::tip
Leave `reconciliation_lookback_mins` unset. This lets the engine request the maximum
execution history the venue provides.
:::
:::warning
Executions before the lookback window still generate alignment events, but with some
information loss that a longer window would avoid. Some venues also filter or drop
older execution data. Persisting all events to the cache database prevents both issues.
:::
Each strategy can claim venue-sourced external orders and materialized reconciliation activity
for an instrument ID via the `external_order_claims` config parameter. 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"`.
These orders participate in portfolio calculations and position tracking like any other order.
:::
For all live trading options, see the `LiveExecEngineConfig` [API Reference](/docs/python-api-latest/config.html#nautilus_trader.live.config.LiveExecEngineConfig).
### 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
```
The system reconciles its state against these reports, which represent external reality:
- **Duplicate check**:
- 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.
- Infers `OrderFilled` events for missing trade reports.
- Generates external order events for unrecognized client order IDs or reports missing a client order ID.
- 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.
- Falls through a price hierarchy when generating reconciliation orders:
1. **Calculated reconciliation price** (preferred): targets the correct average position.
2. **Market mid-price**: uses the current bid-ask midpoint.
3. **Current position average**: uses the existing position's average price.
4. **MARKET order** (last resort): used only when no price data exists (no positions, no market data).
- Uses LIMIT orders when a price can be determined (cases 1-3) to preserve PnL accuracy.
- Skips zero quantity differences after precision rounding.
- **Partial window adjustment**:
- When `reconciliation_lookback_mins` is set, the window may miss opening fills.
- The system adjusts fills using lifecycle analysis to reconstruct positions accurately:
- Detects zero-crossings (position qty crosses through FLAT) to identify separate lifecycles.
- Adds synthetic opening fills when the earliest lifecycle is incomplete.
- Filters out closed lifecycles when the current lifecycle matches the venue position.
- Replaces a mismatched current lifecycle with a synthetic fill reflecting the venue position.
- Synthetic fills use calculated reconciliation prices to target correct average positions.
- See [Partial window adjustment scenarios](#partial-window-adjustment-scenarios) for details.
- **Exception handling**:
- Individual adapter failures do not abort the entire reconciliation process.
- Fill reports arriving before order status reports are deferred until order state is available.
If reconciliation fails, 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** | Venue filled an order but the engine missed the event. | Generates missing `OrderFilled` events. |
| **Multiple fills** | Order has partial fills, some missed by the engine. | Reconstructs complete fill history from venue reports. |
| **External orders** | Orders exist on venue but not in local cache. | Creates unclaimed orders with strategy ID `EXTERNAL` and tag `VENUE`. |
| **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. |
| **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 |
|-------------------------------------|------------------------------------------------------------|------------------------------------------------------|
| **Explicit submit API refusal** | API refuses create‑order before acceptance. | Emits `OrderRejected`. |
| **Ambiguous submit failure** | Submit fails without confirmed venue refusal. | Logs failure and waits for reconciliation. |
| **In‑flight submit timeout** | `SUBMITTED` remains unconfirmed beyond retry exhaustion. | Resolves to `REJECTED`. |
| **In‑flight cancel/update timeout** | `PENDING_CANCEL` or `PENDING_UPDATE` exceeds the retries. | Logs warning and remains unresolved. |
| **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. |
| **Own books audit mismatch** | Own order books diverge from venue public books. | Audits and logs inconsistencies. |
**In‑flight order timeout resolution** (venue does not respond after max retries):
| Current status | Resolved to | Rationale |
|------------------|--------------|---------------------------------------|
| `SUBMITTED` | `REJECTED` | No acceptance received from venue. |
| `PENDING_UPDATE` | *Unresolved* | Modification outcome remains unknown. |
| `PENDING_CANCEL` | *Unresolved* | Cancellation outcome remains unknown. |
**Order consistency checks** (when cache state differs from venue state):
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). |
:::note
**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.
- **`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 and open‑order loop share a single retry counter
(`_recon_check_retries`), bounded by `inflight_check_retries` and
`open_check_missing_retries` respectively. The stricter limit wins for states eligible for
terminal resolution and avoids duplicate venue queries for the same order state.
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
the retry counter resets.
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 cache all events locally.
- **Position mismatches**: External orders that predate the lookback window cause position drift.
Flatten the account before restarting to reset state.
- **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.
- **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, drop cached state or flatten accounts before restarting.
:::
### Reconciliation invariants
The reconciliation system maintains four invariants:
1. **Position quantity**: the final quantity matches the venue within instrument precision.
2. **Average entry price**: the position's average entry price matches the venue's reported price within tolerance (default 0.01%).
3. **PnL integrity**: all generated fills, including synthetic fills, use calculated prices that preserve correct unrealized PnL.
4. **ID determinism**: synthetic `trade_id` and `venue_order_id` values emitted during reconciliation are deterministic functions of the logical event. The same logical fill or position-adjustment order produces the same ID across restarts, so replayed reconciliation events dedupe against earlier runs instead of being treated as new.
These hold even when:
- The reconciliation window misses complete fill history.
- Fills are missing from venue reports.
- Position lifecycles span beyond the lookback window.
- Multiple zero-crossings have occurred.
### Partial window adjustment scenarios
When `reconciliation_lookback_mins` limits the window, the system analyzes position lifecycles
from fills and adjusts to reconstruct positions accurately.
| Scenario | Description | System behavior |
|--------------------------------------------|------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| **Complete lifecycle** | All fills from opening to current state are captured. | No adjustment. |
| **Incomplete single lifecycle** | Window misses opening fills, no zero‑crossings. | Adds synthetic opening fill with calculated price. |
| **Multiple lifecycles, current matches** | Zero‑crossings detected, current lifecycle matches venue. | Filters out old lifecycles, returns current only. |
| **Multiple lifecycles, current mismatch** | Zero‑crossings detected, current lifecycle differs from venue. | Replaces current lifecycle with a single synthetic fill. |
| **Flat position** | Venue reports FLAT regardless of fill history. | No adjustment. |
| **No fills** | Window contains no fill reports. | No adjustment, empty result. |
**Key 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.
## 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.live import LiveNodeConfig
config = LiveNodeConfig(shutdown_on_error=True)
```
Error logs suppressed by component filters or logging bypass mode still request shutdown.
The trigger is cleared and re-armed when a new kernel run starts, so a process can restart
a node without reinitializing the logging system. The per-engine
`graceful_shutdown_on_error` option has been removed; configure shutdown-on-error at the
node/kernel level instead. Shutdown-on-error observes Rust `log` records, not Python
`logging.error(...)` calls.
## Related guides
- [Configure a live trading node](../how_to/configure_live_trading.md) - Node and engine configuration.
- [Adapters](adapters.md) - Venue connectivity.
- [Execution](execution.md) - Order execution in live environments.
- [Backtesting](backtesting.md) - 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 design ensures that the main thread remains performant, avoiding potential bottlenecks caused by log string formatting or file I/O operations.
Logging output is configurable and supports:
- **stdout/stderr writer** for console output
- **file writer** for persistent storage of logs
:::info
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["log_level / log_level_file (LoggingConfig)"]
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 `log_level`/`log_level_file` in `LoggingConfig`.
- **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 `LoggingConfig` object.
By default, log events with an 'INFO' `LogLevel` and higher are written to stdout/stderr.
Log level (`LogLevel`) values include the following (matching standard log level conventions).
The following log levels are supported:
- `OFF` - Disable logging.
- `TRACE` - Most verbose; only emitted by Rust components (cannot be generated from Python).
- `DEBUG` - Detailed diagnostic information.
- `INFO` - General operational messages.
- `WARNING` - Potential issues that don't prevent operation.
- `ERROR` - Errors that may affect functionality.
:::tip
You can set `TRACE` as a filter level to capture trace logs from Rust components, even though Python code cannot emit them directly.
:::
See the `LoggingConfig` [API Reference](/docs/python-api-latest/config.html#nautilus_trader.common.config.LoggingConfig) 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.
- Optionally initialize logging via the PyO3 bridge (`use_pyo3`) to capture log events emitted by Rust components.
- Truncate existing log file on startup if it already exists (`clear_log_file`)
### Standard output logging
Log messages are written to the console via stdout/stderr writers. The minimum log level can be configured using the `log_level` parameter.
### 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.
You can specify a custom log directory using `log_directory` and/or a custom file basename using `log_file_name`.
**Log file formats:**
- `None` (default) - Plain text format with `.log` extension.
- `"json"` - JSON format with `.json` 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**:
- Enabled by specifying the `log_file_max_size` parameter (e.g., `100_000_000` for 100 MB).
- When writing a log entry would make the current file exceed this size, the file is closed and a new one is created.
- **Date-based rotation (default naming only)**:
- Applies when no `log_file_max_size` is specified and no custom `log_file_name` is provided.
- At 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 a custom `log_file_name` is provided without a `log_file_max_size`, 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**:
- Controlled by the `log_file_max_backup_count` parameter (default: 5), limiting 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|json}`
- **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}`: Full ISO 8601-compliant datetime with millisecond resolution.
- `{instance_id}`: A unique instance identifier.
- `{log|json}`: File suffix based on format setting.
**Without size-based rotation (default naming)**:
- **Format**: `{trader_id}_{%Y-%m-%d}_{instance_id}.{log|json}`
- **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|json}`: File suffix based on format setting.
- **Note**: With default naming and no size limit, logs rotate daily at UTC midnight.
**Custom naming**:
If `log_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 `log_component_levels` parameter can be used to set log levels for each component individually.
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.config import LoggingConfig
from nautilus_trader.config import TradingNodeConfig
config_node = TradingNodeConfig(
trader_id="TESTER-001",
logging=LoggingConfig(
log_level="INFO",
log_level_file="DEBUG",
log_file_format="json",
log_component_levels={ "Portfolio": "INFO" },
),
... # Omitted
)
```
For backtesting, the `BacktestEngineConfig` class can be used instead of `TradingNodeConfig`, 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`, `Warning` (or `Warn`), `Error`.
:::note
For Rust-only binaries, setting `NAUTILUS_LOG` enables lazy initialization of the logging subsystem on first use, 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 explicitly listed in `log_component_levels`. All other components are suppressed regardless of the global `log_level` or file level.
Example (Python configuration):
```python
logging = LoggingConfig(
log_level="INFO",
log_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 adapters to Warn, but allow Debug for OKX specifically
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 `log_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 `log_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.
To accommodate for such scenarios, the `LoggingConfig.log_colors` option can be set to `false`.
Disabling `log_colors` will prevent the addition of ANSI color codes to the log messages,
which avoids raw escape codes in environments without color support.
## 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 `TradingNode`,
then you can activate logging in the following way:
```python
from nautilus_trader.common.component import init_logging
from nautilus_trader.common.component import Logger
log_guard = init_logging()
logger = Logger("MyLogger")
```
See the [`init_logging` API Reference](/docs/python-api-latest/common.html) for further details.
:::warning
Only one logging subsystem can be initialized per process with an `init_logging` call. Multiple `LogGuard` instances (up to 255) can exist concurrently, and the logging thread will remain active until all guards are dropped.
:::
## LogGuard: managing log lifecycle
The `LogGuard` ensures that the logging subsystem remains active and operational throughout the lifecycle of a process.
It prevents premature shutdown of the logging subsystem when running multiple engines in the same process.
### 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.
- **Logging thread termination**: When the counter reaches zero (last `LogGuard` dropped), the logging thread is properly joined to ensure all pending log messages are written before the process terminates.
- **Maximum guards**: The system supports up to 255 concurrent `LogGuard` instances. Attempting to create more raises a `RuntimeError`.
This mechanism ensures that:
1. `LogGuard` keeps the logging thread alive and flushes on drop; abrupt termination (crashes, kill signals) can still lose buffered logs.
2. The logging thread remains active as long as any `LogGuard` exists.
3. On graceful shutdown, all buffered logs are properly flushed to their destinations.
### Why use LogGuard?
Without a `LogGuard`, any attempt to run sequential engines in the same process may result in errors such as:
```
Error sending log event: [INFO] ...
```
This occurs because the logging subsystem's underlying channel and Rust `Logger` are closed when the first engine is disposed.
As a result, subsequent engines lose access to the logging subsystem, leading to these errors.
By using a `LogGuard`, you can ensure consistent logging behavior across multiple backtests or engine runs in the same process.
The `LogGuard` retains the resources of the logging subsystem and ensures that logs continue to function correctly,
even as engines are disposed and initialized.
:::note
Using `LogGuard` is required to maintain consistent logging behavior throughout a process with multiple engines.
:::
## Running multiple engines
The following example demonstrates how to use a `LogGuard` when running multiple engines sequentially in the same process:
```python
log_guard = None # Initialize LogGuard reference
for i in range(number_of_backtests):
engine = setup_engine(...)
# Assign reference to LogGuard
if log_guard is None:
log_guard = engine.get_log_guard()
# Add actors and execute the engine
actors = setup_actors(...)
engine.add_actors(actors)
engine.run()
engine.dispose() # Dispose safely
```
### Steps
- **Initialize LogGuard once**: The `LogGuard` is obtained from the first engine (`engine.get_log_guard()`) and is retained throughout the process. This ensures that the logging subsystem remains active.
- **Dispose engines safely**: Each engine is safely disposed of after its backtest completes. The `LogGuard` remains valid after `engine.dispose()` - only the engine is cleaned up, not the logging subsystem.
- **Reuse LogGuard**: The same `LogGuard` instance is reused for subsequent engines, preventing the logging subsystem from shutting down prematurely.
### Considerations
- **Multiple LogGuards per process**: The system supports up to 255 concurrent `LogGuard` instances per process. Each guard increments a reference counter when created and decrements it when dropped.
- **Thread safety**: The logging subsystem, including `LogGuard`, is thread-safe, ensuring consistent behavior even in multi-threaded environments.
- **Automatic cleanup**: When the last `LogGuard` is dropped (reference count reaches zero), the logging thread is properly joined to ensure all pending logs are written before the process terminates.
## 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
Enable the tracing subscriber by setting `use_tracing=True` in `LoggingConfig`:
```python
from nautilus_trader.config import LoggingConfig
from nautilus_trader.config import TradingNodeConfig
config_node = TradingNodeConfig(
trader_id="TESTER-001",
logging=LoggingConfig(
log_level="INFO",
use_tracing=True,
),
... # Omitted
)
```
Alternatively, call `init_tracing()` directly:
```python
from nautilus_trader.core import nautilus_pyo3
nautilus_pyo3.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 `LoggingConfig`.
For external libraries that use the `log` crate (such as `rustls`), their events go through
the Nautilus logger and are filtered by `log_level`/`log_level_file` in `LoggingConfig`.
:::tip
`RUST_LOG` only affects crates using `tracing`. For crates using `log`, configure verbosity
via `LoggingConfig` or the `NAUTILUS_LOG` environment variable (e.g., `NAUTILUS_LOG=stdout=Debug`).
:::
:::note
The tracing subscriber can only be initialized once per process. When using `use_tracing=True` in
`LoggingConfig`, subsequent kernel creations safely skip re-initialization. Direct calls to
`init_tracing()` when already initialized will raise an error.
:::
## Platform-specific considerations
### Windows shutdown behavior
On Windows, non-deterministic garbage collection during interpreter shutdown can occasionally
prevent the logging thread from joining properly. When the last `LogGuard` is dropped, the
logging subsystem signals the background thread to close and joins it to ensure all pending
messages are written. If Python's garbage collector delays dropping the guard until after
interpreter shutdown has begun, this join may not complete, resulting in truncated logs.
This issue is tracked in GitHub [issue #3027](https://github.com/nautechsystems/nautilus_trader/issues/3027).
A more deterministic shutdown mechanism is under consideration.
## 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,
`Actor` and `Strategy` classes provide convenient methods built on top of it:
```python
def publish_data(self, data_type: DataType, data: Data) -> None:
def publish_signal(self, name: str, value, ts_event: int = 0) -> None:
```
These methods allow you to publish custom data and signals efficiently without needing to work directly with the `MessageBus` interface.
## Direct access
For advanced users or specialized use cases, direct access to the message bus is available within `Actor` and `Strategy`
classes through the `self.msgbus` reference, which provides the full message bus interface.
To publish a custom message directly, you can specify a topic as a `str` and any Python `object` as the message payload, for example:
```python
self.msgbus.publish("MyTopic", "MyMessage")
```
## 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 three primary messaging patterns available in NautilusTrader:
| **Messaging Style** | **Purpose** | **Best For** |
|:---------------------------------------------|:--------------------------------------------|:------------------------------------------------------|
| **MessageBus - Publish/Subscribe to topics** | Low‑level, direct access to the message bus | Custom events, system‑level communication |
| **Actor‑Based - Publish/Subscribe Data** | Structured trading data exchange | Trading metrics, indicators, data needing persistence |
| **Actor‑Based - Publish/Subscribe Signal** | Lightweight notifications | Simple alerts, flags, status updates |
Each approach serves different purposes. This section helps you decide which pattern to use.
### MessageBus publish/subscribe to topics
#### Concept
The `MessageBus` is the central hub for all messages in NautilusTrader. It enables a **publish/subscribe** pattern
where components can publish events to **named topics**, and other components can subscribe to receive those messages.
This decouples components, allowing them to interact indirectly via the message bus.
#### Key benefits and use cases
The message bus approach is ideal when you need:
- **Cross-component communication** within the system.
- **Flexibility** to define any topic and send any type of payload (any Python object).
- **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 don't fit within the predefined `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.
#### Quick overview code
```python
from nautilus_trader.core.message import Event
# Define a custom event
class Each10thBarEvent(Event):
TOPIC = "each_10th_bar" # Topic name
def __init__(self, bar):
self.bar = bar
# Subscribe in a component (in Strategy)
self.msgbus.subscribe(Each10thBarEvent.TOPIC, self.on_each_10th_bar)
# Publish an event (in Strategy)
event = Each10thBarEvent(bar)
self.msgbus.publish(Each10thBarEvent.TOPIC, event)
# Handler (in Strategy)
def on_each_10th_bar(self, event: Each10thBarEvent):
self.log.info(f"Received 10th bar: {event.bar}")
```
#### Full example
[MessageBus Example](https://github.com/nautechsystems/nautilus_trader/tree/develop/examples/backtest/example_09_messaging_with_msgbus)
### Actor-based publish/subscribe data
#### Concept
This approach provides a way to exchange trading specific data between `Actor`s in the system.
(note: each `Strategy` inherits from `Actor`). It inherits from `Data`, which ensures proper timestamping
and ordering of events - crucial for correct backtest processing.
#### Key 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.
- **Proper event ordering** via built-in timestamps (`ts_event`, `ts_init`) crucial for backtest accuracy.
- **Data persistence and serialization** through the `@customdataclass` decorator, integrating with NautilusTrader's data catalog system.
- **Standardized trading data exchange** between system components.
#### Considerations
- Requires defining a class that inherits from `Data` or uses `@customdataclass`.
#### Inheriting from `Data` vs. using `@customdataclass`
**Inheriting from `Data` class:**
- Defines abstract properties `ts_event` and `ts_init` that must be implemented by the subclass. These ensure proper data ordering in backtests based on timestamps.
**The `@customdataclass` decorator:**
- Adds `ts_event` and `ts_init` attributes if they are not already present.
- Provides serialization functions: `to_dict()`, `from_dict()`, `to_bytes()`, `to_arrow()`, etc.
- Enables data persistence and external communication.
#### Quick overview code
```python
from nautilus_trader.core.data import Data
from nautilus_trader.model.custom import customdataclass
@customdataclass
class GreeksData(Data):
delta: float
gamma: float
# Publish data (in Actor / Strategy)
data = 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(GreeksData, data)
# Subscribe to receiving data (in Actor / Strategy)
self.subscribe_data(GreeksData)
# Handler (this is static callback function with fixed name)
def on_data(self, data: Data):
if isinstance(data, GreeksData):
self.log.info(f"Delta: {data.delta}, Gamma: {data.gamma}")
```
#### Full example
[Actor-Based Data Example](https://github.com/nautechsystems/nautilus_trader/tree/develop/examples/backtest/example_10_messaging_with_actor_data)
### Actor-based publish/subscribe signal
#### 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.
#### Key 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 primitive data (`int`, `float`, or `str`).
- **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 can contain only **single value** of type: `int`, `float`, and `str`. That means no support for complex data structures or other Python types.
- In the `on_signal` handler, you can only differentiate between signals using `signal.value`, as the signal name is not accessible in the handler.
#### Quick overview code
```python
# Define signal constants for better organization (optional but recommended)
import types
from nautilus_trader.core.datetime import unix_nanos_to_dt
from nautilus_trader.common.enums import LogColor
signals = types.SimpleNamespace()
signals.NEW_HIGHEST_PRICE = "NewHighestPriceReached"
signals.NEW_LOWEST_PRICE = "NewLowestPriceReached"
# Subscribe to signals (in Actor/Strategy)
self.subscribe_signal(signals.NEW_HIGHEST_PRICE)
self.subscribe_signal(signals.NEW_LOWEST_PRICE)
# Publish a signal (in Actor/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 (this is static callback function with fixed name)
def on_signal(self, signal):
# IMPORTANT: We match against signal.value, not signal.name
match signal.value:
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
)
```
#### Full example
[Actor-Based Signal Example](https://github.com/nautechsystems/nautilus_trader/tree/develop/examples/backtest/example_11_messaging_with_actor_signals)
### Summary and decision guide
Here's a quick reference to help you decide which messaging style to use:
#### Decision guide: Which style to choose?
| **Use Case** | **Recommended Approach** | **Setup required** |
|:--------------------------------------------|:--------------------------------------------------------------------------------|:-------------------|
| Custom events or system‑level communication | `MessageBus` + Pub/Sub to topic | Topic + Handler management |
| Structured trading data | `Actor` + Pub/Sub Data + optional `@customdataclass` if serialization is needed | New class definition inheriting from `Data` (handler `on_data` is predefined) |
| Simple alerts/notifications | `Actor` + Pub/Sub Signal | Signal name only |
## 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 currently supported as one external backing for serializable messages. The minimum
supported Redis version is 6.2, required for
[streams](https://redis.io/docs/latest/develop/data-types/streams/) functionality.
:::
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`.
- `encoding`: the payload encoding selected from the message bus encoding policy.
- `payload`: serialized bytes encoded with the selected encoding.
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. The inbound payload type must first be registered for
streaming on the receiving message bus; unregistered types are skipped without decoding.
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 thread keeps the main 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, 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, greeks data, option chain slices, and DeFi
pool swaps are not forwarded because those types do not implement Serde serialization.
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.
### Serialization
Nautilus supports serialization for:
- All Nautilus built-in types (serialized as dictionaries `dict[str, Any]` containing serializable primitives).
- Python primitive types (`str`, `int`, `float`, `bool`, `bytes`).
You can add serialization support for custom types by registering them through the `serialization` subpackage.
```python
def register_serializable_type(
cls,
to_dict: Callable[[Any], dict[str, Any]],
from_dict: Callable[[dict[str, Any]], Any],
):
...
```
- `cls`: The type to register.
- `to_dict`: The delegate to instantiate a dict of primitive types from the object.
- `from_dict`: The delegate to instantiate the object from a dict of primitive types.
## 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::{backing::MessageBusBackingFactory, config::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 backing = RedisMessageBusConfig::default();
let message_bus_backing = backing.create(trader_id, instance_id, config.clone())?;
```
### 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` 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-native factory wiring from config to a backing remains
the caller's responsibility.
### 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 legacy Python/Cython Redis serializer and the Redis cache payload path support MessagePack and
JSON. SBE and Cap'n Proto are schema payload encodings for Rust-native external message bus egress,
not Redis cache encodings.
:::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 the `timestamps_as_iso8601` to `true`.
### Message stream keys
Message stream keys are essential for identifying individual trader nodes and organizing messages within streams.
They can be tailored to meet your specific requirements and use cases. In the context of message bus streams, a trader key is typically structured as follows:
```
trader:{trader_id}:{instance_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` string.
#### 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 `type` objects to the `types_filter` parameter in the message bus configuration,
specifying which types of messages should be excluded from external publication.
```python
from nautilus_trader.config import MessageBusConfig
from nautilus_trader.model.data import QuoteTick
from nautilus_trader.model.data import TradeTick
# Create a MessageBusConfig instance with types filtering
message_bus = MessageBusConfig(
types_filter=[QuoteTick, TradeTick]
)
```
### Stream auto-trimming
The `autotrim_mins` configuration parameter allows you to specify the lookback window in minutes for automatic stream trimming in your message streams.
Automatic stream trimming helps manage the size of your message streams by removing older messages, ensuring that the streams remain manageable in terms of storage and performance.
:::info
The current Redis implementation will maintain the `autotrim_mins` as a maximum width (plus roughly a minute, as streams are trimmed no more than once per minute).
Rather than a maximum lookback window based on the current wall clock time.
:::
## External streams
The message bus within a `TradingNode` (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
```
:::tip
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 backing = RedisMessageBusConfig {
connection_timeout: 2,
response_timeout: 2,
..Default::default()
};
```
#### Consumer node
We configure the `MessageBus` of the consumer node to receive messages from the same `"binance"`
stream. The node listens to the external stream keys when a `MessageBusExternalIngress` is injected
into the `LiveNodeBuilder`, then publishes these messages onto its 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 backing = RedisMessageBusConfig {
connection_timeout: 2,
response_timeout: 2,
..Default::default()
};
```
## Related guides
- [Actors](actors.md) - Actors use the message bus for event handling.
- [Architecture](architecture.md) - Message bus role in system architecture.
# 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_utc`, `underlying`, `multiplier`.
- `OptionSpread`, `CryptoOptionSpread`: a combination of up to 4 option legs,
each weighted by a ratio. Has `underlying`, `expiration_utc`, and
`strategy_type` (vertical, calendar, straddle, etc.). Per-leg `strike_price`
and `option_kind` live on each leg's `OptionContract`/`CryptoOption`, not on
the spread itself. Greeks are computed per leg and aggregated. Spreads are
commonly used for orders (the exchange executes as a single order), while
the individual legs appear as positions. `CryptoOptionSpread` additionally
carries `is_inverse` and `settlement_currency` for venues like Deribit.
- `BinaryOption`: has `expiration_utc` 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.identifiers 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.core import nautilus_pyo3
series_id = nautilus_pyo3.OptionSeriesId(...) # identifies the series (venue, underlying, expiry)
# Subscribe to 5 strikes above and below ATM, snapshot every 1000ms
strike_range = nautilus_pyo3.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. | `nautilus_pyo3.StrikeRange.fixed([...])` |
| `AtmRelative` | N strikes above and N below the current ATM strike. | `nautilus_pyo3.StrikeRange.atm_relative(5, 5)` |
| `AtmPercent` | All strikes within a percentage band around ATM. | `nautilus_pyo3.StrikeRange.atm_percent(0.10)` |
| `Delta` | Strikes whose call or put delta is near a target. | `nautilus_pyo3.StrikeRange.delta(0.25, 0.05)` |
For ATM-based variants, subscriptions are deferred until the ATM price is determined.
ATM is derived from the forward price embedded in venue-provided `OptionGreeks` updates
(the `underlying_price` field). It can also be seeded from an initial forward price
fetched 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/forward price is known, `Delta` is deferred like other
ATM-based 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 neighbouring 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 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 that changes the active chain. 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. A
separate PyO3 `OptionChainManager` exposes the same aggregation core to Python.
```mermaid
flowchart TD
subgraph DataEngine
DE[DataEngine]
TMR[SnapshotTimer]
end
subgraph "OptionChainManager (per series)"
MGR[OptionChainManager]
AGG[OptionChainAggregator]
ATM[AtmTracker]
end
DC[DataClient] -- QuoteTick --> DE
DC -- OptionGreeks --> DE
DE -- "handle_quote()" --> MGR
DE -- "handle_greeks()" --> MGR
MGR --> AGG
MGR --> ATM
ATM -- "forward price" --> AGG
TMR -- "timer tick" --> DE
DE -- "publish_slice()" --> MGR
MGR -- "OptionChainSlice" --> DE
DE -- publish --> MB((MessageBus))
MB -- "on_option_chain" --> S[Actor / Strategy]
DE -- "sub/unsub" --> DC
```
### Component responsibilities
#### DataEngine
Holds one `OptionChainManager` per active `OptionSeriesId`. On
`SubscribeOptionChain`, it resolves instruments from the cache, requests forward
prices for ATM-based 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 Python-facing manager
has `handle_*` methods that return whether the first ATM price bootstrapped the active
instrument set; the Rust manager performs that bootstrap internally.
#### 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 (the venue-provided forward price for that expiry). It can
be pre-seeded from an HTTP forward price response for instant bootstrap without
waiting for WebSocket ticks.
### Bootstrap and rebalancing
For ATM-based strike ranges (`AtmRelative`, `AtmPercent`), the active instrument
set cannot be determined until the ATM price is known. There are two bootstrap
paths:
**Instant bootstrap (forward price available):**
1. `DataEngine` receives `SubscribeOptionChain`, resolves all instruments for the
series from the cache, and requests forward prices from the data client.
2. When the forward 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 forward price):**
1. Same as above, but no matching forward price is found in the response.
2. The engine creates the manager with no initial ATM price. The active set is
empty and no wire subscriptions are made for the chain.
3. Bootstrap depends on relevant Greeks data already flowing from other
subscriptions (e.g., per-instrument `subscribe_option_greeks` calls). 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.
Once bootstrapped, the aggregator monitors ATM drift. On each snapshot timer tick,
the engine calls `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` | Sensitivity to a 1% change in implied volatility. |
| `theta` | `float` | Daily time decay (dV/dt / 365.25). |
| `rho` | `float` | Sensitivity to a change in interest rate. |
| `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 currently support option Greeks subscriptions:
| Adapter | Per‑instrument Greeks | Option chains |
|---------|:---------------------:|:-------------:|
| Deribit | ✓ | ✓ |
| Bybit | ✓ | ✓ |
| OKX | ✓ | - |
## See also
- [Greeks](greeks.md) - Local Greeks calculation and portfolio risk management.
- [Data](data.md) - 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 provides a high-performance order book implemented in Rust, capable of
maintaining full book state from L1 through L3 data. The `OrderBook` is the primary
component for tracking public market depth, while the `OwnOrderBook` tracks your own
orders separately, enabling filtered views that show true available liquidity.
:::note
This guide documents the Rust API. These types are also available from Python via
PyO3 bindings (`nautilus_pyo3.OrderBook`, `nautilus_pyo3.OwnOrderBook`). The v1 legacy
Cython `OrderBook` (`nautilus_trader.model.book.OrderBook`) returned by
`cache.order_book()` has a similar but not identical interface. Refer to the
API reference for differences.
:::
## Book types
`OrderBook` instances are maintained per instrument for both backtesting and live trading:
- `L3_MBO`: **Market by order** data. Tracks every order at every price level, keyed by order ID.
- `L2_MBP`: **Market by price** data. Aggregates orders by price level (one entry per price).
- `L1_MBP`: **Top-of-book** data, also known as best bid and offer (BBO). Captures only the
best prices.
:::note
Top-of-book data such as `QuoteTick`, `TradeTick` and `Bar` can also maintain `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
# L3/L2 incremental deltas
self.subscribe_order_book_deltas(instrument_id)
# Aggregated depth snapshots (up to 10 levels)
self.subscribe_order_book_depth(instrument_id)
# Full book snapshots at a timed interval
self.subscribe_order_book_at_interval(instrument_id, interval_ms=1000)
```
Each subscription type delivers data to the corresponding handler:
```python
def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None:
...
def on_order_book_depth(self, depth: OrderBookDepth10) -> None:
...
def on_order_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_px = book.get_avg_px_for_quantity(quantity, OrderSide::Buy);
// Average price and quantity for a target exposure (notional)
let (price, qty, exposure) =
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
The `book_check_integrity` function validates 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 structural constraints (any number of orders at any level).
- **All types**: Best bid must not exceed best ask (crossed book). Locked markets
(bid == ask) are considered valid.
These checks run internally during delta application. The instrument ID of incoming
deltas is also validated against the book's instrument ID, returning
`BookIntegrityError::InstrumentMismatch` on mismatch.
## Pretty printing
Both `OrderBook` and `OwnOrderBook` provide a `pprint` method that renders the book
as a human-readable table:
```rust
book.pprint(5, None);
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 when submitted
or materialized from reconciliation, updated as state changes arrive, and removed when they
close. Updates include accepted, pending update, pending cancel, partially filled, filled,
canceled, expired, rejected, and denied states as supported by the order model.
Each `OwnBookOrder` carries:
- `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 and the remaining own-book price level.
- `order_type` and `time_in_force`: Order type metadata used by filters and diagnostics.
- `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 order was accepted by the venue.
- `ts_submitted`: Timestamp when the order was submitted.
- `ts_init`: Timestamp when the order was initialized.
These fields let filtered views include or exclude own orders by status and acceptance time
(see [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 and in-flight orders so submitted
orders are not removed during normal 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
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 status = Some(AHashSet::from([OrderStatus::Accepted]));
// Only subtract ACCEPTED orders (ignore SUBMITTED, PENDING_CANCEL, etc.)
let filtered = book.filtered_view(Some(&own_book), None, status, None, None);
```
The `accepted_buffer_ns` parameter provides a grace period: when set, only orders
where `ts_accepted + buffer <= now` are included. This excludes recently accepted
orders that may not yet appear in the public book feed. The buffer applies to the
`ts_accepted` field regardless of order status. Combine with a status filter to
also exclude non-accepted orders.
```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()),
);
```
## Binary markets
For binary/prediction markets (e.g., Polymarket), instruments have two complementary
sides (YES and NO) where prices sum to 1.0. A bid on the NO side at 0.40 is
economically equivalent to an ask on the YES side at 0.60.
The `OwnOrderBook::combined_with_opposite` method handles this transformation,
merging your orders from both sides into a single view:
```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-side orders with 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.
This gives a complete picture of your own liquidity across both sides of the market.
# 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 execution semantics and deterministic time model operate in both research and
live systems. Strategies deploy from research to production with no code changes,
providing research-to-live parity and reducing the divergence that typically introduces
deployment risk.
NautilusTrader is asset-class-agnostic. Any venue with a REST API or WebSocket feed can be
integrated through modular adapters. Current integrations span crypto exchanges (CEX and
DEX), traditional markets (FX, equities, futures, options), and betting exchanges.
## Features
- **Fast**: Rust core with asynchronous networking using [tokio](https://crates.io/crates/tokio).
- **Reliable**: Type- and thread-safety backed by Rust, with optional Redis-backed state persistence.
- **Portable**: Runs on Linux, macOS, and Windows. Deploy using Docker.
- **Flexible**: Modular adapters integrate any REST API or WebSocket feed.
- **Advanced**: Time in force `IOC`, `FOK`, `GTC`, `GTD`, `DAY`, `AT_THE_OPEN`, `AT_THE_CLOSE`, advanced order types and conditional triggers. Execution instructions `post-only`, `reduce-only`, and icebergs. Contingency orders including `OCO`, `OUO`, `OTO`.
- **Customizable**: User-defined components, or assemble entire systems from scratch using the [cache](cache.md) and [message bus](message_bus.md).
- **Backtesting**: Multiple venues, instruments, and strategies simultaneously using historical quote tick, trade tick, bar, order book, 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**: Engine fast enough to train AI trading agents (RL/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 are provided via [PyO3](https://pyo3.rs), with an ongoing migration from
Cython. No Rust toolchain is required at install time.
## Use cases
There are three main use cases for this software package:
- 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`).
The codebase provides a framework for building the software layer of systems that achieve the above.
The default `backtest` and `live` system implementations live in their respectively named subpackages.
A `sandbox` environment can be built using the sandbox adapter.
:::note
- All examples will use these default system implementations.
- We consider trading strategies to be subcomponents of end-to-end trading systems, these systems
include the application and infrastructure layers.
:::
## Distributed
The platform integrates into larger distributed systems.
Nearly all configuration and domain objects serialize using JSON, MessagePack, or Apache Arrow
(Feather) for communication over the network.
## Common core
The common system core is used by all node [environment contexts](architecture.md#environment-contexts) (`backtest`, `sandbox`, and `live`).
User-defined `Actor`, `Strategy` and `ExecAlgorithm` components are managed consistently across these environment 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.
## Live trading
A `TradingNode` ingests data and events from multiple data and execution clients, supporting both
demo/paper trading accounts and real accounts. Running asynchronously on a single
[event loop](https://docs.python.org/3/library/asyncio-eventloop.html) provides high performance,
with the option to use the [uvloop](https://github.com/MagicStack/uvloop) implementation
(available for Linux and macOS) for additional throughput.
## Domain model
The platform features a trading domain model that includes various value types such as
`Price` and `Quantity`, as well as more complex entities such as `Order` and `Position` objects,
which are used to aggregate multiple events to determine state.
## Timestamps
All timestamps use nanosecond precision in UTC.
Timestamp strings follow ISO 8601 (RFC 3339) format with either 9 digits (nanoseconds) or 3 digits (milliseconds) of decimal precision,
(but mostly nanoseconds) always maintaining all digits including trailing zeros.
These can be seen in log messages, and debug/display outputs for objects.
A timestamp string consists of:
- Full date component always present: `YYYY-MM-DD`.
- `T` separator between date and time components.
- Always nanosecond precision (9 decimal places) or millisecond precision (3 decimal places) for certain cases such as GTD expiry times.
- Always UTC timezone designated by `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 platform uses Universally Unique Identifiers (UUID) version 4 (RFC 4122) for unique identifiers.
Our high-performance implementation uses the `uuid` crate for correctness validation when parsing from strings,
ensuring input UUIDs comply with the specification.
A valid UUID v4 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").
- RFC 4122 variant designation (indicated by the fourth group starting with "8", "9", "a", or "b").
Example: `2d89666b-1a1e-4a75-b193-4eb3b454c757`
For the complete specification, refer to [RFC 4122: A Universally Unique Identifier (UUID) URN Namespace](https://datatracker.ietf.org/doc/html/rfc4122).
## Data types
The following market data types can be requested historically, and also subscribed to as live streams when available from a venue / data provider, and implemented in an integrations adapter.
- `OrderBookDelta` (L1/L2/L3)
- `OrderBookDeltas` (container type)
- `OrderBookDepth10` (fixed depth of 10 levels per side)
- `QuoteTick`
- `TradeTick`
- `Bar`
- `Instrument`
- `InstrumentStatus`
- `InstrumentClose`
The following `PriceType` options can be used for bar aggregations:
- `BID`
- `ASK`
- `MID`
- `LAST`
## Bar aggregations
The following `BarAggregation` methods are available:
- `MILLISECOND`
- `SECOND`
- `MINUTE`
- `HOUR`
- `DAY`
- `WEEK`
- `MONTH`
- `YEAR`
- `TICK`
- `VOLUME`
- `VALUE` (a.k.a 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.
The price types and bar aggregations can be combined with step sizes >= 1 in any way through a `BarSpecification`.
This allows alternative bars to be aggregated for live trading.
## Account types
The following account types are available for both live and backtest environments:
- `Cash` single-currency (base currency)
- `Cash` multi-currency
- `Margin` single-currency (base currency)
- `Margin` multi-currency
- `Betting` single-currency
## Order types
The following order types are available (when possible on a venue):
- `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 value types are backed by either 128-bit or 64-bit raw integer values, depending on the
[precision mode](../getting_started/installation.md#precision-mode) used during compilation.
- `Price`
- `Quantity`
- `Money`
### High-precision mode (128-bit)
When the `high-precision` feature flag is **enabled** (default), 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 |
# Plugins
Source: https://nautilustrader.io/docs/latest/concepts/plugins/
The `nautilus-plugin` crate defines the plug-in artifact contract for NautilusTrader. It lets an
independently compiled Rust `cdylib` identify itself with versioned build metadata and a manifest,
and provides the C-ABI boundary primitives used at that boundary. It covers artifact identity and
the boundary types only; it does not load, register, or run plug-ins.
:::warning
The plug-in ABI is early alpha and the contract is unstable. Pin plug-in builds to the matching
`nautilus-plugin` version.
:::
A plug-in is a Rust `cdylib` that exports a single `nautilus_plugin_init` entry symbol. The
`nautilus_plugin!` macro generates that symbol and the static manifest that carries the build
identity:
```rust
nautilus_plugin::nautilus_plugin! {
name: "example-plugin",
vendor: "Nautech",
version: env!("CARGO_PKG_VERSION"),
}
```
Set `crate-type = ["cdylib"]` in the artifact's `Cargo.toml` and depend on the matching
`nautilus-plugin` version. See [the crate docs](https://docs.rs/nautilus-plugin) for the boundary
and manifest types.
# Portfolio
Source: https://nautilustrader.io/docs/latest/concepts/portfolio/
The Portfolio is the central hub for managing and tracking all positions across active strategies for the trading node or backtest.
It consolidates position data from multiple instruments, providing a unified view of your holdings, risk exposure, and overall performance.
## Currency conversion
The Portfolio supports automatic currency conversion for PnL and exposure calculations,
allowing you to view results in your preferred currency. This is particularly useful when
trading across multiple instruments with different settlement currencies or managing multiple
accounts with different base currencies.
### Supported conversions
Currency conversion is available for the following portfolio queries:
- `realized_pnl()` / `realized_pnls()` - Convert realized PnL to target currency.
- `unrealized_pnl()` / `unrealized_pnls()` - Convert unrealized PnL to target currency.
- `total_pnl()` / `total_pnls()` - Convert total PnL to target currency.
- `net_exposure()` / `net_exposures()` - Convert net exposure to target currency.
All methods accept an optional `target_currency` parameter to specify the desired output
currency.
### Single account behavior
When querying a single account without specifying `target_currency`, the Portfolio
automatically converts values to that account's base 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 simultaneously, behavior depends on whether you query
all instruments (`net_exposures()`) or a single instrument (`net_exposure()`):
**For `net_exposures()` (all instruments):**
- **Same base currency**: Automatically converts to the common base currency.
- **Different base currencies**: Returns a dict with multiple currencies, each converted
to its account's base currency. Provide `target_currency` for single-currency results.
**For `net_exposure()` (single instrument across accounts):**
- **Different base currencies**: Returns `None` unless you provide `target_currency`.
```python
# Scenario 1: Multiple accounts, all with USD base currency
exposures = portfolio.net_exposures(venue=BINANCE)
# Returns {USD: Money(...)}
# Scenario 2: Multiple accounts with different base currencies (USD and EUR)
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(...)}
```
### Conversion failures
When `target_currency` is provided and currency conversion fails, behavior depends on
the method type:
- **Single-value methods** (`realized_pnl`, `unrealized_pnl`, `total_pnl`, `net_exposure`):
Return `None` and log an error to prevent incorrect values.
- **Dict-returning methods** (`realized_pnls`, `unrealized_pnls`, `total_pnls`, `net_exposures`):
Omit instruments that fail conversion but return results for successful conversions.
:::warning
Exchange rate data must be available when using `target_currency` for cross-currency
aggregation.
:::
### Conversion price types
When converting exposures to a target currency, the Portfolio uses different price types
depending on the position composition:
- **All long positions**: Uses `BID` prices (conservative for long exposure).
- **All short positions**: Uses `ASK` prices (conservative for short exposure).
- **Mixed positions**: Uses `MID` prices (neutral when both long and short exist).
This ensures conversions reflect realistic market conditions where you would liquidate
long positions at bid and cover short positions at ask. For mixed positions, mid-pricing
provides a neutral valuation.
If `use_mark_xrates` is enabled in the portfolio configuration, `MARK` prices replace
`MID` prices for mixed positions and general conversions.
## Equity and mark-to-market
The Portfolio exposes three pull-style queries for continuous portfolio valuation.
Each returns per-currency results keyed by the relevant account base currency or
native settlement 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. |
| `missing_price_instruments(venue)` | Instruments currently flagged as unpriceable. |
Longs contribute positive notional, shorts contribute negative notional. Flat
positions are skipped.
### Equity formula
Equity combines the account balance with open-position valuation, using a different
second term depending on account type:
- **Cash and betting accounts**: `balances_total + Σ mark_value(open positions)`.
- **Margin accounts**: `balances_total + Σ unrealized_pnl(open positions)`.
The cash and betting path uses `mark_values()` internally. 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` 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`).
If none of the four yield a price, the position goes into the missing-price tracker
and is skipped in the sum.
### Base currency conversion
When `convert_to_account_base_currency=true` (the default) and the account has a
`base_currency` set, settlement-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 settlement currency and no xrate
conversion is applied.
If xrate data is unavailable for a required conversion, that position is treated
as unpriceable and flagged via the missing-price tracker rather than silently
valued at a 1.0 rate.
### Missing-price tracking
The tracker is a per-venue set of instrument IDs that could not be priced on the
last `mark_values()` or `equity()` call. It has two observable behaviors:
- A warning log fires once per instrument on the transition from priced to unpriced,
not on every subsequent call. The next transition back to priced clears the entry
so 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 empty quote, trade, and bar feed for one instrument
is the most common cause of silent gaps.
:::
### Venue and account scope
`mark_values` and `equity` accept an optional `account_id` to scope the
aggregation to a single account. With `account_id=None`, results aggregate
across every account on the venue.
The missing-price tracker is venue-scoped. `missing_price_instruments` takes a
venue only, and an account-filtered `mark_values(venue, account_id)` call does
not clear the venue entry so flags raised by other accounts on the same venue
survive.
## Portfolio statistics
There are a variety of built-in portfolio statistics in
`crates/analysis/src/statistics` which analyse a trading portfolio's performance
for both backtests and live trading.
The statistics are generally categorized as follows.
- PnLs based statistics (per currency)
- Returns based statistics
- Positions based statistics
- Orders based statistics
Backtest statistics are exposed after a run through `engine.get_result()`.
## Custom statistics
Custom metrics for post-run analysis can be computed from reports, snapshots, or
position data and added to the dictionaries passed to visualization APIs such as
`create_tearsheet_from_stats()`.
For example, calculate a win rate from realized PnLs:
```python
import pandas as pd
def calculate_win_rate(realized_pnls: pd.Series) -> float:
if realized_pnls.empty:
return 0.0
winners = realized_pnls[realized_pnls > 0.0]
return len(winners) / len(realized_pnls)
```
Then include the metric in offline tearsheet inputs:
```python
stats_general = {
"Win Rate": calculate_win_rate(realized_pnls),
}
```
:::tip
Your metric should handle degenerate inputs such as empty series or insufficient data.
Return `None` for unknown or incalculable values, or a reasonable default like `0.0`
when semantically appropriate.
:::
## 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
total account balance. A $900 gain on a $100,000 account reports roughly 0.9% for that day.
When the analyzer has account state history spanning at least two distinct calendar days,
it computes portfolio returns automatically and uses them as the primary series for
statistics, tearsheets, and the monthly returns heatmap. Multiple snapshots on the same
day count as one day, so intra-day trading alone does not produce portfolio returns. When
portfolio returns are unavailable, it falls back to position returns.
The convenience accessor `analyzer.returns()` resolves this preference: portfolio returns
when present, position returns otherwise.
### Multi-currency accounts
Portfolio returns require a single-currency balance history. When the account carries
balances in multiple currencies, the analyzer cannot produce a single return series and
falls back to position returns silently. Statistics and tearsheet charts use whichever
series `returns()` resolves to.
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.
### Per-venue calculation
In the backtest engine, the analyzer runs per venue (`engine.pyx`). Each venue's account
produces its own portfolio return series. The tearsheet aggregates across all cached
accounts to produce a combined return series for multi-venue backtests.
## Backtest analysis
Following a backtest run, the engine passes realized PnLs, returns, positions, and orders data to each registered
statistic. Any output is then displayed in the tear sheet under the `Portfolio Performance` heading, grouped as:
- Realized PnL statistics (per currency)
- Returns statistics (for the entire portfolio)
- General statistics derived from position and order data (for the entire portfolio)
## 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 positions work in NautilusTrader, including their lifecycle, aggregation
from order fills, profit and loss calculations, and the important concept of position snapshotting
for netting OMS configurations.
## Overview
A position represents an open exposure to a particular instrument in the market. Positions are
fundamental to tracking trading performance and risk, as they aggregate all fills for a particular
instrument and continuously calculate metrics like unrealized PnL, average entry price, and total
exposure.
The system automatically creates positions when orders fill and tracks them
from open to close. The platform supports both netting
and hedging position management styles through its OMS (Order Management System) configuration.
## Position lifecycle
### Creation
The system opens a position on the first fill:
- **NETTING OMS**: Opens on first fill for an instrument (one position per instrument).
- **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 (`LONG` or `SHORT`).
- Initial quantity and average price.
- 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 (maximum exposure reached).
- Tracks all associated 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 `NETTING` OMS, when the position later reopens, 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 # Now SHORT position
# Final BUY 50 units at $52
signed_qty = 0 # Position FLAT (closed)
```
## Position adjustments
Position adjustments track quantity or PnL changes that occur outside of normal order fills,
ensuring the position quantity accurately reflects the true net asset position. The system
generates `PositionAdjusted` events for these scenarios.
### Base currency commissions
When trading spot currency pairs (e.g., 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. These are logged with `quantity_change = None` and can include PnL impacts.
### Adjustment tracking
All adjustments are preserved in the position event history:
- `position.adjustments` returns the list of all `PositionAdjusted` events.
- Each adjustment includes type (`COMMISSION` or `FUNDING`), quantity change, and timestamps.
- The adjustment history is cleared when positions close and reopen. When events are purged,
commission adjustments tied to the removed fills are regenerated while non-commission adjustments
(for example funding) are preserved.
## OMS types and position management
NautilusTrader supports two primary OMS types that fundamentally affect how positions are tracked
and managed. An `OmsType.UNSPECIFIED` option also exists, which defaults to the component's
context. For full details, see the [Execution guide](execution.md#order-management-system-oms).
### `NETTING`
In `NETTING` mode, all fills for an instrument are aggregated into a single position:
- One position per instrument ID.
- All fills contribute to the same position.
- Position flips from `LONG` to `SHORT` (or vice versa) as net quantity changes.
- 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.
- Closed positions remain in cache history but do not reopen; new fills create new positions.
:::warning
When using `HEDGING` mode, be aware of increased margin requirements as each position
consumes margin independently. Some venues may not support true hedging mode and will
net positions automatically.
:::
### Strategy vs venue OMS
The platform allows different OMS configurations for strategies and venues:
| Strategy OMS | Venue OMS | Behavior |
|--------------|-----------|-------------------------------------------------------------|
| `NETTING` | `NETTING` | Single position per instrument at both strategy and venue. |
| `HEDGING` | `HEDGING` | Multiple positions supported at both levels. |
| `NETTING` | `HEDGING` | Venue tracks multiple, Nautilus maintains single position. |
| `HEDGING` | `NETTING` | Venue tracks single, Nautilus maintains virtual positions. |
:::tip
For most trading scenarios, keeping strategy and venue OMS types aligned simplifies
position management. Override configurations are primarily useful for prop trading
desks or when interfacing with legacy systems. See the [Live guide](live.md)
for venue-specific OMS configuration.
:::
## Position snapshotting
Position snapshotting is an important feature for `NETTING` OMS configurations that preserves
the state of closed positions for accurate PnL tracking and reporting.
### Why snapshotting matters
In a `NETTING` system, when a position closes (becomes `FLAT`) and then reopens 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 `NETTING` position closes and then receives a new fill for the same instrument, the execution
engine snapshots the closed position state before resetting it, preserving:
- Final quantities and prices.
- Realized PnL.
- All fill events.
- Commission totals.
This snapshot is stored in the cache indexed by position ID. The position then resets for the new
cycle while previous snapshots remain accessible. The Portfolio aggregates PnL across all snapshots
for accurate totals.
:::note
This historical snapshot mechanism differs from optional position state snapshots (`snapshot_positions`),
which periodically record open-position state for telemetry. See the [Live guide](live.md) for
`snapshot_positions` and `snapshot_positions_interval_secs` settings.
:::
### Example scenario
```python
# 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)
```
Without snapshotting, only the most recent cycle's PnL would be available, leading to
incorrect reporting and analysis.
## PnL calculations
NautilusTrader provides PnL calculations that account for instrument
specifications and market conventions.
### Realized PnL
Calculated when positions are partially or fully closed:
```python
# For standard instruments
realized_pnl = (exit_price - entry_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 engine automatically applies the correct formula based on position side.
### Unrealized PnL
Calculated using current market prices for open positions. The `price` parameter accepts any
reference price (bid, ask, mid, last, or mark):
```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
```
Returns `Money(0, settlement_currency)` for `FLAT` positions regardless of the price provided.
### Total PnL
Combines 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 settlement currency.
- For Forex, this is typically the quote currency.
- For inverse contracts, PnL may be in the base currency.
- Portfolio aggregates realized PnL per instrument in settlement currency.
- Multi-currency totals require conversion outside the Position class.
## Commissions and costs
Positions track all trading costs:
- 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 settlement 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 currency (standard) or base currency (inverse)
```
**Limitations:**
- Panics if inverse instrument has no `base_currency` set.
- Does not handle quanto contracts (returns quote currency instead of settlement currency).
- For quanto instruments, use `instrument.calculate_notional_value()` instead.
## 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.
### Position state
- `side`: Current position side (`LONG`, `SHORT`, or `FLAT`).
- `entry`: Direction of the currently open position (`Buy` for `LONG`, `Sell` for `SHORT`). Updates when position flips direction.
- `quantity`: Current absolute position size.
- `signed_qty`: Signed position size (positive for `LONG`, negative for `SHORT`).
- `peak_qty`: Maximum quantity reached during position lifetime.
- `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.
### 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 position was closed.
- `duration_ns`: Duration from open to close in nanoseconds.
### Associated data
- `symbol`: The instrument's ticker symbol.
- `venue`: The trading venue.
- `client_order_ids`: All client order IDs associated with position.
- `venue_order_ids`: All venue order IDs associated with position.
- `trade_ids`: All trade/fill IDs from venue.
- `events`: All order fill events applied to position.
- `event_count`: Total number of fill events applied.
- `last_event`: Most recent fill event.
- `last_trade_id`: Most recent trade ID.
:::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.Position).
:::
## Events and tracking
Positions maintain a complete history of events:
- All order fill events are stored chronologically.
- Associated client order IDs are tracked.
- Trade IDs from the venue are preserved.
- Event count indicates total fills applied.
This historical data enables:
- Detailed position analysis.
- Trade reconciliation.
- Performance attribution.
- Audit trails.
:::tip
Use `position.events` to access the full history of fills for reconciliation.
The `position.trade_ids` property helps match against broker statements.
See the [Execution guide](execution.md) for reconciliation best practices.
:::
## Numerical precision
Position calculations use 64-bit floating-point (`f64`) arithmetic for PnL and average price computations.
While fixed-point types (`Price`, `Quantity`, `Money`) preserve exact precision at configured decimal places,
internal calculations convert to `f64` for performance and overflow safety.
### Design rationale
The platform uses `f64` for position calculations to balance performance and accuracy:
- Floating-point operations are significantly faster than arbitrary-precision arithmetic.
- Raw integer multiplication can overflow even with 128-bit integers.
- Each calculation starts from precise fixed-point values, avoiding cumulative error.
- IEEE-754 double precision provides ~15 decimal digits of accuracy.
### Validated precision characteristics
Testing confirms `f64` arithmetic maintains accuracy for typical trading scenarios:
- Standard amounts: No precision loss for amounts ≥ 0.01 in standard currencies.
- High-precision instruments: 9-decimal crypto prices preserved within 1e-6 tolerance.
- Sequential fills: 100 fills show no drift (commission accuracy to 1e-10).
- Extreme prices: Handles range from 0.00001 to 99,999.99999 without overflow.
- Round-trip trades: Opening and closing at same price produces exact PnL (commissions only).
For implementation details, see `test_position_pnl_precision_*` tests in `crates/model/src/position.rs`.
:::note
For regulatory compliance or audit trails requiring exact decimal arithmetic, consider using `Decimal`
types from external libraries. Very small amounts below `f64` epsilon (~1e-15) may round to zero.
This does not affect realistic trading scenarios with standard currency precisions (typically 2-9 decimals).
:::
## Integration with other components
Positions interact with several key components:
- **Portfolio**: Aggregates positions across instruments and strategies.
- **ExecutionEngine**: Creates and updates positions from fills.
- **Cache**: Stores position state and snapshots.
- **RiskEngine**: Monitors position limits and exposure.
:::note
Positions are not created for spread instruments. While contingent orders can still trigger for spreads,
they operate without position linkage. The engine handles spread instruments separately from regular positions.
:::
## Summary
Positions are central to tracking trading activity and performance. Understanding how positions
aggregate fills, calculate PnL, and handle different OMS configurations matters when building
trading strategies. Position snapshotting provides accurate historical tracking in `NETTING`
mode, and the event history supports detailed analysis and reconciliation.
## Related guides
- [Events](events.md) - How fills produce position events.
- [Orders](orders/) - Orders that create and modify positions.
- [Execution](execution.md) - Fill handling that updates positions.
- [Portfolio](portfolio.md) - Portfolio-level position aggregation.
# 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
The `ReportProvider` class in NautilusTrader generates structured analytical reports from
trading data, transforming raw 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.
Reports can be generated using two approaches:
- **Trader helper methods** (recommended): Convenient methods like `trader.generate_orders_report()`.
- **ReportProvider directly**: For more control over data selection and filtering.
Reports provide consistent analytics across both backtesting and live trading environments,
enabling reliable performance evaluation and strategy comparison.
## 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
# Using Trader helper method (recommended)
orders_report = trader.generate_orders_report()
# Or using ReportProvider directly
from nautilus_trader.analysis import ReportProvider
orders = cache.orders()
orders_report = ReportProvider.generate_orders_report(orders)
```
**Returns `pd.DataFrame`. Key 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 (order‑type dependent). |
| `avg_px` | Average fill price (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 (e.g., `trigger_price` for stop orders, `expire_time` 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
# Using Trader helper method (recommended)
fills_report = trader.generate_order_fills_report()
# Or using ReportProvider directly
orders = cache.orders()
fills_report = ReportProvider.generate_order_fills_report(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
# Using Trader helper method (recommended)
fills_report = trader.generate_fills_report()
# Or using ReportProvider directly
orders = cache.orders()
fills_report = ReportProvider.generate_fills_report(orders)
```
**Returns `pd.DataFrame`. Key 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. |
| `ts_event` | Fill timestamp (datetime). |
| `ts_init` | Initialization timestamp (datetime). |
See `OrderFilled.to_dict()` for the complete field list.
### Positions report
Position analysis including snapshots:
```python
# Using Trader helper method (recommended)
# Automatically includes snapshots for NETTING OMS
positions_report = trader.generate_positions_report()
# Or using ReportProvider directly
positions = cache.positions()
snapshots = cache.position_snapshots() # For NETTING OMS
positions_report = ReportProvider.generate_positions_report(
positions=positions,
snapshots=snapshots
)
```
**Returns `pd.DataFrame`. Key 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. |
| `peak_qty` | Maximum size reached. |
| `avg_px_open` | Average entry price. |
| `avg_px_close` | Average exit price (if closed). |
| `commissions` | List of commissions paid. |
| `realized_pnl` | Realized profit/loss. |
| `realized_return` | Return percentage. |
| `ts_init` | Position initialization timestamp. |
| `ts_opened` | Opening timestamp (datetime). |
| `ts_last` | Last update timestamp. |
| `ts_closed` | Closing timestamp (datetime or NA). |
| `duration_ns` | Position duration in nanoseconds. |
| `is_snapshot` | Whether this is a historical snapshot. |
### Account report
Tracks account balance and margin changes over time:
```python
# Using Trader helper method (recommended)
# Requires venue parameter
from nautilus_trader.model.identifiers import Venue
venue = Venue("BINANCE")
account_report = trader.generate_account_report(venue)
# Or using ReportProvider directly
account = cache.account(account_id)
account_report = ReportProvider.generate_account_report(account)
```
**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., SPOT, 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.
- **Commission impact**: Only included when in settlement currency.
:::warning
PnL calculations depend on the OMS type. In `NETTING` OMS, position snapshots
preserve historical PnL when positions reopen. Always include snapshots in
reports for accurate total PnL calculation. In `HEDGING` OMS, snapshots are
not used since each position has a unique ID and is never reopened.
:::
### Multi-currency accounting
When dealing with multiple currencies:
- Each position tracks PnL in its settlement currency.
- Portfolio aggregation requires currency conversion.
- Commission currencies may differ from settlement currency.
```python
# Accessing PnL across positions
for position in positions:
realized = position.realized_pnl # In settlement currency
unrealized = position.unrealized_pnl(last_price)
# Handle multi-currency aggregation (illustrative)
# Note: Currency conversion requires user-provided exchange rates
if position.settlement_currency != base_currency:
# Apply conversion rate from your data source
# rate = get_exchange_rate(position.settlement_currency, base_currency)
# realized_converted = realized.as_double() * rate
pass
```
### Snapshot considerations
For `NETTING` OMS:
```python
from nautilus_trader.model.objects import Money
# Include snapshots for complete PnL (per currency)
pnl_by_currency = {}
# Add PnL from current positions
for position in cache.positions(instrument_id=instrument_id):
if position.realized_pnl:
currency = position.realized_pnl.currency
if currency not in pnl_by_currency:
pnl_by_currency[currency] = 0.0
pnl_by_currency[currency] += position.realized_pnl.as_double()
# Add PnL from historical snapshots
for snapshot in cache.position_snapshots(instrument_id=instrument_id):
if snapshot.realized_pnl:
currency = snapshot.realized_pnl.currency
if currency not in pnl_by_currency:
pnl_by_currency[currency] = 0.0
pnl_by_currency[currency] += snapshot.realized_pnl.as_double()
# Create Money objects for each currency
total_pnls = [Money(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(start=start_time, end=end_time)
# 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
stats_returns = result.stats_returns
stats_general = result.stats_general
```
:::info
For detailed information about available statistics, see the
[Portfolio guide](portfolio.md#portfolio-statistics). The Portfolio guide covers:
- Built-in statistics categories (PnLs, returns, positions, orders based).
- Portfolio report context.
:::
### 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 "nautilus_trader[visualization]"
```
## Report generation patterns
### Live trading
During live trading, generate reports periodically:
```python
import pandas as pd
class ReportingActor(Actor):
def on_start(self):
# Schedule periodic reporting
self.clock.set_timer(
name="generate_reports",
interval=pd.Timedelta(minutes=30),
callback=self.generate_reports
)
def generate_reports(self, event):
# Generate and log reports
positions_report = self.trader.generate_positions_report()
# 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(start=start_time, end=end_time)
# 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)"),
"sharpe_ratio": stats_returns.get("Sharpe Ratio (252 days)"),
"profit_factor": stats_general.get("Profit Factor"),
"win_rate": stats_general.get("Win Rate"),
}
# 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**: Uses reports for performance analysis and metrics calculation.
- **BacktestEngine**: Uses reports for post-run analysis and visualization.
- **Position snapshots**: Required for accurate PnL reporting in `NETTING` OMS.
## Summary
The `ReportProvider` generates reports from orders, fills, positions, and account
states as structured DataFrames for analysis and visualization. For accurate total
PnL in `NETTING` OMS, include position snapshots when generating reports.
## Related guides
- [Visualization](visualization.md) - Interactive tearsheets and charts from backtest results.
- [Portfolio](portfolio.md) - Portfolio statistics and performance metrics.
- [Backtesting](backtesting.md) - 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 across all paths, and the v2 PyO3 path runs
Python strategies on the Rust engine directly.
:::warning
The Rust API is under active development. Method signatures and trait
requirements may change between releases.
:::
## System implementations
Nautilus has three implementations. Understanding where each stands helps
you choose the right one for your use case.
- **v1 legacy**: Cython/Python classes under `nautilus_trader/`. Fully
featured with the broadest component coverage.
- **v2 Rust**: Pure Rust under `crates/`. Runs without Python.
- **v2 PyO3**: Python user-components (actors, strategies) running on
the Rust core via PyO3 bindings. Combines Python convenience with
Rust engine performance.
### Capability matrix
| Component | v1 legacy (Cython) | v2 Rust | v2 PyO3 (Python on Rust) |
|-----------------------|--------------------|----------------|--------------------------|
| Strategy | ✓ | ✓ | ✓ |
| Actor | ✓ | ✓ | ✓ |
| DataEngine | ✓ | ✓ | ✓ |
| ExecutionEngine | ✓ | ✓ | ✓ |
| RiskEngine | ✓ | ✓ | ✓ |
| BacktestEngine | ✓ | ✓ | ✓ |
| BacktestNode | ✓ | ✓ | ✓ |
| LiveNode | ✓ | ✓ | ✓ |
| OrderEmulator | ✓ | ✓ | ✓ |
| Matching engine | ✓ | ✓ | ✓ |
| Portfolio | ✓ | ✓ | ✓ |
| Accounts | ✓ | ✓ | ✓ |
| Cache | ✓ | ✓ | ✓ |
| MessageBus | ✓ | ✓ | ✓ |
| Data catalog | ✓ | ✓ | ✓ |
| Indicators | ✓ | ✓ | ✓ |
| Exec algorithms | TWAP | TWAP | TWAP |
| Controller | ✓ | - | - |
| Tearsheets | ✓ | - | ✓ |
| Config serialization | ✓ | - | - |
### Adapters
| Adapter | v1 legacy (Cython) | v2 Rust | v2 PyO3 |
|---------------------|--------------------|---------|---------|
| Architect AX | ✓ | ✓ | ✓ |
| Betfair | ✓ | ✓ | ✓ |
| Binance | ✓ | ✓ | ✓ |
| BitMEX | ✓ | ✓ | ✓ |
| Bybit | ✓ | ✓ | ✓ |
| Databento | ✓ | ✓ | ✓ |
| Deribit | ✓ | ✓ | ✓ |
| dYdX | ✓ | ✓ | ✓ |
| Hyperliquid | ✓ | ✓ | ✓ |
| Interactive Brokers | ✓ | - | - |
| Kraken | ✓ | ✓ | ✓ |
| OKX | ✓ | ✓ | ✓ |
| Polymarket | ✓ | ✓ | ✓ |
| Sandbox | ✓ | ✓ | ✓ |
| Tardis | ✓ | ✓ | ✓ |
### Choosing a path
- **v1 legacy** is the most complete today. Use it if you need the
Controller, Interactive Brokers, or config serialization.
- **v2 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.
- **v2 PyO3**: Python user-components (actors, strategies) run on the
Rust core engine with Rust performance for data processing and
execution, while keeping the Python authoring experience.
## 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.59"
nautilus-common = "0.59"
nautilus-execution = "0.59"
nautilus-model = { version = "0.59", features = ["stubs"] }
nautilus-trading = { version = "0.59", 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.59"
nautilus-okx = "0.59"
```
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 = ["stubs"] }
nautilus-trading = { git = "https://github.com/nautechsystems/nautilus_trader.git", branch = "develop", features = ["examples"] }
```
The minimum supported Rust version (MSRV) is **1.96.0**.
### Feature flags
| Flag | Crate | Effect |
|------------------|---------------------|---------------------------------------------------------------|
| `high-precision` | `nautilus-model` | 16-digit fixed precision (default is 9). Required for crypto. |
| `stubs` | `nautilus-model` | Test instrument stubs (`audusd_sim`, etc.). |
| `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 can have many decimal
places (e.g. `0.00000001`).
:::
## 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_order_filled` | `OrderFilled` |
| `on_order_canceled` | `OrderCanceled` |
| `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/develop/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`.
### 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.
Normal code uses facade methods such as:
- `actor_id()`
- `trader_id()`
- `is_registered()`
- `config()`
- `strategy_id()`
- `clock()`
- `cache()`
- `order()`
- `portfolio()`
### 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/develop/crates/trading/src/examples/strategies/ema_cross)
and
[`GridMarketMaker`](https://github.com/nautechsystems/nautilus_trader/tree/develop/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.core.nautilus_pyo3.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/develop/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/develop/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 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/` |
| Databento | `crates/adapters/databento/examples/` |
| Deribit | `crates/adapters/deribit/examples/` |
| dYdX | `crates/adapters/dydx/examples/` |
| Hyperliquid | `crates/adapters/hyperliquid/examples/` |
| Kraken | `crates/adapters/kraken/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
- [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.md) - Event types and handler dispatch.
- [Backtesting](backtesting.md) - 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.
**Capabilities**:
- All `Actor` capabilities.
- Order management.
**Relationship with actors**:
The `Strategy` class inherits from `Actor`, which means strategies have access to all actor functionality
plus order management capabilities.
:::tip
We recommend reviewing the [Actors](actors.md) guide before diving into strategy development.
:::
Strategies can be added to Nautilus systems in any [environment contexts](architecture.md#environment-contexts) and will start sending commands and receiving
events based on their logic as soon as the system starts.
With these building blocks of data ingest, event handling, and order management (discussed below),
you can build any type of strategy including directional, momentum, re-balancing,
pairs, market making, etc.
See the [`Strategy` API Reference](/docs/python-api-latest/trading.html) for all available methods.
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.
:::tip
Once a strategy is defined, the same source code can be used for backtesting and live trading.
:::
The main capabilities of a strategy include:
- Historical data requests.
- Live data feed subscriptions.
- Setting time alerts or timers.
- Cache access.
- Portfolio access.
- Creating and managing orders and positions.
:::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.strategy 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
Do not call components such as `clock` and `logger` in the `__init__` constructor (which is prior to registration).
This is because the systems clock and logging subsystem have not yet been initialized.
:::
### 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.
#### 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 nautilus_trader.core import Data
from nautilus_trader.model import OrderBook
from nautilus_trader.model import Bar
from nautilus_trader.model import QuoteTick
from nautilus_trader.model import TradeTick
from nautilus_trader.model import OrderBookDeltas
from nautilus_trader.model import InstrumentClose
from nautilus_trader.model import InstrumentStatus
from nautilus_trader.model import OptionChainSlice
from nautilus_trader.model import OptionGreeks
from nautilus_trader.model.instruments import Instrument
def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None:
def on_order_book(self, order_book: OrderBook) -> None:
def on_quote_tick(self, tick: QuoteTick) -> None:
def on_trade_tick(self, tick: TradeTick) -> None:
def on_bar(self, bar: Bar) -> None:
def on_instrument(self, instrument: Instrument) -> 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: Data) -> None:
def on_data(self, data: Data) -> None: # Custom data passed to this handler
def on_signal(self, signal: Data) -> None: # Custom signals passed to this handler
```
#### 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(...)`
3. `on_event(...)`
```python
from nautilus_trader.model.events import OrderAccepted
from nautilus_trader.model.events import OrderCanceled
from nautilus_trader.model.events import OrderCancelRejected
from nautilus_trader.model.events import OrderDenied
from nautilus_trader.model.events import OrderEmulated
from nautilus_trader.model.events import OrderEvent
from nautilus_trader.model.events import OrderExpired
from nautilus_trader.model.events import OrderFilled
from nautilus_trader.model.events import OrderInitialized
from nautilus_trader.model.events import OrderModifyRejected
from nautilus_trader.model.events import OrderPendingCancel
from nautilus_trader.model.events import OrderPendingUpdate
from nautilus_trader.model.events import OrderRejected
from nautilus_trader.model.events import OrderReleased
from nautilus_trader.model.events import OrderSubmitted
from nautilus_trader.model.events import OrderTriggered
from nautilus_trader.model.events 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_event(self, event: OrderEvent) -> None: # All order event messages are eventually passed to this handler
```
#### 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(...)`
3. `on_event(...)`
```python
from nautilus_trader.model.events import PositionChanged
from nautilus_trader.model.events import PositionClosed
from nautilus_trader.model.events import PositionEvent
from nautilus_trader.model.events 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: PositionEvent) -> None: # All position event messages are eventually passed to this handler
```
#### Generic event handling
This handler will eventually receive all event messages which arrive at the strategy, including those for
which no other specific handler exists.
```python
from nautilus_trader.core.message import Event
def on_event(self, event: Event) -> None:
```
#### 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,
callback=lambda _: self.subscribe_bars(self.bar_type),
)
self.subscribe_quote_ticks(self.instrument_id)
```
Live bars are subscribed via the `request_bars()` `callback` so the stream starts only
once history has loaded; see [Working with bars: request vs. subscribe](data.md#working-with-bars-request-vs-subscribe)
for why this matters under `validate_data_sequence=True`.
### 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 `pd.Timestamp`:
```python
import pandas as pd
now: pd.Timestamp = 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_event` handler at the
specified alert time. In a live context, this might be slightly delayed by a few microseconds.
This example sets a time alert to trigger one minute from the current time:
```python
import pandas as pd
# Fire a TimeEvent one minute from now
self.clock.set_time_alert(
name="MyTimeAlert1",
alert_time=self.clock.utc_now() + pd.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, starting immediately:
```python
import pandas as pd
# Fire a TimeEvent every minute
self.clock.set_timer(
name="MyTimer1",
interval=pd.Timedelta(minutes=1),
)
```
### 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_tick(self.instrument_id)
last_trade = self.cache.trade_tick(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
from nautilus_trader.accounting.accounts.base import Account
from nautilus_trader.model import Venue
from nautilus_trader.model import Currency
from nautilus_trader.model import Money
from nautilus_trader.model import InstrumentId
def account(self, venue: Venue) -> Account
def balances_locked(self, venue: Venue) -> dict[Currency, Money]
def margins_init(self, venue: Venue) -> dict[Currency, Money]
def margins_maint(self, venue: Venue) -> dict[Currency, Money]
def unrealized_pnls(self, venue: Venue) -> dict[Currency, Money]
def realized_pnls(self, venue: Venue) -> dict[Currency, Money]
def net_exposures(self, venue: Venue) -> dict[Currency, Money]
def unrealized_pnl(self, instrument_id: InstrumentId) -> Money
def realized_pnl(self, instrument_id: InstrumentId) -> Money
def net_exposure(self, instrument_id: InstrumentId) -> Money
def net_position(self, instrument_id: InstrumentId) -> decimal.Decimal
def is_net_long(self, instrument_id: InstrumentId) -> bool
def is_net_short(self, instrument_id: InstrumentId) -> bool
def is_flat(self, instrument_id: InstrumentId) -> bool
def is_completely_flat(self) -> bool
```
See the [`Portfolio` API Reference](/docs/python-api-latest/portfolio.html) for all available methods.
#### Reports and analysis
The `Portfolio` also exposes a `PortfolioAnalyzer`, which accepts a flexible amount of data
(to accommodate different lookback windows). The analyzer tracks and generates performance
metrics and statistics.
See the [`PortfolioAnalyzer` API Reference](/docs/python-api-latest/analysis.html) and [Portfolio statistics](portfolio.md#portfolio-statistics) guide.
### Trading commands
The following trading commands are available for order management.
See also the [Execution](../concepts/execution.md) 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 `ExecAlgorithm`.
- 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.enums import OrderSide
from nautilus_trader.model.enums import TriggerType
from nautilus_trader.model.orders import LimitOrder
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 `ExecAlgorithm`.
:::
This example submits a `MARKET` BUY order to a TWAP execution algorithm:
```python
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.model.enums import TimeInForce
from nautilus_trader.model import ExecAlgorithmId
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)
```
#### 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`.
The component a `CancelOrder`, `CancelAllOrders` or `BatchCancelOrders` 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`.
- If an `exec_algorithm_id` is specified (with no `emulation_trigger`), and the order is still active within the local system, the command will *firstly* be sent to the relevant `ExecAlgorithm`.
- Otherwise, the order will *firstly* be sent to the `ExecutionEngine`.
:::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)
```
The following shows how to cancel a batch of orders:
```python
from nautilus_trader.model.orders import Order
my_order_list: list[Order] = [order1, order2, order3]
self.cancel_orders(my_order_list)
```
The following shows how to cancel all orders:
```python
self.cancel_all_orders()
```
#### 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, the order will *firstly* be sent to the `RiskEngine`.
:::info
Once an order is under the control of an execution algorithm, it cannot be directly modified by a strategy (only canceled).
:::
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, new_quantity)
```
:::info
The price and trigger price can also be modified (when emulated or supported by a venue).
:::
#### 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 market exit process:
1. Cancels all open and in-flight orders for the strategy.
2. Closes all open positions with market orders.
3. Periodically checks (at `market_exit_interval_ms`) until all orders resolve and positions close.
4. Calls `post_market_exit()` once flat, or after `market_exit_max_attempts` is reached.
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. 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_tick(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()` will first perform a market exit, then stop the strategy
once 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: None/GTC) - Time in force for closing market orders.
- `market_exit_reduce_only` (default: True) - If closing market orders should be reduce only.
## 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.
Here is an example configuration:
```python
from decimal import Decimal
from nautilus_trader.config import StrategyConfig
from nautilus_trader.model import Bar, BarType
from nautilus_trader.model import InstrumentId
from nautilus_trader.trading.strategy import Strategy
# Configuration definition
class MyStrategyConfig(StrategyConfig):
instrument_id: InstrumentId # example value: "ETHUSDT-PERP.BINANCE"
bar_type: BarType # example value: "ETHUSDT-PERP.BINANCE-15-MINUTE[LAST]-EXTERNAL"
fast_ema_period: int = 10
slow_ema_period: int = 20
trade_size: Decimal
order_id_tag: str
# 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.
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),
order_id_tag="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.
:::
### 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*).
Some venues (such as Binance Futures) support the GTD time in force, so to avoid conflicts when using
`managed_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.
If `strategy_id` is not supplied, the platform builds the strategy ID from the
strategy class name and an order ID tag. The tag can be supplied with `order_id_tag`;
otherwise registration assigns the next numeric tag, starting with `000`. For example,
the above config results in a strategy ID of `MyStrategy-001`.
If `strategy_id` is supplied with `order_id_tag`, Rust appends the tag to the
runtime strategy ID unless the ID already ends with that tag. For example,
`strategy_id=MyStrategy-PRIMARY` with `order_id_tag=ABC` becomes
`MyStrategy-PRIMARY-ABC`.
If `order_id_tag` is omitted, Rust uses the final hyphen-separated part of
`strategy_id` as the order ID tag.
:::note
The platform has built-in safety measures: if two strategies share a duplicated strategy ID,
a `RuntimeError` is raised during registration indicating the strategy ID is already registered.
:::
The reason for this is that 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.
:::info Rust implementation
Rust treats `StrategyConfig` as immutable construction input. The runtime
`StrategyId` carries the order ID tag, matching Python/Cython behavior. This keeps
actor registration, client order ID generation, order list ID generation, and
position ID generation aligned through `strategy_id.get_tag()`.
If `strategy_id` is omitted, `order_id_tag` overrides the generated suffix, for
example `MyStrategy-ABC`.
:::
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.md) - Event types and handler dispatch.
- [Orders](orders/) - Order types and management from strategies.
- [Backtesting](backtesting.md) - 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 `Actor` and `Strategy` components to subscribe to quote or trade feeds.
- Triggering emulated orders from derived prices.
- Constructing bars from synthetic quotes or trades.
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 match. 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 a letter or `_` and then use 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. |
These limits are generous for any realistic pricing formula. A weighted sum of 8 components
uses a peak stack depth of 3 and zero locals.
### 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
Before defining a new synthetic instrument, make sure all component instruments already exist in
the cache.
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.instruments 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_quote_ticks(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.strategy.order_factory.limit(
instrument_id=ETHUSDT_BINANCE.id,
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.strategy.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.instruments.synthetic.SyntheticInstrument)
for input requirements and exceptions.
## Related guides
- [Instruments](instruments/) - Instrument definitions and venue-specific instrument types.
- [Data](data.md) - 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
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.objects 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.objects 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.objects import Price, Quantity, Money
from nautilus_trader.model.currencies import 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.objects 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 stores a precision field indicating the number of decimal places.
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`
field 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.objects 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.
:::note
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.objects 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.objects 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.objects import Money
from nautilus_trader.model.currencies import USD, 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.objects import Money
from nautilus_trader.model.currencies import 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.objects 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.objects import Quantity, Price, Money
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.
All visualization outputs are self-contained HTML files that can be viewed in any modern
browser, shared with stakeholders, or archived for future reference.
:::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 "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.engine 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.
### Customization
Control which charts appear and how they're styled:
```python
from nautilus_trader.analysis 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.currencies import USD
create_tearsheet(
engine=engine,
output_path="usd_only.html",
currency=USD, # Currency object, shows only USD statistics
)
```
When `currency` is `None` (default), statistics for all currencies are displayed
separately in the tearsheet. Return-based charts are reconstructed from account
reports only when the accounts share one currency; pass `currency` for multi-currency
backtests so return charts use the selected currency.
## 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.analysis 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.tearsheet 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.themes 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.analysis 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.tearsheet 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 live `BacktestEngine` usage these values come from
`engine.get_result()`; 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:
### High-level API
Recommended for most use cases:
```python
create_tearsheet(engine=engine, config=config)
```
Automatically extracts data from the `BacktestEngine`, 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.analysis import TearsheetConfig
from nautilus_trader.analysis import TearsheetEquityChart
from nautilus_trader.analysis import TearsheetStatsTableChart
from nautilus_trader.model.data 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 (green up-triangles for buys, red down-triangles for sells). 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.md) - 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
This developer guide provides specifications for how to build an integration adapter for the NautilusTrader platform.
Adapters connect to trading venues and data providers, translating their native APIs into the platform’s unified interface and normalized domain model.
## Structure of an adapter
NautilusTrader adapters follow a layered architecture pattern with:
- **Rust core** for networking clients and performance-sensitive operations.
- **Python layer** for integrating Rust clients into the platform's data and execution engines.
### Rust core (`crates/adapters/your_adapter/`)
The Rust layer handles:
- **HTTP client**: Raw API communication, request signing, rate limiting.
- **WebSocket client**: Low-latency streaming connections, message parsing.
- **Parsing**: Fast conversion of venue data to Nautilus domain models.
- **Python bindings**: PyO3 exports to make Rust functionality available to Python.
Typical Rust structure:
```
crates/adapters/your_adapter/
├── src/
│ ├── common/ # Shared types and utilities
│ │ ├── consts.rs # Venue constants / broker IDs
│ │ ├── credential.rs # API key storage and signing helpers
│ │ ├── enums.rs # Venue enums mirrored in REST/WS payloads
│ │ ├── error.rs # Adapter-level error aggregation (when applicable)
│ │ ├── models.rs # Shared model types
│ │ ├── parse.rs # Shared parsing helpers
│ │ ├── retry.rs # Retry classification (when applicable)
│ │ ├── urls.rs # Environment & product aware base-url resolvers
│ │ └── testing.rs # Fixtures reused across unit tests
│ ├── http/ # HTTP client implementation
│ │ ├── client.rs # HTTP client with authentication
│ │ ├── error.rs # HTTP-specific error types
│ │ ├── models.rs # Structs for REST payloads
│ │ ├── parse.rs # Response parsing functions
│ │ └── query.rs # Request and query builders
│ ├── websocket/ # WebSocket implementation
│ │ ├── client.rs # WebSocket client
│ │ ├── dispatch.rs # Execution event dispatch and order routing
│ │ ├── enums.rs # WebSocket-specific enums
│ │ ├── error.rs # WebSocket-specific error types
│ │ ├── handler.rs # Feed handler (I/O boundary)
│ │ ├── messages.rs # Frame and message enums
│ │ ├── parse.rs # Message parsing functions
│ │ └── subscription.rs # Subscription topic helpers (optional)
│ ├── python/ # PyO3 Python bindings
│ │ ├── enums.rs # Python-exposed enums
│ │ ├── http.rs # Python HTTP client bindings
│ │ ├── urls.rs # Python URL helpers
│ │ ├── websocket.rs # Python WebSocket client bindings
│ │ └── mod.rs # Module exports
│ ├── config.rs # Configuration structures
│ ├── data.rs # Data client implementation
│ ├── execution.rs # Execution client implementation
│ ├── factories.rs # Factory functions
│ └── lib.rs # Library entry point
├── tests/ # Integration tests with mock servers
│ ├── data_client.rs # Data client integration tests
│ ├── exec_client.rs # Execution client integration tests
│ ├── http.rs # HTTP client integration tests
│ └── websocket.rs # WebSocket client integration tests
└── test_data/ # Canonical venue payloads
```
### Python layer (`nautilus_trader/adapters/your_adapter`)
The Python layer provides the integration interface through these components:
1. **Instrument Provider**: Supplies instrument definitions via `InstrumentProvider`.
2. **Data Client**: Handles market data feeds and historical data requests via `LiveDataClient` and `LiveMarketDataClient`.
3. **Execution Client**: Manages order execution via `LiveExecutionClient`.
4. **Factories**: Converts venue-specific data to Nautilus domain models.
5. **Configuration**: User-facing configuration classes for client settings.
Typical Python structure:
```
nautilus_trader/adapters/your_adapter/
├── config.py # Configuration classes
├── constants.py # Adapter constants
├── data.py # LiveDataClient/LiveMarketDataClient
├── execution.py # LiveExecutionClient
├── factories.py # Instrument factories
├── providers.py # InstrumentProvider
└── __init__.py # Package initialization
```
## Adapter implementation sequence
Follow this dependency-driven order when building an adapter. Each phase
builds on the previous one. Implement the Rust core before any Python layer.
### Phase 1: Rust core infrastructure
Build the low-level networking and parsing foundation.
| Step | Component | Description |
|------|----------------------------|----------------------------------------------------------------------------------------------|
| 1.1 | HTTP error types | Define HTTP‑specific error enum with retryable/non‑retryable variants (`http/error.rs`). |
| 1.2 | HTTP client | Implement credentials, request signing, rate limiting, and retry logic. |
| 1.3 | HTTP API models | Define request/response structs for REST endpoints (`http/models.rs`, `http/query.rs`). |
| 1.4 | HTTP parsing | Convert venue responses to Nautilus domain models (`http/parse.rs`, `common/parse.rs`). |
| 1.5 | WebSocket error types | Define WebSocket‑specific error enum (`websocket/error.rs`). |
| 1.6 | WebSocket client | Implement connection lifecycle, authentication, heartbeat, and reconnection. |
| 1.7 | WebSocket messages | Define streaming payload types (`websocket/messages.rs`). |
| 1.8 | WebSocket parsing | Convert stream messages to Nautilus domain models (`websocket/parse.rs`). |
| 1.9 | Python bindings | Expose Rust functionality via PyO3 (`python/mod.rs`). |
**Milestone**: Rust crate compiles, unit tests pass, HTTP/WebSocket clients can authenticate and stream/request raw data.
### Phase 2: Instrument definitions
Instruments are the foundation: both data and execution clients depend on them.
| Step | Component | Description |
|------|----------------------------|----------------------------------------------------------------------------------------------|
| 2.1 | Instrument parsing | Parse venue instrument definitions into Nautilus types (spot, perpetual, future, option). |
| 2.2 | Instrument provider | Implement `InstrumentProvider` to load, filter, and cache instruments. |
| 2.3 | Symbol mapping | Handle venue‑specific symbol formats and Nautilus `InstrumentId` conversion. |
**Milestone**: `InstrumentProvider.load_all_async()` returns valid Nautilus instruments.
### Phase 3: Market data
Build data subscriptions and historical data requests.
| Step | Component | Description |
|------|----------------------------|----------------------------------------------------------------------------------------------|
| 3.1 | Public WebSocket streams | Subscribe to order books, trades, tickers, and other public channels. |
| 3.2 | Historical data requests | Fetch historical bars, trades, and order book snapshots via HTTP. |
| 3.3 | Data client (Python) | Implement `LiveDataClient` or `LiveMarketDataClient` wiring Rust clients to the data engine. |
**Milestone**: Data client connects, subscribes to instruments, and emits market data to the platform.
### Phase 4: Order execution
Build order management and account state.
| Step | Component | Description |
|------|----------------------------|----------------------------------------------------------------------------------------------|
| 4.1 | Private WebSocket streams | Subscribe to order updates, fills, positions, and account balance changes. |
| 4.2 | Basic order submission | Implement market and limit orders via HTTP or WebSocket. |
| 4.3 | Order modification/cancel | Implement order amendment and cancellation. |
| 4.4 | Execution client (Python) | Implement `LiveExecutionClient` wiring Rust clients to the execution engine. |
| 4.5 | Execution reconciliation | Generate order, fill, and position status reports for startup reconciliation. |
**Milestone**: Execution client submits orders, receives fills, and reconciles state on connect.
### Phase 5: Advanced features
Extend coverage based on venue capabilities.
| Step | Component | Description |
|------|----------------------------|----------------------------------------------------------------------------------------------|
| 5.1 | Advanced order types | Conditional orders, stop‑loss, take‑profit, trailing stops, iceberg, etc. |
| 5.2 | Batch operations | Batch order submission, batch cancellation, mass cancel. |
| 5.3 | Venue‑specific features | Options chains, funding rates, liquidations, or other venue‑specific data. |
### Phase 6: Configuration and factories
Wire everything together for production usage.
| Step | Component | Description |
|------|----------------------------|----------------------------------------------------------------------------------------------|
| 6.1 | Configuration classes | Create `LiveDataClientConfig` and `LiveExecClientConfig` subclasses. |
| 6.2 | Factory functions | Implement factory functions to instantiate clients from configuration. |
| 6.3 | Environment variables | Support credential resolution from environment variables. |
### Phase 7: Testing and documentation
Validate the integration and document usage.
| Step | Component | Description |
|------|----------------------------|----------------------------------------------------------------------------------------------|
| 7.1 | Rust unit tests | Test parsers, signing helpers, and business logic in `#[cfg(test)]` blocks. |
| 7.2 | Rust integration tests | Test HTTP/WebSocket clients against mock Axum servers in `tests/`. |
| 7.3 | Python integration tests | Test data/execution clients in `tests/integration_tests/adapters//`. |
| 7.4 | Example scripts | Provide runnable examples demonstrating data subscription and order execution. |
See the [Testing](#testing) section for detailed test organization guidelines.
---
## Rust adapter patterns
### Common code (`common/`)
Group venue constants, credential helpers, enums, and reusable parsers under `src/common`.
Adapters such as OKX keep submodules like `consts`, `credential`, `enums`, and `urls` alongside a `testing` module
for fixtures, providing a single place for cross-cutting pieces.
When an adapter has multiple environments or product categories, add a dedicated `common::urls` helper so
REST/WebSocket base URLs stay in sync with the Python layer.
### Symbol normalization (`common/symbol.rs`)
When a venue uses a different symbol format than Nautilus `InstrumentId`, place bidirectional
conversion helpers in `common/symbol.rs`. Two functions form the standard interface:
- `format_instrument_id(venue_symbol, product_type)` converts a venue symbol string to a
Nautilus `InstrumentId`, appending or transforming product-type suffixes as needed
(e.g., `"BTCUSDT"` + `Linear` becomes `"BTCUSDT-LINEAR.BYBIT"`).
- `format_venue_symbol(instrument_id)` strips Nautilus suffixes to recover the venue-native
symbol for API calls.
Common patterns across adapters:
- **Suffix-based product types**: Bybit appends `-SPOT`, `-LINEAR`, `-INVERSE`, `-OPTION`.
A `BybitSymbol` wrapper validates the suffix and normalizes to uppercase on construction.
- **Implicit product mapping**: Binance USD-M futures append `-PERP` at the Nautilus layer
while COIN-M keeps the venue's existing `_PERP` suffix.
- **Case normalization**: Convert to uppercase on input when venues are case-insensitive.
- **`Ustr` interning**: Store normalized symbols as `Ustr` for zero-cost comparison.
For venues where the raw symbol maps 1:1 to an `InstrumentId` (no suffix gymnastics), inline
helpers in `common/parse.rs` are sufficient and a dedicated `symbol.rs` is not needed.
### URL resolution
Define URL constants and resolution functions in `common/urls.rs`:
```rust
const VENUE_WS_URL: &str = "wss://stream.venue.com/ws";
const VENUE_TESTNET_WS_URL: &str = "wss://testnet-stream.venue.com/ws";
pub const fn get_ws_base_url(testnet: bool) -> &'static str {
if testnet { VENUE_TESTNET_WS_URL } else { VENUE_WS_URL }
}
```
Config structs should provide override fields (`base_url_http`, `base_url_ws`, etc.) that fall back
to these defaults when unset.
### Configurations (`config.rs`)
Expose typed config structs in `src/config.rs` so Python callers toggle venue-specific behaviour
(see how OKX wires demo URLs, retries, and channel flags).
Keep defaults minimal and delegate URL selection to helpers in `common::urls`.
For the user-facing design rationale, see the [Configuration](../concepts/configuration.md)
concept guide.
#### Builder and Default
Config structs derive `bon::Builder` and implement `Default`. The builder owns all default
values via `#[builder(default = value)]` annotations. The `Default` impl delegates to the
builder so defaults are defined in exactly one place:
```rust
#[derive(Clone, Debug, bon::Builder)]
pub struct VenueDataClientConfig {
pub api_key: Option,
#[builder(default = 60)]
pub http_timeout_secs: u64,
#[builder(default = 3)]
pub max_retries: u32,
}
impl Default for VenueDataClientConfig {
fn default() -> Self {
Self::builder().build()
}
}
```
This prevents drift between builder defaults and `Default` output. Never duplicate
default values in the `Default` impl body.
Bon always defaults `Option` fields to `None`. For the rare case where an
`Option` field should default to `Some(value)`, override it in the `Default` impl
and delegate everything else to the builder:
```rust
impl Default for VenueDataClientConfig {
fn default() -> Self {
Self {
poll_interval_secs: Some(60),
..Self::builder().build()
}
}
}
```
#### Field type rules
Use plain `T` with `#[builder(default = value)]` when a field always has a sensible
default and downstream code consumes the value directly:
```rust
#[builder(default = 60)]
pub http_timeout_secs: u64,
```
Use `Option` (no builder annotation) when `None` carries distinct meaning such as
"feature disabled", "unbounded", or "inherit from environment":
```rust
/// Interval in seconds between open order checks.
/// When `None`, open order polling is disabled.
pub open_check_interval_secs: Option,
```
Choose the type based on the config's own semantics, not downstream function
signatures. If `None` means "this feature is off" at the config level, use
`Option`. If the field always resolves to a concrete value, use plain `T`
even when a downstream constructor still accepts `Option` and the call site
wraps with `Some(config.field)`.
#### Python constructors
The `py_new` constructor accepts `Option` for all configurable fields (Python callers
pass `None` to mean "use default"). For plain `T` fields, unwrap against the default:
```rust
fn py_new(http_timeout_secs: Option) -> Self {
let defaults = Self::default();
Self {
http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
..
}
}
```
For `Option` fields, use `.or()` to fall back to the default option value.
When the default is `None`, this preserves the caller's `None`. When the default
is `Some(value)`, this fills in the default if the caller passed `None`:
```rust
open_check_interval_secs: open_check_interval_secs.or(defaults.open_check_interval_secs),
```
#### Default values
Use sensible production defaults: credentials as `None` (resolved from environment at
runtime), mainnet URLs, standard timeouts. For `trader_id` and `account_id`, use
placeholder values like `TraderId::from("TRADER-001")` and `AccountId::from("VENUE-001")`.
The `..Default::default()` pattern keeps examples and tests focused on fields that
differ from defaults:
```rust
let config = VenueExecClientConfig {
trader_id,
account_id,
environment: VenueEnvironment::Testnet,
..Default::default()
};
```
### Error taxonomy (`common/error.rs`)
For adapters with multiple client types, define an adapter-level error enum in `common/error.rs` that
aggregates component errors:
```rust
#[derive(Debug, thiserror::Error)]
pub enum VenueError {
#[error("HTTP error: {0}")]
Http(#[from] VenueHttpError),
#[error("WebSocket error: {0}")]
WebSocket(#[from] VenueWsError),
#[error("Build error: {0}")]
Build(#[from] VenueBuildError),
}
```
This enables unified error handling at the adapter boundary while preserving component-specific
error details for debugging.
### Retry classification (`common/retry.rs`)
When an adapter needs sophisticated retry logic, define a retry classification module in `common/retry.rs`
that distinguishes between retryable, non-retryable, and fatal errors:
```rust
#[derive(Debug, thiserror::Error)]
pub enum VenueError {
#[error("Retryable error: {source}")]
Retryable {
#[source]
source: VenueRetryableError,
retry_after: Option,
},
#[error("Non-retryable error: {source}")]
NonRetryable {
#[source]
source: VenueNonRetryableError,
},
#[error("Fatal error: {source}")]
Fatal {
#[source]
source: VenueFatalError,
},
}
```
Include helper methods like `from_http_status()`, `from_rate_limit_headers()`, `is_retryable()`,
`is_fatal()`, and `retry_after()` to enable consistent error classification across the adapter.
See BitMEX and Bybit adapters for reference implementations.
### Python exports (`python/mod.rs`)
Mirror the Rust surface area through PyO3 modules by re-exporting clients, enums, and helper functions.
When new functionality lands in Rust, add it to `python/mod.rs` so the Python layer stays in sync
(the OKX adapter is a good reference).
### Python bindings (`python/`)
Expose Rust functionality to Python through PyO3.
Mark venue-specific structs that need Python access with `#[pyclass]` and implement `#[pymethods]` blocks with
`#[getter]` attributes for field access.
For async methods in the HTTP client, use `pyo3_async_runtimes::tokio::future_into_py` to convert Rust futures
into Python awaitables.
When returning lists of custom types, map each item with `Py::new(py, item)` before constructing the Python list.
Register all exported classes and enums in `python/mod.rs` using `m.add_class::()` so they're available
to Python code.
Follow the pattern established in other adapters: prefixing Python-facing methods with `py_*` in Rust while using
`#[pyo3(name = "method_name")]` to expose them without the prefix.
When delivering instruments from WebSocket to Python, use `instrument_any_to_pyobject()` which returns PyO3 types
for caching.
For the reverse direction (Python->Rust), use `pyobject_to_instrument_any()` in `cache_instrument()` methods.
Never call `.into_py_any()` directly on `InstrumentAny` as it doesn't implement the required trait.
### Type qualification
Adapter-specific types (enums, structs) and Nautilus domain types should not be fully qualified.
Import them at the module level and use short names (e.g., `OKXContractType` instead of
`crate::common::enums::OKXContractType`, `InstrumentId` instead of `nautilus_model::identifiers::InstrumentId`).
This keeps code concise and readable.
Only fully qualify types from `anyhow` and `tokio` to avoid ambiguity with similarly-named types from other crates.
### String interning
Use `ustr::Ustr` for any non-unique strings the platform stores repeatedly (venues, symbols, instrument IDs) to
minimise allocations and comparisons.
### Instrument cache standardization
All clients that cache instruments must implement three methods with standardized names: `cache_instruments()`
(plural, bulk replace), `cache_instrument()` (singular, upsert), and `get_instrument()` (retrieve by symbol).
WebSocket clients store instruments in `Arc>` on the outer client for
thread-safe access across clones.
### Testing helpers (`common/testing.rs`)
Store shared fixtures and payload loaders in `src/common/testing.rs` for use across HTTP and WebSocket unit tests.
This keeps `#[cfg(test)]` helpers out of production modules and encourages reuse.
### Instrument status diffing (`common/status.rs`)
When a data client polls instrument status via REST, place the diff logic in `common/status.rs`
rather than inlining it in the data client. The standard function signature is:
```rust
pub fn diff_and_emit_statuses(
new_statuses: &AHashMap,
cached_statuses: &mut AHashMap,
subscriptions: Option<&DashSet>,
sender: &tokio::sync::mpsc::UnboundedSender,
ts_event: UnixNanos,
ts_init: UnixNanos,
)
```
The function compares each entry in `new_statuses` against `cached_statuses`, emitting an
`InstrumentStatus` event for any instrument whose `MarketStatusAction` changed. Instruments
present in the cache but absent from the new snapshot are treated as removed and emit
`NotAvailableForTrading`. The cache always reflects the full API state.
Pass `subscriptions` as `Some(&set)` to restrict emissions to subscribed instruments, or
`None` to emit all changes unconditionally. The data client stores the cache in an
`Arc>>` and calls this function on each
poll cycle.
### Factory module (`factories.rs`)
Complex adapters may define a `factories.rs` module for converting venue data to Nautilus types.
This centralizes transformation logic that would otherwise be scattered across HTTP and WebSocket
parsers:
```rust
// factories.rs
pub fn create_instrument(
venue_instrument: &VenueInstrument,
ts_init: UnixNanos,
) -> anyhow::Result {
match venue_instrument.instrument_type {
InstrumentType::Perpetual => parse_perpetual(venue_instrument, ts_init),
InstrumentType::Future => parse_future(venue_instrument, ts_init),
InstrumentType::Option => parse_option(venue_instrument, ts_init),
}
}
```
Use this pattern when the same venue data structures are parsed in multiple places (HTTP responses,
WebSocket updates, historical data).
### Connection lifecycle (`connect`)
Both data and execution clients follow a strict initialization order during `connect()` to prevent
race conditions with reconciliation and strategy startup. The platform waits for all clients to
signal connected before running reconciliation or starting strategies, so all initialization must
complete within `connect()`.
#### Data event emission
Data clients emit events to the platform through an unbounded channel obtained at
construction:
```rust
let data_sender = get_data_event_sender();
```
The `DataEvent` enum carries all data types the client produces:
| Variant | Usage |
|-------------------------------|-------------------------------------------------------|
| `DataEvent::Instrument` | Instrument definitions during bootstrap and updates. |
| `DataEvent::InstrumentStatus` | Market status changes from polling or WS streams. |
| `DataEvent::Data` | Market data (trades, quotes, book deltas, bars). |
| `DataEvent::Response` | Responses to historical data requests. |
| `DataEvent::FundingRate` | Funding rate updates for derivatives. |
Send events with `self.data_sender.send(DataEvent::Instrument(instrument))`. Log warnings
on send failure but do not propagate the error since a closed receiver means the system
is shutting down. Clone the sender for spawned tasks that emit data from async work.
#### Data client
1. **Fetch instruments via REST** - call `bootstrap_instruments()` or equivalent.
2. **Cache locally** - populate the client's internal instrument map and HTTP client cache.
3. **Emit to data engine** - send each instrument as `DataEvent::Instrument` via `data_sender`.
These events are queued during startup and processed before reconciliation runs.
4. **Cache to WebSocket** - call `ws.cache_instruments()` so the handler can parse messages.
5. **Connect WebSocket** - establish the streaming connection.
```rust
async fn connect(&mut self) -> anyhow::Result<()> {
let instruments = self.bootstrap_instruments().await?;
ws.cache_instruments(instruments);
ws.connect().await?;
ws.wait_until_active(10.0).await?;
// ...
}
```
#### Execution client
1. **Initialize instruments** - call `ensure_instruments_initialized_async()` which checks
`self.core.instruments_initialized()` and returns early if instruments are already cached.
Otherwise it fetches instruments via REST and caches them to the HTTP client, WebSocket
client, and any broadcaster clients.
2. **Connect WebSocket** - establish the private streaming connection.
3. **Subscribe to channels** - orders, executions, positions, wallet/margin.
4. **Start WebSocket stream handler** - begin processing incoming messages.
5. **Fetch account state** - call `refresh_account_state()` which requests balances and
margins via REST, builds an `AccountState`, and emits it through the
`ExecutionEventEmitter`.
6. **Await account registered** - call `await_account_registered(timeout_secs)` which polls
`self.core.cache().account(&account_id)` at 10ms intervals until the account appears or
the timeout expires. This step blocks connect so the portfolio can process orders during
reconciliation.
7. **Signal connected** - call `self.core.set_connected()`.
```rust
async fn connect(&mut self) -> anyhow::Result<()> {
self.ensure_instruments_initialized_async().await?;
self.ws_client.connect().await?;
self.ws_client.wait_until_active(10.0).await?;
// ... subscribe channels, start stream ...
self.refresh_account_state().await?;
self.await_account_registered(30.0).await?;
self.core.set_connected();
Ok(())
}
```
#### Account state emission
The `ExecutionEventEmitter` provides two methods for emitting account state:
- `emit_account_state(balances, margins, reported, ts_event)` builds an `AccountState`
from raw parameters using the internal `OrderEventFactory`, then dispatches it. Use
this when the adapter has individual balance and margin values to combine.
- `send_account_state(state)` dispatches a pre-built `AccountState`. Use this when the
adapter already has a fully constructed state from parsing an HTTP or WebSocket payload.
## HTTP client patterns
Adapters use a two-layer HTTP client architecture: a raw client for low-level API operations and a domain
client for high-level logic. The split also enables efficient cloning for Python bindings.
### Client structure
The architecture consists of two complementary clients:
1. **Raw client** (`MyRawHttpClient`) - Low-level API methods matching venue endpoints.
2. **Domain client** (`MyHttpClient`) - High-level methods using Nautilus domain types.
```rust
use std::sync::Arc;
use nautilus_network::http::HttpClient;
// Raw HTTP client - low-level API methods matching venue endpoints
pub struct MyRawHttpClient {
base_url: String,
client: HttpClient, // Use nautilus_network::http::HttpClient, not reqwest directly
credential: Option,
retry_manager: RetryManager,
cancellation_token: CancellationToken,
}
// Domain HTTP client - wraps raw client with Arc, provides high-level API
pub struct MyHttpClient {
pub(crate) inner: Arc,
// Additional domain-specific state (e.g., instrument cache)
instruments: DashMap,
}
```
**Key points**:
- **Raw client** (`MyRawHttpClient`) contains low-level HTTP methods named to match venue endpoints
(e.g., `get_instruments`, `get_balance`, `place_order`). These methods take venue-specific query
objects and return venue-specific response types.
- **Domain client** (`MyHttpClient`) wraps the raw client in an `Arc` for efficient cloning (required
for Python bindings). It provides high-level methods that accept Nautilus domain types
(e.g., `InstrumentId`, `ClientOrderId`) and return domain objects. It may also cache instruments
or other venue metadata.
- Use `nautilus_network::http::HttpClient` instead of `reqwest::Client` directly for rate limiting,
retry logic, and consistent error handling.
- Both clients are exposed to Python, but the domain client is the primary interface.
### Parser functions
Parser functions convert venue-specific data structures into Nautilus domain objects. Place them in
`common/parse.rs` for cross-cutting conversions (instruments, trades, bars) or `http/parse.rs` for
REST-specific transformations. Each parser takes venue data plus context (account IDs, timestamps,
instrument references) and returns a Nautilus domain type wrapped in `Result`.
**Standard patterns:**
- Handle string-to-numeric conversions with proper error context using `.parse::()` and `anyhow::Context`.
- Check for empty strings before parsing optional fields - venues often return `""` instead of omitting fields.
- Map venue enums to Nautilus enums explicitly with `match` statements rather than implementing automatic conversions that could hide mapping errors.
- Accept instrument references when precision or other metadata is required for constructing Nautilus types (quantities, prices).
- Use descriptive function names: `parse_position_status_report`, `parse_order_status_report`, `parse_trade_tick`.
Place parsing helpers (`parse_price_with_precision`, `parse_timestamp`) in the same module as private functions when they're reused across multiple parsers.
### Timestamp conventions
Nautilus uses `UnixNanos` (nanoseconds since epoch). Most venues deliver `ms`. Convert at the
parser boundary using `nautilus_core::datetime::millis_to_nanos`; document the wire unit on the
struct field. `ts_event` is the converted venue timestamp; `ts_init` is `clock.get_time_ns()`.
For records with no venue timestamp (instruments), use `clock.get_time_ns()` for both.
### Method naming and organization
The raw client mirrors venue endpoints with venue-specific parameter and response types. The domain
client wraps it and exposes high-level methods that accept Nautilus domain types.
**Naming conventions:**
- **Raw client methods**: Named to match venue endpoints as closely as possible (e.g., `get_instruments`, `get_balance`, `place_order`). These methods are internal to the raw client and take venue-specific types (builders, JSON values).
- **Domain client methods**: Named based on operation semantics (e.g., `request_instruments`, `submit_order`, `cancel_order`). These are the methods exposed to Python and take Nautilus domain objects (InstrumentId, ClientOrderId, OrderSide, etc.).
**Domain method flow:**
Domain methods follow a three-step pattern: build venue-specific parameters from Nautilus types, call the corresponding raw client method, then parse the response. For endpoints returning domain objects (positions, orders, trades), call parser functions from `common/parse`. For endpoints returning raw venue data (fee rates, balances), extract the result directly from the response envelope. Methods prefixed with `request_*` indicate they return domain data, while methods like `submit_*`, `cancel_*`, or `modify_*` perform actions and return acknowledgments.
The domain client wraps the raw client in an `Arc` for efficient cloning required by Python bindings.
### Query parameter builders
Use the `derive_builder` crate with proper defaults and ergonomic Option handling:
```rust
use derive_builder::Builder;
#[derive(Clone, Debug, Deserialize, Serialize, Builder)]
#[serde(rename_all = "camelCase")]
#[builder(setter(into, strip_option), default)]
pub struct InstrumentsInfoParams {
pub category: ProductType,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option,
}
impl Default for InstrumentsInfoParams {
fn default() -> Self {
Self {
category: ProductType::Linear,
symbol: None,
limit: None,
}
}
}
```
**Key attributes:**
- `#[builder(setter(into, strip_option), default)]` - enables clean API: `.symbol("BTCUSDT")` instead of `.symbol(Some("BTCUSDT".to_string()))`.
- `#[serde(skip_serializing_if = "Option::is_none")]` - omits optional fields from query strings.
- Always implement `Default` for builder parameters.
### Request signing and authentication
Keep signing logic in a `Credential` struct under `common/credential.rs`:
- Store API keys using `Ustr` for efficient comparison, secrets in `Box<[u8]>` with `#[zeroize]`.
- Implement `sign()` and `sign_bytes()` methods that compute HMAC-SHA256 signatures.
- Pass the credential to the raw HTTP client; the domain client delegates signing through the inner client.
For WebSocket authentication, the handler constructs login messages using the same `Credential::sign()` method with a WebSocket-specific timestamp format.
### Credential module structure
Each adapter's `common/credential.rs` must provide two things:
1. **`credential_env_vars()` free function**: returns environment variable names as a tuple.
2. **`Credential::resolve()` method**: resolves credentials from config values or environment
variables using `resolve_env_var_pair` from `nautilus_core::env`.
Config structs are DTOs and must not contain credential resolution logic. All resolution
belongs in `credential.rs`.
**Standard layout:**
```rust
use nautilus_core::env::resolve_env_var_pair;
/// Returns the environment variable names for API credentials.
pub fn credential_env_vars(is_testnet: bool) -> (&'static str, &'static str) {
if is_testnet {
("{VENUE}_TESTNET_API_KEY", "{VENUE}_TESTNET_API_SECRET")
} else {
("{VENUE}_API_KEY", "{VENUE}_API_SECRET")
}
}
impl Credential {
/// Resolves credentials from provided values or environment variables.
pub fn resolve(
api_key: Option,
api_secret: Option,
is_testnet: bool,
) -> Option {
let (key_var, secret_var) = credential_env_vars(is_testnet);
let (k, s) = resolve_env_var_pair(api_key, api_secret, key_var, secret_var)?;
Some(Self::new(k, s))
}
}
```
### Environment variable conventions
Adapters load API credentials from environment variables when not provided directly, avoiding
hardcoded secrets.
**Naming conventions:**
| Environment | API Key Variable | API Secret Variable |
|--------------|---------------------------|---------------------|
| Mainnet/Live | `{VENUE}_API_KEY` | `{VENUE}_API_SECRET` |
| Testnet | `{VENUE}_TESTNET_API_KEY` | `{VENUE}_TESTNET_API_SECRET` |
| Demo | `{VENUE}_DEMO_API_KEY` | `{VENUE}_DEMO_API_SECRET` |
Some venues require additional credentials:
- OKX: `OKX_API_PASSPHRASE`
**Key principles:**
- Environment variable names must be centralized in `credential_env_vars()`, never
duplicated as string literals across files.
- Environment variable resolution should happen in core Rust code, not Python bindings.
- Use `get_or_env_var_opt` for optional credentials (returns `None` if missing).
- Use `get_or_env_var` when credentials are required (returns error if missing).
- Invalid credentials (e.g. malformed keys) must fail fast with an error, never silently
degrade to unauthenticated mode.
### Error handling and retry logic
Use the `RetryManager` from `nautilus_network` for consistent retry behavior.
### Rate limiting
Configure rate limiting through `HttpClient` using `LazyLock` static variables.
**Naming conventions:**
- REST quotas: `{VENUE}_REST_QUOTA` (e.g., `OKX_REST_QUOTA`, `BYBIT_REST_QUOTA`)
- WebSocket quotas: `{VENUE}_WS_{OPERATION}_QUOTA` (e.g., `OKX_WS_CONNECTION_QUOTA`, `OKX_WS_ORDER_QUOTA`)
- Rate limit keys: `{VENUE}_RATE_LIMIT_KEY_{OPERATION}` (e.g., `OKX_RATE_LIMIT_KEY_SUBSCRIPTION`, `OKX_RATE_LIMIT_KEY_ORDER`)
**Standard rate limit keys for WebSocket:**
| Key | Operations |
|---------------------------------|----------------------------------|
| `*_RATE_LIMIT_KEY_SUBSCRIPTION` | Subscribe, unsubscribe, login. |
| `*_RATE_LIMIT_KEY_ORDER` | Place orders (regular and algo). |
| `*_RATE_LIMIT_KEY_CANCEL` | Cancel orders, mass cancel. |
| `*_RATE_LIMIT_KEY_AMEND` | Amend/modify orders. |
**Example:**
```rust
pub static OKX_REST_QUOTA: LazyLock =
LazyLock::new(|| Quota::per_second(NonZeroU32::new(250).unwrap()));
pub static OKX_WS_SUBSCRIPTION_QUOTA: LazyLock =
LazyLock::new(|| Quota::per_hour(NonZeroU32::new(480).unwrap()));
pub const OKX_RATE_LIMIT_KEY_ORDER: &str = "order";
```
Pass rate limit keys when sending WebSocket messages to enforce per-operation quotas:
```rust
self.send_with_retry(payload, Some(vec![OKX_RATE_LIMIT_KEY_ORDER.to_string()])).await
```
**Policy:**
Adapters should converge on the following rate-limiting principles.
- Map each quota to the scope the venue meters it against (per IP, account, API key, connection,
URL, transport, or operation class). Draw every client that shares a scope (data, execution,
pollers) from one limiter keyed by that scope; separate limiters for a shared cap silently double
the effective rate. Add distinct keys only for sub-caps the venue meters independently.
- Bucket data and execution traffic apart only when the venue meters them apart. The split still
matters for recovery: a data-path trip (subscribe, unsubscribe, control frames) rejects a
subscription, surfaces as missing market data, and recovers through adapter retry or reconnect,
usually without the strategy knowing; an execution-path trip is strategy-visible and governed by
the outcome policy below.
- Pace to the venue's actual metering, not just its headline number. Match the window shape, burst,
and any endpoint weights: a token bucket at the documented rate can still overrun a strict rolling
window after an idle burst. Wire latency does not create rate headroom, since a constant delay
shifts arrival times without changing the rate and jitter bunches messages as readily as it
spreads them. Add headroom only when window semantics or shared external traffic require it, never
as a round-number buffer.
- Bound inflight with a closed-loop gate, not the limiter. When a venue caps concurrent
unacknowledged messages separately from the send rate, gate dispatch on a count that releases a
slot on every terminal outcome: acknowledgement, rejection, send failure, and reconnect. A
send-rate limiter cannot do this, because inflight tracks send rate times acknowledgement latency,
which it never observes.
- Treat an execution-path rate-limit response as an unknown outcome, not a rejection. Per the
[order command outcome policy](#order-command-outcome-policy), a rate-limited command may still
have reached the venue, so retry only when the command is idempotent or the venue proves it was
not processed; otherwise leave the order in flight and reconcile.
## WebSocket client patterns
WebSocket clients handle real-time streaming data. They manage connection state, authentication,
subscriptions, and reconnection logic.
### Client structure
WebSocket adapters use a **two-layer architecture** to separate Python-accessible state from high-performance async I/O:
#### Connection state tracking
Track connection state using `Arc>` to provide lock-free, race-free visibility across all clones:
```rust
use arc_swap::ArcSwap;
pub struct MyWebSocketClient {
connection_mode: Arc>, // Shared connection mode (lock-free)
signal: Arc, // Cancellation signal for graceful shutdown
// ...
}
```
**Pattern breakdown:**
- **Outer `Arc`**: Shared across all clones (Python bindings clone clients before async operations).
- **`ArcSwap`**: Enables atomic pointer replacement via `.store()` without replacing the outer Arc.
- **Inner `Arc`**: The actual connection state from `WebSocketClient::connection_mode_atomic()`.
Initialize with a placeholder atomic (`ConnectionMode::Closed`), then in `connect()` call
`.store(client.connection_mode_atomic())` to atomically swap to the real client's state.
All clones see updates instantly through lock-free `.load()` calls in `is_active()`.
The underlying `WebSocketClient` sends a `RECONNECTED` sentinel message when reconnection completes, triggering resubscription logic in the handler.
**Outer client** (`{Venue}WebSocketClient`):
- Orchestrates connection lifecycle, authentication, subscriptions.
- Maintains state for Python access using `Arc>`.
- Tracks subscription state for reconnection logic.
- Stores instruments cache for replay on reconnect.
- Sends commands to handler via `cmd_tx` channel.
- Receives venue events via `out_rx` channel.
**Inner handler** (`{Venue}WsFeedHandler`):
- Runs in dedicated Tokio task as stateless I/O boundary.
- Owns `WebSocketClient` exclusively (no `RwLock` needed).
- Processes commands from `cmd_rx` -> serializes to JSON -> sends via WebSocket.
- Receives raw WebSocket messages -> deserializes into `{Venue}WsFrame` -> converts to `{Venue}WsMessage` -> emits via `out_tx`.
- Owns pending request state using `AHashMap` (single-threaded, no locking).
- Uses `VecDeque<{Venue}WsMessage>` to buffer multi-message yields from a single frame parse.
Some venues expose separate WebSocket endpoints for market data and order management
(different URLs, authentication flows, or message protocols). In this case, split into
two client+handler pairs under `websocket/data/` and `websocket/orders/` subdirectories,
each following the same two-layer pattern. Name them `{Venue}MdWebSocketClient` /
`{Venue}MdWsFeedHandler` and `{Venue}OrdersWebSocketClient` / `{Venue}OrdersWsFeedHandler`.
**Communication pattern:**
```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
```
**Key principles:**
- **No shared locks on hot path**: Handler owns `WebSocketClient`, client sends commands via lock-free mpsc channel.
- **Command pattern for all sends**: Subscriptions, orders, cancellations all route through `HandlerCommand` enum.
- **Event pattern for state**: Handler emits `{Venue}WsMessage` events (including `Authenticated`), client maintains state from events.
- **Pending state ownership**: Handler owns `AHashMap` for matching responses (no `Arc` between layers).
- **Message buffering**: Handler uses `VecDeque<{Venue}WsMessage>` for frames that produce multiple output messages. The `next()` method drains the queue before polling channels.
- **Python constraint**: Client uses `Arc` only for state Python might query; handler uses `AHashMap` for internal matching.
#### Handler initialization handshake (`SetClient`)
The handler does not own its `WebSocketClient` at construction time.
`WebSocketClient` is not `Clone` and is awkward to move into an already-spawned
task constructor; several adapters use a deferred handoff through the command
channel. Lighter uses this stricter ordering:
1. The outer client calls `WebSocketClient::connect(...)` and obtains the
live client.
2. The outer client creates the local `cmd_tx`/`cmd_rx` and `out_tx`/`out_rx`
channels. The connection-mode atomic is captured into a local before
`client` is moved.
3. The outer client sends `HandlerCommand::SetClient(client)` on the local
`cmd_tx` first, followed by any cache-replay commands
(e.g. `InitializeInstruments`).
4. Only after `SetClient` is queued does the outer client publish the new
command channel (swap `self.cmd_tx`) and store the captured connection
mode (transition `is_active()` to true). Doing this in the opposite
order races: a clone observing `is_active()` could enqueue a Subscribe
on the published `cmd_tx` before SetClient lands, and the handler would
drop it because `inner == None`.
5. The outer client spawns the handler task with `cmd_rx`. The handler
constructor takes `inner: Option` initialized to
`None`; the first command it processes is `SetClient`, which moves the
client into `self.inner`.
6. Any subscribe/order commands queued by clones after step 4 land behind
`SetClient` and `InitializeInstruments` in `cmd_rx`, so they reach a
fully wired handler in queue order.
```rust
pub enum HandlerCommand {
SetClient(WebSocketClient),
Disconnect,
Subscribe { /* ... */ },
// ... other commands
}
pub(super) struct {Venue}WsFeedHandler {
inner: Option, // None until SetClient
cmd_rx: tokio::sync::mpsc::UnboundedReceiver,
// ...
}
// In the cmd_rx match arm:
HandlerCommand::SetClient(client) => {
self.inner = Some(client);
}
```
BitMEX, OKX, Bybit, Hyperliquid, and Lighter all use `SetClient` to hand the
connected `WebSocketClient` to the handler. Lighter queues `SetClient` before it
publishes the new `cmd_tx` or marks the connection active; older adapters may
publish the command channel first.
### Authentication
Authentication state is managed through events:
- Handler processes `Login` response -> **returns** `{Venue}WsMessage::Authenticated` immediately.
- Client receives event -> updates local auth state -> proceeds with subscriptions.
- `AuthTracker` (from `nautilus_network::websocket::auth`) tracks auth state across threads.
The `AuthTracker` struct from `nautilus_network` provides thread-safe authentication state:
```rust
pub struct AuthTracker {
tx: Arc>>,
authenticated: Arc,
}
```
`AuthTracker` is internally `Arc`-based, so cloning shares state. Both client and handler
store `auth_tracker: AuthTracker` and receive a `.clone()` of the same instance. The tracker
exposes a four-method lifecycle: `begin()` starts an attempt and returns a one-shot receiver,
`succeed()` sets the authenticated flag and notifies the receiver, `fail(message)` clears
the flag with an error, and `invalidate()` clears the flag on disconnect. Downstream
consumers query `is_authenticated()` for lock-free reads via the internal `AtomicBool`.
**Note**: The `Authenticated` message is consumed in the client's spawn loop for reconnection
flow coordination and is not forwarded to downstream consumers (data/execution clients).
Downstream consumers can query authentication state via `AuthTracker` if needed. The execution
client's `Authenticated` handler only logs at debug level with no important logic depending
on this event.
#### Auth-token rotation (single-endpoint mixed-trust adapters)
Some venues run public market data and authenticated account channels through a
single WebSocket endpoint, gated by a short-lived bearer token attached to each
subscribe request. Lighter is the canonical example: the token is a Schnorr
signature over `(deadline, account_index, api_key_index)` with a venue-imposed
hard cap of 8 hours (`LIGHTER_AUTH_TOKEN_MAX_TTL`). `build_auth_token_for(...)`
currently emits a 7-hour token, and the execution client refreshes account
channel subscriptions every 6 hours.
This contrasts with the per-message-signature pattern (Hyperliquid) and the
session-login pattern (BitMEX, Bybit); neither needs in-session token rotation.
**Token lifecycle**
The outer client owns the schedule; the handler owns the wire send. The
flow is:
1. **Mint before account subscribes**: The execution client calls
`build_auth_token_for(...)` after the WebSocket reaches active state, then
uses that token for the initial account-channel subscriptions.
2. **Distribute on subscribe**: `subscribe_account(...)` attaches the token to
`HandlerCommand::Subscribe`. The WebSocket client stores the exact
`(channel, auth)` pair in `subscription_args` for reconnect replay.
3. **Schedule refresh**: After the execution WebSocket consumer starts, the
execution client spawns a refresh task on `get_runtime()`. The task sleeps
for `AUTH_TOKEN_REFRESH_INTERVAL` (6 hours), mints a new token, and re-issues
`subscribe_account(...)` for every account channel.
4. **Stop on disconnect**: The refresh task observes the execution client's
cancellation token and exits when the client stops or disconnects.
**Where the schedule lives**
Place the rotation timer in the outer client, not the handler. The execution
client owns the credential and decides when to mint; the handler remains an I/O
boundary that sends the supplied token and signs nothing on its own.
```rust
fn spawn_auth_token_refresh(&self, credential: Credential) {
let ws_client = self.ws_client.clone();
let cancellation_token = self.cancellation_token.clone();
let account_index = credential.account_index();
let channels = [
LighterWsChannel::AccountAllOrders(account_index),
LighterWsChannel::AccountAllTrades(account_index),
LighterWsChannel::AccountAllPositions(account_index),
LighterWsChannel::AccountAllAssets(account_index),
];
get_runtime().spawn(async move {
loop {
tokio::select! {
() = cancellation_token.cancelled() => break,
() = tokio::time::sleep(AUTH_TOKEN_REFRESH_INTERVAL) => {
if let Ok(token) = build_auth_token_for(&credential) {
for channel in channels.clone() {
let _ = ws_client
.subscribe_account(channel, token.clone())
.await;
}
}
}
}
}
});
}
```
**Reconnect interaction**
On `Reconnected`, the Lighter WebSocket client replays the tracked
`subscription_args` through `HandlerCommand::Subscribe`. It does not mint a
fresh token on reconnect; fresh account-channel tokens come from the scheduled
execution-client refresh task.
**Failure handling**
Subscription send failures call `mark_failure(topic)` so reconnect replay keeps
the topic pending. Lighter does not currently implement an immediate auth-token
refresh path on a mid-session venue rejection.
### Subscription management
#### Shared `SubscriptionState` pattern
The `SubscriptionState` struct from `nautilus_network::websocket` is shared between client and handler using `Arc>` internally for thread-safe access:
- **`SubscriptionState` is shared via `Arc`**: Both client and handler receive `.clone()` of the same instance (shallow clone of Arc pointers).
- **Responsibility split**: Client tracks user intent (`mark_subscribe`, `mark_unsubscribe`), handler tracks server confirmations (`confirm_subscribe`, `confirm_unsubscribe`, `mark_failure`).
- **Why both need it**: Single source of truth with lock-free concurrent access, no synchronization overhead.
#### Subscription lifecycle
A **subscription** represents any topic in one of two states:
| State | Description |
|---------------|-------------|
| **Pending** | Subscription request sent to venue, awaiting acknowledgment. |
| **Confirmed** | Venue acknowledged subscription and is actively streaming data. |
State transitions follow this lifecycle:
| Trigger | Method Called | From State | To State | Notes |
|-------------------|----------------------|------------|-----------|-------|
| User subscribes | `mark_subscribe()` | | Pending | Topic added to pending set. |
| Venue confirms | `confirm()` | Pending | Confirmed | Moved from pending to confirmed. |
| Venue rejects | `mark_failure()` | Pending | Pending | Stays pending for retry on reconnect. |
| User unsubscribes | `mark_unsubscribe()` | Confirmed | Pending | Temporarily pending until ack. |
| Unsubscribe ack | `clear_pending()` | Pending | Removed | Topic fully removed. |
**Key principles**:
- `subscription_count()` reports **only confirmed subscriptions**, not pending ones.
- Failed subscriptions remain pending and are automatically retried on reconnect.
- Both confirmed and pending subscriptions are restored after reconnection.
- Unsubscribe operations must check the `op` field in acknowledgments to avoid re-confirming topics.
#### Confirmation timing
The handler is responsible for transitioning topics from Pending to Confirmed.
Two patterns are established, chosen per venue based on what the wire format
provides:
**Explicit ack (preferred when the venue supports it)**: used by BitMEX, OKX,
Bybit, and Lighter. The venue sends a dedicated subscribe/unsubscribe
acknowledgment frame (typically
`{ "event": "subscribe", "arg": ..., "code": ... }` or similar). The handler
matches that frame, derives the topic from its `arg`/`req_id` field, and
dispatches:
- success -> `confirm_subscribe(topic)` / `confirm_unsubscribe(topic)`.
- failure -> `mark_failure(topic)` (stays pending; retried on reconnect).
- unsubscribe failures should be treated as still-subscribed and reconfirmed.
Keep the ack handling in one branch or function invoked from the wire-frame
match arm so the lifecycle is auditable in one place.
**Implicit-on-first-frame (fallback)**: used by Lighter as a backstop and by any
venue that either omits subscribe acks or makes them unreliable. The handler calls
`confirm_subscribe(topic)` when the first inbound data frame for that topic
arrives. The topic is recovered from the frame's `channel` field. This
doubles as a backstop when an explicit ack is dropped or arrives after the
first data frame.
```rust
// Inside the data-frame match arm:
let topic = frame_topic(&frame);
self.subscriptions.confirm_subscribe(&topic);
// ... then parse and emit
```
The two patterns can coexist: a venue that sometimes sends acks and sometimes
doesn't can use the explicit handler for ack frames and the implicit
backstop on data frames; `confirm_subscribe()` is idempotent.
Failure paths must call `mark_failure(topic)` (not silently drop the
subscription). `mark_failure()` keeps the topic pending so the reconnect
replay restores it.
#### Topic format patterns
Adapters use venue-specific delimiters to structure subscription topics:
| Adapter | Delimiter | Example | Pattern |
|--------------|-----------|------------------------|------------------------------|
| **BitMEX** | `:` | `trade:XBTUSD` | `{channel}:{symbol}` |
| **OKX** | `:` | `trades:BTC-USDT-SWAP` | `{channel}:{symbol}` |
| **Bybit** | `.` | `orderbook.50.BTCUSDT` | `{channel}.{depth}.{symbol}` |
| **Lighter** | `:` / `/` | `order_book:0` | `{channel}:{market_index}` |
Parse topics using `split_once()` with the appropriate delimiter to extract channel and symbol components.
##### Asymmetric inbound vs outbound delimiters
Some venues use different separators for outbound subscribe payloads versus
inbound frame `channel` fields. Lighter is the canonical example: outbound
subscribe uses `order_book/0` (slash), inbound frames carry
`"channel": "order_book:0"` (colon).
Established workaround:
- Pick the inbound separator for `SubscriptionState::new(delimiter)` so the
handler can confirm subscriptions directly against the `channel` field of
every received frame.
- Expose two methods on the venue's channel/subscription enum:
- `subscription_channel()` returns the outbound-formatted payload (used
when serializing the subscribe/unsubscribe request).
- `topic_key()` returns the canonical topic key (matching the inbound
form) used to key `SubscriptionState` and the reconnection replay map.
This keeps a single canonical topic identity throughout the handler while
honoring the venue's wire format on the way out.
### Reconnection logic
On reconnection, restore authentication and subscriptions:
1. **Track subscriptions**: Preserve original subscription arguments in collections (e.g., `Arc`) to avoid parsing topics back to arguments.
2. **Reconnection flow**:
- Receive `{Venue}WsMessage::Reconnected` from handler.
- If authenticated: Re-authenticate and wait for confirmation.
- Restore all tracked subscriptions via handler commands.
- Forward `{Venue}WsMessage::Reconnected` to downstream consumers via
`out_tx` when those consumers need to reset local state. BitMEX, OKX,
Bybit, and Lighter forward this event after restore is initiated;
Hyperliquid currently consumes it in the WebSocket client after
resubscribing.
For adapters with an `Authenticated` event, the client spawn loop can consume it
for reconnection coordination instead of forwarding it. Downstream consumers can
query `AuthTracker` when they need authentication state.
**Preserving subscription arguments:**
Store original subscription arguments in a separate collection to enable deterministic reconnection
replay without parsing topics back into arguments:
```rust
pub struct MyWebSocketClient {
subscription_state: Arc,
subscription_args: Arc>, // topic -> original args
// ...
}
impl MyWebSocketClient {
async fn subscribe(&self, args: SubscriptionArgs) -> Result<(), Error> {
let topic = args.to_topic();
self.subscription_state.mark_subscribe(&topic);
self.subscription_args.insert(topic.clone(), args.clone());
self.send_cmd(HandlerCommand::Subscribe(args)).await
}
async fn unsubscribe(&self, topic: &str) -> Result<(), Error> {
self.subscription_state.mark_unsubscribe(topic);
self.subscription_args.remove(topic);
self.send_cmd(HandlerCommand::Unsubscribe(topic.to_string())).await
}
async fn restore_subscriptions(&self) {
for entry in self.subscription_args.iter() {
let _ = self.send_cmd(HandlerCommand::Subscribe(entry.value().clone())).await;
}
}
}
```
This avoids complex topic parsing and ensures subscriptions are replayed exactly as originally
requested.
### Ping/Pong handling
Support both WebSocket control frame pings and application-level text pings:
- **Control frame pings**: Handled automatically by `WebSocketClient` via the `PingHandler` callback.
- **Text pings**: Some venues (e.g., OKX) use `"ping"`/`"pong"` text messages. Configure `heartbeat_msg: Some(TEXT_PING.to_string())` in `WebSocketConfig` and respond to incoming `TEXT_PING` with `TEXT_PONG` in the handler.
The handler should check for ping messages early in the message processing loop and respond immediately to maintain connection health.
### Disconnection lifecycle (`close`)
The `close()` method follows a three-step shutdown sequence: signal, command, await.
```rust
impl MyWebSocketClient {
pub async fn close(&mut self) -> Result<(), MyWsError> {
tracing::debug!("Starting close process");
// 1. Send disconnect command so handler can clean up gracefully
if let Err(e) = self.cmd_tx.read().await.send(HandlerCommand::Disconnect) {
tracing::warn!("Failed to send disconnect command to handler: {e}");
}
// 2. Set stop signal so handler loop exits after processing disconnect
self.signal.store(true, Ordering::Release);
// 3. Await task handle with timeout, abort if stuck
if let Some(task_handle) = self.task_handle.take() {
match Arc::try_unwrap(task_handle) {
Ok(handle) => {
let abort_handle = handle.abort_handle();
match tokio::time::timeout(Duration::from_secs(2), handle).await {
Ok(Ok(())) => tracing::debug!("Handler task completed"),
Ok(Err(e)) => tracing::error!("Handler task error: {e:?}"),
Err(_) => {
tracing::warn!("Timeout waiting for handler task, aborting");
abort_handle.abort();
}
}
}
Err(arc_handle) => {
tracing::debug!("Cannot unwrap task handle, aborting");
arc_handle.abort();
}
}
}
Ok(())
}
}
```
**Key points:**
- Send `Disconnect` before setting the stop signal so the handler processes it before exiting.
- Return `Result<(), {Venue}WsError>` so callers can handle failures.
- Use `Ordering::Release` on the signal store so the handler sees the write.
- Extract `abort_handle` before awaiting so it remains available after timeout.
- When `Arc::try_unwrap` fails (other clones exist), abort directly.
### Stream consumption (`stream`)
The outer client exposes a `stream()` method that hands ownership of `out_rx` to the
caller as an async stream. Data and execution clients call this once to drive their
message processing loop:
```rust
impl MyWebSocketClient {
pub fn stream(&mut self) -> impl Stream + 'static {
let rx = self
.out_rx
.take()
.expect("Stream receiver already taken or not connected");
let mut rx = Arc::try_unwrap(rx)
.expect("Cannot take ownership - other references exist");
async_stream::stream! {
while let Some(msg) = rx.recv().await {
yield msg;
}
}
}
}
```
The data/execution client consumes the stream in a `tokio::select!` loop with a
cancellation token or stop signal, matching on `{Venue}WsMessage` variants and calling
parse functions to produce Nautilus domain types.
### Subscription topic helpers (`subscription.rs`)
When a venue's subscription topics have complex structure (multiple parameter types,
instrument type / family / ID variants, candle width encoding), extract topic building
and parsing into `websocket/subscription.rs`. This keeps `client.rs` focused on
connection lifecycle and `handler.rs` focused on I/O.
For venues with simple `{channel}:{symbol}` topics, inline helpers in the client are
sufficient and a separate module is not needed.
### Handler configuration constants
Define handler-specific tuning constants for consistent behavior:
| Constant | Purpose | Typical value |
|----------------------------|--------------------------------------------------|---------------|
| `DEFAULT_HEARTBEAT_SECS` | Interval for sending keep‑alive messages. | 15-30 |
| `WEBSOCKET_AUTH_WINDOW_MS` | Maximum age for authentication timestamps. | 5000-30000 |
| `BATCH_PROCESSING_LIMIT` | Maximum messages processed per event loop cycle. | 100-1000 |
Place these in `websocket/handler.rs` or `common/consts.rs` depending on scope.
### Message routing
The handler uses two message enums to separate wire deserialization from emitted events.
The data and execution client layers convert emitted events into Nautilus domain types.
Define two enums:
1. **`{Venue}WsFrame`**: Serde-deserialized wire frames. Contains every JSON shape the venue
can send (login responses, subscription acks, channel data, order responses, errors, pings).
Typically `pub(super)` since only the handler uses it.
2. **`{Venue}WsMessage`**: Handler output events emitted on `out_tx`. Contains the subset of
wire data the client needs plus synthetic control variants (`Reconnected`, `Authenticated`,
`SendFailed`) that have no wire representation. This is the `pub` type consumers match on.
The handler deserializes raw text into `{Venue}WsFrame`, handles control frames internally
(subscription acks, login, pings), and converts relevant frames into `{Venue}WsMessage` events
sent via `out_tx`. The client receives from `out_rx` and routes to data/execution callbacks,
which convert venue types to Nautilus domain types using parse functions.
#### Message type naming convention
Types prefixed with the venue name (e.g., `OKX`, `Bitmex`) contain raw exchange-specific types.
Types prefixed with `Nautilus` contain normalized domain types ready for the trading system.
**Wire frame enum (serde-deserialized, handler-internal):**
```rust
pub(super) enum MyWsFrame {
Login { event, code, msg, conn_id },
Subscription { event, arg, conn_id, code, msg },
OrderResponse { id, op, code, msg, data },
BookData { arg, action, data: Vec },
Data { arg, data: Value },
Error { code, msg },
Ping,
Reconnected,
}
```
**Handler output enum (emitted to client):**
```rust
pub enum MyWsMessage {
BookData { arg, action, data: Vec },
ChannelData { channel, inst_id, data: Value },
Orders(Vec),
OrderResponse { id, op, code, msg, data },
SendFailed { request_id, client_order_id, op, error },
Instruments(Vec),
Error(MyWebSocketError),
Reconnected,
Authenticated,
}
```
The frame enum includes every wire shape (login acks, subscription acks, pings) for
deserialization. The output enum drops shapes the handler consumes internally and adds synthetic
variants (`Authenticated`, `SendFailed`) that originate in handler logic, not on the wire.
Include `OrderResponse` for venue acknowledgements (place, cancel, amend) and `SendFailed` for
WebSocket send failures after retries are exhausted. The execution client dispatch layer may
convert `OrderResponse` into Nautilus rejection events when the venue explicitly rejects the
command. It must treat `SendFailed` as an unknown outcome and leave the order state open to
reconciliation.
**Conversion in data/exec client:**
The data client's message loop matches on `{Venue}WsMessage` variants and calls parse functions
to produce Nautilus domain types (`Data`, `OrderBookDeltas`, etc.). The execution client's
dispatch layer handles `OrderResponse`, `SendFailed`, and `Orders` variants. `SendFailed`
records that the venue outcome is unknown; it is not a rejection. This keeps the handler focused
on I/O and deserialization while the client layers own domain conversion.
The execution dispatch converts order and fill messages using a two-tier routing contract:
1. The handler emits venue-specific order types (e.g., `Orders(Vec)`).
2. The client dispatch layer tracks which orders were submitted through this client.
3. **Tracked order**: convert venue types to order events (`OrderAccepted`, `OrderCanceled`,
`OrderFilled`, etc.) and synthesize any missing lifecycle events (e.g., `OrderAccepted`
before a fast fill).
4. **External/unknown order**: convert to reports (`OrderStatusReport` or `FillReport`) for
downstream reconciliation.
When the venue outcome is unconfirmed (a command times out, a stream message races the HTTP
response, or a partial state arrives mid-modify), leave the order in its pending state and let the
live execution engine resolve it from venue state. Do not add adapter code to cover every such
race: the engine already owns truth-from-venue resolution through the inflight check and open-order
queries, whose reports reconcile the order. Direct events carry the live happy path, reports carry
external orders and reconciliation, and the engine reconciles the races. For example, a Betfair
`replaceOrders` timeout leaves the order `PendingUpdate`, and the inflight check resolves it from
venue state rather than the adapter adding bespoke timeout-recovery logic.
#### `WsDispatchState`
Execution dispatch state lives in a `WsDispatchState` struct defined in `websocket/dispatch.rs`.
It tracks which lifecycle events have already been emitted to prevent duplicates across
reconnections and fast-fill races:
```rust
#[derive(Debug, Default)]
pub struct WsDispatchState {
pub order_identities: DashMap,
pub emitted_accepted: DashSet,
pub triggered_orders: DashSet,
pub filled_orders: DashSet,
clearing: AtomicBool,
}
```
| Field | Purpose |
|---------------------|-----------------------------------------------------------------|
| `order_identities` | Maps client order ID to identity metadata set at submission. |
| `emitted_accepted` | Prevents duplicate `OrderAccepted` events. |
| `triggered_orders` | Tracks conditional orders that have triggered. |
| `filled_orders` | Prevents duplicate `OrderFilled` events on reconnect replay. |
| `clearing` | Guards concurrent eviction when sets reach capacity. |
Each `DashSet` is bounded by a `DEDUP_CAPACITY` constant (typically 10,000). When a set
reaches capacity, `evict_if_full()` clears it atomically using a compare-exchange on the
`clearing` flag to prevent concurrent clears.
The `dispatch_ws_message()` free function in the same module routes `{Venue}WsMessage`
variants to the appropriate order event builders, using `WsDispatchState` for dedup
and `OrderIdentity` for tracked-vs-external classification.
#### Cross-source fill deduplication
`WsDispatchState` prevents duplicate lifecycle events within a single stream. When an
adapter receives fills from multiple sources (WebSocket user data and HTTP reconciliation),
a separate trade-ID-level dedup is needed to prevent the same fill from being emitted twice.
The `BoundedDedup` pattern addresses this with a fixed-capacity set backed by a
`VecDeque` for insertion order and an `AHashSet` for O(1) lookup. When the set reaches
capacity, the oldest entry is evicted (FIFO). The `insert()` method returns `true` if the
value was already present, signaling a duplicate:
```rust
struct BoundedDedup {
order: VecDeque,
set: AHashSet,
capacity: usize,
}
```
Use this in the execution client to track trade IDs (typically as `(Ustr, i64)` tuples
of symbol and trade ID). A capacity of 10,000 provides sufficient coverage for most
venues without unbounded memory growth.
#### Cancel-replace modifies and in-flight fills
Some venues (e.g. Hyperliquid) implement a modify as a cancel-replace: the venue assigns a new
venue order ID and emits `ACCEPTED(new_voi)` with `CANCELED(old_voi)`. The dispatch promotes the
new leg to `OrderUpdated` and suppresses the stale cancel. A fill carrying the replacement venue
order ID drives the same promotion (falling back to buffering only when the order has no price), so
a dropped `ACCEPTED` does not strand the fill. Fills on the old venue order ID count separately, so
the replacement is sized at the remaining (`target - filled`), not the total.
That subtraction runs when the modify is dispatched, so a fill landing after the request is sent
leaves the replacement oversized and the venue can overfill the order. To prevent this, the
cancel-replace promotion:
- re-reads the cumulative filled quantity;
- queues a corrective reduce of the new venue order to the true remaining when oversized;
- re-arms the in-flight marker on the new venue order ID to suppress the corrective's cancel leg.
The reduce posts off the receive loop. It narrows but does not close the race: if the replacement
fills before the reduce lands, the engine's overfill guard is the backstop.
### Error handling
#### Order command outcome policy
Adapters must emit these rejection events only from definitive command-failure evidence:
- `OrderRejected`.
- `OrderModifyRejected`.
- `OrderCancelRejected`.
Positive venue evidence includes structured order responses, per-order batch responses, or order
status messages that explicitly report a rejection. Positive local evidence includes prepare
failures that prove a cancel or modify command cannot be sent and can be attributed to a single
order command.
Local validation is not automatically a venue rejection:
- Validate submit commands before `OrderSubmitted` and emit `OrderDenied` when validation fails.
- If submit validation fails after `OrderSubmitted`, log the failure and leave the order in flight.
- If cancel or modify prepare fails before the command is sent, emit
`OrderCancelRejected` or `OrderModifyRejected` only when the adapter can attribute the failure
to that command. Otherwise log a warning and do not emit a rejection event.
Do not emit rejection events for errors that leave the venue outcome unknown. Unknown outcomes
include transport errors, WebSocket send failures, request timeouts, disconnects, canceled local
tasks, missing acknowledgements, HTTP 5xx responses, rate limits, retry exhaustion, parse failures
after a request may have reached the venue, and whole-batch request failures without per-order
venue results.
When the outcome is unknown, leave the order in its current in-flight state and let WebSocket
updates, in-flight checks, open-order polling, startup reconciliation, or explicit query commands
resolve the final state. For batch commands, emit rejection events only for per-order venue
results that unambiguously reject the command; a whole-request failure must not become one
rejection per order.
Cancel and modify errors need venue-specific allowlists. A generic venue error such as "not
found", "already closed", or "unknown order" can mean the order filled or canceled before the
request was processed. Emit `OrderCancelRejected` or `OrderModifyRejected` only when the venue
semantics make the command rejection unambiguous.
#### Client-side error propagation
Channel send failures (client -> handler) should propagate loudly as `Result<(), Error>`:
```rust
impl MyWebSocketClient {
async fn send_cmd(&self, cmd: HandlerCommand) -> Result<(), Error> {
self.cmd_tx.read().await.send(cmd)
.map_err(|e| Error::ClientError(format!("Handler not available: {e}")))
}
pub async fn submit_order(...) -> Result<(), Error> {
let cmd = HandlerCommand::PlaceOrder { ... };
self.send_cmd(cmd).await // Propagates channel failures
}
}
```
#### Handler-side retry logic
WebSocket send failures (handler -> network) should be retried by the handler using `RetryManager`:
```rust
pub struct MyWsFeedHandler {
inner: Option,
retry_manager: RetryManager,
// ...
}
impl MyWsFeedHandler {
async fn send_with_retry(&self, payload: String, rate_limit_keys: Option>) -> Result<(), MyWsError> {
if let Some(client) = &self.inner {
self.retry_manager.execute_with_retry(
"websocket_send",
|| async {
client.send_text(payload.clone(), rate_limit_keys.clone())
.await
.map_err(|e| MyWsError::ClientError(format!("Send failed: {e}")))
},
should_retry_error,
create_timeout_error,
).await
} else {
Err(MyWsError::ClientError("No active WebSocket client".to_string()))
}
}
async fn handle_place_order(...) -> anyhow::Result<()> {
let payload = serde_json::to_string(&request)?;
match self.send_with_retry(payload, Some(vec![RATE_LIMIT_KEY])).await {
Ok(()) => Ok(()),
Err(e) => {
// Emit SendFailed so dispatch can record an unknown outcome.
let _ = self.out_tx.send(MyWsMessage::SendFailed {
request_id: request_id.clone(),
client_order_id: Some(client_order_id),
op: Some(MyWsOperation::Order),
error: e.to_string(),
});
Err(anyhow::anyhow!("Failed to send order: {e}"))
}
}
}
}
fn should_retry_error(error: &MyWsError) -> bool {
match error {
MyWsError::NetworkError(_) | MyWsError::Timeout(_) => true,
MyWsError::AuthenticationError(_) | MyWsError::ParseError(_) => false,
}
}
```
**Key principles:**
- Client propagates channel failures immediately (handler unavailable).
- Handler retries transient WebSocket failures (network issues, timeouts).
- Handler emits `SendFailed` when retries are exhausted; the exec client dispatch records an
unknown outcome and waits for reconciliation or a later venue update.
- Use `RetryManager` from `nautilus_network::retry` for consistent backoff.
### Naming conventions
Adapters follow standardized naming conventions for consistency across all venue integrations.
#### Channel naming: `raw` -> `out`
WebSocket message channels follow a two-stage transformation pipeline within the handler:
| Stage | Type | Description | Example |
|-------|------|-------------|---------|
| `raw` | Raw WebSocket frames | Bytes/text from the network layer. | `raw_rx: UnboundedReceiver` |
| `out` | Venue‑specific messages | Parsed venue message types. | `out_tx: UnboundedSender` |
The handler deserializes raw frames into venue-specific types and emits them on `out_tx`.
The data and execution client layers then convert venue types into Nautilus domain types.
**Example flow:**
```rust
// Client creates output channel for venue messages
let (out_tx, out_rx) = tokio::sync::mpsc::unbounded_channel(); // Venue messages (MyWsMessage)
// Handler receives raw frames, outputs venue messages
let handler = MyWsFeedHandler::new(
cmd_rx,
raw_rx, // Input: Message (raw WebSocket frames)
out_tx, // Output: MyWsMessage
// ...
);
```
Channel names reflect the data transformation stage, not the destination. Use `raw_*` for raw
WebSocket frames (`Message`) and `out_*` for venue-specific message types.
### Backpressure strategy
WebSocket channels on latency-sensitive paths are intentionally **unbounded**. The platform
prioritizes latency and prefers an explicit crash (OOM) over delaying or dropping data.
:::note
Do not add bounded channels, buffering limits, or backpressure unless the latency requirement changes.
:::
#### Field naming: `inner` and command channels
Structs holding references to lower-level components follow these conventions:
| Field | Type | Description |
|---------------|-----------------------------------------------------|-------------|
| `inner` | `Option` | Network‑level WebSocket client (handler only, exclusively owned). |
| `cmd_tx` | `Arc>>` | Command channel to handler (client side). |
| `cmd_rx` | `UnboundedReceiver` | Command channel from client (handler side). |
| `out_tx` | `UnboundedSender<{Venue}WsMessage>` | Output channel to client (handler side). |
| `out_rx` | `Option>>` | Output channel from handler (client side). |
| `task_handle` | `Option>>` | Handler task handle. |
**Example:**
```rust
// Client struct
pub struct MyWebSocketClient {
cmd_tx: Arc>>,
out_rx: Option>>,
task_handle: Option>>,
connection_mode: Arc>, // Lock-free connection state
// ...
}
impl MyWebSocketClient {
async fn send_cmd(&self, cmd: HandlerCommand) -> Result<(), Error> {
self.cmd_tx.read().await.send(cmd)
.map_err(|e| Error::ClientError(format!("Handler not available: {e}")))
}
}
// Handler struct
pub(super) struct MyWsFeedHandler {
inner: Option, // Exclusively owned - no RwLock
cmd_rx: UnboundedReceiver,
raw_rx: UnboundedReceiver,
out_tx: UnboundedSender,
pending_requests: AHashMap, // Single-threaded - no locks
pending_messages: VecDeque, // Multi-message buffer
// ...
}
```
The handler exclusively owns `WebSocketClient` without locks. The client sends commands via
`cmd_tx` (wrapped in `RwLock` to allow reconnection channel replacement) and receives events
via `out_rx`. Use a `send_cmd()` helper to standardize command sending.
#### Type naming: `{Venue}Ws{TypeSuffix}`
All WebSocket-related types follow a standardized naming pattern: `{Venue}Ws{TypeSuffix}`
- `{Venue}`: Capitalized venue name (e.g., `OKX`, `Bybit`, `Bitmex`, `Hyperliquid`).
- `Ws`: Abbreviated "WebSocket" (not fully spelled out).
- `{TypeSuffix}`: Full type descriptor (e.g., `Message`, `Error`, `Request`, `Response`).
**Examples:**
```rust
// Correct - abbreviated Ws, full type suffix
pub enum OKXWsMessage { ... }
pub enum BybitWsError { ... }
pub struct HyperliquidWsRequest { ... }
```
**Standard type suffixes:**
- `Message`: WebSocket message enums.
- `Error`: WebSocket error types.
- `Request`: Request message types.
- `Response`: Response message types.
**Tokio channel qualification:**
Always fully qualify tokio channel types as `tokio::sync::mpsc::` to avoid ambiguity with
similarly-named types from other crates. Never import `mpsc` directly at module level.
```rust
// Correct
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::();
```
### Split WebSocket architectures
Some venues expose multiple WebSocket endpoints with distinct protocols or encodings.
When a venue requires separate connections for market data and order management, split
the `websocket/` module into submodules that mirror the connection boundaries:
```
src/
├── websocket/
│ ├── mod.rs # Re-exports from submodules
│ ├── streams/ # Market data pub/sub connection
│ │ ├── client.rs # Streams client
│ │ ├── handler.rs # Streams feed handler
│ │ ├── messages.rs # Streams message types
│ │ └── mod.rs
│ └── trading/ # Order management + user data (authenticated WS API)
│ ├── client.rs # Trading client
│ ├── handler.rs # Trading handler
│ ├── messages.rs # Trading message types
│ ├── user_data.rs # User data stream venue types (execution reports, etc.)
│ ├── parse.rs # Parse functions for user data -> Nautilus types
│ ├── error.rs # Trading error types
│ └── mod.rs
```
Each submodule follows the same two-layer client/handler pattern described above. The
parent `websocket/mod.rs` re-exports the public client types.
The `trading/` module handles both order operations (place, cancel, modify) and the
user data stream (execution reports, account updates). When the venue's authenticated
WebSocket API supports `session.logon` and inline user data subscriptions, both
concerns share a single authenticated connection. This avoids a separate `execution/`
module and the deprecated REST listenKey lifecycle.
For venues where user data events arrive on a separate stream connection (e.g.,
futures APIs that return a listenKey for a dedicated stream URL), the `streams/`
handler dispatches both market data and user data events from the combined connection.
#### Naming conventions for split architectures
Type names include the submodule qualifier to avoid ambiguity:
| Submodule | Command type | Message type |
|--------------|--------------------------------------|-------------------------------------|
| `streams/` | `{Venue}WsStreamsCommand` | `{Venue}WsMessage` (venue types) |
| `trading/` | `{Venue}WsTradingCommand` | `{Venue}WsTradingMessage` |
The `{Venue}Ws` prefix follows the standard type naming convention. The qualifier
(`Streams`, `Trading`) distinguishes types that would otherwise collide across
submodules.
#### When to split
Split the WebSocket module when the venue has:
- Different endpoints with different protocols (e.g., SBE binary for market data, JSON
for trading)
- A dedicated order management WebSocket API (`ws-api` style) alongside pub/sub streams
- User data delivered inline on the authenticated trading connection rather than via a
separate listenKey stream
Do not split when a single connection handles all message types through channel-based
multiplexing (the common pattern for OKX, Bybit, and similar venues).
### Multi-product WebSocket management
Some venues use the same WebSocket protocol for all product types but serve them on
separate endpoints (e.g., Bybit provides distinct URLs for Linear, Spot, and Inverse).
In this case the data client creates one WebSocket client per product type and manages
them in a map:
```rust
pub struct MyDataClient {
ws_clients: AHashMap,
}
```
Each client follows the same two-layer client/handler pattern. Subscription routing
inspects the instrument's product type to select the correct client. On connect, the
data client iterates the map to connect all clients; on disconnect, it closes them all.
This differs from the split architecture (`streams/` vs `trading/`) which separates by
protocol or purpose. Multi-product management separates by product type while sharing
the same protocol.
## Modeling venue payloads
Use the following conventions when mirroring upstream schemas in Rust.
### REST models (`http::models` and `http::query`)
- Put request and response representations in `src/http/models.rs` and derive `serde::Deserialize` (add `serde::Serialize` when the adapter sends data back).
- Mirror upstream payload names with blanket casing attributes such as `#[serde(rename_all = "camelCase")]` or `#[serde(rename_all = "snake_case")]`; only add per-field renames when the upstream key would be an invalid Rust identifier or collide with a keyword (for example `#[serde(rename = "type")] pub order_type: String`).
- Keep helper structs for query parameters in `src/http/query.rs`, deriving `serde::Serialize` to remain type-safe and reusing constants from `common::consts` instead of duplicating literals.
### WebSocket messages (`websocket::messages`)
- Define streaming payload types in `src/websocket/messages.rs`, giving each venue topic a struct or enum that mirrors the upstream JSON.
- Apply the same naming guidance as REST models: rely on blanket casing renames and keep field names aligned with the venue unless syntax forces a change; consider serde helpers such as `#[serde(tag = "op")]` or `#[serde(flatten)]` and document the choice.
- Note any intentional deviations from the upstream schema in code comments and module docs so other contributors can follow the mapping quickly.
### Enum hardening for open venue value sets
A strict enum with no fallback hard-fails the whole message the first time the venue sends a value it does not model. Choose the behavior by what the field drives:
- Reference/descriptive (instrument or product type, market status, ticker type, public trade type): add an `Unknown` fallback so a new value degrades gracefully. Use `#[serde(other)] Unknown`, or a `deserialize_{field}_or_unknown` shim (see `coinbase/src/common/parse.rs`) when the enum also derives `strum::EnumString`. Warn and map `Unknown` to a skip or safe default; never fabricate a value.
- Order/fill/position state (order/position status, order/fill type, liquidity, time-in-force, trigger fields): keep strict, no catch-all. An unmodeled value must fail deserialization so the handler logs it loudly rather than let the engine run out of sync with the venue.
Model known values explicitly instead of via a catch-all: a documented `"unknown"` sentinel becomes one variant mapped to a safe default with a warning (see `kraken/src/common/enums.rs`); a changelog-named value like `FillOrKill` becomes a real variant.
---
## Task management
### Spawning async tasks (`spawn_task`)
Data and execution clients spawn background tasks for WebSocket stream processing,
periodic polling, and order submission. Wrap all spawned work with a `spawn_task()`
method that provides error logging and handle tracking:
```rust
fn spawn_task(&self, description: &'static str, fut: F)
where
F: Future