Nautilus Developer Guide - Rust

Reference-only page. Cortana stays Python. This captures the Nautilus “Developer Guide → Rust” page so that if a future Cortana hot path ever warrants a Rust drop-down (the only realistic candidate at MK3 scope is a UW WebSocket adapter under flow bursts), we have the canonical Nautilus-flavored Rust toolchain, style, and crate-layout notes filed. Cody does not write Rust; per nautilus-rust.md, the full Cortana surface - composite scoring, meta-labeling classifier, position management, TP/SL, IBKR routing - is expressible in Python on top of the Rust core. Codex would author any Rust we ever needed under review. The page exists so that day-zero of a Rust task isn’t spent Googling MSRV, panic policy, anyhow conventions, and the get_runtime().spawn() rule. Toolchain anchors: Rust MSRV 1.95.0, cargo-nextest for tests, cargo +nightly doc for docs, panic = abort in release, LGPL-3.0 file headers, anyhow for errors, log::* for logging, IndexMap/IndexSet on determinism-sensitive paths.

Why this page exists

nautilus-rust.md answers the strategic question: does Cortana ever need to write Rust? (Answer: no, not at MK3 scope.) nautilus-developer-guide.md covers the broader extension contract (adapter layout, testing strategy, Phase 1–7 sequence). This page narrowly captures the Rust style / build / runtime rules from /developer_guide/rust/ so that if Codex ever does drop into the crates/ workspace on Cortana’s behalf, the toolchain and conventions are pre-filed and the review loop is fast.

Filed at low priority. Don’t over-invest. The probability we ever touch Rust is small; the cost of not having this filed if we do is one Codex round-trip into the public docs.

Toolchain anchors

ItemValue
MSRVRust 1.95.0
Test runnercargo nextest (the nextest profile is canonical)
Doc buildercargo +nightly doc --all-features --no-deps --workspace
Lintercargo clippy --all-targets with feature parity to test
Formatmake format (rustfmt with workspace settings)
Panic policypanic = abort in release builds
File licenseLGPL-3.0 standardized header on every .rs file
Copyright2015–<current-year>, enforced by check_copyright_year.sh

End-users never install the Rust toolchain. pip install nautilus_trader ships prebuilt wheels. The toolchain only matters inside crates/ work - i.e., contributing back, building from source, or writing a new adapter.

Cargo manifest conventions

  • [dependencies] ordering: internal nautilus-* crates first (alphabetical) → blank line → required externals (alphabetical) → blank line → optional deps (alphabetical). Inline comments stay attached to their dependency.
  • Add "python" to every extension-module feature list, adjacent to "pyo3/extension-module", so the full Python stack is obvious.
  • [[bin]] filenames in bin/ use snake_case; the name = "..." field uses kebab-case so the compiled binaries get the intended CLI names.
  • Workspace inheritance for shared deps: serde = { workspace = true }. Pin versions directly only for crate-specific deps that aren’t in the workspace.
  • Adapter-only deps go in the workspace Cargo.toml “Adapter dependencies” section. Pre-commit blocks core crates from using them.
  • Keep related deps aligned (pre-commit enforces): capnp/capnpc exact-match; arrow/parquet major.minor; datafusion/object_store; dydx-proto/prost/tonic.

Feature flags

Additive only - enabling a flag must not break existing functionality. Document every flag in crate-level docs.

FlagEffect
default = []Keep defaults minimal
pythonEnables PyO3 bindings
extension-moduleBuilds a Python extension module (always include python)
ffiEnables C FFI bindings
stubsExposes testing stubs
high-precisionSwitches the value-type backing to 128-bit integers
defiDeFi data types (implies high-precision)
streamingCatalog-based data streaming via BacktestNode

Build configurations (avoiding spurious rebuilds)

Cargo’s build cache is keyed by the exact combination of features, profiles, and flags. Any mismatch triggers a full rebuild. The dev guide is explicit: align test, lint, and dev runs to share artifacts.

TargetFeaturesProfile
cargo-testffi,python,high-precision,definextest
cargo-clippy (pre-commit)ffi,python,high-precision,definextest
Docs (make docs-rust)--all-features (nightly)doc
build (Python extension)extension-module + subsetrelease
build-debug (Python extension)extension-module + subsetdev

Test/lint share the cache. Docs and Python-extension builds intentionally don’t - and that’s fine, those aren’t on the inner loop.

Triggers that cause unwanted full rebuilds: different feature combos (--features "a,b" vs --features "a,c"), --no-default-features toggling, profile switching (dev vs nextest vs release).

Code style

File header

Every .rs file starts with the LGPL-3.0 standardized copyright header (2015–current-year). The check_copyright_year.sh pre-commit hook verifies the year.

Spacing

  • One blank line between functions (including tests).
  • One blank line above every doc comment (/// or //!) so the comment is visually detached from the previous code block.

String formatting

Inline format args, not positional. Self-documenting and grep-friendly:

anyhow::bail!("Failed to subtract {n} months from {datetime}");  // good
anyhow::bail!("Failed to subtract {} months from {}", n, datetime); // bad

Type qualification

  • anyhow::* macros and anyhow::Result<T> always fully qualified.
  • Nautilus domain types (Symbol, Price, InstrumentId) used bare after import.
  • tokio::* types fully qualified (because std/futures have collisions).

Logging

Fully qualify backend: log::debug!, log::info!, log::warn!, etc. Messages start capitalized, prefer complete sentences, no terminal period: "Processing batch" not "Processing batch.". Enforced by check_logging_macro_usage.sh.

Constants

SCREAMING_SNAKE_CASE with descriptive names: NANOSECONDS_IN_SECOND, BAR_SPEC_1_MINUTE_LAST.

Error handling

Three patterns, used consistently:

  1. anyhow::Result<T> for fallible functions - primary pattern.
  2. thiserror for domain-specific error enums:
    #[derive(Error, Debug)]
    pub enum NetworkError {
        #[error("Connection failed: {0}")]
        ConnectionFailed(String),
        #[error("Timeout occurred")]
        Timeout,
    }
  3. ? operator for propagation. anyhow::bail! for early returns; anyhow::anyhow! in closure contexts (e.g. ok_or_else()) where early return isn’t possible.

Context strings are lowercase (so they chain naturally), except proper nouns / acronyms:

parse_timestamp(value).context("failed to parse timestamp")?;        // good
connect().context("BitMEX websocket did not become active")?;        // proper noun

Enforced by check_error_conventions.sh and check_anyhow_usage.sh.

Async / runtime patterns

General

  • No special suffix on async function names (no _async).
  • Return anyhow::Result from async functions to match sync convention.
  • Document cancellation safety in the doc comment.
  • Use tokio_stream (or futures::Stream) for async iterators - back-pressure is explicit.
  • Wrap network or long awaits with tokio::time::timeout and propagate the timeout error.

Adapter runtime - the get_runtime() rule (load-bearing)

Adapter crates under crates/adapters/ must never call tokio::spawn directly. When the call originates from a Python thread, plain tokio::spawn panics because Python threads have no Tokio thread-local context. Use the global runtime instead:

use nautilus_common::live::get_runtime;
 
// Correct - works from any thread (Python or native)
get_runtime().spawn(async move { /* ... */ });
 
// Wrong - panics from Python threads
tokio::spawn(async move { /* ... */ });

Sync→async bridges in adapters use get_runtime().block_on(...).

Rust-native binaries owning main() may call set_runtime() before LiveNode::build() or any adapter use. Custom runtimes must be built with tokio::runtime::Builder::new_multi_thread().enable_all() - current-thread runtimes and runtimes without I/O / timer drivers do not satisfy adapter assumptions.

Tests using #[tokio::test] are exempt; they create their own runtime and tokio::spawn works there. The check_tokio_usage.sh pre-commit hook enforces these rules and skips test files.

PyO3 bindings & stub generation

Every Python-exposed type/function needs a matching pyo3-stub-gen annotation so generated .pyi stubs stay in sync.

PyO3 constructStub annotation
#[pyclass] (struct)#[pyo3_stub_gen::derive::gen_stub_pyclass]
#[pyclass] (enum)#[pyo3_stub_gen::derive::gen_stub_pyclass_enum]
#[pymethods]#[pyo3_stub_gen::derive::gen_stub_pymethods]
#[pyfunction]#[pyo3_stub_gen::derive::gen_stub_pyfunction]

Placement: stub annotation directly below the corresponding PyO3 attr, behind #[cfg_attr(feature = "python", ...)] guards. The module = ... parameter must match the Python import path (nautilus_trader.<package>).

Cargo.toml:

[features]
python = ["pyo3", "pyo3-stub-gen"]
 
[dependencies]
pyo3-stub-gen = { workspace = true, optional = true }

Regenerate stubs with make py-stubs-v2 after annotation changes.

Constructor convention: new() vs new_checked()

Pair them - new_checked() returns CorrectnessResult<Self>, new() panics on invalid input via .expect_display(FAILED). The panicking constructor is the API ergonomic; the checked variant is what PyO3 wraps so Python sees a real error.

use nautilus_core::correctness::{CorrectnessResult, CorrectnessResultExt, FAILED};

From<T: AsRef<str>> may panic on invalid input - that’s intentional for ergonomics. Use FromStr / .parse() when error handling matters.

Hash collections - determinism gate

Three concerns drive the choice (in order):

  1. Iteration-order determinism. AHash randomizes per process → AHashMap/AHashSet iteration order varies across runs. If iteration order feeds observable state (events on the message bus, ordered Vecs in public methods, seeded RNG consumption, downstream effect ordering), use IndexMap/IndexSet from indexmap.
  2. Performance. AHashMap/AHashSet is roughly 3× faster than IndexMap on pure lookup; use it on hot lookup paths where iteration order doesn’t escape.
  3. Thread safety. Standard HashMap/HashSet for non-perf, non-iteration-sensitive code (factories, configs, test fixtures) and anywhere hash flooding is a concern (untrusted network input).

Pre-commit hook check-dst-conventions enforces IndexMap/IndexSet in crates/live/src/manager.rs and crates/execution/src/matching_engine/engine.rs (audited as load-bearing for fill ordering and reconciliation).

IndexMap removal nuance: shift_remove (O(n), preserves order) vs swap_remove (O(1), breaks order). Pick based on whether downstream iteration is observable.

Panic philosophy

Per the dev guide and nautilus-architecture.md:

  • Panic on programmer errors (logic bugs, API misuse, fundamental invariant violations: negative timestamps, NaN prices, silently-wrong arithmetic).
  • Return Result/Option on environmental errors (network, file I/O, order constraints, risk limits, user input).
  • Release builds run panic = abort - clean process termination handled by the supervisor (launchd / systemd). This is the crash-only design.

FFI implications:

  • Rust panics must never unwind across extern "C". Wrap every exported symbol in crate::ffi::abort_on_panic(|| { ... }).
  • New PyO3 capsules use PyCapsule::new_with_destructor, never the no-destructor variant.
  • Don’t pytest.raises(BaseException) against PyO3 panic paths in python/tests/. Debug builds may pass; release wheels abort the interpreter.

Module organization

  • One responsibility per module.
  • mod.rs as the module root for submodules.
  • Flat over deep nesting (paths stay manageable).
  • Re-export commonly used items from the crate root for convenience.

Cortana MK3 implications

Do we ever need to write Rust? Per nautilus-rust.md: not at MK3 scope. Cortana is a Python actor + Python strategy on top of the Rust core. The IBKR adapter ships in v1 legacy. Greeks math, message bus, cache, risk, and execution engines are already Rust; Cortana never reimplements any of it.

Single most likely Rust drop-down: the UW WebSocket adapter, if flow-burst latency under load makes Python ingest a bottleneck. The shape would be a crates/adapters/unusual_whales/ crate following the Phase 1–7 sequence in nautilus-developer-guide.md (HTTP errors → client → models → parsing → WS errors → WS client → WS messages → WS parsing → PyO3 bindings → InstrumentProvider → market data wiring → Python factory). Codex authors; Cody reviews. Deferred until a working Python UW ingest is in place and we’ve measured the latency under live load.

If/when that day comes, the rules from this page that bite first:

  1. MSRV 1.95.0 - confirm the toolchain.
  2. Adapter spawn rule - get_runtime().spawn(), never tokio::spawn.
  3. PyO3 stub annotations on every Python-exposed symbol.
  4. anyhow::Result<T> + anyhow::bail! for fallible paths; lowercase context strings.
  5. IndexMap/IndexSet for any collection whose iteration drives observable state (UW event ordering, score updates, fill sequencing).
  6. panic = abort - panics kill the process; supervisor restarts.
  7. LGPL-3.0 header + current-year copyright on every new .rs.

Everything else stays Python forever - strategy, scoring, meta-labeling, position management, ML inference, dashboard, alerting, brain integration.

See Also


Timeline

  • 2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 7 (developer guide).