Nautilus Interactive Brokers Integration
Nautilus ships a first-class IBKR adapter (
nautilus_trader[ib]) that wraps IB’sibapilibrary and exposes equities, ETFs, equity options, futures (including continuous), options-on-futures, forex (IDEALPRO), crypto (PAXOS), bonds, indices, CFDs, and commodities through the standard NautilusDataClient/ExecutionClient/InstrumentProvidercontracts. 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 viaDockerizedIBGatewayConfig(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 throughsubscribe_option_greeks. Cortana must compute Greeks locally (GreeksCalculator) or back-fill from a UW custom data feed. Multi-account / multi-tenant is supported via multipleexec_clientsentries with uniqueibg_client_id+account_idvalues.
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.
| Aspect | TWS (Trader Workstation) | IB Gateway |
|---|---|---|
| Purpose | Full GUI trading platform | API-only minimal app |
| Resource footprint | ~2-3 GB RAM, full GUI | ~600-800 MB RAM (~40% less per reference_ibkr_api.md) |
| Paper port | 7497 | 4002 |
| Live port | 7496 | 4001 |
| Auto-restart (v974+) | Yes (Sunday-to-Sunday) | Yes |
| Daily restart | Required (contract defs refresh) | Required |
| Auth | GUI login required | GUI login required (standalone); programmatic in Dockerized variant |
| Best for | Manual debug + trading | Automation + 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)
| Application | Paper Trading | Live Trading |
|---|---|---|
| TWS | 7497 | 7496 |
| IB Gateway | 4002 | 4001 |
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: 1gThe 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:
- 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. - Direct
username/passwordonDockerizedIBGatewayConfig- acceptable but only when those values are themselves resolved from a secret store at config-build time (never hardcoded). - 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:
- The plist supervises the engine, not the Gateway.
- When the Gateway exited cleanly during a power-cycle, no watchdog noticed.
- 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):
| Subscription | Tier | Required for |
|---|---|---|
| NASDAQ (Network C/UTP) | $1.50/mo | SPY trades + quotes (SPY trades on ARCA but routes via SMART; the underlying data farm is NASDAQ) |
| NYSE Trades and OPRA Subscription | $4.50/mo | SPY trade-by-trade (US Securities Snapshot bundle covers this) |
| OPRA (Options Price Reporting Authority) | $1.50/mo | All US equity option quotes + trades (the entire SPY 0DTE chain) |
| NASDAQ TotalView ArcaBook | optional | L2 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
| Value | Meaning | Cost | Use case |
|---|---|---|---|
REALTIME | Live | Subscription required | Production live trading |
DELAYED | 15-20 min delayed | Free | Development against real (delayed) feed |
DELAYED_FROZEN | Delayed + frozen (does not update) | Free | Smoke tests, fixture replay |
FROZEN | Last known real-time when market closed | Subscription required | Off-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).
reqContractDetailsfor 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.” UsereqSecDefOptParams(which Nautilus invokes underbuild_options_chain=True) - it is not throttled perreference_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:
| AccountType | Meaning |
|---|---|
CASH | Cash account; orders fully funded from balances |
MARGIN | Margin account; portfolio margin or Reg-T |
FUTURES_MARGIN | Futures margin (SPAN-based for IB futures) |
Per the IBKR doc’s “Position management” table:
| Feature | Supported | Notes |
|---|---|---|
| Query positions | yes | Real-time position updates |
| Position mode | yes | Net vs separate long/short positions |
| Leverage control | yes | Account-level margin requirements |
| Margin mode | yes | Portfolio 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/positioncallbacks) - 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 type | Maps to | Native at IBKR? |
|---|---|---|
MARKET | MKT | Yes |
LIMIT | LMT | Yes |
STOP_MARKET | STP | Yes |
STOP_LIMIT | STP LMT | Yes |
MARKET_IF_TOUCHED | MIT | Yes |
LIMIT_IF_TOUCHED | LIT | Yes |
TRAILING_STOP_MARKET | TRAIL | Yes |
TRAILING_STOP_LIMIT | TRAIL LIMIT | Yes |
MARKET + TimeInForce.AT_THE_CLOSE | MOC | Yes |
LIMIT + TimeInForce.AT_THE_CLOSE | LOC | Yes |
| Bracket (parent + TP + SL) | Parent + child IDs | Yes |
| OCO group | OCA group with IBOrderTags | Yes (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
| TIF | Supported | Notes |
|---|---|---|
DAY | yes | Cortana V1 default - flatten by close |
GTC | yes | Avoid for 0DTE (would survive past close - invariant violation) |
IOC | yes | Useful for marketable-but-cancel-rest semantics |
FOK | yes | All-or-nothing |
GTD | yes | Good-Till-Date with explicit expiry |
AT_THE_OPEN | yes | MOO / LOO orders |
AT_THE_CLOSE | yes | MOC / 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:
| Type | Name | Behavior |
|---|---|---|
| 1 | Cancel All with Block | Default - safest, prevents overfills |
| 2 | Reduce with Block | Proportionally reduce, with overfill protection |
| 3 | Reduce without Block | Fastest 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:
load_ids (recommended for simple cases)
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:
| Mode | Format | Example |
|---|---|---|
IB_SIMPLIFIED (default) | Human-readable, asset-class-aware | SPY.ARCA, AAPL230217P00155000.SMART, EUR/USD.IDEALPRO |
IB_RAW | Direct mirror of IB API fields | AAPL=STK.SMART, IBUS30=CFD.IBCFD |
| MIC venue conversion | Standardized MIC codes | SPY.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:
| Config | Effect |
|---|---|
min_expiry_days=0, max_expiry_days=1 | 0DTE only (today + tomorrow) - Cortana’s V1 |
min_expiry_days=7, max_expiry_days=30 | Weekly options window |
min_expiry_days=0, max_expiry_days=60 | Two-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.
Recommended dual-path
Both paths, switched by mode:
| Mode | Primary Greeks source | Fallback |
|---|---|---|
| Backtest | GreeksCalculator (local BSM) | n/a (deterministic replay only) |
| Paper / Live | UW custom data | GreeksCalculator 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_ATTEMPTSenv var (default unlimited). - Connection timeout via
connection_timeoutconfig (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:
- Pull all open orders, all positions, account balances.
- Compare against the Cache.
- Synthesize events (
OrderAccepted,OrderFilled,PositionOpened) for venue state not in cache, markedreconciliation=True. - Continuous reconciliation continues at
open_check_interval_secsthereafter.
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:
| Aspect | Paper | Live |
|---|---|---|
| Port | 4002 (Gateway) / 7497 (TWS) | 4001 (Gateway) / 7496 (TWS) |
| Account ID | DU… prefix (e.g., DUP696099) | U… prefix (e.g., U987654) |
trading_mode | "paper" on DockerizedIBGatewayConfig | "live" |
| Market data | Same subscriptions; DELAYED_FROZEN works for dev | Typically REALTIME |
| Credentials | Same IB login | Same IB login |
| Order behavior | Simulated fills; matches venue rules approximately | Real fills, real money |
| Reconciliation | Same | Same |
| Reconnection | Same | Same |
| Pacing limits | Same | Same |
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
- 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:4002before debugging Nautilus. - Authentication errors - wrong creds, account not logged in.
Especially common for live accounts with 2FA - bump
connection_timeoutto 600s if 2FA is push-based and slow. - Client ID conflicts - every
ibg_client_idmust be unique. Collisions silently break order routing. Watch out:ibg_client_id=0is special - it receives all trades from the TWS GUI plus every API client. Useibg_client_id=1+for Cortana. connection_timeout=300default is 5 min. On a fresh Gateway boot with 2FA, this can be tight. Bump to 600 for production.
Market data issues
- 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 callrequest_*from__init__- useon_start. - Codes 2104/2106/2158 are healthy (“market data farm connection is OK”) - not errors. Don’t alert on them.
reqContractDetailsfor full options chain is throttled. Usebuild_options_chain=True(which uses the unthrottledreqSecDefOptParamsunderneath).- Regulatory snapshots cost $0.01 each, even on paper. Don’t tight-loop snapshot calls.
- 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.
- Max 50 messages/second total. Exceeding triggers error 100
- disconnect. Nautilus’s request scheduler respects this.
Order behavior
- 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. - Bracket parent + children need ~50ms separation (IB error
10006). Nautilus’s
submit_order_listhandles this internally - but if you bypass and submit manually, insert the delay. - PendingCancel / PreCancelled may still fill. Don’t assume a
cancel-in-flight is final. Listen for
OrderCanceled(terminal) orOrderFilled(race winner). - Order IDs must always increase across sessions. Nautilus owns
client_order_idsynthesis; do not pass IDs manually. The engine handles persistent monotonicity. - Duplicate orderStatus messages are normal - IB sometimes
delivers the same status twice. Nautilus dedupes via
Order.is_duplicate_fill()(4-field check) and theOrder.apply()trade_idinvariant. See nautilus-execution.md § Race-condition handling.
Operational
- 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.
- TWS UTC setting - must be flipped via GUI before connect. Make this a preflight check in Compose / launchd.
read_only_api=Truefor data-only deploys - strongest software kill-switch IB exposes.- 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.
- 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:
- Pre-spike (this weekend): stand up a
docker-compose.ymlwith oneib-gatewayservice, paper mode, port4002. Verify Cortana MK2 connects to it as a drop-in replacement for the standalone Gateway. - Spike Saturday: confirm Nautilus
DockerizedIBGatewayConfigbootstraps the same container shape end-to-end. - 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.
- Live migration (MK3 M5/M6): same Compose stack, port
4001,trading_mode: live, real accountU…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.
| Mode | Primary | Fallback |
|---|---|---|
| Backtest | GreeksCalculator (deterministic) | n/a |
| Paper | UW WebSocket Greeks (custom data) | GreeksCalculator if UW down |
| Live | UW 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 shape | Path |
|---|---|
| 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 bracket | Venue-native - but MUST use IBOrderTags(ocaGroup=..., ocaType=1) explicitly |
EOD flatten (market_exit()) | Venue-native MARKET reduce-only |
| Cancel / modify | Venue-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:
- OPRA ($1.50/mo) - required for the 0DTE option chain.
- NASDAQ Network C/UTP ($1.50/mo) - SPY shares trades + quotes.
- 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
- Bracket emulation propagation: when
emulation_triggeris set onorder_factory.bracket(...), does it apply to the parent only / children only / all three? Readnautilus_trader/common/factories/order_factory.bracket(...)on Saturday. (Carries over from nautilus-orders.md Q1.) - OCA
ocaTypeinteraction 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. build_options_chain=Truestartup 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.- Reconciliation behavior for emulated orders across reconnect:
if the engine restarts mid-trade with an
EMULATED-state order, doesLiveExecutionEngine.reconcile_state()resume it, or is it dropped? (Same as nautilus-orders.md Q6.) - Multi-Gateway scaling beyond 32 client IDs: how does Nautilus
route when two
DockerizedIBGatewayConfiginstances 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
- Cryptocurrency or non-US-equity venues - those have their own adapter pages.
- The general adapter contract - see nautilus-adapters.md.
- The general execution pipeline - see nautilus-execution.md.
- The general options model - see nautilus-options.md.
- Greeks ergonomics - see nautilus-greeks.md.
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+TradingNodehosting; reconciliation cadence config - Nautilus Configuration - typed config
contracts, per-tenant
exec_clientinjection 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.