EntsoeSource
Downloads electricity data from the ENTSOE Transparency Platform: load, day-ahead prices, generation, cross-border transfer capacity and installed capacity.
Setup
You need an ENTSOE API key:
- Register at ENTSOE Transparency Platform
- Request an API key from your account settings
Basic Usage
from epftoolbox2.data.sources import EntsoeSourceimport osimport pandas as pd
source = EntsoeSource( country_code="PL", api_key=os.environ.get("ENTSOE_API_KEY"), type=["load", "price"], prefer_15min=False,)
df = source.fetch( start=pd.Timestamp("2024-01-01", tz="UTC"), end=pd.Timestamp("2024-06-01", tz="UTC"),)Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
country_code | str | Yes | Area code (see table below). Prices need a bidding zone, e.g. "DE_LU" rather than "DE". |
api_key | str | Yes | ENTSOE API key. It is sent in the SECURITY_TOKEN request header, never in the URL, and error messages never contain it. |
type | List[str] | Yes | Data types: load, price, generation, transfer_capacity, installed_capacity |
prefer_15min | bool | No | Defaults to False. Selects the grid of the price column: False gives hourly prices, True gives 15-minute prices. See Price resolution. |
neighbours | List[str] or str | With transfer_capacity | Defaults to None. Neighbouring bidding zones for transfer_capacity, e.g. ["DE_LU", "CZ"] or "DE_LU,CZ". Required with that type and rejected without it. |
prefix | str | No | Defaults to "". Prepended to every column as f"{prefix}_{column}", so several zones can be combined in one pipeline. |
Returned range
fetch(start, end) returns the half-open range [start, end) with a UTC index. Data is requested in 3-month
chunks. A chunk that keeps failing raises an error instead of leaving a gap.
Daily values (load_forecast_daily_min/max) are stamped at local midnight of the day they describe: the
market’s local midnight when this source is part of a DataPipeline, and this area’s own otherwise (see
Week-ahead daily values and neighbouring areas below). When
that midnight lies before start (for example a Warsaw day starts at 23:00 UTC the evening before), the value
that covers start is reported at start, so the first day is not lost.
Data Types
Load Data (type=["load"])
| Column | Document | Description |
|---|---|---|
load_actual | A65 / A16 | Actual total load (MW) |
load_forecast | A65 / A01 | Day-ahead total load forecast (MW) |
load_forecast_daily_min | A65 / A31 | Week-ahead forecast of the day’s minimum load (MW), one value per local day |
load_forecast_daily_max | A65 / A31 | Week-ahead forecast of the day’s maximum load (MW), one value per local day |
Load keeps the resolution ENTSO-E publishes (15 minutes for PL and DE_LU in 2024–2025). The daily columns hold
one value at local midnight and are empty in between. A ResampleTransformer(freq="1h") after
TimezoneTransformer spreads each value over its own local day.
Week-ahead daily values and neighbouring areas
load_forecast_daily_min/max (A31) holds one value per this area’s own local calendar day D. Combining
this source with another area’s in one market pipeline needs that value on the market’s local day D, not
necessarily this area’s: a Finnish (EET) neighbour’s day starts an hour before a Polish (CET) market’s, so
without correcting for that, its daily value would land an hour into the wrong side of midnight, and around a
DST switch even on the wrong calendar day.
EntsoeSource fixes this automatically when it is part of a DataPipeline: it stamps the value at the
market’s local midnight of its own day D, instead of this area’s own midnight, so the row already sits
where TimezoneTransformer’s default snap (anchored to the market zone) expects it, a no-op for this column.
Used standalone, or when this area already is the market’s own, the value is stamped at this area’s own
midnight.
# FI's week-ahead daily min/max land on Warsaw's own local days, not Helsinki'spipeline = ( DataPipeline(timezone="Europe/Warsaw") .add_source(EntsoeSource("PL", api_key=api_key, type=["load"])) .add_source(EntsoeSource("FI", api_key=api_key, type=["load"], prefix="fi")) .add_transformer(TimezoneTransformer()))TimezoneTransformer’s default daily_columns recognise a prefixed neighbour’s column
(fi_load_forecast_daily_min) automatically too. See
TimezoneTransformer.
Price Data (type=["price"])
| Column | Document | Description |
|---|---|---|
price | A44 | Day-ahead price of the single day-ahead coupling (SDAC) in the bidding zone (EUR/MWh) |
Price resolution
The day-ahead market moved from 60-minute to 15-minute products on 2025-10-01. Before that date most zones publish
hourly prices, after it 15-minute prices. EntsoeSource keeps every period and returns one uniform grid:
prefer_15min | Grid | Hourly periods | 15-minute periods |
|---|---|---|---|
False (default) | 1 hour | as published | averaged into the hour (an hour with a missing quarter-hour is left empty) |
True | 15 minutes | step-filled: each quarter-hour gets its hour’s price | as published |
Where a zone publishes the same interval at two resolutions, the one matching the grid is used. 30-minute periods are treated the same way.
Generation Data (type=["generation"])
Actual generation per production type (A75) as generation_<type>, and the day-ahead wind and solar forecast (A69)
as generation_<type>_forecast. Where a production type also consumes (pumped-storage pumping, and for some zones
every type, mostly zeros), the consumption is a separate column generation_<type>_consumption. It is never mixed
into the generation column.
| Column | PsrType | Description |
|---|---|---|
generation_biomass | B01 | Biomass (MW) |
generation_fossil_brown_coal_lignite | B02 | Lignite (MW) |
generation_fossil_coal_derived_gas | B03 | Coal-derived gas (MW) |
generation_fossil_gas | B04 | Natural gas (MW) |
generation_fossil_hard_coal | B05 | Hard coal (MW) |
generation_fossil_oil | B06 | Oil (MW) |
generation_fossil_oil_shale | B07 | Oil shale (MW) |
generation_fossil_peat | B08 | Peat (MW) |
generation_geothermal | B09 | Geothermal (MW) |
generation_hydro_pumped_storage | B10 | Pumped-storage hydro, generating (MW) |
generation_hydro_run_of_river_and_pondage | B11 | Run-of-river hydro (MW) |
generation_hydro_water_reservoir | B12 | Reservoir hydro (MW) |
generation_marine | B13 | Marine/tidal (MW) |
generation_nuclear | B14 | Nuclear (MW) |
generation_other_renewable | B15 | Other renewable (MW) |
generation_solar | B16 | Solar (MW) |
generation_waste | B17 | Waste (MW) |
generation_wind_offshore | B18 | Offshore wind (MW) |
generation_wind_onshore | B19 | Onshore wind (MW) |
generation_other | B20 | Other (MW) |
generation_energy_storage | B25 | Energy storage (MW) |
generation_<type>_consumption | any | Consumption reported for that type, e.g. generation_hydro_pumped_storage_consumption (MW) |
generation_solar_forecast, generation_wind_onshore_forecast, generation_wind_offshore_forecast | B16, B19, B18 | Day-ahead forecast (MW) |
Column names are the snake_case of ENTSO-E’s production type names. An unknown code becomes generation_<code>,
e.g. generation_b99.
Transfer Capacity (type=["transfer_capacity"])
Day-ahead forecasted transfer capacity (A61, contract type daily) in MW, for each zone in neighbours:
| Column | Description |
|---|---|
transfer_capacity_export_<neighbour> | Capacity from country_code to the neighbour, e.g. transfer_capacity_export_es |
transfer_capacity_import_<neighbour> | Capacity from the neighbour to country_code |
source = EntsoeSource("FR", api_key=key, type=["transfer_capacity"], neighbours=["ES", "CH"])Installed Capacity (type=["installed_capacity"])
Installed generation capacity per production type (A68, year-ahead) in MW, as installed_capacity_<type> with the
same type names as generation (e.g. installed_capacity_solar, installed_capacity_wind_onshore). There is one
value per year, stamped at local midnight of 1 January. If that lies before start, the value is reported at
start. A ResampleTransformer(freq="1h") after TimezoneTransformer carries each value forward over its year.
Combining zones with prefix
from epftoolbox2.pipelines import DataPipeline
pipeline = ( DataPipeline() .add_source(EntsoeSource("PL", api_key=key, type=["load", "price"])) .add_source(EntsoeSource("CZ", api_key=key, type=["price"], prefix="cz")) # cz_price .add_source(EntsoeSource("DE_LU", api_key=key, type=["price"], prefix="de")) # de_price)Not offered: unavailability of generation units (A77/A80)
Outage documents are versioned, but the API returns only the latest revision of each outage by default. With
mRID it returns every revision, but their createdDateTime is not the original publication time: for Polish
outages active on 2024-10-01..02, all 75 latest revisions, and all three revisions of the one outage checked, were
dated October 2025. So the values known at issue time cannot be rebuilt from the API (it would also take one request
per outage), and the latest revision is look-ahead. These documents are left out.
Information available at forecast time
A day-ahead forecast for delivery day D+1 is issued on day D before the market’s gate closure at 12:00 CET. Commission
Regulation (EU) No 543/2013 sets when each series must be published. Lags are relative to the row’s local date D,
as produced by LagTransformer (x_d-1 is yesterday, x_d+1 tomorrow):
| Column | Publication deadline (543/2013) | Safe as a predictor for a forecast issued on D |
|---|---|---|
price | No later than 1 hour after gate closure (Art. 12) | price (delivery D, cleared on D-1) and _d-k. price_d+1 is the target |
load_actual | No later than 1 hour after the operating period (Art. 6) | _d-1 and older only. Unlagged load_actual is look-ahead for the hours after the issue time |
load_forecast | 2 hours before gate closure (10:00 CET) on the day before delivery, updated when significant changes occur (Art. 6) | load_forecast and _d+1 if the forecast is issued at or after 10:00. _d+2 and later are published on D+1 or later: look-ahead |
load_forecast_daily_min/max | Fridays, 2 hours before gate closure, for Monday–Sunday of the following week. Updated when significant changes occur (Art. 6) | _d+0 to _d+2 always. _d+3 when issued at or after 10:00 (on Friday mornings only the week up to Sunday is known). Larger horizons leak on some weekdays |
generation_* (actual, incl. _consumption) | No later than 1 hour after the operational period (Art. 16) | _d-1 and older only |
generation_*_forecast | 18:00 Brussels time on the day before delivery, updated during intraday trading with at least one update at 08:00 on the delivery day (Art. 14) | _d+0 and older. _d+1 is published after gate closure: not guaranteed at issue time |
transfer_capacity_* | ”Sufficiently in advance of the allocation process” (Art. 11): before the day-ahead gate closure, no fixed hour | _d+0 and older. _d+1 is known before 12:00 on D but at no guaranteed hour, so borderline for an early issue time |
installed_capacity_* | Annually, no later than one week before the end of the previous year (Art. 14) | The current year’s value (and _d+h within the same year). The API serves the latest revision |
The API serves the latest revision of each series, not the version published at the deadline, so forecasts that are revised after the issue time (load forecasts, wind and solar forecasts) can still carry a small amount of look-ahead even within these limits.
Errors and retries
- “No matching data” for a chunk is logged as a warning and the chunk is skipped (for example future dates or
a document type the area does not publish). A warning is also logged when the data starts more than a day after
start(e.g.DE_LUbefore 2018-10-01). - HTTP 429 and 5xx responses and network errors are retried up to 5 times with exponential backoff, honouring the
Retry-Afterheader. Other HTTP errors raiseEntsoeApiErrorwith ENTSO-E’s reason text, e.g.HTTP 401: Authentication failed.
Supported Countries & Area Codes
Any key of epftoolbox2.data.sources.entsoe.AREAS is accepted. Common bidding zones:
| Code | Area | Area Code |
|---|---|---|
| AT | Austria | 10YAT-APG------L |
| BE | Belgium | 10YBE----------2 |
| BG | Bulgaria | 10YCA-BULGARIA-R |
| CH | Switzerland | 10YCH-SWISSGRIDZ |
| CZ | Czechia | 10YCZ-CEPS-----N |
| DE_LU | Germany-Luxembourg | 10Y1001A1001A82H |
| DK_1 | Denmark West | 10YDK-1--------W |
| DK_2 | Denmark East | 10YDK-2--------M |
| EE | Estonia | 10Y1001A1001A39I |
| ES | Spain | 10YES-REE------0 |
| FI | Finland | 10YFI-1--------U |
| FR | France | 10YFR-RTE------C |
| GB | Great Britain | 10YGB----------A |
| GR | Greece | 10YGR-HTSO-----Y |
| HR | Croatia | 10YHR-HEP------M |
| HU | Hungary | 10YHU-MAVIR----U |
| IE_SEM | Ireland (SEM) | 10Y1001A1001A59C |
| IT_NORD | Italy North | 10Y1001A1001A73I |
| LT | Lithuania | 10YLT-1001A0008Q |
| LV | Latvia | 10YLV-1001A00074 |
| NL | Netherlands | 10YNL----------L |
| NO_1 … NO_5 | Norway zones | 10YNO-1--------2 … |
| PL | Poland | 10YPL-AREA-----S |
| PT | Portugal | 10YPT-REN------W |
| RO | Romania | 10YRO-TEL------P |
| RS | Serbia | 10YCS-SERBIATSOV |
| SE_1 … SE_4 | Sweden zones | 10Y1001A1001A44P … |
| SI | Slovenia | 10YSI-ELES-----O |
| SK | Slovakia | 10YSK-SEPS-----K |
Country and control-area codes such as DE (10Y1001A1001A83F), DK, NO, SE, IT, LU and IE work for load
and generation but have no day-ahead price.
Caching
The cache key contains the area, the types, prefer_15min and a schema version of the parser. Changing any of
them uses a new cache directory under .cache/sources/. Directories for other values are not read and can be
deleted.
Output
Data is returned with a UTC DatetimeIndex:
print(df.head())# load_actual price# 2024-01-01 00:00:00+00:00 12508.71 5.75# 2024-01-01 01:00:00+00:00 11845.20 5.27Load and generation have one row per published interval (often 15 minutes), so an hourly price column is empty on
the :15, :30 and :45 rows until you resample, e.g. with ResampleTransformer(freq="1h").