Skip to content
VC
Case 20 of 33 · Python · AI evals

Event forecasting with an LLM ensemble: calibration and Brier scoring

A personal system that forecasts event outcomes on prediction markets (Polymarket): five independent LLM agents with web search over open sources, a supervisor, calibration, every forecast written to a database before the outcome and scored with the Brier score after. This case is about measurement: how the quality gate was set, what it showed across 203 resolved markets, and why real-money trading stayed switched off.

Industry
Prediction markets · own R&D
Stack
Python · DuckDB · Claude Sonnet + Opus · web search
Format
Personal system, no client
Outcome
Brier gate not passed: 13 checkpoints, 203 outcomes
01 · Pain Point

A confident answer from a model has nothing to check it against

A language model with search access produces a coherent argument and a number: "probability 0.82". There is no way to eyeball that number. A single forecast about an event has no right answer until the outcome, and after the outcome one case says nothing about calibration: a confident miss and a cautious hit look the same. What you need is a series of recorded forecasts and a metric that punishes both overconfidence and cowardly 50%s.

The backstory is also about measurement. Before this system, the same venues hosted microstructure strategies: reacting to quote movements, mirroring large accounts. On 12 May 2026 that line was closed with a postmortem: after 2.5 weeks of paper runs, seven strategies, and none of them survived validation. Three causes turned out to be shared. Counting by order-book snapshots inflated the win rate: 51.9% per snapshot against 30.4% when aggregated per market. A signal that passed a historical replay at t = 4.79 decayed to t = 1.73 within two weeks and by 12 May had stopped firing at all: zero triggers in 11 hours across 27 thousand scans. One lucky day masked seven losing days out of eight.

Those became the rules the new system was built on: results count per market, after the outcome, on a sample of a preset size, with allowance for the decay of whatever advantage was found. And a new class of problem: slow events in geopolitics and politics, where the price holds for days, the venue charges no fee for the category, and the model has time to read the resolution criteria. The hypothesis: the price goes wrong where traders read headlines and skip the resolution criteria.

02 · Solution

The forecast is written before the outcome and scored after

A five-step loop. The model owns the reasoning only; market discovery, the database, calibration, outcome tracking and the Brier score live in Python and run without the model. The ensemble recipe comes from the AIA Forecaster paper, and from day one it was recorded so that every element could be recomputed after the fact.

01
Discovery

The venue's public API: binary geopolitics and politics markets, 1–45 days to resolution, price 0.02–0.98, a 24-hour volume floor. A market is re-forecast at most once per 48 hours

02
Ensemble

Five independent Claude Sonnet agents, up to five searches each. An agent sees the question, the resolution criteria and the horizon; its answer ends with a PROBABILITY line

03
Supervisor

A Claude Opus agent reads all five reasoning traces, runs targeted searches on the disputed facts and returns one reconciled probability

04
Calibration & record

Ensemble mean, Platt extremization with α = 1.73, a convex blend with the market price (weight 0.33), a 0.03 divergence gate. DuckDB stores the raw, calibrated and blended probabilities, the price, agent agreement and the reasoning

05
Outcome & Brier

A daily timer checks closed markets by the explicit winner flag, records our Brier and the market's, a paper result at the ask with a 0.015 half-spread, and posts a digest to a messenger

The gate was set before the first forecast

The Brier score is the squared difference between the stated probability and the outcome (1 or 0). Zero means a perfect forecast, a permanent 50% scores exactly 0.25, and a confident miss costs close to one. The same quantity is computed for the market price at forecast time, which gives a natural benchmark: the market is a forecaster too. The gate was written into the report code before the first forecast was recorded:

  • at least 30 resolved markets and our Brier below the market's
  • until both conditions hold, real-money trading is forbidden; the system lives in paper mode
  • the paper result is computed at the ask with a 0.015 half-spread, so the paper run cannot flatter itself
  • the geopolitics category was picked partly because the venue charges it no fee: the spread stays the only execution cost in the model

Two paths to a forecast: the model over the API, and the model as operator

The full Python pipeline calls the model over the API: five agents in parallel, then the supervisor. The server had no API key, so the second path became the working one: a Claude Code session with web search does the research by the recipe written into a skill, and the recording script takes its raw probability, fetches the live market price itself, applies the calibration, computes the divergence and writes the row. A daily batch looks like this: a queue of 13–14 markets, 8–14 Claude Sonnet sub-agents through a workflow, then the operator's arbitration before anything is recorded.

The split of roles is deliberate. The model owns reasoning and search. Python owns what has to survive a change of model or prompt: discovery, the database, calibration, outcome tracking and the report.

The outcome counter stayed silent for seven weeks

The first real failure happened in the most important place. The venue's public API stopped returning closed markets on the endpoint the system used to check outcomes: the response came back empty, with no error. The outcomes table stayed empty from late May until 16 July, the report honestly printed "no resolved forecasts yet", and it all looked like slow markets. Same failure class as in the case on reliability of background services: the process is alive, the data stops arriving, nobody complains.

The fix: outcome checks moved to the exchange-core endpoint keyed by condition id, where every outcome carries an explicit winner flag; the lookup by market slug stayed as a fallback. The very first run recorded 4 outcomes: our Brier 0.499 against the market's 0.235. The market led from the first row, and from that day the measurement became daily.

Every row keeps what a recomputation will need

Each forecast stores the ensemble's raw probability, the calibrated one, the blend with the price, the price itself, the number of agents, their agreement, the signal side, the reasoning and the model version. That decided the project's fate later: when the question came up whether Platt extremization helps, the answer was computed from the rows already accumulated, with no re-runs. Queue hygiene rules came from incidents:

  • a market enters the queue only if discovery saw it within the last 48 hours: May markets with a frozen "1.6 days to resolve" were still sitting in the queue in July
  • divergence is computed against the live price only: a queued price once lagged the real one tenfold (0.04 against 0.389)
  • the discovery horizon was cut from 120 to 45 days: short-dated markets produce resolved pairs faster
03 · Stack

Little code — all of the difficulty is in the measurement

Python 3 · httpx

Clients for the venue's public, unauthenticated APIs: market discovery, live prices, outcome checks

DuckDB

Three tables: markets, forecasts, outcomes. Raw and calibrated probabilities sit side by side, so any layer can be recomputed after the fact

Claude Sonnet ×5 + Claude Opus

Five independent agents with server-side web search, plus a supervisor that reconciles their disagreements

Claude Code + a sub-agent workflow

The operator path: 8–14 sub-agents per batch, a skeptic pass and an arbiter pass, recording through a script

Platt scaling · price blend

Log-odds extremization with α = 1.73 and a blend with the price (weight 0.33) per the paper's recipe; both layers measured separately

systemd timers on the server

Outcome checks and the report once a day with no model involved. The API forecast batch has its own timer ready (08:45 UTC, up to 12 markets per run) and switches on with one command once a key exists on the server

Messenger digest

Markets, forecasts and outcomes counted, Brier against the market, the forecast queue — a daily summary in Discord

Diagnostics over the outcomes table

Loss breakdown by price bucket and family base rates by question pattern

PythonDuckDBhttpxClaude SonnetClaude Opusweb_searchPlatt scalingBrier scoresystemd timercronDiscord
04 · Results

The gate was never passed — and that is measured

Resolved markets in the series
30 203

first checkpoint 31 July, thirteenth on 5 September 2026; over 600 forecasts recorded

Brier: ours / market, n = 203
0.104 / 0.075

13 checkpoints in a row below the gate; the first one read 0.119 against 0.080

Markets where we beat the price
80.8%

164 of 203; the aggregate deficit comes entirely from rare confident misses

Where exactly the accuracy leaks

The signature is stable from checkpoint to checkpoint: on most markets we beat the price, on the total we lose. At n = 66, four misses produced 84% of the entire deficit (3.646 of 4.322); without them, 0.068 against 0.057 on 62 pairs, close to level. Three of the four were confident disagreements with the price of more than 75 points, and in all four the market was right. Two misses share one question template about warships transiting a strait: raw 0.98 and 0.82 against prices of 0.010 and 0.037, both resolved "no". Another confused one conflict with a different one: 0.03 against 0.895, resolved "yes". A breakdown by price bucket at n = 203 showed the leak is concentrated:

  • price below 0.10 — n = 132, our Brier 0.042 against 0.022, with a realised base rate of 2.3%: we overestimated rare events
  • the middle, 0.25–0.75 — n = 20, 0.297 against 0.220: the one bucket where our disagreement with the price hurts statistically
  • price above 0.90 — n = 21, 0.048 against 0.047: level with the market, nothing to fix

The paper's correction made the score worse

Platt extremization stretches probabilities toward the edges: the log-odds are multiplied by 1.73. In the paper it improved the Brier score because models tend to huddle around 50%. On our series the effect ran the other way, and it is visible in the same rows where the raw probability is stored separately: at n = 53 the raw Brier was 0.108, with the correction 0.115, the market's 0.048. In the tails the correction flips the sign of the divergence: raw 0.13 at a price of 0.07 means "we are above the market"; after stretching it becomes 0.036 and a signal in the opposite direction. In one batch of 13 markets the sign flipped on 9.

The practical consequence: the signal field in the database is unreliable for cheap and expensive markets, and divergence is read only as "raw probability minus price". The general lesson for any AI evaluation: a coefficient from a paper gets verified on your own outcomes, and the pre-correction value is what you store.

What changed after the checkpoints

Every change to the recipe was dated and verified on the following checkpoints, over the same table:

  • The market price is hidden from the agent. Despite an explicit ban in the prompt, all five forecasts on one date ladder landed within 1.5 points of the price, and the reasoning said so in plain words. Since 23 August an agent receives only the question, the criteria and the horizon
  • One market family, one agent. Independent agents produced logically impossible pairs on date ladders: 0.25 for "by 31 August" and 0.15 for "by 15 September". A ladder is now derived from a single survival model, and an arbiter checks monotonicity
  • A skeptic pass and an arbiter pass. When the divergence from the price reaches 15 points, a second agent tries to refute the load-bearing facts: on 29 August it rewrote 5 of 14 forecasts. From 20 points, a third agent in the role of "the person who will resolve this market" walks the criteria clause by clause: on 1 September it flipped 3 tails out of 5 (0.94 → 0.33; 0.93 → 0.32; 0.78 → 0.55) and confirmed two
  • Family base rates from the outcomes table. "Ceasefire holds through date" — 14 of 14 "yes" against our average of 0.847; "town captured by date" — 0 of 4 against our 0.268 and a price of 0.095; "blockade lifted by date" — 10 of 10 "no", ours 0.011 against a price of 0.101, Brier 0.0003 against 0.0161, the one family where we are confidently more accurate than the market

The effect shows up in the same buckets. The cheap tail stopped growing: from n = 183 to n = 203 it gained 12 outcomes, and its cumulative gap moved from 2.605 to 2.579; the overestimation of rare events shrank from 2.3× to 1.7× the realised rate. The 0.25–0.75 middle keeps leaking: two new outcomes added 0.371 to the gap. That is where the recording discipline now points: a forecast whose market price falls in the middle is recorded more conservatively than the agents' conclusion.

A sibling line: a numerical ensemble on weather markets

In parallel, the same discipline was applied to daily-temperature markets, where the probability came from an ensemble of numerical weather forecasts (30–50 members). The first lesson from there: a win rate on its own means nothing. A bot with a 91.6% win rate over 1,089 markets stayed in the red: buying favourites at a high price, one loss eats many wins. The extreme bucket showed it cleanly: at an average price of 0.995 the break-even is 99.5%, and the actual 97.54% over 285 markets fell short. The second lesson: the bot picked favourites by price and never looked at the forecast at all; the ensemble gate was connected on 31 May, first in an observe mode where it only logged its decision. Minimum-temperature markets were excluded after a 61% win rate, and the venue fee, once discovered, was added to the paper accounting. On 28 July the line was closed by its own A/B gate: negative expectancy in the tested variants.

The overall outcome, stripped of detail: the system never earned the right to trade real money, and the outcomes table says so across 13 checkpoints in the same voice. The value is in the measurement loop: every forecast carries a date, a raw and a calibrated probability, a price and an outcome, so any argument about quality is settled with a query.

05 · Where it fits

Any AI that answers with a number or a decision needs the same check

Behind this case is the generic problem of "the model answers confidently, and the only way to check it is against accumulated outcomes". That is the core of the AI evals service: record the answer before the result is known, compare it with a reference, find the range where the model goes wrong. The loop transfers almost unchanged:

  • AI chat and document search, where a second model grades the answers — its grades get checked against human grades with the same metric that checked forecasts against outcomes here
  • Scoring and risk assessment, where the model outputs a probability — a bucket breakdown shows in which range it overrates itself
  • Document and ticket classification, where one confident miss on a rare class costs more than a dozen cautious hits
  • Demand and lead-time forecasting — family base rates from your own history beat any prior coefficient
  • Agents with search that build a coherent chain out of a live news story — they need the skeptic pass and the criteria-by-clause arbiter before the answer travels further
  • Any method from a paper — calibration coefficients, weights, thresholds — earns its place only after a recomputation on your own outcomes
What's reused on subsequent projects
  • The storage schema: the answer is written before the outcome, pre- and post-correction values sit side by side, any layer can be recomputed after the fact
  • A quality gate set before the first run: sample size, metric, benchmark, and a ban on the next step until it is met
  • Bucket diagnostics: the score broken down by answer range, plus a query for family base rates by case pattern
  • The skeptic and arbiter passes: refuting the load-bearing facts and walking the criteria clause by clause for answers far from the reference
  • Anchoring control: the model never sees the reference answer during research, and a separate agent checks that a family of answers is coherent
  • Outcome tracking as a separate autonomous process with a silent-failure check: an empty response from the source is a different thing from having no outcomes
Similar challenge?

If a model answers confidently, the question is whether the outcomes are recorded

Two things are the sensible place to start: a table the answer lands in before the result is known, and one metric that punishes a confident miss harder than a cautious one. The first 30–50 pairs usually show the range where the model overrates itself, and from there the tuning runs on numbers.

Ready to start?

The 9,900 ₽ audit — with a concrete report and quote

I'll tell you what to deploy in your business first, what the payback looks like, and whether you need AI for the task at all (sometimes you don't).

Or just send your question — I reply within 2 hours