The Mid Has a Queue Shadow
The displayed midprice is a clean number with a dirty omission.
bid 100.00 30 lots
ask 100.01 26 lots
mid 100.005
The mid says the quotes are one tick apart. It does not say that the bid queue is a little thicker than the ask queue. If the ask is more likely to disappear first, the next mid is more likely to tick up than down.
That missing state is the queue shadow.
From Mid To Microprice
For a one-tick spread, write the current mid as 0, the bid as -0.5 ticks,
and the ask as +0.5 ticks. If the ask queue depletes first, the next mid moves
up by half a tick. If the bid queue depletes first, the next mid moves down by
half a tick.
So a one-step expected mid is:
\[E[\Delta M] = \frac{1}{2}P(\text{ask depletes first}) - \frac{1}{2}P(\text{bid depletes first}).\]Equivalently:
\[E[\Delta M] = P(\text{uptick}) - \frac{1}{2}\]in ticks.
This is not yet Sasha Stoikov’s full micro-price construction, which defines a limit of expected mid-prices conditional on order-book information.1 It is the smallest useful version of the same idea: replace the mid with a conditional expectation that looks at the book, not only at the quote endpoints.
The Race Model
The lab below uses a deliberately small best-quote race.
- The ask queue loses one unit when a buy market order or ask cancellation removes displayed ask size.
- The bid queue loses one unit when a sell market order or bid cancellation removes displayed bid size.
- The first queue to hit zero determines the next mid-price direction.
Let p be the probability that the next unit-depletion event hits the ask side.
With ask depth A and bid depth B, the exact uptick probability is the dynamic
program:
with boundary conditions:
\[u(0,b)=1,\qquad u(a,0)=0.\]This is a pure-death cartoon of the richer queueing models studied by Cont, Stoikov, and Talreja, and by Cont and de Larrard.23 It ignores arrivals behind the best quotes, hidden liquidity, discrete trade sizes, queue priority, latency, and strategic reaction. That is a feature here: the point is to isolate how visible size and flow skew become an expected mid-price adjustment.
Bid depth 30, ask depth 26, flow skew +0%. Exact race microprice is +0.205 ticks; weighted-mid shortcut is +0.036 ticks.
The Default Run
For the default 30 x 26 best-quote race with symmetric depletion flow, the
audited run reports:
| Quantity | Value |
|---|---|
| exact uptick probability | 70.5% |
| Monte Carlo uptick rate | 70.4% |
| race-implied microprice | +0.205 ticks |
| weighted-mid shortcut | +0.036 ticks |
| mean events until quote move | 49.2 |
| sample-path quote move | ask depleted first after 48 events |
The weighted-mid shortcut is:
\[\frac{A_{\text{price}}B_{\text{size}} + B_{\text{price}}A_{\text{size}}} {A_{\text{size}} + B_{\text{size}}}.\]Inside a one-tick spread, relative to the mid, that becomes:
\[\frac{(+1/2)B + (-1/2)A}{A+B} = \frac{1}{2}\frac{B-A}{A+B}.\]In tick units around the mid, the lab uses 0.5 * (B - A) / (A + B).
That shortcut is monotone in imbalance, which is useful. But it is not the exact first-depletion probability of this race. The exact value knows how many same-side events must arrive before the other side finishes.
Why A Small Queue Advantage Can Be Large
With symmetric unit-depletion flow, the event sequence is a sequence of fair
coin flips labeled “ask hit” and “bid hit.” If the ask starts at 26 and the bid
starts at 30, an uptick happens when the sequence records 26 ask hits before
it records 30 bid hits.
That is easier than the raw size difference suggests. The ask does not need to be hit more often than the bid forever. It only needs to finish first.
This is why queue imbalance is not a linear ruler. Near the boundary, a small visible-depth advantage can correspond to a much larger first-passage probability.
Move Flow skew away from zero and the message changes again. A heavy bid queue is not bullish if the next depletion events are much more likely to hit the bid. Size and flow are separate state variables. A microprice model has to say how both enter.
Evidence Versus Interpretation
The evidence in this post is only about the toy model:
- exact dynamic programming and seeded Monte Carlo agree on the first-depletion probability;
- the next-mid expectation can differ sharply from the weighted-mid shortcut;
- queue imbalance matters because price moves are modeled as queue depletions.
The interpretation is broader but weaker: visible imbalance is a plausible short-horizon signal because the best quotes are queues, not decorative labels. The market-microstructure literature gives much richer versions of that idea. Cont and de Larrard’s Markovian model, for example, computes the probability of an upward price move conditional on the order-book state.3 Stoikov’s micro-price paper defines a fair price conditional on book information, expressible as an adjustment to the midprice using spread and imbalance.1
Real markets add everything the lab leaves out: replenishment, queue priority, large orders, hidden size, adverse selection, latency, correlated order flow, and trader reaction. Gould et al.’s survey is a useful antidote to over-trusting any single stylized limit-order-book model.4
To reproduce the deterministic audit sweep from the repository root:
node - <<'NODE'
const lab = require("./assets/js/microprice-queue-lab.js");
const EXPECTED_CASE_SHAPE = [
"1:30/26:pressure=0:trials=5000:seed=29:0.5000:0.704973:0.704200:0.0008:0.205:0.036:49.17:9/9",
"2:20/20:pressure=0:trials=5000:seed=11:0.5000:0.500000:0.496600:0.0034:0.000:0.000:34.86:9/9",
"3:15/55:pressure=35:trials=6000:seed=71:0.6682:0.013373:0.013167:0.0002:-0.487:-0.286:45.13:9/9",
"4:70/12:pressure=-40:trials=6000:seed=9:0.3100:0.999760:0.999333:0.0004:0.500:0.354:38.62:9/9",
"5:35/35:pressure=-65:trials=7000:seed=43:0.2142:0.000000:0.000000:0.0000:-0.500:0.000:44.53:9/9"
];
const EXPECTED_BY_CASE_SHAPE = [
"1:30/26:pressure=0:1/1:9/9",
"2:20/20:pressure=0:1/1:9/9",
"3:15/55:pressure=35:1/1:9/9",
"4:70/12:pressure=-40:1/1:9/9",
"5:35/35:pressure=-65:1/1:9/9"
];
const EXPECTED_CHECK_NAMES = [
"exact probability is bounded",
"microprice lies inside spread",
"weighted mid lies inside spread",
"monte carlo close to exact",
"sample path terminates at a quote depletion",
"balanced book has fair race",
"more bid depth tilts up under symmetric flow",
"buy pressure tilts up",
"curve has one row per alternate split"
];
const EXPECTED_CASES = EXPECTED_CASE_SHAPE.length;
const EXPECTED_CHECKS = EXPECTED_CASES * EXPECTED_CHECK_NAMES.length;
const EXPECTED_CHECK_ROWS = Array.from({ length: EXPECTED_CASES }, (_, index) =>
`${index + 1}:` + EXPECTED_CHECK_NAMES.map((name) => `${name}=true`).join("|")
);
function sameList(actual, expected) {
return actual.length === expected.length &&
actual.every((value, index) => value === expected[index]);
}
function caseShape(row) {
return `${row.caseId}:${row.bid}/${row.ask}:pressure=${row.pressure}:` +
`trials=${row.trials}:seed=${row.seed}:` +
`${row.askEventProbability.toFixed(4)}:${row.exactUp.toFixed(6)}:` +
`${row.simulatedUp.toFixed(6)}:${row.monteCarloError.toFixed(4)}:` +
`${row.raceMicroTicks.toFixed(3)}:${row.weightedTicks.toFixed(3)}:` +
`${row.meanEvents.toFixed(2)}:${row.passedChecks}/${row.totalChecks}`;
}
function byCaseShape(row) {
return `${row.caseId}:${row.bid}/${row.ask}:pressure=${row.pressure}:` +
`${row.passed}/${row.total}:${row.passedChecks}/${row.totalChecks}`;
}
function checkRow(row) {
return `${row.caseId}:` +
row.checks.map((check) => `${check.name}=${check.ok}`).join("|");
}
function rowDrifts(actual, expected) {
return {
missing: expected.filter((row) => !actual.includes(row)),
extra: actual.filter((row) => !expected.includes(row))
};
}
function hasRowDrift(drift) {
return drift.missing.length > 0 || drift.extra.length > 0;
}
const audit = lab.auditGrid();
const failed = audit.cases.filter(
(row) => !row.passed || row.passedChecks !== row.totalChecks
);
console.table(audit.byCase);
const shape = {
byCase: audit.byCase.map(byCaseShape),
cases: audit.cases.map(caseShape),
checkRows: audit.cases.map(checkRow)
};
const checkRowDrifts = rowDrifts(shape.checkRows, EXPECTED_CHECK_ROWS);
const shapeErrors = [];
if (!sameList(shape.cases, EXPECTED_CASE_SHAPE)) {
shapeErrors.push({ name: "cases", actual: shape.cases, expected: EXPECTED_CASE_SHAPE });
}
if (!sameList(shape.byCase, EXPECTED_BY_CASE_SHAPE)) {
shapeErrors.push({
name: "byCase",
actual: shape.byCase,
expected: EXPECTED_BY_CASE_SHAPE
});
}
audit.cases.forEach((row) => {
const names = row.checks.map((check) => check.name);
if (!sameList(names, EXPECTED_CHECK_NAMES)) {
shapeErrors.push({
name: `case ${row.caseId} check names`,
actual: names,
expected: EXPECTED_CHECK_NAMES
});
}
});
if (hasRowDrift(checkRowDrifts)) {
shapeErrors.push({ name: "checkRows", drift: checkRowDrifts });
}
console.table(audit.cases.map((row) => ({
case: row.caseId,
bid: row.bid,
ask: row.ask,
pressure: row.pressure,
exactUp: (100 * row.exactUp).toFixed(1) + "%",
simulatedUp: (100 * row.simulatedUp).toFixed(1) + "%",
mcError: row.monteCarloError.toFixed(4),
raceTicks: row.raceMicroTicks.toFixed(3),
weightedTicks: row.weightedTicks.toFixed(3),
checks: `${row.passedChecks}/${row.totalChecks}`,
passed: row.passed
})));
const summary =
`${audit.passed}/${audit.total} cases and ${audit.passedChecks}/${audit.totalChecks} checks passed`;
if (
shapeErrors.length ||
failed.length ||
!audit.ok ||
audit.total !== EXPECTED_CASES ||
audit.totalChecks !== EXPECTED_CHECKS ||
audit.passed !== audit.total ||
audit.passedChecks !== audit.totalChecks
) {
throw new Error(JSON.stringify({
summary,
failed,
shapeErrors,
shape,
checkRowDrifts,
passed: audit.passed,
total: audit.total,
passedChecks: audit.passedChecks,
totalChecks: audit.totalChecks
}, null, 2));
}
console.log(summary);
NODE
The current sweep covers five queue states and 45 named checks. It verifies bounded exact probabilities, spread-bounded microprices, Monte Carlo agreement with the dynamic program, sample-path quote depletion, balanced-book symmetry, depth monotonicity under symmetric flow, pressure tilt, and the one-row-per-split curve shape used in the visualization.
Boundaries
This lab is not a trading signal and not a claim about expected profit. It is a small accounting identity under a stated race model.
A production estimate would need at least:
- learned state-dependent depletion and refill intensities;
- multiple price levels, not only the best quotes;
- latency-aware queue position and cancellation modeling;
- out-of-sample markout tests after fees, spread, and impact;
- calibration checks by symbol, time of day, volatility, and regime.
The useful lesson is narrower:
midprice = quote geometry
microprice = quote geometry plus a model of the next state
The mid is centered between prices. The microprice is centered between possible futures.
Sources
-
Sasha Stoikov, “The Micro-Price: A High Frequency Estimator of Future Prices”, SSRN, 2017; revised 2020. ↩ ↩2
-
Rama Cont, Sasha Stoikov, and Rishi Talreja, “A Stochastic Model for Order Book Dynamics”, Operations Research 58(3), 549-563, 2010. DOI record: 10.1287/opre.1090.0780. ↩
-
Rama Cont and Adrien de Larrard, “Price Dynamics in a Markovian Limit Order Market”, arXiv:1104.4596; SIAM Journal on Financial Mathematics 4(1), 1-25, 2013. ↩ ↩2
-
Martin D. Gould, Mason A. Porter, Stacy Williams, Mark McDonald, Daniel J. Fenn, and Sam D. Howison, “Limit Order Books”, arXiv:1012.0349, 2010; revised 2013. ↩