vike
Concepts

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 by cross_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 Warning arm.
  • 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.0 on degenerate inputs or non-convergence, the safe verdict.
  • liquidation_price — the isolated linear-perp mark: long avg × (1 − im) / (1 − mm), short avg × (1 + im) / (1 + mm); 0.0 means no liquidation by price.
  • clamp_leverage — request and cap both floored at 1.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.

On this page