Cliff Horizon logo

Layer 3 — Meteorological Data

Public NWP model data — GFS, ECMWF, HRRR, and NWS-LAMP — plus observed data sources for all weather variables.

Layer 3 is the foundation: public numerical weather prediction (NWP) model output and observed ground truth. This is the same data available to everyone — the engine's value is in what it does with it, not in exclusive access.

NWP Models

GFS (Global Forecast System)

AttributeDetail
OperatorNOAA/NCEP
Resolution0.25 deg (~28km) global
Ensemble members31 (GEFS)
Update frequency4 runs/day (00Z, 06Z, 12Z, 18Z)
Forecast horizon384 hours (16 days)
AccessNOMADS server, Open-Meteo API
CostFree, no API key
Variables usedtemperature_2m_max, temperature_2m_min, precipitation_sum, wind_speed_10m_max, wind_gusts_10m_max, shortwave_radiation_sum

GFS is the primary ensemble source for probability estimation. The 31-member GEFS ensemble provides the base probability distribution for all five weather variables.

ECMWF (European Centre for Medium-Range Weather Forecasts)

AttributeDetail
OperatorECMWF
Resolution0.25 deg (operational), variable (ensemble)
Ensemble members51
Update frequency2 runs/day (00Z, 12Z)
Forecast horizon15 days
AccessOpen Data (free subset) via Open-Meteo
CostFree (Open Data tier); paid for full resolution
Engine model IDecmwf_ifs025

ECMWF is widely regarded as the best global NWP model. The 51-member ensemble provides richer probability information than GFS. The engine uses both GFS and ECMWF with model-specific bias correction and pooled member counting (82 total members).

HRRR (High-Resolution Rapid Refresh)

AttributeDetail
OperatorNOAA/NCEP
Resolution3km (CONUS only)
Ensemble membersDeterministic (single run)
Update frequencyHourly
Forecast horizon48 hours
AccessNOMADS, Open-Meteo
CostFree

HRRR provides the highest spatial resolution for US locations. It's deterministic (no ensemble), so it's used as a high-resolution point estimate rather than a probability source.

NWS-LAMP (Localized Aviation MOS Program)

AttributeDetail
OperatorNOAA/MDL
ResolutionStation-level (2000+ US stations)
Update frequencyHourly
Forecast horizon38 hours
AccessFTP (tgftp.nws.noaa.gov), LAMP web services
PurposeBaseline to beat — LAMP is the benchmark for short-range station-level forecasts

LAMP is the most directly comparable public forecast to what the engine produces. If the engine can't beat LAMP at D+1 station-level temperature forecasting, the bias correction and ensemble weighting aren't adding value.

LAMP access: The engine parses the live LAMP bulletin at lamp.mdl.nws.noaa.gov/lamp/Data/bull/lavlamp.txt — a single ~5 MB text file containing all ~2,000 CONUS stations, updated hourly. The parser extracts the TMP row (hourly temperature in degrees F) for each ICAO station and derives the daily high from the maximum across forecast hours. Implementation: src/data_ingestion/lamp_forecast.py.

Observed Data (Ground Truth)

IEM (Iowa Environmental Mesonet)

The primary source for US station data. Free, clean, station-level ASOS/AWOS data covering all major airport stations with decades of history. Used for all non-irradiance variables.

API: mesonet.agron.iastate.edu/cgi-bin/request/daily.py

IEM API quirks:

  • Requires state-specific network codes (e.g., NY_ASOS, IL_ASOS)
  • Uses 3-letter station identifiers (strip leading K from ICAO: KORDORD)
  • Returns ALL columns regardless of the vars parameter
  • Column names differ from vars parameter names:
    • max_temp_f (not max_tmpf)
    • min_temp_f (not min_tmpf)
    • precip_in (not precip)
    • max_wind_speed_kts (not max_sknt)
    • max_wind_gust_kts (not gust_sknt)

Variables downloaded from IEM:

FunctionVariableOutput ColumnUnit
download_daily_highs()Daily max temperaturemax_temp_f°F
download_daily_lows()Daily min temperaturemin_temp_f°F
download_daily_precip()Daily precipitationprecip_inchesinches (renamed from precip_in)
download_daily_max_wind()Max sustained windmax_wind_mphmph (converted from max_wind_speed_kts * 1.15078)
download_daily_max_gust()Max wind gustmax_gust_mphmph (converted from max_wind_gust_kts * 1.15078)

All implemented in src/data_ingestion/iem_observed.py.

ERA5 Reanalysis

IEM has no solar radiation data. The engine uses ERA5 reanalysis as independent ground truth for irradiance.

AttributeDetail
SourceOpen-Meteo ERA5 Archive API
URLarchive-api.open-meteo.com/v1/archive
Variableshortwave_radiation_sum (MJ/m^2)
IndependenceERA5 assimilates millions of global observations — its irradiance values are independent of GFS/ECMWF forecasts

Implementation: download_era5_irradiance() in src/data_ingestion/open_meteo_reanalysis.py.

NWS Climatological Reports

The official settlement source for ForecastEx contracts. Published the morning after each observation day. Contains verified maximum/minimum temperature for the official reporting period.

Phase 0 — Baseline Accuracy (All Variables)

Temperature — Daily High (GFS)

Period: January 1 – March 31, 2026 (90 days, 10 cities, 900 city-days)

StationCityMAE (°F)Bias (°F)RMSE (°F)
KNYCNew York City1.41-0.771.99
KORDChicago1.05+0.011.38
KLAXLos Angeles0.99-0.221.32
KDFWDallas1.11-0.961.35
KIAHHouston1.00-0.551.41
KPHXPhoenix1.47-1.451.78
KDENDenver1.17-0.681.49
KMIAMiami1.34-1.221.61
KATLAtlanta1.58-1.421.83
KSFOSan Francisco1.33-0.131.67
Overall1.24-0.741.60

Key findings:

  • Consistent cold bias — GFS underforecasts daily highs at 9/10 cities (mean bias -0.74°F)
  • City-specific patterns — Phoenix (-1.45°F), Atlanta (-1.42°F), and Miami (-1.22°F) have the largest biases
  • NYC has highest RMSE (1.99°F) — the most variable forecast errors, making it the best ForecastEx market for probability-based trading
  • Chicago is near-zero bias (+0.01°F) — hardest city to improve upon with correction alone

Rainfall (GFS precipitation_sum vs IEM precip_in)

Period: Jan–Mar 2026, 10 cities.

MetricValue
MAE0.036 inches
Bias-0.0018 inches

Low MAE reflects the preponderance of dry days. GFS quantitative precipitation forecasts are essentially unbiased at the daily level, though individual storm events can have large errors.

Wind Speed (GFS wind_speed_10m_max vs IEM max_wind_speed_kts)

Period: Jan–Mar 2026, 10 cities.

MetricValue
MAE7.16 mph
Bias+7.09 mph (massive positive bias)

GFS 10m grid-average wind systematically overforecasts compared to IEM station-level sustained wind. This is a known scale mismatch: GFS represents the average wind over a ~28km grid cell, while ASOS measures point observations at a single anemometer. The multiplicative correction ratio of ~1.61x addresses this effectively.

Irradiance (GFS shortwave_radiation_sum vs ERA5)

Period: Jan–Mar 2026, 10 cities.

MetricValue
MAE1.49 MJ/m^2
Bias-0.13 MJ/m^2

GFS irradiance forecasts have a small negative bias against ERA5 reanalysis. The engine normalises to a clear-sky index (CSI = actual / theoretical clear-sky) before applying the Beta distribution, which handles the bounded [0, 1] domain naturally.

Phase 1 — Bias Correction Results (All Variables)

Temperature — Additive Correction

Per-city, per-month additive bias correction trained on Jan–Feb 2026, tested on March 2026:

StationCityRaw MAECorrected MAERaw BiasCorrected Bias
KATLAtlanta1.270.93-1.16+0.38
KDENDenver1.491.28-0.58+0.15
KDFWDallas1.120.91-0.90+0.09
KIAHHouston1.170.97-0.88-0.49
KLAXLos Angeles0.920.89-0.20+0.03
KMIAMiami1.380.98-1.30-0.13
KNYCNew York City1.601.65-0.62+0.24
KORDChicago1.021.13-0.52-0.80
KPHXPhoenix1.420.61-1.41+0.06
KSFOSan Francisco1.221.25+0.18+0.48
Overall1.261.06-0.740.00

Key findings:

  • Bias eliminated — overall reduced from -0.74°F to 0.00°F
  • MAE improved 16% — from 1.26°F to 1.06°F
  • NYC and Chicago slightly worse — training bias overshoots for March. Suggests seasonal or regime-specific correction needed.
  • Correction is simplecorrected = raw + bias, per-city, per-month

Rainfall — Multiplicative Correction

ratio = mean(observed_wet) / mean(forecast_wet)   per (station, month)
corrected = raw * ratio

With p_zero_obs and p_zero_fct stored for zero-inflated Gamma probability computation. The multiplicative method preserves the zero bound (additive correction can produce negative rainfall).

Wind — Multiplicative Correction

Same method as rainfall. The large scale mismatch (GFS overforecasts by ~7 mph) is captured by the ratio parameter, which is typically ~0.6 (GFS values need to be scaled down to match station observations).

Irradiance — Multiplicative Correction

Applied to raw MJ/m^2 values before clear-sky index computation. Small correction needed (GFS irradiance bias is only -0.13 MJ/m^2).

Phase 1 — Calibration Scores (All Variables)

VariableBrier (Raw)Brier (Calibrated)ImprovementDistributionBias Method
Temperature (DH)0.03740.0349+0.0025GaussianAdditive
Rainfall0.04030.0292+0.0111Gamma (zero-inflated)Multiplicative
Wind Speed0.05140.0462+0.0052WeibullMultiplicative
Irradiance0.05300.0498+0.0032Beta (CSI)Multiplicative

All variables comfortably pass the Brier < 0.10 target. Rainfall shows the largest calibration improvement, likely because the zero-inflated distribution introduces systematic raw probability biases that isotonic regression corrects effectively.

Access via Open-Meteo

Rather than pulling raw GRIB2 files from NOMADS, the engine uses the Open-Meteo API — a free service that provides parsed, JSON-formatted NWP data with no API key required.

This simplifies the ingest pipeline significantly: instead of downloading, parsing, and spatially interpolating multi-gigabyte GRIB2 files, the engine makes REST API calls for specific locations and variables.

Three endpoints used:

EndpointPurpose
ensemble-api.open-meteo.com/v1/ensembleLive GFS 31-member + ECMWF 51-member ensemble
historical-forecast-api.open-meteo.com/v1/forecastArchived deterministic forecasts for backtesting
archive-api.open-meteo.com/v1/archiveERA5 reanalysis for irradiance ground truth

The download_historical_forecasts() function in src/data_ingestion/open_meteo.py accepts configurable variables, unit_params, and value_columns parameters, allowing it to serve all registered weather variables through a single function. The download_ensemble_forecast() function similarly accepts a value_col parameter for generalized column naming.