← Work

Live strategy · Polymarket

Weather derivatives, three edges deep

One system in two halves: a Bayesian inference engine maintaining a posterior over the true airport temperature, and a low-latency execution layer firing against it. Each edge died as market makers adapted, so I built the next one: observation latency, then a Markov decision layer over dying contracts, then the forecast itself. The first two traded live and were retired on an out-of-sample measurement that said the window had closed; the third was calibrated offline and never deployed.

Role

Independent trader

Period

April – July 2026

Stack

Python, numpy, hand-rolled Kalman filter, Monte Carlo, METAR, 5-machine deployment

Venue

Polymarket — daily maximum temperature markets, 28 cities

How I approach a market

My working method is to try to find every variable that enters a market's pricing equation, the whole set rather than a convenient subset of it. Most markets make that impossible; there are too many inputs and too many of them are unobservable.

Weather markets are unusual in that the set is small and almost entirely public. What determines the outcome is: the official observation feed that resolves the contract, the intermediate sensor readings that precede it, private weather stations near the station, forecast curves, the time of day relative to the daily peak, and the market's own orderbook. That is close to complete. And because every one of those has history, the whole thing can be calibrated against the past rather than guessed at.

That property is why I chose these markets: they are one of the few places where a solo participant can plausibly enumerate the variable set and then check their model against reality. What follows is the account of doing that: two edges I traded and measured to death, and a third I built the model for but never deployed.

Over the period covered here the account grew from roughly $30 to roughly $1,200 across everything I was running. The absolute numbers are small and the depth available in these books is measured in hundreds of dollars; what I think the work demonstrates is the process rather than the return.


1. The instrument

Polymarket lists a daily market for each of about 28 cities: what will today's maximum temperature be? The answer space is a set of mutually exclusive integer buckets, 27°C, 28°C, 29°C and so on, each a binary contract.

Two structural properties matter.

Resolution is mechanical. The market settles on the official METAR observation from a designated airport station: LTFM for Istanbul, EGLC for London, KORD for Chicago. METAR is the standardised aviation weather report, published on a fixed schedule (typically twice an hour) and distributed through public channels. No subjective judgment enters anywhere. What is left is a race about a number that is going to become public regardless.

The state is monotone and self-eliminating. A daily maximum only ratchets upward. Once an observation arrives above bucket X, bucket X is dead with certainty and its probability mass must move to higher buckets. The day is therefore a sequence of deterministic eliminations punctuating otherwise continuous drift, and each elimination is a discrete, forced repricing event.


2. Edge I: observation latency

The first edge was pure speed, and it came from research rather than from modelling.

METAR observations reach the public international feeds with a delay. But Turkey's state meteorological service, MGM, publishes its own stations through the web service that backs its public website, and that service exposes the raw METAR string in a rasatMetar field, timestamped, before the same observation propagates through the international distribution that most participants watch.

Measured on Ankara: MGM carried the observation roughly 75 seconds after observation time, while the NOAA path carried it at roughly five minutes. That is an advantage of about four minutes and forty-four seconds on the number that settles the contract.

That is an enormous head start in a market that reprices in seconds. The engineering to exploit it was straightforward by comparison: poll the endpoint on a persistent HTTP/2-less httpx connection (p50 15–37 ms from Turkey), detect the change on the METAR's own DDHHMMz timestamp rather than the sensor field beside it, and fire a pre-signed order.

How it died. Within weeks, market makers found the same feed. What killed the edge was the structure of the venue rather than their reaction speed. A taker order carries roughly 250–330 ms of server-side delay before it can match, while a resting maker who sees the same observation can cancel in 23–50 ms. The asymmetry is about an order of magnitude, and it always favours the side already in the book. Our orders started returning unmatched: not rejected, not filled, just arrived at a price that no longer existed. A representative case: the Ankara 17°C contract, where the ask moved 0.45 → 0.95 while our order was in flight and the fill came back unmatched.

This happened well before the summer, so the pure latency edge had a short life. And the read is not that we were too slow. We were fast. The venue's taker delay hands the last look to the resting side, which is a structural feature of the market rather than a latency problem to engineer away. Recognising that moved the project onto its second phase.


3. Edge II: the mass redistribution of a dying bucket

If you cannot win the race to the observation, the next question is what happens in the orderbook around the elimination, and whether it is priced correctly.

When bucket X dies, its probability mass has to go somewhere. The market's response is not instantaneous or uniform: some contracts reprice immediately, others lag, and the shape of the redistribution depends on how much of the day is left, where the forecast peak sits, and how thin the books are. This is a decision problem rather than a race, and it is where the algorithmic work went.

The decision at the moment of a spike is not "is the upper bucket cheap." It is a joint estimate over several quantities at once:

QuestionWhy it matters
Is this spike a genuine elimination, or a false one?Thin books produce flickers that look identical to a death for a few hundred milliseconds. Acting on those is the dominant loss mode.
What is the distribution over the next observation?The elimination tells you X is dead; it does not tell you whether the day ends at X+1 or two buckets higher. Those are different trades.
How much size is resting in the book?A large nominal edge on a contract with $0–40 of depth is not an edge; it is a quote.
If we buy, when and at what price can we exit?Requires a model of how fast the book reprices after an event, which is not a constant.
What probability are we buying, at what price?The only question that determines whether the trade is positive-value, and the one most easily lost behind the other four.

Those are the state variables of a Markov decision problem, and that is how it was implemented.

The structure. The system carries a state s = (posterior temperature μ, σ², confirmed max so far, forecast curve, orderbook snapshot, open positions, time remaining) and chooses among BUY / SELL / HOLD actions on individual bucket contracts. Expected value is decomposed into two conditional engines:

EV(action) = Σ_x  P(next observation = x)  ·  P(orderbook response | x)  ·  (payoff − cost)
                  └── forecast/posterior ──┘  └──── reaction model ────┘

The first term is the probability engine described in section 5. The second is a reaction model: after an event, a contract's price converges from its pre-event mid toward an analytically derived post-event fair value at an exponential rate,

R(p, Δt) = p_pre · e^(−γΔt) + p_post · (1 − e^(−γΔt))

where γ is the repricing rate, the quantitative form of "how quickly will the maker close this spread." A confirmed elimination drives the dead bucket's NO toward 0.99; γ determines how long the mispricing is available and therefore whether the trade is reachable at all.

Because that model exists, each candidate action is evaluated under two exit horizons and the better one is chosen automatically: hold to resolution, where payoff is Σ_Y P(Y)·payoff(Y), or hold to the next event, where payoff is the reaction model's expected price at that time. Multi-position utility is scored as U = E[PnL] − λ·Var[PnL], so the engine prefers a smaller certain edge to a larger volatile one.

The decision here is expressed as expected value over an explicit distribution, with execution cost, liquidity and exit path inside the calculation rather than bolted on afterward. That is the part I would call trading work rather than engineering.


4. Infrastructure

Five machines, each placed for a measured reason.

LocationRoleRationale
AWS IrelandExecution, orderbook recording~25 ms RTT to Polymarket's CLOB
AWS VirginiaNOAA scraper~5 ms to the source vs ~181 ms from Turkey
Hetzner IstanbulSecond scraping IPIndependent rate-limit budget
İzmir (home Linux box)Primary MGM scraper, probability engine, data analysisTurkish residential IP survives rate limits that ban datacentre ranges
LaptopDevelopment and analysis

Connected over a Tailscale mesh, with execution reachable on a public address so a mesh outage could not block trading.

Measured decisions, not assumed ones. NOAA's 62-byte static text endpoint updates before the JSON API that reads from it: p50 181 ms from Turkey against ~15–20 ms from Virginia. Cold HTTP calls averaged 878 ms versus ~172 ms on persistent connections. Pre-signing EIP-712 orders and warming the tick-size, neg-risk and fee-rate caches moved roughly 470 ms off the critical path. Removing a blocking order-status poll took fire latency from 10,163 ms to 58 ms, a 175× improvement and the largest execution gain in the project.

Tail latency. A single scraping IP showed a persistent p99 near 1,500 ms and one 5,031 ms outlier, during which the market repriced and the order went unmatched. Polling the same station from two independent IPs and taking the first arrival, deduplicated on the observation's own timestamp, removed it. Live logs show the winner alternating between hosts by 209 ms, 113 ms, then −7 ms. Neither host is reliably faster; the pair removes the tail.

Rate limits as a design constraint. Sustained polling at ~166 requests per second got a datacentre IP banned by MGM for hours; the residential IP under identical load was throttled but not banned. The response was to confine bursts to the observation publication window and idle outside it.

Orderbook state. Polymarket's WebSocket sends full book snapshots plus incremental deltas. The deltas carry no top-level asset identifier. The identifiers sit inside the change array, because the YES and NO legs of a binary market are coupled and one order emits two entries. An implementation that filters on the top-level field silently drops every delta. With that handled, the reconstructed book was validated against the exchange's own interface contract-by-contract and matched exactly.


5. The probability engine

The reaction model answers what the book will do. The other half is what the weather will do, and that is a state-estimation problem.

A Kalman filter tracks a hidden continuous temperature as an AR(1) random walk pulled toward a forecast curve. Observations update the posterior at their own frequencies and precisions:

SourceModelCadence
METARround(T) + noise, near-exact, the anchor1–2 per hour
Private weather stationsT + bias + N(0, σ²), solar-adjusted~5 min
Airport sensor feeds (MGM, MetService)T + N(0, σ²)~10–15 min

Monte Carlo simulation of the remaining day produces the three distributions the decision engine consumes: the daily maximum P_now, the next observation Q_next, and the maximum conditional on that observation D_after. They satisfy the Bayesian consistency identity Σ_m Q(m)·D_after(k|m) ≈ P_now(k), which served as a built-in correctness check. Residual came in under 2%, the remainder Monte Carlo noise.

Peak timing. Because the maximum is monotone, the context that matters most is how much daylight remains and where the forecast peak sits. A separate component derives the peak hour from forecast curves and arms the system only within that window; outside it the state is either still climbing predictably or already settled.

Guards. Two failure modes needed explicit handling. A single noisy station reading could drag the posterior and collapse its variance. One market printed a distribution asserting 100% certainty on a bucket with hours of daylight left. I added a divergence guard that inflates observation variance when a reading strays more than about 2.5σ from the anchor, a floor under observation noise, and a guard that widens any distribution claiming near-certainty about a future maximum.


6. Quantitative work

6.1 Falsifying a promising signal

Consistent with trying to see the whole variable set: the engine was using station temperature and solar radiation but discarding eleven other fields the sensor API returns. I re-fetched the full set over 92 days and asked which carry incremental, out-of-sample information about the day's remaining rise. Screening on correlation, then variance inflation for redundancy, then partial correlation to strip out season, then leave-one-day-out error to catch overfitting.

Wind direction looked excellent in Istanbul: partial correlation −0.43 at p < 0.001, stable across five decision hours, with a clean physical story: a northerly over the Bosphorus brings a cooler air mass. I then tried to break it.

StationClimateResult
IstanbulCoastal, sharp air-mass boundaryStrong at every decision hour
ChicagoContinental, frontalMorning only; gone by midday
LondonMaritime, well-mixedAbsent (p ≈ 0.7)

It did not generalise. Its value tracked local geography, and the multi-feature models that scored best pulled a different winning set on each run, which is the signature of overfitting on correlated observations. I rejected it rather than deploying it. Separately, heat index, wind chill, UV and dewpoint proved to be deterministic functions of temperature and humidity (variance inflation up to 60,000) and carried nothing independent.

6.2 Calibration across 28 markets

The calibration pipeline had a structural flaw: the offline fitter never saw the forecast curve, so the forecast-pull parameter always fit to zero, and production silently overrode it with global constants. No market's dynamics were being fitted at all. I rebuilt the fitter to replay against archived historical forecasts, deliberately not reanalysis, since using realised weather would be look-ahead leakage. Candidates were scored by Brier.

Mean Brier reduction against the prior model configuration was 24.2% across 28 fitted markets, ranging 3.5% to 49%, with no market regressing. The fitter's own behaviour was a useful check: for one city whose forecast source carries a 5.28°C bias, it independently chose to ignore the forecast entirely and still improved 35% over the default.

Two limits on that number. It is an offline replay result measured against my own earlier configuration, not against the market's prices, and I never deployed the final calibration — live journaling had already stopped by the time the fit was produced.

6.3 Audits against observed data

  • Unit mismatch. Six US markets resolve in Fahrenheit. The forecast drove the state in Fahrenheit while every observation was fed in raw Celsius. The market's unit was read once at construction and never used again. Those six ran on an incoherent posterior for the whole period. I wrote the fix and did not deploy it — it touched the money path, and I was holding that class of change for a sign-off that never came before the strategy was retired.
  • Publication schedules. Histogramming the minute-of-hour of 40 days of observations per station showed five Asian markets publishing at :00 and :30 while configured for :20 and :50. Every control station matched, which is what made the discrepancies credible.
  • Timezones. The fallback logic was DST-blind and assigned all US markets a fixed offset; one was two hours out in summer, shifting the local-day window used to compute a daily maximum.

7. Edge II, measured

By July the system was running sessions without taking trades. The tempting response was to loosen the filters; instead I measured whether the edge still existed, using ~3.3 GB of tick-level orderbook data recorded across 50 city-days.

The scan reconstructs every genuine elimination, meaning a bucket that was alive and leading whose NO side collapsed to certainty within 60 seconds, and records for each the target contract's price 60 seconds before, its price at the moment of elimination, the time until it repriced beyond entry + 0.10, and the day's realised outcome.

Entry priceEventsWinsWin rateEV per $1
≤ 0.151000%−1.00
0.15 – 0.356117%−0.44
0.35 – 0.606117%−0.57
> 0.6010660%−0.26
All32825%−0.58

Three findings.

The window has closed. The evidence that justified this phase, a Madrid cascade in June, showed the upper contract sitting cheap for about 23 seconds. The median across this dataset is 0.15 seconds. Whatever the reaction model's γ was when the strategy was designed, it is now large enough that a reactive strategy cannot systematically reach the mispricing.

The cheap contracts were cheap for the right reason. Entries at or below 0.15 went 0-for-10. The events that won were mostly already priced at 0.72–0.88 at the moment of elimination. The market was not lagging; it was correct.

No band was profitable. Even the 60%-win band loses money, because at 0.80 a win pays +0.25 and a loss costs −1.00. Maker pre-emptive repricing explained only 9% of cases, so this was efficiency rather than predatory quote-pulling.

Limitations, stated plainly: about 22 of the 32 events are independent, since ten are repeated detections of one persistent situation. The measurement assumes holding to resolution and does not credit intraday exits. It covers five days of summer conditions. I would not call it definitive; I would call it sufficient to stop increasing size.

The response. I cut the clip from $20 to $5, tightened the maximum entry price from 0.90 to 0.35, reduced the daily budget to the account balance, and narrowed the market set. The pre-trade filter that had been refusing to trade turned out to have been right: on the next live session it blocked three signals on a contract that then fell from 0.31 to 0.11.


8. Risk

  • Sizing. Fixed clips initially, later a Kelly-lite function of estimated edge and entry price, bounded well below full Kelly to reflect uncertainty in the edge estimate itself.
  • Circuit breakers. Per-market consecutive-failure limits, a daily spend cap and a maximum fire count, all written after a session in which a single-market bot re-fired every four minutes and lost about $80. The failure was not the thesis; it was the absence of a stop.
  • A pre-trade filter. A pure-orderbook gate requiring clean structure before firing: everything below the leader dead, no live contract above the target, the target cheap. It refuses when the book is ambiguous.
  • Fail-safe defaults. Stale feed means no new positions; missing model feed means skip rather than assume; unknown state means do nothing.

9. Where this leaves me

Two of the three edges I found here are gone and I can say why each one died. The first went to a structural feature of the venue that hands last look to the resting side. The second went to efficiency, measured rather than inferred.

What remains is the third: the forecast itself. Taking a position on the day's maximum before the market resolves the ambiguity, using a better-calibrated distribution than the consensus, is slower and capacity-constrained but is not obviously arbitraged away. The calibration work in section 6 was built toward that. I stopped before deploying it, because the honest sequence is calibrate, verify out-of-sample, paper trade, then size, and I had done the first step and part of the second.

What I know and what I don't. Working through this market end to end left me able to think about these instruments from the maker's side rather than only the taker's: the resolution mechanics, the observation feeds and their relative latencies, the shape of the books, how quotes behave around a forced repricing, where informed flow shows up.

Where I am incomplete is optimisation. Knowing that a book should be updated after flow arrives is not the same as knowing how much to move it, how to skew quotes toward the side that is more likely to be right, or how to separate informed takers from uninformed ones in real time. Those are the questions I would most want to work on, and they are not ones I can answer alone from a residential connection with a four-figure account.

What I would do differently. I built execution before measurement. The analysis in section 7 could have been run six weeks earlier on data I already had; instead I ran sessions and inferred from an absence of fills. I would now insist on an expected-value measurement before scaling a strategy rather than after. I also failed to archive the production forecast, so calibration had to be fitted against a proxy source, which I would not repeat.

A note on scope: when the reactive edge in these markets closed, I went looking for the same structure elsewhere and built a separate system for World Cup group markets, a cross-market probability translation rather than a latency strategy. That is documented separately; it shares no code or thesis with this project beyond the general approach.


Appendix: technical summary

Markets28 cities, 11 buckets each
Data3.3 GB tick-level orderbook (50 city-days); 92 days multi-station sensor data; multi-year observation archives
Machines5, across three providers and a residential line, over a Tailscale mesh
ExecutionPre-signed EIP-712 fill-and-kill; fire latency 10,163 ms → 58 ms; ~25 ms RTT to venue
Decision layerMarkov decision process over posterior × orderbook state; dual exit horizons; mean-variance utility
Reaction modelExponential repricing p_pre·e^(−γΔt) + p_post·(1−e^(−γΔt))
State estimationKalman filter, ensemble forecast hypotheses, Monte Carlo outcome distributions
ScoringBrier score, log loss, expected calibration error, Bayesian consistency residual
StatisticsVariance inflation, partial correlation, leave-one-out cross-validation, out-of-sample EV by entry band
Ask me about it