nautilus-plugin
Plug-in system for NautilusTrader.
[!WARNING] Early alpha. The ABI and public API are not stable and may change between versions while the host-side adapters,
HostVTablesurface, andLiveNodeConfigwiring land. Plug-in builds must be pinned to the matching Nautilus version.
The nautilus-plugin crate defines the C-ABI boundary between a Nautilus host (the live node)
and independently compiled Rust plug-in cdylibs. The host dlopens a plug-in, calls a single
nautilus_plugin_init entry symbol, and registers every plug point the returned manifest
enumerates after validating that the manifest is structurally well-formed. The boundary is
C ABI because Rust's #[repr(Rust)] layout is unstable across compilations, so cross-cdylib
Box<dyn Trait> and async fn are unsound. C ABI is the layer of contract both halves can
compile to without sharing a build.
Authors write normal Rust. The nautilus_plugin! macro emits the extern "C" symbol, the
#[repr(C)] manifest, and the per-type vtables; authors never type extern "C",
#[repr(C)], or unsafe themselves.
Vtable slots are nullable in the ABI structs so the host can report malformed manifests with missing callbacks. Macro-generated vtables fill every required slot, and the loader rejects null slots before registration or instantiation.
The plug-in system supports the following sync trait surfaces. Each lives in its
own module under src/ and follows the same pattern: a #[repr(C)] vtable, a
matching author-facing trait, extern "C" thunks wired through panic::guard,
and a Slice<'static, Registration> field on PluginManifest. Adding a plug
point means adding one module and one Slice field.
| Plug point | Status | Module |
|---|---|---|
| Custom data type | Shipped | surfaces::custom_data |
| Actor / DataActor | Shipped | surfaces::actor |
| Strategy | Shipped | surfaces::strategy |
| Execution algorithm | Not yet | surfaces::exec_algorithm |
| Indicator | Not yet | surfaces::indicator |
| Fill model | Not yet | surfaces::fill_model |
| Pricing / greeks | Not yet | surfaces::pricing |
Out of scope: async client adapters (data and execution), catalog and cache backends, pre-trade risk gating, event store backends, mutable host order book state, native or Python custom-data callbacks that are not backed by a plug-in vtable, and hot reload of any plug-in while the live node is running. Plug-ins load at process startup and live for the process lifetime.
OrderBook crosses the boundary via OrderBookHandle, a #[repr(C)]
wrapper owned by the host that boxes a cloned book snapshot for the duration
of the callback. The plug-in's thunk dereferences the handle once and hands a
borrowed &OrderBook to the trait method; the plug-in never receives mutable
host book state.
OrderBookDeltas cross the boundary via OrderBookDeltasHandle, a
#[repr(C)] wrapper owned by the host that boxes the deltas for the
duration of the callback. The plug-in's thunk dereferences the handle
once and hands a borrowed &OrderBookDeltas to the trait method;
matches the ownership contract OrderBookDeltas_API uses on the Cython
side.
InstrumentAny crosses the boundary via InstrumentAnyHandle, a
#[repr(C)] wrapper that owns a boxed InstrumentAny for the duration
of the callback. InstrumentAny is a Rust enum whose variant payloads
own heap-allocated fields, so the same boundary-owned shape applies: the
plug-in's thunk dereferences the handle once and hands a borrowed
&InstrumentAny to the trait method.
OptionChainSlice crosses the boundary via OptionChainSliceHandle
for the same reason: the snapshot owns
BTreeMap<Price, OptionStrikeData> call and put maps and cannot be
#[repr(C)]. The host boxes the slice in the handle for the duration
of the callback.
Custom data registered through PluginCustomData can flow into actor
and strategy on_data callbacks. The host wraps each decoded value in a
plug-in-owned handle plus vtable, then passes a borrowed
PluginCustomDataRef to the cdylib. Plug-in code can inspect the type
name and downcast to its concrete type locally. The host rejects custom
data that did not come from a plug-in registration, because those values
do not carry the vtable and handle needed for that downcast.
Historical plug-in custom-data responses use the same boundary only when the
value came from a PluginCustomData registration. The host inspects &dyn Any
only inside the adapter, extracts registered plug-in CustomData, and calls
the existing on_data slot with PluginCustomDataRef. No &dyn Any value
crosses the cdylib boundary.
Identifier interning
Nautilus identifier types such as ClientOrderId, InstrumentId,
ClientId, AccountId, PositionId, StrategyId, and TraderId
wrap Ustr. A Rust cdylib has its own ustr global string cache, so
equal text can have different Ustr pointers on the host and plug-in
sides. The plug-in boundary treats Ustr values as receiver-local:
- Host command dispatch re-interns every identifier in boundary-owned
command handles before calling
Strategy::*. - Plug-in event thunks re-intern identifiers in inbound event payloads
before calling
PluginActororPluginStrategytrait methods. - Plug-in authors can compare and store identifiers received through
trait callbacks normally. Code that bypasses the macro-generated
thunks must re-intern copied identifiers with
Ustr::from(value.as_str()).
The policy also covers nested identifiers such as Symbol, Venue,
OrderListId, ExecAlgorithmId, VenueOrderId, OptionSeriesId,
raw Ustr tags and names, and currency codes carried inside command
or event payloads.
NautilusTrader
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, providing research-to-live semantic parity.
Feature flags
This crate provides feature flags to control source code inclusion during compilation:
host: Enables host-side plug-in loading vialibloading. The live node enables this feature; plug-in cdylibs leave it off so they do not linklibloadingthemselves.
Platform
Plug-in cdylibs use the platform-native shared-library format:
- Linux:
lib<name>.so - macOS:
lib<name>.dylib - Windows:
<name>.dll
libloading handles the platform differences on the host side; the example test under
tests/load_example_cdylib.rs builds and loads a cdylib on all three.
Rust's ABI is unstable across compilations on every platform, so the plug-in build must be
pinned to the host's Rust toolchain version and Nautilus version. Each manifest carries a
versioned PluginBuildId with the nautilus-plugin crate version, Rust compiler version when
the build script can read it, target triple, build profile, and fixed-point precision mode. The
loader includes that build identifier in ABI mismatch diagnostics so operators can spot stale or
cross-built cdylibs.
The loader validates the build-id schema version and fixed-point precision mode because precision changes model layout at the plug-in boundary. The specific crate version, compiler version, target triple, and build profile values remain diagnostic.
NAUTILUS_PLUGIN_ABI_VERSION and PLUGIN_BUILD_ID_VERSION remain 1 while this early-alpha
surface is unreleased and unstable. During this phase, those values do not promise compatibility
between Nautilus versions. Rebuild plug-in cdylibs to match the exact host version. The host
refuses to load a plug-in whose PluginManifest::abi_version does not match, and manifest
validation rejects fixed-point precision mode mismatches.
Documentation
See the docs for more detailed usage.
License
The source code for NautilusTrader is available on GitHub under the GNU Lesser General Public License v3.0.
NautilusTrader™ is developed and maintained by Nautech Systems, a technology company specializing in the development of high-performance trading systems. For more information, visit https://nautilustrader.io.
Use of this software is subject to the Disclaimer.
© 2015-2026 Nautech Systems Pty Ltd. All rights reserved.