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.
The margin-call model is crates/vike-exec/src/margin_call.rs's check_margin_call_priced.
It is pure: it computes intents, and the caller — the core's
watchdog, opted in through CoreConfig::margin_call — mints the
client order ids and submits through the normal gate path. A veto surfaces as OrderDenied; a
margin call can never bypass the RiskGate. The buying-power primitives are
crates/vike-model/src/margin.rs, and the breach law is crates/vike-model/src/liquidation.rs.
This page is written from those three modules.
The three verdicts
MarginCallConfig carries three fractions, defaulting to mm_requirement: 0.05 (the
maintenance-margin fraction), warn_fraction: 0.05 and buffer: 0.10. One sweep returns
MarginCall::Healthy; Warning { margin_used, margin_remaining } when remaining margin is at
or below equity × warn_fraction but nothing is liquidatable yet; or Liquidate(intents). A
LiquidationIntent is a position-reducing market order — venue, symbol, position side, the
closing side opposite the position's sign, and a quantity. An unmarked position contributes no
margin and is never liquidated.
One law, the pool is the parameter
vike_model::pool_breached is the breach predicate for every pool: breached only when
pool_maintenance > 0, pool_equity − pool_maintenance ≤ 0, and
pool_maintenance > pool_equity × (1 + buffer). A NaN anywhere reads as not breached — the
law refuses to liquidate on garbage inputs. It takes the folded maintenance product rather than
(rate, notional) because Σ(|q|·mark·mult·rate) and rate·Σ(|q|·mark·mult) differ in
f64, and the live verdict had to stay byte-identical to its pre-law behaviour.
Positions are partitioned by vike_model::MarginMode through vike_model::partition_pools:
- Cross positions — the default, every position today — share one pool. A breach runs
vike_model::cross_liquidation_plan: sort by unrealized PnL ascending so the biggest losers go first (a stable sort, so ties keep insertion order), liquidate only the excess over the line position by position, and stop as soon as the pool is healthy. A cross-only account is byte-identical to the pre-partition sweep, pinned bycross_only_verdicts_byte_identical_to_legacy. - Isolated positions each form their own pool — the walled-off wallet plus their own
unrealized PnL, judged with a zero buffer. A breach closes that position only, full size, and
can never drag the cross pool: its wallet and PnL are subtracted from the cross pool's equity.
An isolated position with no reported wallet, or no mark, is skipped, and isolated pools have
no
Warningarm. - Cash positions never liquidate; fully funded, they structurally cannot breach.
Intents come out cross plan first, then isolated closes — one deterministic release order for the journal-then-submit loop.
One maintenance rate
cfg.mm_requirement prices every pool. A per-symbol venue-reported maintenance rate would take
precedence when present, but no venue adapter parses one today, so the SymbolProperties
carrier is a documented follow-up rather than a guessed wire field. The GUI's liquidation badge
reads the same source — the operator's watchdog config when armed, else that config's default.
Priced the way the operator sees it
The live watchdog passes a resolver closure over the engine's
price board, so margin used shares the price basis of the
resolver-priced equity it is compared against: every price read in the sweep — the cross
margin-used fold, each pool's unrealized PnL, the isolated maintenance line, the cross
candidates — goes through the one price_of, and a stale mark can no longer overstate margin
against a fresh-quote equity or vice versa. The legacy check_margin_call prices at
Account.marks for callers with no board, such as tests and offline tools. The equity itself
is ExecutionEngine::resolved_equity, the scalar every decision path reads so an
auto-liquidation acts on exactly the number the snapshot displays.
A known venue asymmetry: isolated mode without a wallet
A venue can report a position isolated while surfacing no per-position wallet. The sweep then
subtracts only that position's unrealized PnL, skips its isolated judgment (never a spurious
close), and leaves the unknown wallet folded inside the account balance — so the cross pool's
equity is overstated by that wallet and a genuine cross breach is judged late. The venue itself
still liquidates the isolated position. The module records a live probe of 2026-07-19 in which
both isolation paths were refused on the demo account, so the field semantics stayed
unverifiable and isolated_margin: None remains the pinned parse.
When and where it runs
The sweep runs once per closed bar of the engine's own series, where marks are fresh and the
path is the per-bar lane rather than the event fold, so the
latency gate is untouched. CoreThread::sweep_margin_call
runs the model per engine: each account is checked at its own seed and its liquidations are
routed to that engine by index (EngineRoute::Engine) rather than by venue string — two
accounts of one exchange share a venue string, and routing by it once sent a reduce-only close
of account B's position to account A. A Warning lands in the recent-events ring; each
liquidation is a reduce_only market request journaled write-ahead
(append_margin_call_liquidate) before apply_intent, with no mount id, so its fill lands in
the portfolio's unattributed residual row. Without
that record a replay could not reproduce the liquidation, and the determinism fence caught it
as a hash mismatch.
The buying-power primitives
crates/vike-model/src/margin.rs is the venue-free core, as pure f64 functions. Quantities
are signed, margins are in account currency, rate converts the quote currency to the account
currency (1.0 when the same), and im_req/mm_req are fractions — 0.1 means 10×. Callers
own fee handling and pass a fee-adjusted requirement in.
initial_margin—rate × mult × price × qty × im_req, signed, negative for shorts.maintenance_margin—|qty| × price × mult × rate × mm_req.free_buying_power— equity minus margin in use, plus the credit a direction-reversing order frees, minus the required-free haircut, floored at zero.amount_to_order— the signed quantity that lands the position's margin at or under a target on the lot grid;0.0on degenerate inputs or non-convergence, the safe verdict.liquidation_price— the isolated linear-perp mark: longavg × (1 − im) / (1 − mm), shortavg × (1 + im) / (1 + mm);0.0means no liquidation by price.clamp_leverage— request and cap both floored at1.0, so a sub-unity cap can never pull effective leverage below one.
The snapshot's VenueBlock publishes margin_used, free_bp and margin_ratio from the same
primitives, computed only when the margin gate is armed; when it is off free_bp equals equity
and the ratio is zero. Each PositionView::liq_price uses liquidation_price for an isolated
position, vike_model::cross_liquidation_price_est for a cross one — the shared-pool line with
every other position frozen, advisory — and 0.0 for cash. The gate that admits orders in the
first place is the risk and arming page's subject.
Halt
An engaged halt stops orders that open risk and admits orders that close it; the two mechanisms that enforce that rule agree on policy and differ only in how much they can verify.
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.