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:
| rung | slot | meaning | who fills it |
|---|---|---|---|
| 1 | mark | the 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 |
| 2 | bid / ask | the 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 it | any venue with a quote or L2 feed (set_quote) |
| 3 | last_trade | the last price something actually traded at — real and recent, but one-sided and already stale when it lands | any venue with a trade feed, including one subscribed only because a chart asked |
| 4 | bar_close | the close of the last completed candle — a minute old on a 1m chart, an hour on a 1h one | every kline feed, every venue |
| 5 | last-known | not a slot: the same four rungs walked again with the freshness gate relaxed, taking the highest-priority slot holding any value however stale | the 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.
Margin calls
A pure margin-call model over the live account computes liquidation intents that the core submits through the normal risk gate, on one liquidation law whose pool is the parameter, priced by the same resolver the operator sees.
Emulated orders
The core keeps its own book of stop and trailing-stop arms for venues and strategies without native conditional orders; arming bypasses the risk gate, and only the fire crosses it, as a plain market order.