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 viaAccountStateevents whose adapter contract is “complete margin snapshot,” and queries are exposed viaself.portfolio.*and the per-accountAccountobject in the Cache. Same subsystem in backtest, sandbox, and live - the only difference is who emits theAccountState(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 ownTradingNodeprocess, and gets their ownAccountobject 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.0effective leverage at all times. - Pending order capital impact is locked notional: opening a 100 SPY 0DTE
call at 15,900
offreeand 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 debittotal. - 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) andmargin_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:
| Constructor | What you supply | What’s derived | Clamp |
|---|---|---|---|
AccountBalance::from_total_and_locked(total, locked) | total + locked | free = total - locked | free clamped to [0, total] |
AccountBalance::from_total_and_free(total, free) | total + free | locked = total - free | locked 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 |
|---|---|---|
| CASH | Sum of pending-order notional values + open-position notional | Decreases by next-order notional on submit, returns on cancel/close |
| MARGIN | Sum of pending-order initial margin + open-position maintenance margin | Decreases by next-order initial margin on submit |
| BETTING | Sum of stakes for unsettled bets | Decreases 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:
| Concept | Nautilus surface | Notes |
|---|---|---|
| Balance (cash equivalent) | Account.balance(currency).total | Venue-reported; the IB “Net Liquidation” or similar |
| Free / available / buying power | Account.balance(currency).free | What you can spend on a new order |
| Locked / used / margin held | Account.balance(currency).locked | Reserved 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
| Margin | When used | What it gates |
|---|---|---|
margin_init | At order submit and at position open | Whether the account can afford to open |
margin_maint | Continuously, while position is open | Whether 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
FeeModelfor 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
| Surface | Field | Currency handling |
|---|---|---|
OrderFilled event | commission: Money | Single currency per fill |
Position object | commissions() -> list[Money] | One entry per distinct currency seen |
Account.balance(currency).total | Decremented by commission | Per 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) # MoneyNone 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 ofAccountBalance(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
- Adapter pushes a venue update. IBKR’s
accountSummary/updatePortfoliocallbacks translate to one or moreAccountStateevents. - Internal recalculation after a fill. The execution engine may
emit an
AccountStateto reflect the post-fill balance change before the venue’s next push lands. - Reconciliation startup.
LiveExecutionEnginesynthesizesAccountStateevents to align cached account state with venue truth on connect; flagged withreconciliation=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_orderengine-level limits and instrumentmax_notionallimits. … 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 type | Pre-trade check | Triggers OrderDenied? |
|---|---|---|
| CASH | Notional ≤ account.balance(currency).free | Yes |
| MARGIN | Initial margin ≤ available margin capacity | Yes (implicit via margin model) |
| Either | max_notional_per_order (engine-wide config) | Yes |
| Either | Instrument max_notional | Yes |
What’s NOT a built-in
Two checks the framework does NOT do automatically that Cortana might want at MK3:
- 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). - Daily-loss circuit breaker. “If realized P&L for the day < -X,
halt.” Build as an Actor that listens for
PositionClosedevents, tracks cumulative realized-P&L, and flipsTradingState.HALTEDwhen 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.,DUP696099matches 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. EachTradingNodeholds 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:
- Account info (cash balance, NLV, equity, available funds)
- Balances per currency
- Positions (per contract)
- 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:
| Paper | Live | |
|---|---|---|
| Gateway port | 4002 | 4001 |
| TWS port | 7497 | 7496 |
| Account prefix | DU… | 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”:
| Aspect | Backtest | Sandbox / Paper | Live |
|---|---|---|---|
| Account object | Account in Cache | Account in Cache | Account in Cache |
| AccountState emitter | Simulated venue | Adapter (IBKR paper) | Adapter (IBKR live) |
| Balance starting point | Configured starting balance | Venue-reported (DU paper account) | Venue-reported (U live account) |
| Margin model | Configured per instrument | Same | Same |
| RiskEngine balance veto | Active | Active | Active |
| Reconciliation events | None - sim owns truth | Yes (IBKR truth) | Yes (IBKR truth) |
| Commission accumulation | Via FeeModel | Via adapter | Via 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:
- Margin / buying power. Relies on IBKR rejections to discover when an order won’t go through.
- Account equity (NLV + open-position MTM) as a real-time number.
- Per-currency exposure (single-currency USD, so trivially “one number,” but the structure isn’t there).
- Daily-loss circuit breaker as a structural gate (we have docs about it, no enforced veto).
MK3 fixes - by construction
| MK2 gap | MK3 mechanism |
|---|---|
| No margin / buying-power tracking | Built-in Account object + AccountBalance.free; auto-updated on every fill and venue snapshot |
| Discover limits via IBKR rejections | RiskEngine 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 code | position.commissions() and account.balance(currency).total evolution |
| Paper/live drift in account math | Same accounting subsystem; only emitter changes |
Specific IBKR adapter modeling for Cortana
For the spike-day verification:
- Connect to Gateway 4002, account
DUP696099. ConfirmAccountStateevent emits with USD balance, zero margin entries (cash account), expected NLV. - **Submit a 1-contract SPY call for 159
on submit and
totaldecreased by$159 + commissionon fill. - Submit an order whose notional exceeds
free. ExpectOrderDenied(reason="balance impact...")- notOrderRejectedfrom the venue. This validates the RiskEngine pre-trade check is wired correctly for cash accounts. - 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:
- One
Accountobject per tenant per process. No cross-tenant capital visibility. Tenant A’s free balance is not in tenant B’s cache. - 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. - Per-tenant
AccountStateevent 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. - 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’sTradingNodeConfig. Two tenants on identical strategy with different risk tolerances run with different RiskEngine configs in their respective processes. - Redis namespacing. Set
use_instance_id=TrueinCacheConfigper Nautilus Cache § “Redis namespacing knobs” so each tenant’sAccountkeys 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.)AccountStatereconciliation events look real. Checkevent.reconciliationbefore treating anAccountStateas fresh venue activity. Reconciliation can synthesize multipleAccountStateevents 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).totalis NOT equity. Equity = balance + unrealized PnL on open positions. Useportfolio.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.FUTURESexists in the enum but the public docs emphasize CASH / MARGIN / BETTING. Treat FUTURES as a margin specialization with SPAN-style reporting.- Currency mismatch in
Moneyarithmetic raises. From Nautilus Concepts § Value types: “Moneyrequires 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 isFalse. 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
Accountper 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.mdQ5). - 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,
OrderDeniedevent, TradingState (HALTED for circuit breakers). - Nautilus Positions - commission accumulation per currency (lines 294-310); realized PnL formula and the multiplier convention.
- Nautilus Cache -
Accountlives in Cache;account_for_venue(venue); multi-tenantuse_instance_idconfig. - Nautilus Events -
AccountStateas the only account-side event variant; complete-snapshot adapter contract;reconciliationflag. - 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 bypassesbalance_locked, which is correct.- Brain RESOLVER - page filing rules.
Timeline
2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 3.