Derivatives risk¶
A spot venue only moves assets that exist. A derivatives venue extends credit, so it needs margin, a defensible mark, a liquidation path, and a backstop for when liquidation is not enough.
Isolated margin (in the engine)¶
Set SymbolConfig::linearPerp and the engine clears a linear perpetual:
- Initial margin on entry.
notional * initialMarginBpsis reserved in quote collateral; insufficient margin rejects the order. An order with no price of its own (market, triggered stop) is margined at the top of the price band on both sides -- a perp delivers nothing, so its notional grows with price whether the account ends long or short. - Netting positions with an average entry price.
- Mark-to-market PnL realised into collateral through the venue clearing pool: a long's profit is funded by a short's loss, so value is conserved.
- Reduce-only orders are capped to the opposing position on submit, on stop
trigger and on modify -- and re-measured at fill time, against the position
as it is then. A resting order is sized against the position it saw when it
was admitted; by the time it trades that position may be smaller or gone, so
only the part that still reduces executes and the rest is canceled
(
ReduceOnlyNotReducing). That is what makes "never opens or flips" true of the fill and not just of the submit -- and it matters because a reduce-only order reserves no margin, so any part of it that opened a position would open one with none. maxPositionQtyis enforced against the resulting position at fill time, not only against the incoming order: orders that each pass the cap individually cannot settle into a position past it together. The bite that would breach it is cut, and a maker with no room left is pulled (PositionLimitExceeded).- Maintenance sweep.
setMarkPricewalks every position; anything whose equity (posted margin + unrealised PnL, plus a negative wallet if the account is overdrawn) falls under the maintenance requirement is force-closed.
cfg.linearPerp = true;
cfg.initialMarginBps = 1000; // 10x
cfg.maintenanceMarginBps = 500;
cfg.autoDeleverage = true;
venue.submit(InboundCommand{SetMark{SYM, Price::fromDouble(49'500)}}, tsNs);
SetMark and ApplyFunding are commands in the sequenced stream, so a replay
reproduces every mark-driven liquidation and funding payment exactly.
Liquidation waterfall¶
- Cancel the account's resting orders. Their initial margin is the account's own collateral; freeing it first means a total-equity-solvent account covers its own shortfall before the insurance fund pays anything.
- Force-close at the mark through the clearing pool.
- Insurance fund (the venue account) absorbs a negative-equity deficit.
- ADL claws the remainder from the most profitable opposite-side
positions: each winner is closed at the bankruptcy price, giving up exactly
the gain that absorbs the deficit and keeping the excess. Victim selection
uses a total order (
uPnl desc, then account, then symbol), so it does not depend on hash iteration or an unstable sort and a replica picks the same victim.
Portfolio (cross) margin¶
CrossMarginManager runs margin across an account's whole book, so a profit
on one symbol offsets a loss on another.
CrossMarginManager cm(ledger, /*collateral asset*/ USD, VENUE, onLiquidation, /*adl*/ true);
cm.configureSymbol(BTC, /*imBps*/ 1000, /*mmBps*/ 500);
cm.setMark(BTC, Price::fromDouble(50'000));
cm.canOpen(acct, BTC, Side::BUY, qtyRaw, priceRaw); // pre-trade gate
cm.applyFill(acct, BTC, Side::BUY, qtyRaw, priceRaw);
cm.equity(acct); // collateral basket + unrealised PnL
cm.withdrawable(acct); // never lets collateral behind open positions leave
cm.openInterestRaw(); // venue-wide risk gauge
setLiquidationsPaused(true) is the operator switch for an untrustworthy
price feed; see the circuit breaker below.
Handing risk to an owner outside the engine¶
Isolated margin is the engine's own business: it sees one instrument and the collateral behind it, so it can decide alone. Portfolio margin cannot work that way -- a hedge only nets across instruments, and the engines are per symbol. So the decision moves out and the engine executes it, over three seams.
Permission on entry. setCreditCheck is asked before an order is funded,
whether or not this engine posts collateral of its own. The request names the
instrument (the owner serves many), the order type and whether it reduces
exposure; the answer carries a reject reason, so a client refused by portfolio
margin is told that, not a flat funds error.
Fills. Every outbound event carries its owner account, so the risk layer rebuilds each account's basket by subscribing to the same stream everyone else reads. Nothing extra is needed.
Closing. ForceClosePosition closes a position on the owner's decision,
settling through exactly the path the engine's own sweep uses, so the events
and the replay are identical either way. Pair it with
SymbolConfig::externalLiquidation, which stops the engine liquidating on its
own: two systems closing the same position from different numbers is worse than
either doing it alone.
What stays outside: how the requirement is computed, which positions net, what haircuts collateral takes, when to close and in what order. Those differ per venue and are the owner's to define.
Multi-asset collateral¶
CollateralSchedule values a basket with per-asset haircuts. An unconfigured
asset is valued at zero, so a forgotten entry under-counts collateral instead
of extending credit against it. During liquidation the venue buys the
non-quote collateral and the insurance fund only covers the remainder; every
conversion is balanced per asset.
Mark and index price¶
A mark you can push is a mark someone will push. index_feed.h builds one
that resists that:
IndexAggregator idx(/*stalenessNs*/ 5'000'000'000, /*maxDeviationBps*/ 500, /*minSources*/ 3);
idx.update(sourceId, price, tsNs); // several external spot venues
idx.hasIndex(nowNs); // enough fresh sources?
idx.index(nowNs); // median, stale sources and outliers dropped
The mark is the median of {index, last trade, impact-mid}, clamped into a
band around the index. Impact-mid is a VWAP walked into the live book to a
notional depth, so dust or a spoofed top level cannot move the mark.
Feed circuit breaker¶
MarkFeedDriver watches the feed and pauses liquidations when it goes stale:
MarkFeedDriver driver(...);
const bool publish = driver.onTick(nowNs, lastTradeRaw, impactMidRaw);
driver.paused(); // liquidations halted while the index is not fresh
driver.markAgeNs(nowNs); // export as a health gauge
Liquidating traders on a frozen price is how venues turn an outage into an incident. The breaker makes that failure mode explicit and observable.
Funding¶
Two sources behind one settlement path:
FundingSchedule(flox/backtest/) replays a recorded rate tape.FundingCalculator(flox-venue/funding_rate.h) computes the rate live: premium (mark vs index) plus a clamped interest component, capped.
The live path is TWAP by default: sample the premium across the interval and settle on the average, so a momentary dislocation cannot swing the payment.
FundingCalculator f;
f.sample(mark, index); // repeatedly across the interval
const double rate = f.intervalRate();
f.resetSamples();
FundingScheduler closes the loop (sample, settle at the boundary, reset).
Settlement is zero-sum: longs pay shorts or the reverse, and the charges
across a balanced book sum to zero.