Quote on one venue, hedge on another (xmm)¶
xmm is a built-in strategy: it quotes one instrument as a maker and hedges every fill with a taker IOC on a second instrument, usually the same underlying on another venue, optionally falling back to a third. The hedging is a HedgeExecutor. Source: include/fastmm/strategies/xmm.hpp.
Run it¶
Two configs ship:
| Config | Quotes | Hedges |
|---|---|---|
configs/xmm-binance-demo.toml |
Binance Spot Demo Mode, BTCUSDT | Binance USDⓈ-M Demo Trading, BTCUSDT perpetual |
configs/xmm-demo.toml |
Binance USDⓈ-M Demo Trading, BTCUSDT perpetual | Bybit testnet, BTCUSDT linear perpetual |
- Build:
cmake --preset release && cmake --build --preset release -j. -
Check the market data of both venues with a dry run. It needs no keys and sends no order:
Each venue prints a status line every 10 s; both should show
md=liveandbooks=1/1(the order channels stay down in a dry run): -
Put the keys in the environment. Binance Demo Trading keys cover both Binance venues; enable Spot and Futures on the key, put USDT in the USDⓈ-M wallet, set it to one-way position mode, and hold BTC and USDT in the spot wallet (Run on a testnet or Binance Demo):
-
Run it for a fixed time and watch it from a second terminal:
-
Read the quote fills and the hedges from the store:
./build/release/bin/fastmm-pnl fills --engine xmm-binance-demo(Query what you traded).
Behind fastmm-gateway, xmm shares the account with other strategies: start the gateway with both venues, then fastmm-live --config configs/xmm-binance-demo.toml --gateway runs/<gateway name>.gw.
On Binance Demo¶
configs/xmm-binance-demo.toml ran for 2 h 28 min behind fastmm-gateway on 2026-09-29, beside a basic_mm on USDⓈ-M ETHUSDT. A fault came every 20 minutes: kill -9 of xmm right after a spot fill, a 20 s SIGSTOP of the gateway, kill -9 of the gateway with 90 s down, and kill -9 of the other strategy.
| Spot orders / trades | 3011 / 33 |
| Hedge IOCs sent / filled | 25 / 25 |
| Largest unhedged position | 0.00209 BTC, for about 75 ms |
| Unhedged at the end | 0.0000453 BTC |
A script checked the stores against the venue's own order, trade and position records: every trade booked once, no client order id used twice, no order open at the end, positions and balances equal to the venue's. Every hedge left in the same engine step as the fill that caused it, except one that waited 74 ms for the hedge before it to end. The xmm process killed right after a spot fill had sent its hedge 76 ms before; the one that replaced it booked the fill and sent nothing.
Set it up¶
- Configure both venues under
[venues.*]and both instruments under[[instruments]]. One session trades both (threading = "split", the default). - Name the instruments by their position in
[[instruments]](from 0):quote_instrument,hedge_instrumentand, for a second hedge venue,fallback_instrument(-1: none). They are read at start. - Set each venue's fees under
[venues.<name>.fees](or per instrument in[[instruments]]). The quotes price in the quote venue's maker fee (negative for a rebate) and the hedge venue's taker fee. - Keep
[engine] ack_timeout_ms = 0, or well above the venues' round trip: an order the ack timeout cancels is an unknown outcome and holds hedging foruncertain_hold_ms.
Both instruments must be linear (spot, linear perpetuals or futures). Contract sizes may differ: positions are compared in base units, qty * contract_multiplier, so Binance USDⓈ-M (BTC) can be hedged with OKX swaps (0.01 BTC contracts). An inverse instrument leaves the strategy idle with an error in the log. Parameters: fastmm-live --list-strategies.
Pricing¶
ref = hedge mid (use_microprice: the touch microprice)
basis = EWMA of (quote mid - ref), half-life basis_halflife_s; 0 turns it off
mark_basis: (quote mark - quote index) - (hedge mark - hedge index)
carry = ref * (hedge funding - quote funding) over funding_horizon_s; 0 turns it off
fair = ref + basis + carry
half = fair * (edge_bps + quote maker fee + hedge taker fee + slippage_bps)
bid = fair - half, rounded down; ask = fair + half, rounded up
size = one level of quote_qty, base units
Quotes never cross the quote venue's touch. A fair value move under requote_threshold_ticks does not requote.
Perpetual legs¶
The mark, index and funding come from the venues (ctx.mark, ctx.funding); a leg that is not a perpetual counts 0 in both terms.
carry: a bid that fills is hedged by a sell, so the pair holds a short hedge and a long quote. Over the expected holding timefunding_horizon_sthe short receives the hedge's funding and the long pays the quote's, eachctx.funding(id).over(horizon)(the rate per interval scaled to the horizon; positive: longs pay). An ask that fills holds the opposite pair and pays the same amount, so both sides shift by the samecarry. Example: spot quoted on Binance, hedged on a perpetual paying 0.01 % per 8 h, horizon 1 h: fair moves up by 0.00125 % of the hedge mid.mark_basis: the venues' own premia replace the EWMA of the mids, on the assumption that both indices price the same underlying. It follows the funding-driven premium without the EWMA's lag and ignores the noise of two books.
A mark, index or funding rate a term needs that is stale ([accounting] stale_mark_ms, stale_funding_ms) or has not arrived pulls the quotes, as a stale book does. Every PerpState of either instrument requotes while one of the two is on.
Hedging¶
unhedged = quote position + hedge position + fallback position, in base units.- When
|unhedged|rounds down to at least one lot of the hedge instrument (and itsmin_qtyandmin_notional), one IOC limit goes out,hedge_tolerance_bpsthrough the touch (fallback_tolerance_bpson the fallback). A smaller remainder waits for the next fill: a partial maker fill under the hedge venue's minimum notional (Binance USDⓈ-M: 50 USDT) stays unhedged, withinmax_unhedged. - While any order is open on a hedge instrument, including one sent but not acknowledged, no other hedge is sent. When it ends, the positions are read again: a partial fill is followed by a hedge for the rest.
- Hedges follow positions, not fill counts. A restart, a replayed or duplicated execution and a fill booked late by reconciliation all lead to the same hedge.
- No hedge goes out while a venue reconciles (
ctx.reconciling()), nor after a start until every venue has replayed its executions and reconciled, so a quote fill replayed before the hedge that covered it is not hedged twice.tests/integration/xmm_restart_test.cppkillsfastmm-live(and a strategy behindfastmm-gateway) with the hedge out and unanswered, and with a quote filled while it is down. - A hedge reported ended before its executions arrive (Bybit's
orderandexecutiontopics are not ordered) is booked from the venue's cumulative quantity when the end arrives; the executions then correct its price and fee without adding quantity.
The rules are the executor's (What it sends).
Fallback and de-risk¶
With fallback_instrument set, the hedge goes to the fallback while the hedge instrument's venue has an order channel down or its kill switch on, its book is invalid or older than stale_ms, the feed-lag gate holds it, its balance or margin cannot cover the hedge, or it failed max_hedge_failures times within failure_window_ms (it then sits out failover_bench_ms). The next hedge goes back to the hedge instrument as soon as it can take it (Failover). The fair value stays on the hedge instrument's book, so its market data has to stay fresh for the quotes to stay up. tests/integration/xmm_failover_test.cpp runs a hedge venue that refuses every order, kills fastmm-live while the fallback holds the hedge's reply, and restarts it.
With derisk_after_ms set, a residual that no hedge instrument has taken for that long is reduced on the quote instrument: reduce-only IOC orders of at most derisk_step_qty, derisk_tolerance_bps through the quote venue's touch, derisk_interval_ms apart (De-risk). The quotes are off meanwhile.
Guards¶
| Condition | Effect |
|---|---|
Either book invalid or older than stale_ms |
quotes pulled |
mark_basis or funding_horizon_s on, and a mark, index or funding rate it needs stale or missing |
quotes pulled |
| No hedge instrument can take a hedge: each has an order channel down, its venue killed, its book invalid or stale, its venue gated, or is benched | quotes pulled, no hedges |
Hedge instrument's venue held by the feed-lag gate ([risk] max_feed_lag_ms) |
quotes pulled |
A fill would take the unhedged position past max_unhedged |
that side not quoted |
The quote venue's balance cannot cover a fill of a side (ctx.balance_room: a spot bid's quote asset, an ask's base, a derivative's margin) |
that side not quoted |
| No hedge venue's balance or margin can cover the hedge | hedge held, logged and counted once per episode (Stats::hedges_held), no failure; only the side that reduces \|unhedged\| quoted, as at max_unhedged; the next balance report or fill looks again |
| A hedge fills nothing (expired, rejected, refused by risk) | next hedge after hedge_retry_ms |
max_hedge_failures such hedges within failure_window_ms on the last hedge instrument not sitting out |
halted: quotes pulled, no hedges, error logged |
| A hedge ends without the venue saying how (ack timeout, a reconciliation with quantity unaccounted for, a generic venue reject such as a REST timeout) | next hedge after uncertain_hold_ms |
To clear a halt, give restart a new value (fastmm-ctl param restart=1, then 2, ...); hedging restarts from the positions. Pausing and resuming quoting does not clear it, because reconciliations do that on their own.
[risk] max_position applies per instrument, and max_unhedged bounds the net of the legs. For the net across strategies and venues, set [risk.underlying] or, behind a gateway, [gateway.underlying.<BASE>] max_net.
Test it offline¶
A backtest can hold both venues, each with its own latency, replace and self-trade settings ([backtest.venues.<name>]), fed by one recorded feed per venue (Backtesting). tests/integration/xmm_live_test.cpp runs xmm live against two simulated venues.