Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .claude/commands/checks.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Run the following checks:

1. All tests pass using `make ci` (lint, typechecks, test and testnotebooks etc.)
1. Execute all notebooks `make execute-notebooks`
1. Review the README file, check nothing major is missing, suggest additions if something is identified
1. Test coverage should be >80%
1. Review the README file, check nothing major is missing, add additions if something is identified
2 changes: 1 addition & 1 deletion .claude/commands/pr.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Commit all changes to git and raise a PR (unless you're already in an active PR branch?).
Use a branch, as we do TBD-style dev and the main branch is protected.
Use a branch, as we do TBD-style dev and the main branch is protected.
3 changes: 2 additions & 1 deletion .claude/commands/release.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Create release: $ARGUMENTS

1. Read @README.md and @CONTRIBUTING.md file to understand how to publish this release.
1. Perform the git cli commands
1. Use gh to create the release and publish

NOTE: The PR will run a few guardrail checks, so you'll need to wait for these to complete before merging.
NOTE: The PR will run a few guardrail checks, so you'll need to wait for these to complete before merging.
2 changes: 1 addition & 1 deletion .claude/commands/rmbranch.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
If you're not in the main branch, delete the local git branch you're in and move back to main.
Once done, pull the latest main from origin.
Once done, pull the latest main from origin.
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,16 @@ jobs:
- name: Run mypy type checking
run: poetry run mypy src

#- name: Run example notebooks
# run: make test-notebooks
- name: Run example notebooks
run: make test-notebooks
env:
ENTSOE_API_KEY: ${{ secrets.ENTSOE_API_KEY }}

- name: Run tests with coverage
run: |
poetry run pytest --cov=src/nexa_marketdata --cov-report=xml --cov-report=term
env:
ENTSOE_API_KEY: ${{ secrets.ENTSOE_API_KEY }}

- name: Check coverage threshold
run: |
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,13 @@ jobs:

- name: Run example notebooks
run: make test-notebooks
env:
ENTSOE_API_KEY: ${{ secrets.ENTSOE_API_KEY }}

- name: Run tests with coverage
run: poetry run pytest --cov=src/nexa_marketdata --cov-report=xml --cov-report=term
env:
ENTSOE_API_KEY: ${{ secrets.ENTSOE_API_KEY }}

- name: Check coverage threshold
run: poetry run coverage report --fail-under=80
Expand Down
17 changes: 17 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"cSpell.words": [
"datetimes",
"ENTSO",
"entsoe",
"EPEX",
"intraday",
"kipe",
"marketdata",
"Nexa",
"Nord",
"nordpool",
"normalisation",
"Pydantic",
"pytest"
]
}
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
# nexa-marketdata

## What this is

A Python library providing a unified API client for European power market
data sources: Nord Pool, EPEX SPOT, ENTSO-E Transparency Platform, and EEX.
Handles 15-minute MTU resolution, rate limiting, response caching, timezone
normalisation, and format differences across exchanges.
Part of the Phase Nexa ecosystem.

## Audience

Quants, data scientists, and developers at energy trading companies who build
their own trading systems. They are experienced Python users who are currently
using fragile community wrappers (entsoe-py, kipe/nordpool) or hand-rolled
API clients. This library replaces all of that with a single, well-maintained
client.

## Code style

- Python 3.11+
- Type hints everywhere, strict mypy compliance
- Pydantic v2 for data models
Expand All @@ -28,6 +31,7 @@ client.
- Always prefer UK English unless using existing nomenclature popular in energy trading

## Domain context

- MTU = Market Time Unit. EU power markets transitioned to 15-minute
MTUs on 30 Sept 2025. Library must handle both 15-min and hourly resolution.
- ENTSO-E = European Network of Transmission System Operators for Electricity.
Expand All @@ -50,6 +54,7 @@ client.
be cached locally to reduce API load and improve performance.

## Testing

- pytest with fixtures for common data retrieval scenarios
- Use VCR.py or responses library to record/replay HTTP interactions
(never make live API calls in CI)
Expand All @@ -59,6 +64,7 @@ client.
- Run `make test` to run unit tests

## Do not

- Do not use float for prices or volumes. Use Decimal.
- Do not create naive datetimes. Always use timezone-aware.
- Do not add unnecessary dependencies.
Expand All @@ -68,6 +74,7 @@ client.
- Do not scrape Nord Pool's website. Use their official API only.

## Workflow (Trunk-Based Development)

- **NEVER push directly to main**. The main branch is protected.
- **Always work on feature branches**: Create a short-lived feature branch for each task (e.g. `feat/add-entsoe-client`, `fix/nordpool-rate-limit`)
- **Create pull requests**: When work is complete, create a PR to merge into main
Expand Down Expand Up @@ -97,7 +104,7 @@ client.

## Code layout

```
```text
nexa-marketdata/
src/nexa_marketdata/
__init__.py
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Handles 15-minute MTU resolution, rate limiting, response caching, timezone norm
| Nord Pool — day-ahead prices | ✅ |
| Core types & exceptions | ✅ |
| Unified `NexaClient` | 🚧 |
| ENTSO-E client | |
| ENTSO-E client | |
| EPEX SPOT client | ⬜ |
| EEX client | ⬜ |
| Response caching | ⬜ |
Expand Down
617 changes: 617 additions & 0 deletions examples/entsoe_day_ahead_prices.ipynb

Large diffs are not rendered by default.

72 changes: 66 additions & 6 deletions poetry.lock

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

9 changes: 8 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ readme = "README.md"
license = "MIT"
homepage = "https://github.com/phasenexa/nexa-marketdata"
repository = "https://github.com/phasenexa/nexa-marketdata"
keywords = ["energy", "power", "market-data", "nordpool", "entsoe", "epex", "eex"]
keywords = ["energy", "power", "market-data", "nordpool", "entsoe", "entso-e", "epex", "eex"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
Expand All @@ -27,6 +27,7 @@ httpx = ">=0.27"
pydantic = ">=2.0"
pandas = ">=2.0"
tenacity = ">=8.0"
entsoe-py = ">=0.6"

[tool.poetry.group.dev.dependencies]
pytest = ">=8.0"
Expand All @@ -41,6 +42,8 @@ jupyter = ">=1.0"
nbconvert = ">=7.0"
ipykernel = ">=6.0"
matplotlib = ">=3.8"
python-dotenv = ">=1.0"
types-requests = ">=2.31"

[build-system]
requires = ["poetry-core"]
Expand All @@ -65,6 +68,10 @@ mypy_path = "src"
namespace_packages = true
explicit_package_bases = true

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

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=src/nexa_marketdata --cov-report=term-missing --cov-report=xml"
Expand Down
21 changes: 21 additions & 0 deletions src/nexa_marketdata/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pandas as pd

from nexa_marketdata.entsoe import ENTSOEClient
from nexa_marketdata.exceptions import DataNotAvailableError
from nexa_marketdata.nordpool import NordPoolClient
from nexa_marketdata.types import BiddingZone, Resolution
Expand Down Expand Up @@ -35,6 +36,14 @@
}
)

# Zones only available via ENTSO-E (not on Nord Pool Data Portal)
_ENTSOE_ZONES: frozenset[BiddingZone] = frozenset(
{
BiddingZone.CH,
BiddingZone.GB,
}
)


class NexaClient:
"""Unified client for European power market data sources.
Expand All @@ -60,6 +69,9 @@ def __init__(
NordPoolClient(username, password) if (username and password) else None
)
self._entsoe_api_key = entsoe_api_key or os.environ.get("ENTSOE_API_KEY")
self._entsoe = (
ENTSOEClient(self._entsoe_api_key) if self._entsoe_api_key else None
)

def day_ahead_prices(
self,
Expand Down Expand Up @@ -93,6 +105,15 @@ def day_ahead_prices(
return self._nordpool.day_ahead_prices(
zone, start, end, resolution=resolution
)
if zone in _ENTSOE_ZONES:
if self._entsoe is None:
raise DataNotAvailableError(
f"No ENTSO-E API key configured for zone {zone!r}. "
"Set entsoe_api_key or the ENTSOE_API_KEY environment variable."
)
return self._entsoe.day_ahead_prices(
zone, start, end, resolution=resolution
)
raise DataNotAvailableError(
f"No data source available for bidding zone {zone!r}."
)
Loading
Loading