Nautilus Trader - Concepts
NautilusTrader is an open-source, production-grade trading engine - Rust core, Python control plane via Cython bindings - explicitly designed so the same strategy code runs in backtest, sandbox, and live environments without modification. The concepts pages describe how the platform decomposes a trading system into a small set of cooperating components (Kernel, MessageBus, Cache, DataEngine, ExecutionEngine, RiskEngine, Strategy/Actor, Portfolio, Account, clients/adapters) bound together by deterministic, single-threaded event dispatch on a publish/subscribe bus, with externalized state and crash-only design. This page is a synthesis of the official concept docs, intended as the canonical “how Nautilus thinks about trading systems” reference for the Cortana MK3 design effort.
Overview & design priorities
Nautilus describes its quality attributes in a deliberate order: reliability, performance, modularity, testability, maintainability, deployability. That ordering matters - every architectural decision is justified by walking down the list. The framework leans on three named patterns: domain-driven design, event-driven architecture, and ports and adapters. It also adopts crash-only design explicitly, drawing on the research insight that “systems recovering cleanly from crashes prove more robust than those with separate graceful shutdown paths.” Startup and crash recovery share the same code; state is externalized; operations are idempotent; unrecoverable errors fail fast.
The platform “prioritizes data integrity over availability for trading operations.” It rejects invalid deserialization, traps arithmetic overflow, and validates type conversions rather than silently coercing. A guiding assurance principle: “critical code paths should carry executable invariants that verify behaviour matches the business requirements” - encoded as unit and property tests on high-impact components (domain types, risk flows), plus Rust’s ownership/Result/panic-abort hardening for production.
The platform’s stated reach is “multi-asset, multi-venue trading systems,” with adapters for crypto exchanges, traditional venues, and betting platforms, and explicit AI/ML positioning: an “engine fast enough to train AI trading agents (RL/ES).”
NautilusKernel
The NautilusKernel is the central orchestration component. It “initializes
system components, configures messaging infrastructure, maintains
environment-specific behaviors, and coordinates shared resources throughout the
trader instance.” Crucially it is the single entry point shared between
BacktestEngine and the live TradingNode - same Kernel, different
environment context - which is how Nautilus delivers backtest-live parity at
the framework level rather than asking each user to maintain two parallel
codepaths.
Within a node, “the kernel consumes and dispatches messages on a single thread,” producing deterministic event ordering. That single-threaded core encompasses the MessageBus, strategy logic, risk checks, and cache operations - similar to the actor model. Background services (network I/O via WebSocket and REST, persistence on Tokio runtimes, adapter operations on thread pools) run on separate threads but communicate results back through the MessageBus, so the deterministic core never has to reason about lock ordering for trading state.
Components managed by the kernel follow finite state machines: stable states
(PRE_INITIALIZED, READY, RUNNING, STOPPED, DEGRADED, FAULTED,
DISPOSED) and transitional ones (STARTING, STOPPING, etc.).
Differs from “build it yourself”: you do not write your own event loop, your own wiring of strategy → risk → execution → venue, your own restart sequence, or your own fault states. The Kernel composes those for you, and because it is the same object in backtest and live, you cannot accidentally introduce behaviour that exists in only one environment.
MessageBus
The MessageBus is “the backbone of inter-component communication.” It supports three messaging patterns - Point-to-Point, Publish/Subscribe, and Request/Response - and exchanges three message categories - Data, Events, Commands. Components communicate by sending messages, never by holding references to each other; this is what makes the architecture loosely coupled and unit-testable.
A hard contract on every message: immutability. “Once a message is created,
its fields must not be mutated. This includes container fields such as params
maps.” Three ownership rules enforce this: caller request options live on the
message itself; response metadata lives on the response; component workflow
state - “bounded date ranges, grouping state, replay cursors, counters,
processing flags” - lives in component-owned context keyed by message or
request ID. The result is that replay, audit, and debugging are tractable
because no consumer sees a mutated message.
Three publishing styles are exposed:
- Low-level pub/sub on topics - maximum flexibility, but “you must track topic names manually (typos could result in missed messages).”
- Actor-based data publishing via
Datasubclasses or@customdataclass, which gives “proper event ordering via built-in timestamps (ts_event,ts_init) crucial for backtest accuracy.” - Actor-based signal publishing for primitive notifications (int/float/ str) - lightweight alerts.
For external persistence/replication the bus supports Redis: outgoing messages
are “first serialized, then transmitted via a Multiple-Producer Single-Consumer
(MPSC) channel to a separate thread (implemented in Rust),” keeping the main
thread non-blocked. JSON or MessagePack encoding, configurable stream key
organization, types_filter to keep high-frequency messages off external
streams, and autotrim_mins for storage management.
Differs from “build it yourself”: no ad-hoc dictionaries, no direct method calls between strategy and execution, no hand-rolled topic strings without typo protection at the entrypoints, and no question of “did this consumer mutate what the next consumer sees?”
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.” It holds three categories: market data (recent order books, quotes, trades, bars), trading records (orders, positions, accounts, instruments), and arbitrary user-defined objects shared across strategies.
Reads use reverse indexing (index=0 is most recent). Bars come back via
self.cache.bar(bar_type) or self.cache.bar(bar_type, index=1); order books
via order_book(instrument_id); price information via price(instrument_id, price_type) accepting BID, ASK, MID, or LAST. Order queries filter by state
(open / closed / emulated / inflight); position queries filter “by venue,
instrument, strategy, and side.”
Critical contract for ordering: “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.” This cache-then-publish invariant means a
strategy’s on_quote_tick handler can rely on self.cache.quote_tick(...)
returning the very tick that triggered it - there is no race between handler
entry and cache visibility for the data that triggered the handler. In live
contexts the engine “applies updates asynchronously, so you might see a brief
delay between an event and its appearance in the Cache” - a documented
caveat for events that did not flow through the cache-then-publish path.
By default the Cache is in-memory with capacity limits - “the last 10,000 bars
for each bar type and 10,000 trade ticks per instrument” - and oldest data is
evicted when capacity is exceeded. Optional DatabaseConfig adds Redis-backed
persistence so state survives restarts/upgrades/crashes; this is what lets the
crash-only recovery path actually rebuild deterministically.
Differs from “build it yourself”: no risk of strategies reading a stale local-variable copy of last quote because every component reads from the same Cache, with the cache-then-publish ordering ensuring handlers cannot fire before the cache update they depend on.
DataEngine
The DataEngine is the central market-data processing hub. Independent of
environment context (backtest, sandbox, live), normalized data follows the
same path: an adapter (DataClient) parses the venue feed and emits normalized
objects (OrderBookDelta, QuoteTick, TradeTick, Bar, OrderBookDepth10,
MarkPriceUpdate, FundingRateUpdate, etc.), the engine writes to Cache,
and then publishes on the MessageBus topic derived from the instrument ID.
Two distinct interaction patterns: request_bars() (and friends) returns
historical data routed to on_historical_data(); subscribe_bars() (and
friends) establishes a live feed routed to on_bar(). This dual surface is
what lets a strategy hydrate indicators with history at start and then consume
real-time updates without code change.
Every data object carries two timestamps: ts_event “represents when an event
occurred externally (at the exchange)” and ts_init “reflects when Nautilus
created the corresponding internal object.” Their difference “represents total
system latency, including network transmission time, processing overhead, and
any queueing delays” - the framework gives you latency telemetry by
construction.
For order books, deltas carry flags so “the DataEngine accumulates deltas and only publishes to subscribers when it encounters F_LAST” - preventing partial book updates from being acted on. Three book representations: L3 (market-by-order), L2 (market-by-price), L1 (best bid/offer).
Persistence is via ParquetDataCatalog - Rust backend for core types, PyArrow
backend for custom - with local FS, S3, GCS, Azure Blob support, files named
{start_timestamp}_{end_timestamp}.parquet and organized by data type and
identifier.
Differs from “build it yourself”: no per-feed bespoke parsing in your strategy, no manual buffering of book deltas, no hand-rolled ts_event/ts_init plumbing, and no question of which data type goes through Cache vs not.
ExecutionEngine
The ExecutionEngine “manages trade execution and order lifecycle across
multiple strategies and venues.” Routing depends on order shape: emulated
orders → OrderEmulator; orders with exec_algorithm_id → ExecAlgorithm; the
rest → RiskEngine → ExecutionClient → venue. Cancels and queries skip
unrelated stages.
A central design choice is that OMS type is configurable - NETTING
(“combines positions into a single position per instrument”) vs HEDGING
(“supports multiple positions per instrument ID in both long and short
directions”). Strategy and venue may have different OMS types; the engine
reconciles by “overriding or assigning position_id values for received
OrderFilled events,” which is how Nautilus supports the case where a venue
treats LONG and SHORT as one netted position but the trader wants to model
them as separate.
Reconciliation is the live-only responsibility of the ExecutionEngine and
is the single most important live-vs-backtest difference: “Only the
LiveExecutionEngine performs reconciliation, since backtesting controls both
sides.” On startup it generates order status, fill, and position status reports
from the venue, then aligns cached state against them. With cached state,
“report data generates missing events to align the state”; without cached
state, “all orders and positions at the venue are generated from scratch.”
Duplicate fills are caught by comparing trade_id, order_side, last_px,
last_qty against existing fills - “if all fields match an existing fill
exactly, the event is skipped gracefully with a warning log.” Overfills are
gated by allow_overfills: false rejects them, true logs and tracks excess in
overfill_qty.
Four reconciliation invariants the engine guarantees: position quantity matches within instrument precision; average entry price aligns within tolerance; PnL integrity is preserved through calculated pricing; synthetic identifiers are deterministic across restarts. These hold even if the reconciliation lookback window misses parts of fill history. The instruction: “Persist all execution events to the cache database” - once you do, recovery works with short windows.
Built-in execution algorithms include TWAP (“spreads execution evenly over a
specified time horizon”); custom algos subclass ExecAlgorithm and spawn
children via spawn_market/spawn_limit/spawn_market_to_limit carrying an
exec_spawn_id.
Differs from “build it yourself”: you do not write the order-state machine, the reconciliation loop, the duplicate-fill detector, the overfill policy, or the OMS-mismatch translator. They come built-in and are unit-tested in the framework.
RiskEngine
The RiskEngine sits between the Strategy/ExecutionEngine and the
ExecutionClient and “validates price precision, quantity bounds, position
impacts, and trading state restrictions before orders proceed downstream.” It
is the only place where pre-trade checks live, which keeps the strategy code
free of defensive size/price checks that would otherwise drift across
strategies. Trading state can be paused/resumed at the risk layer
(TradingState.HALTED/PAUSED/ACTIVE).
Differs from “build it yourself”: you do not embed if size > X checks in
each strategy; you configure RiskEngine limits centrally and trust the engine
to reject violations before they ever reach a venue.
Strategies and Actors
A Strategy extends Actor. An Actor “receives data, handles events, and
manages state” - the base for any reactive component. A Strategy adds
“order management capabilities.” Use Actor for monitors, scanners, custom data
producers, and analytics; use Strategy when you need to submit/modify/cancel
orders.
Lifecycle: on_start() (subscribe to data, register indicators, fetch
instruments), on_stop() (cancel orders, close positions), with
on_resume(), on_reset(), on_fault(), on_degrade(), on_dispose() for
state transitions. Data handlers: on_quote_tick, on_trade_tick, on_bar,
on_instrument, on_option_greeks, on_option_chain, plus on_data/
on_signal for custom types. Order/position handlers go specific-to-generic:
on_order_filled → on_order_event → on_event; on_position_opened →
on_position_event → on_event. Subscriptions to fills/cancels run “only
through the message bus” without DataEngine involvement.
Order submission is via OrderFactory - strategy IDs and trader IDs are
filled in for you. cancel_order/cancel_orders/cancel_all_orders. A
market_exit() helper cancels open orders and closes positions, with
on_market_exit() and post_market_exit() hooks and an is_exiting()
predicate to prevent re-entry during exit.
Configuration uses optional StrategyConfig subclasses. Quoted: “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.”
Multi-strategy deployment requires unique order_id_tag values per
configuration instance; the framework derives strategy IDs as
ClassName-order_id_tag and prevents duplicates. Strategy-shared services
(cache, portfolio, clock, log, msgbus) are provided by the kernel.
Differs from “build it yourself”: you write only the decision logic (when do I want to be long? when do I exit?), not the wiring to data feeds, the order ID generation, the strategy registry, the lifecycle state machine, or the multi-strategy coordination.
Orders
Orders derive from two fundamentals: Market (“consume liquidity by executing immediately at the best available price”) and Limit (“provide liquidity by resting in the order book at a specified price until matched”). On top of those, the platform provides Stop-Market, Stop-Limit, Market-To-Limit, Market-If-Touched, Limit-If-Touched, Trailing-Stop-Market, Trailing-Stop-Limit
- nine primary types in total.
Time-in-force: GTC, IOC, FOK, GTD, DAY. Execution flags include post-only (“will only ever participate in providing liquidity to the limit order book, and never initiating a trade which takes liquidity”) and reduce-only (“will only ever reduce an existing position on an instrument and never open a new position”). Display quantity for icebergs. Trigger types (LAST, BID_ASK, MARK) and trailing offset types (PRICE, BPS, TICKS, PRICE_TIER).
Contingent orders: OTO (one-triggers-other; supports both all-after-parent-fills and pro-rata partial-trigger models), OCO (one-cancels-other across multiple live orders), OUO (one-updates-other; proportional reduction), and bracket orders combining entry + TP + SL as a single submission.
Emulated orders are a Nautilus-specific power feature: “Emulation lets you use order types even when your trading venue does not natively support them.” The platform mimics the trigger logic locally and submits only basic Market or Limit orders to the venue. On trigger a LIMIT becomes MARKET, a STOP_LIMIT becomes LIMIT. Emulated orders persist across restarts via the cache database. Important caveat: query emulated orders through the Cache rather than holding local references because “the order object transforms when the emulated order is released.”
Differs from “build it yourself”: you do not implement bracket-order semantics, trailing-stop math, OCO/OUO linkage, or local triggering for venue-unsupported order types. They are first-class.
Positions
A position “represents an open exposure to a particular instrument in the
market.” Positions are auto-created from fills. Under NETTING they “open on
first fill for an instrument (one position per instrument)”; under HEDGING they
“open on first fill for a new position_id (multiple positions per
instrument).”
signed_qty carries direction (positive long, negative short, zero closed).
On close, “the closing order ID is recorded” and “final realized PnL is
computed.” Critically for NETTING: “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” - historical realized PnL is
preserved across cycles rather than being lost when the live position resets.
PnL has explicit realized/unrealized split. Realized is computed at partial/full close from entry/exit prices (with side-aware math for inverse instruments). Unrealized uses any reference price - “bid, ask, mid, last, or mark” - and returns zero for FLAT positions regardless of price input. Commissions accumulate by currency; only settlement-currency commissions contribute to realized PnL.
Position events: PositionOpened, PositionChanged, PositionClosed. All
fill events are stored chronologically along with order/trade IDs, enabling
“detailed position analysis” and “trade reconciliation.”
Differs from “build it yourself”: no hand-rolled qty tracking that drifts on race conditions, no separate “position cache” that diverges from the execution-event log, no need to write your own snapshot mechanism for the flip-from-flat case.
Accounting
Three account types: Cash (spot, no leverage; locks notional for pending orders), Margin (derivatives/leveraged; tracks balance, reserves collateral, applies per-instrument leverage; maintains both per-instrument and account-wide margin scopes), and Betting (locks only the stake).
AccountBalance carries three values in one currency, with the invariant
total == locked + free. Quote: “Reduce-only orders do not contribute to
balance_locked on cash accounts and do not add to initial margin on margin
accounts” - because they only decrease exposure, they do not need to reserve
new capacity.
For margin: per-instrument scope models isolated-margin venues; account-wide scope models cross-margin venues that report a single aggregate per collateral currency. Critical contract for adapter authors: “MarginAccount.apply() replaces both stores from the incoming event” - partial snapshots that omit live margin entries cause those entries to disappear until the next full snapshot. Adapters must always send full margin snapshots.
Built-in margin models: StandardMarginModel (fixed percent of notional,
ignoring leverage - traditional brokers) and LeveragedMarginModel (notional
÷ leverage - crypto). Custom models subclass the base.
Query API: margin(instrument_id) for per-instrument, margin_for_currency
for account-wide, total_margin_init/total_margin_maint for sums across
both scopes. Point queries return None when absent; totals always return a
Money (zero if nothing matches).
Differs from “build it yourself”: you do not maintain your own ledger. You do not have to figure out whether a venue is iso-margin or cross-margin; the framework keeps both stores and queries the right one for the venue’s reporting shape.
Portfolio
The Portfolio is “the central hub for managing and tracking all positions across active strategies for the trading node or backtest.” It aggregates positions across instruments and strategies, handles currency conversion for P&L, and exposes net-exposure and equity calculations.
Net exposure uses context-aware pricing: long-only positions use bid (the conservative liquidation price), short-only use ask, mixed portfolios use mid for neutral valuation. Pull-style queries: mark-values (signed mark-to-market totals), equity (account balances + position valuations), missing-price instruments (positions that cannot be priced and are flagged rather than silently zero-valued).
A price fallback chain - “cached mark prices, side-appropriate quotes, last trade prices, and recent bar closes” - means the Portfolio is robust to gaps in the most-recent-data of one type, but it explicitly does not invent prices when nothing is available; affected positions are flagged unpriceable.
The PortfolioAnalyzer adds performance metrics across configurable lookback windows, preferring portfolio-level daily balance changes when available and falling back to position-level returns otherwise.
Differs from “build it yourself”: no per-strategy P&L computation that disagrees with the firm-level total; no silent zero-valuation when a quote is missing.
Time, Clock, and the live-vs-backtest split
The framework exposes a Clock to every strategy and actor. set_time_alert()
fires a TimeEvent once at a specific moment; set_timer() fires recurring
events at intervals. In backtest the Clock is driven by the data stream so
every alert fires deterministically at the right simulated time; in live the
Clock is wall-clock backed but the interface is identical. Strategies never
call time.time() or datetime.now() directly - that is what makes backtest
runs reproducible.
Backtest engine and simulated venue
Two APIs for backtesting:
- High-level:
BacktestNodeorchestrates multipleBacktestEngineinstances from configuration objects. Use when datasets exceed RAM and need streaming, or when leveragingParquetDataCatalog. - Low-level:
BacktestEnginedirectly, with manual setup. Use when “your entire data stream can be processed within the available machine resources (e.g., RAM)” or “to re-run backtests on identical datasets while swapping out components.”
Determinism is the headline: the simulated exchange processes data with strict
ordering - exchange updates its book from incoming data, strategies receive
processed data through callbacks, venue commands settle within the same
timestamp - so “the same replay produces identical results across runs.” The
sim venue respects venue-specific config (L1_MBP/L2_MBP/L3_MBO, margin
models, account types), so configuration carries from sim to live.
Fill modelling acknowledges the unavoidable: “even with perfect historical
market data, we can’t fully simulate how orders may have interacted with other
market participants in real-time.” The platform provides probabilistic fill
models (limit-fill probability, slippage) and a ThreeTierFillModel that
distributes synthetic liquidity across price levels for impact simulation.
When using bar data, OHLC sequencing within a bar is adaptive (“approximately
75-85% accuracy” for high/low ordering).
For repeated runs: BacktestNode recreates fresh engine state per run
(production); BacktestEngine.reset() returns “all stateful fields to their
initial value, except for data and instruments which persist” - fast for
parameter sweeps.
Live engine and reconciliation
Live trading runs through a TradingNode with LiveDataEngine and
LiveExecutionEngine, both async-I/O-driven for venue connectivity. The same
strategies and actors used in backtest run unchanged.
Reconciliation is the line of defence between “what we think we have” and
“what the venue actually has.” States like SUBMITTED, PENDING_UPDATE,
PENDING_CANCEL are “in-flight.” A continuous monitoring loop detects stale
or lost messages. Startup runs a three-step procedure: order status reports,
fill reports, position status reports; the engine then reconciles cached state
against external reports through duplicate checking, order reconciliation, and
position reconciliation. Position-only mismatches generate “external order
events” so the position can converge.
When generate_missing_orders is enabled the engine generates reconciliation
orders following a price hierarchy: calculated reconciliation price → market
mid-price → current position average → MARKET as last resort.
Operational warning the docs surface explicitly: “Live trading involves real financial risk.” Individual adapter failures do not abort the full reconciliation, supporting graceful degradation.
Backtest-vs-Live parity
This is the framework’s main selling point and is achieved through:
- Same Kernel in both environments.
- Same MessageBus dispatch with the same topic naming.
- Same Cache semantics (cache-then-publish for data; identical query API).
- Same Strategy/Actor lifecycle -
on_start,on_quote_tick,on_eventtrigger the same way. - Same Clock interface - strategies cannot tell whether they are in simulated or wall-clock time.
- Same OMS, account, position, order semantics, just with a simulated venue vs real one.
- Reconciliation only on the live side because “backtesting controls both sides.” The simulator owns its own truth.
Differs from “build it yourself”: you cannot have a “backtest path” that exists only in research and a separate “production path” that is hand-coded - both run on the Kernel.
Adapters
Adapters connect data providers and venues. Each adapter typically supplies
HTTP and WebSocket clients, an instrument provider, a DataClient, and an
ExecutionClient. The DataClient is responsible for “market data
subscriptions and requests for a venue” and normalizes inbound bytes into
standardized Nautilus types. The ExecutionClient handles submit/modify/cancel,
processes fills/execution reports, reconciles order state, and processes
account/position updates from the venue.
Normalization is the load-bearing part: strategies see one shape regardless of which venue produced the event, which is what allows the same strategy to run against multiple venues simultaneously. The ExecutionEngine routes commands to the right ExecutionClient based on the order’s destination venue.
Differs from “build it yourself”: you do not put venue-specific JSON parsing inside your strategy. Adapter code is isolated, replaceable, and testable.
Logging
A high-performance subsystem built around an “MPSC channel to receive log
messages” so the main thread never blocks on formatting or file I/O. Sources:
Python components, external log crate, tracing crate. Levels: OFF,
TRACE, DEBUG, INFO, WARNING, ERROR - note “TRACE - Most verbose;
only emitted by Rust components (cannot be generated from Python).”
By default “log events with an ‘INFO’ LogLevel and higher are written to
stdout/stderr.” File logging supports size-based and UTC-date rotation, JSON
or plain-text formatting, ANSI color toggling. Per-component levels via
log_component_levels. The LogGuard (reference-counted, up to 255
instances) keeps the subsystem alive across multiple sequential engines in
one process. Environment override via NAUTILUS_LOG.
Differs from “build it yourself”: no main-thread blocking on file I/O; no inconsistent log shapes across components; rotation is policy-driven not hand-rolled.
Value types - Price, Quantity, Money, Currency
Price, Quantity, Money are immutable, precision-aware, fixed-point
internally. “Stored internally as integers scaled to a global fixed precision,”
not floats. Immutability gives “thread safety: Immutable values can be safely
shared across threads without synchronization,” predictability, and
hashability for use as dict keys.
Each carries a precision field; “precision controls display, not identity” -
two prices with the same numeric value but different precisions compare
equal. Same-type arithmetic preserves type (Price + Price → Price); mixed
dimensional arithmetic returns Decimal (Price * Price → Decimal because
“the result has different dimensional meaning”). Quantity cannot be
negative; Money requires matching currencies and refuses to add USD to EUR.
Differs from “build it yourself”: no float drift, no missed precision mismatches between the venue’s tick size and your code’s representation, no silent currency-mixing bugs.
Instruments
“An instrument represents the specification for any tradable asset or contract.” Implemented as Rust structs with Python/Cython bindings. Coverage spans Equity, Future (standard, spread, crypto), CryptoPerpetual, Option (standard, crypto), FX, CFD, Binary Option, Betting.
Identification: {symbol.venue} like ETHUSDT-PERP.BINANCE, with the rule
that “all native symbols should be unique for a venue” and “the {symbol.venue}
combination must be unique for a Nautilus system.”
Precision is enforced at three layers: instrument creation, RiskEngine pre-trade,
and matching engine in backtest. The framework “does not round values
automatically” - you must use instrument.make_price() and
instrument.make_qty() to produce values that respect the instrument’s
price_precision/size_precision and matching price_increment (tick size)
and size_increment (lot size).
Margin: margin_init (open a position) and margin_maint (keep it open).
Commissions follow a sign convention: “Positive fee rate = commission … Negative
fee rate = rebate.” Built-in MakerTakerFeeModel and FixedFeeModel; custom
via FeeModel subclass.
Persistence - ParquetDataCatalog and Cache database
Two persistence layers. The ParquetDataCatalog is for bulk historical
data - Rust-backend Parquet files for core market types, PyArrow backend for
custom data, deployable on local FS, S3, GCS, or Azure Blob. Filename pattern
{start_timestamp}_{end_timestamp}.parquet with directories partitioned by
data type and identifier.
The Cache database (Redis) is for live execution state - orders, positions, accounts, emulated-order working state, MessageBus stream. This is the externalized state that crash-only design depends on: shut down hard, restart, reconcile against the venue, resume.
These are different problems and Nautilus addresses them separately rather than forcing one tool to do both.
Deterministic Simulation Testing (DST)
DST is a separate testing technique from backtesting. Backtest tests the strategy against historical data; DST tests the runtime itself - async behaviour under controlled scheduling, race conditions, channel wakeups, recovery paths.
“A single seed fully determines an execution, including task scheduling, timer
firings, and random values.” Two runs with identical seed/binary/config produce
“bitwise-identical” observable behaviour. Implementation has two layers:
swapping tokio submodules (time, task, runtime, signal) for
deterministic madsim counterparts under the simulation feature, and
substituting other nondeterminism sources (wall-clock seams, deterministic
Instant, madsim::rand for randomness, IndexMap/IndexSet for hash-stable
iteration order).
Limits: “Python is not in DST scope” - DST only covers the Rust engine. Transport I/O runs on real sockets, not simulated ones; adapters require separate audits.
Differs from “build it yourself”: race-condition bugs become reproducible from a seed. You don’t have to chase Heisenbugs.
Why this matters for Cortana MK3
Bullets connect directly to the 2026-05-06 cluster of bugs.
- Stale
spy_pricecache. Nautilus’s cache-then-publish invariant - the DataEngine writes to Cache before publishing on the bus - directly prevents the class of bug we hit where a strategy handler ran on data that had not yet reached our cache. We have hand-rolled cache writes and publishers in MK2 with no enforced ordering; MK3 needs to either adopt Nautilus or replicate cache-then-publish as a hard invariant on every data path. position_statedivergence. Nautilus’s reconciliation invariants (quantity match within precision, average entry within tolerance, PnL integrity, deterministic synthetic identifiers) are exactly the broker-truth-first guarantees Task #46 is reaching for. TheLiveExecutionEnginereconciles cached state against venue reports on startup and during a continuous monitoring loop - we are doing this ad-hoc via the Flex reconciler and missing the in-flight-state monitor.- Dead-code meta-prob sizing (#88). Nautilus separates Strategy (decision logic) from RiskEngine (centralized pre-trade validation) and OrderFactory (construction). Putting position sizing inside the RiskEngine means it cannot become “dead code” silently - every order passes through it. Meta-prob weighting belongs at the Risk layer, not inline in a strategy module that may or may not be imported.
- Broker truth invariant violations. Nautilus’s overfill policy
(
allow_overfills), duplicate-fill detection by composite key (trade_id + side + last_px + last_qty), and “persist all execution events to the cache database” rule are pre-built answers to issues #26 / #39 (trades.contractsdivergence). MK3 should treat the broker’s executions feed as the source of truth, persist all execution events, and reconcile positions against venue reports on startup. - Backtest-live parity is architectural, not aspirational. The reason Nautilus achieves it is because the same Kernel runs in both environments and reconciliation lives only on the live side. Our MK2 paper/live drift exists because we have parallel pathways. MK3 must adopt one runtime that both environments share, or live divergence will keep recurring.
See also
- nautilus-getting-started.md
- nautilus-integrations.md
- nautilus-developer-guide.md
- writing/2026-05-06-power-outage-state-divergence.md
- writing/2026-05-06-morning-whipsaw-cluster.md