name: nautilus-trader
description: |
Authoritative development helper for the nautilus_trader framework — the core engine of
this project (The-Trader-Was-Replaced). Use this skill whenever the user is working with
nautilus_trader APIs, even if they don't name it explicitly: Actors, Strategies, the
message bus, the data engine, clocks/timers, bar/quote/trade data types, instruments,
BacktestEngine / BacktestNode / BacktestEngineConfig, TradingNode / LiveExecEngine,
NautilusKernel, ParquetDataCatalog, indicators, custom data, adapters, or anything in
python/engine/nautilus_*.py.
Also trigger on the precision-mode / catalog-parquet-schema seam (GH #34): "HIGH_PRECISION",
"PRECISION_BYTES", "FIXED_PRECISION", "standard vs high precision", "8-byte / 16-byte", "i64 / i128",
"fixed_size_binary", "PrecisionMismatch", "precision mismatch", "catalog precision", Price.from_raw /
Quantity.from_raw raw scaling, the on-disk catalog layout
data/<bar|trade_tick|quote_tick>/<identifier>/*.parquet, class_to_filename / filename_to_class,
parquet schema metadata (price_precision / size_precision), and the sdist HIGH_PRECISION=false
source rebuild (build.py Cargo feature) used to match a standard-precision shared catalog. Precision
mode is compiled into the wheel, so wheels differ per platform even at the same version — confirm with
nautilus_pyo3.PRECISION_BYTES, never the version string.
Also trigger on PyO3 in-process embedding of the Python engine (issue #64 Phase 2–4):
"InProcTransport", "PyO3", "pyo3", "Python::with_gil", "Py", "GIL strategy",
"embed Python", "in-proc call", "DataEngine を直接呼ぶ", "PyO3 経由", "in-process transport",
"BACKEND_TRANSPORT=inproc", "InprocLiveServer", "inproc_dispatch", "inproc_live_call",
"GrpcDataEngineServer を直接呼ぶ", "live RPC を inproc 化", "Phase 4", "_NullContext",
"live 系コマンドを inproc", "inproc_python_worker", "live_venue_id".
When working with the DataEngine in python/engine/core.py or the live facade
python/engine/inproc_server.py via PyO3 (not just nautilus_trader APIs), this skill
provides key context: DataEngine instantiation kwargs, replay method signatures,
TradingState.model_dump_json() round-trip to BackendTradingState, and GIL design
(dedicated Python thread + std::sync::mpsc bridge; GIL released between calls via
cmd_rx.recv_timeout outside Python::with_gil).
Phase 4 key facts: InprocLiveServer wraps GrpcDataEngineServer with token=""
(hmac.compare_digest("","")=True); _NullContext.abort() raises RuntimeError;
inproc_live_call closure takes &Bound<PyDict> and needs use pyo3::types::PyDictMethods
in scope; inproc_json_dumps serializes Python dict → JSON string → serde_json::Value
(necessary since return dicts have per-method shapes); inproc_poll_state now calls
live_server.get_state_json() (includes live price/depth cache).
PyO3 build env (Windows, this repo's backcast crate links pyo3 0.22): cargo build/test
needs a real Python interpreter on PATH — the Windows WindowsApps\python alias stub fails with
"no Python 3.x interpreter found", and the repo .venv is Python 3.14 which EXCEEDS pyo3 0.22's
max (3.13: "configured Python interpreter version (3.14) is newer than PyO3's maximum supported").
Build with $env:PYO3_PYTHON='<repo>\.venv\Scripts\python.exe' +
$env:PYO3_USE_ABI3_FORWARD_COMPATIBILITY='1' (stable-ABI forward-compat; fine for tests that don't
init Python). First pyo3 build links all of Bevy (~13 min); run it in background. Pure-Rust unit
tests in src/backend_transport.rs (#[cfg(test)] mod tests) still need this because the crate
links pyo3 even if the test never touches Python. Also: the #64-merged Cargo.lock was OUT OF SYNC
(pyo3 + its tree like indoc missing from backcast's deps; thiserror→2 unification) — a clean
build regenerates it, so cargo build --locked would fail until the lock is committed.
Also trigger on issue #68 BacktestEngine → GUI bridge vocabulary:
"NautilusBacktestRunner", "GuiBridgeActor", "RustBacktestSink", "start_nautilus_replay",
"push_bar", "push_run_complete", "BacktestEngine を GUI に繋ぐ", "bar をチャートに流す",
"msgbus.subscribe で bar を受け取る", "streaming=True", "engine.run(streaming=True)",
"Slice 1", "Slice 2" (issue #68 slices), "tracer bullet backtest".
Also trigger on related vocabulary: "msgbus", "ts_event",
"ts_init", "InstrumentId", "ClientId", "Venue", "BarSpec", "OrderFactory", "ExecAlgorithm",
"PositionEvent", "OrderEvent", "cache" in a trading sense, "Cython .pyx".
Also trigger on the kernel-on-background-loop / signal seam (live attach in
engine_controller.py): "signal only works in main thread", _setup_loop,
add_signal_handler, NautilusKernel(loop=...), "kernel on non-main thread",
"phase8-live-loop", building the kernel inside a coroutine on a daemon-thread asyncio loop
(omit loop= so it doesn't grab process signals — GH #36).
Also trigger on the execution-client / order-event seam (common to live venue adapters
in python/engine/live/*.py): LiveExecutionClient, _submit_order / _modify_order /
_cancel_order, generate_order_accepted / generate_order_canceled /
generate_order_expired / generate_order_filled / generate_order_updated /
generate_order_rejected / generate_order_modify_rejected, the OrderStatus FSM and its
allowed transitions (e.g. PENDING_UPDATE → ACCEPTED/CANCELED/EXPIRED/FILLED),
OrderModifyRejected, ModifyOrder / CancelOrder commands, TradeId, LiquiditySide,
VenueOrderId. These signatures are wide (e.g. generate_order_filled ~14 args) and easy to
misorder — read the mirror / .venv/.../nautilus_trader/execution/client.pyx and
model/orders/base.pyx for ground truth rather than guessing.
The full upstream source tree is mirrored at .claude/skills/nautilus_trader/src/ — use it
as ground truth instead of guessing API shapes from memory. The current branch
(sasa/Phase-6---Nautilus-Replay-Integration) is actively wiring nautilus data types into
the project's replay pipeline, so this skill is in heavy use.
DataEngine in python/engine/core.py or the live facade
python/engine/inproc_server.py via PyO3 (not just nautilus_trader APIs), this skill
provides key context: DataEngine instantiation kwargs, replay method signatures,
TradingState.model_dump_json() round-trip to BackendTradingState, and GIL design
(dedicated Python thread + std::sync::mpsc bridge; GIL released between calls via
cmd_rx.recv_timeout outside Python::with_gil).
Phase 4 key facts: InprocLiveServer wraps GrpcDataEngineServer with token=""
(hmac.compare_digest("","")=True); _NullContext.abort() raises RuntimeError;
inproc_live_call closure takes &Bound<PyDict> and needs use pyo3::types::PyDictMethods
in scope; inproc_json_dumps serializes Python dict → JSON string → serde_json::Value
(necessary since return dicts have per-method shapes); inproc_poll_state now calls
live_server.get_state_json() (includes live price/depth cache).
PyO3 build env (Windows, this repo's backcast crate links pyo3 0.22): cargo build/test
needs a real Python interpreter on PATH — the Windows WindowsApps\python alias stub fails with
"no Python 3.x interpreter found", and the repo .venv is Python 3.14 which EXCEEDS pyo3 0.22's
max (3.13: "configured Python interpreter version (3.14) is newer than PyO3's maximum supported").
Build with $env:PYO3_PYTHON='<repo>\.venv\Scripts\python.exe' +
$env:PYO3_USE_ABI3_FORWARD_COMPATIBILITY='1' (stable-ABI forward-compat; fine for tests that don't
init Python). First pyo3 build links all of Bevy (~13 min); run it in background. Pure-Rust unit
tests in src/backend_transport.rs (#[cfg(test)] mod tests) still need this because the crate
links pyo3 even if the test never touches Python. Also: the #64-merged Cargo.lock was OUT OF SYNC
(pyo3 + its tree like indoc missing from backcast's deps; thiserror→2 unification) — a clean
build regenerates it, so cargo build --locked would fail until the lock is committed.
Also trigger on issue #68 BacktestEngine → GUI bridge vocabulary:
"NautilusBacktestRunner", "GuiBridgeActor", "RustBacktestSink", "start_nautilus_replay",
"push_bar", "push_run_complete", "BacktestEngine を GUI に繋ぐ", "bar をチャートに流す",
"msgbus.subscribe で bar を受け取る", "streaming=True", "engine.run(streaming=True)",
"Slice 1", "Slice 2" (issue #68 slices), "tracer bullet backtest".
Also trigger on related vocabulary: "msgbus", "ts_event",
"ts_init", "InstrumentId", "ClientId", "Venue", "BarSpec", "OrderFactory", "ExecAlgorithm",
"PositionEvent", "OrderEvent", "cache" in a trading sense, "Cython .pyx".
Also trigger on the kernel-on-background-loop / signal seam (live attach in
engine_controller.py): "signal only works in main thread", _setup_loop,
add_signal_handler, NautilusKernel(loop=...), "kernel on non-main thread",
"phase8-live-loop", building the kernel inside a coroutine on a daemon-thread asyncio loop
(omit loop= so it doesn't grab process signals — GH #36).
Also trigger on the execution-client / order-event seam (common to live venue adapters
in python/engine/live/*.py): LiveExecutionClient, _submit_order / _modify_order /
_cancel_order, generate_order_accepted / generate_order_canceled /
generate_order_expired / generate_order_filled / generate_order_updated /
generate_order_rejected / generate_order_modify_rejected, the OrderStatus FSM and its
allowed transitions (e.g. PENDING_UPDATE → ACCEPTED/CANCELED/EXPIRED/FILLED),
OrderModifyRejected, ModifyOrder / CancelOrder commands, TradeId, LiquiditySide,
VenueOrderId. These signatures are wide (e.g. generate_order_filled ~14 args) and easy to
misorder — read the mirror / .venv/.../nautilus_trader/execution/client.pyx and
model/orders/base.pyx for ground truth rather than guessing.
The full upstream source tree is mirrored at .claude/skills/nautilus_trader/src/ — use it
as ground truth instead of guessing API shapes from memory. The current branch
(sasa/Phase-6---Nautilus-Replay-Integration) is actively wiring nautilus data types into
the project's replay pipeline, so this skill is in heavy use.nautilus_trader development helper
nautilus_trader is a Rust-native, event-driven trading engine with a Python control plane. Same execution semantics across backtest, sandbox, and live — strategies move between contexts without code changes. This skill exists because the API surface is large, Cython-heavy, and easy to misremember, and because this project is mid-integration on Phase 6.
First rule: read the source, don't guess
The full upstream codebase is checked into .claude/skills/nautilus_trader/src/. Treat it
as ground truth. Before claiming an API exists or has a certain signature, grep the source.
Common misses without doing this:
- Confusing
nautilus_trader.common.actor.Actorwithnautilus_trader.trading.strategy.Strategy(Strategy ⊂ Actor; Strategy adds order/position management). - Forgetting that most domain types are Cython (
.pyx/.pxd) so signatures live in.pxdfiles and editor go-to-definition can mislead. - Using stale event names (e.g.
OrderFilledis innautilus_trader.model.events, notnautilus_trader.execution.events).
Always check the actual file. The relevant subtrees:
| Concern | Path under .claude/skills/nautilus_trader/src/ |
|---|---|
| Strategies, Trader, Controller | nautilus_trader/trading/ |
| Actor, Component, Clock, MessageBus interfaces | nautilus_trader/common/ |
| Domain model (bars, ticks, orders, events) | nautilus_trader/model/ |
| Data engine, aggregation, custom data | nautilus_trader/data/ |
| Execution engine, order pipeline | nautilus_trader/execution/ |
| Cache (state store) | nautilus_trader/cache/ |
| Backtest engine + node | nautilus_trader/backtest/ |
| Live node, async loop, reconciliation | nautilus_trader/live/ |
| NautilusKernel (shared system bootstrap) | nautilus_trader/system/kernel.py |
| Persistence / ParquetDataCatalog | nautilus_trader/persistence/ |
| Adapters (Binance, IB, Databento, …) | nautilus_trader/adapters/ |
| Conceptual docs (Markdown) | docs/concepts/ |
| API reference (Markdown) | docs/api_reference/ |
| Runnable examples | examples/backtest/, examples/live/ |
When you need a working pattern (custom data, msgbus pub/sub, clock timer, bar aggregation,
indicator, etc.), the examples/backtest/example_01..11_* folders are the fastest reference.
Read one before designing from scratch.
Project context: where nautilus_trader lives in this repo
This project doesn't embed a full BacktestEngine yet. Instead it has its own
deterministic replay pipeline (python/engine/) and is incrementally adopting nautilus types
as the canonical data representation:
python/engine/nautilus_adapter.py— converts nautilusBar/TradeTick→ projectKlineUpdate/TradeUpdate/ReplayTimeUpdated.python/engine/nautilus_runner.py— iterates anIterable[Bar|TradeTick]through the adapter into aReplayEventSink. Critical invariant: always emitReplayTimeUpdatedbefore the data event for each tick, so the reducer sees time advance before state mutates.python/engine/reducer.py— the in-process state reducer. Discards stale-timestamp events.python/engine/core.py—DataEngine(project-level, not the nautilusDataEngine). Owns aReducerState, primes from aBaseReplayProvider, and exposesapply_replay_event.
When extending Phase 6 work:
- Don't shadow nautilus names. This project has its own
DataEngineinpython/engine/core.py. If you need the nautilus one, import it asfrom nautilus_trader.data.engine import DataEngine as NautilusDataEngineand say so. - Preserve the
ts_eventordering invariant. Anything that produces replay events for the project reducer must emitReplayTimeUpdated(ts_event_ns)before the correspondingKlineUpdate/TradeUpdate. This is enforced byNautilusReplayRunner; new entry points must replicate it. - Nanoseconds vs milliseconds. Nautilus uses
uint64nanoseconds end-to-end (ts_event,ts_init). This project's reducer is milliseconds (timestamp_ms). The adapter is the one place this conversion happens — keep it there.
Common tasks: where to look first
When the user asks for one of these, open the listed reference before writing code.
| Task | Open this first |
|---|---|
| Build a new strategy | docs/concepts/strategies.md + any examples/backtest/fx_ema_cross_*.py |
| Build an Actor (no orders, just data/signals) | docs/concepts/actors.md + examples/backtest/example_10_messaging_with_actor_data/ |
| Publish/subscribe over the message bus | docs/concepts/message_bus.md + examples/backtest/example_09_messaging_with_msgbus/ |
| Use a Clock / Timer | examples/backtest/example_02_use_clock_timer/ |
| Aggregate bars from ticks | examples/backtest/example_03_bar_aggregation/ |
| Load data from a custom CSV | examples/backtest/example_01_load_bars_from_custom_csv/ |
| Use the Cache | examples/backtest/example_06_using_cache/ |
| Use Portfolio | examples/backtest/example_05_using_portfolio/ |
| Indicators | examples/backtest/example_07_using_indicators/ + nautilus_trader/indicators/ |
| Custom data type | docs/concepts/custom_data.md |
| Stand up a BacktestEngine directly | docs/getting_started/backtest_low_level.py |
| Use BacktestNode + ParquetDataCatalog | docs/getting_started/backtest_high_level.py |
| Wire a live venue | examples/live/<venue>/ and docs/integrations/ |
| Add a new adapter | docs/concepts/adapters.md + an existing small adapter (e.g. adapters/sandbox) |
For deeper background on a single subsystem, the docs/concepts/*.md files (architecture,
data, events, execution, orders, positions, portfolio, cache, message_bus, logging, dst) are
short and high-signal. Prefer them over the API reference for understanding; use the API
reference (docs/api_reference/) only after you know what you're looking for.
API gotchas worth internalizing
These bite people repeatedly. Worth holding in working memory rather than rediscovering:
- There is NO
StrategyEngine. Nautilus hasDataEngine,ExecutionEngine, andRiskEngine— but strategies are managed by theTrader(nautilus_trader/trading/trader.py, used by bothBacktestEngineand live), and the live system host isTradingNode(nautilus_trader/live/node.py, wrapsNautilusKernel+Trader+ the Live*Engines). You add a strategy withengine.add_strategy(...)/trader.add_strategy(...), not by "enabling a StrategyEngine". Plan/design docs in this repo keep inventing aStrategyEngine— flag it on sight and replace withTrader/TradingNode. - A strategy's
self.clock/self.cache/self.msgbusare injected atregister(), NOT viaStrategyConfig. Seecommon/actor.pyx(self.cache = None # Initialized when registered, set inregister()).StrategyConfigcarries instrument/venue/params only. This is why the same strategy runs unchanged across backtest/live — the engine supplies the clock/data/exec, the strategy never branches on mode. Don't describe portability as "inject clock/data_engine via config" — that's not how it works. - Bar aggregation lives in
data/aggregation.pyx(BarBuilder,BarAggregator,TimeBarAggregator/TickBarAggregatorwithhandle_trade_tick(TradeTick)).BarType's 5th segment (INTERNAL/EXTERNAL) decides whether the engine aggregates from ticks or trusts an external bar feed. Note: this project also has its ownTickBarAggregatorinpython/engine/live/aggregator.pythat emits the project'sKlineUpdatedataclass for the UI — that one is NOT a NautilusBarBuilderwrapper; don't conflate the two. - Cython types use class-only construction. You generally can't subclass
Bar,TradeTick,Quote Tick,OrderFilled, etc. and add attributes. If you need extra state, store it in the cache or attach via msgbus topics. (To tag the origin of an order, use itsStrategyIdor ordertags, not a bolted-on field.) ts_eventvsts_init.ts_eventis when the event happened in the market;ts_initis when nautilus constructed the object. Replay/ordering logic must key onts_event. Logging may wantts_init.- Strategy
on_startruns before any data flows. Subscriptions belong inon_start, not__init__— the message bus and data engine aren't fully wired in the constructor. - When loading a strategy module from a different file than its identity (this repo runs
a cache
.pybut ADR-0021 makesmodule.__file__the original Source Path), setmodule.__file__BETWEENimportlib.util.module_from_spec(spec)andexec_module(module), not after.exec_moduleruns the module body; module-levelPath(__file__).parent / ...resolves at import time, so an after-exec_moduleoverride is too late and import-time artifact resolution silently uses the cache path (#254 codex High).strategy_loader.load(..., original_path=)is the single contract point (python/engine/strategy_runtime/strategy_loader.py). - An
on_startexception is NOT swallowed by Nautilus — it propagates.Component.start()logs and reraises (common/component.pyx, "logged and reraised");Trader.start()/NautilusKernel.start()don't catch. So a strategy thatraises inon_start(fail-loud on missing artifacts) surfaces as a real start failure (in this repo:strategy_host.start_run→LiveStrategyHostError→start_live_strategyreturnssuccess=Falsewitherror_message=str(exc.__cause__); RUNNING is never emitted). A silentreturninstead leaves the run looking RUNNING — prefer raise (#254). self.clock.set_time_alert/set_timerare how strategies schedule callbacks; do not usetime.sleep,asyncio.sleep, or wall-clock APIs inside a strategy. That breaks backtest determinism.- Order submission is async even in backtest.
self.submit_order(order)enqueues; the matching engine processes it on the next event. Don't read fill state in the same callback. - Logging. Use
self.log.info(...)etc. from inside an Actor/Strategy; neverprintorloggingdirectly — those bypass the structured log and the in-memory log buffer used by tests.- There is NO Python log sink — you cannot tap
self.log.*into Python.Logger.{info, warning,error}route straight into the Rust subsystem (nautilus_pyo3.logger_log),init_loggingonly writes stdout/file (no callback param), the strategy's_logiscdef readonly, andLoggeris a non-monkeypatchable extension type. So if you need to forward strategy log lines to an external consumer (a UI, a gRPC stream), do not try to interceptself.log.*— instead publish on a msgbus topic (self.msgbus.publish("strategy.log.{id}", record)) via a small helper that also mirrors toself.log.<level>, and subscribe to that topic on the kernel side (kernel.msgbus.subscribe). This is the same proven seam as bridgingevents.order.{strategy_id}. (Phase 10 §570 /python/engine/live/strategy_log.py.)
- There is NO Python log sink — you cannot tap
- Identifiers are value types.
InstrumentId,ClientId,Venue,StrategyId,ClientOrderId,PositionId— construct from strings with the factory (InstrumentId.from_str("AAPL.NASDAQ")), don't pass raw strings to APIs expecting them. - Bar specifications.
BarType.from_str("AAPL.NASDAQ-1-MINUTE-LAST-EXTERNAL")— the fifth segment (EXTERNAL/INTERNAL) decides whether nautilus aggregates the bars from ticks or trusts an external feed. Mismatch is a common silent bug.
Live execution stack gotchas (custom LiveExecutionClient / live NautilusKernel)
Hit while building Phase 10 Step 4 (python/engine/live/nautilus_exec_client.py +
engine_controller.py, nautilus 1.226.0). These bite anyone wrapping a bespoke venue
adapter as a live client:
- Minimal live stack =
NautilusKerneldirectly, noTradingNodeneeded.NautilusKernel(name=..., config=TradingNodeConfig(trader_id, logging, exec_engine=LiveExecEngineConfig(), risk_engine=LiveRiskEngineConfig(...), data_engine=LiveDataEngineConfig()), loop=loop)gives youkernel.{trader,exec_engine,risk_engine,data_engine,cache,portfolio,msgbus,clock}. Thencache.add_instrument(...)→exec_engine.register_client(client)→trader.add_strategy(...)→kernel.start()/await kernel.stop_async().TradingNodeonly adds client-factory wiring + signal handling on top. - Do NOT pass
loop=when building the kernel on a non-main thread (GH #36). When the kernel is constructed inside a coroutine that runs on a background-thread asyncio loop (our live loop = server_grpc'sphase8-live-loopdaemon thread; tests'_bg_loop), passingloop=loopmakesNautilusKernel.__init__call_setup_loop()→signal.signal(SIGINT, SIG_DFL)+loop.add_signal_handler(...), both of which only work on the main thread. Python 3.14 raisesValueError: signal only works in main thread(older Pythons silently tolerated it). Build it asNautilusKernel(name=..., config=cfg)(noloop=): the kernel binds viaasyncio.get_running_loop()(same loop, since the coroutine runs on it) and skips the wholeif loop is not None:executor+signal block. Safe because the controller uses synckernel.start(), which never calls_register_executor()(onlystart_async()readsself._executor). Theloop=loopform is only correct when you own the main thread (a realTradingNode). This affected production, not just tests. OrderDeniedis only a valid transition fromINITIALIZED. A custom pre-trade rail inLiveExecutionClient._submit_ordermust callgenerate_order_denied(...)beforegenerate_order_submitted(...). Deny after submit →SUBMITTED → DENIEDis rejected and the order sticks at SUBMITTED. (NativeLiveRiskEngineConfigrails —max_notional_per_orderdict,max_order_submit_rate="N/HH:MM:SS"— deny before the client is reached, so they're fine.)- A live client must set its
account_idbeforegenerate_account_state. In_connect:if self.account_id is None: self._set_account_id(AccountId(f"{venue}-001")), thengenerate_account_state(balances=[AccountBalance(total, locked, free)], margins=[], reported=True, ts_event=...). Without an account, RiskEngine free-balance checks deny orders. - Live forbids
LoggingConfig(bypass_logging=True)(InvalidConfiguration). UseLoggingConfig(log_level="ERROR", log_level_file="OFF")to avoid littering*.login cwd. The live kernel initializes Nautilus's process-global logger once, which then defeats a backtest'sbypass_logging=Truein the same process (combined test runs leak backtest dispose logs; production unaffected since replay/live are separate processes). - Live-loop self-deadlock. Code on the live asyncio-loop thread (e.g. an account-sync
callback) must NOT call anything that does
run_coroutine_threadsafe(coro, loop).result()targeting that same loop (e.g. tearing down viakernel.stop_async()). Offload to athreading.Thread.- Offloading to a thread is necessary but NOT sufficient (Phase 10 Step 4 review). If the
offloaded worker holds a lock across its blocking
.result(), and a live-loop callback also acquires that lock, the deadlock just moves: live loop blocks on the lock → can't run the coro → worker waits until.result(timeout=…)fires (leaving a runaway kernel un-torn-down). Invariant: no live-loop callback may acquire a lock that is held across a blocking round-trip to that loop. Fix: give the live-loop-reachable state its own lightweight lock, never held across a round-trip; keep the heavy lifecycle lock (held during start/stop/teardown) off the live-loop path entirely.
- Offloading to a thread is necessary but NOT sufficient (Phase 10 Step 4 review). If the
offloaded worker holds a lock across its blocking
- Cancel a strategy's orders by
strategy.id, not by an instrument attribute. To cancel a run's in-flight orders, query the cache (cache.orders_open(strategy_id=s.id)+cache.orders_inflight(strategy_id=s.id)) andstrategy.cancel_order(o)each — don't rely onstrategy.instrument_id(the baseStrategyhas no such attribute; kwargs/config strategies store it privately, so an attribute-based cancel silently no-ops).cancel_all_ordersrequires anInstrumentIdand also sweeps emulated/exec-algo orders if you use those.
Live data client + INTERNAL bar aggregation (feeding on_bar from venue ticks)
Hit while building Phase 10 Step 8 (python/engine/live/nautilus_data_client.py +
bar_supply.py + engine_controller.py). To make a strategy's on_bar fire in live mode you
must supply ticks to the engine — there is no automatic feed:
- A strategy subscribing an
INTERNALBarTypeneeds a registeredMarketDataClientfor that venue, oron_barnever fires — silently. When the strategy callsself.subscribe_bars(<...-INTERNAL>),DataEngine._handle_subscribe_bars→_start_bar_aggregatorcreates aTimeBarAggregator(handler=self.process)and issues aSubscribeTradeTicks(LAST price type) /SubscribeQuoteTicksto the venue's client (data/engine.pyx:_subscribe_bar_aggregator). If no data client is registered for that venue, the subscribe command is dropped with only a log line and the aggregator is never wired. So you mustkernel.data_engine.register_client(your_client)beforekernel.start()(the strategy'son_startruns during start and subscribes there). - A minimal custom
LiveMarketDataClientis almost all no-ops. Implement_connect/_disconnect(no-op if a sibling session already owns the venue connection), and the_subscribe_trade_ticks/_unsubscribe_trade_ticks(and quote variants) coroutines as no-ops — the basesubscribe_*sync wrappers already record the subscription set, and INTERNAL aggregation is driven engine-side, not by the client. The client only needs a way to push ticks in. - Push ticks via
self._handle_data(tick).MarketDataClient._handle_datais justself._msgbus.send("DataEngine.process", data)(data/client.pyx) — it does not depend on connection state or on the trade-tick subscription having been processed. As long as the kernel is started and the strategy subscribed the bar (so the aggregator is on the trades topic), feeding aTradeTickflows:_handle_data→ engine routes todata.trades.{id}→aggregator.handle_trade_tick→ builds →on_bar. Call it on the live-loop thread. - Time bars close on the
LiveClocktimer, not on tickts_event. Feeding ticks only updates the in-progress builder; the bar is emitted when the aggregator's clock timer fires. So a deterministic full-path unit test of a 1-MINUTE bar can't observe a close without waiting wall-clock — use a1-SECOND-...-INTERNALbar + a ~2s real settle, and assert on the bar withvolume>0(theDataEngineConfig.time_bars_build_with_no_updates=Truedefault also emits empty carry-forward bars). Pure OHLCV-aggregation correctness is better pinned with a standaloneTimeBarAggregator+TestClock(advance +event.handle()), independent of the engine.
How to research an API question
When the user asks "how do I do X with nautilus", follow this in order:
- Grep the source.
Grep -r "<symbol or phrase>" .claude/skills/nautilus_trader/src/nautilus_trader/— you'll usually land on either the implementation or a docstring with a working example. - Check
examples/. If grep didn't yield a runnable pattern, scanexamples/backtest//examples/live/for the closest analogue and adapt. - Read the matching
docs/concepts/<topic>.md. Concept docs are short and explain why the API is shaped the way it is — important for not fighting the framework. - Only then suggest code. State which source file you confirmed against
(
nautilus_trader/.../foo.pyx:LNN) so the user can verify.
Skipping step 1 produces plausible-but-wrong API calls — Cython signatures and event names in particular drift between versions, and this skill's mirror reflects exactly the version this project depends on.
When working on Phase 6 replay integration
Specific to the current branch (sasa/Phase-6---Nautilus-Replay-Integration):
- Tests covering the adapter/runner live in
python/tests/test_nautilus_adapter_engine.pyandpython/tests/test_nautilus_runner.py. Run these first whenever editingpython/engine/nautilus_*.py— they pin theReplayTimeUpdated → data-eventinvariant. - The adapter intentionally does not depend on a running
NautilusKernelor any nautilus engine — it operates on pure data objects (Bar,TradeTick). Keep it that way; if a conversion needs context, push the context into the call site, not into a kernel reference. - The eventual goal (later phases) is to feed the project reducer from a real
BacktestDataEngineor aLiveDataEngine, replacing the bespokeJQuants*ReplayProvider. Designs should leave room for that without forcing it now.
Output expectations
When answering nautilus_trader questions:
- Cite the file you confirmed the API against —
path/to/file.pyx:line— so the user can click through. Don't quote large blocks; a precise pointer is more useful. - Prefer the smallest working snippet over a full strategy class.
- If two APIs could plausibly satisfy the request (e.g. Actor vs Strategy, msgbus publish vs cache write), name the tradeoff in one sentence and let the user choose, rather than silently picking.
- If the user is mid-Phase-6 work, frame answers in terms of
python/engine/integration points, not standalone nautilus examples.