Nautilus Portfolio
The Nautilus
Portfoliois the central, kernel-owned aggregator for every open Position across every Strategy, Actor, and venue in the running node. It is exposed asself.portfolioon every Strategy and Actor - a pull-style query surface for net exposure, equity, mark-to-market, realized/unrealized PnL, and “what is currently unpriceable.” It performs context-aware valuation (longs at bid, shorts at ask, mixed at mid; mark prices when configured), automatic currency conversion to the account’s base currency (or to a caller-providedtarget_currency), and explicit missing-price flagging instead of silent zero-valuation. The companionPortfolioAnalyzerproduces realized-PnL statistics, position- return statistics, and portfolio-return statistics across configurable windows, drives the backtest tearsheet, and accepts custom statistics via thePortfolioStatisticbase class. For Cortana MK3 this is the direct replacement for MK2’s hand-rolled equity calculation, P&L attribution, dashboard summary endpoints, and the M2 “expectancy harness” Phase 1 work - mostly subsumed, partially specialized.
This page specializes Nautilus Concepts § Portfolio (lines 389-414) which gives the topology-level view. This page is the API saturation reference Cody will keep open during Steps 4-7 of the 2026-05-09 spike: every query method, every valuation rule, every conversion edge case, every analyzer behavior. Sister to Nautilus Positions (the units the Portfolio aggregates) and the parallel-filed nautilus-accounting.md (the account-balance source the Portfolio reads).
Core claim
The Portfolio is the single source of truth for “what does my book look
like right now.” There is no second store to drift from, no per-strategy
P&L tally that disagrees with the firm-level total, no silent zero when a
quote is missing. Every dashboard summary, every equity curve, every “am I
flat?” predicate, every per-instrument exposure, every aggregate-by-venue
query routes through the same Portfolio object. MK2’s three competing
truths (in-memory tracker totals, SQLite ledger sums, IBKR updateAccount
callback values) collapse to one.
API surface - full method matrix
All methods are reachable as self.portfolio.<method>(...) from any
Strategy or Actor. Pull-style: every call recomputes from current
Cache/Position state. No staleness window other than the documented
cache-then-publish caveat (live mode may show a brief delay between an
event and its appearance in Cache).
Per-instrument predicates
| Method | Returns | Meaning |
|---|---|---|
is_net_long(instrument_id) | bool | Net signed quantity > 0 |
is_net_short(instrument_id) | bool | Net signed quantity < 0 |
is_flat(instrument_id) | bool | Net signed quantity == 0 (no open position OR closed) |
is_completely_flat() | bool | True if every instrument is flat - node-wide “do I have any exposure?” |
Per-instrument values
| Method | Returns | Meaning |
|---|---|---|
net_position(instrument_id) | Decimal | Signed net quantity in instrument units |
net_exposure(instrument_id, account_id=None, target_currency=None) | Money | None | Notional exposure in account base or target_currency; None if unconvertible |
unrealized_pnl(instrument_id, account_id=None, target_currency=None) | Money | None | Mark-to-market PnL on open quantity |
realized_pnl(instrument_id, account_id=None, target_currency=None) | Money | None | Cumulative realized PnL on closed portions |
total_pnl(instrument_id, account_id=None, target_currency=None) | Money | None | realized + unrealized |
Per-venue / multi-instrument aggregates (dict-returning)
| Method | Returns | Meaning |
|---|---|---|
net_exposures(venue, account_id=None, target_currency=None) | dict[Currency, Money] | Per-currency total exposure across all open positions on the venue |
unrealized_pnls(venue, account_id=None, target_currency=None) | dict[Currency, Money] | Per-currency total unrealized PnL |
realized_pnls(venue, account_id=None, target_currency=None) | dict[Currency, Money] | Per-currency total realized PnL |
total_pnls(venue, account_id=None, target_currency=None) | dict[Currency, Money] | Per-currency realized + unrealized |
Equity / mark-to-market triad
| Method | Returns | Meaning |
|---|---|---|
mark_values(venue, account_id=None) | dict[Currency, Money] | Signed MTM totals for open positions only (longs +, shorts −, flats skipped) |
equity(venue, account_id=None) | dict[Currency, Money] | balances_total + (mark_values for cash/betting OR Σ unrealized_pnl for margin) |
missing_price_instruments(venue) | set[InstrumentId] | Instruments flagged unpriceable on last mark_values() / equity() call |
Configuration handle
self.portfolio.analyzer returns the kernel’s PortfolioAnalyzer instance.
Used to register custom statistics and to inspect computed return series.
Important method-by-method semantics
net_exposureis side-aware: longs are valued at the bid (conservative liquidation), shorts at the ask, mixed books at mid. Withuse_mark_xrates=true, mark prices replace mid for mixed positions.equity()formula branches on account type:- Cash and betting accounts:
balances_total + Σ mark_value(open positions). - Margin accounts:
balances_total + Σ unrealized_pnl(open positions). The cash/betting path usesmark_values()internally; the margin path uses the same cached unrealized-PnL pipeline that powersunrealized_pnls().
- Cash and betting accounts:
- Per-venue scope: most methods take
venuefor scoping.account_id=Noneaggregates across every account on that venue; passingaccount_idnarrows to one account. missing_price_instrumentsis venue-scoped, not account-scoped - an account-filteredmark_values(venue, account_id)call does not clear the venue tracker, so flags raised by other accounts on the same venue survive.
Query patterns - the load-bearing ones for Cortana
”What’s open right now?” - the dashboard summary call
# One call returns every per-currency exposure across all accounts on the venue
exposures = self.portfolio.net_exposures(venue=IBKR_VENUE)
# {USD: Money(15_400, USD)} - long-only Cortana book
# Companion call: per-currency unrealized PnL
upnl = self.portfolio.unrealized_pnls(venue=IBKR_VENUE)
# {USD: Money(+312, USD)}
# Companion call: per-currency realized PnL since session start
rpnl = self.portfolio.realized_pnls(venue=IBKR_VENUE)
# {USD: Money(+1_280, USD)}net_exposures(venue=...) is the single most useful query method for the
“what’s my book right now” dashboard endpoint. It replaces MK2’s
/api/positions/summary handler, which today aggregates from
position_state rows + the in-memory tracker + the IBKR account snapshot
and frequently disagrees with itself. Under MK3 the dashboard call
becomes a thin Redis subscriber that re-reads Portfolio once per N
seconds - no aggregation logic in the dashboard path at all.
”Am I flat?” - EOD-flatten predicate
if self.portfolio.is_completely_flat():
self.log.info("EOD flat confirmed")
else:
open_positions = self.cache.positions_open()
self.log.warning(f"{len(open_positions)} positions still open at EOD")
# ... liquidation logic ...is_completely_flat() is the predicate the EOD-flatten subroutine should
gate on, not a hand-counted dict-len. Single source of truth.
”What’s my equity curve right now?” - live equity sample
eq = self.portfolio.equity(venue=IBKR_VENUE)
# {USD: Money(105_312.40, USD)}The dashboard’s live equity bar pulls this once per second. It already combines the broker balance with current mark-to-market. No hand-rolled “sum balances + sum unrealized” arithmetic in any user code.
”Why is my equity wrong?” - missing-price triage
missing = self.portfolio.missing_price_instruments(IBKR_VENUE)
if missing:
self.log.warning(f"{len(missing)} instruments unpriceable: {missing}")The doc is explicit: “If equity() understates what you expect, check
missing_price_instruments(venue) before investigating the math. An empty
quote, trade, and bar feed for one instrument is the most common cause
of silent gaps.” This is the structural fix for MK2’s “tracker says +180” investigations - the answer is now an API call.
”What’s the per-instrument PnL?” - attribution
for pos in self.cache.positions_open(strategy_id=self.id):
upnl = self.portfolio.unrealized_pnl(pos.instrument_id)
rpnl = self.portfolio.realized_pnl(pos.instrument_id)
self.log.info(f"{pos.instrument_id}: unrealized={upnl}, realized={rpnl}")MK2’s attribution pipeline (decisions.db row aggregation joined to
updatePortfolio snapshots joined to position_state row deltas) collapses
to one Portfolio call per Position.
Currency conversion - full semantics
The Portfolio supports automatic currency conversion for PnL and exposure calculations, allowing display in a preferred currency. This is essential for multi-currency books and for downstream reporting that wants everything in USD.
Supported conversions
Every PnL/exposure query (realized_pnl, realized_pnls,
unrealized_pnl, unrealized_pnls, total_pnl, total_pnls,
net_exposure, net_exposures) accepts an optional target_currency
parameter to specify the desired output currency.
Single-account behavior
When querying a single account without specifying target_currency, the
Portfolio automatically converts values to that account’s base currency:
# Returns exposure in the account's base currency (e.g., USD)
exposure = self.portfolio.net_exposures(venue=IBKR_VENUE, account_id=account_id)For Cortana with one IBKR USD account, every query already returns USD -
no target_currency plumbing needed.
Multi-account behavior
For net_exposures() (all instruments across multiple accounts):
- Same base currency: automatically converts to the common base currency.
- Different base currencies: returns a dict with multiple currencies, each
converted to its account’s base currency. Pass
target_currencyfor a single-currency result.
For net_exposure() (single instrument across accounts):
- Different base currencies: returns
Noneunless you providetarget_currency.
# Multiple accounts, all USD
exposures = self.portfolio.net_exposures(venue=BINANCE)
# {USD: Money(...)}
# Multiple accounts with USD and EUR
exposures = self.portfolio.net_exposures(venue=BINANCE)
# {USD: Money(...), EUR: Money(...)}
# Force single currency across mixed-base accounts
exposures = self.portfolio.net_exposures(venue=BINANCE, target_currency=USD)
# {USD: Money(...)}Conversion failures
When target_currency is provided and conversion fails, behavior depends
on method shape:
- Single-value methods (
realized_pnl,unrealized_pnl,total_pnl,net_exposure): returnNoneand log an error to prevent silently- wrong values. Code must check forNone. - Dict-returning methods (
realized_pnls,unrealized_pnls,total_pnls,net_exposures): omit instruments that fail conversion but return successful conversions for the rest. Partial-success semantics - count keys to detect omissions.
Exchange rate data must be available in Cache when using
target_currency. With use_mark_xrates=true, the cached mark xrate from
Cache.get_mark_xrate() is used first and falls back to Cache.get_xrate()
(MID) if unavailable.
Conversion price types - context-aware valuation
When converting exposures to a target currency, the Portfolio uses different price types depending on position composition:
| Composition | Price type used | Rationale |
|---|---|---|
| All long | BID | Conservative - price you’d actually receive on liquidation |
| All short | ASK | Conservative - price you’d pay to cover |
| Mixed (longs + shorts) | MID | Neutral - neither side preferred |
Mixed, use_mark_xrates=true | MARK | Continuous fair value |
For Cortana (long-only options book), every conversion uses BID. This is exactly the right semantic - we exit by hitting the bid, so equity should reflect that liquidation reality.
Equity and mark-to-market - formula details
Three pull-style queries for continuous portfolio valuation, each returning per-currency results keyed by the relevant account base currency (or native settlement currency, depending on configuration).
mark_values(venue, account_id) - signed MTM
Returns signed mark-to-market totals for open positions only:
- Longs contribute positive notional.
- Shorts contribute negative notional.
- Flat positions are skipped entirely.
For Cortana long-only book: mark_values() returns the positive notional
of every open call/put position at the side-appropriate price. This is
“how much is my book worth right now.”
equity(venue, account_id) - total combined valuation
Equity combines account balance with open-position valuation, with formula branching on account type:
Cash / betting accounts: balances_total + Σ mark_value(open positions)
Margin accounts: balances_total + Σ unrealized_pnl(open positions)
Cortana’s IBKR account is a margin account, so equity uses the
balances_total + Σ unrealized_pnl form. The same cached unrealized-PnL
pipeline that powers unrealized_pnls() is reused.
Price fallback chain - robustness without invention
Valuation asks Cache for a price in this order, stopping at the first match:
- Mark price - if
use_mark_prices=trueinPortfolioConfigand a mark price is cached. - Side-appropriate quote - BID for longs, ASK for shorts.
- Last trade price.
- Most recent cached bar close - populated when
bar_updates=true.
If none of the four yield a price, the position goes into the
missing-price tracker and is skipped in the sum. Critically: the
Portfolio does not invent a price when nothing is available. No
silent zero, no last-known-good staleness, no fabricated mid. The
position is flagged unpriceable and the caller can inspect via
missing_price_instruments(venue).
Base currency conversion semantics
When convert_to_account_base_currency=true (the default) and the
account has a base_currency set:
- Settlement-currency values are converted to the base currency using MID
xrates from
Cache.get_xrate(). - With
use_mark_xrates=true, the cached mark xrate fromCache.get_mark_xrate()is used first, falling back to MID if unavailable. - Output dictionary has a single key matching the base currency.
When convert_to_account_base_currency=false, or the account has no
base_currency, results are keyed by each position’s native settlement
currency and no xrate conversion is applied.
If xrate data is unavailable for a required conversion, that position is treated as unpriceable and flagged via the missing-price tracker rather than silently valued at a 1.0 rate. No silent FX-rate-of-1.0 bug possible by design.
Missing-price tracking - observable behaviors
The tracker is a per-venue set of instrument IDs that could not be priced
on the last mark_values() or equity() call. Two observable behaviors:
- A warning log fires once per instrument on the transition from priced to unpriced, not on every subsequent call. Avoids log spam during sustained data outages.
- The next transition back to priced clears the entry, so a future drop re-warns. Edge-triggered.
- When a venue goes flat (no open positions), its tracker entry is cleared so stale instruments do not remain flagged.
missing_price_instruments(venue) returns the current set for inspection.
Why this matters for Cortana
This is a structural prevention of one of MK2’s recurring confusions:
“the dashboard shows my account at +80.” Today the answer requires walking three code paths and
comparing against three data sources. Under MK3 the answer is a single
API call: if missing_price_instruments(IBKR_VENUE) is non-empty, the
discrepancy is explained - one or more open positions are unpriceable
right now. If empty, the discrepancy is in downstream code, not the
data.
Multi-strategy aggregation - how Portfolio sees the world
The Portfolio aggregates across every Strategy and every Actor that
owns positions, not just the calling Strategy’s positions. This is by
design: Portfolio is firm-level, not strategy-level. Per-strategy slicing
is via self.cache.positions_open(strategy_id=self.id) - the Portfolio
queries take account_id and venue for scoping but not strategy_id.
Implication for Cortana MK3
If Cortana ever runs multiple Strategies under one Nautilus node (e.g.,
SPY 0DTE + QQQ 0DTE + a hedging Strategy), self.portfolio.equity(...)
returns the combined equity across all strategies. To get
strategy-level equity attribution, a custom aggregator subscribes to
PositionEvents, accumulates per-strategy_id realized PnL, and reads
per-position unrealized PnL from position.unrealized_pnl(price).
Strategy-level rollups are downstream of Portfolio, not built in.
For the spike (single-Strategy Cortana port), this never matters. Note it for the SaaS/multi-tenant phase.
PortfolioAnalyzer - performance statistics
Built-in statistics analyze trading-portfolio performance for both backtests and live trading. Categorized as:
- PnLs based statistics (per currency)
- Returns based statistics
- Positions based statistics
- Orders based statistics
The trader’s PortfolioAnalyzer (engine.portfolio.analyzer) can
calculate statistics at any arbitrary time, including during a backtest
or live trading session.
Custom statistics
Inherit from PortfolioStatistic and implement any of the calculate_*
methods. The built-in WinRate example:
import pandas as pd
from typing import Any
from nautilus_trader.analysis.statistic import PortfolioStatistic
class WinRate(PortfolioStatistic):
"""Calculates the win rate from a realized PnLs series."""
def calculate_from_realized_pnls(
self, realized_pnls: pd.Series
) -> Any | None:
if realized_pnls is None or realized_pnls.empty:
return 0.0
winners = [x for x in realized_pnls if x > 0.0]
losers = [x for x in realized_pnls if x <= 0.0]
return len(winners) / float(max(1, (len(winners) + len(losers))))Register with the analyzer:
stat = WinRate()
engine.portfolio.analyzer.register_statistic(stat)Statistics should handle degenerate inputs (None, empty series,
insufficient data). Return None for unknown/incalculable values, or a
reasonable default like 0.0 when semantically appropriate.
Returns: position vs portfolio
Two distinct return series:
- Position returns (
analyzer.position_returns()) - per-position, side-aware price return relative to the average open price. Reflects pure instrument price movement between entry and exit, independent of account size or leverage. Always available once positions close. - Portfolio returns (
analyzer.portfolio_returns()) - daily percentage change in total account balance. A 100,000 account reports ~0.9% for that day. Requires account state history spanning at least two distinct calendar days.
When the analyzer has account state history spanning ≥2 distinct calendar days, it computes portfolio returns automatically and uses them as the primary series for statistics, tearsheets, and the monthly returns heatmap. Multiple snapshots on the same day count as one day, so intra-day trading alone does not produce portfolio returns - you need at least one overnight balance change.
When portfolio returns are unavailable, falls back to position returns.
The convenience accessor analyzer.returns() resolves this preference:
portfolio returns when present, position returns otherwise.
Multi-currency accounts
Portfolio returns require a single-currency balance history. When the
account carries balances in multiple currencies, the analyzer cannot
produce a single return series and falls back to position returns
silently. Statistics and tearsheet charts use whichever series
returns() resolves to. For portfolio-level returns on a multi-currency
account, compute externally by converting balances to a common currency
before calculating percentage changes.
For Cortana (single-currency USD account), portfolio returns are always available once the analyzer has multi-day history. During a single- day backtest, position returns are the only available series - important for spike interpretation.
Per-venue calculation
In the backtest engine, the analyzer runs per venue (engine.pyx). Each
venue’s account produces its own portfolio return series. The tearsheet
aggregates across all cached accounts to produce a combined return
series for multi-venue backtests.
Backtest analysis output
Following a backtest run, the engine passes realized PnLs, returns, positions, and orders data to each registered statistic. Output is displayed in the tearsheet under the Portfolio Performance heading, grouped as:
- Realized PnL statistics (per currency)
- Returns statistics (for the entire portfolio)
- General statistics derived from position and order data
Cortana MK3 implications - MK2 mapping
What Portfolio replaces
| MK2 component | MK2 location | MK3 replacement |
|---|---|---|
| Hand-rolled equity calculation | dashboard/equity.py style aggregator over balances + position_tracker | self.portfolio.equity(venue=IBKR_VENUE) |
| P&L attribution per instrument | analytics/pnl_attribution.py SQL joins over decisions.db + position_state | self.portfolio.realized_pnl(instrument_id) + unrealized_pnl(instrument_id) |
Dashboard /api/positions/summary endpoint | aggregator handler in dashboard/app.py | thin wrapper around self.portfolio.net_exposures(venue) + unrealized_pnls(venue) |
Dashboard /api/equity/live endpoint | sums balances + tracker mark-to-market | self.portfolio.equity(venue=IBKR_VENUE) |
| ”Am I flat at EOD?” check | counts dict entries in position_tracker | self.portfolio.is_completely_flat() |
| Per-position UI line (“SPY 727C: +12%“) | computed from tracker + last quote | pos.unrealized_pnl(quote.bid) directly on the Position |
| ”Why is the dashboard wrong?” debugging | walks 3 code paths | self.portfolio.missing_price_instruments(IBKR_VENUE) |
| Currency-converted display | not implemented (USD-only book) | free via target_currency |
Does PortfolioAnalyzer subsume the M2 expectancy-harness Phase 1?
Mostly yes - partial. The expectancy harness Phase 1 needs:
| Expectancy-harness need | PortfolioAnalyzer coverage |
|---|---|
| Per-trade realized PnL series | Yes - position_returns() + realized_pnls series |
| Win rate over rolling N trades | Yes - built-in WinRate statistic, register with rolling window via custom PortfolioStatistic subclass |
| Average winner / average loser | Yes - straightforward custom PortfolioStatistic over realized_pnls series |
| Expectancy = (WR × avg_win) − ((1−WR) × avg_loss) | Yes - composite PortfolioStatistic calling the above two |
| Sharpe / Sortino on portfolio returns | Yes - built-in or trivial custom statistic over portfolio_returns() |
| Drawdown depth and duration | Likely yes - built-in tearsheet covers it; custom statistic if more detail needed |
Per-trigger-type expectancy (repeated_hits vs flow_alert vs impulse:strike_stack) | Partial - needs custom Actor. PortfolioAnalyzer is per-instrument and per-venue, not per-trigger-tag. The trigger tag must be attached to each Position via a custom event (ExitReason style; see nautilus-events.md) and aggregated by a custom Actor that subscribes to on_position_event and bins by trigger. |
| Score-conditioned expectancy (“trades with composite ≥ 8 vs < 8”) | Partial - needs custom Actor. Same shape as above - score at entry must be tagged on the Position via a custom field or a sidecar EntryScore event keyed on position_id, then aggregated externally. |
| Regime-conditioned expectancy (“opening range” vs “midday” vs “power hour”) | Partial - needs custom Actor. Same shape - regime tag attached at entry, aggregated by custom Actor. |
So: PortfolioAnalyzer covers ≥60% of expectancy-harness work for free. The remaining ~40% (per-trigger, per-score-bucket, per-regime expectancy) requires a custom Actor that subscribes to position events, joins entry-time tags to exit-time PnL, and produces conditional statistics. That custom Actor is small (≤200 LOC) and is the right place for it - exactly the kind of “analytics on top of the kernel event stream” that Actors exist for.
Verdict: PARTIAL. PortfolioAnalyzer is the right foundation. The M2 work is shorter than it would have been from scratch, but it is not zero. The Cortana-specific value is the conditional expectancy analysis, which is downstream of Portfolio and lives in a custom Actor.
What still requires custom code at the Strategy / Actor layer
- Entry-time annotations on Positions. Score, conviction, bias,
trigger type, regime tag at the moment of entry. Nautilus does not
encode these - publish a custom
EntryAnnotationevent tied toposition_idor use a custom field if Position is subclassable for the spike (consultnautilus-positions.md). - Per-trigger and per-regime expectancy rollup. Custom Actor
subscribing to
PositionClosed+EntryAnnotation, accumulating conditional statistics, publishing periodic summary events. - Exit-reason taxonomy. TP / SL / time-in-trade / EOD-flatten /
thesis-invalid. Publish via
ExitReasonevent (seenautilus-events.md). - Tick-driven mark display for the live dashboard. Cortana shows
current %-PnL on every quote tick; this is not a Portfolio event.
Subscribe a tick handler that reads
cache.position(id).unrealized_pnl(price)on the fly and emits a customPositionTelemetryevent. - MK2 win-rate dashboard with Cortana’s specific 80% mandate framing.
The 80% target is a bespoke threshold; the comparison and
visualization live in the dashboard layer, sourcing data from
analyzer.statistic("WinRate")over the user-chosen window.
What MK3 gains beyond bug elimination
- Free currency conversion for any future non-USD account.
- Free missing-price tracking - every silent-data-gap bug class becomes one API call to investigate.
- Free side-aware valuation - long book auto-uses bid, no manual “always use bid” plumbing.
- Free PortfolioAnalyzer - much of the Phase-1 statistics surface comes for free.
- Free multi-account, multi-venue aggregation. When Cortana adds a second venue (or runs the SaaS multi-tenant path), every aggregation query already supports per-venue and per-account scoping.
Caveats and gotchas
missing_price_instrumentsis venue-scoped, not account-scoped. Multi-account-on-one-venue setups can have one account flag an instrument that another account never tried to price. Read the doc.- Conversion failures return
Nonefor single-value methods. Defensive code must check for None before formatting. Easy to miss in templating. - Conversion failures silently omit entries from dict-returning methods. No exception is raised; consumers must compare the returned dict’s currency keys against the expected set.
- Portfolio returns require multi-day history. Single-day spike backtests will see the analyzer fall back to position returns silently. If your tearsheet looks empty in the “monthly returns heatmap” panel, this is why.
- Multi-currency accounts disable portfolio returns silently.
Position returns become the only series. Tearsheet uses whatever
returns()resolves to. Compute portfolio-level returns externally if needed. - Cache update lag in live mode. Per
nautilus-events.md: live events apply asynchronously and may show a brief delay before Cache reflects them. Portfolio queries during the immediate post-event window may see slightly stale state; use the event payload for exact-at-event values. - Portfolio is firm-level, not strategy-level. Per-strategy
rollups require
cache.positions_open(strategy_id=self.id)plus manual aggregation. Plan for it during multi-Strategy phases. use_mark_pricesanduse_mark_xratesdefaults vary by venue adapter. Verify inPortfolioConfigbefore assuming the price- fallback chain order. Cortana’s IBKR adapter likely defaults touse_mark_prices=false; quotes are the primary path.
When this concept applies
- Replacing MK2’s hand-rolled equity / P&L / dashboard summary code with Portfolio queries.
- Computing realized/unrealized/total PnL inside a Strategy or Actor.
- Implementing the M2 expectancy-harness Phase 1 work as a thin layer
on top of
PortfolioAnalyzer+ a small custom analytics Actor. - Writing the “is the dashboard accurate?” runbook - three Portfolio calls explain every discrepancy class.
- Designing the multi-tenant SaaS query layer where each tenant’s Portfolio is queried via per-account scoping.
- Backtesting and reading the auto-generated tearsheet.
When it breaks / does not apply
- Per-trigger or per-regime conditional expectancy. Portfolio is unconditional (instrument × venue × account). Conditional analytics live in a custom Actor that joins entry-time tags to exit-time PnL.
- Tick-driven mark display. Quote ticks do not emit Portfolio events. Build a custom telemetry event.
- Strategy-level equity attribution in a multi-Strategy node. Portfolio aggregates firm-wide. Use Cache + manual aggregation for per-strategy slicing.
- Multi-currency portfolio returns. Falls back to position returns silently. Compute externally.
- Single-day backtests reading the monthly heatmap. Insufficient
history;
portfolio_returns()is empty.
See Also
- Nautilus Concepts § Portfolio (lines 389-414) - topology-level Portfolio summary; this page is the deep dive.
- Nautilus Positions - the units the Portfolio aggregates; full Position field reference and lifecycle.
- Nautilus Accounting - parallel-filed; the Account balance source the Portfolio reads for its equity formula.
- Nautilus Cache - the underlying store; price
fallback chain reads via
Cache.get_xrate()andCache.get_mark_xrate(). - Nautilus Events -
PositionEventtaxonomy feeding any custom expectancy Actor. - Nautilus Strategies -
self.portfolioaccess pattern and the order-submission boundary. - Nautilus Actors - where conditional-expectancy rollup Actors live.
- 2026-05-09 Nautilus Spike Plan - Saturday evaluation; this page supports Steps 4 (port one signal), 5 (validate Strategy outputs), and 7 (multi-tenant evaluation).
- Brain RESOLVER - page filing rules.
Timeline
- 2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 3.