All research
6 min readExecutionMicrostructure

Regime exposure as a session-diagnostic primitive

A simple bucketing pattern that turns “the session lost money” into “the session spent 33% of its minutes in HOLD_ALL and 14% in BOTH.”

A single PnL number tells you almost nothing about what a market-making session did. Two multi-hour sessions with similar fill counts, similar inventory paths, and the same nominal strategy can post wildly different PnL — they differ in which regimes the session spent its time in and which of those regimes were profitable. This is a short note on the simplest tool we use to recover that information after the fact.

The pattern

Every minute (or every quote-decision tick) of the session gets tagged along a small fixed set of axes. We use six in practice:

  • Basis bucket — discretised(perp_mid − index) / index. Buckets: FLAT, POS, NEG, POS_STRETCHED, NEG_STRETCHED.
  • Inventory bucket — current net position as a fraction of capacity. Buckets: FLAT, LOW, MID, HIGH, STRETCHED.
  • Mode bucket — what the quote engine actually did. Buckets: BOTH (both sides up), BID_ONLY, ASK_ONLY, HOLD_ALL (no quotes).
  • Spread bucket — the live book's quoted spread measured in ticks. Buckets: 1tick, 2tick,wide.
  • Volatility bucket — quintiles of a short-window mid-return realised vol. Buckets: Q1_LOW through Q5_HIGH.
  • Toxicity bucket — quintiles of a flow-toxicity proxy (signed taker imbalance, VPIN-ish). Buckets: Q1_LOW through Q5_HIGH.

The cardinality of the full cross-product is large, and most cells are empty in any single session. The useful artefact is the per-axis marginal: for each bucket on each axis, how many minutes were spent there, what fraction of the session that represents, and — if you join against the fill table — what the per-fill markout and net PnL looked like inside that bucket.

What it looks like in practice

From a recent multi-hour OKX SUI-USDT-SWAP session (the one referenced in the markout-vs-rebate note), the by-axis marginals looked roughly like this. Per-axis percentages sum to 100% of session minutes; numbers below are rounded.

AxisBucket% of session
ModeHOLD_ALL~33%
BID_ONLY~30%
ASK_ONLY~23%
BOTH~14%
BasisFLAT~58%
NEG~23%
POS~19%
InventoryLOW~81%
FLAT~19%
ToxicityQ5_HIGH~57%
Q3~43%
Spread1 tick~100%

The picture this paints in a single glance:

  • BOTH ~14%, HOLD_ALL ~33%. The strategy was up on both sides for one minute in seven. The plurality of session time was spent not quoting. This is a useful aggregate signature — it tells you the gates were doing most of the work.
  • Toxicity Q5_HIGH ~57%. The majority of the session ran in the top-quintile toxicity regime. That isn't a calibration problem on its own — the venue's flow looked this way — but it pairs naturally with the markout result. The rebate doesn't cover the markout because the markout regime dominated.
  • Inventory LOW ~81%. The strategy was holding inventory most of the time, not running flat. Whether that is good or bad depends on whether the basis it was holding agreed with the inventory direction — which is exactly what a joined (basis × inventory) cell would tell you.
  • Spread 1-tick ~100%. The microstructure was exactly what you would expect on a major OKX perp. No spread capture available; whatever the strategy earned had to come from selection or skew alpha.

Why marginals first, joints second

The full six-axis cube is overwhelming and the per-cell sample sizes collapse fast: a multi-hour session's worth of minute-tags spread over a few thousand non-empty cells gives single-digit counts in most of them. Marginals are robust because they aggregate over everything else; per-axis you have the whole session's minutes to spend.

Joints become useful at exactly two scales. The first is two-axis cells with strong economic meaning — basis × inventory (am I holding the side the venue would pay me for?), mode × toxicity (am I quoting both sides during the toxic minutes?), mode × basis (am I structurally leaning the wrong way?). The second is when you have enough sessions stitched together that the cube has hundreds of minutes per cell, at which point per-cell PnL becomes meaningful and you can start to identify regimes the strategy genuinely earns in.

Joining against the fill table

The minute-tag view is the time view. The fill view is the event view. Joining them — assigning each fill the bucket the bot was in at the moment of fill — turns the marginals into something stronger: per-bucket fill counts, per-bucket markout, per-bucket gross and net PnL. From there:

  • “Where did we make money?” Sum net per bucket, sort.
  • “Where did we lose money?” Same, descending.
  • “Where did we trade most?” Count fills per bucket. Often the answer surprises — high-fill buckets aren't always the high-PnL buckets.
  • “Where do we have enough data to draw any conclusion?” Same count, plus a hard floor (we use ~50 fills as a minimum for anything to be more than directional; see the sample-size note).

What this is not

Regime exposure is descriptive, not predictive. It tells you where you spent your minutes; it does not tell you where you should have spent them. The translation from “HOLD_ALL was 33%” to “therefore the HOLD_ALL gates are too aggressive” is a modelling step — the gates may be precisely as aggressive as they should be, given that toxicity was Q5_HIGH for 57% of the window. Exposure numbers raise questions; they do not answer them.

The discipline is to look at the exposure pattern first, before any per-fill analysis, so the per-fill numbers are interpreted in the right context. A barely-down session that spent 14% in BOTH is a different story than a barely-down session that spent 80% in BOTH; the diagnoses point to different things even when the headline number is identical.