ADR-0014: Paper broker switches to cross-margin; live brokers source state from the exchange
- Status: accepted, supersedes ADR-0013
- Date: 2026-06-02
- Affects:
packages/brokers/paper,packages/brokers/hyperliquid-*,packages/shared/src/broker.ts,packages/agent-runtime/src/context.ts
Context
ADR-0013 introduced leverage + liquidation accounting in the paper broker using an isolated-margin model. That ADR explicitly listed cross-margin as deferred and acknowledged the model would overstate liquidation risk for multi-position accounts (a winner cushions a loser on Hyperliquid; we ignored that).
Two follow-up corrections:
- Hyperliquid is cross-margin by default. The whole account equity backs every position; per-position liquidation in isolation is not how the exchange actually behaves. Sim results from the isolated model would systematically diverge from live for any portfolio with more than one position. A trader iterating on a multi-symbol strategy against paper would see "safer" results than they'd get live.
- For live brokers, margin + liquidation are not ours to compute. The exchange is the source of truth: position margin, maintenance margin tier, and the per-position liquidation price all live on Hyperliquid's clearinghouse. Any locally-derived value will drift — silently, dangerously — from what the exchange uses at liquidation time. The live adapter must read these from the API; reimplementing the formula is a footgun.
The Position.liquidationPrice and EquitySnapshot.marginUsedUsd / marginFreeUsd fields on the shared BrokerAdapter contract already exist (added in ADR-0013). What needs to change is who computes them and how.
Decision
1. Paper broker → cross-margin model
Account-level equity backs all positions. The trader still picks a per-symbol leverage at order time; that sets the initial margin reserved for the position (entryNotional / leverage). Maintenance margin is notional * maintenanceMarginRate, summed across positions. Liquidation triggers at the account level when:
equity < Σ position_notional * maintenanceMarginRateWhen liquidation triggers, the broker force-closes all positions at current marks and clamps cash at zero. (Real Hyperliquid auto-deleverages incrementally; all-or-nothing is conservative — the trader's sim shows the worst case.)
Per-position liquidation price (for display, matching the Hyperliquid UI) is computed dynamically at each snapshot() by solving for the symbol's mark at which account equity would equal maintenance margin requirement, holding all other positions' marks constant:
Long: M_liq = (cash + otherPnl − entry·size − otherMaintMargin)
/ (size · (mm − 1)) // denominator is negative
Short: M_liq = (cash + otherPnl + entry·size − otherMaintMargin)
/ (size · (1 + mm))For positions whose cushion makes liquidation unreachable from this symbol alone (a long where price→0 still wouldn't trigger, given cash + other-position cushion), the broker returns 0 for longs / Infinity for shorts. The agent context renders these as liq=unreachable (cushion from cash + other positions) instead of a misleading numeric value.
The math lives in computeCrossMarginLiqPrice (exported from the paper broker for testability).
2. BrokerAdapter contract: state is broker-authoritative
The shared BrokerAdapter interface docs are amended to explicitly state that Position.liquidationPrice, Position.leverage, EquitySnapshot.marginUsedUsd, and EquitySnapshot.marginFreeUsd are the broker's own truth — paper computes them; live brokers MUST source them from the exchange API (clearinghouseState on Hyperliquid). The engine and the agent runtime trust whatever the broker returns from snapshot() and do not recompute.
3. Hyperliquid adapter stub documents the requirement
The Hyperliquid mainnet adapter stub (testnet was removed in ADR-0015) will carry a header note when implemented: source margin + liquidation directly from Hyperliquid's info endpoint (clearinghouseState). Do not reimplement the tier formula — Hyperliquid changes maintenance margin tiers, position size brackets, and isolated/cross treatment over time; any local reimplementation will drift.
Alternatives considered
Alt A — Keep isolated-margin (ADR-0013) and let live use Hyperliquid's number
- Paper sim stays simple; trader picks leverage and a clean liq formula computes a single liq price.
- Sim results materially diverge from live for multi-position portfolios. A momentum-rotation strategy iterating in sim would routinely show survival where live would liquidate, or show liquidation where live has enough cushion to ride through.
- This is the iterating-on-strategy use case; sim fidelity matters more than implementation simplicity. Not picked.
Alt B — Cross-margin in paper, but also have paper compute a per-position liq price using a static "single-position" assumption
- Trader gets a stable per-position liq number even though account-level math is in play.
- Misleading: the displayed number wouldn't match what
setMarkPriceactually triggers on. The agent would see "liq=$40,250" and never get liquidated at that price. - Not picked. Liq price must be derived from the actual account state.
Alt C — Implement Hyperliquid's exact tiered maintenance-margin schedule in paper
- Maximally faithful sim.
- Requires per-symbol tier tables that Hyperliquid updates; ongoing maintenance burden in our code for behavior the exchange owns.
- A single
maintenanceMarginRateconstant (default 50 bps, configurable) is faithful enough for strategy authoring confidence. Paper users tuning for specific small-cap symbols can override. - Not picked, but configurable so users can approximate per-symbol if they need to.
Alt D — Auto-deleverage paper to incrementally restore MM (instead of all-or-nothing)
- More faithful to Hyperliquid's actual liquidation engine.
- Significantly more complex (which positions to close first? Hyperliquid uses ranking heuristics that vary).
- All-or-nothing is conservative for the trader's sim — they see the worst case. That's the right bias for a strategy-authoring tool.
- Not picked for MVP.
Consequences
Positive
- Sim matches live's accounting model. Multi-position strategies get the cross-margin cushion the exchange will actually give them in live. No more surprise liquidations or surprise survivals between sim and mainnet.
- Per-position liquidation price reflects portfolio state. As one position moves into profit, others' liq prices move further away — exactly as the trader's mental model expects from Hyperliquid's UI.
- Live brokers can't go wrong. Adapter authors are explicitly told to source liq + margin from the exchange. No "I'll just use the same formula" trap — it's not present.
- Liq math is testable in isolation.
computeCrossMarginLiqPriceis exported as a pure function.
Negative / trade-offs
- Liq price recomputed each
snapshot(). Two passes over positions on every snapshot (first for totals, second for per-position liq). Linear in number of positions; negligible for any realistic skill. - All-or-nothing liquidation is still a simplification. Real Hyperliquid would close less. Documented; the bias is toward making the trader's sim more conservative, which is the right error direction.
- Single maintenance-margin rate. Doesn't replicate per-symbol tiers. Mitigated by
maintenanceMarginRatebeing a configurable broker option; sims for alt symbols can pass a higher value. - Bar-aware liquidation still deferred. A bar whose intra-bar low touched the trigger but whose close didn't will still not liquidate. Same caveat as ADR-0013.
Things we'll need to revisit
- If observed sim vs mainnet divergence exceeds a few % equity for multi-position strategies, build a per-symbol maintenance-margin tier table (loaded from a static config) and let the paper broker look up the right rate per position.
- Bar-range liquidation in the simulator (broker accepts a
markRange(symbol, low, high)call so it can liquidate based on intra-bar extremes). - An explicit "you were liquidated on tick T" signal in the agent's
## Last decisionsection — currently the agent has to infer it from a vanished position + dropped equity.
References
packages/brokers/paper/src/index.ts— cross-margin implementation; seecomputeCrossMarginLiqPriceandmaybeLiquidateAccountpackages/brokers/hyperliquid-mainnet/(when scaffolded) — adapter must source liq + margin fromclearinghouseState; testnet adapter was removed (ADR-0015)packages/shared/src/broker.ts—BrokerAdaptercontract; explicit note that margin + liq are broker-authoritativepackages/agent-runtime/src/context.ts—formatPortfolio/formatLiqrendering- ADR-0013 — superseded; the isolated-margin MVP this replaces
- ADR-0001 — Hyperliquid as the MVP exchange
- Hyperliquid docs: cross-margin model,
clearinghouseStateinfo endpoint