Nautilus Strategies
A Nautilus
Strategyis the only component permitted to submit, modify, and cancel orders. It extendsActorwith order management. Every other reactive piece of a system - data ingestion, scoring, signal publishing, monitoring, dashboards - should live as anActor. For Cortana MK3, the entry-decision path that consumes scores and fires bracket orders is unambiguously aStrategy. The composite scoring engine, UW flow ingestion, meta-model classifier, and brain logger areActors. The only open question is whether UW raw market data ingestion should live in anActoror be elevated to a customDataClient/adapter - the answer is “Actor for the spike, custom DataClient for production multi-tenant deployment.”
Cody’s question - answered
Q: Is Strategy where I will need to port Cortana over to, or is Cortana
the actor?
A: Both. Cortana decomposes into one Strategy plus several Actors. The
piece you “port Cortana over to” - meaning the entry/exit decision and order
placement path - is a Strategy. The pieces that observe data, compute scores,
and publish events without touching orders are Actors.
The doc is explicit: “The Strategy class inherits from Actor, which means
strategies have access to all actor functionality plus order management
capabilities.” That single sentence is the routing rule. The discriminator is
order submission, nothing else. If a component calls self.submit_order(...)
or self.cancel_order(...), it is a Strategy. If it only consumes data,
calls indicators, runs ML, or publishes signals, it is an Actor.
Concrete mapping for Cortana’s 5 entry triggers
The 5 triggers (repeated_hits, flow_alert, cumulative_flow, timer,
impulse:strike_stack) are all entry decisions that culminate in an order.
They all belong to the same Strategy. The five branches are routing inside
on_data / on_signal / on_event handlers, not five separate Strategies.
(Justification below in “Cortana MK3 implications.“)
Concrete mapping for the supporting components
| Cortana component | Nautilus role | Why |
|---|---|---|
| Composite scoring engine (78 features → score+conviction+bias) | Actor | No order submission. Subscribes to bars/quotes/UW data; publishes a ScoreUpdate custom message. |
| UW flow alert publisher | Actor (spike) → DataClient (production) | Spike: poll UW REST/WS in an Actor’s on_start and republish as UWFlowAlert. Production: full adapter under crates/adapters/unusual_whales/ per nautilus-developer-guide.md. |
| Meta-model classifier (win-prob gate) | Actor OR risk-engine rule | Actor if you want it as a transparent intermediate signal (publishes MetaProbUpdate). Risk-engine rule if you want it as a hard pre-trade gate that the Strategy never bypasses. The Cortana mandate (“80% win rate”) argues for risk-engine placement so it cannot become dead code (#88). |
| Cyclical encoding (sin/cos minute-of-day, dte) | Actor | Pure derivation from time + market state; publishes encoded features. |
| Position manager (TP/SL fallback, time-in-trade exits) | Lives inside the Strategy | Touches orders. on_quote_tick checks for SL trigger and calls self.submit_order(reduce_only=True). |
Brain logger (writes to ~/brain on close) | Actor | Consumes PositionClosed events via on_event; no order touch. |
| ML dashboard | Out-of-band Redis subscriber, not an Actor | Per the developer guide: dashboards live outside the kernel and read the message bus over Redis. |
The doc’s Actor description aligns with this split: “Use Actor for monitors, scanners, custom data producers, and analytics; use Strategy when you need to submit/modify/cancel orders.”
Why not “one big Strategy that does everything”?
You technically could put the scoring engine, meta-model, and entry logic
all inside one Strategy.on_quote_tick. The doc does not forbid it. Reasons
not to:
- Multi-tenant SaaS framing (Step 7.5 of the spike). When customer #2
joins, they may want a different scoring engine but the same Cortana entry
logic - or vice versa. Composing
[ScoringActor, CortanaStrategy]allows substitution at deployment time. A monolithic Strategy does not. - Backtest reuse. An
ActorpublishingScoreUpdatecan be replayed independently of any Strategy, which lets you backtest the score against future-state outcomes without coupling to a specific entry rule. - Determinism and message-bus replay. Per
nautilus-concepts.md, every message is immutable and timestamped. Putting score derivation behind a bus message makes the entire decision pipeline replayable from logs - crucial for postmortems (#46, the chop-day cluster, the 2026-05-06 outage). - Single-responsibility. A
Strategythat also runs feature engineering tends to grow unbounded. Separating concerns at theActor/Strategyboundary preserves the “early and right” mandate by keeping decision latency visible - you can measure ts_event → ts_init → score publish → strategy entry as separate stages on the bus.
Strategy reference
Inheritance and capabilities
Strategy extends Actor. From the doc: “The Strategy class inherits
from Actor, which means strategies have access to all actor functionality
plus order management capabilities.” Everything in
concepts/nautilus-actors.md (the parallel page) applies to Strategies too:
data subscriptions, indicator registration, signal publishing, time alerts,
cache and portfolio queries, lifecycle hooks. Strategies add: order
submission via self.order_factory + self.submit_order(...), order
modification, cancellation, and position-event handlers.
Lifecycle hooks (full list)
def on_start(self) -> None # Subscribe to data, register indicators, fetch instruments
def on_stop(self) -> None # Cancel orders, close positions, cleanup
def on_resume(self) -> None # Resume from a paused/degraded state
def on_reset(self) -> None # Reset state between backtest runs (BacktestEngine.reset())
def on_degrade(self) -> None # Adapter degraded; partial functionality
def on_fault(self) -> None # Unrecoverable error path; engine moving to FAULTED
def on_dispose(self) -> None # Final cleanup before disposal
def on_save(self) -> dict[str, bytes] # Persist user state across restarts (Cache database)
def on_load(self, state: dict[str, bytes]) -> None # Restore on restartCritical constraint from the doc: “Do not call components such as clock
and logger in the __init__ constructor (which is prior to registration).”
Subscriptions, indicator registration, and any clock/logger access go in
on_start. The constructor only stores config.
Data handlers (full list)
def on_order_book_deltas(self, deltas: OrderBookDeltas) -> None
def on_order_book(self, order_book: OrderBook) -> None
def on_quote_tick(self, tick: QuoteTick) -> None
def on_trade_tick(self, tick: TradeTick) -> None
def on_bar(self, bar: Bar) -> None
def on_instrument(self, instrument: Instrument) -> None
def on_instrument_status(self, data: InstrumentStatus) -> None
def on_instrument_close(self, data: InstrumentClose) -> None
def on_option_greeks(self, greeks: OptionGreeks) -> None
def on_option_chain(self, chain: OptionChainSlice) -> None
def on_historical_data(self, data: Data) -> None # Hydration via request_*()
def on_data(self, data: Data) -> None # Custom data subclasses
def on_signal(self, signal: Data) -> None # Lightweight primitive notificationson_option_greeks and on_option_chain are particularly relevant for SPY
0DTE: native chain delivery is supported as a first-class data type. We do
not have to invent a custom message for option chain updates.
Order/position event handlers - specific-to-generic dispatch
The doc specifies a fan-in: each event hits the most specific handler first,
then on_order_event (or on_position_event), then the generic on_event.
A strategy can implement any subset.
# Order events (in dispatch order - specific first)
def on_order_initialized(self, event: OrderInitialized) -> None
def on_order_denied(self, event: OrderDenied) -> None
def on_order_emulated(self, event: OrderEmulated) -> None
def on_order_released(self, event: OrderReleased) -> None
def on_order_submitted(self, event: OrderSubmitted) -> None
def on_order_rejected(self, event: OrderRejected) -> None
def on_order_accepted(self, event: OrderAccepted) -> None
def on_order_canceled(self, event: OrderCanceled) -> None
def on_order_expired(self, event: OrderExpired) -> None
def on_order_triggered(self, event: OrderTriggered) -> None
def on_order_pending_update(self, event: OrderPendingUpdate) -> None
def on_order_pending_cancel(self, event: OrderPendingCancel) -> None
def on_order_modify_rejected(self, event: OrderModifyRejected) -> None
def on_order_cancel_rejected(self, event: OrderCancelRejected) -> None
def on_order_updated(self, event: OrderUpdated) -> None
def on_order_filled(self, event: OrderFilled) -> None
def on_order_event(self, event: OrderEvent) -> None
def on_event(self, event: Event) -> None
# Position events
def on_position_opened(self, event: PositionOpened) -> None
def on_position_changed(self, event: PositionChanged) -> None
def on_position_closed(self, event: PositionClosed) -> None
def on_position_event(self, event: PositionEvent) -> NonePer nautilus-concepts.md, fill/cancel events flow “only through the message
bus” without DataEngine involvement. This is why you cannot capture them as
an Actor-only concern: Actors do not own a position lifecycle.
Order submission API
All order construction flows through self.order_factory (an OrderFactory
the kernel provides; strategy IDs and trader IDs are auto-filled). Submission
is via self.submit_order(order) for a single order, or self.submit_order_list(...)
for grouped/contingent submissions.
order = self.order_factory.market(
instrument_id=self.instrument_id,
order_side=OrderSide.BUY,
quantity=self.instrument.make_qty(self.trade_size),
time_in_force=TimeInForce.FOK,
exec_algorithm_id=ExecAlgorithmId("TWAP"),
exec_algorithm_params={"horizon_secs": 20, "interval_secs": 2.5},
)
self.submit_order(order)Routing rule (verbatim from the doc): “If an emulation_trigger is
specified, the command will firstly be sent to the OrderEmulator. If an
exec_algorithm_id is specified (with no emulation_trigger), the command
will firstly be sent to the relevant ExecAlgorithm. Otherwise, the command
will firstly be sent to the RiskEngine.” In all paths the RiskEngine
ultimately validates before the venue.
OrderFactory methods
The full set of order types (also enumerated in nautilus-concepts.md):
self.order_factory.market(...)
self.order_factory.limit(...)
self.order_factory.stop_market(...)
self.order_factory.stop_limit(...)
self.order_factory.market_to_limit(...)
self.order_factory.market_if_touched(...)
self.order_factory.limit_if_touched(...)
self.order_factory.trailing_stop_market(...)
self.order_factory.trailing_stop_limit(...)
self.order_factory.bracket(...) # Entry + TP + SL as a single submissionFor Cortana MK3 the load-bearing one is bracket(...) - it expresses the
“entry + TP + SL” pattern as a single OTO/OCO group, which directly addresses
feedback_dual_tp_defense_in_depth.md (TP must have software fallback when
broker LMT fails - Nautilus emulates the bracket locally if the venue does
not support it natively, and the cache-then-publish invariant means the
strategy sees consistent state).
Common parameters
time_in_force:GTC,IOC,FOK,GTD,DAYpost_only:Trueto refuse taking liquidity (limit-only). Refused if it would cross.reduce_only:Trueto ensure the order can only reduce an existing position. Critical for stop-loss orders that must never accidentally flip the position.emulation_trigger:LAST_PRICE,BID_ASK,MARK- when set, the OrderEmulator handles trigger logic locally and only submits a basic Market or Limit to the venue when triggered.exec_algorithm_id+exec_algorithm_params: hand off to a customExecAlgorithm(e.g., TWAP).display_qty: iceberg display.trigger_type/trigger_offset_type:PRICE,BPS,TICKS,PRICE_TIERfor trailing stops.
Modify and cancel
self.modify_order(order, new_quantity)
self.cancel_order(order)
self.cancel_orders(my_order_list)
self.cancel_all_orders() # Optional instrument/side filtersModify sets order state to PENDING_UPDATE; cancel sets it to
PENDING_CANCEL. If the order is already closed, a warning is logged and the
call is a no-op.
Close positions
self.close_position(position)
self.close_all_positions()These submit market orders sized to the position’s current qty with
reduce_only=True.
market_exit() - graceful shutdown helper
self.market_exit()
def on_market_exit(self) -> None: # Hook called at start of exit
self.log.info("Beginning market exit...")
def post_market_exit(self) -> None: # Hook called once flat (or after max attempts)
self.log.info("Market exit complete")
def is_exiting(self) -> bool: # Predicate to prevent re-entry during exit
...Behavior:
- Cancels all open and in-flight orders.
- Closes all open positions with reduce-only market orders.
- Periodically re-checks (interval and max attempts configurable).
- Calls
post_market_exitonce flat or after max attempts.
“During a market exit, non-reduce-only orders are automatically denied.”
This is a load-bearing guarantee for the EOD-flat invariant - it is
impossible for an in-flight on_data to open a new position while
market_exit is in progress. Aligns with feedback_no_kill_with_open_positions.md.
StrategyConfig exposes the relevant knobs:
StrategyConfig(
manage_stop=True, # Auto-call market_exit on stop()
market_exit_interval_ms=100,
market_exit_max_attempts=100,
market_exit_time_in_force=None, # Defaults to GTC
market_exit_reduce_only=True,
)RiskEngine integration
The RiskEngine sits between the strategy/exec engine and the
ExecutionClient. It is the only place pre-trade validation lives - quantity
bounds, price precision, position impact, trading-state checks
(HALTED/PAUSED/ACTIVE). Strategy code never embeds size guards; the
RiskEngine is configured centrally and rejects violations before they reach
the venue. From nautilus-concepts.md: “You do not embed if size > X
checks in each strategy; you configure RiskEngine limits centrally.”
For Cortana the placement of meta-prob sizing matters. Per
project_codex_review_p2s.md and #88 (dead-code meta-prob), inline sizing in
a strategy module can become silently dead. Putting meta-prob weighting in a
custom RiskEngine rule means every order passes through it by construction.
This is the correct home for the meta gate.
Indicator registration
self.fast_ema = ExponentialMovingAverage(period=10)
self.register_indicator_for_bars(self.bar_type, self.fast_ema)
self.register_indicator_for_quote_ticks(self.instrument_id, my_indicator)
self.register_indicator_for_trade_ticks(self.instrument_id, my_indicator)Once registered, the engine drives the indicator on every matching update.
The strategy reads self.fast_ema.value in handlers without manually
feeding ticks. Indicator hydration uses request_bars(...) followed by
subscribe_bars(...): history flows through on_historical_data, then the
live feed through on_bar.
Portfolio queries - self.portfolio
self.portfolio.account(venue: Venue) -> Account
# Per-venue aggregates
self.portfolio.balances_locked(venue) -> dict[Currency, Money]
self.portfolio.margins_init(venue) -> dict[Currency, Money]
self.portfolio.margins_maint(venue) -> dict[Currency, Money]
self.portfolio.unrealized_pnls(venue) -> dict[Currency, Money]
self.portfolio.realized_pnls(venue) -> dict[Currency, Money]
self.portfolio.net_exposures(venue) -> dict[Currency, Money]
# Per-instrument
self.portfolio.unrealized_pnl(instrument_id) -> Money
self.portfolio.realized_pnl(instrument_id) -> Money
self.portfolio.net_exposure(instrument_id) -> Money
self.portfolio.net_position(instrument_id) -> decimal.Decimal
# Predicates
self.portfolio.is_net_long(instrument_id) -> bool
self.portfolio.is_net_short(instrument_id) -> bool
self.portfolio.is_flat(instrument_id) -> bool
self.portfolio.is_completely_flat() -> boolThese are pull-style queries against the kernel’s central Portfolio. Per
nautilus-concepts.md, valuation uses a price fallback chain (cached mark,
side-appropriate quote, last trade, recent bar close) and explicitly flags
unpriceable positions rather than silently zero-valuing them.
Cache queries - self.cache
self.cache.quote_tick(instrument_id, index=0) # Reverse-indexed: 0 = most recent
self.cache.trade_tick(instrument_id, index=0)
self.cache.bar(bar_type, index=0)
self.cache.order_book(instrument_id)
self.cache.price(instrument_id, price_type) # BID, ASK, MID, LAST
self.cache.order(client_order_id)
self.cache.position(position_id)
self.cache.orders_open(...)
self.cache.orders_closed(...)
self.cache.orders_emulated(...)
self.cache.orders_inflight(...)
self.cache.positions_open(...) # Filter by venue/instrument/strategy/side
self.cache.positions_closed(...)Returns None when missing. The cache-then-publish invariant guarantees that
inside an on_quote_tick handler, self.cache.quote_tick(instrument_id)
returns the very tick that triggered the handler - no race. Critical for
Cortana since the spy_price-cache-stale class of bug we hit on 2026-05-06
disappears under this invariant.
Emulated-order gotcha: query emulated orders through the Cache, not by holding a local reference, because “the order object transforms when the emulated order is released.”
Subscriptions and historical requests
# Live subscriptions
self.subscribe_quote_ticks(instrument_id)
self.subscribe_trade_ticks(instrument_id)
self.subscribe_bars(bar_type)
self.subscribe_instrument(instrument_id)
self.subscribe_instrument_status(instrument_id)
self.subscribe_order_book_deltas(instrument_id)
self.subscribe_order_book_at_interval(instrument_id, interval_ms=...)
self.subscribe_data(data_type, client_id=...) # Custom data
self.subscribe_signal(name, client_id=...) # Custom signals
# Historical requests (route to on_historical_data)
self.request_bars(bar_type)
self.request_quote_ticks(instrument_id)
self.request_trade_ticks(instrument_id)
self.request_instrument(instrument_id)
self.request_data(data_type, ...)Subscriptions emit on the live handler (on_bar, etc.); requests emit on
on_historical_data. This dual surface is what lets a strategy hydrate
indicators with history at start and then consume real-time updates without
code change.
Custom data and signal publishing
Although these mostly live on the Actor surface, Strategies inherit them:
self.publish_data(data_type, data) # Typed custom data
self.publish_signal(name, value, ts_event) # Primitive int/float/str alerts
self.msgbus.publish(topic, message) # Low-level pub/sub (typo-prone)The doc’s three publishing styles (per nautilus-concepts.md):
- Low-level pub/sub on topics - most flexible, must track topic strings manually.
- Actor-based custom data publishing via
Datasubclasses or@customdataclass- proper event ordering viats_event/ts_init. - Actor-based signal publishing for primitive notifications.
For Cortana, ScoreUpdate and MetaProbUpdate should use option 2
(@customdataclass) so they carry timestamps and replay correctly under
backtest.
Clock and timer API
now = self.clock.utc_now() # pd.Timestamp
ns = self.clock.timestamp_ns() # Unix nanoseconds (int)
# Single-fire alert -> dispatched as TimeEvent to on_event
self.clock.set_time_alert(
name="EOD_FLATTEN",
alert_time=self.clock.utc_now() + pd.Timedelta(minutes=15),
)
# Recurring timer -> dispatched as TimeEvent to on_event
self.clock.set_timer(
name="HEARTBEAT",
interval=pd.Timedelta(minutes=1),
)
self.clock.cancel_timer(name="HEARTBEAT")Per nautilus-concepts.md: in backtest the Clock is driven by the data
stream so alerts fire deterministically at the simulated moment; in live
the Clock is wall-clock backed but the interface is identical. Strategies
must never call time.time() or datetime.now() directly - that is what
makes backtest runs reproducible. This directly enables the power-hour
regime detection (project_eod_power_hour.md) - set a 14:30 CT
set_time_alert and it fires at the right simulated moment in backtest and
the right wall-clock moment in live, with no code change.
StrategyConfig
from nautilus_trader.config import StrategyConfig
class CortanaConfig(StrategyConfig):
instrument_id: InstrumentId
bar_type: BarType
score_threshold: int = 65
meta_prob_threshold: float = 0.55
trade_size: Decimal
order_id_tag: str
class CortanaStrategy(Strategy):
def __init__(self, config: CortanaConfig) -> None:
super().__init__(config)
# Configuration accessible as self.config.score_threshold, etc.Quoted from the doc: “A separate configuration class gives full flexibility over where and how a strategy is instantiated. Configurations serialize over the wire, enabling distributed backtesting and remote live trading.”
This is what makes the “Cody as customer #1” multi-tenant framing tractable -
each tenant’s CortanaConfig instance is shippable JSON, instantiated in a
per-tenant TradingNode.
order_id_tag and multi-instance disambiguation
“If you intend running multiple instances of the same strategy, with
different configurations (such as trading different instruments), then you
will need to define a unique order_id_tag for each of these strategies.”
“A strategy ID is made up of the strategy class name, and the strategies
order_id_tag separated by a hyphen.” So CortanaStrategy with
order_id_tag="SPY-0DTE-CALL" and order_id_tag="SPY-0DTE-PUT" produce
strategy IDs CortanaStrategy-SPY-0DTE-CALL and CortanaStrategy-SPY-0DTE-PUT.
Duplicate strategy IDs raise RuntimeError at registration. This is also
how the multi-tenant pattern resolves: per-tenant order_id_tag = tenant_id.
Backtest vs live
“Once a strategy is defined, the same source code can be used for backtesting and live trading.” The split lives at the Engine layer, not the Strategy layer. The strategy never knows or cares which environment it runs in. Behavioral differences a strategy author should know:
- Reconciliation runs only in live.
LiveExecutionEnginereconciles cached state against venue reports on connect.BacktestEnginecontrols both sides and skips reconciliation. The Strategy sees a clean state in both cases. - Clock differs but interface does not. Backtest Clock is data-driven; Live Clock is wall-clock. Strategy code is identical.
- Fills are simulated in backtest via the matching engine + fill model
(probabilistic limit fills, slippage, optional
ThreeTierFillModel). In live, fills come from the venue’s execution feed via theExecutionClient. The strategy seesOrderFilledevents in the same shape. on_resetonly fires in backtest. It returns “all stateful fields to their initial value, except for data and instruments which persist” - fast for parameter sweeps. Live engines do not callon_reset.
Cortana MK3 implications
One Strategy or five?
The 5 entry triggers (repeated_hits, flow_alert, cumulative_flow,
timer, impulse:strike_stack) all converge on the same decision: “open a
SPY 0DTE call/put position right now.” They share sizing logic, the same
TP/SL geometry, the same EOD-flatten requirement, and the same risk model.
Use one CortanaStrategy. The triggers branch inside on_data (or
on_signal for impulse, or on_event for the timer alert) and call a
shared _attempt_entry(side, conviction, source) method that goes through
the meta gate and the order factory.
Alternative architectures considered:
- Five thin Strategies, one per trigger. Adds management overhead with no
benefit. Each Strategy needs its own
order_id_tag, its own meta-gate config, and its own EOD-flatten logic. Cross-trigger interactions (cooldowns, contradiction checks) become bus-mediated coordination that is hard to reason about. Reject. - One big Strategy with no Actors. Loses the multi-tenant composability and the bus-replay value. Reject.
- One Strategy + multiple Actors. Recommended. Score derivation, meta-prob, UW ingestion, brain logging are all Actors. Entry/exit decisions live in the Strategy.
Sizing multiplier - where does meta-prob live?
Two viable placements:
- Inside
CortanaStrategy._attempt_entryas inline sizing logic. Easy to read; risks becoming dead code (#88) if a refactor accidentally drops the call. - Inside a custom
RiskEnginerule. Every order routes through the RiskEngine by construction, so meta-prob sizing cannot be silently bypassed. Aligns withproject_codex_review_p2s.md.
Recommendation: meta-prob lives in the RiskEngine. The strategy submits
an order at the unweighted base size, and a custom RiskEngine rule scales
the quantity based on the most recent MetaProbUpdate published by the
meta-model Actor. This makes the gate impossible to bypass, even if the
strategy’s branch logic changes. (The Risk layer can also veto the order
entirely if meta_prob < threshold.)
Meta gate placement - Actor signal or RiskEngine rule?
These are not mutually exclusive. The pattern:
- The MetaModelActor subscribes to
ScoreUpdateand publishesMetaProbUpdateon the bus. - The RiskEngine rule consumes the latest
MetaProbUpdatefrom the Cache (or via aself.cache.signal(...)query) and uses it to scale or veto incoming order commands. - The CortanaStrategy does not read
MetaProbUpdatedirectly. It just submits orders at base size and lets the Risk layer apply the gate.
This gives transparent telemetry (the dashboard subscribes to
MetaProbUpdate for visualization) AND hard enforcement (orders cannot reach
the venue without passing through the gate).
EOD power hour
Per project_eod_power_hour.md, the last 15-30 minutes is a first-class
regime. Implement as:
- A
RegimeDetectorActorthat publishesRegimeUpdate(regime="POWER_HOUR")starting at 14:30 CT (set viaclock.set_time_alert). - A
CortanaStrategy.on_databranch that checks the cached regime when evaluating any entry trigger and adjusts thresholds accordingly. - An EOD-flatten
time_alertat 14:55 CT that callsself.market_exit()- reusing the platform’s market-exit semantics gives us the reduce-only-during-exit guarantee for free.
Position manager - Strategy, not Actor
TP/SL fallback, time-in-trade exits, and the dual-TP defense-in-depth
pattern (feedback_dual_tp_defense_in_depth.md) all touch orders. They live
inside CortanaStrategy.on_quote_tick and on_position_opened. Submitting a
bracket order at entry handles the broker-side TP/SL; the Strategy’s
on_quote_tick runs the software fallback that fires a reduce-only
market exit if the broker LMT fails to fill within tolerance.
What goes into on_save/on_load
Per the doc, on_save -> dict[str, bytes] is the mechanism for persisting
user state across restarts (backed by the Cache database). Cortana state
that needs to survive:
- Cooldown timers (
cooldown_state.pyequivalent). - Trigger history (which triggers fired in the last N minutes - for contradiction gates).
- Meta-prob smoothing buffer if any.
State that should NOT go in on_save because it is owned by the platform:
- Open orders / positions (Cache + ExecutionEngine handle these via reconciliation).
- Account balances (Account handles these).
See Also
- Nautilus Actors (parallel page - distinguishes Actor capabilities and idioms)
- Nautilus Concepts (architecture canon - Kernel, MessageBus, Cache, ExecutionEngine, RiskEngine, Portfolio)
- Nautilus Developer Guide (Cortana → Nautilus translation table; custom adapter sequence)
- Nautilus Spike Plan (Saturday 2026-05-09 evaluation - this page is reference for Step 4 and Step 5)
~/brain/concepts/nautilus-getting-started.md(install + smoke test)~/brain/concepts/nautilus-integrations.md(IBKR adapter detail)~/brain/concepts/nautilus-how-to.md(recipes)~/brain/concepts/nautilus-tutorials.md(learning path)
Timeline
- 2026-05-07 | Cody - Filed during pre-spike concept mastery sweep.