NOAA's National Blend of Models: What It Is and Why We Switched to It
TL;DR / Key Takeaways
- The National Blend of Models (NBM) is NOAA's operational multi-model ensemble, combining GFS, ECMWF, HRRR, and others with statistical post-processing and bias correction already applied.
- Raw model output (GFS, ECMWF) is not calibrated. NBM is. That difference matters enormously when you're converting a forecast into a probability.
- We rebuilt the Weather Bot around NBM after a 112-trade audit showed our custom ensemble had no predictive skill — Brier score 0.2858 vs. 0.2439 for guessing the base rate.
- NBM data is free, covers the exact stations Kalshi settles on, and is accessible via HTTPS with no API key required.
Why I'm Writing This
When I rebuilt the Weather Bot after the v2.1 post-mortem, the core decision was simple: stop hand-rolling a calibration layer on top of raw model output and use NOAA's NBM instead. NOAA already does this, professionally, for free, for exactly the weather stations we care about.
But "just use NBM" is a hand-wave without explaining what NBM actually is and why calibration matters. So this post is that explanation. If you're building anything that converts weather forecasts into probabilities, you need to understand what you're working with before you write a single line of code.
What the National Blend of Models Actually Is
NBM is NOAA's operational post-processed multi-model ensemble. It's been running in production since 2017 and is updated multiple times per day. The short version: it takes output from a large collection of numerical weather prediction models, combines them, applies statistical bias correction, and produces calibrated probabilistic forecasts.
The models that feed into NBM include:
- GFS (Global Forecast System) — NOAA's global model, 0.25-degree resolution
- ECMWF — European Centre for Medium-Range Weather Forecasts, generally considered the best global model in the world
- HRRR (High-Resolution Rapid Refresh) — NOAA's convection-allowing model, 3km resolution, excellent for short-range
- NAM (North American Mesoscale) — regional model, 12km resolution
- HREF (High-Resolution Ensemble Framework) — multi-model ensemble for short-range convective forecasting
- Regional and mesoscale models as appropriate
NBM doesn't just average these. It applies a statistical post-processing step called Model Output Statistics (MOS) and more recently machine-learning-based Quantile Mapping to correct known biases in each model and calibrate the combined probability distribution against historical observations.
The output is a set of probabilistic forecasts at specific locations — not grid cells, but point forecasts for actual observing stations. Including the stations Kalshi uses for temperature contract settlement.
Calibrated vs. Uncalibrated: Why This Distinction Wrecked Us
This is the part that matters most for anyone trying to turn a weather forecast into a trading probability.
Raw model output is not calibrated. When GFS says the probability of exceeding 90°F is 70%, that number was not derived from any comparison against observed outcomes. It's a deterministic or ensemble-spread estimate. It can be systematically biased — warm-biased, cold-biased, overconfident, underconfident — depending on the region, season, and forecast hour.
A calibrated probability means something specific: if the model says 70% 10,000 times, it should have been right roughly 7,000 times. That's the standard. NOAA measures this. They have decades of observations to verify against, and they tune NBM to meet it.
Our old ensemble was not calibrated in this sense. We were combining 164 members across 4 model families and then converting the ensemble spread into a probability using a custom formula. The result looked like probabilities. They were not. Our model was producing 95%+ confidence estimates and winning about 60% of the time. That's not a rounding error. That's structural overconfidence.
The v2.1 Brier score tells the story: 0.2858 for our model, 0.2439 for simply guessing the historical base rate. We were worse than doing nothing. The fix wasn't a smarter ensemble formula. It was using forecasts that were already calibrated by people who have been doing this professionally for decades.
The Specific NBM Products We Use
NBM publishes forecasts in GRIB2 format via the NOAA Operational Model Archive Distribution System (NOMADS) and also via the NBM text bulletin format. We use the text bulletins for temperature because they're easier to parse and provide exactly what we need: point forecasts for specific stations with quantile information.
The relevant NBM product for temperature is NBM v4.2 (and later), specifically:
- T2m (2-meter air temperature) at hourly and 6-hourly resolution
- MaxT and MinT (daily maximum and minimum temperature) with percentile forecasts
- Station identifiers that map directly to ICAO codes
Kalshi settles temperature contracts on daily high temperature at specific airports. NBM publishes MaxT forecasts with full percentile distributions — 10th, 25th, 50th, 75th, 90th percentiles — for exactly those airports. That's the number the contract settles on. That's what we need.
Accessing NBM Data: The Actual API
NBM text bulletins are available via HTTPS. No API key. No authentication. No rate limiting that I've hit in practice.
The base URL for NBM text bulletins:
https://blend.nomads.ncep.noaa.gov/blend.YYYYMMDD/tHHz/text/blend_nbhtx.t{HH}z
Where:
YYYYMMDDis the run dateHHis the model run hour (00, 06, 12, 18)
Here's a minimal Python function to fetch and parse the NBM text bulletin for a target station:
import requests
import re
from datetime import datetime, timezone
NBM_BASE = "https://blend.nomads.ncep.noaa.gov/blend.{date}/t{hour:02d}z/text/blend_nbhtx.t{hour:02d}z"
def fetch_nbm_bulletin(run_date: datetime, run_hour: int) -> str:
"""
Fetch the NBM text bulletin for a given model run.
run_date: datetime object (UTC)
run_hour: 0, 6, 12, or 18
Returns raw text content.
"""
url = NBM_BASE.format(
date=run_date.strftime("%Y%m%d"),
hour=run_hour
)
resp = requests.get(url, timeout=30)
resp.raise_for_status()
return resp.text
def parse_nbm_maxt(bulletin_text: str, station_id: str) -> dict | None:
"""
Extract MaxT percentile forecasts for a given station from NBM bulletin.
station_id: 4-character ICAO code (e.g. 'KMDW' for Chicago Midway)
Returns dict with percentile keys, or None if station not found.
"""
lines = bulletin_text.splitlines()
# Find the station block
station_pattern = re.compile(rf"^{station_id}\s", re.IGNORECASE)
station_start = None
for i, line in enumerate(lines):
if station_pattern.match(line):
station_start = i
break
if station_start is None:
return None
# NBM text format: rows are elements, columns are forecast hours
# MaxT rows are labeled with 'MXT'
maxt_data = {}
for line in lines[station_start:station_start + 80]:
if line.strip().startswith("MXT"):
# Parse percentile rows: MXT/10, MXT/25, MXT/50, MXT/75, MXT/90
parts = line.split()
if len(parts) > 2:
label = parts[0] # e.g. 'MXT/50'
percentile = label.split("/")[-1] if "/" in label else "50"
try:
maxt_data[f"p{percentile}"] = [int(v) for v in parts[1:] if v.lstrip("-").isdigit()]
except ValueError:
continue
elif maxt_data and not line.strip().startswith("MX"):
# End of MXT block
break
return maxt_data if maxt_data else None
This is simplified. The actual NBM text format has quirks — fixed-width columns, continuation lines for longer forecast periods, and some stations that appear multiple times in a bulletin. But this gives you the shape of it.
Station Mapping: The Part That Actually Bites You
Fetching the data is the easy part. Using the right station identifier is where things break.
Kalshi settles Chicago temperature contracts on Midway (KMDW), not O'Hare (KORD). Houston settles on Hobby (KHOU), not Bush Intercontinental (KIAH). These are different weather stations that can have meaningfully different temperatures, especially if one is closer to water or urban heat effects.
We discovered the correct stations by querying Kalshi's own metadata API rather than assuming. Here's the query:
import requests
KALSHI_API_BASE = "https://api.elections.kalshi.com/trade-api/v2"
def get_market_settlement_details(ticker: str, api_key_data: dict) -> dict:
"""
Pull settlement details for a Kalshi market.
Returns the full market object including settlement source.
"""
headers = {
"Content-Type": "application/json",
# RSA-PSS auth headers go here — see SETUP_KEYS.md
}
resp = requests.get(
f"{KALSHI_API_BASE}/markets/{ticker}",
headers=headers,
timeout=10
)
resp.raise_for_status()
return resp.json().get("market", {})
Once you have the settlement details, you can confirm the station name in the settlement_sources field and cross-reference it against NOAA's station list to get the correct ICAO code. Then pin that mapping in your tests so it never silently drifts.
# In your test suite — this should fail loudly if the mapping changes
KNOWN_STATION_MAPPINGS = {
"Chicago": "KMDW", # Midway, not O'Hare
"Houston": "KHOU", # Hobby, not IAH
"New York": "KNYC", # Central Park
"Los Angeles": "KLAX",
# ... all markets confirmed against Kalshi metadata
}
def test_station_mappings_are_confirmed():
for city, icao in KNOWN_STATION_MAPPINGS.items():
assert icao in CONFIRMED_KALSHI_STATIONS, (
f"{city} maps to {icao} but this hasn't been confirmed "
f"against Kalshi metadata. Verify and add to confirmed list."
)
Why NBM Beats Hand-Rolling an Ensemble
If you've been doing ML work for a while, the instinct is to build your own calibration layer. Pull the raw model data, train a correction on historical residuals, ship it. That's reasonable instinct. It's also what we did with v2.1, and it failed.
The problem isn't the approach. It's the scale of data required to calibrate well. NOAA has been collecting weather observations since before most of us were born. Their calibration is trained on decades of verifications across every weather regime, every season, every geographic region. Our 4 months of trades and however many historical API calls we could backfill is not competitive with that.
There's also the maintenance burden. Model bias drifts as NOAA updates GFS, ECMWF updates their model, seasons change. NOAA's calibration is updated continuously. Your custom layer is updated when you have time.
The right answer is to use the calibrated output and spend your engineering time on the trading logic — position sizing, market selection, edge detection — not on rebuilding meteorology infrastructure that NOAA provides for free.
NBM covers 4,228 stations in its text bulletins. Kalshi's temperature markets use a small subset of those. Every station we care about is in there, already bias-corrected, already calibrated, already available on a schedule that predates contract expiry by enough hours to act on.
The Remaining Uncertainty: What NBM Doesn't Solve
NBM is not a crystal ball. Calibrated probabilities are still probabilities.
A few things NBM doesn't fix:
Short-range convective events. If a thunderstorm rolls through and drops the afternoon high by 8 degrees, NBM's MaxT forecast from 18 hours earlier may have had wide uncertainty bounds — but the market may not have priced that uncertainty correctly either. This is where potential edge lives, but it's also where you can be confidently wrong.
Lead time degradation. NBM accuracy drops as you go further out. For temperature, 24-hour forecasts are materially better than 5-day forecasts. We only trade contracts settling within a forecast window where the skill is reasonable.
Ensemble spread is not the same as probability. Even with NBM's percentile forecasts, converting "the 75th percentile MaxT is 89°F and the contract strikes at 90°F" into a trade decision requires additional logic. The calibration tells you the forecast is honest. It doesn't tell you what the market is mispricing.
That last point is the actual job of the bot, and it's still being validated.
Current Status
The Weather Bot is rebuilt around NBM, fixed seven technical defects found in the audit, and is in paper-trading mode. We have not validated whether the strategy finds edge against market pricing. That takes time — roughly two weeks to assess forecast quality, four to six months and 100+ completed trades to assess whether the overall approach works.
We're not there yet. "Rebuilt and undergoing validation" is the accurate description. Using calibrated forecasts is a necessary precondition for the strategy to work. It's not sufficient on its own.
The data pipeline is solid. The question the bot is now in position to answer is whether Kalshi's market pricing ever diverges enough from NBM's calibrated output to generate a systematic edge. We'll know when we know.
NBM gave us the right foundation. Whether we built the right structure on top of it is still an open question.
NBM documentation and bulletin access: https://blend.nomads.ncep.noaa.gov. The technical specification for the text bulletin format is in the NBM v4.2 Technical Documentation, available via NOAA's National Weather Service.