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 theget_runtime().spawn()rule. Toolchain anchors: Rust MSRV 1.95.0,cargo-nextestfor tests,cargo +nightly docfor docs,panic = abortin release, LGPL-3.0 file headers,anyhowfor errors,log::*for logging,IndexMap/IndexSeton 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
| Item | Value |
|---|---|
| MSRV | Rust 1.95.0 |
| Test runner | cargo nextest (the nextest profile is canonical) |
| Doc builder | cargo +nightly doc --all-features --no-deps --workspace |
| Linter | cargo clippy --all-targets with feature parity to test |
| Format | make format (rustfmt with workspace settings) |
| Panic policy | panic = abort in release builds |
| File license | LGPL-3.0 standardized header on every .rs file |
| Copyright | 2015–<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: internalnautilus-*crates first (alphabetical) → blank line → required externals (alphabetical) → blank line → optional deps (alphabetical). Inline comments stay attached to their dependency.- Add
"python"to everyextension-modulefeature list, adjacent to"pyo3/extension-module", so the full Python stack is obvious. [[bin]]filenames inbin/usesnake_case; thename = "..."field useskebab-caseso 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/capnpcexact-match;arrow/parquetmajor.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.
| Flag | Effect |
|---|---|
default = [] | Keep defaults minimal |
python | Enables PyO3 bindings |
extension-module | Builds a Python extension module (always include python) |
ffi | Enables C FFI bindings |
stubs | Exposes testing stubs |
high-precision | Switches the value-type backing to 128-bit integers |
defi | DeFi data types (implies high-precision) |
streaming | Catalog-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.
| Target | Features | Profile |
|---|---|---|
cargo-test | ffi,python,high-precision,defi | nextest |
cargo-clippy (pre-commit) | ffi,python,high-precision,defi | nextest |
Docs (make docs-rust) | --all-features (nightly) | doc |
build (Python extension) | extension-module + subset | release |
build-debug (Python extension) | extension-module + subset | dev |
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); // badType qualification
anyhow::*macros andanyhow::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:
anyhow::Result<T>for fallible functions - primary pattern.thiserrorfor domain-specific error enums:#[derive(Error, Debug)] pub enum NetworkError { #[error("Connection failed: {0}")] ConnectionFailed(String), #[error("Timeout occurred")] Timeout, }?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 nounEnforced 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::Resultfrom async functions to match sync convention. - Document cancellation safety in the doc comment.
- Use
tokio_stream(orfutures::Stream) for async iterators - back-pressure is explicit. - Wrap network or long awaits with
tokio::time::timeoutand 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 construct | Stub 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):
- Iteration-order determinism.
AHashrandomizes per process →AHashMap/AHashSetiteration order varies across runs. If iteration order feeds observable state (events on the message bus, orderedVecs in public methods, seeded RNG consumption, downstream effect ordering), useIndexMap/IndexSetfromindexmap. - Performance.
AHashMap/AHashSetis roughly 3× faster thanIndexMapon pure lookup; use it on hot lookup paths where iteration order doesn’t escape. - Thread safety. Standard
HashMap/HashSetfor 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/Optionon 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 incrate::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 inpython/tests/. Debug builds may pass; release wheels abort the interpreter.
Module organization
- One responsibility per module.
mod.rsas 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:
- MSRV 1.95.0 - confirm the toolchain.
- Adapter spawn rule -
get_runtime().spawn(), nevertokio::spawn. - PyO3 stub annotations on every Python-exposed symbol.
anyhow::Result<T>+anyhow::bail!for fallible paths; lowercase context strings.IndexMap/IndexSetfor any collection whose iteration drives observable state (UW event ordering, score updates, fill sequencing).panic = abort- panics kill the process; supervisor restarts.- 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
- Nautilus Rust - the strategic “do we need Rust” page
- Nautilus Architecture - runtime topology
- Nautilus Developer Guide - adapter contract & testing
- 2026-05-09 Nautilus Spike Plan:
~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-nautilus-spike.md
Timeline
- 2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 7 (developer guide).