Prices Polymarket temperature contracts off a calibrated forecast error model, sizes positions with fractional Kelly, and refuses to quote until sigma has been fitted against real data.
A market like "Will the high temperature in London exceed 25C on Aug 8?" is a binary option on a continuous underlying. The fair value is a tail integral. Everything that decides whether an edge is real lives in two places: which daily statistic settles the contract, and how wide the distribution is at that forecast horizon.
$ weather-edge calibrate --history data/forecast_history_synthetic.csv
samples 1200
mean signed error -0.10 F
sigma(h) = 2.231 * sqrt(1 + h / 45.8)
fit R^2 0.962
sigma at 12h 2.51 F
sigma at 24h 2.75 F
sigma at 168h 4.82 F
The engine is covered by 77 tests. Calibration recovers a known error process: fitting against 1,200 samples drawn from sigma(h) = 2.40 * sqrt(1 + h/60) returns 2.23 * sqrt(1 + h/45.8) with an R^2 of 0.962.
On the 500 synthetic markets in data/, the model scores a Brier of 0.161 against a base rate benchmark of 0.249, and reliability holds across the range:
| predicted | realised | n |
|---|---|---|
| 0.048 | 0.047 | 43 |
| 0.253 | 0.309 | 55 |
| 0.547 | 0.604 | 53 |
| 0.850 | 0.852 | 54 |
| 0.952 | 1.000 | 58 |
Those numbers come from synthetic data and are not evidence of edge. scripts/make_synthetic_data.py draws outcomes from the same distribution family the model assumes and injects a deliberate mispricing into every quoted price, so the model finds what was planted. The backtest reports a large profit on that file. It means the machinery is wired correctly end to end. It means nothing about real markets.
To find out whether there is real edge, point calibrate at your own Tomorrow.io history and backtest at markets you actually recorded. The tool prints skill against the base rate on every run and says so in plain text when the model fails to beat it.
pip install -r requirements-dev.txt
python scripts/make_synthetic_data.py
PYTHONPATH=src python -m weather_edge.cli backtest \
--markets data/markets_synthetic.csv --sigma-base 2.2314 --sigma-scale 45.7882Run the tests with PYTHONPATH=src python -m pytest.
For live pricing, copy .env.example to .env, add a Tomorrow.io key and your fitted sigma, then:
PYTHONPATH=src python -m weather_edge.cli price \
"Will the high temperature in Seoul exceed 30C?" --date 2026-08-08question text ──> markets.parse_market ──> MarketTerms
city, threshold, statistic, side, local tz
│
Tomorrow.io ──> forecast.summarise ──────────────> Forecast
(local day boundaries) high / low / mean, lead time
│
forecast history ──> calibration.fit_horizon_sigma ──> sigma(h)
│
v
pricing.fair_probability
│
┌───────────────┴───────────────┐
v v
sizing.stake_usd liquidity.walk_book
fractional Kelly fill at your limit
└───────────────┬───────────────┘
v
strategy.evaluate
trade, or a logged reason
Sigma has no default. Settings.from_env raises if it is unset and tells you to run the calibrator. A fixed sigma prices a seven day forecast with the same confidence as a twelve hour one, which is the fastest way to talk yourself into a position that does not exist.
Edge is measured in probability points, not percent of price. Relative edge explodes on cheap contracts: a 1c market quoted at a 2c fair value shows a 100% edge while offering one point of real advantage. Sizing off that number pushes the entire book into longshots.
It does not place orders. Live execution needs a funded Polygon wallet and a Polymarket CLOB client, and untested code that moves real money is worse than no code. The pricing, sizing, and fill simulation are complete and tested; wiring them to an exchange is deliberately left out.
Cities are limited to the nine in markets.CITIES, each with the timezone its local calendar day is defined in.
This replaces a bot I wrote on top of polymarket/agents. Rewriting it surfaced seven bugs, each of which now has a regression test:
| Bug | Effect |
|---|---|
| Priced daily-high markets off the daily mean | Understated P(exceed) on every market touched. An 86F high with a 75F mean prices an 80F threshold at 0.96 or at 0.08 depending on which you use |
| UTC day boundaries for local settlement days | Seoul's day was off by nine hours, mixing two days into one high |
| Fixed sigma of 3.5F at every horizon | A seven day forecast priced as confidently as a twelve hour one |
(\d+)\s*°?\s*[Cc] matched years |
"2024 Championship" parsed as a 2024 degree Celsius threshold |
| Liquidity summed every ask at any price | One share at your limit behind a wall at 40c looked like a deep market |
| YES only | When fair value sat below the market, the trade is to buy NO. The bot skipped it |
Sizing was min(edge * 0.1, 0.05) |
Ad hoc. Replaced with fractional Kelly, f* = (q - p) / (1 - p) |
Writing the fill simulator turned up an eighth: the minimum stake was checked against the intended size but never against what actually filled, so a thin book could open a $0.05 position under a $1.00 minimum.
MIT. See LICENSE.