Nautilus Accounting

Nautilus models the brokerage account as a first-class engine object with three balance fields per currency (total == locked + free), per-instrument and account-wide margin scopes, and a configurable margin model that turns notional into initial / maintenance margin requirements. Every order’s capital impact is computed by the engine - not by the strategy - and the RiskEngine consults the account on every submit/modify so cash-account balance impact and per-currency exposure are pre-trade gating concerns rather than post-hoc broker rejections. State arrives via AccountState events whose adapter contract is “complete margin snapshot,” and queries are exposed via self.portfolio.* and the per-account Account object in the Cache. Same subsystem in backtest, sandbox, and live - the only difference is who emits the AccountState (simulated venue vs IBKR adapter). For Cortana MK2, which has no explicit margin/buying-power tracking and discovers limits via IBKR rejections, this is a structural upgrade. For MK4+ multi-tenant SaaS, accounting must scope per-tenant - each customer brings their own IBKR account, runs in their own TradingNode process, and gets their own Account object inside their own Cache, with no cross-tenant capital visibility.

Core claim

A Nautilus Account is the engine-owned, single-source-of-truth record of who can pay for what on a given venue. It carries balances and margin state, accumulates commissions per currency, and emits AccountState events on every venue update. Strategies and Actors do not maintain parallel balance views - they query self.cache.account(...) or self.portfolio.equity(...). The RiskEngine is wired to consult the account before passing an order down the pipeline, which is how “buying-power exhaustion” becomes a typed OrderDenied event with reason rather than an opaque venue reject.

This page is the specialization of Nautilus Concepts § Accounting (lines 356-388) at API-and-implication level: every account type, every balance field, the margin-model implementations, the adapter-side contract for AccountState, the IBKR-specific behavior, and the multi-tenant scoping that the SaaS roadmap requires.

Account types - the four flavors

The framework supports four account types in code (AccountType enum) even though the public docs lead with three. The order of frequency for Cortana use:

CASH - spot, no leverage

Definition (verbatim): “For spot trading; locks the notional value for every position a pending order would open.”

Mechanics:

  • No leverage. 1.0 effective leverage at all times.
  • Pending order capital impact is locked notional: opening a 100 SPY 0DTE call at 15,900offreeand moves it tolocked`.
  • On fill: the locked amount converts to a position; the position’s unrealized PnL flows from quote movement.
  • On cancel: the locked amount returns to free.
  • On close: realized PnL hits total; commissions debit total.
  • Reduce-only orders do not contribute to balance_locked - they only decrease exposure, so reserving new capacity makes no sense.

This is the right model for Cortana MK2’s actual setup: paper DUP696099, single-currency USD, single-leg long-options-only book. No margin extension, no leverage, no short-sell reservation logic.

MARGIN - derivatives, leveraged

Definition (verbatim): “For derivatives with leverage; tracks account balances, reserve margin for open orders and positions, and apply a configurable leverage per instrument.”

Mechanics:

  • Two margin types per position: margin_init (open) and margin_maint (keep open).
  • Per-instrument leverage configurable.
  • Reserves margin (not full notional) for pending orders.
  • Tracks both per-instrument scope (isolated margin) and account-wide scope (cross-margin) - see § Margin scopes below.
  • Reduce-only orders do not add to initial margin (parallel to cash account treatment).

Not Cortana MK2. Could become Cortana MK3+ if we ever short verticals, sell premium, or trade futures. The framework does not require us to use it; it’s there if we add leveraged products.

BETTING - prediction markets

Verbatim: “For prediction markets; locks only the stake required by the venue; leverage and margin do not apply.”

Out of scope for Cortana entirely. Listed for completeness - anyone running Polymarket/Kalshi adapters uses this.

FUTURES - listed-derivatives subset of margin

The page lead (Concepts § Accounting) mentions three account types verbatim, but AccountType.FUTURES does exist in nautilus_trader.model.enums. Treat it as a specialization of margin accounting tuned to listed-futures SPAN-style margin reporting. Not Cortana-relevant short-term; relevant if we ever trade /ES or VX.

AccountBalance - the three-field invariant

Every AccountBalance is three Money values plus a currency:

AccountBalance(
    total=Money(...),     # Venue-reported balance figure
    locked=Money(...),    # Reserved against orders/positions
    free=Money(...),      # total - locked, available for new orders
    currency=Currency(...),
)

Hard invariant: total == locked + free per currency. The framework checks this on construction.

Derived constructors (Rust adapters)

Two convenience constructors are used by adapter code so the venue only needs to report two of the three values:

ConstructorWhat you supplyWhat’s derivedClamp
AccountBalance::from_total_and_locked(total, locked)total + lockedfree = total - lockedfree clamped to [0, total]
AccountBalance::from_total_and_free(total, free)total + freelocked = total - freelocked clamped to [0, total]

Clamping prevents negative free when a venue’s “locked” briefly overshoots total (e.g., during settlement seam). The invariant holds post-clamp.

What “locked” means by account type

Account type”Locked” computation”Free” effect on submit
CASHSum of pending-order notional values + open-position notionalDecreases by next-order notional on submit, returns on cancel/close
MARGINSum of pending-order initial margin + open-position maintenance marginDecreases by next-order initial margin on submit
BETTINGSum of stakes for unsettled betsDecreases by stake on submit

The mechanical rule: “locked” is whatever capacity the account must reserve to support the open exposure plus the in-flight intent. “Free” is whatever’s available for a new submission.

Equity vs balance vs free - the four numbers strategists care about

Mapping to the words traders actually use:

ConceptNautilus surfaceNotes
Balance (cash equivalent)Account.balance(currency).totalVenue-reported; the IB “Net Liquidation” or similar
Free / available / buying powerAccount.balance(currency).freeWhat you can spend on a new order
Locked / used / margin heldAccount.balance(currency).lockedReserved for pending + open
Equity (BAL + open-position MTM)self.portfolio.equity(venue=...) returns dict[Currency, Money]Account balances plus position valuations using the price-fallback chain

Equity is computed by the Portfolio, not stored on the Account. From Nautilus Concepts § Portfolio: “equity = account balances + position valuations,” with side-aware pricing (long-only → bid, short-only → ask, mixed → mid) and the documented fallback chain (cached mark → side-appropriate quote → last trade → recent bar close → flag unpriceable).

For Cortana MK2’s “what’s my account worth right now” UI: the answer is portfolio.equity(venue) not account.balance(currency).total, because mid-trade unrealized PnL belongs in the displayed equity number.

Margin model - initial vs maintenance

Margin only applies to MARGIN accounts. Two computation paths exist; adapters pick one based on venue convention.

StandardMarginModel - fixed percent of notional, leverage-blind

Used by traditional brokers (TWS-style, futures clearinghouses).

initial_margin     = notional_value * instrument.margin_init
maintenance_margin = notional_value * instrument.margin_maint

Leverage on the position is ignored - the margin requirement is dictated by the instrument’s published margin %, not by user-selected leverage.

LeveragedMarginModel - notional ÷ leverage, percent-scaled

Used by crypto exchanges (Binance Futures, Bybit, Deribit, Hyperliquid, OKX, BitMEX, Kraken Futures - all listed in the doc as cross-margin account-wide reporters).

initial_margin     = (notional_value / leverage) * instrument.margin_init
maintenance_margin = (notional_value / leverage) * instrument.margin_maint

leverage is set per-instrument by the strategy or the venue’s default-leverage policy. Higher leverage means lower margin held.

Custom margin models

Subclass the base and register on the MarginAccount. Use case: a specific venue with non-standard margin math (cross-product offsets, risk-array-based SPAN, portfolio margin). Not common; not Cortana.

Initial vs maintenance - what they’re for

MarginWhen usedWhat it gates
margin_initAt order submit and at position openWhether the account can afford to open
margin_maintContinuously, while position is openWhether the position must be liquidated (margin call)

Difference is set per instrument. Concrete example: an instrument with margin_init=0.10 and margin_maint=0.05 requires 10% of notional to open and 5% to keep open - so a 100% drawdown would still permit the position to live until net equity dropped below the maintenance line.

Margin scopes - per-instrument vs account-wide

MarginBalance entries carry an instrument_id field that’s either set (per-instrument scope) or None (account-wide scope).

Per-instrument scope (instrument_id is set)

Used for isolated-margin venues. Each instrument has its own margin pool; a loss on one instrument cannot drain capacity from another.

Adapter contract: emit MarginBalance::new(initial, maint, Some(id)) per open position.

Account-wide scope (instrument_id is None`)

Used for cross-margin venues. The venue reports a single aggregate margin per collateral currency; capacity is fungible across all positions in that currency.

Adapter contract: emit MarginBalance::new(initial, maint, None) keyed by currency only.

Critical adapter contract - MarginAccount.apply() REPLACES

Verbatim load-bearing rule: MarginAccount.apply() replaces both stores from the incoming event. It does not merge with prior state.”

The implication for adapter authors: every AccountState event must carry complete margin snapshots. Partial snapshots that omit live entries cause those entries to disappear until the next full snapshot.

This is a recurring pitfall referenced in Nautilus Events and Nautilus Concepts. For the Cortana IBKR path: the IBKR adapter’s AccountState emission must always be a full margin snapshot, not deltas. If we ever write a custom adapter (UW data, hypothetical custom broker), this is the #1 thing to get right.

Commission and fee handling

Commissions accumulate per currency on the position object (cf. nautilus-positions.md lines 294-310), but the account-side accounting treats them as immediate balance impacts.

Sign convention

From Nautilus Concepts § Instruments (lines 575-577): “Positive fee rate = commission … Negative fee rate = rebate.”

A negative fee rate (rebate) on a maker-taker venue is supported and accumulates as a credit to the position’s commission ledger, which reduces the effective cost basis. Cortana doesn’t currently get rebates on IBKR options (we pay $0.65/contract or similar), so the negative path is unused but architecturally available.

Fee models

  • MakerTakerFeeModel - different rates for liquidity-taking vs liquidity-providing fills. Built-in.
  • FixedFeeModel - flat per-trade fee. Built-in.
  • Custom - subclass FeeModel for venue-specific schedules (tiered, notional-based, monthly-volume-discounted, options-per-contract style like IBKR).

For IBKR options, the fee model needs to account for the $0.65/contract standard plus exchange fees plus regulatory fees. The IBKR adapter handles this; verify on spike day that the fee landing in OrderFilled.commission matches the actual broker statement.

Where commissions land

SurfaceFieldCurrency handling
OrderFilled eventcommission: MoneySingle currency per fill
Position objectcommissions() -> list[Money]One entry per distinct currency seen
Account.balance(currency).totalDecremented by commissionPer currency

Edge case (FX-specific, not Cortana): “Base Currency Commissions: Applied only to spot currency pairs where commission currency matches instrument.base_currency. Commission deducts from opening trades’ quantity; affects signed_qty on closing fills.”

Net-of-fees PnL computation

Position-level:

net_pnl = position.realized_pnl - sum(position.commissions())

(within a currency).

Account-level: balance evolution already incorporates commissions, so total - initial_total gives net P&L over the lifetime of the account (modulo deposits/withdrawals).

Multi-currency support

Each AccountBalance is keyed by Currency. A MarginBalance is keyed by (instrument_id, currency) for isolated margin or just currency for cross-margin.

Strategy / Actor query API

# Account-level per-currency
self.cache.account(account_id).balance(currency)            # AccountBalance
self.cache.account(account_id).balances()                   # dict[Currency, AccountBalance]
 
# Margin queries
self.cache.account(account_id).margin(instrument_id)        # MarginBalance | None  (per-instrument)
self.cache.account(account_id).margin_for_currency(ccy)     # MarginBalance | None  (account-wide)
self.cache.account(account_id).margins()                    # dict[InstrumentId, MarginBalance]
self.cache.account(account_id).account_margins()            # dict[Currency, MarginBalance]
 
# Initial-margin shortcut
self.cache.account(account_id).margin_init(instrument_id)   # Money | None
self.cache.account(account_id).margin_init_for_currency(ccy)# Money | None
 
# Totals across both scopes
self.cache.account(account_id).total_margin_init(currency)  # Money
self.cache.account(account_id).total_margin_maint(currency) # Money

None from a point query means “no entry”; a Money(0, currency) from a totals query means “no margin held in that currency.” The framework distinguishes between absence and zero.

Portfolio-level multi-currency

self.portfolio.unrealized_pnls(venue) -> dict[Currency, Money]
self.portfolio.realized_pnls(venue)   -> dict[Currency, Money]
self.portfolio.net_exposures(venue)   -> dict[Currency, Money]
self.portfolio.equity(venue)          -> dict[Currency, Money]
self.portfolio.margins_init(venue)    -> dict[InstrumentId, Money]

Cortana is single-currency USD today. Multi-currency support is relevant if we ever route to a venue that quotes in EUR or if a tenant in MK4+ holds a non-USD account. The framework treats each currency independently - no automatic conversion. P&L “in USD” for a multi- currency portfolio requires a separate FX-conversion step at the reporting layer; the kernel does not invent FX rates.

AccountState event - the single account-side event variant

Verbatim from Nautilus Events:

AccountState: Snapshot of AccountBalance (total, locked, free) per currency, plus margin entries (per-instrument and account-wide) for margin accounts. Fires on venue update OR after a portfolio recalculation.

There is only one account-side event. State changes are not split into BalanceChanged / MarginAdded / BalanceLocked / etc. - the whole snapshot is the event. This is the consequence of the “apply() replaces” semantic: there’s no merge-style protocol, so there’s no merge-style event taxonomy.

Subscribing to AccountState

Like any other event, dispatch ladder is specific-to-generic:

AccountState -> on_account_state(event)
             -> on_event(event)

No intermediate on_account_event because there’s only one variant.

When AccountState fires

  1. Adapter pushes a venue update. IBKR’s accountSummary / updatePortfolio callbacks translate to one or more AccountState events.
  2. Internal recalculation after a fill. The execution engine may emit an AccountState to reflect the post-fill balance change before the venue’s next push lands.
  3. Reconciliation startup. LiveExecutionEngine synthesizes AccountState events to align cached account state with venue truth on connect; flagged with reconciliation=True.

Audit-logger implication

Subscribing one Actor to on_account_state (or to on_event and filtering) gives an append-only log of every balance/margin transition. This is the data Cortana needs for daily P&L attribution that today is hand-stitched from decisions.db + IBKR Flex + IBKR updatePortfolio callbacks. Per Nautilus Events § Custom events, sink to Parquet for long-term retention.

Margin / buying-power as a RiskEngine pre-trade veto

This is the question the Cortana MK3 plan asks directly. Answer: yes, configurable, built-in.

Nautilus Execution § RiskEngine enumerates the built-in pre-trade checks (lines 144-159), including verbatim:

max_notional_per_order engine-level limits and instrument max_notional limits.Cash-account balance impact for non-margin accounts.”

So the RiskEngine, by default, refuses orders whose cash-account balance impact exceeds available free. For margin accounts, the equivalent veto is implicit in the per-instrument margin model - the engine consults margin_init for the instrument and rejects if the account cannot reserve it.

The exact built-in coverage

Account typePre-trade checkTriggers OrderDenied?
CASHNotional ≤ account.balance(currency).freeYes
MARGINInitial margin ≤ available margin capacityYes (implicit via margin model)
Eithermax_notional_per_order (engine-wide config)Yes
EitherInstrument max_notionalYes

What’s NOT a built-in

Two checks the framework does NOT do automatically that Cortana might want at MK3:

  1. Concentration limits. “Don’t put more than 30% of NLV into a single ticker.” Build as a custom RiskEngine rule (subject to the open extension API question, see nautilus-execution.md § Custom RiskEngine rules).
  2. Daily-loss circuit breaker. “If realized P&L for the day < -X, halt.” Build as an Actor that listens for PositionClosed events, tracks cumulative realized-P&L, and flips TradingState.HALTED when the threshold trips.

For Cortana MK3 spike-day: confirm the cash-account free check fires by submitting an order whose notional exceeds free and observing OrderDenied(reason="...balance impact..."). This validates the “buying power as RiskEngine veto” claim before we rely on it.

IBKR-specific behavior

From Nautilus Integrations IBKR section (lines 25-77):

Account ID and connection

  • IBKR paper accounts: DU… prefix (e.g., DUP696099 matches Cortana MK2’s). Live accounts: U… prefix (e.g., U987654).
  • Connection is per-process: env vars TWS_USERNAME, TWS_PASSWORD, TWS_ACCOUNT, IB_MAX_CONNECTION_ATTEMPTS. Each TradingNode holds its own credentials. (Multi-tenant implication - see § Multi-tenant scoping below.)
  • Multiple execution clients in one TradingNode is possible (different ibg_client_id + account_id) - useful for paper-and-live in one process, or two distinct accounts on the same Gateway.

What the IBKR adapter pulls on connect

Verbatim from InteractiveBrokersClientAccountMixin:

  1. Account info (cash balance, NLV, equity, available funds)
  2. Balances per currency
  3. Positions (per contract)
  4. Margin requirements (initial / maintenance, per position and account-wide)

These flow as AccountState event(s) on connect. Cortana MK2’s current “poll IBKR for account summary every N seconds” loop becomes event-driven.

Position-mode handling

IBKR is NETTING at the broker level (one net position per contract; cf. nautilus-positions.md § IBKR specifically). The Nautilus IBKR adapter exposes account_id per ExecutionClient, and the strategy’s declared OMS type interacts as the four-quadrant matrix in Nautilus Execution § OMS.

For Cortana: declare OmsType.NETTING on the strategy to match IBKR’s broker-side reality. No virtual sub-positions, no aggregation drift.

Multi-account on one IBKR login

The IBKR adapter supports multi-account connections - one IBKR login can manage multiple sub-accounts (FA / advisor accounts). Each sub-account becomes a distinct Account object in the cache. Not Cortana MK2. Potentially relevant for MK4+ if a tenant uses an FA structure.

Paper vs live equivalence - port and mode flags

Per Nautilus Integrations § Cortana mapping:

PaperLive
Gateway port40024001
TWS port74977496
Account prefixDU…U…

trading_mode="paper" vs trading_mode="live" is a config-only switch. No code changes. The accounting subsystem is identical - same Account, same balance fields, same RiskEngine checks, same AccountState event. The simulated venue (in backtest) emits its own AccountState; IBKR (in paper or live) emits IBKR-shaped AccountState. Strategy code reads the same fields either way.

This is the structural property that closes the MK2 paper-vs-live drift class: there is no separate “paper account math” code path. Same subsystem, same invariants, different upstream emitter.

Paper vs sim vs live - accounting equivalence

Compare with Nautilus Execution § “Paper vs sim vs live”:

AspectBacktestSandbox / PaperLive
Account objectAccount in CacheAccount in CacheAccount in Cache
AccountState emitterSimulated venueAdapter (IBKR paper)Adapter (IBKR live)
Balance starting pointConfigured starting balanceVenue-reported (DU paper account)Venue-reported (U live account)
Margin modelConfigured per instrumentSameSame
RiskEngine balance vetoActiveActiveActive
Reconciliation eventsNone - sim owns truthYes (IBKR truth)Yes (IBKR truth)
Commission accumulationVia FeeModelVia adapterVia adapter

Strategy code reads the same account.balance(currency).free in all three contexts. The only behavioral differences are who supplied the starting balance and whether reconciliation runs.

For backtest authors: setting a deliberate starting balance lets you test “ran out of capital” scenarios - the RiskEngine OrderDenied fires identically whether the venue is sim or real.

Cortana MK3 implications - concrete mappings

MK2 today - what’s missing

Cortana MK2 does not explicitly track:

  1. Margin / buying power. Relies on IBKR rejections to discover when an order won’t go through.
  2. Account equity (NLV + open-position MTM) as a real-time number.
  3. Per-currency exposure (single-currency USD, so trivially “one number,” but the structure isn’t there).
  4. Daily-loss circuit breaker as a structural gate (we have docs about it, no enforced veto).

MK3 fixes - by construction

MK2 gapMK3 mechanism
No margin / buying-power trackingBuilt-in Account object + AccountBalance.free; auto-updated on every fill and venue snapshot
Discover limits via IBKR rejectionsRiskEngine pre-trade check rejects with OrderDenied(reason=...) - typed event, not opaque venue reject
Hand-rolled “what’s my account worth”self.portfolio.equity(venue) returns dict[Currency, Money] with the price-fallback chain
Per-position commission summing in custom codeposition.commissions() and account.balance(currency).total evolution
Paper/live drift in account mathSame accounting subsystem; only emitter changes

Specific IBKR adapter modeling for Cortana

For the spike-day verification:

  1. Connect to Gateway 4002, account DUP696099. Confirm AccountState event emits with USD balance, zero margin entries (cash account), expected NLV.
  2. **Submit a 1-contract SPY call for 159 on submit and total decreased by $159 + commission on fill.
  3. Submit an order whose notional exceeds free. Expect OrderDenied(reason="balance impact...") - not OrderRejected from the venue. This validates the RiskEngine pre-trade check is wired correctly for cash accounts.
  4. Verify position.commissions() matches the IBKR Flex statement for the day’s fills.

Multi-tenant scoping - per-tenant accounting

Per Nautilus Cache § Multi-tenant scoping (lines 549-610) and Nautilus Concepts § “Running multiple TradingNode instances”, multi-tenant requires process-per-tenant deployment. Implications for accounting:

  1. One Account object per tenant per process. No cross-tenant capital visibility. Tenant A’s free balance is not in tenant B’s cache.
  2. Per-tenant IBKR credentials. Each tenant’s process holds its own TWS_USERNAME / TWS_PASSWORD / TWS_ACCOUNT. The IBKR adapter is configured per-process. The platform has no “shared brokerage pool” - each tenant brings their own IBKR account.
  3. Per-tenant AccountState event stream. The audit logger Actor in each tenant process sees only that tenant’s account events. Cross-tenant aggregation (firm-level dashboard) requires an out-of-band consumer that subscribes to the Redis-backed MessageBus namespaced per-tenant.
  4. Per-tenant RiskEngine config. Cash-account vs margin-account policy, max_notional_per_order, daily-loss thresholds are all per-process - set in the tenant’s TradingNodeConfig. Two tenants on identical strategy with different risk tolerances run with different RiskEngine configs in their respective processes.
  5. Redis namespacing. Set use_instance_id=True in CacheConfig per Nautilus Cache § “Redis namespacing knobs” so each tenant’s Account keys live under a distinct Redis prefix. No accidental cross-tenant key collision.

The multi-tenant accounting story is structural: no kernel changes, just deployment discipline.

Fees / commissions in backtest replay

For backtest accuracy on historical Cortana data, the FeeModel must match IBKR’s actual schedule:

  • $0.65/contract for SPY options (standard)
  • Plus exchange fees ($0.30–0.55/contract typical)
  • Plus regulatory fees (SEC, ORF, OCC - sub-cent each)

Built MakerTakerFeeModel plus per-trade fixed exchange/regulatory adders, or a custom FeeModel that mirrors the IBKR fee schedule exactly. For replay determinism, this matters - if backtest commissions diverge from live by 10%, your historical win-rate calculation is biased.

Spike-day action: after a paper trading session, compare sum(position.commissions() for position in cache.positions_closed()) against the IBKR Flex commission report. Drift > 5% means the fee model needs tuning.

Daily-loss circuit breaker as Actor + TradingState

Build a DailyLossCircuit Actor:

class DailyLossCircuit(Actor):
    def on_start(self):
        self.threshold = Money(-2_000.00, USD)  # configurable
        self.daily_realized = Money(0, USD)
        # Reset at session start
 
    def on_position_closed(self, event: PositionClosed):
        self.daily_realized += event.realized_pnl
        if self.daily_realized < self.threshold:
            self.log.error(f"Daily loss limit hit: {self.daily_realized}")
            # Flip TradingState to HALTED
            self.cmd.set_trading_state(TradingState.HALTED)

The HALTED state denies new submits while allowing cancels (cf. nautilus-execution.md § TradingState). Open positions can still be closed via reduce-only orders.

This is the structural answer to the “no kill with open positions” feedback (feedback_no_kill_with_open_positions.md) - the engine isn’t killed, it’s just refusing new entries while letting exits land.

Caveats and gotchas

  • MarginAccount.apply() REPLACES. Adapters must emit complete margin snapshots. Partial snapshots silently delete entries until the next full snapshot. (Repeat from § Margin scopes - most important rule on this page for adapter authors.)
  • AccountState reconciliation events look real. Check event.reconciliation before treating an AccountState as fresh venue activity. Reconciliation can synthesize multiple AccountState events on connect to bring cache into alignment with venue truth.
  • Cash-account balance check is the only built-in risk veto for capital. Concentration limits, daily-loss caps, position-count limits, and time-of-day gates are NOT built-in - must be custom RiskEngine rules or Actors.
  • account.balance(currency).total is NOT equity. Equity = balance + unrealized PnL on open positions. Use portfolio.equity(venue) for “total worth” displays, not raw balance.
  • Multi-currency requires explicit FX strategy. Nautilus does not invent FX rates. P&L reports in a single reporting currency for a multi-currency portfolio require an out-of-band conversion step.
  • Reduce-only orders bypass balance_locked / margin_init. This is intentional (they only decrease exposure) but easy to overlook when reasoning about capacity arithmetic. Reduce-only doesn’t consume capital.
  • Commission sign convention is reversed from intuition. Positive fee rate = commission (you pay). Negative = rebate (you receive). Custom fee model authors: get this right or P&L sign inverts.
  • AccountType.FUTURES exists in the enum but the public docs emphasize CASH / MARGIN / BETTING. Treat FUTURES as a margin specialization with SPAN-style reporting.
  • Currency mismatch in Money arithmetic raises. From Nautilus Concepts § Value types: Money requires matching currencies and refuses to add USD to EUR.” No silent FX bugs - but multi-currency accounting requires explicit conversion plumbing in your strategy.
  • Per-tenant Redis isolation requires use_instance_id=True. Default is False. Multi-tenant deployments must flip this; missing it risks cross-tenant cache key collision.

When this concept applies

  • Designing the MK3 IBKR account integration surface.
  • Reasoning about whether Cortana’s order will be denied for capital reasons before it reaches IBKR.
  • Building daily-loss / concentration / time-of-day risk gates.
  • Computing equity / NLV / free balance / locked margin for the dashboard.
  • Multi-tenant SaaS architecture: one Account per tenant per process.
  • Verifying paper-vs-live accounting equivalence.
  • Tuning fee models for backtest fidelity against IBKR’s actual schedule.

When it breaks / does not apply

  • Daily-loss circuit breakers: not built-in. Build an Actor.
  • Concentration limits: not built-in. Build a custom RiskEngine rule (subject to extension-API resolution; see nautilus-execution.md Q5).
  • Position-count limits: not directly. Use cache.positions_open_count() in a custom rule.
  • FX P&L unification: kernel doesn’t invent FX. Reporting layer’s problem.
  • Tax-lot accounting (FIFO/LIFO/specific identification): out of scope per Nautilus Positions § Aggregation. Build downstream from the events list.
  • Cross-tenant capital views: per-process isolation is the design; cross-tenant rollup is an out-of-band consumer concern.

See Also

  • Nautilus Concepts - § Accounting (lines 356-388) for the architectural overview; § Portfolio (lines 389-414) for equity computation; § MessageBus for the dispatch model that delivers AccountState.
  • Nautilus Execution - RiskEngine pre-trade checks, OrderDenied event, TradingState (HALTED for circuit breakers).
  • Nautilus Positions - commission accumulation per currency (lines 294-310); realized PnL formula and the multiplier convention.
  • Nautilus Cache - Account lives in Cache; account_for_venue(venue); multi-tenant use_instance_id config.
  • Nautilus Events - AccountState as the only account-side event variant; complete-snapshot adapter contract; reconciliation flag.
  • Nautilus Integrations - IBKR adapter account-bootstrap, DU…/U… prefix convention, multi-account support, paper-vs-live mode flag.
  • 2026-05-09 Nautilus Spike Plan: ~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-nautilus-spike.md
    • Step 4 (RiskEngine integration), Step 7.5 (multi-tenant), Step 8 (paper-vs-live equivalence).
  • project_pm_ibkr_exit_invariant.md - broker-truth alignment that the AccountState reconciliation pattern reinforces.
  • feedback_no_kill_with_open_positions.md - TradingState.HALTED is the structural answer; account-state-driven daily-loss Actor is the trigger.
  • feedback_dual_tp_defense_in_depth.md - reduce-only TP fallback bypasses balance_locked, which is correct.
  • Brain RESOLVER - page filing rules.

Timeline

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