vike
Concepts

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.

On this page