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.
Ingest lanes
The producer-side channels every venue adapter holds — lossless where a missed message is a hole, latest-wins where only the freshest value matters — and why they live one crate below the core that drains them.
Contingent orders
One-triggers-other arming and one-cancels-other cancellation are decided by a single resolver that every fill lane calls, so the paper book and the backtest broker can no longer disagree about what a bracket does.