Documentation

Technical reference for Grid Poet

1. DA Price Forecast

Grid Poet forecasts day-ahead electricity prices for the DE-LU bidding zone (Germany + Luxembourg). The forecast is generated daily at 07:00 CET — well before the EPEX SPOT order book close at 12:00 CET — and predicts 24 hourly prices for the next delivery day (D+1).

The model outputs three quantiles: P10 (10th percentile, lower bound), P50 (median, point forecast), and P90 (90th percentile, upper bound), forming an 80% prediction interval.

1.1 Pipeline Flow

The daily forecast pipeline runs in five stages:

07:00 CET — Daily Forecast Pipeline ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ Energy-Charts SMARD Open-Meteo DA prices Generation mix Weather fcst (D-10 → D-1) Consumption 6 locations hourly EUR/MWh Residual load (D-10 → D+1) └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ └────────────────┬───────┴────────────────────────┘ ┌────────────────────────┐ 1. FETCH & MERGE Align to UTC hourly Deduplicate, outer join on utc_start └───────────┬────────────┘ ┌────────────────────────┐ 2. FEATURE ENGINEERING Weather aggregation Calendar features Price/gen lags (D-1) Derived (wind³, solar) └───────────┬────────────┘ ┌────────────────────────┐ 3. PREDICT LightGBM × 3 models → P10, P50, P90 for 24 delivery hours └───────────┬────────────┘ ┌────────────────────────┐ 4. STORE JSON per delivery day data/da_forecasts/ └────────────────────────┘ 13:30 CET — Actuals Scoring ┌────────────────────────┐ Energy-Charts Actual DA prices (published after noon) └───────────┬────────────┘ ┌────────────────────────┐ 5. SCORE MAE, RMSE, bias 80% coverage check Update forecast JSON └────────────────────────┘
The forecast is generated before the EPEX SPOT day-ahead auction closes at 12:00 CET. All input data is available by 07:00 CET: historical prices up to D-1, SMARD generation up to D-1, and weather forecasts for D+1.

1.2 Data Sources

Source Data Resolution Auth
Energy-Charts DA prices (DE-LU bidding zone) Hourly None
SMARD Generation by source, consumption, residual load Hourly None (CC BY 4.0)
Open-Meteo Wind speed (100m), radiation, temperature, cloud cover Hourly None

Weather Locations

Weather data is fetched from 6 representative locations across Germany, weighted by installed renewable capacity:

LocationCoordinatesRepresents
North Sea54.5°N, 7.0°EOffshore wind cluster
North53.5°N, 10.0°EOnshore wind (Schleswig-Holstein, Niedersachsen)
Central West51.5°N, 7.5°ENRW industrial demand, onshore wind
Central East51.3°N, 12.4°ESachsen-Anhalt / Brandenburg wind+solar
South West48.8°N, 9.2°EBaden-Württemberg solar
South East48.1°N, 11.6°EBavaria solar

Wind speed is weighted by installed wind capacity (offshore/onshore splits). Solar radiation is weighted by installed PV capacity (concentrated in southern Germany). Temperature uses equal weighting.

1.3 Features (~30 total)

Weather (D+1 forecast)

  • wind_speed_weighted — capacity-weighted wind speed at 100m hub height
  • radiation_weighted — capacity-weighted direct solar radiation
  • temp_weighted — average temperature across locations
  • cloud_weighted — average cloud cover
  • wind_north_south_gradient — spatial wind difference (congestion proxy)
  • radiation_north_south_gradient — spatial radiation difference

Price Lags (strict no-leakage: D-1 and earlier)

  • price_d1_same_hour — same hour yesterday
  • price_d7_same_hour — same hour one week ago
  • price_d1_mean/min/max — yesterday's daily statistics
  • price_7d_rolling_mean/std — 7-day rolling window

Generation Lags (D-1)

  • gas_share_d1, coal_share_d1, renewable_share_d1
  • consumption_d1, residual_load_d1
  • gas_to_coal_ratio — implicit fuel economics proxy (no direct gas/coal/carbon price API)

Calendar

  • hour, day_of_week, month
  • is_weekend, is_holiday (all 16 German federal states), is_bridge_day
  • hour_sin/cos, month_sin/cos — cyclical encodings

Derived

  • wind_power_proxy — wind speed cubed (cubic power law, capped at 25 m/s cut-out)
  • solar_cf_proxy — radiation × temperature derating (-0.4%/°C above 25°C)
  • residual_x_peak — residual load × peak hour interaction
No-leakage guarantee: All lag features use shift(24) — only data from D-1 or earlier. Weather features use D+1 forecasts (available morning of D). No feature uses actual data from the delivery day.

1.4 Model

LightGBM gradient-boosted decision trees with quantile regression objective. Three separate models predict P10, P50 (median), and P90, sharing the same feature set.

A single model covers all 24 hours with hour as a feature, rather than 24 separate hour-specific models. This gives the model more training data per hour and lets it learn cross-hour patterns.

Key hyperparameters

ParameterValueNotes
n_estimators1000 (early stopping)Stops if validation loss plateaus for 50 rounds
learning_rate~0.05–0.10Tuned via Optuna
max_depth4–7Shallow trees to reduce overfitting
num_leaves30–63Controls tree complexity
subsample0.6–0.8Row sampling per tree
colsample_bytree0.5–0.8Feature sampling per tree

Training data

Historical data from January 2024 to present (~19,000 hourly samples). Walk-forward evaluation: train on all data except the last 30 days, test on the final 30 days.

Hyperparameters are tuned using Optuna (50 trials) to minimize MAE on the test set. Models are retrained monthly to capture seasonal patterns and structural changes (capacity additions, market reforms).

1.5 Evaluation

Metrics

MetricDescription
MAEMean absolute error — average distance between forecast and actual price
RMSERoot mean squared error — penalises large errors more heavily
Mean biasAverage (forecast − actual). Positive = systematically overestimating
80% coverageFraction of actuals falling within the P10–P90 interval. Target: 80%
Skill score1 − (MAE / baseline_MAE). Positive = better than persistence (same-hour D-1)
Pinball lossProper scoring rule for quantile forecasts (lower = better calibrated)

Baseline

The persistence baseline predicts tomorrow's price = today's price for the same hour. This is the simplest reasonable forecast and the benchmark to beat. A skill score of 0.40 means the model reduces MAE by 40% compared to persistence.

Performance metrics and detailed error analysis are available on the forecast page.

1.6 Timestamp Conventions

Power market timestamps are interval-aligned: 14:00 means the interval from 14:00 to 15:00. All internal timestamps are left-aligned naive UTC (utc_start).

SourceNative formatNormalisation
Energy-ChartsUnix seconds (left-aligned)Direct conversion to UTC
SMARDUnix milliseconds (left-aligned)÷1000 to seconds, then UTC
Open-MeteoISO 8601 in requested timezoneRequest in UTC, no conversion needed

DST transitions are handled explicitly: a CET delivery date may have 23, 24, or 25 hours. The function delivery_date_hours() computes the correct UTC hours for any given Berlin-time date.

2. Grid Poet Image Pipeline

The image pipeline runs hourly and transforms live grid data into AI-generated artwork:

  1. Fetch — current generation mix, consumption, weather from SMARD + Open-Meteo
  2. Analyse — Claude AI writes an analysis, poem, and image prompt based on the grid state
  3. Generate — FLUX.2 Pro creates a painting in the 19th-century German Romantic landscape tradition
  4. Publish — image + metadata saved, served on the main page

Each image captures a moment in Germany's energy transition — the interplay of wind, solar, fossil fuels, and weather — through the lens of landscape painting.

3. Captured Prices & Cannibalization

The Analytics page shows captured prices and generation-vs-price scatter plots using data from data/da_history.parquet, which is refreshed daily at 02:00 CET.

Data sources (from APIs, stored as-is)

ColumnSourceUnitNotes
priceEnergy-ChartsEUR/MWhDA price, DE-LU bidding zone
wind_onshoreSMARD (4067)MWhHourly generation
wind_offshoreSMARD (1225)MWhHourly generation
solarSMARD (4068)MWhHourly generation
biomassSMARD (4066)MWhHourly generation
hydroSMARD (1226)MWhHourly generation
natural_gasSMARD (4071)MWhHourly generation
hard_coalSMARD (4069)MWhHourly generation
brown_coalSMARD (1223)MWhHourly generation
consumptionSMARD (410)MWhNetzlast (grid load)

Derived metrics (computed, not from any API)

MetricFormulaNotes
total_generation Sum of 8 generation sources Computed in da_forecast/fetch.py during data ingest
net_import max(0, consumption − total_generation) Positive only during import hours
Captured price (per tech) Σ(priceh × genh) / Σ(genh) Volume-weighted average DA price, computed daily
Baseload price Σ(priceh) / N Simple arithmetic mean — the reference line
Source vs derived: Raw SMARD/Energy-Charts data is stored unmodified in the parquet file. All captured prices and net imports are computed on-the-fly from this source data. If a derived metric looks wrong, check the source columns first.

4. European Markets

The /markets page compares day-ahead electricity prices, generation mixes, and cross-border flows across 14 European bidding zones: Germany (DE-LU), France (FR), Netherlands (NL), Austria (AT), Poland (PL), Denmark West (DK1), Belgium (BE), Czech Republic (CZ), Spain (ES), Portugal (PT), Norway South (NO1), Sweden Mid (SE3), Finland (FI), and Great Britain (GB).

Data sources

Day-ahead prices for continental zones are sourced from the ENTSO-E Transparency Platform (authoritative TSO data). Generation mix data is sourced from the Energy-Charts API (Fraunhofer ISE, CC BY 4.0). GB uses Elexon BMRS (settlement prices, not DA auction). Data is fetched daily at 03:00 CET and stored locally in parquet files.

Timestamp convention

All timestamps refer to the delivery date — the time interval during which power was physically delivered. Timestamps are left-aligned: "14:00" means the interval 14:00–15:00. All internal storage uses naive UTC. Display conversion to local time is applied per zone.

Source vs derived data

In parquet files, columns without a prefix (e.g., price, wind_onshore) are source data fetched directly from APIs. Columns with a _ prefix (e.g., _total_generation, _renewable_share) are computed/derived values.

ENTSO-E price data notes

DA prices are sourced from ENTSO-E at 15-minute resolution (PT15M) since the SDAC 15-minute go-live. Prices are averaged to hourly for display and storage. Raw 15-minute data is preserved in data/markets/entsoe_raw/ for traceability.

Germany DA prices use the coupled DE-AT-LU domain code (10Y1001A1001A82H), not the standalone DE-LU code. This is because ENTSO-E publishes German DA prices under the historically coupled zone. Austria has its own separate DA prices under 10YAT-APG------L.

All 13 continental zones were cross-validated against Energy-Charts prices for the full Jan 2024 – Mar 2026 period: 99.9–100% of hours match within 0.01 EUR/MWh. One known exception: AT on 26 June 2024 shows Energy-Charts anomalies (up to 1,965 EUR/MWh) that ENTSO-E does not have — Energy-Charts data error, not an ENTSO-E issue.

Cross-border flows

Cross-border data shows scheduled commercial exchanges for Germany. Positive values = export from DE, negative = import to DE. Source: Energy-Charts /cbet endpoint (values in GW).

5. Data Attribution

  • SMARD — Bundesnetzagentur, licensed under CC BY 4.0
  • ENTSO-E — Transparency Platform, DA prices for 13 continental zones
  • Energy-Charts — Fraunhofer ISE, generation mix data (CC BY 4.0)
  • Open-Meteo — open-source weather API using ECMWF, DWD, and other NWP models
  • German holidays — computed using the holidays Python library