Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ NORDPOOL_PASSWORD=
# ENTSO-E Transparency Platform security token
# Obtain from: https://transparency.entsoe.eu/usrm/user/myAccountSettings
ENTSOE_API_KEY=

# EXAA (Energy Exchange Austria) — certificate-based authentication
# Register at: https://www.exaa.at/en/trading/registration
EXAA_USERNAME=
EXAA_PASSWORD=
EXAA_PRIVATE_KEY_PATH=/path/to/exaa_private_key.pem
EXAA_CERTIFICATE_PATH=/path/to/exaa_certificate.pem
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Handles 15-minute MTU resolution, rate limiting, response caching, timezone norm
| Core types & exceptions | ✅ |
| Unified `NexaClient` | 🚧 |
| ENTSO-E client | ✅ |
| EXAA client (AT day-ahead) | ✅ |
| EPEX SPOT client | ⬜ |
| EEX client | ⬜ |
| Response caching | ⬜ |
Expand Down Expand Up @@ -71,6 +72,10 @@ Set API credentials as environment variables:
export NORDPOOL_USERNAME="your-username"
export NORDPOOL_PASSWORD="your-password"
export ENTSOE_API_KEY="your-key-here"
export EXAA_USERNAME="your-exaa-username"
export EXAA_PASSWORD="your-exaa-password"
export EXAA_PRIVATE_KEY_PATH="/path/to/exaa_private_key.pem"
export EXAA_CERTIFICATE_PATH="/path/to/exaa_certificate.pem"
```

Or use a `.env` file (see `.env.example`).
Expand Down
126 changes: 119 additions & 7 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pydantic = ">=2.0"
pandas = ">=2.0"
tenacity = ">=8.0"
entsoe-py = ">=0.6"
nexa-connect-exaa = {git = "https://github.com/phasenexa/nexa-connect-exaa.git"}

[tool.poetry.group.dev.dependencies]
pytest = ">=8.0"
Expand Down Expand Up @@ -60,6 +61,8 @@ ignore = []

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["ANN"]
# exaa.py wraps an untyped third-party library; Any is unavoidable there.
"src/nexa_marketdata/exaa.py" = ["ANN401"]

[tool.mypy]
python_version = "3.11"
Expand All @@ -72,6 +75,10 @@ explicit_package_bases = true
module = ["entsoe", "entsoe.*"]
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = ["nexa_connect_exaa", "nexa_connect_exaa.*"]
ignore_missing_imports = true

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=src/nexa_marketdata --cov-report=term-missing --cov-report=xml"
Expand Down
42 changes: 41 additions & 1 deletion src/nexa_marketdata/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pandas as pd

from nexa_marketdata.entsoe import ENTSOEClient
from nexa_marketdata.exaa import EXAAClient
from nexa_marketdata.exceptions import DataNotAvailableError
from nexa_marketdata.nordpool import NordPoolClient
from nexa_marketdata.types import BiddingZone, Resolution
Expand All @@ -28,7 +29,6 @@
BiddingZone.DK2,
BiddingZone.FI,
BiddingZone.DE_LU,
BiddingZone.AT,
BiddingZone.BE,
BiddingZone.NL,
BiddingZone.FR,
Expand All @@ -44,6 +44,16 @@
}
)

# Zones served by EXAA (Energy Exchange Austria).
# AT is EXAA's home market; the Classic auction at 10:15 CET is the
# authoritative Austrian day-ahead price and the only source of 15-minute
# products for AT.
_EXAA_ZONES: frozenset[BiddingZone] = frozenset(
{
BiddingZone.AT,
}
)


class NexaClient:
"""Unified client for European power market data sources.
Expand All @@ -55,13 +65,25 @@ class NexaClient:
``NORDPOOL_PASSWORD`` environment variable.
entsoe_api_key: ENTSO-E Transparency Platform security token. Falls
back to ``ENTSOE_API_KEY`` environment variable.
exaa_username: EXAA trading account username. Falls back to
``EXAA_USERNAME`` environment variable.
exaa_password: EXAA trading account password. Falls back to
``EXAA_PASSWORD`` environment variable.
exaa_private_key_path: Path to EXAA RSA private key PEM file. Falls
back to ``EXAA_PRIVATE_KEY_PATH`` environment variable.
exaa_certificate_path: Path to EXAA X.509 certificate PEM file. Falls
back to ``EXAA_CERTIFICATE_PATH`` environment variable.
"""

def __init__(
self,
nordpool_username: str | None = None,
nordpool_password: str | None = None,
entsoe_api_key: str | None = None,
exaa_username: str | None = None,
exaa_password: str | None = None,
exaa_private_key_path: str | None = None,
exaa_certificate_path: str | None = None,
) -> None:
username = nordpool_username or os.environ.get("NORDPOOL_USERNAME")
password = nordpool_password or os.environ.get("NORDPOOL_PASSWORD")
Expand All @@ -73,6 +95,16 @@ def __init__(
ENTSOEClient(self._entsoe_api_key) if self._entsoe_api_key else None
)

exaa_user = exaa_username or os.environ.get("EXAA_USERNAME")
exaa_pass = exaa_password or os.environ.get("EXAA_PASSWORD")
exaa_key = exaa_private_key_path or os.environ.get("EXAA_PRIVATE_KEY_PATH")
exaa_cert = exaa_certificate_path or os.environ.get("EXAA_CERTIFICATE_PATH")
self._exaa = (
EXAAClient(exaa_user, exaa_pass, exaa_key, exaa_cert)
if (exaa_user and exaa_pass and exaa_key and exaa_cert)
else None
)

def day_ahead_prices(
self,
zone: BiddingZone,
Expand Down Expand Up @@ -114,6 +146,14 @@ def day_ahead_prices(
return self._entsoe.day_ahead_prices(
zone, start, end, resolution=resolution
)
if zone in _EXAA_ZONES:
if self._exaa is None:
raise DataNotAvailableError(
f"No EXAA credentials configured for zone {zone!r}. "
"Set exaa_username, exaa_password, exaa_private_key_path, and "
"exaa_certificate_path or the EXAA_* environment variables."
)
return self._exaa.day_ahead_prices(zone, start, end, resolution=resolution)
raise DataNotAvailableError(
f"No data source available for bidding zone {zone!r}."
)
Loading
Loading