@uk-energy/elexon 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # @uk-energy/elexon
2
+
3
+ ## 0.3.0
4
+
5
+ - `curtailment.wastedWind(date, opts)`: Octopus's "Wasted Wind" definition from the settlement
6
+ bid/offer stacks — SO-flagged wind bids (MWh, £ at bid price) plus the curtailed volume
7
+ re-bought from the cheapest non-SO offers. Matches wastedwind.energy to the penny on the
8
+ recorded periods; `aggregateWastedWind()` exported.
9
+ - `balancing.settlementStack(bidOffer, date, sp)` (ISPSTACK) and the `SettlementStackRow` type.
10
+ - Fixtures for 30 Sep 2026 SP20–21; 5 new tests.
11
+
12
+ ## 0.2.0
13
+
14
+ - `curtailment.forDay(date, opts)`: bid acceptances per unit per settlement period joined
15
+ from BOAV (MWh), EBOCF (£) and BOALF (`soFlag`), with fuel type, GSP group and capacity from
16
+ the reference list; per-unit and day totals. `aggregateCurtailment()` exported.
17
+ - `balancing.acceptanceVolumes(bidOffer, date, filter)` (BOAV) and
18
+ `balancing.cashflows(bidOffer, date, filter)` (EBOCF), per day or per period; unit lists
19
+ chunked at `MAX_UNITS_PER_REQUEST` (200).
20
+ - `reference.units({ fuelType, gspGroupId, leadParty })` and `unitId()` (falls back to the NGC
21
+ id for the 48 wind units without an Elexon id).
22
+ - Four more fixtures (three curtailed units on 15 Oct 2026, one uncurtailed); 7 new tests.
23
+
24
+ ## 0.1.0
25
+
26
+ - `ElexonClient` with `datasets.*` (`stream` for any `/datasets/{code}/stream`, plus
27
+ `boalf`, `bod`, `fuelinst`, `pn`, `pnStream`, `mels`, `mils`), `reference.*`
28
+ (`bmUnits`, `fuelTypes`) and `balancing.*` (`systemPrices`, `marketIndex`, `acceptances`,
29
+ `bidOffers`, `physical`).
30
+ - Types generated from the Insights OpenAPI 3.0.4 document (`openapi/insights-v1.json`,
31
+ 322 paths); `DatasetRow<"BOALF">` etc. derived from the spec so `stream()` is typed per
32
+ dataset code.
33
+ - Bare-array (`/stream`) and `{ data }` envelopes unwrapped; wrong shapes throw
34
+ `UkEnergyError`; settlement periods validated 1–50; date inputs validated or formatted via
35
+ `@uk-energy/core` dates.
36
+ - Default 10 rps / burst 20 per host; 5-minute cache TTL when a cache is supplied.
37
+ - 14 fixture-backed tests on 20 recorded responses (every method and both FUELINST query
38
+ modes; one API 400), shared with `uk_energy.elexon`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joshua Serpis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,151 @@
1
- # Temporary Holding Version
1
+ # @uk-energy/elexon
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typed client for [Elexon Insights](https://bmrs.elexon.co.uk/api-documentation) (BMRS): bid-offer
4
+ acceptances and prices, physical notifications, generation by fuel, BM unit reference data and
5
+ system prices. No API key. Types are generated from Elexon's OpenAPI document, so field names and
6
+ nullability are the API's own.
7
+
8
+ > Contains BMRS data © Elexon Limited copyright and database right 2026. Keep this line wherever
9
+ > you show the data. The [BMRS data licence](https://www.elexon.co.uk/data/balancing-mechanism-reporting-agent/copyright-licence-bmrs-data/)
10
+ > allows copying, adapting and selling derived works; it does not imply endorsement.
11
+
12
+ Python mirror: `uk_energy.elexon` in the `uk-energy` distribution.
13
+
14
+ ```bash
15
+ pnpm add @uk-energy/elexon
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { ElexonClient } from "@uk-energy/elexon";
22
+
23
+ const ex = new ElexonClient();
24
+
25
+ // Bid-offer acceptances for one hour, every unit. soFlag=true bids on wind units are curtailment.
26
+ const boalf = await ex.datasets.boalf({ from: "2026-10-15T00:00Z", to: "2026-10-15T01:00Z" });
27
+ const curtailed = boalf.filter((r) => r.soFlag && r.bmUnit?.endsWith("W-1"));
28
+
29
+ // Or by settlement period: from/to are then dates, not instants.
30
+ await ex.datasets.boalf({
31
+ from: "2026-10-15", to: "2026-10-15",
32
+ settlementPeriodFrom: 1, settlementPeriodTo: 4,
33
+ bmUnit: ["T_ARCHW-1"],
34
+ });
35
+
36
+ // Submitted bid/offer prices (BOD). Wind units bid negative: they are paid to turn down.
37
+ const bod = await ex.datasets.bod({ from: "2026-10-15T00:00Z", to: "2026-10-15T01:00Z", bmUnit: ["T_ARCHW-1"] });
38
+
39
+ // Generation by fuel type, 5-minute resolution
40
+ const fuel = await ex.datasets.fuelinst({ publishDateTimeFrom: "2026-10-15T00:00Z", publishDateTimeTo: "2026-10-15T00:30Z" });
41
+
42
+ // Every BM unit, with fuel type and capacity
43
+ const units = await ex.reference.bmUnits();
44
+ const wind = units.filter((u) => u.fuelType === "WIND"); // 287 on 20 Oct 2026
45
+
46
+ // System prices for a settlement day or one period
47
+ const prices = await ex.balancing.systemPrices("2026-10-15"); // 48 rows
48
+ const sp1 = await ex.balancing.systemPrices("2026-10-15", 1);
49
+
50
+ // Any other /datasets/{code}/stream endpoint, typed by code
51
+ const mels = await ex.datasets.stream("MELS", { from: "2026-10-15T00:00Z", to: "2026-10-15T01:00Z" });
52
+ ```
53
+
54
+ ### Curtailment
55
+
56
+ ```ts
57
+ // Every WIND unit (287), one settlement day: the reference list + 3 requests per 200 units (BOAV, EBOCF, BOALF)
58
+ const day = await ex.curtailment.forDay("2026-10-15");
59
+ day.total; // { curtailedMwh, soFlaggedMwh, cashflowGbp, units, acceptances }
60
+ day.units[0]; // biggest curtailed unit: { bmUnit, leadPartyName, gspGroupId, capacityMw, curtailedMwh, cashflowGbp, … }
61
+ day.periods; // one row per (unit, settlement period) with volume, £ and SO flag counts
62
+
63
+ // A few units, a few periods
64
+ await ex.curtailment.forDay("2026-10-15", { bmUnit: ["T_AKGLW-3"], settlementPeriodFrom: 1, settlementPeriodTo: 8 });
65
+
66
+ // The feeds behind it
67
+ await ex.balancing.acceptanceVolumes("bid", "2026-10-15", { bmUnit: ["T_AKGLW-3"] }); // BOAV, MWh per acceptance per period
68
+ await ex.balancing.cashflows("bid", "2026-10-15", { bmUnit: ["T_AKGLW-3"] }); // EBOCF, £ per unit per period
69
+ await ex.reference.units({ fuelType: ["WIND"], gspGroupId: ["_P", "_N"] }); // Scottish wind units
70
+ ```
71
+
72
+ ### Octopus's "Wasted Wind" figure
73
+
74
+ ```ts
75
+ const day = await ex.curtailment.wastedWind("2026-09-30");
76
+ day.total; // { curtailedMwh: 124226, curtailmentCostGbp: 4040737, turnUpMwh, turnUpCostGbp: 25986249, totalCostGbp: 30026986 }
77
+ day.periods[19]; // SP20 (09:30 BST): 2,415.70 MWh, £72,256.84 to switch off, £506,067.61 to replace
78
+ ```
79
+
80
+ [wastedwind.energy](https://wastedwind.energy)'s definition, reproduced from the settlement bid
81
+ and offer stacks (`balancing.settlementStack`): SO-flagged wind bids priced at their bid price,
82
+ plus the curtailed volume re-bought from the cheapest non-SO offers in the same period. Two
83
+ requests per settlement period. It is a definition, not settlement truth — see
84
+ [`examples/`](../../examples) for a month reproduced to 0.2% and a comparison with the settled
85
+ view below.
86
+
87
+ A curtailment is a **bid acceptance**: NESO accepts the unit's (negative-priced) bid to reduce
88
+ output, so the unit is *paid* to turn down and `cashflowGbp` is positive. `soFlaggedMwh` is the
89
+ share from acceptances BOALF marks `soFlag: true` — system (constraint) actions rather than energy
90
+ balancing. On 15 Oct 2026, AKGLW-3 was curtailed in 42 of 48 periods for 1,019 MWh and £49.5k,
91
+ all SO-flagged. `aggregateCurtailment()` is exported for callers with their own rows.
92
+
93
+ Every method is `async` and returns the API's rows unwrapped: `/stream` endpoints are bare arrays,
94
+ `/datasets/{code}` and `/balancing/*` endpoints come back as `{ data, metadata }` and only `data`
95
+ is returned. A response of the wrong shape throws `UkEnergyError`; an HTTP error throws
96
+ `HttpError` with the API's RFC 9110 problem details in `body`.
97
+
98
+ ### Surface
99
+
100
+ | Namespace | Methods |
101
+ | --- | --- |
102
+ | `datasets` | `stream(code, query)`, `boalf`, `bod`, `fuelinst`, `pn(date, sp, filter)`, `pnStream`, `mels`, `mils` |
103
+ | `reference` | `bmUnits()`, `fuelTypes()`, `units({ fuelType?, gspGroupId?, leadParty? })` |
104
+ | `balancing` | `systemPrices(date, sp?)`, `marketIndex`, `acceptances(date, sp, filter)`, `bidOffers(date, sp, filter)`, `physical(dataset, date, sp, filter)`, `acceptanceVolumes(bidOffer, date, filter)`, `cashflows(bidOffer, date, filter)`, `settlementStack(bidOffer, date, sp)` |
105
+ | `curtailment` | `forDay(date, { bmUnit?, fuelType?, settlementPeriodFrom?, settlementPeriodTo? })`, `wastedWind(date, { settlementPeriodFrom?, settlementPeriodTo?, fuelType?, concurrency? })` |
106
+
107
+ Row types are exported: `BoalfRow`, `BodRow`, `FuelinstRow`, `PnRow`, `MelsRow`, `BmUnit`,
108
+ `SystemPriceRow`, `MarketIndexRow`, `AcceptanceRow`, `BidOfferRow`, `PhysicalRow`, plus the
109
+ `SettlementWindow` / `UnitFilter` / `FuelinstQuery` argument types; the full
110
+ generated `paths`/`components` are under `@uk-energy/elexon` as well for anything not wrapped.
111
+
112
+ ## Things to know about the API
113
+
114
+ - **Settlement time is local.** A settlement day starts at Europe/London midnight; in BST,
115
+ SP1 of 15 Oct starts at `2026-10-14T23:00Z`. Days have 48 periods, 46 on the spring clock
116
+ change and 50 on the autumn one. `@uk-energy/core` exports `settlementPeriodAt`,
117
+ `settlementPeriodStart` and friends for the conversion.
118
+ - **`from`/`to` change meaning** on BOALF/BOD: instants normally, but calendar dates when
119
+ `settlementPeriodFrom`/`To` are given.
120
+ - **BOALF is a volume of levels, not a list of curtailment events.** One hour across all
121
+ units is ~1,650 rows; one settlement period of acceptances is ~730. Filter by `bmUnit`
122
+ where you can.
123
+ - **`reference/bmunits/all`** is ~3,100 units with capacities as strings (`"114.000"`), and
124
+ `gspGroupId` is `null` for 235 of the 287 wind units (transmission-connected), so GSP group
125
+ cannot place most wind farms on a constraint boundary. 48 wind units have no `elexonBmUnit`;
126
+ `unitId()` falls back to `nationalGridBmUnit`, which the API accepts as a filter too. The list
127
+ also has duplicates (`T_WLNYO-4` twice); `curtailment.forDay` de-duplicates ids.
128
+ - **Unit filters can be long.** The API accepted all 287 wind ids in one 4.9 KB URL; the client
129
+ splits lists at `MAX_UNITS_PER_REQUEST` (200) anyway. Unfiltered daily BOAV is ~9 MB.
130
+ - **Rate limits:** none published and none observed (310 requests at ~120 rps on 20 Oct 2026,
131
+ all 200). The client defaults to 10 rps / burst 20 per host, shared across clients in the
132
+ process; pass `rateLimit` to change it.
133
+ - **Errors** are RFC 9110 problem details with a `400` for a `from` later than `to` or a bad
134
+ dataset code.
135
+
136
+ ## Options
137
+
138
+ ```ts
139
+ new ElexonClient({
140
+ rateLimit: { requestsPerSecond: 10, burst: 20 }, // default
141
+ cache: new MemoryCache(), // optional; TTL defaults to 5 minutes
142
+ cacheTtlMs: 5 * 60 * 1000,
143
+ retry: { retries: 3 }, timeoutMs: 30_000, fetch, // forwarded to @uk-energy/core HttpClient
144
+ });
145
+ ```
146
+
147
+ ## Regenerating the types
148
+
149
+ `openapi/insights-v1.json` is a snapshot of `https://data.elexon.co.uk/swagger/v1/swagger.json`.
150
+ Replace it and run `pnpm --filter @uk-energy/elexon generate` (TypeScript) and
151
+ `uv run python scripts/gen_elexon_types.py` from `python/` (Python TypedDicts).