mmsell: entry-timing study, on the right clock per settle mode - #167
Merged
50thycal merged 3 commits intoAug 6, 2026
Merged
Conversation
Answers "does WHEN we enter matter" on our own book — 10,337 in-play trades across 3,783 distinct markets, vs the 1,137 h2h entries the original U-shape backtest was built on. THE FINDING THAT SHAPES THE DESIGN: hours_to_close is a valid clock for scheduled/discrete markets and a fiction for in-play ones. Kalshi sets a sports market's close_time to a far-future fallback, so KXUFCFIGHT reads 335h-to-close on a fight resolving in 0.4h — pinned at our own htcmax=336 cap. Measured across the book, in_play averages 145.9h reported vs 2.0h actual; scheduled and discrete are accurate to within 2 and 7.5 hours respectively. Bucketing in-play trades by hours_to_close files every one under "24-72h" and measures nothing while looking perfectly healthy, so the study scores in_play on realized time-to-resolution and prints the clock validation first on every run as a guardrail against switching it back. Two consequences beyond the bucketing: the global htcmin=1.0 floor never binds on sports at all (so the books already enter deep in-play with no rule saying so), and a LIVE in-play timing gate is impossible until the worker persists a forward-looking field. Result: the endgame half of the U-shape replicates strongly — <0.25h +8.85c (n=588), 0.25-0.5h +6.72c (n=1318), decaying to -1.55c in the 1-2h cell, which is also the largest. It was the half predicted most likely to be a mirage. h2h is the sharpest read: +8.97c in the final 15 minutes against -13.34c beyond 12 hours. The market-type census scored h2h at +0.63c pooled and called it the weakest large in-play type; that number was averaging those two ends. h2h is not a bad type, it is a badly-timed one. total and spread behave the same way; player_prop and outright do NOT (they peak at 0.5-1h and 2-4h/12h+), so the per-type cut is what keeps the pooled shape from being over-generalized. Three caveats documented and gated on, the first being the serious one: hold time is entry->resolution, so a game that runs long lands in a later bucket, and games run long when they are close — some share of the gradient is "blowouts end on schedule" rather than "late entry is safe". Plus settlement-detection lag, and the standing fill-everything caveat (the endgame is exactly where a resting maker is picked off, and both prior timing signals died there). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECFY4eoPk2g1JUanmFqkee
…ing capture
TIMING VERDICT: the entry-timing edge does not survive the maker-fill haircut.
The study now projects each window's own entry-price mix through the live
calibration and prints a REALIZABLE column beside the paper one:
window paper realizable cov
<0.25h +8.85c +0.50c 63%
0.25-0.5h +6.72c +0.46c 50%
1-2h -1.55c +0.55c 48%
2-4h +1.77c -0.23c 55%
4-12h -4.28c -0.95c 43%
A 13.1c paper spread collapses to 1.45c realizable, and the ordering inverts
where it matters: the 1-2h window that looked worst on paper is the best
realizable cell, while the <0.25h endgame that looked best is indistinguishable
from it. The endgame edge is composed almost entirely of fills a resting maker
never gets — the outcome predicted before the test, and the third timing signal
to die at this step after mmsell7 (htcmax=24) and mmsell11 (htcmin=6). Scheduled
tells the same story: its 72h+ cell, the largest paper edge in the study at
+10.50c, reads -0.87c realizable. No .timeX book should be built on this.
CAPTURE (forward-only, cannot be backfilled — Kalshi drops settled markets after
~70 days), on the row the entry scan already writes:
* hours_to_expiration — the only FORWARD-LOOKING resolution clock. hours_to_close
comes from close_time, which on sports is a far-future fallback (KXUFCFIGHT
reports 335h on a fight resolving in 0.4h), so it cannot express "enter in the
final 30 minutes". The study can score history on realized hold; a live gate
cannot. Recorded, never gated on.
* strike_type / floor_strike / cap_strike / yes_sub_title — the contract's LINE.
The taxonomy says a ticker is a spread or a total; these say WHICH ONE. Probed
via ticker regex, MLB totals run a 9.7% loss rate at the 10-11 run line and
22-26% at 15-16 — a wider spread than most type-level differences. The regex is
not durable (encoding is series-specific: WNBA totals carry raw points, an
exact-score suffix yields a digit that is not a line at all), so the fields are
now captured as Kalshi states them.
Strike parsing stores NULL rather than 0.0 for unparseable values, since a 0 line
is real and the two must stay distinguishable; subtitle/strike_type are clamped to
their column widths so an over-long payload can never abort a cycle's capture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECFY4eoPk2g1JUanmFqkee
The endgame taker result (+4.06c at <15min, +6.71c at 15-30min) rests on
`taker = paper - spread`, which silently assumes liquidity at the touch is
unlimited. It is a per-CONTRACT number, so a window can look excellent at 1
contract and be untradeable at 20 — and the endgame is exactly where books are
thinnest. Depth is the one input that turns that into a measurable ceiling, and
we were not storing it.
Captures both sides on the candidate tick, because they mean different things to
a book that sells the YES tail by buying NO:
depth_at_best_bid contracts resting at the YES bid — what a TAKER entry lifts
depth_at_best_ask the YES-ask queue — what a MAKER entry sits behind
Surfaced in the timing study as a `takerQ` column rendering median(coverage%), so
a window's capacity is read next to its edge rather than assumed. Capture is
forward-only (Kalshi drops settled markets after ~70 days), so historical windows
read n/a by design.
The study now PROBES for the column via information_schema instead of hard-
depending on it: the ops channel runs this against whatever is deployed, which
may be either side of this migration, and a crash on a missing diagnostic column
is worse than reporting it as uncovered — which is what its absence means. Caught
by running it against production before shipping.
Columns are added to the existing (unmerged, unapplied) migration rather than
chained behind it, so this stays one deploy step.
Also documents, in docs/MMSELL_TIMING_STUDY.md, what the Kalshi API actually
allows — checked against the docs rather than inferred from this repo:
* there is NO market order type; a "market order" is a marketable limit with
time_in_force=immediate_or_cancel
* post_only cannot be combined with IOC, and self_trade_prevention_type is
required — both already learned live and annotated in live/executor.py
* the taker path already exists and is proven (the closeout order is a recorded
status-201 taker-IOC request); a taker ENTRY is that payload with side="ask"
* maker fee is ceil(0.0175*C*P*(1-P)) vs taker ceil(0.07*...), a 4x discount
that per-trade rounding to a cent ERASES at 1-contract clips in the cheap band
(both charge 1c at yes<=11c). So taking costs the spread and nothing else, and
`taker = paper - spread` needs no fee correction.
That last point contradicts docs/MMSELL_ROADMAP.md's claim that paper overcharges
makers ~1c/contract; recorded as unresolved, since it moves maker realizable by a
full cent outside the endgame.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECFY4eoPk2g1JUanmFqkee
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it answers
Does WHEN we enter matter? Measured on our own book: 10,337 in-play trades across 3,783 distinct markets, vs the 1,137 h2h entries the original U-shape backtest was built on.
The finding that shapes the whole design
hours_to_close— the obvious timing variable — is valid for scheduled/discrete markets and a fiction for in-play ones. Kalshi sets a sports market'sclose_timeto a far-future fallback, not the end of the contest:in_playscheduleddiscretePer series it's worse —
KXUFCFIGHTreads 335h-to-close on a fight that resolves in 0.4h, pinned at our ownhtcmax=336cap. Bucketing in-play trades byhours_to_closefiles every one under "24–72h" and measures nothing while looking perfectly healthy. So the study uses a different clock per mode and prints the validation table first on every run, as a guardrail against anyone switching it back.Two consequences beyond the bucketing:
mmsell_min_hours_to_close = 1.0floor never binds on sports at all — the books already enter deep in-play (0.4–1.5h of contest left) with no rule saying so.expected_expiration_time).Result — the endgame half of the U-shape replicates
It was the half predicted most likely to be a mirage, and the largest single cell (1–2h, n=3,488) is the one losing money.
h2h is the sharpest read: +8.97¢ in the final 15 minutes against −13.34¢ beyond 12 hours.
docs/MMSELL_MARKET_TYPES.mdscored h2h at +0.63¢ pooled and called it the weakest large in-play type — that number was averaging those two ends. h2h is not a bad type, it's a badly-timed one.totalandspreadbehave the same way.player_propandoutrightdo not (they peak at 0.5–1h and 2–4h/12h+), which is why the per-type cut exists — the pooled shape does not generalize.Caveats, documented and gated on
mmsell7htcmax=24was its cohort's worst;mmsell11htcmin=6went +2.38¢ paper → −0.86¢ realizable). Nothing here is promotable until it clearsmmsell fill model.Pre-registered gates for a
.timeXbook are indocs/MMSELL_TIMING_STUDY.md: edge ≥ +3.0pp above the adjacent window at n ≥ 300 and ≥ 150 distinct markets, holding within a single type, plus positive realizable ¢/trade.Notes
Taxonomy and per-cell statistics are imported from
scripts/mmsell_market_types.pyrather than re-declared, so a type cannot mean one thing in the census and another here. Read-only, self-contained; the only non-script edit is theALLOWED_SCRIPTSentry.Testing
tests/test_mmsell_timing_study.py— 7 tests pinning the per-mode clock mapping (the finding itself), lower-bound-inclusive bucketing, grid well-formedness, that a missing or impossible clock value drops out as coverage loss rather than flooring into the shortest bucket (where it would manufacture exactly the late-entry edge under test), and that the taxonomy is imported rather than copied. Full suite green. Script has been run end-to-end against the live DB via the ops channel.🤖 Generated with Claude Code
https://claude.ai/code/session_01ECFY4eoPk2g1JUanmFqkee
Generated by Claude Code