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
5 changes: 4 additions & 1 deletion .claude/specs/spec-02.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ This is the simplest way for a customer to bring their own data without
implementing `SignalProvider` from scratch.

Expected CSV format:

```csv
timestamp,value
2026-03-15T00:00:00+01:00,42.31
Expand Down Expand Up @@ -164,6 +165,7 @@ nexa run examples/simple_da_algo.py \
```

The CLI needs to:

- Accept a path to a Python file containing an algo
- Import the file and find the algo (look for a subclass of `SimpleAlgo`
or a function decorated with `@algo`)
Expand Down Expand Up @@ -208,6 +210,7 @@ synthetic forecast values (slightly noisy version of the actual clearing
prices from the test fixture, offset by the publication delay).

The example should be runnable via:

```bash
nexa run examples/simple_da_algo.py \
--exchange nordpool \
Expand All @@ -225,7 +228,7 @@ nexa run examples/simple_da_algo.py \
For this task, signal CSV files are discovered by convention. The engine
looks in `{data_dir}/signals/` for CSV files matching the signal name:

```
```text
data_dir/
signals/
price_forecast.csv
Expand Down
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"docstrings",
"EPEX",
"intraday",
"lookback",
"marketdata",
"matplotlib",
"Mypy",
Expand All @@ -23,6 +24,9 @@
"quants",
"Scikit",
"sklearn",
"teardown",
"timedelta",
"timezone",
"VWAP",
"Zipline"
]
Expand Down
11 changes: 6 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.PHONY: install lint typecheck test ci test-notebooks execute-notebooks
.PHONY: install lint typecheck test ci
# test-notebooks execute-notebooks

install:
poetry install
Expand All @@ -15,8 +16,8 @@ test:

ci: lint typecheck test

test-notebooks:
poetry run jupyter nbconvert --to notebook --execute notebooks/*.ipynb --output-dir /tmp/
# test-notebooks:
# poetry run jupyter nbconvert --to notebook --execute notebooks/*.ipynb --output-dir /tmp/

execute-notebooks:
poetry run jupyter nbconvert --to notebook --execute --inplace notebooks/*.ipynb
# execute-notebooks:
# poetry run jupyter nbconvert --to notebook --execute --inplace notebooks/*.ipynb
77 changes: 77 additions & 0 deletions examples/simple_da_algo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Example DA algo: buy when the clearing price is below a price forecast.

Demonstrates the signal system: the algo subscribes to a price forecast CSV
and places buy orders only when the forecast is meaningfully above the
current clearing price, suggesting the market is undervaluing the delivery
period.

Run via::

nexa run examples/simple_da_algo.py \\
--exchange nordpool \\
--start 2026-03-01 \\
--end 2026-03-31 \\
--products NO1_DA \\
--data-dir tests/fixtures \\
--capital 100000
"""

from __future__ import annotations

from decimal import Decimal

from nexa_backtest.algo import SimpleAlgo
from nexa_backtest.context import TradingContext
from nexa_backtest.exceptions import SignalError
from nexa_backtest.types import AuctionInfo, Fill, Order


class ForecastAlgo(SimpleAlgo):
"""Buy when the DA clearing price is below the price forecast minus a threshold.

The algo subscribes to a ``price_forecast`` signal (loaded from
``{data_dir}/signals/price_forecast.csv``) and places a buy order for
each delivery product where the forecast indicates the price will be at
least ``threshold`` EUR/MWh above the clearing price.

Because we fill at the clearing price (price-taker assumption), the order
is placed at ``forecast - threshold``. If that bid is >= the clearing
price, we fill. This means the algo selects delivery periods it expects to
be cheap relative to the forecast, accumulating positive VWAP alpha.
"""

def on_setup(self, ctx: TradingContext) -> None:
"""Subscribe to the price forecast signal and set the bid threshold."""
self.subscribe_signal("price_forecast")
self.threshold: Decimal = Decimal("5.0")
self.volume_mw: Decimal = Decimal("10")

def on_auction_open(self, ctx: TradingContext, auction: AuctionInfo) -> None:
"""Place a buy order when the forecast exceeds clearing + threshold."""
try:
signal = ctx.get_signal("price_forecast")
except SignalError:
# No forecast available yet for this period — skip
return

forecast = Decimal(str(signal.value))
bid_price = forecast - self.threshold

ctx.place_order(
Order.buy(
product_id=auction.product_id,
volume_mw=self.volume_mw,
price_eur_mwh=bid_price,
)
)

def on_fill(self, ctx: TradingContext, fill: Fill) -> None:
"""Log each fill for visibility."""
ctx.log(
f"FILL {fill.side} {fill.volume} MW @ {fill.price} EUR/MWh "
f"[{fill.product_id}]"
)

def on_teardown(self, ctx: TradingContext) -> None:
"""Log the final unrealised PnL."""
ctx.log(f"Backtest complete. Unrealised PnL: {ctx.get_unrealised_pnl()} EUR")
171 changes: 166 additions & 5 deletions poetry.lock

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

7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ python = "^3.11"
pydantic = ">=2.0"
pyarrow = ">=14.0"
numpy = ">=1.26"
pandas = ">=2.0"
click = ">=8.0"

[tool.poetry.scripts]
nexa = "nexa_backtest.cli.main:cli"

[tool.poetry.extras]
pandas = ["pandas"]
plot = ["matplotlib"]
ml = ["onnxruntime", "scikit-learn"]

Expand All @@ -24,6 +28,7 @@ pytest = ">=7.4"
pytest-cov = ">=4.1"
mypy = ">=1.8"
ruff = ">=0.3"
pandas-stubs = ">=2.0"

[tool.ruff]
target-version = "py311"
Expand Down
Loading
Loading