---
name: project-mm-charge-more-ab
description: Live paper PRICE LADDER (0-5c above the market ask, per leg) measuring the fill curve; historical reconstruction cannot answer it because of queue priority
metadata:
  type: project
---

**Running experiment** in `main/mm_quoter.py` (paper, places nothing).

**Current design (widened 2026-07-28).** `arm_for(event, side)` = md5 of
`event|side` mod 6 -> a bump of **0/1/2/3/4/5 cents** above the market ask.
Deliberately md5, not Python's `hash()`, which is randomised per process and
would silently reassign everything on each restart.

- **Per LEG, not per book.** Every book now contains a spread of price levels, so
  book quality (liquidity, league, hours to kickoff) is controlled WITHIN book
  instead of confounded with arm. That confound made the first 2-arm read look
  decisive when all four unfilled legs happened to sit in the two youngest books.
  It also triples the independent observations.
- **Why a ladder:** the question is no longer "does +1c fill" but "what price
  maximises EV", which needs a fill CURVE, `EV(p) = P(fill|p) x (p - fair)`. Two
  arms give a slope; six give a shape. Cannot be estimated from history — we have
  only ever rested at the touch, so any model extrapolates into unposted prices.

**CRITICAL for analysis: use the REALISED delta `our_ask - k_ask`, never the
`arm` label.** Our ask is `min(fair + margin, market_ask + bump)`, so when
fair+margin binds first the realised bump is smaller. Observed on the very first
cycle: an arm-3.0 leg quoted +2c, and an arm-2.0 leg quoted **+0c**. Using the
label would overstate the prices actually tested and flatten the curve.

**Pre-2026-07-28 rows** carry `arm` values `cap`/`bump` (= 0c/1c) and were
assigned per BOOK. Prefer analysing from 2026-07-28 forward rather than pooling.

**Earlier 2-arm read (per book, ~1 day):** cap 9/9 legs filled, bump 1/6.
Suggestive that the curve is near-vertical at the touch, but confounded by book
age as above — do not treat as the answer.

**Rate:** ~15 legs/day across 6 arms is ~2.5 legs/arm/day. A week gives ~17 per
arm: enough to see the shape of a steep curve, not to get precision on a flat
one. Expect the top of the ladder to fill rarely — that is still an answer
("quote at the touch, there is no optimum").

**Why this had to be measured live rather than replayed:** three historical
reconstructions all failed, see [[project-mm-gross-margin-gate]] for the data
filters. The final causal version (`research/mm_charge_more_causal.py`, fills
strictly after the posting price is fixed) still predicted **97.5%** of books
fill all three legs naturally at +0c — while live on 2026-07-27 we saw **0 of
7**. The gap is QUEUE PRIORITY: a replay fills you whenever any trade prints at
your price, but a real resting order sits behind the size already there. No tape
reconstruction can see queue position; only our own orders can.

**Bug found while wiring it (fixed, worth remembering as a class):**
`math.ceil(aa * 100)` turned a 28c cap into a 29c ask because `0.28 * 100` is
`28.000000000000004`. Affects cent prices **7, 14, 28, 55, 56**. The quoter had
therefore been silently posting ABOVE the market on those prices since the cap
was written — the opposite of the cap's purpose, and a plausible contributor to
the poor natural fill rate. Any cents-from-float conversion in this project
needs `round(x*100, 6)` before `ceil`/`floor`.

See also [[project-mm-logging-fields]] for the queue-depth / market-age / local-
hour features added the same day to predict which legs sell.
