Skip to content

Multi-Country Architecture

Overview

EPF runs forecasts for four countries: Spain (ES), Portugal (PT), France (FR), and Germany (DE). Multi-country support shipped over milestones M1–M5 (2026-04-09/10) and was refined through the Phase 5 / v6.0 / Z3 ablation sprint (2026-04-14 → 17).

This page explains the architectural choices that make the model country-aware, what’s shared across countries, and what’s gated per country.

What’s shared

ComponentShared?Notes
Feature builder (src/data/feature_engineering.py)YesTakes a country parameter; internal logic branches on holidays and solar latitude
Target transform residual_1wYes (ES/PT/DE); FR trains with no transform
Base model recipeYesXGBoost depth=12, lr=0.03, q=0.55, pw3, d365
Weather ingestionYesOpen-Meteo with country-specific population-weighted stations
Commodity featuresYesTTF gas, Brent, ETS — priced in EUR and globally applicable
Conformal calibrationYesPer country × horizon
Cloud Run JobsYesContainer image predictor:v11.0 is country-agnostic; country is passed via env + joblib path
API surfaceYesEvery relevant endpoint accepts a ?country= query param with one of ES, PT, FR, DE

What’s per-country

ComponentWhy it variesConfiguration
Market data sourceES uses REE/ESIOS + OMIE; PT/FR/DE use ENTSO-E Transparency Platformsrc/countries.py registry
ResolutionES has 15-minute native; PT/FR/DE are hourlyapproach flag — ES uses hybrid15, others use hourly
TimezoneES/PT use Europe/Madrid/Lisbon; FR uses Europe/Paris; DE uses Europe/Berlin_COUNTRY_TIMEZONES map in src/countries.py
Holidays (Z1)Country-specific public holidays_get_country_holidays(country_code) in feature_engineering.py
Solar elevation (Z4)Country-specific latitude_COUNTRY_LATITUDE map
Cross-country prices (Z3)Only DE benefits; ES/FR/PT regressEPF_CROSS_PRICE_COUNTRIES env var (production: DE)
Target transformFR trains better without itEPF_TARGET_TRANSFORM per country
Joblib artifactsEach country × horizon has its ownNaming convention direct_model_xgboost_[COUNTRY]_15min_hybrid_[run_mode]_[date].joblib

See the cross-price gating decision for why Z3 is gated to DE only.

Data ingestion

ENTSO-E Transparency Platform (PT/FR/DE)

For the three non-Spanish countries, EPF ingests directly from transparency.entsoe.eu via the ENTSOE_API_TOKEN:

  • Day-ahead prices — A44 document type, hourly resolution
  • Actual generation by type — A73 (per PSR type: B16 solar, B19 wind onshore, B18 wind offshore, B01 biomass, B14 nuclear, …)
  • Generation forecast (Z2 feature) — A71 for wind+solar, used as a forward-looking model input
  • Total load — A65

Five pipeline bug fixes were needed during the initial M1 backfill to handle ENTSO-E’s API quirks — drop trailing Z from timestamps, extract the XML namespace dynamically (version drifts), dedupe stacked TimeSeries revisions, use the DE-LU bidding zone EIC (10Y1001A1001A82H, not the control-area EIC), and chunk multi-month backfills into 30-day windows. These all live in src/data/entsoe_pipeline.py.

REE/ESIOS (ES only)

Spain keeps its richer native feed (16 electricity indicators, 15-minute resolution, real-time dispatch data) alongside the ENTSO-E feed. ES uses REE/ESIOS as the primary source and ENTSO-E as a redundancy check.

Per-country performance (145-day backtest, 2025-11-01 → 2026-03-25)

CountryDA MAEStrategic MAEWinning configuration
ES13.9917.35v12.0 ablation (no Z3) DA; v11.0 ST
PT21.9424.67v6.0 ablation DA; v6.0 with Z3 ST
FR24.5228.47v6.0 ablation DA; v5.0 ST
DE27.6435.99v6.0 with Z3 (only country where cross-prices help)

Important context on these numbers: the MAE ceiling is regime-dependent, not structural. On calmer months (e.g. February 2026 for PT) MAE drops to ~14 EUR/MWh. The 145-day window MAE is inflated by the volatile late-2024/early-2025 months that all countries saw.

See the v11.0 post-LSTM correction changelog for the sprint detail.

Country registry

The single source of truth for country metadata lives in src/countries.py. It contains:

  • Country code (ISO 2-letter)
  • Display name, market name (OMIE / EPEX SPOT), timezone
  • Current production model tag per horizon
  • Feature flags: ree_native, supports_intraday, supports_news_phase35
  • Weather station list
  • Latitude for solar elevation

Adding a fifth country is a contained change: extend src/countries.py, create a new ENTSO-E bidding-zone entry, deploy two Cloud Run Jobs + two Cloud Scheduler triggers, one GCS folder. No VM infrastructure changes needed.

What multi-country changed in the API

Every endpoint that touches data or forecasts accepts a country query parameter (default ES). The key new endpoints added in M2 (shipped 2026-04-10):

  • /api/v1/countries — enumerate supported countries and their display metadata
  • /api/v1/market/price-history?country=ES — historical day-ahead prices across countries
  • /api/v1/market/generation?country=ES — ENTSO-E generation-by-type breakdown

Full API surface is documented on the API overview page.