Nautilus Interactive Brokers Integration

Nautilus ships a first-class IBKR adapter (nautilus_trader[ib]) that wraps IB’s ibapi library and exposes equities, ETFs, equity options, futures (including continuous), options-on-futures, forex (IDEALPRO), crypto (PAXOS), bonds, indices, CFDs, and commodities through the standard Nautilus DataClient / ExecutionClient / InstrumentProvider contracts. Two connection paths are supported: (1) attach to an existing TWS or IB Gateway instance you started manually, or (2) boot a Dockerized IB Gateway in-process via DockerizedIBGatewayConfig(username, password, trading_mode, read_only_api, timeout) - the latter is the documented “automated deployments” path and is the standalone-win Cody flagged for the 2026-05-06 power-outage failure mode (IB Gateway exited cleanly during boot, the engine could not reconnect because nothing was watching the gateway lifecycle). The adapter handles real-time

  • delayed market data, options chain materialization (build_options_chain=True, min_expiry_days=0, max_expiry_days=1 = native 0DTE), bracket / OCO / conditional orders, and continuous reconciliation against broker truth. Native Greeks streaming is not supported on IBKR - the adapter does not surface IBKR tick-13 model Greeks through subscribe_option_greeks. Cortana must compute Greeks locally (GreeksCalculator) or back-fill from a UW custom data feed. Multi-account / multi-tenant is supported via multiple exec_clients entries with unique ibg_client_id + account_id values.

This page is the canonical IBKR-on-Nautilus reference. It specializes nautilus-integrations.md (which introduces the IBKR adapter at concept level) and nautilus-adapters.md (which defines the five-component adapter contract) with the operational depth required for the 2026-05-09 spike Step 0/1/4/5 (install, smoke, Cortana → Nautilus mapping, Strategy implementation).

Cody’s question - answered

Q1: TWS or IB Gateway, paper and live?

A: IB Gateway for both. Lower resource footprint (~40% less than TWS per reference_ibkr_api.md), no GUI clutter, headless-friendly. For the spike and through MK3 launch, Dockerized IB Gateway is the right choice - it programmatically supplies username/password/trading mode at startup (the standalone applications require manual GUI login), survives container restarts, and gives Cortana a healthcheck seam that the 2026-05-06 outage proved we need. TWS is only a fallback for interactive debugging.

Q2: Greeks path for carryover #8?

A: UW wrap as the primary live path; local Black-Scholes (GreeksCalculator) as the backup and the backtest path. Drop the “native IBKR Greeks adapter” goal - it does not exist. The IBKR adapter does not implement subscribe_option_greeks. Routing UW Greeks through a @customdataclass (per nautilus-greeks.md and the UW-data-only adapter sketch in nautilus-adapters.md) preserves the existing UW Greeks pipeline that already feeds the scoring engine. For backtest replay, GreeksCalculator running over IBKR option quote ticks is the deterministic path; in live mode the UW-supplied Greeks dominate because they reflect actual venue dealer-vol surfaces (especially in the last hour where BSM under-prices theta - see project_eod_power_hour.md).

Q3: Multi-tenant per-tenant IBKR creds?

A: Use the documented “Multiple IB execution clients for different accounts” pattern. Each tenant gets its own entry in exec_clients with a unique key (e.g., IB-CODY, IB-TENANT-2), unique ibg_client_id (1-32, must be unique across the whole TWS/Gateway instance), and unique account_id. Routing happens by ClientId, account_id issuer, or default-routing flag. Caveat: IB Gateway allows max 32 simultaneous connections per instance - beyond that each tenant needs its own Gateway container. For MK3 a per-tenant container is the cleaner model anyway (isolates auth, restart, logs).

Reference: TWS vs Gateway

The two standalone applications differ on resource footprint, GUI posture, and update cadence - but NautilusTrader treats them identically at the wire level: both speak the IB API over a TCP socket; the adapter doesn’t care which one is listening.

AspectTWS (Trader Workstation)IB Gateway
PurposeFull GUI trading platformAPI-only minimal app
Resource footprint~2-3 GB RAM, full GUI~600-800 MB RAM (~40% less per reference_ibkr_api.md)
Paper port74974002
Live port74964001
Auto-restart (v974+)Yes (Sunday-to-Sunday)Yes
Daily restartRequired (contract defs refresh)Required
AuthGUI login requiredGUI login required (standalone); programmatic in Dockerized variant
Best forManual debug + tradingAutomation + production

Cortana’s port 4002 is Gateway paper. Account DUP696099 follows IB’s paper convention (DU prefix; the P in DUP is non-standard but IB does mint accounts with extra letters after DU). Migration to live: change port to 4001, change account_id to the live U… account, flip trading_mode="live" on the Dockerized config - no code change beyond config.

Default ports (verbatim from doc)

ApplicationPaper TradingLive Trading
TWS74977496
IB Gateway40024001

Recommendation for MK3: IB Gateway, Dockerized, both paper and live. TWS only as a developer-station tool when you need to see the GUI to diagnose an issue.

Reference: Dockerized IB Gateway

The Dockerized variant is the canonical “production” path per the docs. It removes the manual-login step that the 2026-05-06 outage exposed as a single point of failure (Gateway exited, nothing logged back in, the engine could not reconnect).

Configuration shape

from nautilus_trader.adapters.interactive_brokers.config import DockerizedIBGatewayConfig
from nautilus_trader.adapters.interactive_brokers.gateway import DockerizedIBGateway
 
gateway_config = DockerizedIBGatewayConfig(
    username="cody@…",                 # or env: TWS_USERNAME
    password="…",                      # or env: TWS_PASSWORD
    trading_mode="paper",              # "paper" | "live"
    read_only_api=False,               # True locks order submission off
    timeout=300,                       # startup timeout seconds
)
gateway = DockerizedIBGateway(config=gateway_config)
gateway.start()
 
# Health check
print(gateway.is_logged_in(gateway.container))
print(gateway.container.logs())

The read_only_api flag is the strongest software kill-switch IBKR exposes: when True, the API connection cannot place orders no matter what the strategy submits. Useful for data-only deployments and read-only research nodes.

Wiring into the data + execution clients

When dockerized_gateway is supplied on the data and execution client configs, ibg_host and ibg_port are managed automatically - do not set them manually:

data_client_config = InteractiveBrokersDataClientConfig(
    ibg_client_id=1,
    market_data_type=IBMarketDataTypeEnum.REALTIME,
    instrument_provider=instrument_provider_config,
    dockerized_gateway=gateway_config,    # auto-managed host/port
)
 
exec_client_config = InteractiveBrokersExecClientConfig(
    ibg_client_id=1,
    account_id=os.environ["TWS_ACCOUNT"],
    instrument_provider=instrument_provider_config,
    dockerized_gateway=gateway_config,    # same gateway instance
    routing=RoutingConfig(default=True),
)

Container restart policy + healthcheck pattern

The doc does not prescribe a Docker Compose restart policy directly, but the container-as-managed-process model implies it. For Cortana MK3 the right shape is:

# docker-compose.yml (sketch)
services:
  ib-gateway:
    image: ghcr.io/extrange/ibkr:latest    # community image
    restart: unless-stopped
    ports:
      - "127.0.0.1:4002:4002"              # bind localhost only
    environment:
      TWS_USERNAME: ${TWS_USERNAME}
      TWS_PASSWORD: ${TWS_PASSWORD}
      TRADING_MODE: paper
      READ_ONLY_API: "no"
    healthcheck:
      test: ["CMD", "nc", "-z", "127.0.0.1", "4002"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 90s                    # IB login is slow
    deploy:
      resources:
        limits:
          memory: 1g

The Nautilus DockerizedIBGatewayConfig is wrapper-equivalent - the framework speaks the Docker SDK to start/stop the same container shape. Whether to use DockerizedIBGatewayConfig (Nautilus owns the container) or an external Compose stack (ops owns the container, Nautilus only connects to it) is a choice:

  • Nautilus-owned (DockerizedIBGatewayConfig): simpler for a single-tenant single-strategy deploy. The TradingNode lifecycle controls the gateway lifecycle. If the node crashes, the gateway exits.
  • Externally-owned (Docker Compose): better for multi-tenant - one Gateway container per tenant, restart policy independent of any one TradingNode. Recommended for MK3 M4+ multi-tenant.

Secrets handling

The doc shows three injection points, in order of preference:

  1. Environment variables (TWS_USERNAME, TWS_PASSWORD, TWS_ACCOUNT, IB_MAX_CONNECTION_ATTEMPTS) - populated from a secret store (Doppler, 1Password, AWS Secrets Manager) at container start.
  2. Direct username/password on DockerizedIBGatewayConfig - acceptable but only when those values are themselves resolved from a secret store at config-build time (never hardcoded).
  3. Manual login on a non-Dockerized TWS/Gateway - for developer debugging only.

The “best practices” section of the IBKR doc states verbatim: “Never hardcode credentials in source code. Use environment variables for sensitive information.” This aligns with feedback_no_kill_with_open_positions.md posture - credentials separate from code.

MK2 → MK3 migration: replace the launchd plist

Cortana MK2 currently uses a launchd plist (com.cortanaroi.mk2.engine.plist) that supervises the engine process. Per project_system_hours.md it kicks off at 8:10 AM CT and runs through market hours. The 2026-05-06 outage exposed that:

  1. The plist supervises the engine, not the Gateway.
  2. When the Gateway exited cleanly during a power-cycle, no watchdog noticed.
  3. The engine retried IB.connect(127.0.0.1, 4002) against a port nothing was listening on, classified the result as “transient connectivity issue,” and never escalated.

MK3 fix: replace the engine-only plist with a Compose stack (or an nautilus-managed DockerizedIBGatewayConfig) where the Gateway container has its own restart policy, healthcheck, and log stream. The engine process becomes one of many supervised siblings rather than the sole supervisor of the venue connection. This is the standalone win the spike plan flags - it pays off regardless of MK3 GO/NO-GO.

Reference: market data subscriptions

Required IB market data subscriptions for SPY 0DTE (Cortana’s V1 universe):

SubscriptionTierRequired for
NASDAQ (Network C/UTP)$1.50/moSPY trades + quotes (SPY trades on ARCA but routes via SMART; the underlying data farm is NASDAQ)
NYSE Trades and OPRA Subscription$4.50/moSPY trade-by-trade (US Securities Snapshot bundle covers this)
OPRA (Options Price Reporting Authority)$1.50/moAll US equity option quotes + trades (the entire SPY 0DTE chain)
NASDAQ TotalView ArcaBookoptionalL2 SPY shares depth - Cortana V1 does not need this

For development / paper trading without subscriptions, set market_data_type=IBMarketDataTypeEnum.DELAYED_FROZEN - IB returns 15-20 min delayed data for free.

IBMarketDataTypeEnum values

ValueMeaningCostUse case
REALTIMELiveSubscription requiredProduction live trading
DELAYED15-20 min delayedFreeDevelopment against real (delayed) feed
DELAYED_FROZENDelayed + frozen (does not update)FreeSmoke tests, fixture replay
FROZENLast known real-time when market closedSubscription requiredOff-hours analysis

Cortana’s spike-day default: DELAYED_FROZEN. Production paper: REALTIME (the live SPY 0DTE chain demands real prices to be representative).

Regulatory snapshot caveat

Per reference_ibkr_api.md: regulatory snapshots are $0.01 each, even on paper accounts, capped per listing exchange. Do not use the reqMktData(snapshot=True) path in a tight loop. Nautilus’s subscribe_quote_ticks uses the streaming subscription, not the snapshot endpoint, so this gotcha does not apply to normal usage - but be aware if you call request_quote_ticks(...) in a way that maps to snapshots.

Pacing limits

IB enforces:

  • 50 messages/second total - exceeding triggers error 100 (disconnect).
  • reqContractDetails for full options chains is throttled - the doc explicitly warns: “Interactive Brokers enforces pacing limits; excessive historical-data or order requests trigger pacing violations and IB can disable the API session for several minutes.” Use reqSecDefOptParams (which Nautilus invokes under build_options_chain=True) - it is not throttled per reference_ibkr_api.md.
  • Tick-by-tick max 1 request per instrument per 15 seconds.
  • Max 32 simultaneous client connections per TWS/Gateway.

The adapter exposes request_timeout_secs (default 60) on both data and execution client configs; tune up for large historical pulls.

Reference: account model

Supported account types

The framework’s AccountType enum is venue-agnostic; the IBKR adapter populates it from the live account state. Supported:

AccountTypeMeaning
CASHCash account; orders fully funded from balances
MARGINMargin account; portfolio margin or Reg-T
FUTURES_MARGINFutures margin (SPAN-based for IB futures)

Per the IBKR doc’s “Position management” table:

FeatureSupportedNotes
Query positionsyesReal-time position updates
Position modeyesNet vs separate long/short positions
Leverage controlyesAccount-level margin requirements
Margin modeyesPortfolio vs individual margin

Real-time account state

The InteractiveBrokersClientAccountMixin pulls and streams:

  • Account balances and margin (reqAccountSummary-style data)
  • Real-time position updates (via updatePortfolio / position callbacks)
  • P&L per position (~1Hz updates per reference_ibkr_api.md)
  • Account-level realized + unrealized P&L
  • Multi-account scenarios

These flow into Nautilus’s AccountState events, which the engine’s Portfolio component consumes. Important per nautilus-execution.md: the adapter must emit complete margin snapshots - partial snapshots overwrite account-wide entries silently. The IBKR adapter does this correctly out of the box.

Position-mode interaction with OMS

IB’s effective OMS for equities + equity options is NETTING (one net position per (symbol, strike, right, expiry)). Cortana’s strategy OMS should match (OmsType.NETTING or UNSPECIFIED → inherits NETTING from venue). This is the default and what Cortana wants for V1 single-leg flow per nautilus-execution.md § OMS.

Option exercise tracking

Opt-in via the execution client config:

exec_client_config = InteractiveBrokersExecClientConfig(
    ...,
    track_option_exercise_from_position_update=True,
)

When True, the adapter detects when a position changes due to exercise/assignment (rather than a fill) and emits the appropriate events. Cortana V1 mandate is never hold past close - every position is flat by 15:55 ET - so exercise risk is structurally zero. Leave default False. Flip on if/when MK3 introduces short-premium structures.

Reference: supported order types

The IBKR adapter supports the full Nautilus order taxonomy. Verbatim from the doc:

Nautilus order typeMaps toNative at IBKR?
MARKETMKTYes
LIMITLMTYes
STOP_MARKETSTPYes
STOP_LIMITSTP LMTYes
MARKET_IF_TOUCHEDMITYes
LIMIT_IF_TOUCHEDLITYes
TRAILING_STOP_MARKETTRAILYes
TRAILING_STOP_LIMITTRAIL LIMITYes
MARKET + TimeInForce.AT_THE_CLOSEMOCYes
LIMIT + TimeInForce.AT_THE_CLOSELOCYes
Bracket (parent + TP + SL)Parent + child IDsYes
OCO groupOCA group with IBOrderTagsYes (via tags)

MARKET_TO_LIMIT is supported by Nautilus generally and by IB generally, but the doc does not enumerate it for the IBKR adapter - verify on spike if needed.

Time in force

TIFSupportedNotes
DAYyesCortana V1 default - flatten by close
GTCyesAvoid for 0DTE (would survive past close - invariant violation)
IOCyesUseful for marketable-but-cancel-rest semantics
FOKyesAll-or-nothing
GTDyesGood-Till-Date with explicit expiry
AT_THE_OPENyesMOO / LOO orders
AT_THE_CLOSEyesMOC / LOC orders

Bracket + emulation interaction

nautilus-orders.md is the load-bearing reference for bracket emulation. Recap: a bracket without emulation_trigger is venue-native (IBKR owns the bracket as a parent + 2 children, OCA group via IBOrderTags). A bracket with emulation_trigger=MARK_PRICE (or BID_ASK) is emulated locally

  • the OrderEmulator subscribes to the trigger feed, watches it in-process, and only sends a basic MARKET/LIMIT to IBKR when triggered. Both are supported by the IBKR adapter.

Cortana MK3 default: emulated brackets on the TP/SL legs (per feedback_dual_tp_defense_in_depth.md - software fallback when broker LMT misses). The bracket parent (entry MARKET) cannot be emulated.

OCA caveat - explicit configuration required

Critical gotcha: IBKR’s adapter does not auto-create OCA groups from ContingencyType.OCO or ContingencyType.OUO. Verbatim from the doc:

“OCA functionality is only available through explicit configuration: IBOrderTags Required … No Automatic Detection … Manual Configuration”

Cortana’s bracket-style exits must populate IBOrderTags:

from nautilus_trader.adapters.interactive_brokers.common import IBOrderTags
 
oca_tags = IBOrderTags(
    ocaGroup="CORTANA-EXIT-001",
    ocaType=1,                  # 1 = Cancel All with Block (recommended)
)
 
bracket = order_factory.bracket(
    instrument_id=call_id,
    order_side=OrderSide.BUY,
    quantity=instrument.make_qty(contracts),
    tp_price=instrument.make_price(entry * 1.10),
    sl_trigger_price=instrument.make_price(entry * 0.75),
    tp_tags=[oca_tags.value],   # explicit; not auto from ContingencyType
    sl_tags=[oca_tags.value],
)
self.submit_order_list(bracket)

OCA types per the doc:

TypeNameBehavior
1Cancel All with BlockDefault - safest, prevents overfills
2Reduce with BlockProportionally reduce, with overfill protection
3Reduce without BlockFastest execution, higher overfill risk

Cortana V1 recommendation: Type 1 (Cancel All with Block) - single-shot exit, no scale-out, overfill-protection matters more than execution speed.

Conditional orders

Six condition types supported:

  • price - trigger when an instrument price crosses a threshold
  • time - trigger at a specific datetime (UTC)
  • volume - trigger on traded volume threshold
  • execution - trigger when a trade occurs in another instrument
  • margin - trigger on account margin cushion
  • percent_change - trigger on % price move

Combined via conjunction: "and" | "or". conditionsCancelOrder=False transmits the order when conditions met; True cancels.

Cortana V1 does not need conditional orders (the strategy + scoring engine generates the entry timing). But this is the seam if MK3 ever wants “enter only after SPY trades above X” mechanically rather than via Strategy logic.

Bracket parent timing - the 50ms gotcha

reference_ibkr_api.md notes IBKR error 10006 (“missing parent order”) fires when a child is sent before the parent is registered - needs ~50ms pause. Nautilus’s submit_order_list for a bracket sequences this correctly (parent is submitted first, the engine tracks parent acceptance, children fire after). Cortana strategy code does not need to insert a delay - but if you bypass the bracket primitive and submit parent + children manually, you do.

Reference: instrument provider

InteractiveBrokersInstrumentProvider

Translates IB contracts (IBContract / IBContractDetails) into Nautilus Instrument instances. Two loading paths:

Human-readable simplified-symbology IDs:

InteractiveBrokersInstrumentProviderConfig(
    symbology_method=SymbologyMethod.IB_SIMPLIFIED,
    load_ids=frozenset([
        "SPY.ARCA",                # Cortana underlying
        "QQQ.NASDAQ",
        "EUR/USD.IDEALPRO",        # forex example
        "ESM4.CME",                # individual future
        "^SPX.CBOE",               # index
    ]),
)

load_contracts (required for chains, complex contracts)

IBContract instances directly:

from nautilus_trader.adapters.interactive_brokers.common import IBContract
 
# Cortana's 0DTE SPY chain
spy_0dte_chain = IBContract(
    secType="STK",
    symbol="SPY",
    exchange="SMART",
    primaryExchange="ARCA",
    build_options_chain=True,
    min_expiry_days=0,
    max_expiry_days=1,             # 0DTE-style window
)
 
InteractiveBrokersInstrumentProviderConfig(
    symbology_method=SymbologyMethod.IB_SIMPLIFIED,
    load_contracts=frozenset([spy_0dte_chain]),
)

The provider materializes every strike in the active chain as an OptionContract instance with full metadata: strike, expiry, multiplier (100 for SPY equity options), tick size, min size.

Symbology methods

Three modes, set on InstrumentProviderConfig.symbology_method:

ModeFormatExample
IB_SIMPLIFIED (default)Human-readable, asset-class-awareSPY.ARCA, AAPL230217P00155000.SMART, EUR/USD.IDEALPRO
IB_RAWDirect mirror of IB API fieldsAAPL=STK.SMART, IBUS30=CFD.IBCFD
MIC venue conversionStandardized MIC codesSPY.ARCX (with convert_exchange_to_mic_venue=True)

Recommendation for Cortana: IB_SIMPLIFIED (default). MK2 already uses readable contract identifiers internally; preserve that habit. Only switch to IB_RAW if the existing IB conId tracking exposes a parsing edge case (e.g., LEAPs with unusual symbology).

MIC venue conversion (optional)

InteractiveBrokersInstrumentProviderConfig(
    convert_exchange_to_mic_venue=True,        # CME → XCME, ARCA → ARCX, …
    symbol_to_mic_venue={
        "SPY": "ARCX",
        "SPX": "XCBO",                         # OPT routed via SMART → CBOE MIC
    },
)

Useful if MK3 ever cross-references Databento (which uses MIC conventions) or compliance tooling that demands MIC. Otherwise leave off.

Instrument cache

cache_validity_days=1 is the default - instrument metadata is cached for one day, then refreshed. IB requires a daily restart anyway (per reference_ibkr_api.md: contract definitions refresh at restart) so the 1-day cadence aligns naturally.

Filtering security types

InteractiveBrokersInstrumentProviderConfig(
    load_ids=frozenset(["SPY.ARCA"]),
    filter_sec_types=frozenset({"WAR", "IOPT"}),   # skip warrants, structured options
)

Useful when build_options_chain=True returns mixed types you don’t want.

HistoricInteractiveBrokersClient for backtest data

Separate from the live data client - used for one-off historical pulls into a ParquetDataCatalog:

from nautilus_trader.adapters.interactive_brokers.historical.client import HistoricInteractiveBrokersClient
 
client = HistoricInteractiveBrokersClient(
    host="127.0.0.1", port=7497, client_id=5,
    market_data_type=MarketDataTypeEnum.DELAYED_FROZEN,
)
await client.connect()
bars = await client.request_bars(
    bar_specifications=["1-MINUTE-LAST", "5-MINUTE-MID", "1-DAY-LAST"],
    start_date_time=datetime(2026, 4, 1, 9, 30),
    end_date_time=datetime(2026, 4, 30, 16, 30),
    tz_name="America/New_York",
    contracts=[IBContract(secType="STK", symbol="SPY", exchange="SMART", primaryExchange="ARCA")],
    use_rth=True,
)
catalog = ParquetDataCatalog("./catalog")
catalog.write_data(bars)

For Cortana’s backtest replay, this is the path to populate a catalog with IBKR option ticks + bars; pair with UW @customdataclass Greeks per nautilus-options.md.

Reference: contract specs

Asset classes covered

The adapter supports all major asset classes through IB:

  • Equities (stocks, ETFs)
  • Equity options
  • Bonds (ISIN, CUSIP)
  • Derivatives - futures, options, warrants
  • Continuous futures (secType="CONTFUT")
  • Options on futures (FOP)
  • Forex (IDEALPRO)
  • Cryptocurrencies (PAXOS)
  • Indices, index options
  • CFDs, commodities

Contract resolution - front month, expiry

For continuous futures (secType="CONTFUT"), the adapter creates an ID like ES.CME representing the front month with automatic roll. For specific contracts (secType="FUT"), the ID is ESM4.CME (single-digit year per IB convention). For options chains, individual contracts are materialized at their full expiry: AAPL230217P00155000.SMART.

Front-month and expiry-window controls

When build_options_chain=True:

ConfigEffect
min_expiry_days=0, max_expiry_days=10DTE only (today + tomorrow) - Cortana’s V1
min_expiry_days=7, max_expiry_days=30Weekly options window
min_expiry_days=0, max_expiry_days=60Two-month chain (heavy load)
lastTradeDateOrContractMonth='20240718'Single specific expiry
options_chain_exchange='CBOE'Override exchange (e.g., load CBOE-listed SPX options instead of SMART)

For Cortana’s V1: (0, 1) materializes today’s expiry only - minimal load, fastest startup.

Option spread support - BAG contracts

IBKR supports option spreads through BAG contracts. Nautilus exposes new_generic_spread_id([(leg1_id, ratio1), (leg2_id, ratio2), ...]):

call_leg = InstrumentId.from_str("SPY 250509C00580000.SMART")
put_leg = InstrumentId.from_str("SPY 250509P00580000.SMART")
 
straddle_id = new_generic_spread_id([
    (call_leg, 1),
    (put_leg, 1),
])

Spreads must be request_instrument(spread_id)’d before they can be traded or subscribed. Cortana V1 does not use spreads - flagged for V2+ if defined-risk plays come into scope.

Specific OPT contract construction

spy_call_580 = IBContract(
    secType='OPT',
    exchange='SMART',
    symbol='SPY',
    lastTradeDateOrContractMonth='20260509',   # YYYYMMDD
    strike=580.0,
    right='C',                                 # 'C' or 'P'
)

For programmatic strike selection (Cortana’s “buy delta-0.30 call” pattern) the cleaner path is build_options_chain=True plus an on_option_chain handler that scans for the target delta - see nautilus-options.md § Strike selection.

Greeks gap (carryover #8)

Critical finding from nautilus-options.md § “Adapter Support” and nautilus-greeks.md: Nautilus’s native subscribe_option_greeks is implemented for Deribit, Bybit, and OKX. IBKR is NOT in that table.

This means Cortana cannot rely on the IBKR adapter to deliver OptionGreeks events through the standard subscription. Two paths:

Path A - Local Black-Scholes via GreeksCalculator

nautilus_trader.model.greeks.GreeksCalculator (Cython class wrapping Rust black_scholes_greeks / imply_vol_and_greeks). Inputs: option quote (premium), underlying quote (already in cache from subscribe_quote_ticks(SPY)), expiry (from OptionContract), risk-free rate (config). Output: GreeksData event.

Pros: deterministic, replays bit-identically in backtest, no external dependency.

Cons: BSM under-prices theta in the last hour (per project_eod_power_hour.md). Not ideal for live power-hour decisions. Implied vol surfaces from venue dealers carry information BSM doesn’t.

Path B - UW Greeks via custom adapter

UW already publishes Greeks per contract (delta, gamma, vega, theta, rho, mark IV) via WebSocket. Wrap as a @customdataclass flowing through the UW data-only adapter described in nautilus-adapters.md. Strategy subscribes via subscribe_data(DataType(UWOptionGreeks)) and consumes via on_data.

Pros: real venue dealer-vol surfaces, accurate theta in the power hour, ports directly from existing Cortana UW pipeline.

Cons: external dependency (UW outage = no Greeks). Live-only - backtest replay still needs the local calculator unless we serialize UW Greeks into the catalog.

Both paths, switched by mode:

ModePrimary Greeks sourceFallback
BacktestGreeksCalculator (local BSM)n/a (deterministic replay only)
Paper / LiveUW custom dataGreeksCalculator if UW WS disconnected

The Cortana strategy reads from cache.greeks(instrument_id) (or the equivalent custom-data lookup) - the source-switching happens upstream in the data layer, not in the strategy. This matches the existing MK2 pattern of UW being the authoritative scoring/Greeks source with a defensive fallback.

Closing carryover #8: drop the goal of “native IBKR Greeks adapter.” It does not exist and Nautilus does not plan one. Adopt the dual-path above. File the decision with the spike outcome.

Reference: reconnection behavior

Adapter-level reconnect

The InteractiveBrokersClientConnectionMixin provides:

  • Automatic reconnection with retry count from IB_MAX_CONNECTION_ATTEMPTS env var (default unlimited).
  • Connection timeout via connection_timeout config (default 300s = 5 min - accommodates 2FA push delays).
  • Connection watchdog monitoring socket health, triggering reconnect on drop.
  • Graceful error handling - error classification distinguishes client errors, connectivity issues, and request errors.

Engine-level reconciliation on reconnect

Per nautilus-execution.md § Reconcile-on-startup: on every successful (re)connect, the LiveExecutionEngine runs the full reconciliation sequence:

  1. Pull all open orders, all positions, account balances.
  2. Compare against the Cache.
  3. Synthesize events (OrderAccepted, OrderFilled, PositionOpened) for venue state not in cache, marked reconciliation=True.
  4. Continuous reconciliation continues at open_check_interval_secs thereafter.

This is what closes the GH #46 trust class. A reconnect after a gateway exit (e.g., the 2026-05-06 outage scenario) does not lose state - the engine resyncs from broker truth.

fetch_all_open_orders flag

exec_client_config = InteractiveBrokersExecClientConfig(
    ...,
    fetch_all_open_orders=True,    # default False
)

When True, on connect the adapter pulls orders placed by any API client on the account (not just this ibg_client_id). Useful if another process (a manual TWS user, a separate Cortana instance, recovery tooling) placed orders the engine should adopt as EXTERNAL strategy orders. Default False is correct for a single-process Cortana - flip on only if multiple processes share an account.

Daily restart requirement

Per reference_ibkr_api.md: TWS / Gateway require a daily restart to refresh contract definitions. v974+ supports auto-restart Sunday-to-Sunday; the Saturday-night server reset still requires re-auth (and will pause the gateway for 5-10 minutes). Cortana’s launchd or Compose stack should not run trades during this window - align with project_system_hours.md (engine starts 8:10 AM CT, nowhere near the maintenance window).

TWS UTC requirement (preflight)

Critical configuration step that the doc mentions in passing:

“Configure TWS or IB Gateway to return market data timestamps in UTC before connecting NautilusTrader. This setting must be enabled by the user in TWS/IB Gateway, as NautilusTrader is designed to work with UTC timestamps.”

This is a GUI checkbox in TWS/Gateway settings, not a Nautilus config flag. If not flipped, timestamps arrive in local time, the engine misinterprets them, and the entire event ordering breaks.

MK3 action: make this a launchd / Compose preflight check - fail fast if the Gateway is not configured for UTC. Ideally the Dockerized image bakes this in (most community images do).

Reference: paper vs live differences

The adapter is mode-agnostic at the wire level. The only differences:

AspectPaperLive
Port4002 (Gateway) / 7497 (TWS)4001 (Gateway) / 7496 (TWS)
Account IDDU… prefix (e.g., DUP696099)U… prefix (e.g., U987654)
trading_mode"paper" on DockerizedIBGatewayConfig"live"
Market dataSame subscriptions; DELAYED_FROZEN works for devTypically REALTIME
CredentialsSame IB loginSame IB login
Order behaviorSimulated fills; matches venue rules approximatelyReal fills, real money
ReconciliationSameSame
ReconnectionSameSame
Pacing limitsSameSame

Critical observation: paper trading on IBKR is closer to live than most simulators because it routes orders through the same matching infrastructure - the differences are stochastic (paper fills sometimes more generously than live for marketable limits) but the adapter shape is identical.

Running paper + live simultaneously

The “Multiple IB execution clients” pattern lets you run both:

exec_clients = {
    "IB-PAPER": InteractiveBrokersExecClientConfig(
        ibg_port=7497, ibg_client_id=2, account_id="DUP696099",
        ...,
    ),
    "IB-LIVE": InteractiveBrokersExecClientConfig(
        ibg_port=7496, ibg_client_id=3, account_id="U987654",
        routing=RoutingConfig(default=True),
        ...,
    ),
}

Useful for shadow-testing a new strategy on paper while live production runs the proven version. Caveat: each ibg_client_id must be unique across the entire TWS/Gateway instance - collisions silently break order routing.

Common pitfalls

Compiled from the IBKR doc’s troubleshooting section, reference_ibkr_api.md’s “Critical Gotchas,” and prior Cortana incidents:

Connection issues

  1. Connection refused (TCP-level) - TWS/Gateway not running, wrong port, firewall. IB error 502 is OS-level, not in TWS logs. Verify Gateway is listening on 127.0.0.1:4002 before debugging Nautilus.
  2. Authentication errors - wrong creds, account not logged in. Especially common for live accounts with 2FA - bump connection_timeout to 600s if 2FA is push-based and slow.
  3. Client ID conflicts - every ibg_client_id must be unique. Collisions silently break order routing. Watch out: ibg_client_id=0 is special - it receives all trades from the TWS GUI plus every API client. Use ibg_client_id=1+ for Cortana.
  4. connection_timeout=300 default is 5 min. On a fresh Gateway boot with 2FA, this can be tight. Bump to 600 for production.

Market data issues

  1. Wait for farm connection (codes 2104/2106) before subscribing. Per reference_ibkr_api.md: requests sent before the farm-OK message may get NO response. Nautilus’s connect handshake handles this; user-strategy code should not call request_* from __init__ - use on_start.
  2. Codes 2104/2106/2158 are healthy (“market data farm connection is OK”) - not errors. Don’t alert on them.
  3. reqContractDetails for full options chain is throttled. Use build_options_chain=True (which uses the unthrottled reqSecDefOptParams underneath).
  4. Regulatory snapshots cost $0.01 each, even on paper. Don’t tight-loop snapshot calls.
  5. Historical vs real-time volume differs - IB filters away-from-NBBO trades from historical. Backtests using historical bars may not match live tick volume exactly.
  6. Max 50 messages/second total. Exceeding triggers error 100
    • disconnect. Nautilus’s request scheduler respects this.

Order behavior

  1. OCA / OCO does not auto-create OCA groups. Set IBOrderTags(ocaGroup=..., ocaType=1) explicitly. This is the most-likely-to-bite-Cortana gotcha during the spike - V1 brackets need the OCA tags or the legs won’t actually cancel each other.
  2. Bracket parent + children need ~50ms separation (IB error 10006). Nautilus’s submit_order_list handles this internally - but if you bypass and submit manually, insert the delay.
  3. PendingCancel / PreCancelled may still fill. Don’t assume a cancel-in-flight is final. Listen for OrderCanceled (terminal) or OrderFilled (race winner).
  4. Order IDs must always increase across sessions. Nautilus owns client_order_id synthesis; do not pass IDs manually. The engine handles persistent monotonicity.
  5. Duplicate orderStatus messages are normal - IB sometimes delivers the same status twice. Nautilus dedupes via Order.is_duplicate_fill() (4-field check) and the Order.apply() trade_id invariant. See nautilus-execution.md § Race-condition handling.

Operational

  1. Daily restart required - TWS / Gateway must restart daily to refresh contract definitions. v974+ has auto-restart. Cortana’s operating window is well clear of the maintenance window.
  2. TWS UTC setting - must be flipped via GUI before connect. Make this a preflight check in Compose / launchd.
  3. read_only_api=True for data-only deploys - strongest software kill-switch IB exposes.
  4. Connection 1100/1101/1102 codes indicate connectivity loss and recovery. 1101 = data lost, must re-subscribe. Nautilus’s watchdog handles re-subscribe automatically; user code should not.
  5. Positions for FA accounts with > 50 subaccounts use reqPositionsMulti. Not relevant to Cortana single-account setup; flagged here in case multi-tenant ever consolidates many sub-accounts under one master.

Cortana MK3 implications

(a) Dockerized Gateway adoption plan + plist replacement

Adopt Dockerized IB Gateway for both paper and live, replacing the launchd plist for Gateway supervision. The engine plist remains for engine-process supervision, but the Gateway moves to its own container with its own restart policy and healthcheck.

Migration steps:

  1. Pre-spike (this weekend): stand up a docker-compose.yml with one ib-gateway service, paper mode, port 4002. Verify Cortana MK2 connects to it as a drop-in replacement for the standalone Gateway.
  2. Spike Saturday: confirm Nautilus DockerizedIBGatewayConfig bootstraps the same container shape end-to-end.
  3. Post-spike if MK3 GO: bake the Gateway image into the MK3 deploy, switch the engine plist to a Compose service, add healthcheck-driven alerts.
  4. Live migration (MK3 M5/M6): same Compose stack, port 4001, trading_mode: live, real account U… ID.

Standalone-win regardless of MK3: even if the spike says NO-GO, the MK2 codepath benefits from a containerized Gateway because it isolates the auth + restart story. The 2026-05-06 outage proved the Gateway needs supervision separate from the engine.

(b) Greeks path decision (carryover #8)

Recommendation: dual-path, UW primary in live, BSM primary in backtest. Drop the “native IBKR Greeks” target from the carryover - Nautilus does not implement it and there is no signal that Nautilus will.

ModePrimaryFallback
BacktestGreeksCalculator (deterministic)n/a
PaperUW WebSocket Greeks (custom data)GreeksCalculator if UW down
LiveUW WebSocket Greeks (custom data)GreeksCalculator if UW down

Implementation: see nautilus-greeks.md for the calculator; see nautilus-adapters.md UW adapter sketch for the custom-data shape.

File this decision with the spike outcome - it closes carryover #8.

(c) Per-tenant IBKR creds via exec_clients={"IB": ...}

For MK3 multi-tenant (M4+), each tenant gets its own execution client entry:

exec_clients = {
    f"IB-{tenant.id}": InteractiveBrokersExecClientConfig(
        ibg_host="127.0.0.1",
        ibg_port=4002 + tenant.gateway_offset,    # one Gateway container per tenant
        ibg_client_id=1,                          # within their gateway, ID=1
        account_id=tenant.ib_account_id,
        instrument_provider=instrument_provider_config,
        dockerized_gateway=tenant_gateway_config,  # per-tenant credentials
        routing=RoutingConfig(default=False),
    )
    for tenant in active_tenants
}

Important constraint: a single TWS/Gateway instance allows max 32 client IDs. Multi-tenant beyond ~10 active clients should use one Gateway container per tenant, not a shared Gateway. Per-tenant isolation is also better for auth (tenant brings their own IBKR credentials, container holds only their secrets), restart blast radius, and audit.

Per-tenant account_id is mandatory - it’s the routing key for fills back to the right tenant’s positions. The framework’s AccountId is IB-CODY-DUP696099 style (issuer-prefix + raw account).

(d) Order types: venue-native vs emulated

For Cortana V1 + MK3 V1:

Order shapePath
Bracket parent (entry MARKET)Venue-native (cannot be emulated; MARKET passes through)
Bracket TP (LIMIT child)Emulated with emulation_trigger=MARK_PRICE - software fallback (feedback_dual_tp_defense_in_depth)
Bracket SL (STOP_MARKET child)Emulated with emulation_trigger=MARK_PRICE - same defense-in-depth
OCO group on the bracketVenue-native - but MUST use IBOrderTags(ocaGroup=..., ocaType=1) explicitly
EOD flatten (market_exit())Venue-native MARKET reduce-only
Cancel / modifyVenue-native (cancels skip RiskEngine; see nautilus-execution.md)

The combination that gives Cortana defense-in-depth on exits: emulated TP/SL legs (Nautilus owns the trigger) + native OCA group (IBKR owns the cancel-on-fill). Both layers are independent: if IBKR’s OCA fails, the local emulator fires the fallback. If the local emulator fails, IBKR’s bracket fires.

(e) Market data subscriptions Cortana needs to confirm active

For SPY 0DTE in production:

  1. OPRA ($1.50/mo) - required for the 0DTE option chain.
  2. NASDAQ Network C/UTP ($1.50/mo) - SPY shares trades + quotes.
  3. NYSE Securities Snapshot Bundle ($4.50/mo, optional) - SPY trade-by-trade detail (Cortana can survive without this if OPRA + UTP are active).

Verify ahead of MK3 live cutover: log into IB account → Settings → User Settings → Market Data Subscriptions → confirm all three active for the target IB account (paper account uses live data subscription if you’ve paid for it).

For paper / dev: IBMarketDataTypeEnum.DELAYED_FROZEN is free and sufficient for plumbing validation.

Open spike-day questions

  1. Bracket emulation propagation: when emulation_trigger is set on order_factory.bracket(...), does it apply to the parent only / children only / all three? Read nautilus_trader/common/factories/order_factory.bracket(...) on Saturday. (Carries over from nautilus-orders.md Q1.)
  2. OCA ocaType interaction with emulated children: if both legs are emulated and one fires locally, does the framework cancel the IBKR-side OCA group? Or does only the local emulator know? Test on paper.
  3. build_options_chain=True startup time: how long does it take to materialize SPY’s 0DTE chain (~80-100 strikes × 2 sides = ~200 contracts) at engine boot? Pacing matters; if startup spans minutes, that delays the 9:30 ET first signal.
  4. Reconciliation behavior for emulated orders across reconnect: if the engine restarts mid-trade with an EMULATED-state order, does LiveExecutionEngine.reconcile_state() resume it, or is it dropped? (Same as nautilus-orders.md Q6.)
  5. Multi-Gateway scaling beyond 32 client IDs: how does Nautilus route when two DockerizedIBGatewayConfig instances run on the same host? Verify port collision handling.

When this concept applies

  • Spike-day implementation of the IBKR data + execution clients.
  • MK3 deploy planning - which Gateway path, how secrets flow.
  • Multi-tenant per-tenant IBKR config design (M4 / M5).
  • Replacing the launchd Gateway-supervision approach with a containerized + healthcheck-driven approach.
  • Closing carryover #8 (Greeks path).
  • Validating paper-vs-live differences are config-only (no code changes).

When it does not apply

See Also

  • Nautilus Integrations - the umbrella page introducing the IBKR adapter alongside other venues; this page is the canonical operational deep-dive
  • Nautilus Adapters - five-component adapter contract; the abstract structure the IBKR adapter implements
  • Nautilus Execution - the execution pipeline; reconciliation reports the IBKR adapter must emit
  • Nautilus Orders - bracket emulated-mode resolution; OCO/OCA semantics
  • Nautilus Options - options chain model (strike selection, ATM tracker)
  • Nautilus Greeks - local BSM calculator and the venue-stream alternative (carryover #8)
  • Nautilus Order Book - L2 shares / L1 options OPRA cap
  • Nautilus Accounting - AccountState, margin snapshots, position model
  • Nautilus Live - LiveNode + TradingNode hosting; reconciliation cadence config
  • Nautilus Configuration - typed config contracts, per-tenant exec_client injection pattern
  • 2026-05-09 Nautilus Spike Plan: ~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-nautilus-spike.md
  • ~/.claude/projects/-Users-codysmith-conductor-workspaces-cortanaroi-cortanaroi-mk2/memory/reference_ibkr_api.md
    • IBKR TWS API master reference (ports, orders, market data, error codes, gotchas) - complementary to this page
  • ~/.claude/projects/-Users-codysmith-conductor-workspaces-cortanaroi-cortanaroi-mk2/memory/project_pm_ibkr_exit_invariant.md
    • the broker-truth invariant Nautilus’s reconciliation enforces
  • ~/.claude/projects/-Users-codysmith-conductor-workspaces-cortanaroi-cortanaroi-mk2/memory/feedback_dual_tp_defense_in_depth.md
    • software-fallback pattern realized via emulated bracket children
  • ~/.claude/projects/-Users-codysmith-conductor-workspaces-cortanaroi-cortanaroi-mk2/memory/project_system_hours.md
    • operating window vs IB daily-restart window
  • Source: https://nautilustrader.io/docs/latest/integrations/ib/

Timeline

2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 4.