Skip to content

Datadancer

Datenverarbeitung und Analyse.

API-Referenz

datadancer

datadancer — High-level data fetching and DataFrame conversion.

Schnellstart

from datadancer import SmardClient, Filter, Region, Resolution

async with SmardClient() as client: df = await client.fetch_latest(Filter.WIND_ONSHORE, hours=48) print(df.tail())

EEXClient

Synchroner Client für öffentliche EEX Gas-Transparenzdaten.

Kein API-Key erforderlich. Alle Daten stammen von gasandregistry.eex.com.

Parameters:

Name Type Description Default
timeout int

HTTP-Timeout in Sekunden (default: 30)

_TIMEOUT
verify_ssl bool

SSL-Zertifikat prüfen (default: True)

True
Source code in datadancer/sources/eex/client.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
class EEXClient:
    """
    Synchroner Client für öffentliche EEX Gas-Transparenzdaten.

    Kein API-Key erforderlich. Alle Daten stammen von gasandregistry.eex.com.

    Args:
        timeout: HTTP-Timeout in Sekunden (default: 30)
        verify_ssl: SSL-Zertifikat prüfen (default: True)
    """

    def __init__(self, timeout: int = _TIMEOUT, verify_ssl: bool = True) -> None:
        """Initialisiert den EEXClient mit einer persistenten HTTP-Session.

        Args:
            timeout: Maximale Wartezeit pro HTTP-Request in Sekunden (default: 30).
            verify_ssl: SSL-Zertifikat der EEX-Server prüfen. Kann bei lokalen
                        Zertifikatsproblemen auf False gesetzt werden (default: True).
        """
        self._session = requests.Session()
        self._session.headers.update(_HEADERS)
        self._timeout = timeout
        self._verify = verify_ssl

    def _get_csv(self, url: str) -> str:
        """Führt einen GET-Request durch und gibt den dekodieren Response-Text zurück.

        Dekodiert explizit mit ``utf-8-sig`` um das UTF-8-BOM der EEX-CSVs zu
        entfernen und das korrekte Rendering des €-Zeichens sicherzustellen.

        Args:
            url: Vollständige Download-URL.

        Returns:
            Vollständiger CSV-Inhalt als String.

        Raises:
            requests.HTTPError: Bei HTTP-Fehlerstatus (4xx/5xx).
            requests.Timeout: Wenn ``timeout`` überschritten wird.
        """
        logger.debug("GET %s", url)
        r = self._session.get(url, timeout=self._timeout, verify=self._verify)
        r.raise_for_status()
        # EEX CSVs are UTF-8 with BOM; decode explicitly to avoid mangled € sign
        return r.content.decode("utf-8-sig")

    def get_ngp(
        self,
        market_area: str = "TTF",
        *,
        history: bool = True,
    ) -> pd.DataFrame:
        """Lädt den EEX Neutral Gas Price (NGP).

        Args:
            market_area: Marktgebiet. Verfügbar: TTF, FIN, LTU, LVA-EST, ETF.
                         Hinweis: THE ist für NGP nicht verfügbar.
            history: True → 60-Tage-Historie (täglich), False → 15-Minuten-Intraday.

        Returns:
            DataFrame mit Spalten: timestamp, market_area, price, volume_mwh,
            num_trades, unit, index_name, source.

        Raises:
            ValueError: Wenn das Marktgebiet für NGP nicht verfügbar ist.
            requests.HTTPError: Bei HTTP-Fehlerstatus.
        """
        area = market_area.upper()
        if area not in _NGP_AREAS:
            raise ValueError(
                f"NGP ist nicht für Marktgebiet '{market_area}' verfügbar. "
                f"Verfügbar: {sorted(_NGP_AREAS)}. "
                f"Für THE/CEGH/NBP bitte get_ndi() verwenden."
            )
        url = (NGP_60DAYS_URL if history else NGP_15MIN_URL).format(area=area)
        return parse_ngp(self._get_csv(url), market_area=area)

    def get_ndi(self, market_area: str | None = None) -> pd.DataFrame:
        """Lädt den EEX Next Day Index (NDI) für alle oder ein bestimmtes Marktgebiet.

        Der NDI enthält volumengewichtete Durchschnittspreise aller Day-Ahead-Trades
        (08:00–18:00 CET) für THE, TTF, CEGH, NBP, PEG, CZ VTP und weitere Hubs.

        Args:
            market_area: Optionaler Filter (z.B. "THE", "TTF"). None gibt alle Hubs zurück.

        Returns:
            DataFrame mit Spalten: timestamp, market_area, price, unit, index_name, source.

        Raises:
            requests.HTTPError: Bei HTTP-Fehlerstatus.
        """
        return parse_ndi(self._get_csv(NDI_URL), market_area=market_area)

    def get_egsi(self, market_area: str = "THE") -> pd.DataFrame:
        """EEX European Gas Spot Index (EGSI) — nicht mehr öffentlich verfügbar.

        Der EGSI wurde im März 2024 als kostenloser CSV-Download eingestellt.
        Für aktuelle Daten EEX DataSource (kostenpflichtig) kontaktieren:
        datasource@eex-group.com

        Raises:
            NotImplementedError: Immer.
        """
        raise NotImplementedError(
            "EGSI ist seit März 2024 nicht mehr als kostenloser CSV-Download verfügbar. "
            "Bitte EEX DataSource kontaktieren: datasource@eex-group.com"
        )

    def get_egix(self, market_area: str = "THE") -> pd.DataFrame:
        """EEX Daily EGIX / Monthly Index — nicht mehr öffentlich verfügbar.

        Der EGIX wurde im März 2024 als kostenloser CSV-Download eingestellt.
        Für aktuelle Daten EEX DataSource (kostenpflichtig) kontaktieren:
        datasource@eex-group.com

        Raises:
            NotImplementedError: Immer.
        """
        raise NotImplementedError(
            "EGIX ist seit März 2024 nicht mehr als kostenloser CSV-Download verfügbar. "
            "Bitte EEX DataSource kontaktieren: datasource@eex-group.com"
        )

    def close(self) -> None:
        """Schließt die zugrundeliegende HTTP-Session und gibt Ressourcen frei.

        Wird automatisch aufgerufen beim Verlassen eines ``with``-Blocks.
        Kann auch manuell aufgerufen werden, wenn kein Kontextmanager verwendet wird.
        """
        self._session.close()

    def __enter__(self) -> "EEXClient":
        """Gibt den Client beim Eintritt in einen ``with``-Block zurück.

        Returns:
            EEXClient: Diese Instanz.
        """
        return self

    def __exit__(self, *_) -> None:
        """Schließt die HTTP-Session beim Verlassen des ``with``-Blocks."""
        self.close()

__enter__()

Gibt den Client beim Eintritt in einen with-Block zurück.

Returns:

Name Type Description
EEXClient 'EEXClient'

Diese Instanz.

Source code in datadancer/sources/eex/client.py
168
169
170
171
172
173
174
def __enter__(self) -> "EEXClient":
    """Gibt den Client beim Eintritt in einen ``with``-Block zurück.

    Returns:
        EEXClient: Diese Instanz.
    """
    return self

__exit__(*_)

Schließt die HTTP-Session beim Verlassen des with-Blocks.

Source code in datadancer/sources/eex/client.py
176
177
178
def __exit__(self, *_) -> None:
    """Schließt die HTTP-Session beim Verlassen des ``with``-Blocks."""
    self.close()

__init__(timeout=_TIMEOUT, verify_ssl=True)

Initialisiert den EEXClient mit einer persistenten HTTP-Session.

Parameters:

Name Type Description Default
timeout int

Maximale Wartezeit pro HTTP-Request in Sekunden (default: 30).

_TIMEOUT
verify_ssl bool

SSL-Zertifikat der EEX-Server prüfen. Kann bei lokalen Zertifikatsproblemen auf False gesetzt werden (default: True).

True
Source code in datadancer/sources/eex/client.py
47
48
49
50
51
52
53
54
55
56
57
58
def __init__(self, timeout: int = _TIMEOUT, verify_ssl: bool = True) -> None:
    """Initialisiert den EEXClient mit einer persistenten HTTP-Session.

    Args:
        timeout: Maximale Wartezeit pro HTTP-Request in Sekunden (default: 30).
        verify_ssl: SSL-Zertifikat der EEX-Server prüfen. Kann bei lokalen
                    Zertifikatsproblemen auf False gesetzt werden (default: True).
    """
    self._session = requests.Session()
    self._session.headers.update(_HEADERS)
    self._timeout = timeout
    self._verify = verify_ssl

close()

Schließt die zugrundeliegende HTTP-Session und gibt Ressourcen frei.

Wird automatisch aufgerufen beim Verlassen eines with-Blocks. Kann auch manuell aufgerufen werden, wenn kein Kontextmanager verwendet wird.

Source code in datadancer/sources/eex/client.py
160
161
162
163
164
165
166
def close(self) -> None:
    """Schließt die zugrundeliegende HTTP-Session und gibt Ressourcen frei.

    Wird automatisch aufgerufen beim Verlassen eines ``with``-Blocks.
    Kann auch manuell aufgerufen werden, wenn kein Kontextmanager verwendet wird.
    """
    self._session.close()

get_egix(market_area='THE')

EEX Daily EGIX / Monthly Index — nicht mehr öffentlich verfügbar.

Der EGIX wurde im März 2024 als kostenloser CSV-Download eingestellt. Für aktuelle Daten EEX DataSource (kostenpflichtig) kontaktieren: datasource@eex-group.com

Raises:

Type Description
NotImplementedError

Immer.

Source code in datadancer/sources/eex/client.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def get_egix(self, market_area: str = "THE") -> pd.DataFrame:
    """EEX Daily EGIX / Monthly Index — nicht mehr öffentlich verfügbar.

    Der EGIX wurde im März 2024 als kostenloser CSV-Download eingestellt.
    Für aktuelle Daten EEX DataSource (kostenpflichtig) kontaktieren:
    datasource@eex-group.com

    Raises:
        NotImplementedError: Immer.
    """
    raise NotImplementedError(
        "EGIX ist seit März 2024 nicht mehr als kostenloser CSV-Download verfügbar. "
        "Bitte EEX DataSource kontaktieren: datasource@eex-group.com"
    )

get_egsi(market_area='THE')

EEX European Gas Spot Index (EGSI) — nicht mehr öffentlich verfügbar.

Der EGSI wurde im März 2024 als kostenloser CSV-Download eingestellt. Für aktuelle Daten EEX DataSource (kostenpflichtig) kontaktieren: datasource@eex-group.com

Raises:

Type Description
NotImplementedError

Immer.

Source code in datadancer/sources/eex/client.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def get_egsi(self, market_area: str = "THE") -> pd.DataFrame:
    """EEX European Gas Spot Index (EGSI) — nicht mehr öffentlich verfügbar.

    Der EGSI wurde im März 2024 als kostenloser CSV-Download eingestellt.
    Für aktuelle Daten EEX DataSource (kostenpflichtig) kontaktieren:
    datasource@eex-group.com

    Raises:
        NotImplementedError: Immer.
    """
    raise NotImplementedError(
        "EGSI ist seit März 2024 nicht mehr als kostenloser CSV-Download verfügbar. "
        "Bitte EEX DataSource kontaktieren: datasource@eex-group.com"
    )

get_ndi(market_area=None)

Lädt den EEX Next Day Index (NDI) für alle oder ein bestimmtes Marktgebiet.

Der NDI enthält volumengewichtete Durchschnittspreise aller Day-Ahead-Trades (08:00–18:00 CET) für THE, TTF, CEGH, NBP, PEG, CZ VTP und weitere Hubs.

Parameters:

Name Type Description Default
market_area str | None

Optionaler Filter (z.B. "THE", "TTF"). None gibt alle Hubs zurück.

None

Returns:

Type Description
DataFrame

DataFrame mit Spalten: timestamp, market_area, price, unit, index_name, source.

Raises:

Type Description
HTTPError

Bei HTTP-Fehlerstatus.

Source code in datadancer/sources/eex/client.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def get_ndi(self, market_area: str | None = None) -> pd.DataFrame:
    """Lädt den EEX Next Day Index (NDI) für alle oder ein bestimmtes Marktgebiet.

    Der NDI enthält volumengewichtete Durchschnittspreise aller Day-Ahead-Trades
    (08:00–18:00 CET) für THE, TTF, CEGH, NBP, PEG, CZ VTP und weitere Hubs.

    Args:
        market_area: Optionaler Filter (z.B. "THE", "TTF"). None gibt alle Hubs zurück.

    Returns:
        DataFrame mit Spalten: timestamp, market_area, price, unit, index_name, source.

    Raises:
        requests.HTTPError: Bei HTTP-Fehlerstatus.
    """
    return parse_ndi(self._get_csv(NDI_URL), market_area=market_area)

get_ngp(market_area='TTF', *, history=True)

Lädt den EEX Neutral Gas Price (NGP).

Parameters:

Name Type Description Default
market_area str

Marktgebiet. Verfügbar: TTF, FIN, LTU, LVA-EST, ETF. Hinweis: THE ist für NGP nicht verfügbar.

'TTF'
history bool

True → 60-Tage-Historie (täglich), False → 15-Minuten-Intraday.

True

Returns:

Type Description
DataFrame

DataFrame mit Spalten: timestamp, market_area, price, volume_mwh,

DataFrame

num_trades, unit, index_name, source.

Raises:

Type Description
ValueError

Wenn das Marktgebiet für NGP nicht verfügbar ist.

HTTPError

Bei HTTP-Fehlerstatus.

Source code in datadancer/sources/eex/client.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def get_ngp(
    self,
    market_area: str = "TTF",
    *,
    history: bool = True,
) -> pd.DataFrame:
    """Lädt den EEX Neutral Gas Price (NGP).

    Args:
        market_area: Marktgebiet. Verfügbar: TTF, FIN, LTU, LVA-EST, ETF.
                     Hinweis: THE ist für NGP nicht verfügbar.
        history: True → 60-Tage-Historie (täglich), False → 15-Minuten-Intraday.

    Returns:
        DataFrame mit Spalten: timestamp, market_area, price, volume_mwh,
        num_trades, unit, index_name, source.

    Raises:
        ValueError: Wenn das Marktgebiet für NGP nicht verfügbar ist.
        requests.HTTPError: Bei HTTP-Fehlerstatus.
    """
    area = market_area.upper()
    if area not in _NGP_AREAS:
        raise ValueError(
            f"NGP ist nicht für Marktgebiet '{market_area}' verfügbar. "
            f"Verfügbar: {sorted(_NGP_AREAS)}. "
            f"Für THE/CEGH/NBP bitte get_ndi() verwenden."
        )
    url = (NGP_60DAYS_URL if history else NGP_15MIN_URL).format(area=area)
    return parse_ngp(self._get_csv(url), market_area=area)

EEXMarketArea

Bases: StrEnum

Verfügbare EEX Gas-Marktgebiete und Hubs.

Nicht alle Marktgebiete sind für jeden Index verfügbar: - NGP: TTF, FIN, LTU, LVA_EST, ETF - NDI: THE, TTF, CEGH, ETF, FIN, LTU, LVA_EST, PEG, PVB, ZTP, CZ

Werte entsprechen den Hub-Bezeichnungen in den EEX-CSVs und können direkt als market_area-Argument übergeben werden.

Source code in datadancer/sources/eex/models.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class MarketArea(StrEnum):
    """Verfügbare EEX Gas-Marktgebiete und Hubs.

    Nicht alle Marktgebiete sind für jeden Index verfügbar:
    - NGP: TTF, FIN, LTU, LVA_EST, ETF
    - NDI: THE, TTF, CEGH, ETF, FIN, LTU, LVA_EST, PEG, PVB, ZTP, CZ

    Werte entsprechen den Hub-Bezeichnungen in den EEX-CSVs und können
    direkt als ``market_area``-Argument übergeben werden.
    """

    TTF     = "TTF"      # Title Transfer Facility (Niederlande) — meistgehandelter EU-Gashub
    THE     = "THE"      # Trading Hub Europe (Deutschland)
    FIN     = "FIN"      # Finnland
    LTU     = "LTU"      # Litauen
    LVA_EST = "LVA-EST"  # Lettland & Estland (gemeinsames Bilanzierungsgebiet)
    ETF     = "ETF"      # Estonian-Finnish Trading Hub
    CEGH    = "CEGH"     # Central European Gas Hub (Österreich/CEGH VTP)
    NBP     = "NBP"      # National Balancing Point (Großbritannien)
    PEG     = "PEG"      # Point d'Échange de Gaz (Frankreich)
    PVB     = "PVB"      # Punto Virtual de Balance (Spanien)
    ZTP     = "ZTP"      # Zeebrugge Trading Point (Belgien)
    CZ      = "CZ VTP"   # Czech Virtual Trading Point (Tschechien)

Filter

Bases: IntEnum

SMARD Filter-IDs — entsprechen den Datenkategorien.

Source code in datadancer/sources/smard/constants.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class Filter(IntEnum):
    """SMARD Filter-IDs — entsprechen den Datenkategorien."""

    # Realisierte Erzeugung
    BIOMASSE                   = 1223
    WASSERKRAFT                = 4066
    WIND_ONSHORE               = 1225
    WIND_OFFSHORE              = 4067
    PHOTOVOLTAIK               = 4068
    SONSTIGE_ERNEUERBARE       = 1228
    BRAUNKOHLE                 = 1224
    STEINKOHLE                 = 1227
    ERDGAS                     = 1226
    PUMPSPEICHER_EINSPEISUNG   = 1229
    SONSTIGE_KONVENTIONELLE    = 1230
    KERNENERGIE                = 1221

    # Erzeugungsprognosen
    PROGNOSE_WIND_ONSHORE      = 3791
    PROGNOSE_WIND_OFFSHORE     = 3792
    PROGNOSE_PHOTOVOLTAIK      = 3793
    PROGNOSE_SONSTIGE          = 6379
    PROGNOSE_GESAMTLAST        = 3794
    PROGNOSE_RESIDUALLAST      = 3795
    PROGNOSE_PUMPSPEICHER      = 3796

    # Realisierter Verbrauch
    GESAMT_VERBRAUCH           = 410
    RESIDUALLAST               = 4359
    PUMPSPEICHER_VERBRAUCH     = 4387

    # Marktpreise (€/MWh)
    DAY_AHEAD_DE_LU            = 4169
    DAY_AHEAD_AT               = 5078
    DAY_AHEAD_BE               = 4996
    DAY_AHEAD_NO2              = 4997
    DAY_AHEAD_NL               = 5313

    # Grenzüberschreitender Handel
    GRENZHANDEL_GESAMT         = 1152
    GRENZHANDEL_DE_AT          = 4081
    GRENZHANDEL_DE_CZ          = 4082
    GRENZHANDEL_DE_DK1         = 4083
    GRENZHANDEL_DE_DK2         = 4084
    GRENZHANDEL_DE_FR          = 4085
    GRENZHANDEL_DE_NL          = 4086
    GRENZHANDEL_DE_NO2         = 4087
    GRENZHANDEL_DE_PL          = 4088
    GRENZHANDEL_DE_SE4         = 4089
    GRENZHANDEL_DE_CH          = 4090

    @property
    def label(self) -> str:
        """Gibt den lesbaren deutschen Anzeigenamen des Filters zurück.

        Returns:
            str: Lesbarer Label-String (z. B. "Wind Onshore"), Fallback auf den Enum-Namen.
        """
        return _FILTER_LABELS.get(self, self.name)

    @property
    def unit(self) -> str:
        """Gibt die Maßeinheit der Zeitreihenwerte zurück.

        Returns:
            str: Einheit (z. B. "MWh" oder "€/MWh").
        """
        return _FILTER_UNITS.get(self, "MWh")

    @property
    def group(self) -> str:
        """Gibt die thematische Gruppe des Filters zurück.

        Returns:
            str: Gruppenname (z. B. "Erzeugung", "Marktpreise"), Fallback auf "Sonstige".
        """
        return _FILTER_GROUPS.get(self, "Sonstige")

group property

Gibt die thematische Gruppe des Filters zurück.

Returns:

Name Type Description
str str

Gruppenname (z. B. "Erzeugung", "Marktpreise"), Fallback auf "Sonstige".

label property

Gibt den lesbaren deutschen Anzeigenamen des Filters zurück.

Returns:

Name Type Description
str str

Lesbarer Label-String (z. B. "Wind Onshore"), Fallback auf den Enum-Namen.

unit property

Gibt die Maßeinheit der Zeitreihenwerte zurück.

Returns:

Name Type Description
str str

Einheit (z. B. "MWh" oder "€/MWh").

Region

Bases: StrEnum

Verfügbare Regionen / Regelzonen.

Source code in datadancer/sources/smard/constants.py
170
171
172
173
174
175
176
177
178
179
180
class Region(StrEnum):
    """Verfügbare Regionen / Regelzonen."""
    DE          = "DE"
    AT          = "AT"
    LU          = "LU"
    DE_LU       = "DE-LU"
    DE_AT_LU    = "DE-AT-LU"
    FIFTYHERTZ  = "50Hertz"
    AMPRION     = "Amprion"
    TENNET      = "TenneT"
    TRANSNETBW  = "TransnetBW"

Resolution

Bases: StrEnum

Zeitliche Auflösung der Zeitreihe.

Source code in datadancer/sources/smard/constants.py
183
184
185
186
187
188
189
190
class Resolution(StrEnum):
    """Zeitliche Auflösung der Zeitreihe."""
    QUARTERHOUR = "quarterhour"
    HOUR        = "hour"
    DAY         = "day"
    WEEK        = "week"
    MONTH       = "month"
    YEAR        = "year"

SmardClient

Async Client für die SMARD Strommarktdaten-API.

Verwendung als Kontextmanager (empfohlen): async with SmardClient() as client: df = await client.fetch_latest(Filter.WIND_ONSHORE)

Parameters:

Name Type Description Default
region Region

Standard-Region (default: DE)

DE
resolution Resolution

Standard-Auflösung (default: HOUR)

HOUR
tz str

Zeitzone für den DataFrame-Index (default: Europe/Berlin)

'Europe/Berlin'
retries int

Wiederholungsversuche bei HTTP-Fehlern

3
Source code in datadancer/sources/smard/client.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
class SmardClient:
    """
    Async Client für die SMARD Strommarktdaten-API.

    Verwendung als Kontextmanager (empfohlen):
        async with SmardClient() as client:
            df = await client.fetch_latest(Filter.WIND_ONSHORE)

    Args:
        region:     Standard-Region (default: DE)
        resolution: Standard-Auflösung (default: HOUR)
        tz:         Zeitzone für den DataFrame-Index (default: Europe/Berlin)
        retries:    Wiederholungsversuche bei HTTP-Fehlern
    """

    def __init__(
        self,
        region: Region = Region.DE,
        resolution: Resolution = Resolution.HOUR,
        tz: str = "Europe/Berlin",
        retries: int = 3,
    ) -> None:
        """Initialisiert den SmardClient mit Standard-Verbindungsparametern."""
        self.default_region = region
        self.default_resolution = resolution
        self.tz = tz
        self._retries = retries
        self._client: httpx.AsyncClient | None = None

    async def __aenter__(self) -> "SmardClient":
        """Öffnet eine HTTP-Session und gibt den Client zurück.

        Returns:
            SmardClient: Der initialisierte Client.
        """
        self._client = httpx.AsyncClient(headers=_HEADERS, timeout=_TIMEOUT)
        return self

    async def __aexit__(self, *_) -> None:
        """Schließt die HTTP-Session beim Verlassen des Kontextmanagers."""
        await self.close()

    async def close(self) -> None:
        """Schließt den zugrundeliegenden HTTP-Client und setzt ihn auf None."""
        if self._client:
            await self._client.aclose()
            self._client = None

    @property
    def _http(self) -> httpx.AsyncClient:
        """Gibt den aktiven HTTP-Client zurück; erstellt einen neuen, falls keiner vorhanden.

        Returns:
            httpx.AsyncClient: Der aktive HTTP-Client.
        """
        if self._client is None:
            self._client = httpx.AsyncClient(headers=_HEADERS, timeout=_TIMEOUT)
        return self._client

    # ── Interne API-Calls ─────────────────────────────────────────────────────

    async def _get_json(self, url: str) -> dict:
        """Führt einen GET-Request durch und gibt den JSON-Body zurück, mit exponentiellem Retry.

        Args:
            url (str): Die vollständige URL der SMARD-API.

        Returns:
            dict: Der geparste JSON-Antwort-Body.

        Raises:
            RuntimeError: Wenn alle Wiederholungsversuche fehlschlagen.
        """
        last_exc: Exception | None = None
        for attempt in range(self._retries):
            try:
                r = await self._http.get(url)
                r.raise_for_status()
                return r.json()
            except (httpx.HTTPError, httpx.TimeoutException) as e:
                last_exc = e
                if attempt < self._retries - 1:
                    await asyncio.sleep(2 ** attempt)
        raise RuntimeError(f"SMARD API nicht erreichbar: {last_exc}")

    async def _fetch_timestamps(self, filter_id: int, region: str, resolution: str) -> list[int]:
        """Ruft die verfügbaren Block-Zeitstempel für einen Filter ab.

        Args:
            filter_id (int): Numerische SMARD Filter-ID.
            region (str): Regionscode (z. B. "DE").
            resolution (str): Zeitauflösung (z. B. "hour").

        Returns:
            list[int]: Liste von Unix-Millisekunden-Zeitstempeln der verfügbaren Datenblöcke.
        """
        url = f"{_BASE_URL}/{filter_id}/{region}/index_{resolution}.json"
        return (await self._get_json(url)).get("timestamps", [])

    async def _fetch_series(self, filter_id: int, region: str, resolution: str, timestamp: int) -> list:
        """Ruft die Rohdaten-Zeitreihe für einen bestimmten Block-Zeitstempel ab.

        Args:
            filter_id (int): Numerische SMARD Filter-ID.
            region (str): Regionscode (z. B. "DE").
            resolution (str): Zeitauflösung (z. B. "hour").
            timestamp (int): Block-Zeitstempel in Unix-Millisekunden.

        Returns:
            list: Liste von [timestamp_ms, value]-Paaren aus dem API-Block.
        """
        url = f"{_BASE_URL}/{filter_id}/{region}/{filter_id}_{region}_{resolution}_{timestamp}.json"
        return (await self._get_json(url)).get("series", [])

    async def _fetch_price_series(self, filter_id: int, region: str, resolution: str, timestamp: int) -> list:
        """Ruft Preiszeitreihen-Daten über den table_data-Endpunkt ab.

        Identisch zu ``_fetch_series``, verwendet jedoch ``table_data`` statt ``chart_data``
        als Basis-URL — erforderlich für Marktpreisfilter der SMARD-API.

        Args:
            filter_id (int): Numerische SMARD Filter-ID.
            region (str): Regionscode (z. B. "DE").
            resolution (str): Zeitauflösung (z. B. "hour").
            timestamp (int): Block-Zeitstempel in Unix-Millisekunden.

        Returns:
            list: Liste von [timestamp_ms, value]-Paaren aus dem API-Block.
        """
        url = f"{_TABLE_URL}/{filter_id}/{region}/{filter_id}_{region}_{resolution}_{timestamp}.json"
        return (await self._get_json(url)).get("series", [])

    # ── Öffentliche API ───────────────────────────────────────────────────────

    async def fetch(
        self,
        filter: Filter,
        *,
        region: Region | None = None,
        resolution: Resolution | None = None,
        start: datetime | None = None,
        end: datetime | None = None,
        dropna: bool = True,
    ) -> pd.DataFrame:
        """Lädt eine vollständige Zeitreihe für einen SMARD-Filter als DataFrame.

        Args:
            filter (Filter): Der gewünschte SMARD-Datenkategorie-Filter.
            region (Region | None): Region; nutzt ``default_region`` falls None.
            resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
            start (datetime | None): Optionaler Startzeitpunkt (inklusiv).
            end (datetime | None): Optionaler Endzeitpunkt (inklusiv).
            dropna (bool): Wenn True, werden Zeilen ohne Messwert entfernt.

        Returns:
            pd.DataFrame: DataFrame mit DatetimeIndex (``Europe/Berlin``) und den Spalten
            ``value``, ``unit``, ``filter``, ``label``, ``region``.

        Raises:
            RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
        """
        rgn = str(region or self.default_region)
        res = str(resolution or self.default_resolution)

        timestamps = await self._fetch_timestamps(filter, rgn, res)
        if not timestamps:
            return self._empty_df()

        ts_start = int(start.timestamp() * 1000) if start else 0
        ts_end   = int(end.timestamp() * 1000) if end else float("inf")
        relevant = [t for t in timestamps if ts_start <= t <= ts_end]
        if not relevant:
            return self._empty_df()

        chunks = await asyncio.gather(
            *[self._fetch_series(filter, rgn, res, ts) for ts in relevant]
        )
        raw = {ts: val for chunk in chunks for ts, val in chunk}
        return self._build_df(raw, filter, rgn, dropna)

    async def fetch_latest(
        self,
        filter: Filter,
        *,
        hours: int = 48,
        region: Region | None = None,
        resolution: Resolution | None = None,
    ) -> pd.DataFrame:
      """Lädt die letzten N Stunden ab dem neuesten verfügbaren Datenpunkt.

      Args:
          filter (Filter): Der gewünschte SMARD-Datenkategorie-Filter.
          hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.
          region (Region | None): Region; nutzt ``default_region`` falls None.
          resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.

      Returns:
          pd.DataFrame: DataFrame mit DatetimeIndex (``Europe/Berlin``) und den Spalten
          ``value``, ``unit``, ``filter``, ``label``, ``region``.

      Raises:
          RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
      """
      rgn = str(region or self.default_region)
      res = str(resolution or self.default_resolution)

      timestamps = await self._fetch_timestamps(filter, rgn, res)
      if not timestamps:
          return self._empty_df()

      # Letzten Block ohne Zeitfilter laden
      last_ts = timestamps[-1]
      series = await self._fetch_series(filter, rgn, res, last_ts)

      # Rohdaten in DataFrame umwandeln
      raw = {ts: val for ts, val in series}
      df = self._build_df(raw, filter, rgn, dropna=True)

      if df.empty:
          return df

      # Auf gewünschte Stunden beschneiden
      cutoff = df.index[-1] - timedelta(hours=hours)
      return df[df.index >= cutoff]

    async def fetch_day_ahead_prices(
        self,
        *,
        resolution: Resolution | None = None,
        hours: int = 48,
    ) -> pd.DataFrame:
        """Lädt Day-Ahead-Marktpreise (DE/LU) über den table_data-Endpunkt.

        Verwendet immer Filter 4169 (DAY_AHEAD_DE_LU) und Region "DE" — API-seitig
        sind Preisdaten nur über table_data abrufbar und ausschließlich für "DE" verfügbar.

        Args:
            resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
            hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

        Returns:
            pd.DataFrame: DataFrame mit DatetimeIndex (``Europe/Berlin``) und der Spalte
            ``price_eur_per_mwh`` (Day-Ahead-Preis in €/MWh).

        Raises:
            RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
        """
        res = str(resolution or self.default_resolution)
        timestamps = await self._fetch_timestamps(Filter.DAY_AHEAD_DE_LU, "DE", res)
        if not timestamps:
            return pd.DataFrame(columns=["price_eur_per_mwh"])

        series = await self._fetch_price_series(Filter.DAY_AHEAD_DE_LU, "DE", res, timestamps[-1])
        raw = {ts: val for ts, val in series}
        df = self._build_df(raw, Filter.DAY_AHEAD_DE_LU, "DE", dropna=True)

        if df.empty:
            return pd.DataFrame(columns=["price_eur_per_mwh"])

        cutoff = df.index[-1] - timedelta(hours=hours)
        return df.loc[df.index >= cutoff, ["value"]].rename(columns={"value": "price_eur_per_mwh"})

    async def fetch_multi(
        self,
        filters: Sequence[Filter],
        *,
        region: Region | None = None,
        resolution: Resolution | None = None,
        start: datetime | None = None,
        end: datetime | None = None,
        hours: int | None = None,
        dropna: bool = True,
        wide: bool = True,
    ) -> pd.DataFrame:
      """Ruft mehrere SMARD-Filter parallel ab und kombiniert sie in einem DataFrame.

      Args:
          filters (Sequence[Filter]): Liste der abzurufenden Filter.
          region (Region | None): Region; nutzt ``default_region`` falls None.
          resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
          start (datetime | None): Optionaler Startzeitpunkt; ignoriert wenn ``hours`` gesetzt.
          end (datetime | None): Optionaler Endzeitpunkt; ignoriert wenn ``hours`` gesetzt.
          hours (int | None): Wenn angegeben, wird ``fetch_latest`` pro Filter verwendet.
          dropna (bool): Wenn True, werden Zeilen ohne Messwert entfernt (nur bei ``hours=None``).
          wide (bool): Wenn True, wird ein Wide-Format mit einem Filter pro Spalte zurückgegeben.

      Returns:
          pd.DataFrame: Kombinierter DataFrame. Im Wide-Format eine Spalte pro Filter (Enum-Name
          als Spaltenüberschrift); im Long-Format ein gestapelter DataFrame mit allen Metadatenspalten.

      Raises:
          RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
      """
      if hours is not None:
          # fetch_latest pro Filter — lädt letzten Block und beschneidet auf hours
          dfs = await asyncio.gather(*[
              self.fetch_latest(f, hours=hours, region=region, resolution=resolution)
              for f in filters
          ])
      else:
          dfs = await asyncio.gather(*[
              self.fetch(f, region=region, resolution=resolution,
                         start=start, end=end, dropna=dropna)
              for f in filters
          ])

      if wide:
          combined = pd.concat(
              [df["value"].rename(f.name) for f, df in zip(filters, dfs) if not df.empty],
              axis=1,
          )
          combined.index.name = "timestamp"
          return combined
      return pd.concat([df for df in dfs if not df.empty], ignore_index=False)


    async def fetch_generation_mix(
        self,
        *,
        region: Region | None = None,
        resolution: Resolution | None = None,
        hours: int = 48,
    ) -> pd.DataFrame:
        """Lädt den vollständigen Stromerzeugungsmix als Wide-DataFrame mit lesbaren Spaltennamen.

        Enthält: Biomasse, Wasserkraft, Wind Onshore/Offshore, Photovoltaik, Sonstige
        Erneuerbare, Braunkohle, Steinkohle, Erdgas und Kernenergie.

        Args:
            region (Region | None): Region; nutzt ``default_region`` falls None.
            resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
            hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

        Returns:
            pd.DataFrame: Wide-DataFrame mit DatetimeIndex und einer Spalte pro Energieträger
            (Spaltennamen entsprechen den deutschen Label-Strings der Filter).

        Raises:
            RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
        """
        filters = [
            Filter.BIOMASSE, Filter.WASSERKRAFT, Filter.WIND_ONSHORE,
            Filter.WIND_OFFSHORE, Filter.PHOTOVOLTAIK, Filter.SONSTIGE_ERNEUERBARE,
            Filter.BRAUNKOHLE, Filter.STEINKOHLE, Filter.ERDGAS,
            Filter.KERNENERGIE,
        ]
        df = await self.fetch_multi(filters, region=region, resolution=resolution,
                                    hours=hours, wide=True)
        df.columns = [Filter[c].label if c in Filter.__members__ else c for c in df.columns]
        return df

    async def fetch_renewable_share(
        self,
        *,
        region: Region | None = None,
        resolution: Resolution | None = None,
        hours: int = 48,
    ) -> pd.DataFrame:
        """Berechnet den Erneuerbaren-Anteil an der Gesamterzeugung.

        Aggregiert Biomasse, Wasserkraft, Wind Onshore/Offshore, Photovoltaik und Sonstige
        Erneuerbare sowie konventionelle Träger (Braun-/Steinkohle, Erdgas, Kernenergie)
        und berechnet den prozentualen Anteil der Erneuerbaren.

        Args:
            region (Region | None): Region; nutzt ``default_region`` falls None.
            resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
            hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

        Returns:
            pd.DataFrame: DataFrame mit DatetimeIndex und den Spalten ``renewable_mwh``
            (erneuerbare Erzeugung), ``total_mwh`` (Gesamterzeugung) und
            ``renewable_pct`` (Anteil in Prozent, gerundet auf eine Dezimalstelle).

        Raises:
            RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
        """
        renewable    = [Filter.BIOMASSE, Filter.WASSERKRAFT, Filter.WIND_ONSHORE,
                        Filter.WIND_OFFSHORE, Filter.PHOTOVOLTAIK, Filter.SONSTIGE_ERNEUERBARE]
        conventional = [
              Filter.BRAUNKOHLE, Filter.STEINKOHLE, Filter.ERDGAS, Filter.KERNENERGIE
          ]

        re_df, conv_df = await asyncio.gather(
            self.fetch_multi(renewable,    region=region, resolution=resolution, hours=hours, wide=True),
            self.fetch_multi(conventional, region=region, resolution=resolution, hours=hours, wide=True),
        )
        result = pd.DataFrame(index=re_df.index.union(conv_df.index))
        result["renewable_mwh"] = re_df.sum(axis=1)
        result["total_mwh"]     = re_df.sum(axis=1) + conv_df.sum(axis=1)
        result["renewable_pct"] = (result["renewable_mwh"] / result["total_mwh"] * 100).round(1)
        return result.dropna()

    async def fetch_price_and_load(
        self,
        *,
        region: Region | None = None,
        resolution: Resolution | None = None,
        hours: int = 48,
    ) -> pd.DataFrame:
        """Lädt Day-Ahead-Strompreis und Gesamtlast kombiniert in einem Wide-DataFrame.

        Args:
            region (Region | None): Region; nutzt ``default_region`` falls None.
            resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
            hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

        Returns:
            pd.DataFrame: Wide-DataFrame mit DatetimeIndex und den Spalten
            ``price_eur_mwh`` (Day-Ahead-Preis in €/MWh) und ``load_mwh`` (Gesamtlast in MWh).

        Raises:
            RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
        """
        df = await self.fetch_multi(
            [Filter.DAY_AHEAD_DE_LU, Filter.GESAMT_VERBRAUCH],
            region=region, resolution=resolution, hours=hours, wide=True,
        )
        df.columns = ["price_eur_mwh", "load_mwh"]
        return df

    async def list_available_timestamps(
        self,
        filter: Filter,
        *,
        region: Region | None = None,
        resolution: Resolution | None = None,
    ) -> pd.DatetimeIndex:
        """Gibt alle verfügbaren Block-Zeitstempel für einen Filter als DatetimeIndex zurück.

        Args:
            filter (Filter): Der gewünschte SMARD-Datenkategorie-Filter.
            region (Region | None): Region; nutzt ``default_region`` falls None.
            resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.

        Returns:
            pd.DatetimeIndex: Zeitzonenbewusster Index (``Europe/Berlin``) aller verfügbaren
            Datenblock-Zeitstempel.

        Raises:
            RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
        """
        rgn = str(region or self.default_region)
        res = str(resolution or self.default_resolution)
        timestamps = await self._fetch_timestamps(filter, rgn, res)
        return pd.to_datetime(timestamps, unit="ms", utc=True).tz_convert(self.tz)

    # ── Hilfsmethoden ─────────────────────────────────────────────────────────

    def _build_df(self, raw: dict, filter: Filter, region: str, dropna: bool) -> pd.DataFrame:
        """Konvertiert Rohdaten-Paare in einen vollständig annotierten DataFrame.

        Args:
            raw (dict): Mapping von Unix-Millisekunden-Zeitstempel zu Messwert.
            filter (Filter): Der Quell-Filter, dessen Metadaten (label, unit) übernommen werden.
            region (str): Regionscode, der als Spalte gespeichert wird.
            dropna (bool): Wenn True, werden Zeilen mit NaN-Werten entfernt.

        Returns:
            pd.DataFrame: DataFrame mit DatetimeIndex (``Europe/Berlin``) und den Spalten
            ``value``, ``unit``, ``filter``, ``label``, ``region``.
        """
        df = pd.DataFrame(sorted(raw.items()), columns=["timestamp_ms", "value"])
        df["timestamp"] = (pd.to_datetime(df["timestamp_ms"], unit="ms", utc=True)
                           .dt.tz_convert(self.tz))
        df = df.set_index("timestamp").drop(columns=["timestamp_ms"])
        df["unit"]   = filter.unit
        df["filter"] = filter.name
        df["label"]  = filter.label
        df["region"] = region
        return df.dropna(subset=["value"]) if dropna else df

    @staticmethod
    def _empty_df() -> pd.DataFrame:
        """Erstellt einen leeren DataFrame mit dem Standard-Spalten-Schema.

        Returns:
            pd.DataFrame: Leerer DataFrame mit den Spalten
            ``value``, ``unit``, ``filter``, ``label``, ``region``.
        """
        return pd.DataFrame(columns=["value", "unit", "filter", "label", "region"])

__aenter__() async

Öffnet eine HTTP-Session und gibt den Client zurück.

Returns:

Name Type Description
SmardClient 'SmardClient'

Der initialisierte Client.

Source code in datadancer/sources/smard/client.py
65
66
67
68
69
70
71
72
async def __aenter__(self) -> "SmardClient":
    """Öffnet eine HTTP-Session und gibt den Client zurück.

    Returns:
        SmardClient: Der initialisierte Client.
    """
    self._client = httpx.AsyncClient(headers=_HEADERS, timeout=_TIMEOUT)
    return self

__aexit__(*_) async

Schließt die HTTP-Session beim Verlassen des Kontextmanagers.

Source code in datadancer/sources/smard/client.py
74
75
76
async def __aexit__(self, *_) -> None:
    """Schließt die HTTP-Session beim Verlassen des Kontextmanagers."""
    await self.close()

__init__(region=Region.DE, resolution=Resolution.HOUR, tz='Europe/Berlin', retries=3)

Initialisiert den SmardClient mit Standard-Verbindungsparametern.

Source code in datadancer/sources/smard/client.py
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(
    self,
    region: Region = Region.DE,
    resolution: Resolution = Resolution.HOUR,
    tz: str = "Europe/Berlin",
    retries: int = 3,
) -> None:
    """Initialisiert den SmardClient mit Standard-Verbindungsparametern."""
    self.default_region = region
    self.default_resolution = resolution
    self.tz = tz
    self._retries = retries
    self._client: httpx.AsyncClient | None = None

close() async

Schließt den zugrundeliegenden HTTP-Client und setzt ihn auf None.

Source code in datadancer/sources/smard/client.py
78
79
80
81
82
async def close(self) -> None:
    """Schließt den zugrundeliegenden HTTP-Client und setzt ihn auf None."""
    if self._client:
        await self._client.aclose()
        self._client = None

fetch(filter, *, region=None, resolution=None, start=None, end=None, dropna=True) async

Lädt eine vollständige Zeitreihe für einen SMARD-Filter als DataFrame.

Parameters:

Name Type Description Default
filter Filter

Der gewünschte SMARD-Datenkategorie-Filter.

required
region Region | None

Region; nutzt default_region falls None.

None
resolution Resolution | None

Zeitauflösung; nutzt default_resolution falls None.

None
start datetime | None

Optionaler Startzeitpunkt (inklusiv).

None
end datetime | None

Optionaler Endzeitpunkt (inklusiv).

None
dropna bool

Wenn True, werden Zeilen ohne Messwert entfernt.

True

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame mit DatetimeIndex (Europe/Berlin) und den Spalten

DataFrame

value, unit, filter, label, region.

Raises:

Type Description
RuntimeError

Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.

Source code in datadancer/sources/smard/client.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
async def fetch(
    self,
    filter: Filter,
    *,
    region: Region | None = None,
    resolution: Resolution | None = None,
    start: datetime | None = None,
    end: datetime | None = None,
    dropna: bool = True,
) -> pd.DataFrame:
    """Lädt eine vollständige Zeitreihe für einen SMARD-Filter als DataFrame.

    Args:
        filter (Filter): Der gewünschte SMARD-Datenkategorie-Filter.
        region (Region | None): Region; nutzt ``default_region`` falls None.
        resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
        start (datetime | None): Optionaler Startzeitpunkt (inklusiv).
        end (datetime | None): Optionaler Endzeitpunkt (inklusiv).
        dropna (bool): Wenn True, werden Zeilen ohne Messwert entfernt.

    Returns:
        pd.DataFrame: DataFrame mit DatetimeIndex (``Europe/Berlin``) und den Spalten
        ``value``, ``unit``, ``filter``, ``label``, ``region``.

    Raises:
        RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
    """
    rgn = str(region or self.default_region)
    res = str(resolution or self.default_resolution)

    timestamps = await self._fetch_timestamps(filter, rgn, res)
    if not timestamps:
        return self._empty_df()

    ts_start = int(start.timestamp() * 1000) if start else 0
    ts_end   = int(end.timestamp() * 1000) if end else float("inf")
    relevant = [t for t in timestamps if ts_start <= t <= ts_end]
    if not relevant:
        return self._empty_df()

    chunks = await asyncio.gather(
        *[self._fetch_series(filter, rgn, res, ts) for ts in relevant]
    )
    raw = {ts: val for chunk in chunks for ts, val in chunk}
    return self._build_df(raw, filter, rgn, dropna)

fetch_day_ahead_prices(*, resolution=None, hours=48) async

Lädt Day-Ahead-Marktpreise (DE/LU) über den table_data-Endpunkt.

Verwendet immer Filter 4169 (DAY_AHEAD_DE_LU) und Region "DE" — API-seitig sind Preisdaten nur über table_data abrufbar und ausschließlich für "DE" verfügbar.

Parameters:

Name Type Description Default
resolution Resolution | None

Zeitauflösung; nutzt default_resolution falls None.

None
hours int

Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

48

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame mit DatetimeIndex (Europe/Berlin) und der Spalte

DataFrame

price_eur_per_mwh (Day-Ahead-Preis in €/MWh).

Raises:

Type Description
RuntimeError

Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.

Source code in datadancer/sources/smard/client.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
async def fetch_day_ahead_prices(
    self,
    *,
    resolution: Resolution | None = None,
    hours: int = 48,
) -> pd.DataFrame:
    """Lädt Day-Ahead-Marktpreise (DE/LU) über den table_data-Endpunkt.

    Verwendet immer Filter 4169 (DAY_AHEAD_DE_LU) und Region "DE" — API-seitig
    sind Preisdaten nur über table_data abrufbar und ausschließlich für "DE" verfügbar.

    Args:
        resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
        hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

    Returns:
        pd.DataFrame: DataFrame mit DatetimeIndex (``Europe/Berlin``) und der Spalte
        ``price_eur_per_mwh`` (Day-Ahead-Preis in €/MWh).

    Raises:
        RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
    """
    res = str(resolution or self.default_resolution)
    timestamps = await self._fetch_timestamps(Filter.DAY_AHEAD_DE_LU, "DE", res)
    if not timestamps:
        return pd.DataFrame(columns=["price_eur_per_mwh"])

    series = await self._fetch_price_series(Filter.DAY_AHEAD_DE_LU, "DE", res, timestamps[-1])
    raw = {ts: val for ts, val in series}
    df = self._build_df(raw, Filter.DAY_AHEAD_DE_LU, "DE", dropna=True)

    if df.empty:
        return pd.DataFrame(columns=["price_eur_per_mwh"])

    cutoff = df.index[-1] - timedelta(hours=hours)
    return df.loc[df.index >= cutoff, ["value"]].rename(columns={"value": "price_eur_per_mwh"})

fetch_generation_mix(*, region=None, resolution=None, hours=48) async

Lädt den vollständigen Stromerzeugungsmix als Wide-DataFrame mit lesbaren Spaltennamen.

Enthält: Biomasse, Wasserkraft, Wind Onshore/Offshore, Photovoltaik, Sonstige Erneuerbare, Braunkohle, Steinkohle, Erdgas und Kernenergie.

Parameters:

Name Type Description Default
region Region | None

Region; nutzt default_region falls None.

None
resolution Resolution | None

Zeitauflösung; nutzt default_resolution falls None.

None
hours int

Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

48

Returns:

Type Description
DataFrame

pd.DataFrame: Wide-DataFrame mit DatetimeIndex und einer Spalte pro Energieträger

DataFrame

(Spaltennamen entsprechen den deutschen Label-Strings der Filter).

Raises:

Type Description
RuntimeError

Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.

Source code in datadancer/sources/smard/client.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
async def fetch_generation_mix(
    self,
    *,
    region: Region | None = None,
    resolution: Resolution | None = None,
    hours: int = 48,
) -> pd.DataFrame:
    """Lädt den vollständigen Stromerzeugungsmix als Wide-DataFrame mit lesbaren Spaltennamen.

    Enthält: Biomasse, Wasserkraft, Wind Onshore/Offshore, Photovoltaik, Sonstige
    Erneuerbare, Braunkohle, Steinkohle, Erdgas und Kernenergie.

    Args:
        region (Region | None): Region; nutzt ``default_region`` falls None.
        resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
        hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

    Returns:
        pd.DataFrame: Wide-DataFrame mit DatetimeIndex und einer Spalte pro Energieträger
        (Spaltennamen entsprechen den deutschen Label-Strings der Filter).

    Raises:
        RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
    """
    filters = [
        Filter.BIOMASSE, Filter.WASSERKRAFT, Filter.WIND_ONSHORE,
        Filter.WIND_OFFSHORE, Filter.PHOTOVOLTAIK, Filter.SONSTIGE_ERNEUERBARE,
        Filter.BRAUNKOHLE, Filter.STEINKOHLE, Filter.ERDGAS,
        Filter.KERNENERGIE,
    ]
    df = await self.fetch_multi(filters, region=region, resolution=resolution,
                                hours=hours, wide=True)
    df.columns = [Filter[c].label if c in Filter.__members__ else c for c in df.columns]
    return df

fetch_latest(filter, *, hours=48, region=None, resolution=None) async

Lädt die letzten N Stunden ab dem neuesten verfügbaren Datenpunkt.

Parameters:

Name Type Description Default
filter Filter

Der gewünschte SMARD-Datenkategorie-Filter.

required
hours int

Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

48
region Region | None

Region; nutzt default_region falls None.

None
resolution Resolution | None

Zeitauflösung; nutzt default_resolution falls None.

None

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame mit DatetimeIndex (Europe/Berlin) und den Spalten

DataFrame

value, unit, filter, label, region.

Raises:

Type Description
RuntimeError

Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.

Source code in datadancer/sources/smard/client.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
async def fetch_latest(
    self,
    filter: Filter,
    *,
    hours: int = 48,
    region: Region | None = None,
    resolution: Resolution | None = None,
) -> pd.DataFrame:
  """Lädt die letzten N Stunden ab dem neuesten verfügbaren Datenpunkt.

  Args:
      filter (Filter): Der gewünschte SMARD-Datenkategorie-Filter.
      hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.
      region (Region | None): Region; nutzt ``default_region`` falls None.
      resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.

  Returns:
      pd.DataFrame: DataFrame mit DatetimeIndex (``Europe/Berlin``) und den Spalten
      ``value``, ``unit``, ``filter``, ``label``, ``region``.

  Raises:
      RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
  """
  rgn = str(region or self.default_region)
  res = str(resolution or self.default_resolution)

  timestamps = await self._fetch_timestamps(filter, rgn, res)
  if not timestamps:
      return self._empty_df()

  # Letzten Block ohne Zeitfilter laden
  last_ts = timestamps[-1]
  series = await self._fetch_series(filter, rgn, res, last_ts)

  # Rohdaten in DataFrame umwandeln
  raw = {ts: val for ts, val in series}
  df = self._build_df(raw, filter, rgn, dropna=True)

  if df.empty:
      return df

  # Auf gewünschte Stunden beschneiden
  cutoff = df.index[-1] - timedelta(hours=hours)
  return df[df.index >= cutoff]

fetch_multi(filters, *, region=None, resolution=None, start=None, end=None, hours=None, dropna=True, wide=True) async

Ruft mehrere SMARD-Filter parallel ab und kombiniert sie in einem DataFrame.

Parameters:

Name Type Description Default
filters Sequence[Filter]

Liste der abzurufenden Filter.

required
region Region | None

Region; nutzt default_region falls None.

None
resolution Resolution | None

Zeitauflösung; nutzt default_resolution falls None.

None
start datetime | None

Optionaler Startzeitpunkt; ignoriert wenn hours gesetzt.

None
end datetime | None

Optionaler Endzeitpunkt; ignoriert wenn hours gesetzt.

None
hours int | None

Wenn angegeben, wird fetch_latest pro Filter verwendet.

None
dropna bool

Wenn True, werden Zeilen ohne Messwert entfernt (nur bei hours=None).

True
wide bool

Wenn True, wird ein Wide-Format mit einem Filter pro Spalte zurückgegeben.

True

Returns:

Type Description
DataFrame

pd.DataFrame: Kombinierter DataFrame. Im Wide-Format eine Spalte pro Filter (Enum-Name

DataFrame

als Spaltenüberschrift); im Long-Format ein gestapelter DataFrame mit allen Metadatenspalten.

Raises:

Type Description
RuntimeError

Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.

Source code in datadancer/sources/smard/client.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
async def fetch_multi(
    self,
    filters: Sequence[Filter],
    *,
    region: Region | None = None,
    resolution: Resolution | None = None,
    start: datetime | None = None,
    end: datetime | None = None,
    hours: int | None = None,
    dropna: bool = True,
    wide: bool = True,
) -> pd.DataFrame:
  """Ruft mehrere SMARD-Filter parallel ab und kombiniert sie in einem DataFrame.

  Args:
      filters (Sequence[Filter]): Liste der abzurufenden Filter.
      region (Region | None): Region; nutzt ``default_region`` falls None.
      resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
      start (datetime | None): Optionaler Startzeitpunkt; ignoriert wenn ``hours`` gesetzt.
      end (datetime | None): Optionaler Endzeitpunkt; ignoriert wenn ``hours`` gesetzt.
      hours (int | None): Wenn angegeben, wird ``fetch_latest`` pro Filter verwendet.
      dropna (bool): Wenn True, werden Zeilen ohne Messwert entfernt (nur bei ``hours=None``).
      wide (bool): Wenn True, wird ein Wide-Format mit einem Filter pro Spalte zurückgegeben.

  Returns:
      pd.DataFrame: Kombinierter DataFrame. Im Wide-Format eine Spalte pro Filter (Enum-Name
      als Spaltenüberschrift); im Long-Format ein gestapelter DataFrame mit allen Metadatenspalten.

  Raises:
      RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
  """
  if hours is not None:
      # fetch_latest pro Filter — lädt letzten Block und beschneidet auf hours
      dfs = await asyncio.gather(*[
          self.fetch_latest(f, hours=hours, region=region, resolution=resolution)
          for f in filters
      ])
  else:
      dfs = await asyncio.gather(*[
          self.fetch(f, region=region, resolution=resolution,
                     start=start, end=end, dropna=dropna)
          for f in filters
      ])

  if wide:
      combined = pd.concat(
          [df["value"].rename(f.name) for f, df in zip(filters, dfs) if not df.empty],
          axis=1,
      )
      combined.index.name = "timestamp"
      return combined
  return pd.concat([df for df in dfs if not df.empty], ignore_index=False)

fetch_price_and_load(*, region=None, resolution=None, hours=48) async

Lädt Day-Ahead-Strompreis und Gesamtlast kombiniert in einem Wide-DataFrame.

Parameters:

Name Type Description Default
region Region | None

Region; nutzt default_region falls None.

None
resolution Resolution | None

Zeitauflösung; nutzt default_resolution falls None.

None
hours int

Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

48

Returns:

Type Description
DataFrame

pd.DataFrame: Wide-DataFrame mit DatetimeIndex und den Spalten

DataFrame

price_eur_mwh (Day-Ahead-Preis in €/MWh) und load_mwh (Gesamtlast in MWh).

Raises:

Type Description
RuntimeError

Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.

Source code in datadancer/sources/smard/client.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
async def fetch_price_and_load(
    self,
    *,
    region: Region | None = None,
    resolution: Resolution | None = None,
    hours: int = 48,
) -> pd.DataFrame:
    """Lädt Day-Ahead-Strompreis und Gesamtlast kombiniert in einem Wide-DataFrame.

    Args:
        region (Region | None): Region; nutzt ``default_region`` falls None.
        resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
        hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

    Returns:
        pd.DataFrame: Wide-DataFrame mit DatetimeIndex und den Spalten
        ``price_eur_mwh`` (Day-Ahead-Preis in €/MWh) und ``load_mwh`` (Gesamtlast in MWh).

    Raises:
        RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
    """
    df = await self.fetch_multi(
        [Filter.DAY_AHEAD_DE_LU, Filter.GESAMT_VERBRAUCH],
        region=region, resolution=resolution, hours=hours, wide=True,
    )
    df.columns = ["price_eur_mwh", "load_mwh"]
    return df

fetch_renewable_share(*, region=None, resolution=None, hours=48) async

Berechnet den Erneuerbaren-Anteil an der Gesamterzeugung.

Aggregiert Biomasse, Wasserkraft, Wind Onshore/Offshore, Photovoltaik und Sonstige Erneuerbare sowie konventionelle Träger (Braun-/Steinkohle, Erdgas, Kernenergie) und berechnet den prozentualen Anteil der Erneuerbaren.

Parameters:

Name Type Description Default
region Region | None

Region; nutzt default_region falls None.

None
resolution Resolution | None

Zeitauflösung; nutzt default_resolution falls None.

None
hours int

Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

48

Returns:

Type Description
DataFrame

pd.DataFrame: DataFrame mit DatetimeIndex und den Spalten renewable_mwh

DataFrame

(erneuerbare Erzeugung), total_mwh (Gesamterzeugung) und

DataFrame

renewable_pct (Anteil in Prozent, gerundet auf eine Dezimalstelle).

Raises:

Type Description
RuntimeError

Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.

Source code in datadancer/sources/smard/client.py
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
async def fetch_renewable_share(
    self,
    *,
    region: Region | None = None,
    resolution: Resolution | None = None,
    hours: int = 48,
) -> pd.DataFrame:
    """Berechnet den Erneuerbaren-Anteil an der Gesamterzeugung.

    Aggregiert Biomasse, Wasserkraft, Wind Onshore/Offshore, Photovoltaik und Sonstige
    Erneuerbare sowie konventionelle Träger (Braun-/Steinkohle, Erdgas, Kernenergie)
    und berechnet den prozentualen Anteil der Erneuerbaren.

    Args:
        region (Region | None): Region; nutzt ``default_region`` falls None.
        resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.
        hours (int): Anzahl der Stunden, die vom letzten Datenpunkt zurück geladen werden.

    Returns:
        pd.DataFrame: DataFrame mit DatetimeIndex und den Spalten ``renewable_mwh``
        (erneuerbare Erzeugung), ``total_mwh`` (Gesamterzeugung) und
        ``renewable_pct`` (Anteil in Prozent, gerundet auf eine Dezimalstelle).

    Raises:
        RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
    """
    renewable    = [Filter.BIOMASSE, Filter.WASSERKRAFT, Filter.WIND_ONSHORE,
                    Filter.WIND_OFFSHORE, Filter.PHOTOVOLTAIK, Filter.SONSTIGE_ERNEUERBARE]
    conventional = [
          Filter.BRAUNKOHLE, Filter.STEINKOHLE, Filter.ERDGAS, Filter.KERNENERGIE
      ]

    re_df, conv_df = await asyncio.gather(
        self.fetch_multi(renewable,    region=region, resolution=resolution, hours=hours, wide=True),
        self.fetch_multi(conventional, region=region, resolution=resolution, hours=hours, wide=True),
    )
    result = pd.DataFrame(index=re_df.index.union(conv_df.index))
    result["renewable_mwh"] = re_df.sum(axis=1)
    result["total_mwh"]     = re_df.sum(axis=1) + conv_df.sum(axis=1)
    result["renewable_pct"] = (result["renewable_mwh"] / result["total_mwh"] * 100).round(1)
    return result.dropna()

list_available_timestamps(filter, *, region=None, resolution=None) async

Gibt alle verfügbaren Block-Zeitstempel für einen Filter als DatetimeIndex zurück.

Parameters:

Name Type Description Default
filter Filter

Der gewünschte SMARD-Datenkategorie-Filter.

required
region Region | None

Region; nutzt default_region falls None.

None
resolution Resolution | None

Zeitauflösung; nutzt default_resolution falls None.

None

Returns:

Type Description
DatetimeIndex

pd.DatetimeIndex: Zeitzonenbewusster Index (Europe/Berlin) aller verfügbaren

DatetimeIndex

Datenblock-Zeitstempel.

Raises:

Type Description
RuntimeError

Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.

Source code in datadancer/sources/smard/client.py
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
async def list_available_timestamps(
    self,
    filter: Filter,
    *,
    region: Region | None = None,
    resolution: Resolution | None = None,
) -> pd.DatetimeIndex:
    """Gibt alle verfügbaren Block-Zeitstempel für einen Filter als DatetimeIndex zurück.

    Args:
        filter (Filter): Der gewünschte SMARD-Datenkategorie-Filter.
        region (Region | None): Region; nutzt ``default_region`` falls None.
        resolution (Resolution | None): Zeitauflösung; nutzt ``default_resolution`` falls None.

    Returns:
        pd.DatetimeIndex: Zeitzonenbewusster Index (``Europe/Berlin``) aller verfügbaren
        Datenblock-Zeitstempel.

    Raises:
        RuntimeError: Wenn die SMARD-API nach allen Wiederholungsversuchen nicht erreichbar ist.
    """
    rgn = str(region or self.default_region)
    res = str(resolution or self.default_resolution)
    timestamps = await self._fetch_timestamps(filter, rgn, res)
    return pd.to_datetime(timestamps, unit="ms", utc=True).tz_convert(self.tz)