Trade Bot Dictionary · Weather

aqi_anchor

aqi_anchor is a master switch wired to how bad the air is in the worst big American city. Every three hours we ask one free air-quality API for the US Air Quality Index in sixteen major metros, keep the highest of the sixteen, and compare it to your number. While the worst city is above your level, the bot trades. While it isn't, the bot places no trades at all that cycle, in any market. The premise is the wildfire-smoke regime: when a big city is choking, air-quality markets move.

Bots using it
0 (none live, none out-of-sample, none ever)
Data source
Open-Meteo Air Quality /v1/air-quality — the registry says “undocumented”
Refresh
Every 3 hours (47 */3 * * *)
Rows stored
242 readings 31 days — the rest is deleted
Backtest-replayable
No — and four years of it are one URL away (catch #5)
Anchor module
quants/aqi_anchor.py

Let me put the shape of this one up front, because it is not the shape of the others. Every setting this site ships for aqi_anchor is the same one — worst-metro AQI above 100 — and it is the first shipped setting in this entire dictionary that is genuinely well-calibrated. Over four years of hourly history it is open 27% of the time. Not a constant, not a switch that has been stuck since 2024, not a threshold the data has never once reached. A real gate with a real duty cycle. After a long run of signals on this shelf whose recommended numbers turned out to be permanently on or permanently off, somebody got this one right.

And it is measuring the wrong thing about half the time it fires. This is also the first signal here that could be checked against actual instruments — 231 EPA air-quality monitors, free and keyless, the same network that produces the official number. Against those monitors the signal has real skill on the thing it was built for: during a genuine multi-city smoke episode in mid-July it was open 96% of the time and the monitors agreed 100%. Day to day it is a coin flip — when the gate opens, the monitors agree 48% of the time — because the model behind it runs about 26 AQI points high in exactly the band where the decision gets made.

And on 4 August a dust storm buried Phoenix. Every monitor in the metro pegged at the top of the AQI scale. Our signal read Phoenix at 62 and the gate stayed shut.

Where the number actually comes from

Every three hours, at 47 minutes past — 47 */3 * * *predictive/aqi_poller.py makes one HTTP request to air-quality-api.open-meteo.com/v1/air-quality, free and with no API key, asking for current=us_aqi at sixteen latitude/longitude pairs at once. I ran the exact URL the poller builds from the production box while writing this: HTTP 200 in 0.69 seconds, sixteen series returned, worst metro Washington DC at 68. The pipe is connected.

The sixteen metros are New York, Los Angeles, Chicago, Dallas–Fort Worth, Houston, Washington DC, Philadelphia, Atlanta, Miami, Phoenix, Boston, San Francisco, Detroit, Seattle, Minneapolis and Denver — the same list, byte for byte, that degree_days_anchor, heat_index_anchor, qpf_anchor and wind_anchor use. Two numbers come out and go into aqi_index in predictive.db: worst_aqi, the single highest of the sixteen, and mean_aqi, a population-weighted average. Both are stored with the name of the worst metro. 242 rows, 14 July to 13 August 2026, all sixteen metros present on every one.

The part that matters: this is a model, not a monitor

Open-Meteo is a distributor, not a measurer. Their documentation names what sits behind the number, and for American cities it is this: CAMS Global Atmospheric Composition Forecasts — 0.4° (~45 km), 3-hourly, August 2022 onwards, updated every 12 hours. The 11 km figure in Open-Meteo's own headline is the European product; the United States is served by the coarse global one. So the "current air quality in Los Angeles" our signal reads is a computed forecast from a weather-chemistry model, averaged over a 45-kilometre square — a cell wider than most of the cities it claims to describe.

That is not a criticism by itself. It is free, it is global, it never has a station outage, and for large smoke plumes it is genuinely the right instrument. But it is a different kind of object from the number on the news, which comes from EPA's monitor network. The docs confirm us_aqi is computed the same way EPA defines it — "the consolidated us_aqi returns the maximum of all individual indices" across PM2.5, PM10, NO₂, ozone, SO₂ and CO. What the docs spell out only for the European index is the averaging convention (PM on a rolling 24 hours, gases hourly); for the US index they are silent, so I am not going to tell you what averaging window sits inside it.

THE DATA PATH — ONE REQUEST, SIXTEEN CITIES, ONE NUMBER CAMS global 45 km grid 3-hourly, 12 h runs a MODEL Open-Meteo /v1/air-quality current=us_aqi free · keyless aqi_poller.py 47 */3 * * * 8 polls a day 466 of 467 OK aqi_index predictive.db 242 rows 31 days kept the gate worst > 100 ? row < 6 h old ? fails closed every trade open → bot runs shut → no trades 16 metros in NY · LA · CHI · DFW · HOU · DC · PHL · ATL · MIA · PHX BOS · SF · DET · SEA · MSP · DEN ↓ keep only the single highest — 15 of the 16 readings are discarded every poll 1 number out 4 AUGUST 2026 — A DUST STORM BURIES PHOENIX AND THE GATE STAYS SHUT 500 300 200 0 US AQI AQI 100 — the gate every monitor: 500 — off the scale our signal never leaves 61–73 21:0000:0003:00 06:0009:0012:00 15:0018:0021:00 UTC, 3 Aug 21:00 → 4 Aug 21:00 EPA monitors, Phoenix metro (median of 25 sites) our signal's reading for Phoenix HOW OFTEN THE SHIPPED SETTING IS OPEN — FOUR YEARS, BY MONTH (THE MODEL'S OWN VIEW) 50%25%0 JanFebMar AprMayJun JulAugSep OctNovDec 9.931.9 44.7 4-yr average 26.99% the two months our only live data covers — 14 Jul to 13 Aug 2026 December — a winter particulate season — is nearly as high as midsummer, and we have no monitor check on it WHICH CITY OPENS THE GATE — PERCENTAGE POINTS OF THE 26.99% DUTY CYCLE LOST IF YOU DROP IT Los Angeles 9.68 pp Phoenix 1.30 Atlanta 1.07 New York 0.75 Houston 0.73 Boston 0.46 Detroit 0.43 Philadelphia 0.41 Chicago 0.40 Washington DC 0.38 Seattle 0.35 Dallas-Ft Worth 0.33 San Francisco 0.33 Denver 0.28 Minneapolis 0.27 Miami 0.15 but not an LA detector an LA-only gate would miss 55.2% of open hours 35.8% of opens are 2+ cities
Top: the data path. One free request every three hours reaches a 45 km chemistry model; sixteen city readings are collapsed to a single maximum, and that one number switches the whole bot on or off. Second: 4 August 2026, the day a dust storm hit Phoenix — the red line is the median of 25 EPA monitors in the metro, the blue line is what our signal read for the same city at the same hours. Third: how often the shipped setting {metric:"worst", above:100} would have been open in each month across four years of archive — this is the model's own view of itself, not verified against monitors, because our monitor comparison only covers July–August. Bottom: how many percentage points of that 27% duty cycle each city is responsible for, measured by dropping it and re-running.

What it does for your bot

It is a global gate, not a filter. It does not pick better markets or size positions — it decides whether the bot runs at all this cycle. The engine reads the newest row of aqi_index, checks it is less than six hours old, compares worst_aqi (or mean_aqi) to your threshold, and if the answer is no it short-circuits the entire strategy. No candidates, no orders, in any category, on either book. The same shape as sentiment_anchor and degree_days_anchor.

There are two metrics. worst is the maximum across the sixteen — "is any big city's air unhealthy?" mean is the population-weighted average — "is the country broadly polluted?" Everything this site ships uses worst.

What we checked it against

EPA publishes every hour of its monitor network as a plain file, free and with no key, at files.airnowtech.org. I pulled 240 of those hourly files covering 14 July to 12 August 2026 — the exact window our own table covers — and for each of the sixteen metros took every US monitor within 40 km. That is 231 monitors: 22 around New York, 25 around Phoenix, 13 around Los Angeles, and so on. For each monitor I took the highest of its reported pollutant indices, capped at 500 (the top of the AQI scale; AirNow's raw files extrapolate past it during extreme events), and then took the median monitor in each metro as that city's observed AQI — the conservative choice, since taking the worst monitor of twenty-five would flatter our signal's misses.

Then I ran the identical rule on both sides: worst of the sixteen cities, above 100. Our side is not the archive or a reconstruction — it is the 242 rows the poller actually wrote. 237 of them paired to an observation hour.

Our stored signal vs 231 EPA monitors, 237 paired samplesValue95% CI
Correlation of worst-of-16, ours vs monitors+0.34
Gate verdicts that agree70.9%64.8 – 76.3
When our gate opens, monitors agree (precision)48.4%38.6 – 58.3
Real unhealthy hours we catch (recall)69.7%57.8 – 79.4
Our gate is open40.1%of samples
Monitors justify it being open27.8%of samples
Cohen's κ (agreement beyond chance)0.362"fair"
Lift — precision ÷ base rate1.74×
We name the right worst city21.1%chance = 6.25%

Read that honestly and it says two things at once. The gate is not noise — κ of 0.36 and a lift of 1.74× mean that when it fires, official exceedance is close to twice as likely as it would be at random. And the gate is not reliable — it fires 1.4× more often than reality warrants, and when it fires it is right about half the time.

The catch

1 🔴 It slept through the worst air in the window — twice

On 4 August 2026 a haboob hit Phoenix. EPA monitors across the metro measured PM10 of 2,300 to 4,841 µg/m³ — all 25 of them, the median as well as the worst, pegged at the top of the AQI scale. Our poller ran at 06:47 UTC that morning and stored worst_aqi = 79 (Denver). The gate was shut. It read Phoenix at 62, and stayed between 62 and 73 for the whole event. The same thing happened again on 6 August.

The mechanism is in the vendor's own documentation, and it is not a bug: a haboob is a wall of coarse dust, tens of kilometres across, lasting an hour or two, thrown up by a downdraft. The model is a 45 km grid on a 3-hourly step, refreshed twice a day. The event is smaller than one cell and shorter than one step. It cannot see it, and no threshold you choose will make it see it. Across the whole window there are 11 samples of 237 where monitors say unhealthy and our signal says fine; 9 of the 11 are Phoenix dust.

2 🔴 It runs high exactly where it makes the decision

The obvious complaint about a coarse model is that it smooths — high in clean air, low in dirty air. The first half is easy to over-read, because comparing a noisy model to a noisy monitor always looks biased when you condition on one of them being low. So I binned it by what the model said, which does not have that problem:

What the model readnMonitors readBias
0 – 40 (good)30128.2+4.9
40 – 601,62838.9+12.9
60 – 801,32949.0+17.8
80 – 100 (just below the line)30372.1+16.2
100 – 130 (the gate opens here)14085.3+25.9
130 +91131.2+24.5

The bias is largest in the band where the gate decides. When the model reads 100–130 — precisely the readings that flip the switch — the monitors average 85. That is why precision is 48%: a large share of the openings are the model's warm bias crossing a line that reality did not cross. Set the same gate at 126 instead of 100 and you would be asking the model the question you meant to ask.

3 🔴 The market exists, we hold it, and the bot we ship cannot see it

Unlike wind_anchor and sst_anchor, this signal has a real market. In July 2026 Polymarket ran eight of them and we ingested all eight: "NYC Air Quality Index below 100 by July 18?" and its siblings for 19, 20 and 21 July, three for Chicago, one for Columbus. Real money — $72,451 of total volume, the busiest single contract $20,466.

Every one of them is filed in our ingest under category other. The starter bot we ship, "Wildfire-Smoke Watch", is configured categories: ['weather']. It could not have traded a single one of them. That makes it the third signal in this dictionary whose one shipped bot is disqualified by the category rule rather than by the data — after tropical_anchor and sst_anchor, which both ask for a category called tropical that our ingest cannot produce. Here the category asked for is real; the markets are simply filed somewhere else.

And repointing it would not save it, because the direction is wrong too. Those contracts pay out when a city's air is below 100. Our starter buys YES when the worst metro is above 100. On Kalshi, KXAQICITY ("AQI in city at time") exists in the catalogue — I checked it live — but it has 0 open markets right now, and we have ingested 0 air-quality rows from Kalshi ever, out of 542,272.

4 🟠 It names the wrong city four times out of five

The signal stores which metro was worst. Against the monitors it gets that right 21.1% of the time — better than the 6.25% you would get by guessing, so there is real skill in it, but four times in five the city it is pointing at is not the city with the problem. Part of that is a genuinely unfair comparison: one 45 km grid cell versus the median of up to 25 monitors spread across a metro is not the same object, and LA's ozone lives inland from the coordinate we sample. But if you were planning to use worst_metro to pick which city's contract to trade, don't.

5 🟠 Not backtest-replayable — and four years of it is one URL parameter away

aqi_anchor appears nowhere in quants/backtest.py — not in the supported list, not even in the documentary _UNSUPPORTED_ENTRY list. It is caught only by the catch-all, so a backtest of a bot using it runs without the gate and still returns a curve, flagging unsupported_rules. The poller's docstring justifies this: "no long archive here, so the signal is live-only."

That is false. The same endpoint takes start_date and end_date. I pulled 2022-08-05 to 2026-08-12 for all sixteen metros — 35,256 complete hourly readings each — in 9.5 seconds, keyless. Replayed against our own live rows the reconstructed gate gives the same verdict 97.0% of the time.

One real caveat, and it is Open-Meteo's own wording: for the United States there is no reanalysis — the European domain has one, the global domain does not — so the archive is archived forecasts, and the model behind it has been upgraded during the period. The underlying numbers differ from what we captured live by a mean of 3.4 AQI points. So it is good enough to backtest the gate, and it is not the same tape the bot would have seen.

6 🟠 One city carries a third of it

Los Angeles is above 100 on 12.09% of all hours by itself. Dropping it from the sixteen takes the duty cycle from 26.99% to 17.32% — 9.68 percentage points. No other city is worth more than 1.30. In 35.9% of the hours the gate is open, LA is the only unhealthy metro on the list.

I originally wrote this catch harder than the data supports, and it is worth saying why. It is tempting to note that an LA-only gate gives the identical verdict on 85% of hours and call the other fifteen cities decorative. That 85% is a trick: both gates are shut 73% of the time, so agreeing on "nothing is happening" does most of the work. An LA-only gate would miss 55.2% of the hours the sixteen-city gate opens, and 35.8% of open hours already have two or more cities unhealthy. LA is the biggest single contributor by a distance; it is not the whole signal.

7 🟡 The timestamp says one thing and means another

The poller asks Open-Meteo for timezone=auto, which returns each city's reading stamped in its own local time. The code stores the first metro's stamp — always New York's — in a column the schema documents as "current-observation time from the API (ISO)". So as_of holds a New York local timestamp carrying no UTC offset, on all 242 rows. Read as UTC, as its neighbouring column fetched_at is, every row looks 4.8 hours stale. It isn't — the readings are simultaneous and current — and in winter the apparent lag will silently become 5.8 hours when the clocks change. Nothing currently reads this column, which is the only reason it is a 🟡.

8 🟡 Settings that validate and can never open

validate_config is stricter than most on this shelf — it correctly rejects metric:"max", a missing condition, booleans and negatives. It still accepts:

  • {above:100, below:50} — unsatisfiable, and to_english renders it as "> 100 and < 50" without complaint. The same defect finance_anchor has.
  • {above:600} — the US AQI scale stops at 500. Permanently shut, no warning.
  • {below:0} — permanently shut. {above:0} — permanently open.

9 🟡 The 30-day prune keeps 31 days

DELETE FROM aqi_index WHERE fetched_at < datetime('now','-30 days') compares an ISO string (2026-07-14T00:47:01+00:00) against SQLite's space-separated format (2026-07-14 14:33:21) as text. Because 'T' > ' ', the cut-off day never prunes: 31 distinct dates are kept, 5 rows outlive a real 30-day test. Harmless here, and the eleventh poller in this dictionary with the identical bug. Not fixed — this loop does not touch pollers.

The case for the defence

  • It caught the real thing. Between 17 and 19 July a genuine multi-city smoke episode crossed the eastern US — NYC monitors peaked at 289, Chicago and Columbus at the top of the scale. Our gate was open on 95.7% of those samples and the monitors justified it 100% of the time. This is what the signal is for, and on the one occasion it was asked, it did it.
  • The shipped threshold is well-chosen — 26.99% over four years, ranging 9.9% in March to 44.7% in August. Where other signals on this shelf ship settings that are constants on the data we hold, this one genuinely discriminates.
  • The pipeline is nearly spotless — 466 successful runs out of 467, a single database is locked crash, all 16 metros present on all 242 rows, no NULLs. The _MIN_METROS = 8 guard has never had to fire.
  • The freshness check is correctly written — it parses the ISO timestamp into a real timezone-aware datetime and fails closed on anything unparseable, rather than comparing strings the way finance_anchor and tropical_anchor do. It has essentially never had to act: the three largest gaps in 242 polls are 21,601s, 21,600s and 21,599s against a 21,600-second threshold — one missed poll, landing on the knife edge.
  • A coarse model is the right instrument for the job it was given. Missing a ninety-minute dust wall and catching a three-day continental smoke plume is not incompetence; it is what a 45 km grid with satellite assimilation is built to do. If what moves a market is "is bad air a national story", the failures above are mostly the wrong scorecard.

What this page does not tell you

The record

There isn't one. Zero bots use aqi_anchor — 0 of 76 strategy configs (21 live, 15 out-of-sample, 36 archived, 4 draft), 0 of 88 strategy versions, 0 of 1,064 stored genome configurations. No trades, no fills, no evaluations, no P&L. We have polled this feed eight times a day since 14 July and nothing has ever read it.

The registry resolves this signal almost correctly — poller aqi_poller, cron 47 */3 * * *, table aqi_index, replayable: false, and live_bots: 0 where 0 genuinely means zero. The one field it gets wrong is the source, which it reports as "undocumented" even though both the anchor and the poller name Open-Meteo in plain English in their opening lines — the registry's SOURCE_HINTS table still has no Open-Meteo entry, a gap first noted on the heat_index page and carried by five pages since.

Written 13 August 2026. Every number here traces to quants/signal_registry.py, the source of quants/aqi_anchor.py and predictive/aqi_poller.py, a read-only query against predictive.db, Open-Meteo's own documentation, or the EPA AirNow hourly observation files at files.airnowtech.org — all read while writing this page. Live-window figures cover 14 July – 13 August 2026; archive figures cover 5 August 2022 – 12 August 2026. Nothing here is estimated or extrapolated, and where I could not measure something I have said so.