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’slocalSymbol-derived pattern{root}{YYMMDD}{C|P}{strike×1000, 8-digit zero-padded}- Cortana’s SPY 5/9 727 Call becomesSPY260509C00727000.SMARTunderIB_SIMPLIFIED.instrument.make_price(...)andmake_qty(...)round raw inputs to the venue’sprice_increment/size_increment- all order math goes through these methods, never raw arithmetic.InstrumentProvideris the adapter-supplied loader that materializes instruments at startup;load_allvsload_idsis the lazy-vs-eager switch. For Cortana V1 (SPY 0DTE only) we will lean on the IBKR adapter’s built-inOptionContractmaterialization withbuild_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:
| Category | Class | Notes |
|---|---|---|
| Cash markets | Equity | Stocks/ETFs. SPY, QQQ, NVDA. |
| Cash markets | CurrencyPair | FX (EUR/USD), spot crypto (BTCUSDT). |
| Cash markets | Commodity | Spot metals, energy. |
| Cash markets | IndexInstrument | Cash indices (SPX, NDX) - non-tradable directly but priced. |
| Futures | FuturesContract | Standard listed futures: ESM5, CLN5. Has expiration_utc. |
| Futures | FuturesSpread | Calendar/inter-commodity spreads (e.g. ES Jun-Sep). |
| Futures | CryptoFuture | Dated crypto futures (Deribit, Binance Futures). |
| Futures | CryptoPerpetual | Perp contracts (no expiry). |
| Futures | PerpetualContract | Generic perpetual (TradFi-style). |
| Options | OptionContract | Single-leg call/put. Strike + expiry + kind + underlying. |
| Options | OptionSpread | Exchange-defined multi-leg combo (up to 4 legs each with ratio). |
| Options | CryptoOption | Crypto-settled options. Inverse/quanto styles. |
| Options | BinaryOption | Fixed-payout (e.g. Polymarket) - no strike, no kind. |
| OTC / specialized | Cfd | Contracts for difference. |
| OTC / specialized | BettingInstrument | Sports books (Betfair). |
| OTC / specialized | SyntheticInstrument | Locally-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 viaInstrumentId(Symbol("..."), Venue("..."))orInstrumentId.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, IBKRIB_SIMPLIFIED)BF-B.NYSE(equity with class share, IBKRIB_SIMPLIFIED- spaces in IB localSymbol replaced with hyphens)SPY.ARCA(equity, IBKRIB_SIMPLIFIED)AAPL230217P00155000.SMART(option, IBKRIB_SIMPLIFIED- see § Options)AAPL=STK.SMART(equity, IBKRIB_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:
| Field | Purpose | Example |
|---|---|---|
price_precision | Decimal places on prices, triggers, fills | 2 for SPY equity (cents) |
price_increment | Tick size as a Price value | Price(0.01, 2) |
size_precision | Decimal places on quantities | 0 for equity options (whole contracts) |
size_increment | Lot size as a Quantity value | Quantity(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_incrementThese 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 boundsmax_notional,min_notional- order notional boundsmax_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.10for 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 forBTCUSDT).quote_currency- the unit prices are quoted in (USDT forBTCUSDT).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:
| Field | Description |
|---|---|
strike_price | Price - the option’s strike. |
option_kind | OptionKind.CALL / OptionKind.PUT. |
expiration_utc | UTC expiry timestamp. |
underlying | Underlying InstrumentId (e.g. SPY.ARCA). |
multiplier | Standard SPY = 100; some products differ. |
price_increment / size_increment | Per-venue grid (penny increments for SPY weeklies; nickel for some). |
margin_init / margin_maint | For 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- root260509- 2026-05-09 expiry (YYMMDD)C- Call00727000- strike $727.00, scaled ×1000 = 727000, zero-padded to 8 digits.SMART- IB SMART routing exchange (the venue side ofInstrumentId;.AMEX/.CBOEare 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 whenload_all=Truein config.await provider.load_ids_async(instrument_ids)- load a specific set. Used in production whenload_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; returnsInstrumentorNone.
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 asOptionContractinstances.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
- Standalone - research / backtest scripts call
provider.load_all_async()directly and iterate instruments. - Runtime - Actors and Strategies receive instruments via
on_instrument(instrument)callbacks as the provider streams them. Strategies do not callload_*themselves; theLiveNodeorchestrates.
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:
- Expiry semantics on the instrument itself - strategies check
clock.utc_now() < instrument.expiration_utcbefore submitting orders. - Re-loading on
TradingNoderestart - startup re-runsload_ids_async/load_contractsdiscovery; expired instruments simply don’t come back. - Per-day chain rebuild for 0DTE - the IBKR adapter’s
build_options_chain=Trueruns 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:
- Live trading uses IBKR exclusively -
SPY.ARCAis the only form strategies see at runtime. - Backtest replay over Databento - Databento-loaded
ParquetDataCatalogmay haveSPY.ARCX(or similar). Either (a) re-stamp the catalog instruments toSPY.ARCAat load time, or (b) write anInstrumentIdtranslation 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 +.SMARTvenue. This is whatcache.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
OPRAfor consolidated options data. Likely form:SPY260509C00727000.OPRAor 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 NautilusInstrumentId).
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
SyntheticInstrumentfor SPY-VIX delta if Cortana adds vol- regime-aware sizing - derived locally, untradable, signal-only. - A custom non-
InstrumentDatasubclass for UW alerts (UWFlowAlert,UWGexUpdate) - these are signals about instruments, not instruments themselves; see nautilus-custom-data.md. - Possibly a
BinaryOptionwrapper for any event-contract signal source Cortana ingests in MK5+ (Polymarket / Kalshi) - clearly out-of-scope for the spike.
See Also
- Nautilus Concepts - instrument identifiers + types, partial coverage that this page expands
- Nautilus Value Types -
InstrumentId,Symbol,Venue,Price,Quantity,Moneycontracts - Nautilus Options -
OptionContract,OptionSpread, chain subscription, Greeks - Nautilus Adapters -
InstrumentProviderpattern, factory registration, 5-component bundle - Nautilus Data - how instruments and
Dataevents flow through theDataEngine - Nautilus Cache - where loaded instruments live;
cache.instrument(id)lookup - Nautilus Custom Data -
@customdataclassfor non-instrument data (UW flow, GEX) - Nautilus Integrations - IBKR adapter
symbology (
IB_SIMPLIFIEDvsIB_RAW),build_options_chain - Databento vs UW vs IBKR data feeds
- vendor-layering decision, OPRA replay path
- Spike plan
- Saturday 2026-05-09 evaluation, where this page is consulted
Timeline
- 2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 4.