resolution_anchor
resolution_anchor lets a bot enter only when markets like this one have historically settled its way often enough. Same rough category, same price decile, same distance from closing — go and look at what actually happened to those markets before, and require a hit rate. It is the only signal in our library built from data we generated ourselves rather than fetched from anyone: every Kalshi market we have watched quote and then settle. It is also the signal most likely to tell you something the price already told you, and most of this page is about why that matters.
- Bots using it
- 3 (1 live, 2 out-of-sample)
- Data source
- Our own resolved market history
- Refresh
- Daily, 5:50 AM UTC
- Markets behind it
- 11,551 settled, with usable quotes
- Backtest-replayable
- No — live engine only
- Anchor module
- quants/resolution_anchor.py
Where the data comes from
There is no vendor here, no API key, and nothing to rate-limit. Every number this signal uses was produced by
our own polling of Kalshi over the past several weeks, sitting in two tables of the same SQLite file
(predictive.db) that feeds the rest of the library.
The raw material — two tables we already had
kalshi_marketssupplies the answers: every market whoseresultisyesorno. That is 25,589 settled markets as of this writing.market_price_snapshotssupplies the questions: 37,531,471 rows of bid, ask, volume and timestamp. This table is what our minute-by-minute Kalshi poller has been writing all along — the same feed arb_anchor reads live. Its earliest row is 2026-06-22, and that date matters more than any other number on this page.
The builder — predictive/resolution_buckets.py, nightly
A cron job on the production box, 50 5 * * * — once a day at 5:50 AM UTC. The buckets move
slowly, so daily is plenty. For every settled market it pulls that market's price snapshots and then does the
careful part:
- One observation per time band per market. Five bands (under 6 hours to close, 6–24, 24–72, 72–168, beyond a week), and for each one it keeps the single snapshot nearest that band's midpoint. A market we happened to poll ten thousand times does not get ten thousand votes.
- Liquid, two-sided snapshots only. A snapshot with no bid, or no 24-hour volume, is thrown away. The module's own comment explains why, and it is a genuinely good catch: an illiquid market's ask sits pinned near 99¢ with nothing bidding against it, which is a quote artifact, not a 99% probability. Counting those would have poisoned the whole top decile.
- Bucketed by mid price, not by the ask.
(bid + ask) / 2, dropped into one of ten deciles. Worth remembering, because your bot's entry filter is written against the ask.
Of the 25,589 settled markets, 11,551 survived that filtering on the last successful run — the rest
either settled before we were snapshotting prices at all, or never had a liquid two-sided quote. Each survivor
contributes its outcome to its own category's bucket and to an __all__ rollup.
The table — resolution_rate_buckets
117 rows. The key is (category, price_band, ttc_band); the value is a yes-count and a
total. That is the entire artifact. The builder writes it to a fresh table and renames it into place, so a bot
reading mid-rebuild sees the old numbers rather than half the new ones.
One oddity you may notice on the index: our signal registry
lists min_samples and min_win_rate alongside resolution_rate_buckets as
if all three were tables. They aren't — those two are the config keys, picked up by the registry's pattern
matching on the module docstring. The only table the gate reads is resolution_rate_buckets.
What it means for your bot
You attach it with a click and it takes two numbers:
min_win_rate(required, 0 to 1) — the historical hit rate you demand.min_samples(default 30) — how many past markets must be in the bucket before you'll believe its rate at all.
The one piece of design worth praising: the rate is expressed for the side you trade. A YES bot is
checked against the bucket's historical yes-rate; a NO bot is checked against one minus that. So
"min_win_rate: 0.6" means "only when my side wins at least 60% of the time" no matter which side
you're on, and you never have to flip it in your head. The builder renders it in English as: only when
markets like this (price + time-to-close) have historically settled your way ≥ 60% of the time.
Then it looks up your market's own category, and if that bucket is too thin it falls back to the
__all__ rollup. If that is also below your sample floor, or the market has no bid or no
volume, the gate returns false and your bot skips. It never estimates. That is the right call, and catch #4 is
about how much it ends up skipping.
So — is the market well priced? Here is the actual table
This signal exists to answer a question you can now check yourself. Below is the real
__all__ rollup for markets 6 to 24 hours from closing — the one time band where all ten deciles
have a usable sample. "Settled yes" is what our history says happened. "Price implied" is the midpoint of the
decile, which is roughly what you'd have paid.
| Price decile | Markets | Settled yes | Price implied | Difference |
|---|---|---|---|---|
| 0–10¢ | 7,597 | 1.5% | 5% | −3.5 |
| 10–20¢ | 722 | 14.0% | 15% | −1.0 |
| 20–30¢ | 513 | 25.7% | 25% | +0.7 |
| 30–40¢ | 492 | 32.7% | 35% | −2.3 |
| 40–50¢ | 482 | 42.9% | 45% | −2.1 |
| 50–60¢ | 363 | 53.4% | 55% | −1.6 |
| 60–70¢ | 205 | 62.0% | 65% | −3.0 |
| 70–80¢ | 121 | 80.2% | 75% | +5.2 |
| 80–90¢ | 190 | 87.9% | 85% | +2.9 |
| 90–100¢ | 609 | 96.9% | 95% | +1.9 |
Read the last column and you have the whole story. Across all 30 buckets in the table with at least 30 markets in them, the average gap between what history says and what the price implied is 3.07 percentage points. The market is close to right. A gate that says "only trade the 90–100¢ bucket, it settles yes 97% of the time" is, to a first approximation, saying "only trade markets priced around 95¢."
There is a shape in the residuals, and it is the oldest one in betting: the cheap end settles yes less often than its price implies and the dear end settles yes more often. Longshots are overpriced and favourites are underpriced — the favourite–longshot bias, visible in our own data. That asymmetry, not the raw hit rate, is the only thing here that can pay you.
The catch
There are five. The first is the reason this page is worth reading.
1. A high win rate is not an edge — and this gate is a win-rate machine
Set min_win_rate to 0.93 and you will get a bot that wins about 93% of the time. It feels like
a triumph. It is nearly worthless, because of one line of arithmetic: for a binary contract bought at
price p, the break-even win rate is exactly p. Pay 91¢ and you must win 91% of the
time simply to stand still. The gate hands you a high hit rate by steering you to expensive contracts,
which is where high hit rates live. Your edge is win rate minus price, and nothing else.
We can show this with our own three bots, because they differ mainly in where that gap landed. All are house bots trading $5 flat in simulated money; the window is 2026-06-11 to 2026-08-10 for the first, 2026-06-16 to 2026-08-10 for the other two:
| Bot | Side | Avg price paid | Won | Edge | Closed | P&L |
|---|---|---|---|---|---|---|
| Calibrated Tail v2 (live) | YES | 91.67¢ | 91.56% | −0.11 | 486 | −$3.74 |
| Calibrated Favorites (oos) | YES | 88.26¢ | 89.17% | +0.91 | 545 | +$25.41 |
| Calibrated Longshot Fade (oos) | NO | 90.23¢ | 92.83% | +2.60 | 544 | +$82.01 |
Look at the live one. It won 445 of its 486 closed trades — a 91.56% strike rate that would make any newcomer's chart look magnificent — and it is down $3.74, because it paid 91.67¢ on average. It missed break-even by about a tenth of a point and that is the entire result. Meanwhile the bot with the worst-looking premise, fading longshots on the NO side, earned the most, because its gap was 2.6 points.
The ordering is not a coincidence, and it isn't an analogy either — it is the engine's own arithmetic. A
trade spends its $5 buying 5 ÷ price contracts and settles for
contracts × (payoff − price), so the expected result of one trade is
5 × (win rate − price) ÷ price. Feeding the three rows above into that gives roughly −$2.92,
+$28.10 and +$78.40, against actual −$3.74, +$25.41 and +$82.01. Don't expect those to match exactly:
dividing by price makes the maths non-linear, so a book of hundreds of trades is never quite the same as
one trade at the average price. It is close enough to confirm the mechanism, not close enough to be an
identity.
Judge a bot with this signal on P&L, never on win rate. If a fork of yours advertises 90% wins,
that number is a description of its price band, not of its skill.
2. "Our own history" is 99.3% weather — and the bots using it don't trade weather
Of the 25,589 settled markets behind these buckets, 25,418 are weather brackets. Sports contributes 123, crypto 25, "other" 21, economics 2. That is not a flaw in the builder; it is what our polling has actually resolved, and it is a fair reflection of where Kalshi's settled volume has been. But it has a sharp consequence.
No non-weather category has a single bucket big enough to clear a sensible min_samples — the
largest is a sports bucket with 38 markets in it. So for any non-weather market the category lookup fails
and the gate falls through to the __all__ rollup, which is 99.3% weather by
construction. All three of our bots exclude weather from their own category lists. Every one of them is
therefore being gated on the settle behaviour of weather brackets.
It is one step stranger for the live bot. Calibrated Tail v2 trades Polymarket exclusively — all 488 of its trades — while every bucket in the table is built from Kalshi markets. A Polymarket crypto contract is being judged by how Kalshi temperature brackets have settled. That may still be a defensible prior about how binary contracts behave near expiry in general. It is emphatically not "markets like this one", which is what the signal's name promises.
And there is a hint in the results that the transfer really does leak. That bot demands
min_win_rate 0.93, and at its settings only three buckets in the whole rollup can let it in —
they read 99.6%, 96.9% and 93.4%. So every trade it has ever taken came out of a bucket promising
at least 93.4%. Across 486 closed trades it actually won 91.56%: below the weakest bucket
that could have admitted it, and further below the two stronger ones. We won't oversell that — at this
sample size the shortfall is around one and a half standard errors, which is suggestive rather than proof.
But it points the way you would expect if a rate learned from Kalshi weather brackets does not describe
Polymarket crypto contracts.
The straightforward fix is to use it on weather, where the sample is genuinely deep and genuinely yours —
see wx_anchor for that side of the
library. Everywhere else, treat the rate as a general prior about price bands rather than a measurement of
markets like yours, and set min_samples high enough that you know you are getting the rollup.
3. The history is seven weeks deep, and the backtester can't replay it
The buckets read outcomes going back a long way, but they can only use a market if we have a price snapshot
for it — and market_price_snapshots begins on 2026-06-22. Whatever "our own resolved
history" sounds like, the price side of it is about seven weeks old. Nothing in these buckets has seen a
different market regime, a quiet season, or an election. Rates computed from a single seven-week window can
move when the window moves.
Relatedly, resolution_anchor sits on the backtester's unsupported list, with the reason
recorded in the source and dated 2026-06-09: the buckets are computed from history as known now,
so replaying them across the window they were built from would be mild lookahead. It is live-engine-only.
The behaviour to watch is the same one as with arb_anchor:
the backtest does not refuse. It runs without the gate and hands you a P&L curve for a bot you
did not build. It does name the rule in its unsupported_rules output — the backtester flags
any entry key it can't replay, so this can't pass silently — but you have to read that line. Forward
results are the only honest read on this signal.
4. It fails closed harder than you think — most of the board is invisible
Fail-closed is the right design and we would not change it. But be clear about the size of it. Beyond 72 hours to close the table is nearly empty: the 72–168 hour band has just two buckets with 30-plus markets in them, and the beyond-a-week band has buckets of n=1. Any bot combining this signal with an entry window more than three days out will simply never trade.
Even inside the useful range it is narrow. Take the live bot's settings — YES, min_win_rate 0.93,
min_samples 50. Across the whole rollup exactly three cells pass: the 90–100¢ decile under 6
hours, the 90–100¢ decile at 6–24 hours, and the 80–90¢ decile at 24–72 hours. One of the misses is worth
savouring — the 90–100¢ decile at 24–72 hours has settled yes 100% of the time, and is rejected,
because it has only 26 markets in it and the bot asked for 50. That is the gate doing its job perfectly.
Two more quiet exclusions: a market with no bid or no 24-hour volume is skipped outright, matching how the buckets were built; and because the gate buckets by mid price while your entry filter uses the ask, a bot told to buy at 85–95¢ is often being judged against the decile above or below the one it thinks it is in.
5. It will happily serve you yesterday's numbers — silently
The gate reads whatever is in the table. It does not check how old that is. On the morning this page was
written the nightly rebuild failed — database is locked, the builder colliding with the
live pollers writing to the same SQLite file — so the buckets in use are stamped
2026-08-09T05:50:56Z and the bots have been trading on them for about a day and a half.
In fairness this is rare: the job has succeeded 55 times since 16 June and failed once. And the buckets
genuinely do move slowly, so a stale day is not a crisis. But there is no retry and nothing surfaces the
staleness to a bot or to you, and it is the kind of thing that is only rare until it isn't. If you are
relying on this signal, the computed_at column in the table is the thing to look at.
How to actually use it
Do not reach for a high min_win_rate because it sounds safe — and do not avoid one because catch
#1 made expensive contracts sound like a trap. Neither the hit rate nor the price is the thing to read.
The gap between them is. That means the last column of the table above, not the middle two.
That column has a shape, and it points at both ends of the board at once. In the 6–24 hour sample, the dear deciles settled yes more often than their price implied — 70–80¢ by 5.2 points, 80–90¢ by 2.9 — so the buyer of a favourite was underpaying. The cheap end settled yes less often than implied — 0–10¢ by 3.5 points — so the favourite–longshot bias shows up there too, and the side it favours is NO. The widest single miss in the whole band is the 70–80¢ decile, not a cheap one. The dead zone is the middle, where the residuals are small and mostly negative.
Our own results line up with that. The two bots that took a side where the residual was positive — YES on
favourites, NO on longshots — made +$25.41 and
+$82.01. The one that lost was not on the wrong side of the board; it
simply paid too much for the same kind of trade. So "expensive" is not the enemy and "high win rate" is not
the goal — win rate − price is the only score.
And set min_samples deliberately. It is the difference between a rate you can lean on and eight
markets that happened to go the same way.
Where to go next
The signal with a genuinely deep sample behind it is wx_anchor, our most-used; arb_anchor is the other entry reason that needs no forecast. If any of the vocabulary here is new, the glossary and what is a signal? come first, and how a bot works explains where these gates sit in the pipeline.
Every claim on this page traces to one of three things: the output of
quants/signal_registry.py; the source of quants/resolution_anchor.py,
predictive/resolution_buckets.py, quants/engine.py and
quants/backtest.py; or a read-only query against our own database with its window stated. The
cron schedule and the rebuild failure were read from the production crontab and
/var/log/resolution-buckets.log. Bucket counts, calibration figures and bot results were read at
5:27 PM UTC on 2026-08-10 and will have moved since. "Live bots" counts strategies our registry marks live or
out-of-sample, which is 3; three further archived bots also reference the signal and are excluded. P&L is
simulated — TinyCorp bots trade paper money only. Verified 2026-08-10.