vike
Concepts

The price board

One cell per (venue, symbol) holds a price from every source that has spoken, and a side-aware read walks them in a fixed order so a position is valued at the price it could actually be exited at.

crates/vike-exec/src/price_board.rs's PriceBoard is where every price the core hears is kept, tagged by where it came from. Its write side sits on the hot fold and does nothing but store; its read side runs only on cold paths — snapshot publish, sweeps, timers — and answers one question: what is this position worth to me right now, if I had to act on it. The board is deliberately not serialized: it sits outside EngineSnapshot, so the journal's determinism-fence hash is untouched. This page is written from that module and from crates/vike-exec/src/account.rs's MarkSource.

The cell

A PriceCell holds five optional (price, timestamp) slots: mark, bid, ask, last_trade, bar_close. The timestamp is the producer's where the lane carries one, else the dispatch clock. Every write is unconditional and source-tagged by construction, so a candle close and a venue mark both land without either destroying the other — which is why no precedence question arises on the write side. A zero, negative or NaN price is ignored and creates no slot; a quote with one dead side stores only the live side. Cells are keyed by (venue, symbol) in insertion order.

The valuation law

PriceBoard::classify walks the slots in a fixed order and returns at the first fresh one. The ordering is the law — a rung is consulted only when every rung above it is absent or stale:

rungslotmeaningwho fills it
1markthe venue's own mark or index price — what its liquidation and funding engines key off; the only price that answers "will I be liquidated"the crypto perps' mark streams (binance-family @markPrice@1s, bybit tickers.markPrice, okx mark-price, hyperliquid activeAssetCtx.markPx), each on by default; aster's is off until VIKE_MARK_STREAMS_ASTER=1, and VIKE_MARK_STREAMS=0 turns them all off. On any venue, also a fill's mark_price and a reconcile pass's position_mark_px
2bid / askthe live top of book, read side-aware: a long at the bid, a short at the ask — the side it would have to cross to exit, so the spread is booked against the position, never for itany venue with a quote or L2 feed (set_quote)
3last_tradethe last price something actually traded at — real and recent, but one-sided and already stale when it landsany venue with a trade feed, including one subscribed only because a chart asked
4bar_closethe close of the last completed candle — a minute old on a 1m chart, an hour on a 1h oneevery kline feed, every venue
5last-knownnot a slot: the same four rungs walked again with the freshness gate relaxed, taking the highest-priority slot holding any value however stalethe freshest surviving slot

resolve is a thin projection of classify: Fresh becomes Resolution::Priced { px, source, ts }, Missing is a first-class answer, and rung 5 becomes Resolution::Stale only when the caller opted into PriceCfg::stale_fallback, so a consumer valuing off it knows it is stale. classify itself is independent of that knob and always reports fresh, stale or missing, so an operator query can see "priced but stale" even when valuation will not fall back to it.

Freshness is off by default

Every PriceCfg max-age field defaults to None and stale_fallback to false, so a caller that has not opted into a window sees the pure priority order, nothing is ever stale, and rung 5 is unreachable. A fresh venue mark early-returns at rung 1, so the common case pays for one slot check. use_mark: false drops the mark rung from both walks.

Why the candle close is last

Before the mark-slot split, every kline feed wrote its close into the mark slot, so a stale candle outranked a live quote and a live trade on the same symbol. Putting the close at the tail is a real, intended behaviour change on every venue that has no venue mark but does have a quote or trade feed running — alpaca, IBKR, binance spot, an aster perp not opted into its mark stream, any perp under VIKE_MARK_STREAMS=0: those positions are now valued at the live side-aware quote or the last trade instead of the candle close. Valuation moves by up to the spread, toward the price the position could actually be exited at.

Missing prices

PriceBoard::note keeps a per-venue set of symbols whose resolution came back Missing. The first insertion of an episode warns once — no price for open position — subscribe to quotes, trades or bars for continuous mark-to-market — and a Priced or Stale resolution removes the symbol, re-arming the warning for the next episode. The equity sampler (see timers) notes every open position once per sample; the snapshot surfaces the count as VenueBlock::missing_prices and each position's mark_source: None, and Portfolio::missing_price_instruments names the pairs.

The board is not the account mark slot

Account.marks is the opposite shape: one untagged scalar per symbol, written by the same lanes and read by the pre-trade gate, the margin-call law and LiveBroker.price. Its precedence rule lives in vike_exec::MarkSource and is enforced inside Account::set_mark_from, the only door into that map. A genuine venue mark — streamed (VenueMark) or reported by a reconcile snapshot (ReconcileMark, ranked equal) — owns the slot for a window: DEFAULT_MARK_STALENESS_MS is 10 000 ms, about ten missed ticks of a 1s stream, and DEFAULT_RECONCILE_STALENESS_MS is 150 000 ms, 2.5× the default reconcile interval, so a reconciled stream-less venue's slot stays owned continuously between passes rather than alternating with candle closes every minute. A mark stream that dies hands the slot back after its window and closes resume, so valuation degrades to the previous rung instead of freezing at a stale mark. The module doc is explicit that the board must not reimplement that rule: a freshness predicate on the board is how an earlier round of the fix ended up bypassable by every lane that did not happen to call it.

Who reads it

ExecutionEngine::resolve_equity builds a whole ResolvedEquity — equity, unrealized total, the missing count and one ResolvedPosition per position — from one resolve per engine, so the snapshot's VenueBlock and every PositionView come from the same read. Its scalar twin resolved_equity is the decision-path entry point: the liquidation watchdog, the drawdown latch, the armed margin gate, a strategy's ctx.equity and the portfolio-snap journal all read equity through it, so an auto-liquidation acts on exactly the number the operator sees. The readiness gate on a mount flips a pending mount to ready the moment either side of its symbol resolves through its own engine's board.

On this page