wx_anchor
wx_anchor lets a bot trade a daily-temperature market only when today's actual temperature at one specific US airport says it should. Kalshi runs markets on "will today's official high at Phoenix land in the 100–101°F bracket?" This signal is how a bot forms an opinion about that — either because the number is already physically locked in and can't change, or because our own bias-corrected forecast says the market's price is wrong.
- Live bots using it
- 4 (the most of any signal)
- Data source
- NWS api.weather.gov · no key
- Refresh
- Every 2 minutes
- Stations
- 20 airports
- Backtest-replayable
- Lock mode yes · forecast-edge no
- Anchor module
- quants/wx_anchor.py
Where the data comes from
All of it comes from the US National Weather Service, free, no API key. NWS asks only that you send a User-Agent identifying yourself, which our pollers do. There are two separate chains, because the two modes of this signal want two different things — what the temperature is, and what it will be.
Chain 1 — what the temperature is right now (lock mode)
predictive/wx_obs_poller.py runs on cron */2 * * * * — every two minutes — and
pulls the observation stream for 20 airport stations from
api.weather.gov/stations/{ICAO}/observations. That's the five-minute ASOS feed: the same
readings the official climate report is eventually built from. If that endpoint fails it falls back to FAA
METAR at aviationweather.gov. Readings land in wx_nws_observations.
The precision detail is what makes this signal work. The poller reads the raw METAR text and labels each
observation by how precise it is: a Txxxxxxxx group gives temperature to a tenth of a degree
Celsius (tenth_c_faa), a six-hour RMK block is the one that locks a day's
extreme (rmk_6h), and everything else is whole-degree (whole_c_metar). Then
predictive/wx_cf6_projector.py runs every five minutes (*/5 * * * *), rolls those
observations up per station per local-standard-time day, and writes
wx_cf6_projections — today's running high and low, plus the
high_locked / low_locked flags, which it sets only when an rmk_6h
observation lands in the right window. wx_cf6_projections is the table the gate actually reads.
The 20 stations are the ones Kalshi settles temperature markets on: KATL, KAUS, KBOS, KDAL, KDCA, KDEN, KHOU, KLAS, KLAX, KMDW, KMIA, KMSP, KMSY, KNYC, KOKC, KPHL, KPHX, KSAT, KSEA, KSFO. Local Standard Time, never daylight saving — that's the climate-report convention, and Phoenix is on MST all year.
Chain 2 — what the temperature will be (forecast-edge mode)
predictive/nws_poller.py runs every fifteen minutes (*/15 * * * *) and fetches the
NWS gridpoint forecast for 19 stations from api.weather.gov/gridpoints/…/forecast. Crucially it
writes a history table, nws_forecast_history — every forecast we ever saw, stamped with
when we saw it. That timestamp is the whole trick: it lets the gate ask "what did NWS say yesterday about
today, before today began?" and compare that against what actually happened, which it reads from
wx_cooling_evolution, our own record of settled daily highs and lows.
The third input has no poller and no table at all: the live market price. yes_bid and
yes_ask are handed to the gate by the trading engine, straight from the Kalshi API at the
moment it evaluates a market. Remember that — it's catch #2.
What it means for your bot
You don't write code. You add wx_anchor to your bot's entry rules and set a few options, and
those options change the strategy completely — same signal name, opposite risk profile.
Lock mode — trade an answer, not a forecast
Set require_locked: true and your bot won't touch a bracket until that day's high or low is
physically incapable of changing. Add bracket_satisfied: true and it goes further: it fires
only on markets where our observed value means your side wins at settlement. That is not
forecasting. That is reading an answer that has already been decided while the market is still pricing it
as an open question.
Add stations: ["KMSP"] to scope a bot to one city. You need this for LOW markets, because
Kalshi's LOW market titles don't contain a city name — a title filter physically cannot narrow them, but the
ticker can, and this gate parses the ticker.
Forecast-edge mode — trade a disagreement
Set forecast_edge: {min_edge: 0.10, sigma_floor: 1.5, spread_max: 0.08} and the gate stops
being a filter and becomes the entire decision — it short-circuits the lock checks above. It takes
yesterday's forecast for today, corrects it by that station's own historical bias (shrunk toward zero so a
noisy sample can't swing it), spreads a normal distribution around it, and integrates over the 2°F bracket
to get our probability. Then it only fires if our probability beats the live ask by min_edge
for YES, or the live bid beats our probability by min_edge for NO. It refuses if the bid-ask
spread is wider than spread_max.
One more you'll want: bracket_only: true restricts the bot to 2°F B brackets and
skips T threshold markets. See catch #4 for why that isn't optional.
The catch
There are five, and every one of them is in the code rather than in our marketing.
1. Our number is not the judge's number
This is the big one. The gate reads our projection, computed by our own five-minute-old rollup of station observations. Kalshi settles on the official NWS climate report. Those usually agree — they're built from the same readings — but they are not the same object, and our projector's own docstring says it's "intentionally simpler" than the full pipeline it was modelled on. So "locked" means we believe the extreme can no longer be beaten, not the official report has been published. A 1°F disagreement is a total loss on a 2°F bracket you thought was already won.
2. A backtest of a forecast-edge bot is not testing the forecast edge
Our signal registry marks wx_anchor backtest-replayable, and for lock mode that's true and
carefully done: pass a timestamp and the gate will only look at projection rows recomputed at or before
it, so it cannot peek at the future.
Forecast-edge mode is a different story. It needs a live bid and ask, and the backtest replay doesn't pass prices — with no prices the check returns false and the bot does nothing. So a backtest that includes a forecast-edge bot is silently testing an inert bot. It is live-only. Judge it on forward results, not on a backtest curve.
3. The bias correction is trained on six days
Forecast-edge corrects each station's forecast using its own recent errors, and needs at least three
samples before it will act. It has more than three — but only barely, and not because the history is
short. nws_forecast_history is pruned to seven days to stop it growing without bound, so no
matter how long we've been running, only the last week of forecasts survives to be scored.
Checked on 2026-08-10: we hold 1,974 settled station-days of actual highs and lows going back to May, but
every one of the 38 station-and-kind cells has exactly 6 usable forecast-versus-actual samples.
Six days is enough to pass the gate's own sanity check and nowhere near enough to characterise a
station's seasonal forecast bias. Treat that sigma_floor as load-bearing.
4. require_locked on its own is not an edge — and two of our live bots do exactly that
require_locked: true narrows you to "days whose extreme is locked". It does not
check that your side wins. That check is a separate option, bracket_satisfied. Without it you
can buy a confidently locked bracket that locked at the wrong number. Our two Weather Lock bots are
configured {kind, require_locked} with no bracket_satisfied — worth knowing
before you fork one.
Separately, T-threshold tickers are genuinely ambiguous: a comment in the source says the
above/below flavour can't be determined from the ticker, so the settlement test can be wrong on them.
That's why bracket_only exists. If you use bracket_satisfied without
bracket_only, you have inherited a known bug on purpose.
5. It is the most-used signal we have, and it is losing money
We'd rather print this than have you find it out with your own bot. Four bots use
wx_anchor. Here is every one of them, per bot, straight from the trade table, over
2026-06-21 to 2026-08-10:
| Bot | Mode | Trades | Wins | P&L |
|---|---|---|---|---|
| WX Forecast Edge (YES) | forecast-edge | 2,040 | 150 / 2,004 | −$3,449.45 |
| WX Forecast Edge (NO) | forecast-edge | 2,040 | 1,067 / 2,001 | −$1,023.97 |
| Weather Lock — Low | lock | 1 | 1 / 1 | +$0.95 |
| Weather Lock — High | lock | 0 | — | — |
Read that honestly: the plumbing works, the gate fires, and the edge hasn't been there. The two
forecast-edge legs trade constantly and both lose — and the YES leg winning 150 of 2,004 tells you it's
firing on cheap long-shot brackets that mostly don't come in. Meanwhile the two lock bots are running on
the same engine and have one trade between them in seven weeks, which is fail-closed plus
require_locked being as restrictive as it sounds. Six more single-city lock bots (NYC high;
Denver, DC, Phoenix, Minneapolis and Austin lows) exist but are archived, not live.
The paper-trading point stands: this cost us nothing to learn. All simulated, flat $5 a bet, no real money anywhere.
Where to go next
See what these feeds say right now on the weather signal board or look up a single city with WX Lookup. If the vocabulary here is new, the weather primer and the glossary come first. When you want to build one, the weather builder adds this signal with a click — no code.
Every claim on this page traces to one of three things: the output of
quants/signal_registry.py, the source of quants/wx_anchor.py and the pollers it
depends on, or a read-only query against our own database with the window stated. Cron schedules were read
from the production crontab. P&L is simulated — TinyCorp bots trade paper money only. Verified
2026-08-10.