Skip to content

Multi-Country v2.0 — PT/FR/DE Production Models (Production)

Date: April 11, 2026 | Status: Production

What happened

The v1.0 models for Portugal, France, and Germany (shipped April 10, 2026 as part of M5-M7 milestones) had a critical bug: they used the 15-minute feature builder (build_direct_features_15min) on hourly ENTSO-E data. This made every price lag 4x too long — a “24-hour lag” was actually 96 hours (4 days), a “7-day lag” was actually 28 days.

v2.0 fixes this by switching to the correct hourly feature builder, adds ES cross-price features as the strongest market coupling signal, and applies per-country hyperparameter tuning.

Three root causes fixed

1. Resolution mismatch (the biggest bug)

All non-ES data from ENTSO-E is hourly. The training pipeline used --approach hybrid15 which routes to build_direct_features_15min, where:

  • price_lag_24h = price.iloc[i - 96] assumes 96 = 24h at 15-min resolution
  • But with hourly data, i - 96 = 96 hours = 4 days ago

Every lag, rolling average, and momentum feature was computed at the wrong timescale. The model could still learn patterns from the wrong lags (XGBoost is flexible), but the features carried far less signal than intended.

Fix: Use --approach hourly --resolution hourly for non-ES countries.

2. Inference zero-fill (repeat of v10.x LSTM bug)

The ES cross-price features (es_price_lag_24h, es_price_rolling_168h, es_pt_spread_lag_24h, etc.) were added to the training feature builder but not to the prediction feature builder. At inference, these features were silently filled with 0.0:

# direct_predictor.py line 447-449
for f in feature_cols:
if f not in row.columns:
row[f] = 0.0 # ← silent zero-fill

This is the same class of bug that affected the v10.x LSTM embeddings. The model learned from real ES prices during training but predicted with zeros.

Fix: Added ES cross-price computation to both _build_origin_features (hourly) and _build_origin_features_15min in direct_predictor.py.

3. Actual price backfill from wrong table

backfill_actual_prices() hardcoded ree_hourly (ES-only table) for looking up actual prices. Non-ES countries store prices in market_hourly with column price_eur_mwh. This meant backtest evaluation metrics were computed against ES prices, not the target country’s prices.

Fix: Country-aware backfill using market_hourly WHERE country = :c for non-ES countries.

Experiment results

Portugal (10 scouts)

TagConfig changeMAEvs v1.0
pt-v1.015min builder, no ES features42.48
pt-v1.1+ ES cross-price (6 features)31.88-24.9%
pt-v1.2+ hydro/gen features35.70Rejected (+12% vs v1.1)
pt-v1.3+ depth=8, pw=4031.41-26.1%
pt-v2.0Hourly resolution + all fixes8.43-80.2%

France (10 scouts)

TagConfig changeMAEvs v1.0
fr-v1.015min builder, no ES features23.14
fr-v2.0Hourly + ES xprice + depth=126.40-72.3%
fr-v2.2+ no transform (remove residual_1w)5.81-74.9%
fr-v2.3+ q=0.50 (symmetric loss)5.66-75.5%
fr-v2.6+ halflife=904.87-78.9%

Germany (7 scouts)

TagConfig changeMAEvs v1.0
de-v1.015min builder, no ES features20.44
de-v2.0Hourly + ES xprice + depth=125.47-73.2%
de-v2.4+ halflife=1805.28-74.2%
de-v2.5+ halflife=904.14-79.7%

Per-country best configs

ParameterES v11.0PT v2.0FR v2.0DE v2.0
Resolution15min (hybrid15)hourlyhourlyhourly
Target transformresidual_1wresidual_1wnoneresidual_1w
Quantile0.550.550.500.55
Halflife365d365d90d90d
ES cross-priceN/A (is ES)yesyesyes
Depth12121212
Price weighting3x > 60 EUR3x > 60 EUR3x > 60 EUR3x > 60 EUR

What ES cross-price features are and why they work

The six ES cross-price features added to PT/FR/DE models:

  1. es_price_lag_24h — ES day-ahead price from 24 hours ago
  2. es_price_lag_48h — ES day-ahead price from 48 hours ago
  3. es_price_lag_168h — ES day-ahead price from 7 days ago
  4. es_price_rolling_24h — 24-hour rolling average of ES prices
  5. es_price_rolling_168h — 7-day rolling average of ES prices
  6. es_pt_spread_lag_24h — ES price minus target country price (24h lag)

Why they work: European electricity markets are physically and financially interconnected. OMIE operates a unified Iberian market (ES+PT coupling rate >95%). France and Germany are connected to Spain via cross-border transmission. ES day-ahead prices, published by 12:30 CET, are available before other countries’ prediction runs and provide the strongest available signal for European price levels.

Day-of-week patterns

Both FR and DE show the same weakness: Monday and Sunday predictions are ~2x worse than midweek. This reflects the weekend-to-weekday demand transition, which is harder to forecast because industrial load patterns shift.

DayFR v2.0 MAEDE v2.0 MAE
Monday8.229.40
Tuesday3.983.14
Wednesday2.601.66
Thursday3.531.76
Friday3.152.41
Saturday4.693.25
Sunday7.747.35

Comparison with ES v11.0

MetricES v11.0PT v2.0FR v2.0DE v2.0
DA MAE14.268.434.874.14
Correlation0.8910.855+0.85+0.85+
vs v1.0-80.2%-78.9%-79.7%

All three non-ES countries now outperform ES v11.0 on MAE. This reflects two factors: (1) the hourly resolution gives correct lag semantics, and (2) ES cross-price features provide a strong anchor signal that ES itself doesn’t need (since it IS the anchor).