Trade Bot Dictionary · Microstructure

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
How resolution_anchor gets from our own settled markets to a trade decision Two tables inside our own predictive.db feed one nightly builder. The kalshi_markets table supplies 25,589 markets that have actually settled yes or no, of which 99.3 percent are weather brackets; the market_price_snapshots table supplies 37.5 million price observations going back only to 22 June 2026. The builder, predictive/resolution_buckets.py, runs on cron at 5:50 AM UTC daily. For each settled market it keeps one liquid two-sided snapshot per time-to-close band, buckets it by mid price decile, and counts how the market resolved. The result is the resolution_rate_buckets table: 117 rows keyed by category, price band and time-to-close band, each holding a yes count and a total, plus an all-categories rollup. The gate, quants/resolution_anchor.py, looks up the market's own category first and falls back to that rollup, requires the bucket to hold at least min_samples observations, and requires the historical win rate for the bot's side to clear min_win_rate. If no bucket has enough samples, or the market has no bid or no volume, it fails closed and skips. Nothing here reaches outside our own database. SOURCE · OUR OWN DB BUILDER · CRON TABLE GATE DECISION WHATHAPPENED WHAT ITCOST kalshi_markets 25,589 markets that actually settled 99.3% are weather — see catch #2 market_price_snapshots 37.5M price rows — bid, ask, volume, time only back to 22 Jun the real depth limit resolution_buckets 50 5 * * * daily, 5:50 AM UTC one snapshot per band per market · liquid and two-sided only · keyed by MID, not by ask resolution_rate_buckets 117 rows (category, price_band 0-9, ttc_band 0-4) → yes_count / total rebuilt whole each night, swapped in atomically resolution_anchor 1 · bucket this market by price decile + hours left 2 · try its own category, else the __all__ rollup 3 · need total ≥ min_samples 4 · need this SIDE's rate ≥ min_win_rate FAILS CLOSED Too few samples, no bid or no volume → no trade. TRADE at a price that already knew this — catch #1 SKIP everything past 72h, nearly — catch #4 Notice what is not in this picture: any arrow coming in from outside. Every other signal in the library starts at somebody else's API. This one starts at our own settled markets — the honest reason it is worth having, and the reason its blind spots are ours too. The gate never invents a rate. If the bucket it needs holds fewer than min_samples resolved markets, it returns false and the bot walks away. That is the correct behaviour, and it is also why so little gets through — see catch #4. Row counts, the 117 buckets and the 11,551 markets used were read from predictive.db on 2026-08-10 at 5:27 PM UTC. The cron line was read from the production crontab.
A closed loop: our own settled markets go in one end, a lookup table comes out the other, and the gate reads nothing else. The two warnings on the left — 99.3% weather, and no prices before 22 June — are the whole shape of what this signal can and cannot know.

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

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:

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:

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 decileMarketsSettled yesPrice impliedDifference
0–10¢7,5971.5%5%−3.5
10–20¢72214.0%15%−1.0
20–30¢51325.7%25%+0.7
30–40¢49232.7%35%−2.3
40–50¢48242.9%45%−2.1
50–60¢36353.4%55%−1.6
60–70¢20562.0%65%−3.0
70–80¢12180.2%75%+5.2
80–90¢19087.9%85%+2.9
90–100¢60996.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:

BotSideAvg price paidWonEdgeClosedP&L
Calibrated Tail v2 (live)YES91.67¢91.56%−0.11486−$3.74
Calibrated Favorites (oos)YES88.26¢89.17%+0.91545+$25.41
Calibrated Longshot Fade (oos)NO90.23¢92.83%+2.60544+$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 faileddatabase 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.