A first-person, engineering-side write-up of the part of an exchange that's terrifying to build: the system that decides, in real time, when a trader can no longer cover their position — and closes it before the platform eats the loss.
The matching engine gets all the attention. But if you're building a venue that offers leverage — perps, futures, margin — the component that actually keeps you solvent is the risk and liquidation engine. It's the least glamorous, most unforgiving code I've worked on. Here's what building one taught me, in the hope it saves you a few of the mistakes we made.
What the thing actually has to do
On every price tick and every position change, for every account, answer one question fast enough to act on it:
Does this account still have enough margin to hold its positions — and if not, what do we close, at what price, right now?
Get it too slow and the platform takes losses it can't recover (the trader is already underwater). Get it too aggressive and you liquidate people who didn't need to be liquidated, and they never come back. That tension — fast and correct and fair — is the whole job.
Lesson 1: Margin math is easy; doing it on every tick is not
The formula for whether an account is safe looks trivial:
equity = balance + unrealizedPnL
maintenance = Σ (positionNotional_i * maintenanceMarginRate_i)
healthy = equity >= maintenance
The hard part is that unrealizedPnL moves every time the mark price moves, for every open position, and a busy venue has a lot of both. You cannot recompute every account on every tick naively — you'll fall behind exactly during the volatility that makes liquidations necessary.
What worked for us:
- Index accounts by the price that threatens them. Precompute each position's liquidation price and keep positions in a structure keyed by it, so a price move only wakes the accounts that price actually endangers — not the whole book.
- Recompute on events, not on a timer. A tick, a fill, a deposit/withdrawal, a funding payment — those are the only things that change health. React to them; don't poll.
Lesson 2: The mark price is a security-critical input
Early on we liquidated on the last traded price. Bad idea. On a thin market, a single wick — or a deliberate one — can spike the last price and trigger a cascade of liquidations that shouldn't have happened. That's not a bug so much as an attack surface.
The fix is a mark price built to resist manipulation: an index across multiple sources, medianized, often blended with a fair-value/funding basis. Liquidations fire off the mark, not the raw last trade. If you take one thing from this post: never liquidate on a price a single actor can move.
Lesson 3: Liquidation is a state machine, not a delete
The naïve mental model is "account underwater → close position." Reality has stages, and each needs to be explicit and idempotent:
HEALTHY → AT_RISK → LIQUIDATING → (PARTIAL | FULL) → SETTLED
↘ INSURANCE_COVERED
↘ AUTO_DELEVERAGED
- Partial before full. Close only enough to bring the account back above maintenance where you can — full closure is a last resort and worse for the user.
- Idempotent transitions. A liquidation can be interrupted (a crash, a re-tick mid-close). Re-entering the flow must not double-close or double-charge. We key every action to a sequence number and make replay produce the same result.
- Where the loss goes when it goes bad. If a position can't be closed above bankruptcy price, the loss has to be absorbed — an insurance fund first, then auto-deleveraging counterparties as the backstop. You must decide this design before the volatile day, not during it.
Lesson 4: Determinism is what lets you sleep
This engine moves real money and will absolutely be audited — by regulators, by counterparties, by your own incident review. We made the whole thing deterministic: same ordered stream of ticks/fills/events in → identical liquidations out, on every replica. That bought us three things that turned out to be non-negotiable:
- Hot-standby failover — a replica rebuilt from the event log is in exactly the same state.
- Replay — we can re-run last Tuesday's crash and see precisely why each liquidation fired.
- Testability — see the next point.
No wall-clock reads, no RNG on the hot path. "Now" comes from the sequenced event, not System.currentTimeMillis().
Lesson 5: You cannot manually test this — simulate crashes
Unit tests won't give you confidence here. What did:
- Historical replay of real crash days (and synthetic worse-than-real ones), asserting invariants: no account left below maintenance, insurance fund never silently negative, conservation of value across every liquidation.
- Property-based tests — for any random-but-valid sequence of price moves and orders, the engine never liquidates a healthy account and never leaves an unhealthy one open.
- Chaos: kill the process mid-liquidation, restart from the log, assert identical end state.
If your test suite has never simulated a 40%-in-90-seconds move, your liquidation engine is untested for the only moment it exists to handle.
Would I build it again?
Honestly? For most teams, no — and I say that having built one. A risk/liquidation engine is safety-critical systems work with regulatory exposure and no room for "we'll fix it next sprint." Unless the risk model is your differentiator, it's usually the wrong place to spend your one shot at building something novel. We wrote up the higher-level version of that build-vs-buy reasoning — and how risk and liquidation engines fit the rest of an exchange — over here if it's useful: how risk and liquidation engines work.
But if you do build it: index by liquidation price, liquidate on a manipulation-resistant mark, model it as an idempotent state machine, make it deterministic, and test it against crashes that are worse than anything you think can happen. The market will eventually oblige.
I work on trading-platform infrastructure at GammaFloww. This is deliberately vendor-neutral — the lessons apply whether you build or buy.
Top comments (0)