How-To - Configure a Live Trading Node
The Nautilus
how_to/configure_live_trading/page is the operational assembly guide forTradingNodeConfig- the top-level config struct that wires Cache, MessageBus, DataEngine, RiskEngine, ExecEngine, Portfolio, and per-venuedata_clients+exec_clientsinto a single live runtime. The page commits to four hard constraints up front: (1) never run a liveTradingNodein a Jupyter notebook (asyncio conflict, no graceful shutdown), (2) oneTradingNodeper process (global singleton state - multi-tenant means multi-process), (3) never block the event loop in user code (strategy callbacks must return quickly; offload heavy work to executors), (4) Windows SIGTERM is unsupported (use `try/except KeyboardInterrupt + node.stop()
- node.dispose()
). Most of the page isLiveExecEngineConfigreference: reconciliation knobs, in-flight + open-order continuous loops, retry coordination, position-consistency checks, memory-purge loops, and queue management. The page does **not** documentDockerizedIBGatewayConfig, the TWS UTC checkbox, or per-tenant Redis namespacing - those live innautilus-ib.mdandnautilus-configuration.md. For Cortana MK3 this page is the Saturday-morning Step-1-through-Step-3 reference: build theTradingNodeConfig, register IBKR client factories, callnode.build()thennode.run(), paper accountDUP696099` → Dockerized IB Gateway port 4002 → reconciliation against IBKR truth on every boot.
This page specializes Nautilus Live Trading
(reconciliation semantics, FSM lifecycle, shutdown contract) and
Nautilus Configuration (config object
hierarchy, forbid_unknown_fields, multi-tenant patterns) by being the
copy-paste-ready assembly recipe. It’s the parallel of
Nautilus IB (which covers the IBKR adapter wire
details + Dockerized Gateway + UTC preflight) - this page tells you how
to plug those configs into a TradingNode; that page tells you what
goes in the IBKR-specific config.
Why this page exists (vs. siblings)
The 2026-05-09 spike Step 1 (“install + first run”) and Step 4 (“paper
IBKR connection via TradingNode”) both depend on knowing which knobs
are mandatory vs optional, which defaults are safe vs dangerous, and
which reconciliation cadences match Cortana’s 0DTE workload. The
how_to/configure_live_trading/ doc is the canonical answer; this
brain page distills it into one place plus adds Cortana-specific
mappings (account ID, port, paper mode, single-tenant for spike,
flag-for-M4 multi-tenant).
Hard constraints (verbatim from the doc)
1. No Jupyter for live trading
“Do not run live trading nodes in Jupyter notebooks. Event loop conflicts and operational risks make them unsuitable:
- Jupyter runs its own asyncio event loop, which conflicts with
TradingNode’s event loop.- Workarounds like
nest_asyncioare not production-grade.- Cells can run out of order, kernels can crash, and state can disappear.
- Notebooks lack the logging, monitoring, and graceful shutdown needed for production trading.”
“Use Jupyter for backtesting, analysis, and experimentation. For live trading, run nodes as standalone Python scripts or services.”
For Cortana MK3 this aligns with current MK2 practice: live runs are
launchd-managed Python processes, not notebooks. The spike’s Step 4
“paper IBKR connect” must be a python -m cortana_mk3.live script,
not a notebook cell.
2. One TradingNode per process
“Running multiple
TradingNodeinstances concurrently in the same process is not supported due to global singleton state. Add multiple strategies to a single node, or run additional nodes in separate processes for parallel execution.”
This is the structural fact that forces process-per-tenant for M4-M5 multi-tenant. Confirmed in nautilus-configuration.md and nautilus-cache.md. For the spike (single-tenant Cody), one process is fine; for M4 onward, the spawning service materializes one Python process per tenant.
3. Do not block the event loop
“User code on the event loop thread (strategy callbacks, actor handlers,
on_eventmethods) must return quickly. This applies to both Python and Rust. Blocking operations like model inference, heavy calculations, or synchronous I/O cause missed fills, stale data, and delayed order submissions. Offload long-running work to an executor or a separate thread/process.”
For Cortana: meta-gate inference (LightGBM predict_proba) runs
~300μs and is fine inline. Anything heavier (NN inference, REST
fallback to UW, file I/O) must use a background executor with the
result delivered back via publish_data so it lands in the cache via
the proper path.
4. Windows signal handling
“Windows: asyncio event loops do not implement
loop.add_signal_handler. As a result,TradingNodedoes not receive OS signals via asyncio on Windows. Use Ctrl+C (SIGINT) handling or programmatic shutdown; SIGTERM parity is not expected on Windows.”
Cortana runs on macOS / Linux (launchd / systemd); Windows is N/A. Note for M5 if any customer requests Windows deployment: shutdown contract is degraded; recommend WSL2 or a Linux container instead.
TradingNodeConfig - the assembly skeleton
Verbatim minimal example from the doc:
from nautilus_trader.config import TradingNodeConfig
config = TradingNodeConfig(
trader_id="MyTrader-001",
# Component configurations
cache=CacheConfig(),
message_bus=MessageBusConfig(),
data_engine=LiveDataEngineConfig(),
risk_engine=LiveRiskEngineConfig(),
exec_engine=LiveExecEngineConfig(),
portfolio=PortfolioConfig(),
# Client configurations
data_clients={
"BINANCE": BinanceDataClientConfig(),
},
exec_clients={
"BINANCE": BinanceExecClientConfig(),
},
)Core configuration parameters (verbatim table)
| Setting | Default | Description |
|---|---|---|
trader_id | ”TRADER-001” | Unique trader identifier (name‑tag format). |
instance_id | None | Optional unique instance identifier. |
timeout_connection | 30.0 | Connection timeout in seconds. |
timeout_reconciliation | 10.0 | Reconciliation timeout in seconds. |
timeout_portfolio | 10.0 | Portfolio initialization timeout. |
timeout_disconnection | 10.0 | Disconnection timeout. |
timeout_post_stop | 5.0 | Post‑stop cleanup timeout. |
Cortana implications:
trader_id="CORTANA-PAPER"(thenCORTANA-LIVElater, thenCORTANA-{tenant_id}at M4). Stable across restarts.instance_id=Nonefor the spike (auto-UUID); pin to a stable value once Redis is wired so cache keys survive restarts.timeout_connection=30.0- IBKR Gateway boot can take 20-30s on cold start (Docker pull + auth); the default is correct.timeout_reconciliation=10.0- IBKR mass-status reports for a paper account with <50 orders should complete in 2-5s. Bump to 30.0 if Saturday spike sees timeouts.
Sub-config: Cache database
Verbatim:
from nautilus_trader.config import CacheConfig
from nautilus_trader.config import DatabaseConfig
cache_config = CacheConfig(
database=DatabaseConfig(
host="localhost",
port=6379,
username="nautilus",
password="pass",
connection_timeout=2,
response_timeout=2,
),
encoding="msgpack", # or "json"
timestamps_as_iso8601=True,
buffer_interval_ms=100,
flush_on_start=False,
)For Cortana spike: leave database=None (in-memory only). Redis
ops shape is M3 work. Confirmed safe per
nautilus-cache.md: “Redis recommended for live”
but not required to demonstrate the architecture.
Sub-config: MessageBus
Verbatim:
from nautilus_trader.config import MessageBusConfig
from nautilus_trader.config import DatabaseConfig
message_bus_config = MessageBusConfig(
database=DatabaseConfig(
connection_timeout=2,
response_timeout=2,
),
timestamps_as_iso8601=True,
use_instance_id=False,
types_filter=[QuoteTick, TradeTick], # Filter specific message types
stream_per_topic=False,
autotrim_mins=30,
heartbeat_interval_secs=1,
)For Cortana spike: omit MessageBusConfig entirely (in-process bus
default). External Redis Streams are M4 multi-tenant work.
use_instance_id=False is the spike default; flip to True at M4
along with streams_prefix=f"tenant_{tenant.id}".
Multi-venue configuration
Verbatim doc example (Binance):
config = TradingNodeConfig(
trader_id="MultiVenue-001",
data_clients={
"BINANCE_SPOT": BinanceDataClientConfig(
account_type=BinanceAccountType.SPOT,
environment=BinanceEnvironment.LIVE,
),
"BINANCE_FUTURES": BinanceDataClientConfig(
account_type=BinanceAccountType.USDT_FUTURES,
environment=BinanceEnvironment.LIVE,
),
},
exec_clients={
"BINANCE_SPOT": BinanceExecClientConfig(...),
"BINANCE_FUTURES": BinanceExecClientConfig(...),
},
)For Cortana: single venue ("IB") for the spike. Multi-venue would
be relevant if M3+ adds e.g. UW as a separate DataClient (for
historical backfill of the score-feature stream); spike stays
IBKR-only.
LiveExecEngineConfig - the bulk of the doc
This is the most operationally important section. The doc breaks it into Reconciliation, Order Filtering, Continuous Reconciliation, Retry Coordination, Additional Options, Memory Management, Queue Management.
Reconciliation (verbatim table)
| Setting | Default | Description |
|---|---|---|
reconciliation | True | Activate reconciliation at startup to align internal state with the venue. |
reconciliation_lookback_mins | None | How far back (minutes) to request past events for reconciling uncached state. |
reconciliation_instrument_ids | None | Include list of instrument IDs to reconcile. |
filtered_client_order_ids | None | Client order IDs to skip during reconciliation (for venue‑side duplicates). |
Cortana posture: leave all four at default.
reconciliation=True- never disable. This is the structural fix to the 2026-05-06 power-outage state divergence.reconciliation_lookback_mins=None- request maximum venue history. Confirmed correct in nautilus-live.md.reconciliation_instrument_ids=None- reconcile all (Cortana only trades SPY 0DTE; restricting buys nothing).filtered_client_order_ids=None- only set if a known-bad order ID collides with venue history.
Order filtering (verbatim table)
| Setting | Default | Description |
|---|---|---|
filter_unclaimed_external_orders | False | Drop unclaimed external orders so they do not affect the strategy. |
filter_position_reports | False | Drop position status reports. Useful when multiple nodes trade one account. |
Order tagging behavior (verbatim note):
“Reconciliation tags orders by origin:
VENUEtag: external orders discovered at the venue (placed outside this system).RECONCILIATIONtag: synthetic orders generated to align position discrepancies.When
filter_unclaimed_external_ordersis enabled, onlyVENUE- tagged orders are filtered.RECONCILIATION-tagged orders are never filtered, so position alignment always succeeds.”
Cortana posture: leave both at default False for spike. At M5
(multi-tenant against shared IBKR account would never happen - each
tenant has their own account - so filter_position_reports=False
remains correct).
Continuous reconciliation - in-flight timeout resolution (verbatim)
A background loop starts after startup reconciliation completes. The key tables verbatim:
In-flight order timeout resolution (venue does not respond after max retries):
| Current status | Resolved to | Rationale |
|---|---|---|
SUBMITTED | REJECTED | No confirmation received from venue. |
PENDING_UPDATE | CANCELED | Modification remains unacknowledged. |
PENDING_CANCEL | CANCELED | Venue never confirmed the cancellation. |
Order consistency checks (when cache state differs from venue state):
| Cache status | Venue status | Resolution | Rationale |
|---|---|---|---|
SUBMITTED | Not found | REJECTED | Order never confirmed by venue (e.g., lost during network error). |
ACCEPTED | Not found | REJECTED | Order doesn’t exist at venue, likely was never successfully placed. |
ACCEPTED | CANCELED | CANCELED | Venue canceled the order (user action or venue‑initiated). |
ACCEPTED | EXPIRED | EXPIRED | Order reached GTD expiration at venue. |
ACCEPTED | REJECTED | REJECTED | Venue rejected after initial acceptance (rare but possible). |
PARTIALLY_FILLED | CANCELED | CANCELED | Order canceled at venue with fills preserved. |
PARTIALLY_FILLED | Not found | CANCELED | Order doesn’t exist but had fills (reconciles fill history). |
Reconciliation caveats (verbatim):
“‘Not found’ resolutions only apply in full-history mode (
open_check_open_only=False). Open-only mode (the default) skips these checks because venue ‘open orders’ endpoints exclude closed orders by design, making it impossible to distinguish missing orders from recently closed ones.”“Recent order protection: the engine skips reconciliation for orders whose last event falls within the
open_check_threshold_mswindow (default 5s). This prevents false positives from race conditions where the venue is still processing.”“Targeted query safeguard: before marking an order
REJECTEDorCANCELEDwhen ‘not found’, the engine issues a single-order query to the venue. This catches false negatives from bulk query limitations or timing delays.”“
FILLEDorders that are ‘not found’ at the venue are silently ignored. Venues commonly drop completed orders from their query results.”
This table is the structural fix to MK2’s “alert without action”
class (project_pm_ibkr_exit_invariant). A SUBMITTED order that
never gets ack’d no longer sits indefinitely; after retries, it
resolves to REJECTED and on_order_rejected fires.
Retry coordination (verbatim)
“The inflight loop and open-order loop share a single retry counter (
_recon_check_retries), bounded byinflight_check_retriesandopen_check_missing_retriesrespectively. The stricter limit wins, and avoids duplicate venue queries for the same order state.”“When the open-order loop exhausts retries, the engine issues one targeted
GenerateOrderStatusReportprobe before applying a terminal state. If the venue returns the order, reconciliation proceeds and the retry counter resets.”“Single-order query protection: the engine caps single-order queries per cycle via
max_single_order_queries_per_cycle(default: 10). Remaining orders are deferred to the next cycle. A configurable delay (single_order_query_delay_ms, default: 100ms) spaces out consecutive queries to avoid rate limits.”
Continuous reconciliation knobs (verbatim table)
| Setting | Default | Description |
|---|---|---|
inflight_check_interval_ms | 2,000 ms | How often to check in‑flight order status. Set to 0 to disable. |
inflight_check_threshold_ms | 5,000 ms | Time before an in‑flight order triggers a venue status check. Lower if colocated. |
inflight_check_retries | 5 retries | Retry attempts to verify an in‑flight order with the venue. |
open_check_interval_secs | None | How often (seconds) to check open orders at the venue. None or 0.0 disables. Recommended: 5-10s. |
open_check_open_only | True | When true, query only open orders; when false, fetch full history (resource‑intensive). |
open_check_lookback_mins | 60 min | Lookback window (minutes) for order status polling. Only orders modified within this window. |
open_check_threshold_ms | 5,000 ms | Minimum time since last cached event before acting on venue discrepancies. |
open_check_missing_retries | 5 retries | Max retries before resolving an order open in cache but not found at venue. |
max_single_order_queries_per_cycle | 10 | Cap on single‑order queries per cycle. Prevents rate‑limit exhaustion. |
single_order_query_delay_ms | 100 ms | Delay (ms) between single‑order queries to avoid rate limits. |
reconciliation_startup_delay_secs | 10.0 s | Delay (seconds) after startup reconciliation before continuous checks begin. |
own_books_audit_interval_secs | None | Interval (seconds) between auditing own order books against public books. |
position_check_interval_secs | None | Interval (seconds) between position consistency checks. None disables. Recommended: 30-60s. |
position_check_lookback_mins | 60 min | Lookback window (minutes) for querying fill reports on position discrepancy. |
position_check_threshold_ms | 5,000 ms | Minimum time since last local activity before acting on position discrepancies. |
position_check_retries | 3 retries | Max attempts per instrument before the engine stops retrying that discrepancy. |
Hard warnings (verbatim):
“
open_check_lookback_mins: do not reduce below 60 minutes. A short window triggers false ‘missing order’ resolutions because orders fall outside the query range.”“
reconciliation_startup_delay_secs: do not reduce below 10 seconds in production. The delay lets the system stabilize after startup reconciliation before continuous checks begin.”
Cortana posture for spike:
inflight_check_interval_ms=2000(default) - fine for 0DTE.inflight_check_threshold_ms=5000(default) - IBKR Gateway over Docker localhost is sub-100ms; 5s is generous, leave default.open_check_interval_secs=10(set explicitly; defaultNonedisables this loop) - Cortana NEEDS this active to catch silent drift between cache and IBKR.position_check_interval_secs=60(set explicitly) - every minute, reconcile position state against IBKR. This is the explicit defense-in-depth forproject_pm_ibkr_exit_invariant.- All other lookbacks/retries: default.
Additional options (verbatim)
| Setting | Default | Description |
|---|---|---|
allow_overfills | False | Allow fills exceeding order quantity (logs warning). Useful when reconciliation races fills. |
generate_missing_orders | True | Generate LIMIT orders during reconciliation to align position discrepancies (strategy EXTERNAL, tag RECONCILIATION). |
snapshot_orders | False | Take order snapshots on order events. |
snapshot_positions | False | Take position snapshots on position events. |
snapshot_positions_interval_secs | None | Interval (seconds) between position snapshots. |
debug | False | Enable debug logging for execution. |
Cortana posture:
allow_overfills=False(default) - strict. If reconciliation races produce overfills, log + reject. Cortana’s MK2 baseline never tolerated overfills; preserve.generate_missing_orders=True(default) - KEEP ON. This is the mechanism that aligns position state via synthetic LIMIT orders.snapshot_orders=True+snapshot_positions=Truefor spike observability - flip on so the audit logger has rich event data to debug from. Production may dial back for performance.debug=Truefor spike day;Falsein production.
Memory management (verbatim)
| Setting | Default | Description |
|---|---|---|
purge_closed_orders_interval_mins | None | How often (minutes) to purge closed orders from memory. Recommended: 10-15 min. |
purge_closed_orders_buffer_mins | None | How long (minutes) an order must be closed before purging. Recommended: 60 min. |
purge_closed_positions_interval_mins | None | How often (minutes) to purge closed positions from memory. Recommended: 10-15 min. |
purge_closed_positions_buffer_mins | None | How long (minutes) a position must be closed before purging. Recommended: 60 min. |
purge_account_events_interval_mins | None | How often (minutes) to purge account events from memory. Recommended: 10-15 min. |
purge_account_events_lookback_mins | None | How old (minutes) an account event must be before purging. Recommended: 60 min. |
purge_from_database | False | Also delete from the backing database (Redis/PostgreSQL). Use with caution. |
Cortana posture: leave purge intervals at None for spike (Cortana
runs <40 trades/day; memory is not the bottleneck). At production with
Redis, set 10-15 min intervals + 60 min buffers per the doc’s
recommendation. Never set purge_from_database=True in production
(loses durable audit trail).
Queue management (verbatim)
| Setting | Default | Description |
|---|---|---|
qsize | 100,000 | Size of internal queue buffers. |
graceful_shutdown_on_exception | False | Gracefully shut down on unexpected queue processing exceptions (not user code). |
Cortana posture: leave qsize=100_000 (default; massive overkill
for 0DTE). Set graceful_shutdown_on_exception=True for production -
on an unexpected queue exception, prefer graceful shutdown over panic.
Strategy configuration (verbatim tables)
Identification
| Setting | Default | Description |
|---|---|---|
strategy_id | None | Unique strategy identifier. |
order_id_tag | None | Unique tag appended to this strategy’s order IDs. |
Order management
| Setting | Default | Description |
|---|---|---|
oms_type | None | OMS type for position ID and order processing. |
use_uuid_client_order_ids | False | Use UUID4 values for client order IDs. |
external_order_claims | None | Instrument IDs whose external orders this strategy claims. |
manage_contingent_orders | False | Automatically manage OTO, OCO, and OUO contingent orders. |
manage_gtd_expiry | False | Manage GTD expirations for orders. |
Cortana posture for spike:
strategy_id="CORTANA-BULL-CALL-V1"(andBEAR-PUT-V1etc).order_id_tag="C1"- short, unique-per-strategy.oms_type="NETTING"- single-position-per-instrument; matches Cortana’s design (one open SPY 0DTE leg at a time).use_uuid_client_order_ids=False- keep human-readable order IDs for log debugging.external_order_claims=[InstrumentId("SPY ... .SMART")]- adopt any pre-existing SPY 0DTE orders on boot (covers the workspace-archive recovery path).manage_contingent_orders=True- Cortana uses TP+SL OCO; let Nautilus auto-cancel siblings on fill.manage_gtd_expiry=False- Cortana uses DAY orders, not GTD.
Lifecycle (the bit the page omits)
The doc focuses on config; node.build() / node.run() /
node.dispose() are documented in Nautilus Live.
Canonical sequence:
node = TradingNode(config=config)
node.add_strategy(MyStrategy(...))
node.build() # wire components, register adapter factories
# CALL THIS BEFORE node.run()
node.run() # blocks; reconciles, then dispatches events
# SIGINT / SIGTERM → graceful shutdown (Unix)
node.dispose() # tear down clients, flush writersAdapter factory registration (load-bearing for IBKR)
Per Nautilus IB, IBKR adapter requires:
from nautilus_trader.adapters.interactive_brokers.factories import (
InteractiveBrokersLiveDataClientFactory,
InteractiveBrokersLiveExecClientFactory,
)
node = TradingNode(config=config)
node.add_data_client_factory("IB", InteractiveBrokersLiveDataClientFactory)
node.add_exec_client_factory("IB", InteractiveBrokersLiveExecClientFactory)
node.build()Forgetting add_*_client_factory is the most likely Saturday-morning
bug: the config references data_clients={"IB": ...} but the node
has no factory for "IB", so node.build() raises an obscure error.
The how-to page does not include this step explicitly (it shows only
the config struct, not the registration). Catch this in spike Step 1.
Windows shutdown pattern (verbatim)
try:
node.run()
except KeyboardInterrupt:
pass
finally:
try:
node.stop()
finally:
node.dispose()Not relevant for Cortana (macOS/Linux). Listed for completeness.
Cortana paper-IBKR config - copy-paste-ready
This is the Saturday-morning Step 1 starting point. Drop into
scripts/cortana_mk3_paper.py, fill in env vars, run.
"""Cortana MK3 paper-IBKR live config - spike day starter.
Run: TWS_USERNAME=... TWS_PASSWORD=... python -m cortana_mk3.live
Account: DUP696099 (paper)
Gateway: Dockerized IB Gateway, port 4002, paper mode
Pre-flight checklist:
1. Docker running, ib-gateway image pulled.
2. TWS_USERNAME / TWS_PASSWORD exported in env.
3. UTC timestamp checkbox confirmed in Gateway image (most community
images bake this in; if not, see nautilus-ib.md "TWS UTC requirement").
4. Redis NOT required for spike (in-memory cache).
"""
import os
from nautilus_trader.adapters.interactive_brokers.config import (
DockerizedIBGatewayConfig,
IBMarketDataTypeEnum,
InteractiveBrokersDataClientConfig,
InteractiveBrokersExecClientConfig,
InteractiveBrokersInstrumentProviderConfig,
)
from nautilus_trader.adapters.interactive_brokers.factories import (
InteractiveBrokersLiveDataClientFactory,
InteractiveBrokersLiveExecClientFactory,
)
from nautilus_trader.config import (
ImportableStrategyConfig,
LiveDataEngineConfig,
LiveExecEngineConfig,
LoggingConfig,
RoutingConfig,
TradingNodeConfig,
)
from nautilus_trader.live.node import TradingNode
from nautilus_trader.model.identifiers import InstrumentId
# ---- IB Gateway (Dockerized) ----
gateway_config = DockerizedIBGatewayConfig(
username=os.environ["TWS_USERNAME"],
password=os.environ["TWS_PASSWORD"],
trading_mode="paper", # paper account
read_only_api=False, # allow order submission
timeout=300, # 5 min cold-start budget
)
# ---- Instrument provider (SPY 0DTE options chain) ----
instrument_provider_config = InteractiveBrokersInstrumentProviderConfig(
build_futures_chain=False,
build_options_chain=True,
min_expiry_days=0,
max_expiry_days=1, # today's expiry only
load_ids=frozenset(), # populate at runtime per session
)
# ---- IB data client ----
data_client_config = InteractiveBrokersDataClientConfig(
ibg_client_id=1, # unique within Gateway
market_data_type=IBMarketDataTypeEnum.DELAYED_FROZEN,
# Spike day uses delayed-frozen (free; matches paper account's no-sub state).
# Production paper: REALTIME with $4.50/mo OPRA snapshot subscription.
instrument_provider=instrument_provider_config,
dockerized_gateway=gateway_config, # auto-manages host/port (4002)
)
# ---- IB execution client ----
exec_client_config = InteractiveBrokersExecClientConfig(
ibg_client_id=1, # SAME id as data client (sharing Gateway)
account_id="DUP696099", # paper account
instrument_provider=instrument_provider_config,
dockerized_gateway=gateway_config,
routing=RoutingConfig(default=True), # SMART routing
)
# ---- TradingNode config ----
config = TradingNodeConfig(
trader_id="CORTANA-PAPER",
instance_id=None, # auto-UUID for spike; pin at M3
timeout_connection=30.0,
timeout_reconciliation=30.0, # bumped from 10.0 for IBKR cold start
timeout_portfolio=10.0,
timeout_disconnection=10.0,
timeout_post_stop=5.0,
logging=LoggingConfig(
log_level="INFO",
log_component_levels={
"RiskEngine": "DEBUG",
"ExecutionEngine": "DEBUG",
},
bypass_logging=False,
print_config=False, # NEVER True in production
),
# No CacheConfig.database for spike (in-memory).
# No MessageBusConfig (in-process bus).
data_engine=LiveDataEngineConfig(
time_bars_build_with_no_updates=True,
time_bars_timestamp_on_close=True,
validate_data_sequence=False,
qsize=10_000,
),
exec_engine=LiveExecEngineConfig(
# Reconciliation
reconciliation=True,
reconciliation_lookback_mins=None,
reconciliation_startup_delay_secs=10.0,
# Continuous reconciliation (CORTANA NEEDS THESE ACTIVE)
inflight_check_interval_ms=2_000,
inflight_check_threshold_ms=5_000,
inflight_check_retries=5,
open_check_interval_secs=10, # poll IBKR every 10s for drift
open_check_open_only=True,
open_check_lookback_mins=60,
open_check_threshold_ms=5_000,
open_check_missing_retries=5,
position_check_interval_secs=60, # defense-in-depth for #46
position_check_lookback_mins=60,
position_check_threshold_ms=5_000,
position_check_retries=3,
# Behavior
generate_missing_orders=True,
allow_overfills=False,
snapshot_orders=True, # spike-day observability
snapshot_positions=True, # spike-day observability
debug=True, # spike-day observability
# Queue
qsize=100_000,
graceful_shutdown_on_exception=True,
),
data_clients={"IB": data_client_config},
exec_clients={"IB": exec_client_config},
strategies=[
ImportableStrategyConfig(
strategy_path="cortana_mk3.strategies:CortanaBullCallStrategy",
config_path="cortana_mk3.strategies:CortanaBullCallStrategyConfig",
config={
"strategy_id": "CORTANA-BULL-CALL-V1",
"order_id_tag": "C1",
"oms_type": "NETTING",
"manage_contingent_orders": True,
"manage_gtd_expiry": False,
"external_order_claims": ["SPY.SMART"],
"instrument_id": "SPY.SMART",
# Cortana-specific knobs filled in by spike-day strategy port:
"score_threshold": 65,
"meta_prob_threshold": 0.55,
"bias_filter": "BULL",
},
),
],
actors=[], # spike day adds AuditLoggerActor at Step 5
exec_algorithms=[],
)
def main() -> None:
node = TradingNode(config=config)
# MUST register factories before build()
node.add_data_client_factory("IB", InteractiveBrokersLiveDataClientFactory)
node.add_exec_client_factory("IB", InteractiveBrokersLiveExecClientFactory)
node.build()
try:
node.run()
except KeyboardInterrupt:
pass
finally:
try:
node.stop()
finally:
node.dispose()
if __name__ == "__main__":
main()This is the Step 1 / Step 4 reference for Saturday. If anything goes wrong with first connect, the issue is almost certainly one of:
- Adapter factory not registered before
build(). TWS_USERNAME/TWS_PASSWORDnot exported.- Docker daemon not running.
- UTC timestamp checkbox not enabled in the Gateway image (community images usually bake this in but VERIFY - see “Saturday-morning friction” below).
ibg_client_idcollision with another Gateway connection.
Cortana MK3 implications
Spike (M0/M1) - single-tenant, in-memory
The config above is correct for the spike. No Redis, no MessageBus
externalization, no instance_id pinning. One process, one tenant
(Cody), one IBKR account (DUP696099).
M3 (single-tenant production) - add Redis
Flip on:
cache=CacheConfig(database=DatabaseConfig(host="...", port=6379))message_bus=MessageBusConfig(database=DatabaseConfig(...))instance_id="cortana-prod"(pin so Redis keys survive restarts)purge_closed_orders_interval_mins=15,purge_closed_orders_buffer_mins=60(and the position/account parallels)
Per nautilus-cache.md: Redis is the structural fix to the 2026-04-22 workspace-archive class.
M4 (multi-tenant scaffolding)
Flip on:
use_instance_id=Trueon bothCacheConfigandMessageBusConfig.streams_prefix=f"tenant_{tenant.id}"onMessageBusConfig.- Per-tenant
trader_id=f"CORTANA-{tenant.id}". - Per-tenant Gateway container with per-tenant port
(
4002 + tenant.gateway_offset). - Per-tenant
account_idinInteractiveBrokersExecClientConfig. - Process-per-tenant deployment (one Python process per
TradingNodeper the singleton constraint).
M5 (Cody as Customer #1, web-app onboarding)
Layer Pydantic validation on top of the strategy config dict (per
nautilus-configuration.md - “the
range-validation gap”). Web-app form → Pydantic-validated dict →
ImportableStrategyConfig.config={...} → tenant TradingNode.
Saturday-morning friction points (most likely first)
Ranked by probability of biting at Step 1 / Step 4:
- Adapter factory registration forgotten. The doc shows
data_clients={"IB": config}but never explicitly says “you must callnode.add_data_client_factory('IB', ...)beforebuild()”. First crash will be cryptic. Highest-probability bug. - UTC timestamp checkbox not enabled in IB Gateway. Per
nautilus-ib.md: “This setting must be enabled
by the user in TWS/IB Gateway, as NautilusTrader is designed to
work with UTC timestamps.” Most community Dockerized images
(e.g.,
ghcr.io/extrange/ibkr) bake this in, but VERIFY by exec’ing into the container and checking. If not flipped: every timestamp is misinterpreted, event ordering breaks. The how-to page does NOT mention this - it lives in the IBKR concept doc. - Docker container port not bound. Compose file must publish
127.0.0.1:4002:4002. If port isn’t bound, Nautilus’sdockerized_gatewayauto-discovery silently fails. ibg_client_idcollision. If TWS GUI is running on the same machine withclient_id=1, the Nautilus connection will be refused. Useclient_id=1only if Gateway is the sole consumer.timeout_reconciliation=10.0too low for cold start. First IBKR connection on Saturday morning may pull historical orders, instruments, and account state - easily exceeds 10s. Bumped to 30.0 in the config above.- Strategy import path typo.
ImportableStrategyConfig.strategy_pathraisesImportErroratnode.build()time, not at config-load time. Test the import in a REPL first. - Paper account market data subscription.
DUP696099may not have OPRA-options subscriptions.DELAYED_FROZENworks for free but data is delayed. Confirm before assuming realtime works.
Caveats and gotchas
- Adapter factory registration is mandatory and undocumented on
this page. Always
add_data_client_factory+add_exec_client_factorybeforenode.build(). reconciliation_startup_delay_secs=10.0adds 10s to every boot. Do not reduce in production - the doc explicitly forbids it.open_check_interval_secs=None(default) DISABLES the open-orders venue poll. Cortana MUST set this explicitly (10s recommended). Otherwise the engine relies on the in-flight loop alone, which doesn’t catch all drift modes.position_check_interval_secs=None(default) DISABLES position consistency checks. Cortana MUST set this explicitly (60s) to satisfyproject_pm_ibkr_exit_invariantdefense-in-depth.open_check_lookback_minsreductions below 60 are explicitly forbidden by the doc - false missing-order resolutions.generate_missing_orders=Falseis dangerous. Confirmed in nautilus-live.md. DefaultTrueis correct.print_config=Truelogs the entire config including any decrypted creds - never enable in production / multi-tenant.bypass=Trueon RiskEngine disables ALL risk checks. Never ship to live; backtest research only.flush_on_start=Truewipes Redis state. Almost always wrong.- No native env-var interpolation. Read env vars in user code, pass into config dict.
- One TradingNode per process. Multi-tenant = multi-process.
- Jupyter is forbidden for live nodes. Run as standalone Python scripts under launchd / systemd / docker.
Open questions for the spike
- Does
node.build()validate adapter factory registration? Or does it crash at runtime when the firstdata_clients["IB"]message tries to dispatch? (Saturday Step 1 will surface this.) - Does the Dockerized Gateway image actually have UTC enabled by
default? Per
nautilus-ib.mdcarryover: “Ideally the Dockerized image bakes this in (most community images do).” Spike-time verification needed. - Is
position_check_interval_secs=60aggressive enough for 0DTE? A SPY 0DTE position can move 30% on a 5-min bar. 60s reconciliation is fine for state-divergence detection but not for risk action. (Risk action is the strategy’s responsibility, not reconciliation’s.) - Does
timeout_reconciliation=30.0cover the worst case of an IBKR account with 100s of historical orders? May need 60+ for M5 customers with active manual trading history. - Do
snapshot_orders=True+snapshot_positions=Trueproduce too much log volume for production? Fine for spike, possibly throttle in M3.
When this concept applies
- Spike Saturday Step 1 (install + first run) and Step 4 (paper IBKR connect via TradingNode).
- M3 production cutover (adding Redis cache + MessageBus externalization).
- M4 multi-tenant scaffolding (per-tenant config materialization).
- M5 customer onboarding (Pydantic validation layer + per-tenant process spawning).
- Any time wiring an unfamiliar venue adapter into a
TradingNode.
When it breaks / does not apply
- The page does NOT cover
DockerizedIBGatewayConfig- see nautilus-ib.md. - The page does NOT cover the TWS UTC timestamp checkbox - see nautilus-ib.md.
- The page does NOT cover lifecycle methods (
build,run,dispose) - see nautilus-live.md. - The page does NOT cover Redis ops shape or per-tenant Redis isolation - see nautilus-cache.md and nautilus-message-bus.md.
- The page does NOT cover Pydantic-style range validation - see nautilus-configuration.md.
- The page does NOT cover
Strategy.market_exit()/HALTED TradingStateshutdown patterns - see nautilus-execution.md.
See Also
- Nautilus Live Trading - TradingNode lifecycle, reconcile-on-startup, shutdown semantics, FSM.
- Nautilus Configuration - config object
hierarchy,
forbid_unknown_fields, Pydantic gap, multi-tenant patterns. - Nautilus Cache - Redis recommended for live,
use_instance_id=True, cache-then-publish. - Nautilus Execution -
LiveExecEngineConfigfull knobs,RiskEngineConfig,market_exit()graceful flatten. - Nautilus Message Bus - multi-tenant Redis Streams pattern, producer/consumer split.
- Nautilus IBKR - Dockerized IB Gateway, paper-vs-live ports, account ID conventions, UTC checkbox preflight, OCA gotcha.
- 2026-05-09 Nautilus Spike Plan:
~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-nautilus-spike.md project_pm_ibkr_exit_invariant(#46) - broker-truth alignment thatposition_check_interval_secsenforces.project_data_loss_april22- workspace-archive class addressed by Redis-backed Cache + reconciliation.- Source: https://nautilustrader.io/docs/latest/how_to/configure_live_trading/
Timeline
2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 6 (how-tos).