Portfolio and snapshot
The core publishes one immutable state view through an arc-swap cell that the window reads on repaint and can never back-pressure; the portfolio inside it is the named cross-venue aggregator, with the two totals that must not be printed as one.
crates/vike-core/src/snapshot.rs's CoreSnapshot is the immutable state view the core
publishes to the window through arc-swap. The GUI is a lossy downstream observer: it reads the
latest snapshot on repaint and can never back-pressure the core.
crates/vike-core/src/portfolio.rs's Portfolio is the named cross-venue aggregator inside it,
and crates/vike-core/src/runtime/publish.rs is the publish. This page is written from those
three.
When a snapshot is built
Snapshots are built coalesced, never per event while busy: dirty-flagged, at most once per
CoreConfig::snapshot_interval while the queue is busy — the interval is a minimum spacing, not a
deadline — and immediately whenever the core is about to go idle — if self.dirty directly before
blocking_recv. On a venue whose events arrive sporadically that idle path publishes per event,
which is why anything CoreSnapshot::build does is per-event work; the
execution core page carries that consequence. seq
increments per publish for change detection, and the build cost is measured in the latency
harness.
publish_guarded wraps the build and the GUI's repaint hook in catch_unwind: neither may kill
the core thread. On a panic the engine is halted and a minimal fault snapshot is stored —
trading_state: Halted, fault set — so the window still sees terminal state, never a stale
active cell. The recent-events ring holds already-rendered lines, so publish is a refcount bump
per entry, and an opt-in memory-mapped counter mirror is written at this same cadence.
What the snapshot carries
Beyond the primary engine's trading_state, balance and balance_mode, a snapshot holds
portfolio; positions and marks; orders, the registry in insertion order across every
engine; held_exits, the pending bracket exits held off the venue until their OTO parent fills,
which have no registered order and would otherwise be invisible; the core-owned bars cache,
shared by pointer; mounts; recent_events; fault; conflated_market_drops (latest-wins
working, not a bug) and rejected_commands (never silent); the recon block of held
divergences awaiting an operator confirm plus the last pass timestamp; and recon_coin_deltas,
populated only by a venue whose position report carries a delta — Deribit today — so the
greeks tool can fold a perp or future hedge leg.
Each PositionView carries the resolver-priced unrealized and the mark_source that priced
it (None means unpriceable), the effective leverage, an estimated liq_price, the
margin_mode and any isolated_margin. Each VenueBlock — one per engine, primary first —
carries balance, realized PnL, fees, funding, balance_mode, resolver-priced equity and
unrealized, the missing_prices count, margin_used, free_bp and margin_ratio (computed
only when the margin gate is armed, so the default path stays zero-cost), the effective
fee_schedule, the venue's trading_state, and the contract-multiplier grid shared with the
engine's account by Arc — the whole grid, because the window needs a multiplier for symbols
it holds no position in. CoreSnapshot::build does one resolve per engine through the
price board, so a block's equity, its unrealized total, its
missing count and every position's mark source come from the same read.
The mount rows and the residual
mounts holds one MountView per live strategy mount — venue, symbol, interval, ready, and
the per-mount attribution: net position, realized PnL net of fees, unrealized PnL, notional, any
budget and whether it latched. When any mount exists the vector carries one trailing row,
MountRowKind::Residual: the account's realized PnL net of fees minus the sum of every mount
row's. Attribution is keyed by client order id, an un-minted id is simply not attributed, and
nothing used to sum the two sides — so the mount rows and the account disagreed systematically
and silently. With the row, Σ mount rows + residual == account realized (net) is an invariant
anything can assert, and manual tickets, margin-call and budget-latch liquidations,
reconcile-adopted orders and fills whose mount could not be reconstructed across a restart all
land there visibly. Read kind before treating a row as a mount.
The portfolio
Portfolio groups what used to be loose scalars: equity (the primary engine,
resolver-priced), equity_total, realized_pnl, fees_paid, funding_paid,
margin_used_total, capital_base, missing_prices_total, the per-asset
balances_by_asset ledger, and the venues blocks. It is a naming reshape — no value changed —
with a read-only query surface over the blocks: venue(name), equity(venue),
unrealized_pnl(venue), net_position, gross_position, is_net_long, is_flat,
position_count (rows, not open positions), and missing_price_instruments, which names the
(venue, symbol) pairs the count only counts. None of it runs on the fold.
equity_total is heterogeneous — do not print it as one figure
A VenueBlock's equity answers a different question depending on its balance_mode. A
Delta block is the daemon's own book-keeping from a configured seed — seed + own cash flow + realized + unrealized — an assertion about this mount's trading, which is why it can be summed
across venues. An Authoritative block is the venue's attested wallet for the whole account the
credentials open, adopted verbatim by reconcile or an account-state push, and the daemon cannot
tell whether that account is dedicated to it: cash is fetched account-wide while positions and
fills are symbol-scoped. Measured on prod2 on 2026-08-17: nine paper mounts at 1000 seed each
plus one bybit mount that had adopted a shared account's 53647 USDT wallet summed to a headline
equity_total of 62647.10600813 — neither the account's cash nor the daemon's book. The value
is unchanged; the honest split sits beside it as equity_book_total and equity_wallet_total,
which partition it by mode, and wallet_venues names whose wallets were added. They do not sum
back to equity_total bit-for-bit, because each is its own compensated sum over a subsequence.
The scalar realized_pnl is the primary engine's only
realized_pnl, fees_paid and funding_paid on the portfolio are bound from the primary
engine's account, so on a multi-venue core they answer about whichever venue is registered
first. Measured on prod2: the daemon's summary line read realized_pnl: 0.0 from an untraded
binance primary while equity_total tracked a bybit mount's ten maker fills to eight decimals.
A report that carries equity_total must fold realized_pnl_total, fees_paid_total and
funding_paid_total — the same compensated sum over the same vector in the same order.
The risk curve
capital_base is the operator's configured capital, Σ seed_cash in venue registration order:
a config constant, not an observation, identical across a restart and untouchable by a third
party moving money in a shared account. A portfolio built off the node wire reports 0.0
there, which is honest — the observe path has no seeds and drives no alerting — and 0.0
means unknown, never "no capital". pnl_total is the daemon's own profit and loss across every
venue, realized − fees + funding + unrealized, mode-blind and carrying no balance level, so a
wallet movement with no trading behind it does not move it. drawdown_curve is
capital_base + pnl_total: the quantity the watchdog's drawdown
latch folds its high-water mark on and the alerting drawdown rule
compares against, pinned bit-identical between the two. Use it, not equity_total, for any
drawdown fraction — and guard a non-positive value rather than divide by it.
Timers
One allocation-free, deterministic deadline wheel, advanced once per drain-loop boundary and never per message, carries every periodic sweep the live core runs.
The control boundary
The live core has exactly one external control surface — commands and order intents in, snapshot queries out — and every front end, in-process or remote, is a translator over it.