Nautilus Instruments

Nautilus models every tradable as an Instrument - a precision-aware, identifier-keyed metadata object that carries tick size, lot size, multiplier, currency, expiry (if dated), and option-specific fields where relevant. Concrete subclasses cover cash markets (Equity, CurrencyPair, Commodity, IndexInstrument), futures (FuturesContract, FuturesSpread, CryptoFuture, CryptoPerpetual, PerpetualContract), options (OptionContract, OptionSpread, CryptoOption, BinaryOption), and OTC/specialized (Cfd, BettingInstrument, SyntheticInstrument). Identification is {symbol}.{venue} (e.g. SPY.ARCA). For IBKR options the symbol follows IB’s localSymbol-derived pattern {root}{YYMMDD}{C|P}{strike×1000, 8-digit zero-padded} - Cortana’s SPY 5/9 727 Call becomes SPY260509C00727000.SMART under IB_SIMPLIFIED. instrument.make_price(...) and make_qty(...) round raw inputs to the venue’s price_increment / size_increment - all order math goes through these methods, never raw arithmetic. InstrumentProvider is the adapter-supplied loader that materializes instruments at startup; load_all vs load_ids is the lazy-vs-eager switch. For Cortana V1 (SPY 0DTE only) we will lean on the IBKR adapter’s built-in OptionContract materialization with build_options_chain=True; no custom instruments needed.

Core claim

The Nautilus instrument model is rich enough to express Cortana V1’s SPY 0DTE universe, MK4+ ES futures + index options, and (future) BTC options without writing a custom instrument class. The framework owns: identifier discipline (typed InstrumentId/Symbol/Venue), tick/lot quantization (precision-aware fixed-point math through make_price / make_qty), and the venue-side metadata pipeline (InstrumentProvider). Cortana’s job in MK3 is to (a) configure the IBKR adapter to load the right instrument set at startup, (b) consume OptionContract instances out of the cache, and (c) - only if cross-venue replay demands it - reconcile the convention difference between IBKR’s SPY260509C00727000.SMART and Databento’s MIC-based encoding.

Instrument taxonomy (full)

The doc enumerates instrument classes by category:

CategoryClassNotes
Cash marketsEquityStocks/ETFs. SPY, QQQ, NVDA.
Cash marketsCurrencyPairFX (EUR/USD), spot crypto (BTCUSDT).
Cash marketsCommoditySpot metals, energy.
Cash marketsIndexInstrumentCash indices (SPX, NDX) - non-tradable directly but priced.
FuturesFuturesContractStandard listed futures: ESM5, CLN5. Has expiration_utc.
FuturesFuturesSpreadCalendar/inter-commodity spreads (e.g. ES Jun-Sep).
FuturesCryptoFutureDated crypto futures (Deribit, Binance Futures).
FuturesCryptoPerpetualPerp contracts (no expiry).
FuturesPerpetualContractGeneric perpetual (TradFi-style).
OptionsOptionContractSingle-leg call/put. Strike + expiry + kind + underlying.
OptionsOptionSpreadExchange-defined multi-leg combo (up to 4 legs each with ratio).
OptionsCryptoOptionCrypto-settled options. Inverse/quanto styles.
OptionsBinaryOptionFixed-payout (e.g. Polymarket) - no strike, no kind.
OTC / specializedCfdContracts for difference.
OTC / specializedBettingInstrumentSports books (Betfair).
OTC / specializedSyntheticInstrumentLocally-derived from a formula over components - venue is SYNTH, not directly tradable (but can trigger emulated orders on components).

Bonds and other fixed-income are handled inside the IBKR adapter via contract metadata (ISIN/CUSIP routing) rather than as a distinct Instrument subclass at the framework level.

Identification - InstrumentId recap

Per nautilus-value-types.md, the identity trio is:

  • Symbol - venue-native ticker. Validated at construction, no whitespace drift.
  • Venue - venue identifier (IB, BINANCE, OPRA, ARCA, SMART, …).
  • InstrumentId - composite. Format: {symbol}.{venue}. Constructed via InstrumentId(Symbol("..."), Venue("...")) or InstrumentId.from_str("..."). Required uniqueness: “the {symbol}.{venue} combination must be unique for a Nautilus system.”

Examples (from the docs):

  • BTCUSDT.BINANCE (crypto spot)
  • ETHUSDT-PERP.BINANCE (crypto perp)
  • EUR/USD.IDEALPRO (FX, IBKR IB_SIMPLIFIED)
  • BF-B.NYSE (equity with class share, IBKR IB_SIMPLIFIED - spaces in IB localSymbol replaced with hyphens)
  • SPY.ARCA (equity, IBKR IB_SIMPLIFIED)
  • AAPL230217P00155000.SMART (option, IBKR IB_SIMPLIFIED - see § Options)
  • AAPL=STK.SMART (equity, IBKR IB_RAW - {localSymbol}={secType}.{exchange})

raw_symbol vs derived symbol: the raw_symbol field on the instrument preserves whatever the venue’s API delivered before any Nautilus-side massaging (hyphen substitution, namespace prefixing). For audit + replay, the raw form is the receipt; the constructed InstrumentId is the addressable handle.

Precision and increment fields

Every instrument carries four scalar fields that govern price/size math:

FieldPurposeExample
price_precisionDecimal places on prices, triggers, fills2 for SPY equity (cents)
price_incrementTick size as a Price valuePrice(0.01, 2)
size_precisionDecimal places on quantities0 for equity options (whole contracts)
size_incrementLot size as a Quantity valueQuantity(1, 0)

Hard rule from the doc: “Increment precision must exactly match declared precision.” Constructing Equity(price_precision=2, price_increment=Price(0.001, 3)) raises at construction. There is no implicit rounding - Nautilus rejects mismatch fast.

The matching engine (in backtest), the RiskEngine (in pre-trade validation), and the venue itself enforce these increments. The user-side escape hatch is instrument.make_price(...) and instrument.make_qty(...) which round arbitrary inputs to the declared grid:

instrument = self.cache.instrument(instrument_id)
price = instrument.make_price(0.90500)   # rounded to price_increment
qty   = instrument.make_qty(150)         # rounded to size_increment

These methods “round the input to the instrument’s declared precision, ensuring the result will pass precision checks.” Strategies should call them at every order-submission point - never construct Price(...) or Quantity(...) directly inside trading logic.

Multiplier and notional

multiplier is the contract size factor used in P&L, margin, and notional calculations:

  • Equity - multiplier = 1 (one share = one share).
  • OptionContract (standard SPY equity option) - multiplier = 100 (one contract = 100 underlying shares).
  • FuturesContract (ES) - multiplier = 50 (one E-mini = $50 × index).
  • IndexInstrument (SPX cash) - varies by venue.

Strategies never multiply by 100 by hand. Premium-paid = instrument.notional_value(qty, price) (returns Money in the instrument’s currency); P&L flows through instrument.calculate_pnl(...) likewise. This closes one of the most common MK2 bug surfaces (“forgot to multiply by multiplier”).

Limits (optional fields)

Exchange-imposed bounds, present where the venue declares them:

  • max_quantity, min_quantity - order size bounds
  • max_notional, min_notional - order notional bounds
  • max_price, min_price - price bounds (rare for equities; common for perps with insurance fund logic)

Absent fields default to None (unbounded). The RiskEngine consults these pre-trade.

Margin and fees

  • margin_init - initial margin rate (decimal, e.g. 0.10 for 10× leverage).
  • margin_maint - maintenance margin rate.
  • maker_fee - typically negative (rebate) for maker fills.
  • taker_fee - typically positive (commission) for taker fills.

These flow into the Account / Portfolio for margin reservation and commission accumulation. Cortana V1 is cash-options on a margin-disabled paper account - margin_init/margin_maint are advisory at the framework level, while IBKR enforces real margin server-side.

Currency fields

Per-instrument currency triple:

  • base_currency - the unit being traded (BTC for BTCUSDT).
  • quote_currency - the unit prices are quoted in (USDT for BTCUSDT).
  • settlement_currency - the unit P&L settles in (often = quote_currency, but can differ for inverse/quanto futures).

Money arithmetic enforces same-currency-only addition; cross-currency ops require explicit FX conversion through the Portfolio. For SPY options: base_currency = USD, quote_currency = USD, settlement_currency = USD - single-currency simplicity that won’t bite us until MK4+ multi-venue.

Expiration handling

Dated instruments (FuturesContract, OptionContract, OptionSpread, CryptoFuture, CryptoOption, BinaryOption) carry expiration_utc as a UTC timestamp. Day-count and DTE math is not baked into the instrument - there is no instrument.dte() helper. Strategies compute DTE off clock.utc_now() - expiration_utc. For Cortana 0DTE that means DTE = 0 on entry, in seconds rather than days, and the live theta in OptionGreeks is what drives decay-aware sizing - see nautilus-options.md.

Option-specific fields (OptionContract)

From the doc + code:

FieldDescription
strike_pricePrice - the option’s strike.
option_kindOptionKind.CALL / OptionKind.PUT.
expiration_utcUTC expiry timestamp.
underlyingUnderlying InstrumentId (e.g. SPY.ARCA).
multiplierStandard SPY = 100; some products differ.
price_increment / size_incrementPer-venue grid (penny increments for SPY weeklies; nickel for some).
margin_init / margin_maintFor short-premium plays.

Exercise style: the doc references American vs European but does not expose exercise_style as a top-level field on OptionContract - the distinction shows up via the IBKR adapter’s contract metadata and the venue’s exercise / assignment events. SPY options are American-style (can be exercised any time before expiry); SPX options are European (exercise at expiry only). For Cortana V1 (“never hold past close”) this distinction is mooted by the exit invariant - see nautilus-options.md § “Exercise / assignment”.

OptionSpread is a venue-defined combo (vertical, calendar, butterfly) with up to 4 legs each carrying a signed ratio. Position-side decomposes back to per-leg OptionContract positions. Cortana V1 is single-leg only - OptionSpread is dormant until V2.

BinaryOption deliberately omits strike_price, option_kind, and underlying - binary outcomes don’t have them. Polymarket-style markets use this.

IBKR option symbol encoding (the load-bearing detail for Cortana)

Per nautilus-integrations.md and the IBKR docs, options under SymbologyMethod.IB_SIMPLIFIED follow:

{localSymbol_with_spaces_removed}.{exchange}

The IB localSymbol for an equity option is the OCC-compatible 21-char string: {root}{YYMMDD}{C|P}{strike×1000, 8-digit zero-padded}.

Verbatim doc example: AAPL230217P00155000.SMART (AAPL, 2023-02-17 expiry, Put, $155.00 strike).

Cortana SPY 5/9/26 727 Call: SPY260509C00727000.SMART.

Decomposition:

  • SPY - root
  • 260509 - 2026-05-09 expiry (YYMMDD)
  • C - Call
  • 00727000 - strike $727.00, scaled ×1000 = 727000, zero-padded to 8 digits
  • .SMART - IB SMART routing exchange (the venue side of InstrumentId; .AMEX / .CBOE are alternates if explicitly routed)

Under IB_RAW the form changes to {localSymbol}={secType}.{exchange}: SPY260509C00727000=OPT.SMART. Cortana should pick one symbology mode and stick with it (IB_SIMPLIFIED is the recommended default - it matches OCC OSI conventions closely enough that humans can read it).

This format is OCC’s OSI (Options Symbology Initiative) layout applied to the IB localSymbol. Nautilus does not document an OCC/OPRA abstraction layer - the OSI string lives inside the venue-specific symbol; the Venue half of InstrumentId (.SMART, .OPRA, .CBOE) disambiguates which exchange/feed produced it.

InstrumentProvider - the metadata loader

Every adapter ships an InstrumentProvider that materializes venue instrument definitions into Nautilus Instrument objects. Detail in nautilus-adapters.md; the instrument-side contract:

Methods

  • await provider.load_all_async() - full-universe load. Used in research / backtesting scripts and when load_all=True in config.
  • await provider.load_ids_async(instrument_ids) - load a specific set. Used in production when load_ids=[...] in config.
  • await provider.load_async(instrument_id) - single-instrument load.
  • provider.list_all() - returns currently-loaded instruments.
  • provider.find(instrument_id) - point lookup; returns Instrument or None.

Config

InstrumentProviderConfig controls startup behavior:

# Load every instrument the venue exposes (research / dev)
InstrumentProviderConfig(load_all=True)
 
# Load only specific instruments (recommended for prod)
InstrumentProviderConfig(
    load_ids=["BTCUSDT-PERP.BINANCE", "ETHUSDT-PERP.BINANCE"],
)

For IBKR specifically, InteractiveBrokersInstrumentProviderConfig extends this with:

  • load_ids=frozenset(["SPY.ARCA"]) - equity universe.
  • load_contracts=[IBContract(secType="STK", symbol="SPY", build_options_chain=True, min_expiry_days=0, max_expiry_days=1)] - the 0DTE-chain shortcut: load SPY equity and materialize all expiring-today option strikes as OptionContract instances.
  • min_expiry_days / max_expiry_days - DTE filter band.
  • cache_validity_days - symbol cache TTL.

build_options_chain=True is the Cortana-MK3-shaped pattern: at startup the IBKR adapter auto-derives every OptionContract for SPY 0DTE without us specifying strikes, expiries, or strike grids by hand.

Two access patterns

  1. Standalone - research / backtest scripts call provider.load_all_async() directly and iterate instruments.
  2. Runtime - Actors and Strategies receive instruments via on_instrument(instrument) callbacks as the provider streams them. Strategies do not call load_* themselves; the LiveNode orchestrates.

Custom instrument creation

For custom or simulated instruments (backtesting against synthetic universes; venues without a Nautilus adapter):

from nautilus_trader.model.instruments import OptionContract
from nautilus_trader.model.identifiers import InstrumentId, Symbol, Venue
from nautilus_trader.model.objects import Price, Quantity, Currency
from nautilus_trader.model.enums import OptionKind
from datetime import datetime, timezone
 
opt = OptionContract(
    instrument_id=InstrumentId.from_str("SPY260509C00727000.SMART"),
    raw_symbol=Symbol("SPY260509C00727000"),
    asset_class=AssetClass.EQUITY,
    currency=Currency.from_str("USD"),
    price_precision=2,
    price_increment=Price.from_str("0.01"),
    size_precision=0,
    size_increment=Quantity.from_int(1),
    multiplier=Quantity.from_int(100),
    underlying="SPY.ARCA",
    strike_price=Price.from_str("727.00"),
    option_kind=OptionKind.CALL,
    expiration_utc=datetime(2026, 5, 9, 20, 0, tzinfo=timezone.utc),
    margin_init=Decimal("0"),
    margin_maint=Decimal("0"),
    maker_fee=Decimal("0"),
    taker_fee=Decimal("0"),
    ts_event=0,
    ts_init=0,
)

(Construction signature paraphrased - exact Nautilus API may rename fields; verify via Python API ref during the spike.) For Cortana V1 we do not need to do this - the IBKR adapter materializes OptionContract instances natively and they live in the cache. Custom construction is only relevant for backtest replay over Databento data where we may need to mint instruments before any IBKR connection exists.

SyntheticInstrument

Locally-derived instrument with venue SYNTH - not directly tradable. Useful for spread analytics, basket pricing, custom indices.

from nautilus_trader.model.instruments import SyntheticInstrument
 
synth = SyntheticInstrument(
    symbol=Symbol("BTC-ETH"),
    price_precision=8,
    components=[
        InstrumentId.from_str("BTCUSDT.BINANCE"),
        InstrumentId.from_str("ETHUSDT.BINANCE"),
    ],
    formula="BTCUSDT.BINANCE / ETHUSDT.BINANCE",
)
# instrument_id == "BTC-ETH.SYNTH"

Formula language supports + - * / % ^, comparisons, boolean logic, abs / ceil / floor / round / min / max / if, local variable assignments. Components must already exist in the cache.

Synthetics cannot be traded directly but can act as a trigger_instrument_id for emulated orders that fire venue-side orders on the components.

For Cortana: candidate uses are SPY-vs-VIX spread analytics, beta- weighted basket pricing - none required for V1, optional for MK4+.

Instrument lifecycle (loaded vs unloaded)

The doc treats instruments as append-only metadata: once loaded, they remain in the InstrumentProvider’s registry and the cache for the session. There’s no documented “unload” path - symbol churn (a futures contract expiring, an option series rolling off) is handled by:

  1. Expiry semantics on the instrument itself - strategies check clock.utc_now() < instrument.expiration_utc before submitting orders.
  2. Re-loading on TradingNode restart - startup re-runs load_ids_async / load_contracts discovery; expired instruments simply don’t come back.
  3. Per-day chain rebuild for 0DTE - the IBKR adapter’s build_options_chain=True runs at every startup, so each trading day we get the freshly-listed 0DTE strikes.

Cache stores all loaded instruments by InstrumentId. cache.instrument(id) is the primary lookup. cache.instruments(venue=Venue("IB")) lists by venue. See nautilus-cache.md for full Cache contract.

No documented corporate-actions handling. Stock splits, dividends, ticker changes are not modeled at the instrument level - they require either (a) reload after the corporate action takes effect at the venue, or (b) a custom adapter layer that detects the change and emits an instrument update event. For Cortana SPY 0DTE this is mooted: SPY itself doesn’t split, dividends are continuous, and 0DTE options expire same-day so there’s no overnight CA exposure.

Cortana MK3 implications

(a) SPY equity InstrumentId across IBKR vs Databento - the conventions question

IBKR side (IB_SIMPLIFIED): SPY.ARCA is the canonical form. Per the IBKR doc: stocks follow {localSymbol}.{primaryExchange}, and SPY’s primary listing exchange is NYSE Arca (ARCA). It is not SPY.SMART (that’s a routing destination, not a listing) and not SPY.NASDAQ (SPY is not NASDAQ-listed). SPY.NYSE would be an alternate form some adapters use, but Nautilus’s IBKR adapter under IB_SIMPLIFIED resolves to SPY.ARCA per the doc’s verbatim example.

Databento side: the Databento adapter “uses an ISO 10383 MIC (Market Identifier Code) from the definition message for the Nautilus venue.” For SPY equity Databento publishes the MIC of the listing venue (likely XASE for NYSE American or ARCX for NYSE Arca; the adapter pulls this from the definition message rather than hard-coding).

The convention difference: IBKR’s SPY.ARCA and Databento’s SPY.ARCX (or whichever MIC Databento returns) refer to the same real-world instrument with different InstrumentId strings. Cortana MK3 needs an explicit reconciliation layer when bridging:

  1. Live trading uses IBKR exclusively - SPY.ARCA is the only form strategies see at runtime.
  2. Backtest replay over Databento - Databento-loaded ParquetDataCatalog may have SPY.ARCX (or similar). Either (a) re-stamp the catalog instruments to SPY.ARCA at load time, or (b) write an InstrumentId translation map that the strategy uses transparently.

The cleanest fix is option (a): the DatabentoDataLoader step in the spike (Step 0.5 of the spike plan) writes the catalog with IBKR venue codes so backtest and live see identical InstrumentId strings. Verify this is configurable on the loader during the spike; if not, file a translation layer.

(b) SPY OPRA option InstrumentId encoding

For Cortana’s SPY 5/9/26 727 Call:

  • IBKR (IB_SIMPLIFIED): SPY260509C00727000.SMART - OCC OSI 21-char localSymbol + .SMART venue. This is what cache.instrument(id) returns when the IBKR adapter materializes the 0DTE chain.
  • IBKR (IB_RAW): SPY260509C00727000=OPT.SMART.
  • Databento: the doc does not state the venue suffix verbatim, but Databento’s OPRA feed uses MIC OPRA for consolidated options data. Likely form: SPY260509C00727000.OPRA or with the per-exchange MIC if Databento delivers per-venue prints.
  • OCC OSI standalone: SPY 260509C00727000 (root padded to 6 chars with spaces - this is the on-the-wire exchange format, not used as a Nautilus InstrumentId).

The mismatch with IBKR’s equity-side SPY.ARCA: equity is .ARCA because that’s the listing exchange; options are .SMART because IB routes options through SMART by default. Strategies should not hardcode the venue suffix - extract from the InstrumentId returned by the chain query, never reconstruct from string parts.

Cortana MK3 implication for symbology choice: stick with IB_SIMPLIFIED everywhere. It maps cleanly to OSI for human readability and is the doc-blessed default. Document the equity-vs-option venue asymmetry (.ARCA for SPY equity, .SMART for SPY options) in the strategy’s instrument-resolution layer so future contributors don’t trip over it.

(c) MK4+ ES futures and futures options

FuturesContract carries expiration_utc and the standard precision fields. For ES the form would be ESM5.GLOBEX or similar (verify via IBKR adapter’s actual symbology output during MK4 planning).

build_options_chain=True extends to FOPs (futures options) but the underlying must be a FuturesContract registered in the cache. MK4+ config sketch:

load_contracts=[
    IBContract(secType="FUT", symbol="ES", lastTradeDateOrContractMonth="202506",
               exchange="GLOBEX", build_options_chain=True,
               min_expiry_days=0, max_expiry_days=7),
]

Multiplier semantics differ from equity options: ES options carry multiplier=50 (matches the futures contract), not 100. Strategies that hard-coded * 100 for SPY would silently mis-size ES - another reason all dollar math goes through instrument.notional_value(...), not hand-rolled.

(d) Custom Cortana instruments

For V1 - none. IBKR adapter materializes everything we trade.

For V2+ candidates (deferred):

  • A SyntheticInstrument for SPY-VIX delta if Cortana adds vol- regime-aware sizing - derived locally, untradable, signal-only.
  • A custom non-Instrument Data subclass for UW alerts (UWFlowAlert, UWGexUpdate) - these are signals about instruments, not instruments themselves; see nautilus-custom-data.md.
  • Possibly a BinaryOption wrapper for any event-contract signal source Cortana ingests in MK5+ (Polymarket / Kalshi) - clearly out-of-scope for the spike.

See Also


Timeline

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