Skip to content

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:

  1. Register at ENTSOE Transparency Platform
  2. Request an API key from your account settings

Basic Usage

from epftoolbox2.data.sources import EntsoeSource
import os
import 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

ParameterTypeRequiredDescription
country_codestrYesArea code (see table below). Prices need a bidding zone, e.g. "DE_LU" rather than "DE".
api_keystrYesENTSOE API key. It is sent in the SECURITY_TOKEN request header, never in the URL, and error messages never contain it.
typeList[str]YesData types: load, price, generation, transfer_capacity, installed_capacity
prefer_15minboolNoDefaults to False. Selects the grid of the price column: False gives hourly prices, True gives 15-minute prices. See Price resolution.
neighboursList[str] or strWith transfer_capacityDefaults 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.
prefixstrNoDefaults 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"])

ColumnDocumentDescription
load_actualA65 / A16Actual total load (MW)
load_forecastA65 / A01Day-ahead total load forecast (MW)
load_forecast_daily_minA65 / A31Week-ahead forecast of the day’s minimum load (MW), one value per local day
load_forecast_daily_maxA65 / A31Week-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's
pipeline = (
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"])

ColumnDocumentDescription
priceA44Day-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_15minGridHourly periods15-minute periods
False (default)1 houras publishedaveraged into the hour (an hour with a missing quarter-hour is left empty)
True15 minutesstep-filled: each quarter-hour gets its hour’s priceas 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.

ColumnPsrTypeDescription
generation_biomassB01Biomass (MW)
generation_fossil_brown_coal_ligniteB02Lignite (MW)
generation_fossil_coal_derived_gasB03Coal-derived gas (MW)
generation_fossil_gasB04Natural gas (MW)
generation_fossil_hard_coalB05Hard coal (MW)
generation_fossil_oilB06Oil (MW)
generation_fossil_oil_shaleB07Oil shale (MW)
generation_fossil_peatB08Peat (MW)
generation_geothermalB09Geothermal (MW)
generation_hydro_pumped_storageB10Pumped-storage hydro, generating (MW)
generation_hydro_run_of_river_and_pondageB11Run-of-river hydro (MW)
generation_hydro_water_reservoirB12Reservoir hydro (MW)
generation_marineB13Marine/tidal (MW)
generation_nuclearB14Nuclear (MW)
generation_other_renewableB15Other renewable (MW)
generation_solarB16Solar (MW)
generation_wasteB17Waste (MW)
generation_wind_offshoreB18Offshore wind (MW)
generation_wind_onshoreB19Onshore wind (MW)
generation_otherB20Other (MW)
generation_energy_storageB25Energy storage (MW)
generation_<type>_consumptionanyConsumption reported for that type, e.g. generation_hydro_pumped_storage_consumption (MW)
generation_solar_forecast, generation_wind_onshore_forecast, generation_wind_offshore_forecastB16, B19, B18Day-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:

ColumnDescription
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):

ColumnPublication deadline (543/2013)Safe as a predictor for a forecast issued on D
priceNo 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_actualNo 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_forecast2 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/maxFridays, 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_*_forecast18: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_LU before 2018-10-01).
  • HTTP 429 and 5xx responses and network errors are retried up to 5 times with exponential backoff, honouring the Retry-After header. Other HTTP errors raise EntsoeApiError with 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:

CodeAreaArea Code
ATAustria10YAT-APG------L
BEBelgium10YBE----------2
BGBulgaria10YCA-BULGARIA-R
CHSwitzerland10YCH-SWISSGRIDZ
CZCzechia10YCZ-CEPS-----N
DE_LUGermany-Luxembourg10Y1001A1001A82H
DK_1Denmark West10YDK-1--------W
DK_2Denmark East10YDK-2--------M
EEEstonia10Y1001A1001A39I
ESSpain10YES-REE------0
FIFinland10YFI-1--------U
FRFrance10YFR-RTE------C
GBGreat Britain10YGB----------A
GRGreece10YGR-HTSO-----Y
HRCroatia10YHR-HEP------M
HUHungary10YHU-MAVIR----U
IE_SEMIreland (SEM)10Y1001A1001A59C
IT_NORDItaly North10Y1001A1001A73I
LTLithuania10YLT-1001A0008Q
LVLatvia10YLV-1001A00074
NLNetherlands10YNL----------L
NO_1 … NO_5Norway zones10YNO-1--------2 …
PLPoland10YPL-AREA-----S
PTPortugal10YPT-REN------W
RORomania10YRO-TEL------P
RSSerbia10YCS-SERBIATSOV
SE_1 … SE_4Sweden zones10Y1001A1001A44P …
SISlovenia10YSI-ELES-----O
SKSlovakia10YSK-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.27

Load 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").