Nautilus Portfolio

The Nautilus Portfolio is the central, kernel-owned aggregator for every open Position across every Strategy, Actor, and venue in the running node. It is exposed as self.portfolio on 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-provided target_currency), and explicit missing-price flagging instead of silent zero-valuation. The companion PortfolioAnalyzer produces realized-PnL statistics, position- return statistics, and portfolio-return statistics across configurable windows, drives the backtest tearsheet, and accepts custom statistics via the PortfolioStatistic base 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

MethodReturnsMeaning
is_net_long(instrument_id)boolNet signed quantity > 0
is_net_short(instrument_id)boolNet signed quantity < 0
is_flat(instrument_id)boolNet signed quantity == 0 (no open position OR closed)
is_completely_flat()boolTrue if every instrument is flat - node-wide “do I have any exposure?”

Per-instrument values

MethodReturnsMeaning
net_position(instrument_id)DecimalSigned net quantity in instrument units
net_exposure(instrument_id, account_id=None, target_currency=None)Money | NoneNotional exposure in account base or target_currency; None if unconvertible
unrealized_pnl(instrument_id, account_id=None, target_currency=None)Money | NoneMark-to-market PnL on open quantity
realized_pnl(instrument_id, account_id=None, target_currency=None)Money | NoneCumulative realized PnL on closed portions
total_pnl(instrument_id, account_id=None, target_currency=None)Money | Nonerealized + unrealized

Per-venue / multi-instrument aggregates (dict-returning)

MethodReturnsMeaning
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

MethodReturnsMeaning
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_exposure is side-aware: longs are valued at the bid (conservative liquidation), shorts at the ask, mixed books at mid. With use_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 uses mark_values() internally; the margin path uses the same cached unrealized-PnL pipeline that powers unrealized_pnls().
  • Per-venue scope: most methods take venue for scoping. account_id=None aggregates across every account on that venue; passing account_id narrows to one account.
  • missing_price_instruments is venue-scoped, not account-scoped - an account-filtered mark_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_currency for a single-currency result.

For net_exposure() (single instrument across accounts):

  • Different base currencies: returns None unless you provide target_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): return None and log an error to prevent silently- wrong values. Code must check for None.
  • 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:

CompositionPrice type usedRationale
All longBIDConservative - price you’d actually receive on liquidation
All shortASKConservative - price you’d pay to cover
Mixed (longs + shorts)MIDNeutral - neither side preferred
Mixed, use_mark_xrates=trueMARKContinuous 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:

  1. Mark price - if use_mark_prices=true in PortfolioConfig and a mark price is cached.
  2. Side-appropriate quote - BID for longs, ASK for shorts.
  3. Last trade price.
  4. 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 from Cache.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:

  1. 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.
  2. The next transition back to priced clears the entry, so a future drop re-warns. Edge-triggered.
  3. 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 componentMK2 locationMK3 replacement
Hand-rolled equity calculationdashboard/equity.py style aggregator over balances + position_trackerself.portfolio.equity(venue=IBKR_VENUE)
P&L attribution per instrumentanalytics/pnl_attribution.py SQL joins over decisions.db + position_stateself.portfolio.realized_pnl(instrument_id) + unrealized_pnl(instrument_id)
Dashboard /api/positions/summary endpointaggregator handler in dashboard/app.pythin wrapper around self.portfolio.net_exposures(venue) + unrealized_pnls(venue)
Dashboard /api/equity/live endpointsums balances + tracker mark-to-marketself.portfolio.equity(venue=IBKR_VENUE)
”Am I flat at EOD?” checkcounts dict entries in position_trackerself.portfolio.is_completely_flat()
Per-position UI line (“SPY 727C: +12%“)computed from tracker + last quotepos.unrealized_pnl(quote.bid) directly on the Position
”Why is the dashboard wrong?” debuggingwalks 3 code pathsself.portfolio.missing_price_instruments(IBKR_VENUE)
Currency-converted displaynot 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 needPortfolioAnalyzer coverage
Per-trade realized PnL seriesYes - position_returns() + realized_pnls series
Win rate over rolling N tradesYes - built-in WinRate statistic, register with rolling window via custom PortfolioStatistic subclass
Average winner / average loserYes - 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 returnsYes - built-in or trivial custom statistic over portfolio_returns()
Drawdown depth and durationLikely 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 EntryAnnotation event tied to position_id or use a custom field if Position is subclassable for the spike (consult nautilus-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 ExitReason event (see nautilus-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 custom PositionTelemetry event.
  • 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_instruments is 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 None for 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_prices and use_mark_xrates defaults vary by venue adapter. Verify in PortfolioConfig before assuming the price- fallback chain order. Cortana’s IBKR adapter likely defaults to use_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


Timeline

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