Trade Bot Dictionary · Weather

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
How wx_anchor gets from a weather station to a trade decision Three inputs feed the wx_anchor gate. Lock mode: the NWS station observations endpoint is polled every two minutes by wx_obs_poller into wx_nws_observations, then wx_cf6_projector rolls it up every five minutes into wx_cf6_projections. Forecast-edge mode: the NWS gridpoint forecast is polled every fifteen minutes by nws_poller into nws_forecast_history, scored against our own settled highs and lows in wx_cooling_evolution. Live prices come straight from the Kalshi API with no poller and no table. The gate then either trades or, by default, skips the market. SOURCE POLLER · CRON TABLE IN predictive.db GATE DECISION LOCKMODE FCASTEDGE PRICE api.weather.gov /stations/{ICAO}/ observations 5-min ASOS · no API key 20 airport stations fallback: FAA METAR wx_obs_poller */2 * * * * every 2 min wx_cf6_projector */5 * * * * rolls up wx_cf6_ projections projected_high/low_ precise_f high_locked · low_locked ← wx_nws_observations api.weather.gov /gridpoints/../ forecast day-ahead high & low no API key 19 stations nws_poller */15 * * * * every 15 min pruned to 7 days — see catch #3 nws_forecast_ history every forecast we saw, with when we saw it wx_cooling_evolution our settled truth Kalshi API live yes_bid / yes_ask no poller · no table wx_anchor parse ticker → station + date + bracket FAILS CLOSED any missing input → don't trade. It never guesses a temperature. TRADE flat $5, paper money SKIP the default answer Every arrow is a real cron job on the production box. Nothing on this path is paid for, and nothing needs an API key. The dashed box is the one input with no storage — it is read live at decision time, which is why forecast-edge mode can't be backtested.
Three inputs, one gate, two answers. Read it left to right: a public government endpoint, a cron job that fetches it, a table it lands in, and the gate that turns it into a yes-or-no on one specific market.

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:

BotModeTradesWinsP&L
WX Forecast Edge (YES)forecast-edge2,040150 / 2,004−$3,449.45
WX Forecast Edge (NO)forecast-edge2,0401,067 / 2,001−$1,023.97
Weather Lock — Lowlock11 / 1+$0.95
Weather Lock — Highlock0

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.