How to Use Actors and Signals for Messaging Between Components in NautilusTrader
NautilusTrader enables decoupled communication between trading components through an actor-based messaging system where actors publish lightweight signals via publish_signal() and receive them asynchronously through on_signal() callbacks routed by the central MessageBus.
NautilusTrader uses actors and signals for messaging between components to eliminate tight coupling between strategies, execution algorithms, and custom modules. This architecture allows any component inheriting from the Actor base class to emit typed notifications that other registered actors consume through the MessageBus. Understanding this publish-subscribe pattern is essential for building modular, maintainable trading systems that function identically in backtesting and live trading environments.
Core Architecture of Actor Messaging
The messaging system relies on five key components working together to route signals between actors:
| Component | Role | Key Implementation |
|---|---|---|
| Actor | Base class for messaging-enabled components; handles registration, topic caching, and signal publishing/subscribing | nautilus_trader/common/actor.pyx (lines 25-140) |
| Signal Generation | Dynamically creates concrete Data subclasses for signal names and value types, handling Arrow and message-bus serialization |
nautilus_trader/common/signal.py (generate_signal_class) |
| MessageBus | Central hub storing topic-handler mappings and forwarding published messages to subscribers | nautilus_trader/common/component.pyx (lines 140-210) |
| ActorExecutor | Queues and runs async actor callbacks on the event loop with cancellation support via TaskId |
nautilus_trader/common/executor.py (lines 60-115) |
| Trader | Orchestrates actor registration, creates the MessageBus, and starts the executor |
nautilus_trader/trading/trader.py (lines 33-45) |
The Actor Base Class
The Actor class in nautilus_trader/common/actor.pyx provides the foundation for all messaging components. It maintains internal topic caches and exposes methods for publishing and subscribing to signals. Strategies inherit from this class automatically, but custom components like risk managers or execution analyzers can also subclass Actor to participate in the messaging ecosystem.
Dynamic Signal Generation
Signals are not predefined classes. Instead, nautilus_trader/common/signal.py provides generate_signal_class(), which creates a concrete Data subclass on demand for a given signal name and value type (int, float, or str). This dynamic approach ensures type safety while keeping the system lightweight, as each signal carries only a single primitive value plus timestamps.
Publishing Signals Between Components
Actors emit signals using the publish_signal() method, implemented in nautilus_trader/common/actor.pyx (lines 13-48). This method validates inputs, generates the appropriate signal class, timestamps the data, and forwards it to the MessageBus.
self.publish_signal(name="MySignal", value=42, ts_event=timestamp_ns)
The implementation flow (lines 35-47 of actor.pyx) follows these steps:
- Validation → Checks name and value type (lines 35-40)
- Class Generation → Calls
generate_signal_classto createSignalMySignal(lines 35-40) - Timestamping → Uses
self.clock.timestamp_ns()if not provided (lines 41-46) - Publication → Forwards to
self.publish_data()which routes to theMessageBus(line 47)
Subscribing to Actor Signals
To receive signals, actors must explicitly subscribe to specific signal names or to all signals globally. The subscribe_signal() method in nautilus_trader/common/actor.pyx (lines 65-71) registers the actor's handler with the MessageBus.
# Subscribe to a specific signal
self.subscribe_signal(name="MySignal")
# Subscribe to all signals (wildcard)
self.subscribe_signal()
When subscribing to a specific name, the MessageBus maps the topic signal:MySignal to the actor's handle_signal method. A wildcard subscription maps to signal:*, capturing all signal publications.
Handling Incoming Signals
When a matching signal arrives, the MessageBus invokes handle_signal() on the subscribing actor. This method, located at line 4580 of nautilus_trader/common/actor.pyx, performs two critical checks before processing:
- State Validation → Verifies the actor is in the
RUNNINGstate - Callback Dispatch → Invokes the actor's
on_signal()method, which developers override to implement custom logic
The ActorExecutor in nautilus_trader/common/executor.py schedules these callbacks asynchronously on the trader's event loop, ensuring non-blocking execution while maintaining sequential ordering per actor.
Practical Example: Cross-Component Signal Strategy
The following complete example demonstrates a strategy that publishes threshold-crossing signals and reacts to them. This pattern applies identically to custom actors, risk managers, or execution algorithms.
from nautilus_trader.trading.strategy import Strategy
from nautilus_trader.common.enums import LogColor
from nautilus_trader.core.datetime import unix_nanos_to_dt
class ThresholdStrategy(Strategy):
"""
Publish a signal when price crosses a configurable threshold.
Demonstrates actor-based messaging between components.
"""
def on_start(self):
# Request market data
self.subscribe_bars(self.config.bar_type)
# Subscribe to our own signal to receive it when published
self.subscribe_signal("PriceAboveThreshold")
self.log.info("Subscribed to bars and signal", color=LogColor.YELLOW)
def on_bar(self, bar):
# Simple threshold logic
if bar.close > self.config.threshold:
# Publish signal with the price value
self.publish_signal(
name="PriceAboveThreshold",
value=bar.close,
ts_event=bar.ts_event,
)
self.log.info(f"Published PriceAboveThreshold: {bar.close}")
def on_signal(self, signal):
"""
Handle incoming signals from the MessageBus.
Automatically called by the ActorExecutor when signals arrive.
"""
self.log.info(
f"Threshold crossed! Signal value={signal.value} "
f"at {unix_nanos_to_dt(signal.ts_event)}",
color=LogColor.GREEN,
)
This example illustrates the complete lifecycle of actors and signals for messaging between components:
- Subscription →
subscribe_signal("PriceAboveThreshold")registers the handler with theMessageBus - Publication →
publish_signal()creates a dynamicSignalPriceAboveThresholdclass and routes it viapublish_data() - Consumption →
on_signal()receives the value through theActorExecutorscheduling mechanism
The full working example is available in the repository at examples/backtest/example_11_messaging_with_actor_signals/strategy.py.
Summary
- Decoupled architecture – Actors communicate exclusively through the
MessageBususing signals, eliminating direct dependencies between strategies, risk managers, and execution components. - Dynamic signal classes – The
generate_signal_classfunction innautilus_trader/common/signal.pycreates type-safeDatasubclasses on demand forint,float, orstrvalues. - Simple API – Use
publish_signal(name, value, ts_event)to emit events andsubscribe_signal(name)to receive them, with automatic routing throughnautilus_trader/common/actor.pyx. - Async execution – The
ActorExecutorinnautilus_trader/common/executor.pyschedules allon_signalcallbacks on the trader's event loop, ensuring thread-safe, non-blocking processing. - Environment parity – The messaging system works identically in backtesting and live trading, allowing seamless strategy portability across execution modes.
Frequently Asked Questions
What is the difference between actors and strategies in NautilusTrader?
Strategies are specialized actors that inherit from the Actor base class defined in nautilus_trader/common/actor.pyx. While all strategies are actors, not all actors are strategies—custom risk managers, execution analyzers, or data processors can subclass Actor directly to gain access to publish_signal() and subscribe_signal() without implementing strategy-specific logic like order management. This distinction allows you to build utility components that participate in the messaging ecosystem without being full trading strategies.
Can signals carry complex data types or only primitives?
Signals are restricted to single primitive values—specifically int, float, or str—as implemented in nautilus_trader/common/signal.py. The generate_signal_class() function dynamically creates a Data subclass specifically for that signal name and value type, ensuring minimal serialization overhead. For complex data structures, components should use the standard publish_data() method with custom Data subclasses rather than the signal system, as signals are optimized for lightweight notifications like threshold breaches or status flags.
How does the MessageBus handle concurrent signal processing?
The MessageBus in nautilus_trader/common/component.pyx routes signals synchronously to subscribers, but the ActorExecutor in nautilus_trader/common/executor.py schedules the actual on_signal callbacks asynchronously on the trader's main asyncio event loop. This architecture prevents race conditions by ensuring that signal handlers run sequentially per actor while allowing concurrent execution across different actors. The executor also provides cancellation support via TaskId, allowing the system to cleanly shut down pending signal processing when stopping the trader.
Where can I find working examples of actor messaging?
The repository includes a complete working demonstration at examples/backtest/example_11_messaging_with_actor_signals/strategy.py, which shows a strategy publishing threshold-crossing signals and subscribing to them. Additionally, unit tests verifying publish_signal, subscribe_signal, and on_signal behavior are available in tests/unit_tests/common/test_actor.py, providing reference implementations for custom actor development. These examples demonstrate the same patterns used by internal NautilusTrader components, ensuring best practices for production systems.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →