vike
Concepts

The account

Positions and realized PnL are a read model folded from the fill stream, with one writer, no equity cache, insertion-ordered sums pinned by the parity gate, and a fill-dedup ledger welded to the money it protects.

crates/vike-exec/src/account.rs's Account is the money-side read model of one venue account: positions and realized PnL derived from the fill stream, plus cash, fees, funding and the mark slot equity is valued at. Its header lists the contract in one block, and this page is that block expanded.

One writer, no cache, fixed fold order

Account is per-venue — apply_fill asserts the fill's venue matches its own — and keys positions on (venue, symbol, position_side), with Both for one-way and spot and the side reserved for hedge-mode perpetuals. A PositionEntry is a signed size and an avg_px, plus a margin-mode carrier so a position can be isolated. fold is the sole writer of position and realized PnL: both apply_fill and apply_liquidation route through it, and it delegates the arithmetic to vike_model::compute_fill — the one cost-basis primitive, whose doc forbids reordering an operation or "simplifying" an expression — so the backtest engines and the live read model can never drift about what open, add, reduce, close and flip mean. A position opening from flat takes its asset's ruling margin mode rather than the type default, because one venue publishes isolated-only per asset and a bare default booked every new position cross with nothing downstream ever correcting it.

There is no equity cache anywhere — the header names catastrophic cancellation as the risk — so every read is a sparse full recompute. equity_all is mode-aware: under BalanceMode::Delta it is seed + balance + realized + Σ unrealized; under Authoritative, set when a venue's AccountState assigns the balance absolutely, it is balance + Σ unrealized, because the venue's number already folds the rest in. The sum is vike_model::py_sum over positions in insertion order, and that is why every map on this type is an IndexMap and never a HashMap: the insertion order is the f64 fold order the parity gate pins against fixtures/r0/account_scenarios.json. margin_in_use is the same discipline applied to margin — one fold that had been hand-rolled four times and had drifted on the rate fallback; only the rate policy differs per caller now.

The mark slot has one writer and a precedence law

Account.marks is a single untagged scalar per symbol, read by the pre-trade risk gate, the margin-call law, the trailing-stop seed and what a mounted strategy calls "the price". Several feeds write it at wildly different cadences, so set_mark_from — the only way in; the field is private after the law was bypassed three times — carries the rule: while a symbol's slot holds a genuine venue mark observed within its source's ownership window, a bar-close or trade-tick write is dropped; otherwise last-write-wins. A streamed mark owns for DEFAULT_MARK_STALENESS_MS (10 s, about ten missed ticks); a mark sampled by reconciliation owns for DEFAULT_RECONCILE_STALENESS_MS (150 s, sized past the default reconcile interval so the slot does not alternate between passes). The same writer is the hostile-venue guard: a non-finite mark is refused, and the doc explains why px > 0.0 is not that check — +inf passes it and turns equity into NaN, where every risk comparison reads false. The concepts the resolver drops still fill their own price board slots.

The fill-dedup ledger lives on the aggregate that owns the money

apply_fill moves money — commission out of cash, fees accrued per asset, the position fold, realized PnL — so it is not safe to call twice for one venue execution. The header records why the guard moved here from the engine: the number of paths that re-deliver an already-folded fill kept growing, and each is a producer that knows it duplicates — bybit's fast-then-authoritative execution pair, binance perp's early fill and its twin sharing one id, hyperliquid's whole userFills snapshot re-sent on every reconnect, alpaca's since_id boundary, the resync and replay lanes. Account owns the set, consults it before anything moves, and reports a FillFold: Applied, Duplicate (nothing moved, duplicate_fills_refused advanced — a large value here is normal on those venues), or Collision.

Keyed on the bare trade id, with a fingerprint that makes the narrowness visible

Venue trade ids are per-symbol sequences on several venues, and the paper client's counter is per-client, so two different executions can carry the same id string. Widening the key would be the move that loses money: the reconciliation diff compares venue-reported ids as bare strings, so a composite key would stop matching there and an already-folded fill would come back as a MissingFill and be folded a second time — the exact double-count the ledger exists to prevent. So the key stays bare, and each entry stores an FNV-1a fingerprint over (symbol, side, last_qty, last_px). Same id and same fingerprint is a re-delivery, refused silently. Same id and a different fingerprint is an id collision — a genuine fill about to be dropped — counted into colliding_fills_refused and logged at error naming both fills. It is still refused, because folding on a mismatch would make the guard depend on venue rounding being stable; any nonzero value on that counter is a fault, and it understates the position. Reconcile the venue.

Two properties of the set are deliberate. It is unbounded: every capped alternative re-admits a duplicate the moment a replay window reaches back past the cap, and the widest window here is a one-hour lookback plus row-count-bounded venue history endpoints, not a fill count. And it is a within-session guarantee: crates/vike-exec/src/account.rs's seed_seen_fill_ids records that seeding is the caller's job and no shipped binary does it, so a fresh process starts empty and a replay lane reaching before the restart re-folds prior sessions' fills; a replay lane's spawn-time floor and this ledger are independent, so neither substitutes for the other. An empty trade id is unrepresentable: FillEvent::trade_id is a TradeId whose constructor refuses the empty string.

The other folds

apply_funding nets a periodic funding cashflow into cash. apply_account_state sets the balance authoritatively from a venue snapshot — the quote-asset row if present, else a single balance, else the py_sum of all — and flips the mode. apply_liquidation closes min(reported, held) at the liquidation price through fold — a reported qty of exactly 0.0 closes the WHOLE held size, the Python truthiness it ports — clamped so an over-reported frame can never flip the position, and deducts the fee. What each event carries is one page each under events; which divergences reconciliation folds back into this account is under reconciliation.

snapshot and restore round-trip the account for the journal's Snap record and for replay. Five things are deliberately not restored: the per-asset ruling margin-mode grid (mount configuration rather than folded state — every restored position keeps its own margin mode, but an open-from-flat after a bare restore reads an empty grid, so a live restore path has to re-apply it), the balance-sync realized-PnL baseline (cleared, so the first reconcile pass re-adopts venue cash), mark provenance (a restored slot is unowned, and the next writer takes it), the two refusal counters (live health signals, not folded state), and the dedup ledger itself — its durable home is the engine snapshot's seen_trade_ids, seeded back immediately after restore, which is what kept the journal wire and its state_hash fence byte-identical when the set moved onto this type.

On this page