How-To - Configure a Live Trading Node

The Nautilus how_to/configure_live_trading/ page is the operational assembly guide for TradingNodeConfig - the top-level config struct that wires Cache, MessageBus, DataEngine, RiskEngine, ExecEngine, Portfolio, and per-venue data_clients + exec_clients into a single live runtime. The page commits to four hard constraints up front: (1) never run a live TradingNode in a Jupyter notebook (asyncio conflict, no graceful shutdown), (2) one TradingNode per 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 is LiveExecEngineConfigreference: 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 in nautilus-ib.mdandnautilus-configuration.md. For Cortana MK3 this page is the Saturday-morning Step-1-through-Step-3 reference: build the TradingNodeConfig, register IBKR client factories, call node.build()thennode.run(), paper account DUP696099` → 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_asyncio are 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 TradingNode instances 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_event methods) 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, TradingNode does 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)

SettingDefaultDescription
trader_id”TRADER-001”Unique trader identifier (name‑tag format).
instance_idNoneOptional unique instance identifier.
timeout_connection30.0Connection timeout in seconds.
timeout_reconciliation10.0Reconciliation timeout in seconds.
timeout_portfolio10.0Portfolio initialization timeout.
timeout_disconnection10.0Disconnection timeout.
timeout_post_stop5.0Post‑stop cleanup timeout.

Cortana implications:

  • trader_id="CORTANA-PAPER" (then CORTANA-LIVE later, then CORTANA-{tenant_id} at M4). Stable across restarts.
  • instance_id=None for 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)

SettingDefaultDescription
reconciliationTrueActivate reconciliation at startup to align internal state with the venue.
reconciliation_lookback_minsNoneHow far back (minutes) to request past events for reconciling uncached state.
reconciliation_instrument_idsNoneInclude list of instrument IDs to reconcile.
filtered_client_order_idsNoneClient 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)

SettingDefaultDescription
filter_unclaimed_external_ordersFalseDrop unclaimed external orders so they do not affect the strategy.
filter_position_reportsFalseDrop position status reports. Useful when multiple nodes trade one account.

Order tagging behavior (verbatim note):

“Reconciliation tags orders by origin:

  • VENUE tag: external orders discovered at the venue (placed outside this system).
  • RECONCILIATION tag: synthetic orders generated to align position discrepancies.

When filter_unclaimed_external_orders is enabled, only VENUE- 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 statusResolved toRationale
SUBMITTEDREJECTEDNo confirmation received from venue.
PENDING_UPDATECANCELEDModification remains unacknowledged.
PENDING_CANCELCANCELEDVenue never confirmed the cancellation.

Order consistency checks (when cache state differs from venue state):

Cache statusVenue statusResolutionRationale
SUBMITTEDNot foundREJECTEDOrder never confirmed by venue (e.g., lost during network error).
ACCEPTEDNot foundREJECTEDOrder doesn’t exist at venue, likely was never successfully placed.
ACCEPTEDCANCELEDCANCELEDVenue canceled the order (user action or venue‑initiated).
ACCEPTEDEXPIREDEXPIREDOrder reached GTD expiration at venue.
ACCEPTEDREJECTEDREJECTEDVenue rejected after initial acceptance (rare but possible).
PARTIALLY_FILLEDCANCELEDCANCELEDOrder canceled at venue with fills preserved.
PARTIALLY_FILLEDNot foundCANCELEDOrder 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_ms window (default 5s). This prevents false positives from race conditions where the venue is still processing.”

Targeted query safeguard: before marking an order REJECTED or CANCELED when ‘not found’, the engine issues a single-order query to the venue. This catches false negatives from bulk query limitations or timing delays.”

FILLED orders 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 by inflight_check_retries and open_check_missing_retries respectively. 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 GenerateOrderStatusReport probe 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)

SettingDefaultDescription
inflight_check_interval_ms2,000 msHow often to check in‑flight order status. Set to 0 to disable.
inflight_check_threshold_ms5,000 msTime before an in‑flight order triggers a venue status check. Lower if colocated.
inflight_check_retries5 retriesRetry attempts to verify an in‑flight order with the venue.
open_check_interval_secsNoneHow often (seconds) to check open orders at the venue. None or 0.0 disables. Recommended: 5-10s.
open_check_open_onlyTrueWhen true, query only open orders; when false, fetch full history (resource‑intensive).
open_check_lookback_mins60 minLookback window (minutes) for order status polling. Only orders modified within this window.
open_check_threshold_ms5,000 msMinimum time since last cached event before acting on venue discrepancies.
open_check_missing_retries5 retriesMax retries before resolving an order open in cache but not found at venue.
max_single_order_queries_per_cycle10Cap on single‑order queries per cycle. Prevents rate‑limit exhaustion.
single_order_query_delay_ms100 msDelay (ms) between single‑order queries to avoid rate limits.
reconciliation_startup_delay_secs10.0 sDelay (seconds) after startup reconciliation before continuous checks begin.
own_books_audit_interval_secsNoneInterval (seconds) between auditing own order books against public books.
position_check_interval_secsNoneInterval (seconds) between position consistency checks. None disables. Recommended: 30-60s.
position_check_lookback_mins60 minLookback window (minutes) for querying fill reports on position discrepancy.
position_check_threshold_ms5,000 msMinimum time since last local activity before acting on position discrepancies.
position_check_retries3 retriesMax 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; default None disables 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 for project_pm_ibkr_exit_invariant.
  • All other lookbacks/retries: default.

Additional options (verbatim)

SettingDefaultDescription
allow_overfillsFalseAllow fills exceeding order quantity (logs warning). Useful when reconciliation races fills.
generate_missing_ordersTrueGenerate LIMIT orders during reconciliation to align position discrepancies (strategy EXTERNAL, tag RECONCILIATION).
snapshot_ordersFalseTake order snapshots on order events.
snapshot_positionsFalseTake position snapshots on position events.
snapshot_positions_interval_secsNoneInterval (seconds) between position snapshots.
debugFalseEnable 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=True for spike observability - flip on so the audit logger has rich event data to debug from. Production may dial back for performance.
  • debug=True for spike day; False in production.

Memory management (verbatim)

SettingDefaultDescription
purge_closed_orders_interval_minsNoneHow often (minutes) to purge closed orders from memory. Recommended: 10-15 min.
purge_closed_orders_buffer_minsNoneHow long (minutes) an order must be closed before purging. Recommended: 60 min.
purge_closed_positions_interval_minsNoneHow often (minutes) to purge closed positions from memory. Recommended: 10-15 min.
purge_closed_positions_buffer_minsNoneHow long (minutes) a position must be closed before purging. Recommended: 60 min.
purge_account_events_interval_minsNoneHow often (minutes) to purge account events from memory. Recommended: 10-15 min.
purge_account_events_lookback_minsNoneHow old (minutes) an account event must be before purging. Recommended: 60 min.
purge_from_databaseFalseAlso 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)

SettingDefaultDescription
qsize100,000Size of internal queue buffers.
graceful_shutdown_on_exceptionFalseGracefully 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

SettingDefaultDescription
strategy_idNoneUnique strategy identifier.
order_id_tagNoneUnique tag appended to this strategy’s order IDs.

Order management

SettingDefaultDescription
oms_typeNoneOMS type for position ID and order processing.
use_uuid_client_order_idsFalseUse UUID4 values for client order IDs.
external_order_claimsNoneInstrument IDs whose external orders this strategy claims.
manage_contingent_ordersFalseAutomatically manage OTO, OCO, and OUO contingent orders.
manage_gtd_expiryFalseManage GTD expirations for orders.

Cortana posture for spike:

  • strategy_id="CORTANA-BULL-CALL-V1" (and BEAR-PUT-V1 etc).
  • 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 writers

Adapter 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:

  1. Adapter factory not registered before build().
  2. TWS_USERNAME / TWS_PASSWORD not exported.
  3. Docker daemon not running.
  4. UTC timestamp checkbox not enabled in the Gateway image (community images usually bake this in but VERIFY - see “Saturday-morning friction” below).
  5. ibg_client_id collision 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=True on both CacheConfig and MessageBusConfig.
  • streams_prefix=f"tenant_{tenant.id}" on MessageBusConfig.
  • Per-tenant trader_id=f"CORTANA-{tenant.id}".
  • Per-tenant Gateway container with per-tenant port (4002 + tenant.gateway_offset).
  • Per-tenant account_id in InteractiveBrokersExecClientConfig.
  • Process-per-tenant deployment (one Python process per TradingNode per 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:

  1. Adapter factory registration forgotten. The doc shows data_clients={"IB": config} but never explicitly says “you must call node.add_data_client_factory('IB', ...) before build()”. First crash will be cryptic. Highest-probability bug.
  2. 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.
  3. Docker container port not bound. Compose file must publish 127.0.0.1:4002:4002. If port isn’t bound, Nautilus’s dockerized_gateway auto-discovery silently fails.
  4. ibg_client_id collision. If TWS GUI is running on the same machine with client_id=1, the Nautilus connection will be refused. Use client_id=1 only if Gateway is the sole consumer.
  5. timeout_reconciliation=10.0 too 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.
  6. Strategy import path typo. ImportableStrategyConfig.strategy_path raises ImportError at node.build() time, not at config-load time. Test the import in a REPL first.
  7. Paper account market data subscription. DUP696099 may not have OPRA-options subscriptions. DELAYED_FROZEN works 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_factory before node.build().
  • reconciliation_startup_delay_secs=10.0 adds 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 satisfy project_pm_ibkr_exit_invariant defense-in-depth.
  • open_check_lookback_mins reductions below 60 are explicitly forbidden by the doc - false missing-order resolutions.
  • generate_missing_orders=False is dangerous. Confirmed in nautilus-live.md. Default True is correct.
  • print_config=True logs the entire config including any decrypted creds - never enable in production / multi-tenant.
  • bypass=True on RiskEngine disables ALL risk checks. Never ship to live; backtest research only.
  • flush_on_start=True wipes 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

  1. Does node.build() validate adapter factory registration? Or does it crash at runtime when the first data_clients["IB"] message tries to dispatch? (Saturday Step 1 will surface this.)
  2. Does the Dockerized Gateway image actually have UTC enabled by default? Per nautilus-ib.md carryover: “Ideally the Dockerized image bakes this in (most community images do).” Spike-time verification needed.
  3. Is position_check_interval_secs=60 aggressive 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.)
  4. Does timeout_reconciliation=30.0 cover the worst case of an IBKR account with 100s of historical orders? May need 60+ for M5 customers with active manual trading history.
  5. Do snapshot_orders=True + snapshot_positions=True produce 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

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 - LiveExecEngineConfig full 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 that position_check_interval_secs enforces.
  • 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).