docs/how_to/write_rust_actor.md
An actor receives market data, custom data/signals, and system events but does not manage orders.
This guide walks through building a SpreadMonitor that subscribes to quotes
and logs the bid-ask spread.
For background on actors, traits, and handler dispatch, see the Actors and Rust concept guides.
An actor owns a DataActorCore and any state it needs. The core stores runtime
state for the actor. User code normally reaches that state through the
DataActor facade methods such as:
clock()cache()config()actor_id()trader_id()use nautilus_common::{nautilus_actor, actor::{DataActor, DataActorConfig, DataActorCore}};
use nautilus_model::{data::QuoteTick, identifiers::{ActorId, InstrumentId}};
pub struct SpreadMonitor {
core: DataActorCore,
instrument_id: InstrumentId,
}
Create a DataActorConfig with an actor ID, then pass it to DataActorCore::new.
The config fields use Option with defaults, so ..Default::default() covers
everything except the actor ID.
impl SpreadMonitor {
pub fn new(instrument_id: InstrumentId) -> Self {
let config = DataActorConfig {
actor_id: Some(ActorId::from("SPREAD_MON-001")),
..Default::default()
};
Self {
core: DataActorCore::new(config),
instrument_id,
}
}
}
The nautilus_actor! macro connects the actor's DataActorCore field to the
runtime contract. By default it delegates to a field named core; pass a second
argument for a different field name. Normal callbacks do not call the generated
native accessors; use the DataActor facade methods on self.
Runtime registration uses blanket Actor and Component implementations.
The macro supplies the native runtime wiring; implement Debug manually or
derive it.
nautilus_actor!(SpreadMonitor);
impl std::fmt::Debug for SpreadMonitor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SpreadMonitor").finish()
}
}
Override handler methods to receive data. All handlers have default no-op
implementations, so you only override what you need. Each handler returns
anyhow::Result<()>.
impl DataActor for SpreadMonitor {
fn on_start(&mut self) -> anyhow::Result<()> {
self.subscribe_quotes(self.instrument_id, None, None);
Ok(())
}
fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
let spread = quote.ask_price.as_f64() - quote.bid_price.as_f64();
log::info!("Spread: {spread:.5}");
Ok(())
}
}
subscribe_quotes is available directly on self through the DataActor
trait. See the handler table for all
available handlers.
Use the public DataActor facade by default. Add DataActorNative only for an
explicit native-only access path that the facade methods cannot serve.
Read-only properties are available on the facade:
config()actor_id()trader_id()is_registered()The Rust native traits section covers the native-traits applicability matrix and this method table:
Those types do not cross Python or plug-in boundaries, so portable actors should use facade methods such as:
clock()cache()With a BacktestEngine:
let actor = SpreadMonitor::new(instrument_id);
engine.add_actor(actor)?;
With a LiveNode:
let actor = SpreadMonitor::new(instrument_id);
node.add_actor(actor)?;
When the system dispatches messages to your actor, it obtains a short-lived
ActorRef guard from the registry. You do not manage these guards directly.
If you write code that accesses other actors in a callback, follow these
rules:
ActorRef..await point.The subscription methods on DataActorCore handle this correctly by
capturing the actor ID and performing the lookup inside the callback closure.
See Runtime invariants for
the full threading and registry model.
See
BookImbalanceActor
for a more complete actor that tracks per-instrument state and prints a
summary on stop.