Nautilus Developer Guide - Python
The Python authoring contract for any code that touches the Nautilus runtime. Python is the user-facing layer on top of the Rust core: every Strategy, Actor, custom data class, custom RiskEngine rule, custom DataClient, and config object is written in Python and wired into a Rust kernel that owns dispatch, cache, persistence, and FFI. The dev guide page is short on words but load-bearing on rules: PEP-8 with one notable departure (always
is Nonefor non-collection nullity checks), type hints mandatory on every signature using PEP 604 unions (Instrument | None, neverOptional[Instrument]), NumPy-style docstrings in imperative mood, no docstrings on private (_-prefixed) methods, descriptivetest_*names,ruffenforced via pre-commit, and a property-vs-method discipline at the PyO3 boundary that controls whether a Rust field is exposed asobj.valueorobj.value(). Cython (.pyx/.pxd) is legacy - new code uses Python + PyO3, and any Cython function returningvoid/bint/int/doublemust declareexcept *or Python exceptions are silently swallowed. This page is the canonical Python authoring reference for every Cortana MK3 component (CortanaStrategy,ScoringActor,MetaGateActor,RegimeDetectorActor,BrainLoggerActor, the UWLiveDataClient, customRiskEnginerules, and every@customdataclass).
Source URL availability
| URL | Status | Notes |
|---|---|---|
/docs/latest/developer_guide/python/ | 200 OK | Canonical Python style + boundary rules |
/docs/latest/developer_guide/coding_standards/ | (parallel batch) | Cross-language standards |
/docs/latest/developer_guide/design_principles/ | (parallel batch) | Architectural principles |
/docs/latest/how_to/write_python_strategy/ | 404 | Lives in concepts/strategies/ |
/docs/latest/how_to/write_python_actor/ | 404 | Lives in concepts/actors/ |
Core claim
Python in Nautilus is not the implementation language for hot paths - it is the control plane language for user-replaceable logic. The dev guide encodes three structural facts:
- Type hints are not optional. Every signature is annotated. Pre-
commit hooks reject untyped callables. PEP 604 unions are required;
Optional[T]is rejected. - The PyO3 boundary is asymmetric. Python sees Rust types as
immutable views with
#[getter]-exposed properties for cheap reads and explicit methods for anything that allocates, mutates, or does I/O. Thecost-of-callis what dictates property-vs-method. - Cython is legacy. New extensions go through PyO3 (Rust). Any
Cython that remains must respect the
except *rule or it silently swallows exceptions across the C boundary - a class of bug the Python+PyO3 path closes by construction.
Reference - code style (PEP-8 + departures)
The codebase generally follows PEP-8. The single notable departure from default Python idiom (per the dev guide):
“Always use
if foo is None:(oris not None) to check for a None value. The other value might be a value that’s false in a boolean context!”
- Non-collection nullity: explicit
is None/is not None. Never truthiness for arguments that could legitimately be0,0.0,Decimal("0"), an emptyPrice, etc. - Collection emptiness:
if not my_list:(truthiness OK here). - String emptiness:
if not s:is acceptable for strings only if empty-string is the same as missing in context. In doubt, prefer explicitis Nonefirst then a separate emptiness check.
For Cortana: scoring features are floats where 0.0 is a meaningful
neutral value. Never write if feature_value: to check whether a
feature is set - write if feature_value is None: because 0.0 is a
valid score.
Reference - type hints (PEP 604 unions, mandatory)
“All function and method signatures must include type annotations.”
# Required
def __init__(self, config: ScoringActorConfig) -> None:
def on_bar(self, bar: Bar) -> None:
def on_save(self) -> dict[str, bytes]:
def on_load(self, state: dict[str, bytes]) -> None:
# Required: PEP 604 unions
def get_instrument(self, id: InstrumentId) -> Instrument | None:
# Rejected: legacy Optional[]
def get_instrument(self, id: InstrumentId) -> Optional[Instrument]:Generic types use TypeVar:
T = TypeVar("T")
class ThrottledEnqueuer(Generic[T]):
def enqueue(self, item: T) -> None: ...Cortana migration hazard. Any MK2 file using Optional[X] or
Union[A, B] will fail Nautilus’s pre-commit hook. The mechanical fix
is from __future__ import annotations plus X | None everywhere.
Codex handoff: do this in one bulk pass during the migration; don’t
mix syntaxes within a module.
Reference - docstrings (NumPy style, imperative)
NumPy docstring spec is used throughout. Imperative mood:
def make_qty(self, value: float) -> Quantity:
"""
Return a Quantity for the instrument with size_precision applied.
Parameters
----------
value : float
The raw quantity value to round.
Returns
-------
Quantity
Rounded to the instrument's size_increment.
Raises
------
ValueError
If value is negative or exceeds the precision capacity.
"""“Python docstrings should be written in the imperative mood - e.g. ‘Return a cached client.‘”
Public class/method/module docstrings are part of the auto-generated
docs. Private methods (prefixed _) must not carry docstrings:
“Docstrings on private methods incorrectly imply they are part of the public API.”
Two narrow exceptions where private docstrings are acceptable per the dev guide:
- Very complex methods with non-trivial logic, multiple steps, or important edge cases.
- Methods requiring detailed parameter or return value documentation due to complexity.
For everything else, a single inline # comment near the relevant
logic is preferred over a _method docstring.
Reference - properties vs methods (PyO3 boundary)
This is the rule that controls whether a Rust field shows up in Python
as obj.value or obj.value(). The dev guide is explicit (verbatim):
“When exposing Rust types to Python via PyO3, use
#[getter](property) or a plain method based on what the call site communicates, not whether the value can change.”
When to use a property (#[getter])
Cheap, side-effect-free, attribute-like view of current state. Scalar fields, predicates, lightweight derived values.
Examples from the dev guide: status, side, quantity, price,
is_open, has_inputs, realized_pnl, venue_order_id.
order.status # property - cheap read
order.is_open # property - cheap predicate
position.quantity # property - scalar field viewWhen to use a method (no #[getter])
Actions, mutations, nontrivial work, allocations/copies, I/O, or anything that takes arguments.
Examples from the dev guide: apply(fill), unrealized_pnl(price),
calculate_pnl(...).
position.apply(fill) # mutation
position.unrealized_pnl(price) # takes argument
account.calculate_pnl(...) # takes argument + computationGray area - getters that allocate
“Gray area (prefer method): getters that clone or allocate a collection each call. Using a method signals the cost to the caller.”
Examples: events(), adjustments(), client_order_ids(),
trade_ids(). These return a fresh Vec<T> each call - the parens are
the cost signal.
Cortana implication. When reading from a Position or Order in a
hot loop (e.g., on_quote_tick running at 100Hz), prefer scalar
properties. If you need a list of fills, call position.events() once
and cache locally - don’t call it on every tick.
Reference - test naming
Descriptive names that explain the scenario; no test_foo_1,
test_foo_2. The dev guide examples:
def test_currency_with_negative_precision_raises_overflow_error(self):
def test_sma_with_no_inputs_returns_zero_count(self):
def test_sma_with_single_input_returns_expected_value(self):The pattern: test_<subject>_<condition>_<expected_outcome>. Test
names are part of CI output; they read as English specifications.
For Cortana: test_scoring_actor_with_stale_uw_alert_drops_event,
test_meta_gate_with_prob_below_threshold_blocks_order, etc.
Reference - Ruff (linter)
ruff is the canonical Python linter. Rules live in the top-level
pyproject.toml with ignore justifications commented in-line. Pre-
commit runs ruff check and ruff format; CI fails on any unjustified
ignore.
For the Cortana spike: install ruff in the venv (uv pip install ruff) and run before any commit - Nautilus’s CI uses the same rules.
Reference - Cython (legacy)
The .pyx / .pxd path is legacy. New extensions go through PyO3
(Rust). For any remaining Cython:
“For
.pyxand.pxdfiles, make sure all functions and methods returningvoidor a primitive C type (such asbint,int,double) include theexcept *keyword in the signature. Without it, Python exceptions are silently ignored.”
cdef int my_function(int x) except *: # required
if x < 0:
raise ValueError("must be non-negative")
return x * 2Cortana implication. None of MK3’s user-replaceable surface is
Cython. If you find yourself reading a .pyx file, you’re inside the
v1 legacy runtime - note the path you’re on (TradingNode is v1
Cython; LiveNode is v2 Rust+PyO3 - see nautilus-developer-guide.md)
and do not author new code there.
Reference - what the page does NOT cover (cross-references)
The Python page is intentionally narrow - style, types, docstrings, PyO3 boundary, Cython legacy. The full Python authoring contract spans several pages:
concepts/strategies/→nautilus-strategies.md-Strategyauthoring,StrategyConfigrules, lifecycle, sizing-via-RiskEngine pattern.concepts/actors/→nautilus-actors.md-Actorauthoring,ActorConfigrules, what an Actor cannot do (no order placement, noon_save/on_load).concepts/custom_data/→nautilus-custom-data.md-@customdataclassdecorator,DataRegistry, JSON/Arrow envelope.concepts/value_types/→nautilus-value-types.md-Price,Quantity,Money,Decimalwidening rules.concepts/message_bus/→nautilus-message-bus.md- three publishing styles, immutability contract.developer_guide/index/→nautilus-developer-guide.md- adapter layered structure, FFI memory contract.developer_guide/coding_standards/(parallel batch) - full cross-language standards.developer_guide/design_principles/(parallel batch) - broader architectural principles.
This page is the Python-specific authoring contract; the others are the surface-area-specific recipes.
Cortana MK3 implications - idiomatic patterns by component
Cortana MK3 writes a lot of Python: one Strategy, ~5–7 Actors, ~5 custom-data classes, one custom RiskEngine rule, one custom DataClient. The dev guide rules apply uniformly; this section is the per-component checklist.
Custom data class (UWFlowAlert, ScoreUpdate, MetaProb,
EmaDecayValue, RegimeChange)
Canonical pattern. Use the @customdataclass decorator with typed
fields and PEP 604 union syntax. The decorator auto-generates
to_dict/from_dict/to_bytes/from_bytes/schema and registers
the class with the DataRegistry.
from nautilus_trader.model.custom import customdataclass
from nautilus_trader.core import Data
from nautilus_trader.model import InstrumentId
@customdataclass
class UWFlowAlert(Data):
"""A single Unusual Whales flow alert.
Subscribed by ScoringActor; published by UWLiveDataClient.
"""
instrument_id: InstrumentId = InstrumentId.from_str("SPY.ARCA")
underlying: str = "SPY"
side: str = "" # "CALL" / "PUT"
strike: float = 0.0
expiry: str = ""
aggressor: str = "" # "BUY" / "SELL" / "MIXED"
premium_usd: float = 0.0
size_contracts: int = 0
is_sweep: bool = False
is_block: bool = False
flow_score: float = 0.0
underlying_price: float = 0.0
raw_id: str = ""Rules baked in.
- Every field is type-annotated (mandatory per dev guide).
- Defaults are explicit so
from_dictcan reconstruct partial payloads. - No
Optional[float]- if a field can be missing, default to0.0,"", orFalse. PEP 604float | Nonewould work but the decorator’s Arrow encoder is happier with non-null defaults. - Class lives in a shared module (
cortana/mk3/data/uw_types.py) imported by every producer and every consumer (the live DataClient, the scoring Actor, the strategy, the backtest harness, the brain logger). If two modules each declare their ownUWFlowAlert, theDataRegistryregisters two distincttype_namevalues and the bus topic mismatch silently drops every message (pernautilus-custom-data.md). - Docstring is on the public class - fine. Private helper methods on the class would not get docstrings per the dev guide rule.
MK2 pattern that breaks on Nautilus. MK2 used dataclass (stdlib)
with Optional[float] fields and __post_init__ validation. Three
issues: (1) Optional[float] is rejected by ruff; (2) stdlib
dataclass doesn’t auto-register with DataRegistry, so the bus and
catalog can’t route the type; (3) __post_init__ can mutate fields,
violating Nautilus’s “messages are immutable post-publish” rule. Fix:
@customdataclass instead of @dataclass; defaults instead of
Optional; validation moves to the publisher (DataClient or Actor)
before construction, not after.
Strategy (CortanaStrategy)
Canonical pattern. Subclass Strategy, paired StrategyConfig
subclass with frozen=True, type-annotated fields, lifecycle hooks
in on_start/on_stop. No order placement in __init__ (clock/log/
msgbus aren’t wired yet).
from decimal import Decimal
from nautilus_trader.config import StrategyConfig
from nautilus_trader.model import InstrumentId, BarType
from nautilus_trader.trading.strategy import Strategy
class CortanaStrategyConfig(StrategyConfig, frozen=True):
instrument_id: InstrumentId
bar_type: BarType
trade_size: Decimal
score_threshold: int = 65
meta_prob_threshold: float = 0.55
order_id_tag: str = "CORTANA-001"
class CortanaStrategy(Strategy):
"""Submits bracket orders on score+meta-prob confluence."""
def __init__(self, config: CortanaStrategyConfig) -> None:
super().__init__(config)
# Local state ONLY - no clock/log/msgbus access here
self._last_score: float | None = None
self._last_meta_prob: float | None = None
def on_start(self) -> None:
# Subscriptions, indicators, timers - all here
self.subscribe_data(ScoreUpdate)
self.subscribe_data(MetaProb)
def on_stop(self) -> None:
# Cancel timers, flush state
pass
def on_data(self, data: Data) -> None:
if isinstance(data, ScoreUpdate):
self._last_score = data.composite_score
elif isinstance(data, MetaProb):
self._last_meta_prob = data.meta_prob
self._maybe_submit()
def _maybe_submit(self) -> None:
# Private - no docstring per dev guide
if self._last_score is None or self._last_meta_prob is None:
return
if self._last_score < self.config.score_threshold:
return
if self._last_meta_prob < self.config.meta_prob_threshold:
return
self._submit_bracket()MK2 patterns that break.
- MK2 instantiates clock/log in
__init__. Fix: move all subscriptions and clock/log access toon_start. The Kernel wires these after registration; the constructor runs before. - MK2 stores config values as instance attributes with mutable defaults
(e.g.,
self.threshold = 65). Fix: read throughself.config.thresholdso distributed-backtest serialization round- trips correctly.StrategyConfig(frozen=True)makes this enforceable. - MK2 calls
time.time()anddatetime.now()directly. Fix:self.clock.timestamp_ns()everywhere.time.time()in handlers breaks backtest determinism - the kernel’s data-driven clock is ignored.
Actor (ScoringActor, MetaGateActor, etc.)
Canonical pattern. Same shape as Strategy but without order
placement. Subclass Actor; paired ActorConfig; subscriptions in
on_start; on_data discriminates custom-data subclasses by
isinstance.
from nautilus_trader.common.actor import Actor
from nautilus_trader.config import ActorConfig
from nautilus_trader.core import Data
from nautilus_trader.model import InstrumentId, BarType
class ScoringActorConfig(ActorConfig):
instrument_id: InstrumentId
bar_type: BarType
flow_decay_half_life_seconds: float = 30.0
class ScoringActor(Actor):
def __init__(self, config: ScoringActorConfig) -> None:
super().__init__(config)
self._flow_pressure: float = 0.0
def on_start(self) -> None:
self.subscribe_data(UWFlowAlert)
self.subscribe_bars(self.config.bar_type)
def on_data(self, data: Data) -> None:
if isinstance(data, UWFlowAlert):
self._on_flow(data)MK2 pattern that breaks. MK2 has one engine.py module with a
fat Engine class that does scoring, position management, and order
placement. Fix: decompose into one Strategy (orders) + N Actors
(scoring, regime, decay). Actors cannot call submit_order - the
method doesn’t exist on the base class. If you find yourself wanting
to call it from an Actor, the component is actually a Strategy.
Custom DataClient (UW WebSocket adapter)
Canonical pattern. Subclass LiveMarketDataClient; bytes-in →
Data-subclass-out. The DataClient owns network I/O; the Actor owns
derived computation.
from nautilus_trader.live.data_client import LiveMarketDataClient
class UWLiveDataClient(LiveMarketDataClient):
async def _on_message(self, frame: dict) -> None:
# frame is the parsed UW WebSocket payload
alert = UWFlowAlert(
instrument_id=InstrumentId.from_str("SPY.ARCA"),
side=frame["side"],
strike=float(frame["strike"]),
premium_usd=float(frame["premium"]),
aggressor=frame["aggressor"],
flow_score=float(frame["score"]),
ts_event=int(frame["ts_ms"]) * 1_000_000, # ms → ns
ts_init=self._clock.timestamp_ns(),
)
# Routes through DataEngine → Cache → MessageBus
self._handle_data(alert)Async/await rules. The DataClient is the one place in user code
where async def is the norm - the underlying network loop is async.
But the data-handler call (self._handle_data(alert)) is sync -
it crosses into the kernel dispatch, which is single-threaded and
synchronous. Strategy/Actor handlers are always sync (on_bar,
on_data, on_quote_tick); never declare them async def.
MK2 pattern that breaks. MK2’s UW client is a synchronous polling
loop in a thread. Nautilus expects an asyncio task that the runtime
manages. Fix: LiveMarketDataClient.connect() returns a coroutine;
the runtime spawns and supervises it. Don’t manage your own thread.
Custom RiskEngine rule (meta-prob gate, max-position)
Canonical pattern. Subclass the framework’s risk-rule base, read
from self.cache.custom_data(MetaProb, ...) to gate orders pre-trade.
Per nautilus-strategies.md § “Sizing multiplier”, enforcement lives
in a RiskEngine rule, not in the strategy - so dead-code regressions
(GH 88-class) are impossible by construction.
class MetaProbGateRule:
"""Reject orders when meta-prob is below threshold."""
def __init__(self, threshold: float = 0.55) -> None:
self._threshold = threshold
def check(self, order, cache) -> RiskCheckResult:
meta_prob = cache.custom_data(
MetaProb, instrument_id=order.instrument_id,
)
if meta_prob is None or meta_prob.meta_prob < self._threshold:
return RiskCheckResult.reject("meta-prob below threshold")
return RiskCheckResult.pass_()Decimal vs float. Meta-prob is a probability (0.0–1.0) - float is
fine. Sizing math that produces a contract count must round through
instrument.make_qty(...) per nautilus-value-types.md; never use
int(qty) or qty // 1.
BrainLoggerActor
Canonical pattern. Subscribes to subscribe_order_fills and
subscribe_order_cancels; writes markdown to ~/brain/writing/ on
on_order_filled. No bus publication - output is a side-effect file
write.
class BrainLoggerActor(Actor):
def on_start(self) -> None:
self.subscribe_order_fills(self.config.instrument_id)
def on_order_filled(self, event) -> None:
# File-write side-effect - fine in an Actor
path = f"{self.config.brain_path}/trades/{event.ts_event}.md"
with open(path, "w") as f:
f.write(self._format_trade(event))Heavy-work rule. File I/O on every fill is fine at Cortana volumes
(double-digit fills/day). At higher volume, queue writes and flush on
a timer or push to a separate process consuming the bus over Redis
(per nautilus-howto-write-actor.md § “Common pitfalls”).
Cortana MK3 implications - async/await rules summary
| Surface | Sync or async | Notes |
|---|---|---|
Strategy.on_* handlers | Sync | Kernel dispatch is single-threaded sync |
Actor.on_* handlers | Sync | Same as Strategy |
LiveDataClient._on_message etc. | Async | Network loop is async |
LiveDataClient._handle_data | Sync call | Crosses into kernel dispatch |
Custom RiskEngine rule check | Sync | Pre-trade synchronous gate |
Timers (clock.set_timer) | Callback is sync | Don’t declare async def callbacks |
on_save / on_load | Sync | Persistence path |
If you need to do async work from inside a sync handler (e.g., HTTP
call from on_bar), spawn a task on the kernel runtime - never block
the dispatcher. For Cortana, the right answer is almost always
“don’t do async work from a handler; queue the work and let an
async-aware Actor consume the queue.”
Cortana MK3 implications - error handling
The dev guide doesn’t enumerate Python error-handling rules explicitly, but the framework’s behavior is consistent across the concept pages:
- Construction errors (
Price,Quantity,Money,InstrumentId, identifier types) raiseValueErrorat the boundary. The MK2 NaN-into-sizing bug class is closed by construction (nautilus-value-types.md). - Risk rejections flow as
OrderDeniedevents, not exceptions. - Adapter errors propagate as
OrderRejected/OrderDeniedevents on the bus; the strategy reads them viaon_order_rejected. - Inside handlers, raising any exception puts the component in
FAULTEDstate - the kernel disposes it. Raise only when the only correct response is “stop trading”; otherwise log + recover. - Logging. Use
self.log.info(...)/self.log.error(...)/self.log.debug(...)- notprint(), notlogging.getLogger(...). The framework’s logger is structured, queue-backed, and routes to the configured sink (file, stdout, remote).
def on_data(self, data: Data) -> None:
if isinstance(data, ScoreUpdate):
try:
self._handle_score(data)
except (ValueError, KeyError) as e:
# Log + recover; don't let one bad event kill the actor
self.log.error(f"score handler failed: {e}")
returnCortana MK3 implications - logging from Python
self.log.debug("verbose debug")
self.log.info("normal flow event")
self.log.warning("recoverable anomaly")
self.log.error("operation failed; recovering")
self.log.exception("unexpected; printing stack")Three rules:
- Don’t use
print(). It bypasses the structured logger and doesn’t show up in remote sinks. - Don’t capture log output for assertions. Per
nautilus-developer-guide.mdtesting section: “Don’t capture log output to assert on log messages - verify observable behavior instead.” For Cortana, this means don’t assert on log strings; assert on emitted bus events or cache state. - No private-method docstrings, but inline
#comments are fine. Pair a tricky log line with a one-line#comment if the why isn’t obvious from the message text.
Cortana MK3 implications - migration hazards from MK2
A consolidated list of patterns in MK2 that silently misbehave on Nautilus, with the fix:
| MK2 pattern | Why it breaks | Fix |
|---|---|---|
from typing import Optional; def f() -> Optional[X] | Pre-commit ruff rejects | def f() -> X | None |
@dataclass for bus events | No DataRegistry registration; bus topic miss | @customdataclass |
__post_init__ validation that mutates | Violates “messages are immutable post-publish” | Validate at the publisher before construction |
time.time() / datetime.now() in handlers | Breaks backtest determinism | self.clock.timestamp_ns() |
print() statements | Bypasses structured logger | self.log.info(...) |
Float prices in comparisons (price == 100.0) | Float drift | Price value-type with precision-aware equality |
qty // 3 integer-division on contracts | Truncates silently | Quantity / N → Decimal; round via instrument.make_qty |
| NaN mark-price into sizing | NaN propagates through float math | Price/Money constructors raise on non-finite input |
Subscriptions in __init__ | Clock/log/msgbus not wired yet | Move to on_start |
submit_order from a non-Strategy | Method doesn’t exist on Actor | Refactor: that component IS a Strategy |
| Sync polling thread for UW | Bypasses kernel runtime supervision | async def in LiveDataClient |
Truthiness check for nullity (if score:) | 0.0 is a valid score | if score is None: then separate logic |
Optional[float] field on a custom-data class | Decorator’s Arrow encoder unhappy | Default to 0.0; if truly nullable, document and test round-trip |
Stdlib logging.getLogger(__name__) | Bypasses framework logger | self.log (already wired by Actor/Strategy) |
| Mutating a published event downstream | Violates immutability | Construct a new local event with derived fields |
| Capturing log output in tests | Fragile - global logger, non-deterministic test order | Assert on observable behavior (bus events, cache state) |
When this concept applies
Every Python file authored for MK3 - Strategy, Actor, custom-data class, RiskEngine rule, DataClient, ConfigClass, test. The dev guide rules are uniform across all of them.
When this concept does NOT apply
- Rust crates under
crates/. Those follow the Rust dev guide (separate page). New adapter cores are Rust-first per the developer guide; only the wiring layer is Python. - Cython
.pyx/.pxdfiles in the legacy v1 runtime. New code doesn’t go there. If you find yourself reading one, you’re on the v1 path - switch to the v2LiveNoderoute. - External Python tooling (CI scripts, ad-hoc analysis notebooks) that doesn’t import Nautilus. The dev guide rules are conventions for the framework codebase; external scripts can be more relaxed but Cortana benefits from holding the same line for consistency.
See Also
- Nautilus Developer Guide - adapter layered structure, FFI memory contract, contributing back
- Nautilus Coding Standards (parallel) - cross-language standards
- Nautilus Design Principles (parallel) - architectural principles
- Nautilus Value Types -
Price,Quantity,Money,Decimalwidening, NaN handling - Nautilus Custom Data -
@customdataclassdecorator,DataRegistry, JSON envelope - Nautilus How-To Write an Actor -
Actor recipe,
ScoringActorskeleton - Nautilus How-To Write a Strategy - Strategy recipe,
CortanaStrategyskeleton - Nautilus Strategies -
StrategyConfig, sizing-via-RiskEngine pattern - Nautilus Actors -
ActorConfig, what an Actor cannot do - Nautilus Message Bus - pub/sub spine, immutability contract
- 2026-05-09 Nautilus Spike Plan:
~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-nautilus-spike.md - Source: https://nautilustrader.io/docs/latest/developer_guide/python/
Timeline
- 2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 7 (developer guide).