Actors
An Actor receives data, handles events, and manages state. The Strategy class extends Actor
with order management capabilities.
Key capabilities:
- Data subscription and requests (market data, custom data).
- Event handling and publishing.
- Timers and alerts.
- Cache and portfolio access.
- Logging.
Basic example
Actors support configuration through a pattern similar to strategies.
from nautilus_trader.config import ActorConfig
from nautilus_trader.model import InstrumentId
from nautilus_trader.model import Bar, BarType
from nautilus_trader.common.actor import Actor
class MyActorConfig(ActorConfig):
instrument_id: InstrumentId # example value: "ETHUSDT-PERP.BINANCE"
bar_type: BarType # example value: "ETHUSDT-PERP.BINANCE-15-MINUTE[LAST]-INTERNAL"
lookback_period: int = 10
class MyActor(Actor):
def __init__(self, config: MyActorConfig) -> None:
super().__init__(config)
# Custom state variables
self.count_of_processed_bars: int = 0
def on_start(self) -> None:
# Subscribe to bars matching the configured bar type
self.subscribe_bars(self.config.bar_type)
def on_bar(self, bar: Bar) -> None:
self.count_of_processed_bars += 1Actor configuration and IDs
Actors can receive an ActorConfig subclass. The base config may include an actor_id;
if supplied, the actor registers with that ID. If omitted, the system derives a runtime
actor ID.
Treat configuration as construction data for the actor. Read user-supplied settings through
self.config, and keep runtime state on the actor itself.
:::info Rust implementation
For Rust actors, generated or assigned runtime IDs live on the actor core rather than being
written back into DataActorConfig. This differs from Python bridge paths which may copy
inherited config fields into runtime state when a Python object is created from an importable
config.
Rust authors implement DataActor and use the facade methods on self.
DataActorNative is native-only access for runtime wiring and borrowed
core state. Import it only for same-binary performance paths or internal runtime wiring.
:::
Lifecycle
Actors follow a defined state machine through their lifecycle:
Override these methods to hook into lifecycle events:
| Method | When called |
|---|---|
on_start() | Actor is starting (subscribe to data here). |
on_stop() | Actor is stopping (cancel timers, clean up resources). |
on_resume() | Actor is resuming from a stopped state. |
on_reset() | Reset indicators and internal state (called between backtest runs). |
on_degrade() | Actor is entering a degraded state (partial functionality). |
on_fault() | Actor has encountered a fault. |
on_dispose() | Actor is being disposed (final cleanup). |
Timers and alerts
Actors have access to a clock for scheduling:
def on_start(self) -> None:
# Set a recurring timer with a callback (fires every 5 seconds)
self.clock.set_timer(
"my_timer",
timedelta(seconds=5),
callback=self._on_timer,
)
# Set a one-time alert with a callback
self.clock.set_time_alert(
"my_alert",
self.clock.utc_now() + timedelta(minutes=1),
callback=self._on_alert,
)
def on_stop(self) -> None:
# Cancel timers to prevent resource leaks across stop/resume cycles
self.clock.cancel_timer("my_timer")
def _on_timer(self, event: TimeEvent) -> None:
self.log.info("Timer fired!")
def _on_alert(self, event: TimeEvent) -> None:
self.log.info("Alert triggered!")Pass a callback to direct TimeEvent objects to your own method. If you
omit the callback, the event is delivered to on_event instead.
System access
Actors have access to core system components:
| Property | Description |
|---|---|
self.cache | Shared state for instruments, orders, positions, etc. |
self.portfolio | Portfolio state and calculations. |
self.clock | Current time and timer/alert scheduling. |
self.log | Structured logging. |
self.msgbus | Publish/subscribe to custom messages. |
For custom messaging between components, see the Message Bus guide.
Data handling and callbacks
The system uses different callback handlers depending on whether data is historical or real-time. Understanding the relationship between data requests/subscriptions and their handlers is key.
Historical vs real-time data
The system distinguishes between two data flows:
-
Historical data (from requests):
- Obtained through methods like
request_bars(),request_quote_ticks(), etc. - Processed through the
on_historical_data()handler. - Used for initial data loading and historical analysis.
- Obtained through methods like
-
Real-time data (from subscriptions):
- Obtained through methods like
subscribe_bars(),subscribe_quote_ticks(), etc. - Processed through specific handlers like
on_bar(),on_quote_tick(), etc. - Used for live data processing.
- Obtained through methods like
Callback handlers
Different data operations map to these handlers:
| Operation | Category | Handler | Purpose |
|---|---|---|---|
subscribe_data() | Real‑time | on_data() | Live data updates. |
subscribe_instrument() | Real‑time | on_instrument() | Live instrument definition updates. |
subscribe_instruments() | Real‑time | on_instrument() | Live instrument definition updates (for venue). |
subscribe_order_book_deltas() | Real‑time | on_order_book_deltas() | Live order book deltas. |
subscribe_order_book_depth() | Real‑time | on_order_book_depth() | Live order book depth snapshots. |
subscribe_order_book_at_interval() | Real‑time | on_order_book() | Live order book snapshots at intervals. |
subscribe_quote_ticks() | Real‑time | on_quote_tick() | Live quote updates. |
subscribe_trade_ticks() | Real‑time | on_trade_tick() | Live trade updates. |
subscribe_mark_prices() | Real‑time | on_mark_price() | Live mark price updates. |
subscribe_index_prices() | Real‑time | on_index_price() | Live index price updates. |
subscribe_bars() | Real‑time | on_bar() | Live bar updates. |
subscribe_funding_rates() | Real‑time | on_funding_rate() | Live funding rate updates. |
subscribe_instrument_status() | Real‑time | on_instrument_status() | Live instrument status updates. |
subscribe_instrument_close() | Real‑time | on_instrument_close() | Live instrument close updates. |
subscribe_option_greeks() | Real‑time | on_option_greeks() | Live option greeks updates. |
subscribe_option_chain() | Real‑time | on_option_chain() | Live option chain slice snapshots. |
request_data() | Historical | on_historical_data() | Historical data processing. |
request_order_book_deltas() | Historical | on_historical_data() | Historical order book deltas. |
request_order_book_depth() | Historical | on_historical_data() | Historical order book depth. |
request_order_book_snapshot() | Historical | on_historical_data() | Historical order book snapshot. |
request_instrument() | Historical | on_instrument() | Instrument definition. |
request_instruments() | Historical | on_instrument() | Instrument definitions. |
request_quote_ticks() | Historical | on_historical_data() | Historical quotes processing. |
request_trade_ticks() | Historical | on_historical_data() | Historical trades processing. |
request_bars() | Historical | on_historical_data() | Historical bars processing. |
request_aggregated_bars() | Historical | on_historical_data() | Historical aggregated bars (on‑the‑fly). |
request_funding_rates() | Historical | on_historical_data() | Historical funding rates processing. |
Example
This example shows both historical and real-time data handling:
from nautilus_trader.common.actor import Actor
from nautilus_trader.config import ActorConfig
from nautilus_trader.core.data import Data
from nautilus_trader.model import Bar, BarType
from nautilus_trader.model import ClientId, InstrumentId
class MyActorConfig(ActorConfig):
instrument_id: InstrumentId # example value: "AAPL.XNAS"
bar_type: BarType # example value: "AAPL.XNAS-1-MINUTE-LAST-EXTERNAL"
class MyActor(Actor):
def __init__(self, config: MyActorConfig) -> None:
super().__init__(config)
self.bar_type = config.bar_type
def on_start(self) -> None:
# Request historical data - will be processed by on_historical_data() handler
self.request_bars(
bar_type=self.bar_type,
# Many optional parameters
start=None, # pd.Timestamp | None
end=None, # pd.Timestamp | None
callback=None, # Callable[[UUID4], None] | None
update_catalog_mode=None, # UpdateCatalogMode | None
params=None, # dict[str, Any] | None
)
# Subscribe to real-time data - will be processed by on_bar() handler
self.subscribe_bars(
bar_type=self.bar_type,
# Many optional parameters
client_id=None, # ClientId, optional
params=None, # dict[str, Any], optional
)
def on_historical_data(self, data: Data) -> None:
# Handle historical data (from requests)
if isinstance(data, Bar):
self.log.info(f"Received historical bar: {data}")
def on_bar(self, bar: Bar) -> None:
# Handle real-time bar updates (from subscriptions)
self.log.info(f"Received real-time bar: {bar}")When validate_data_sequence=True, subscribe to live bars via the request_bars()
callback (rather than a separate subscribe_bars() call) so the stream starts only
after history has loaded; see
Working with bars: request vs. subscribe.
Separating historical and real-time handlers lets you apply different processing logic based on context. For example:
- Use historical data to initialize indicators or establish baseline metrics.
- Process real-time data differently for live trading decisions.
- Apply different validation or logging for historical vs real-time data.
When debugging data flow issues, check that you're looking at the correct handler for your data source.
If you're not seeing data in on_bar() but see log messages about receiving bars, check on_historical_data()
as the data might be coming from a request rather than a subscription.
Order event subscriptions
Actors that need order lifecycle events can subscribe directly to the message bus. Use this for monitoring fills, cancels, or all order events for a strategy without adding dedicated Actor callback methods.
Subscribe in on_start() and unsubscribe in on_stop() using the same handler. This
keeps direct message bus subscriptions aligned with the actor lifecycle.
Common order event topics:
| Topic pattern | Receives |
|---|---|
events.order_filled.{instrument_id} | Fill events for one instrument. |
events.order_canceled.{instrument_id} | Cancel events for one instrument. |
events.order.{strategy_id} | All order events routed to one strategy. |
events.order.* | All strategy‑routed order events. |
Instrument fill and cancel events
from nautilus_trader.common.actor import Actor
from nautilus_trader.config import ActorConfig
from nautilus_trader.model import InstrumentId
from nautilus_trader.model.events import OrderCanceled
from nautilus_trader.model.events import OrderFilled
class MyActorConfig(ActorConfig):
instrument_id: InstrumentId # example value: "ETHUSDT-PERP.BINANCE"
class FillMonitorActor(Actor):
def __init__(self, config: MyActorConfig) -> None:
super().__init__(config)
self.fill_count = 0
self.total_volume = 0.0
def on_start(self) -> None:
instrument_id = self.config.instrument_id
self.msgbus.subscribe(
topic=f"events.order_filled.{instrument_id}",
handler=self._on_order_filled,
)
self.msgbus.subscribe(
topic=f"events.order_canceled.{instrument_id}",
handler=self._on_order_canceled,
)
def on_stop(self) -> None:
instrument_id = self.config.instrument_id
self.msgbus.unsubscribe(
topic=f"events.order_filled.{instrument_id}",
handler=self._on_order_filled,
)
self.msgbus.unsubscribe(
topic=f"events.order_canceled.{instrument_id}",
handler=self._on_order_canceled,
)
def _on_order_filled(self, event: OrderFilled) -> None:
self.fill_count += 1
self.total_volume += float(event.last_qty)
self.log.info(
f"Fill received: {event.order_side} {event.last_qty} @ {event.last_px}, "
f"Total fills: {self.fill_count}, Volume: {self.total_volume}"
)
def _on_order_canceled(self, event: OrderCanceled) -> None:
self.log.info(f"Cancel received: {event.client_order_id}")Strategy order lifecycle events
from nautilus_trader.common.actor import Actor
from nautilus_trader.config import ActorConfig
from nautilus_trader.model import StrategyId
from nautilus_trader.model.events import OrderEvent
class MyActorConfig(ActorConfig):
strategy_id: StrategyId # example value: "EMA-CROSS-001"
class StrategyOrderMonitorActor(Actor):
def __init__(self, config: MyActorConfig) -> None:
super().__init__(config)
self.order_event_count = 0
def on_start(self) -> None:
self._order_topic = f"events.order.{self.config.strategy_id}"
self.msgbus.subscribe(
topic=self._order_topic,
handler=self._on_strategy_order_event,
)
def on_stop(self) -> None:
self.msgbus.unsubscribe(
topic=self._order_topic,
handler=self._on_strategy_order_event,
)
def _on_strategy_order_event(self, event: OrderEvent) -> None:
self.order_event_count += 1
self.log.info(
f"Order event received: {type(event).__name__}, "
f"Total events: {self.order_event_count}"
)Direct message bus subscriptions do not send data engine commands. They receive messages published on matching topics until you unsubscribe the handler.
Related guides
- Strategies - Strategies extend actors with order management capabilities.
- Data - Data types and subscriptions available to actors.
- Message Bus - The messaging system actors use for communication.