Weather trading system
Builds its own temperature forecast, prices every Kalshi band off it, and trades the gap.
Kalshi lists markets on the daily high temperature at a single weather station. Austin's series is KXHIGHAUS: one event per day, and inside the event a row of contracts covering a strip of temperatures. -B65.5 pays out if the high lands in the 64.5–66.5 band. -T61 pays if the day never gets above 61, -T68 if it clears 68. Exactly one of them wins.
The bot builds its own forecast, converts it into a probability for each band, and buys the side where the market's price is far enough from that number to cover the fee.
Collecting the forecast
Three sources, polled every five minutes:
- NWS — hourly gridpoint forecast from the public API, collapsed into a daily max. Current temperature comes off the station timeseries page.
- AccuWeather — daily highs and the current reading, scraped from the forecast pages.
- Wunderground — same two values, scraped with a headless browser.
Results merge by date into today and tomorrow, and get appended to current.csv and high_prediction.csv in a folder named after the event. Nothing is overwritten, so the whole day is a time series of how the forecast moved. At local midnight the finished day's folder is uploaded to Google Drive, tomorrow becomes today, and the files start over.
Three sources instead of one is not for redundancy. The spread between them is the input the pricing model actually needs.
Reading the market
A websocket connection subscribes to every open ticker in the series. Each market keeps four sorted books — yes bids, yes asks, no bids, no asks — and because a NO at price p is a YES at 100 − p, every update writes to both sides at once. That way top-of-book is correct no matter which side the quote arrived on.
Updates carry a sequence number, and anything at or below the last one seen is dropped rather than applied. Snapshots, deltas, public trades, and the account's own fills all get appended to CSV as they arrive.
Fills feed a position tracker that nets against the opposing side first: a NO fill in a market already holding YES reduces the YES position instead of opening a second one, and running exposure per event moves accordingly. State is written to portfolio.json after every fill.
Turning weather into probabilities
Take the highs from all three sources, take the mean and the standard deviation, and treat that as a normal distribution over where the high lands.
The standard deviation alone is too tight, because it only measures how much the forecasters disagree right now — not how much the day can still surprise them. So it gets scaled by time: the further away the high still is, the wider the distribution, with an extra widening past 24 hours out. A floor of 1 degree keeps three agreeing sources from producing a near-zero spread.
Each contract's probability is then the area under that curve between its bounds:
- band contracts use center ± 1 degree
- the bottom tail runs from the highest temperature already recorded today up to its cutoff
- the top tail runs from its cutoff to open-ended
Anchoring the bottom tail on the observed high matters — once the station has hit 63, no outcome below 63 is still possible. Bands that fall entirely under the observed high are marked dead and priced at zero, as are markets with no live quote on one side. The rest are normalized to sum to one.
Deciding to trade
For each side, expected value in cents:
yes_ev = p * (100 - yes_ask) + (1 - p) * (-yes_ask) - fee
no_ev = (1 - p) * (100 - no_ask) + p * (-no_ask) - fee
The fee is Kalshi's taker formula, ceil(0.07 × contracts × price × (1 − price)), largest at 50¢ and shrinking toward either end. Pricing it in before the decision matters because most of the edge on these markets is a few cents wide.
The better side is taken only if its EV lands between 6 and 40 cents. The floor skips trades too thin to survive the fee; the ceiling skips the ones that look too good, which in a thin market usually means the quote is stale or the band is already dead.
Probability gates sit on top of EV and differ by day. Today's event only buys YES on bands it is already fairly confident in, and NO only on bands under 20%. Tomorrow's event, where the forecast can still move a lot, uses a looser YES threshold.
Sizing
Each event has a fixed exposure limit, split across its live bands by confidence — measured as distance from a coin flip, |0.5 − p|. Bands the model has an opinion about get more of the budget, bands sitting near 50/50 get less, dead bands get zero. Limits are recomputed on every probability update, so the allocation shifts through the day as the forecast sharpens.
Paper and real
Both paths run at once. Strategies are registered as production or beta, and every trade goes through validation that routes to a real order or a simulated one based on that flag. Beta strategies keep their own separate exposure limits and position book, so a new rule can run all day against live market data and its results can be compared against the strategies actually trading.
Probability tables, position limits, and each trade post to Discord as they happen.
More writeups
- It's a Metric, Too — Solving Jane Street's October 2026 puzzle by guessing well and letting a solver prove me wrong.
- Knight Moves 7 — Solving Jane Street's July 2026 puzzle, where the knight can climb.
- PocketLens — Self hosted personal finance app.
- flightwatch — Python automation to hunt cheap flights to Europe.
- Wordle solver — My attempt at solving the game.
- LLM distillation feature store — Cache the expensive model's answers once, train a cheap model on them forever.
- NFL Big Data Bowl — Two metrics for tackling — one for the chase, one for the finish.