Signals and actors¶
These are the two extension points you are most likely to touch. Signals answer "what is true about the market right now?" Actors answer "so what should we do?"
Writing a signal¶
A signal implements four things that matter and a lot of optional callbacks:
id() -> its unique name
dependencies() -> other signals it reads
subscriptions -> which raw events it wants
callbacks -> how it reacts
The one rule¶
Subscribe to a raw event only when your signal needs the raw event payload. Depend on other signals when your signal only needs their current output.
Getting this wrong is the most common mistake. A signal that subscribes to every book update "just in case" recomputes far more often than it needs to, and a signal that reimplements a dependency's math drifts away from it over time.
Choosing a callback¶
| Your signal… | Declare | Implement |
|---|---|---|
| Reads the book when book data changes | book_update_subscriptions() |
on_book_update |
| Reacts to trades | trade_update_subscriptions() |
on_trade_update |
| Recomputes when a dependency changes | dependencies() |
on_signal_update |
| Cares only when a dependency becomes usable | dependencies() |
on_signal_valid_update |
| Needs the whole packet applied first | after_packet_subscribed() |
on_after_packet |
| Emits or resets at packet end | before_end_of_packet_subscribed() |
on_before_end_of_packet |
| Fires on a clock rather than on data | time_clock_subscribed() |
the time-clock callback |
Returning a result¶
A callback does not return the new value. It returns a SignalUpdate — two
dirty flags:
SignalUpdate {
value_dirty: bool,
validity_changed: bool,
}
After the callback returns, the runtime reads your value() and is_valid()
and refreshes the shared snapshot. Downstream signals are scheduled only if
your flags say something changed, so honest flags are what keep propagation
cheap. Marking everything dirty works, and quietly costs you on every packet.
flowchart LR
EV["Raw event<br/>or dependency change"] --> CB["Your callback<br/>updates internal state"]
CB --> UP["Returns SignalUpdate<br/>dirty flags only"]
UP --> RD["Runtime reads<br/>value() and is_valid()"]
RD --> SNAP["Snapshot refreshed"]
SNAP --> DOWN["Dependents scheduled<br/>only if dirty"]
classDef key fill:#2e7d32,stroke:#1b5e20,color:#fff
class UP key
Writing an actor¶
An actor is smaller than a signal. Two required methods, two optional:
| Method | Called when | Required |
|---|---|---|
on_signal_update |
A subscribed signal's value changed | Yes |
on_signal_valid_update |
A signal's validity changed | Yes |
on_end_of_packet_signal_changed |
At packet end, if signals moved | No |
on_order_event |
One of its orders was accepted, rejected, filled, canceled | No |
Each returns Vec<OrderIntent> — possibly empty. Returning nothing is normal
and means "no change from what I already have resting."
What an actor gets to see¶
ActorContext is the entire world an actor is allowed to reason about:
| Field | Use |
|---|---|
fair_value |
The signal snapshot — check is_valid before using it |
book |
Current book event |
strategy_position |
This strategy's position |
global_position |
Position across strategies, when configured |
mode |
The StrategyMode safety dial |
tick_size |
Venue price granularity |
resting_orders |
What this actor already has live |
can_cancel_pending_submit |
Whether an unacknowledged order can be pulled |
There is a convenience method, fair_value_number(), that returns None when
the fair value is invalid — prefer it over reading value directly, because it
makes the validity check impossible to forget.
The shape of a quoting actor¶
if fair_value is not valid -> cancel everything
if mode is Stopped or paused -> cancel or hold, per policy
otherwise:
compute desired prices from fair value, edge, and skew
compare against resting_orders
return intents only for what differs
The last line is the one that matters for venue load: actors are expected to be idempotent. Recomputing the same desired book should produce no intents at all, so quoting cost tracks how much the market moved, not how often the actor ran.
Where these run in Dyson¶
Dyson binds actors to venues, drives them with live NATS packets, and routes their intents to real adapters. The actor itself cannot tell the difference between that and a backtest — see Runtime and strategies and Multi-venue strategy for a strategy that binds one shared actor to two venues at once.