@uk-energy/elexon 0.0.0-stage → 0.2.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 +29 -0
- package/LICENSE +21 -0
- package/README.md +135 -2
- package/dist/index.cjs +360 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +31138 -0
- package/dist/index.d.ts +31138 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# @uk-energy/elexon
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- `curtailment.forDay(date, opts)`: bid acceptances per unit per settlement period joined
|
|
6
|
+
from BOAV (MWh), EBOCF (£) and BOALF (`soFlag`), with fuel type, GSP group and capacity from
|
|
7
|
+
the reference list; per-unit and day totals. `aggregateCurtailment()` exported.
|
|
8
|
+
- `balancing.acceptanceVolumes(bidOffer, date, filter)` (BOAV) and
|
|
9
|
+
`balancing.cashflows(bidOffer, date, filter)` (EBOCF), per day or per period; unit lists
|
|
10
|
+
chunked at `MAX_UNITS_PER_REQUEST` (200).
|
|
11
|
+
- `reference.units({ fuelType, gspGroupId, leadParty })` and `unitId()` (falls back to the NGC
|
|
12
|
+
id for the 48 wind units without an Elexon id).
|
|
13
|
+
- Four more fixtures (three curtailed units on 15 Oct 2026, one uncurtailed); 7 new tests.
|
|
14
|
+
|
|
15
|
+
## 0.1.0
|
|
16
|
+
|
|
17
|
+
- `ElexonClient` with `datasets.*` (`stream` for any `/datasets/{code}/stream`, plus
|
|
18
|
+
`boalf`, `bod`, `fuelinst`, `pn`, `pnStream`, `mels`, `mils`), `reference.*`
|
|
19
|
+
(`bmUnits`, `fuelTypes`) and `balancing.*` (`systemPrices`, `marketIndex`, `acceptances`,
|
|
20
|
+
`bidOffers`, `physical`).
|
|
21
|
+
- Types generated from the Insights OpenAPI 3.0.4 document (`openapi/insights-v1.json`,
|
|
22
|
+
322 paths); `DatasetRow<"BOALF">` etc. derived from the spec so `stream()` is typed per
|
|
23
|
+
dataset code.
|
|
24
|
+
- Bare-array (`/stream`) and `{ data }` envelopes unwrapped; wrong shapes throw
|
|
25
|
+
`UkEnergyError`; settlement periods validated 1–50; date inputs validated or formatted via
|
|
26
|
+
`@uk-energy/core` dates.
|
|
27
|
+
- Default 10 rps / burst 20 per host; 5-minute cache TTL when a cache is supplied.
|
|
28
|
+
- 14 fixture-backed tests on 20 recorded responses (every method and both FUELINST query
|
|
29
|
+
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,136 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @uk-energy/elexon
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
A curtailment is a **bid acceptance**: NESO accepts the unit's (negative-priced) bid to reduce
|
|
73
|
+
output, so the unit is *paid* to turn down and `cashflowGbp` is positive. `soFlaggedMwh` is the
|
|
74
|
+
share from acceptances BOALF marks `soFlag: true` — system (constraint) actions rather than energy
|
|
75
|
+
balancing. On 15 Oct 2026, AKGLW-3 was curtailed in 42 of 48 periods for 1,019 MWh and £49.5k,
|
|
76
|
+
all SO-flagged. `aggregateCurtailment()` is exported for callers with their own rows.
|
|
77
|
+
|
|
78
|
+
Every method is `async` and returns the API's rows unwrapped: `/stream` endpoints are bare arrays,
|
|
79
|
+
`/datasets/{code}` and `/balancing/*` endpoints come back as `{ data, metadata }` and only `data`
|
|
80
|
+
is returned. A response of the wrong shape throws `UkEnergyError`; an HTTP error throws
|
|
81
|
+
`HttpError` with the API's RFC 9110 problem details in `body`.
|
|
82
|
+
|
|
83
|
+
### Surface
|
|
84
|
+
|
|
85
|
+
| Namespace | Methods |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `datasets` | `stream(code, query)`, `boalf`, `bod`, `fuelinst`, `pn(date, sp, filter)`, `pnStream`, `mels`, `mils` |
|
|
88
|
+
| `reference` | `bmUnits()`, `fuelTypes()`, `units({ fuelType?, gspGroupId?, leadParty? })` |
|
|
89
|
+
| `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)` |
|
|
90
|
+
| `curtailment` | `forDay(date, { bmUnit?, fuelType?, settlementPeriodFrom?, settlementPeriodTo? })` |
|
|
91
|
+
|
|
92
|
+
Row types are exported: `BoalfRow`, `BodRow`, `FuelinstRow`, `PnRow`, `MelsRow`, `BmUnit`,
|
|
93
|
+
`SystemPriceRow`, `MarketIndexRow`, `AcceptanceRow`, `BidOfferRow`, `PhysicalRow`, plus the
|
|
94
|
+
`SettlementWindow` / `UnitFilter` / `FuelinstQuery` argument types; the full
|
|
95
|
+
generated `paths`/`components` are under `@uk-energy/elexon` as well for anything not wrapped.
|
|
96
|
+
|
|
97
|
+
## Things to know about the API
|
|
98
|
+
|
|
99
|
+
- **Settlement time is local.** A settlement day starts at Europe/London midnight; in BST,
|
|
100
|
+
SP1 of 15 Oct starts at `2026-10-14T23:00Z`. Days have 48 periods, 46 on the spring clock
|
|
101
|
+
change and 50 on the autumn one. `@uk-energy/core` exports `settlementPeriodAt`,
|
|
102
|
+
`settlementPeriodStart` and friends for the conversion.
|
|
103
|
+
- **`from`/`to` change meaning** on BOALF/BOD: instants normally, but calendar dates when
|
|
104
|
+
`settlementPeriodFrom`/`To` are given.
|
|
105
|
+
- **BOALF is a volume of levels, not a list of curtailment events.** One hour across all
|
|
106
|
+
units is ~1,650 rows; one settlement period of acceptances is ~730. Filter by `bmUnit`
|
|
107
|
+
where you can.
|
|
108
|
+
- **`reference/bmunits/all`** is ~3,100 units with capacities as strings (`"114.000"`), and
|
|
109
|
+
`gspGroupId` is `null` for 235 of the 287 wind units (transmission-connected), so GSP group
|
|
110
|
+
cannot place most wind farms on a constraint boundary. 48 wind units have no `elexonBmUnit`;
|
|
111
|
+
`unitId()` falls back to `nationalGridBmUnit`, which the API accepts as a filter too. The list
|
|
112
|
+
also has duplicates (`T_WLNYO-4` twice); `curtailment.forDay` de-duplicates ids.
|
|
113
|
+
- **Unit filters can be long.** The API accepted all 287 wind ids in one 4.9 KB URL; the client
|
|
114
|
+
splits lists at `MAX_UNITS_PER_REQUEST` (200) anyway. Unfiltered daily BOAV is ~9 MB.
|
|
115
|
+
- **Rate limits:** none published and none observed (310 requests at ~120 rps on 20 Oct 2026,
|
|
116
|
+
all 200). The client defaults to 10 rps / burst 20 per host, shared across clients in the
|
|
117
|
+
process; pass `rateLimit` to change it.
|
|
118
|
+
- **Errors** are RFC 9110 problem details with a `400` for a `from` later than `to` or a bad
|
|
119
|
+
dataset code.
|
|
120
|
+
|
|
121
|
+
## Options
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
new ElexonClient({
|
|
125
|
+
rateLimit: { requestsPerSecond: 10, burst: 20 }, // default
|
|
126
|
+
cache: new MemoryCache(), // optional; TTL defaults to 5 minutes
|
|
127
|
+
cacheTtlMs: 5 * 60 * 1000,
|
|
128
|
+
retry: { retries: 3 }, timeoutMs: 30_000, fetch, // forwarded to @uk-energy/core HttpClient
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Regenerating the types
|
|
133
|
+
|
|
134
|
+
`openapi/insights-v1.json` is a snapshot of `https://data.elexon.co.uk/swagger/v1/swagger.json`.
|
|
135
|
+
Replace it and run `pnpm --filter @uk-energy/elexon generate` (TypeScript) and
|
|
136
|
+
`uv run python scripts/gen_elexon_types.py` from `python/` (Python TypedDicts).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@uk-energy/core');
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var DEFAULT_FUEL_TYPES = ["WIND"];
|
|
7
|
+
function compareIds(a, b) {
|
|
8
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
9
|
+
}
|
|
10
|
+
function num(s) {
|
|
11
|
+
if (s == null || s === "") return null;
|
|
12
|
+
const n = Number(s);
|
|
13
|
+
return Number.isFinite(n) ? n : null;
|
|
14
|
+
}
|
|
15
|
+
function aggregateCurtailment(settlementDate, range, units, volumes, cashflows, boalf) {
|
|
16
|
+
const byId = /* @__PURE__ */ new Map();
|
|
17
|
+
for (const u of units) {
|
|
18
|
+
if (u.elexonBmUnit) byId.set(u.elexonBmUnit, u);
|
|
19
|
+
if (u.nationalGridBmUnit) byId.set(u.nationalGridBmUnit, u);
|
|
20
|
+
}
|
|
21
|
+
const soByAcceptance = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const r of boalf) {
|
|
23
|
+
if (r.acceptanceNumber != null) {
|
|
24
|
+
soByAcceptance.set(
|
|
25
|
+
r.acceptanceNumber,
|
|
26
|
+
(soByAcceptance.get(r.acceptanceNumber) ?? false) || r.soFlag === true
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const cashByKey = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const c of cashflows) {
|
|
32
|
+
const sp = c.settlementPeriod;
|
|
33
|
+
if (!c.bmUnit || sp === void 0 || sp < range.from || sp > range.to) continue;
|
|
34
|
+
const key = `${c.bmUnit}|${sp}`;
|
|
35
|
+
cashByKey.set(key, (cashByKey.get(key) ?? 0) + (c.totalCashflow ?? 0));
|
|
36
|
+
}
|
|
37
|
+
const periods = /* @__PURE__ */ new Map();
|
|
38
|
+
const seen = /* @__PURE__ */ new Map();
|
|
39
|
+
for (const v of volumes) {
|
|
40
|
+
const sp = v.settlementPeriod;
|
|
41
|
+
const acceptance = v.acceptanceId;
|
|
42
|
+
if (!v.bmUnit || sp === void 0 || acceptance === void 0) continue;
|
|
43
|
+
if (sp < range.from || sp > range.to) continue;
|
|
44
|
+
const mwh = -(v.totalVolumeAccepted ?? 0);
|
|
45
|
+
if (!(mwh > 0)) continue;
|
|
46
|
+
const key = `${v.bmUnit}|${sp}`;
|
|
47
|
+
let p = periods.get(key);
|
|
48
|
+
let ids = seen.get(key);
|
|
49
|
+
if (!p || !ids) {
|
|
50
|
+
const ref = byId.get(v.bmUnit);
|
|
51
|
+
p = {
|
|
52
|
+
settlementDate: v.settlementDate ?? settlementDate,
|
|
53
|
+
settlementPeriod: sp,
|
|
54
|
+
startTime: v.startTime ?? "",
|
|
55
|
+
bmUnit: v.bmUnit,
|
|
56
|
+
nationalGridBmUnit: v.nationalGridBmUnit ?? ref?.nationalGridBmUnit ?? null,
|
|
57
|
+
leadPartyName: v.leadPartyName ?? ref?.leadPartyName ?? null,
|
|
58
|
+
fuelType: ref?.fuelType ?? null,
|
|
59
|
+
gspGroupId: ref?.gspGroupId ?? null,
|
|
60
|
+
capacityMw: num(ref?.generationCapacity),
|
|
61
|
+
curtailedMwh: 0,
|
|
62
|
+
soFlaggedMwh: 0,
|
|
63
|
+
cashflowGbp: cashByKey.get(key) ?? null,
|
|
64
|
+
acceptances: 0,
|
|
65
|
+
soFlaggedAcceptances: 0
|
|
66
|
+
};
|
|
67
|
+
ids = /* @__PURE__ */ new Set();
|
|
68
|
+
periods.set(key, p);
|
|
69
|
+
seen.set(key, ids);
|
|
70
|
+
}
|
|
71
|
+
p.curtailedMwh += mwh;
|
|
72
|
+
const so = soByAcceptance.get(acceptance) === true;
|
|
73
|
+
if (so) p.soFlaggedMwh += mwh;
|
|
74
|
+
if (!ids.has(acceptance)) {
|
|
75
|
+
ids.add(acceptance);
|
|
76
|
+
p.acceptances += 1;
|
|
77
|
+
if (so) p.soFlaggedAcceptances += 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const rows = [...periods.values()].sort(
|
|
81
|
+
(a, b) => a.settlementPeriod - b.settlementPeriod || compareIds(a.bmUnit, b.bmUnit)
|
|
82
|
+
);
|
|
83
|
+
const unitTotals = /* @__PURE__ */ new Map();
|
|
84
|
+
const unitAcceptances = /* @__PURE__ */ new Map();
|
|
85
|
+
const allAcceptances = /* @__PURE__ */ new Set();
|
|
86
|
+
const total = { curtailedMwh: 0, soFlaggedMwh: 0, cashflowGbp: 0, units: 0, acceptances: 0 };
|
|
87
|
+
for (const p of rows) {
|
|
88
|
+
let u = unitTotals.get(p.bmUnit);
|
|
89
|
+
if (!u) {
|
|
90
|
+
u = {
|
|
91
|
+
bmUnit: p.bmUnit,
|
|
92
|
+
nationalGridBmUnit: p.nationalGridBmUnit,
|
|
93
|
+
leadPartyName: p.leadPartyName,
|
|
94
|
+
fuelType: p.fuelType,
|
|
95
|
+
gspGroupId: p.gspGroupId,
|
|
96
|
+
capacityMw: p.capacityMw,
|
|
97
|
+
curtailedMwh: 0,
|
|
98
|
+
soFlaggedMwh: 0,
|
|
99
|
+
cashflowGbp: 0,
|
|
100
|
+
periods: 0,
|
|
101
|
+
acceptances: 0
|
|
102
|
+
};
|
|
103
|
+
unitTotals.set(p.bmUnit, u);
|
|
104
|
+
}
|
|
105
|
+
u.curtailedMwh += p.curtailedMwh;
|
|
106
|
+
u.soFlaggedMwh += p.soFlaggedMwh;
|
|
107
|
+
u.cashflowGbp += p.cashflowGbp ?? 0;
|
|
108
|
+
u.periods += 1;
|
|
109
|
+
const ids = seen.get(`${p.bmUnit}|${p.settlementPeriod}`) ?? /* @__PURE__ */ new Set();
|
|
110
|
+
let acc = unitAcceptances.get(p.bmUnit);
|
|
111
|
+
if (!acc) {
|
|
112
|
+
acc = /* @__PURE__ */ new Set();
|
|
113
|
+
unitAcceptances.set(p.bmUnit, acc);
|
|
114
|
+
}
|
|
115
|
+
for (const id of ids) {
|
|
116
|
+
acc.add(id);
|
|
117
|
+
allAcceptances.add(id);
|
|
118
|
+
}
|
|
119
|
+
u.acceptances = acc.size;
|
|
120
|
+
total.curtailedMwh += p.curtailedMwh;
|
|
121
|
+
total.soFlaggedMwh += p.soFlaggedMwh;
|
|
122
|
+
total.cashflowGbp += p.cashflowGbp ?? 0;
|
|
123
|
+
}
|
|
124
|
+
total.units = unitTotals.size;
|
|
125
|
+
total.acceptances = allAcceptances.size;
|
|
126
|
+
return {
|
|
127
|
+
settlementDate,
|
|
128
|
+
settlementPeriodFrom: range.from,
|
|
129
|
+
settlementPeriodTo: range.to,
|
|
130
|
+
periods: rows,
|
|
131
|
+
units: [...unitTotals.values()].sort((a, b) => b.curtailedMwh - a.curtailedMwh),
|
|
132
|
+
total
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function curtailmentForDay(client, settlementDate, opts = {}) {
|
|
136
|
+
const day = core.formatDate(settlementDate);
|
|
137
|
+
const last = core.settlementPeriodsInDay(day);
|
|
138
|
+
const from = opts.settlementPeriodFrom ?? 1;
|
|
139
|
+
const to = opts.settlementPeriodTo ?? last;
|
|
140
|
+
if (from < 1 || to > last || from > to) {
|
|
141
|
+
throw new core.UkEnergyError(`Settlement periods ${from}-${to} are outside 1-${last} for ${day}`);
|
|
142
|
+
}
|
|
143
|
+
const units = await client.reference.bmUnits();
|
|
144
|
+
let ids;
|
|
145
|
+
if (opts.bmUnit) {
|
|
146
|
+
ids = opts.bmUnit;
|
|
147
|
+
} else {
|
|
148
|
+
const fuel = opts.fuelType ?? DEFAULT_FUEL_TYPES;
|
|
149
|
+
ids = units.filter((u) => u.fuelType != null && fuel.includes(u.fuelType)).map(unitId).filter((id) => id !== "");
|
|
150
|
+
}
|
|
151
|
+
ids = [...new Set(ids)];
|
|
152
|
+
if (ids.length === 0) throw new core.UkEnergyError("No BM units to query");
|
|
153
|
+
const [volumes, cashflows, boalfParts] = await Promise.all([
|
|
154
|
+
client.balancing.acceptanceVolumes("bid", day, { bmUnit: ids }),
|
|
155
|
+
client.balancing.cashflows("bid", day, { bmUnit: ids }),
|
|
156
|
+
Promise.all(
|
|
157
|
+
chunk(ids, MAX_UNITS_PER_REQUEST).map(
|
|
158
|
+
(bmUnit) => client.datasets.boalf({
|
|
159
|
+
from: day,
|
|
160
|
+
to: day,
|
|
161
|
+
settlementPeriodFrom: 1,
|
|
162
|
+
settlementPeriodTo: last,
|
|
163
|
+
bmUnit
|
|
164
|
+
})
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
]);
|
|
168
|
+
const boalf = boalfParts.flat();
|
|
169
|
+
return {
|
|
170
|
+
unitsQueried: ids.length,
|
|
171
|
+
...aggregateCurtailment(day, { from, to }, units, volumes, cashflows, boalf)
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/client.ts
|
|
176
|
+
var BASE_URL = "https://data.elexon.co.uk/bmrs/api/v1";
|
|
177
|
+
var DEFAULT_RATE_LIMIT = { requestsPerSecond: 10, burst: 20 };
|
|
178
|
+
var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
179
|
+
function unitId(u) {
|
|
180
|
+
return u.elexonBmUnit || u.nationalGridBmUnit || "";
|
|
181
|
+
}
|
|
182
|
+
var MAX_UNITS_PER_REQUEST = 200;
|
|
183
|
+
function chunk(items, size) {
|
|
184
|
+
const out = [];
|
|
185
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
function windowQuery(w) {
|
|
189
|
+
const q = { from: core.formatDateTime(w.from), to: core.formatDateTime(w.to) };
|
|
190
|
+
if (w.settlementPeriodFrom !== void 0) {
|
|
191
|
+
core.assertSettlementPeriod(w.settlementPeriodFrom);
|
|
192
|
+
q.settlementPeriodFrom = w.settlementPeriodFrom;
|
|
193
|
+
}
|
|
194
|
+
if (w.settlementPeriodTo !== void 0) {
|
|
195
|
+
core.assertSettlementPeriod(w.settlementPeriodTo);
|
|
196
|
+
q.settlementPeriodTo = w.settlementPeriodTo;
|
|
197
|
+
}
|
|
198
|
+
return q;
|
|
199
|
+
}
|
|
200
|
+
function periodQuery(settlementDate, settlementPeriod) {
|
|
201
|
+
core.assertSettlementPeriod(settlementPeriod);
|
|
202
|
+
return { settlementDate: core.formatDate(settlementDate), settlementPeriod };
|
|
203
|
+
}
|
|
204
|
+
var ElexonClient = class {
|
|
205
|
+
http;
|
|
206
|
+
constructor(opts = {}) {
|
|
207
|
+
this.http = new core.HttpClient({
|
|
208
|
+
baseUrl: BASE_URL,
|
|
209
|
+
rateLimit: DEFAULT_RATE_LIMIT,
|
|
210
|
+
cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,
|
|
211
|
+
...opts
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/** Endpoints returning a bare array. */
|
|
215
|
+
async list(path, query) {
|
|
216
|
+
const res = await this.http.get(path, query);
|
|
217
|
+
if (!Array.isArray(res)) throw new core.UkEnergyError(`Unexpected response shape for ${path}`);
|
|
218
|
+
return res;
|
|
219
|
+
}
|
|
220
|
+
/** Endpoints returning `{ data: [...] }`. */
|
|
221
|
+
async data(path, query) {
|
|
222
|
+
const res = await this.http.get(path, query);
|
|
223
|
+
if (!res || !Array.isArray(res.data)) {
|
|
224
|
+
throw new core.UkEnergyError(`Unexpected response shape for ${path}`);
|
|
225
|
+
}
|
|
226
|
+
return res.data;
|
|
227
|
+
}
|
|
228
|
+
/** `/datasets/{code}` — raw datasets. The `/stream` variants are used: lighter, no envelope. */
|
|
229
|
+
datasets = {
|
|
230
|
+
/**
|
|
231
|
+
* Any stream dataset (77 codes: BOALF, BOD, FUELINST, PN, MELS, WINDFOR, FREQ, …), with the
|
|
232
|
+
* query parameters typed from the spec. Values are sent as given (strings in API format,
|
|
233
|
+
* "YYYY-MM-DDTHH:MMZ" or "YYYY-MM-DD"); the named methods below format and validate dates.
|
|
234
|
+
*/
|
|
235
|
+
stream: async (dataset, query) => this.list(`datasets/${dataset}/stream`, query),
|
|
236
|
+
/** Bid-offer acceptance levels (BOALF). Curtailment shows as `soFlag: true` bids. */
|
|
237
|
+
boalf: async (w) => this.list("datasets/BOALF/stream", { ...windowQuery(w), bmUnit: w.bmUnit }),
|
|
238
|
+
/** Bid-offer data (BOD): submitted bid and offer prices per pair. */
|
|
239
|
+
bod: async (w) => this.list("datasets/BOD/stream", { ...windowQuery(w), bmUnit: w.bmUnit }),
|
|
240
|
+
/** Generation by fuel type at 5-minute resolution (FUELINST). */
|
|
241
|
+
fuelinst: async (q) => {
|
|
242
|
+
const query = { fuelType: q.fuelType };
|
|
243
|
+
if ("publishDateTimeFrom" in q) {
|
|
244
|
+
query.publishDateTimeFrom = core.formatDateTime(q.publishDateTimeFrom);
|
|
245
|
+
query.publishDateTimeTo = core.formatDateTime(q.publishDateTimeTo);
|
|
246
|
+
} else {
|
|
247
|
+
query.settlementDateFrom = core.formatDate(q.settlementDateFrom);
|
|
248
|
+
query.settlementDateTo = core.formatDate(q.settlementDateTo);
|
|
249
|
+
if (q.settlementPeriod) {
|
|
250
|
+
for (const p of q.settlementPeriod) core.assertSettlementPeriod(p);
|
|
251
|
+
query.settlementPeriod = q.settlementPeriod;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return this.list("datasets/FUELINST/stream", query);
|
|
255
|
+
},
|
|
256
|
+
/** Physical notifications for one settlement period (`/datasets/PN` takes exactly one). */
|
|
257
|
+
pn: async (settlementDate, settlementPeriod, filter = {}) => this.data("datasets/PN", {
|
|
258
|
+
...periodQuery(settlementDate, settlementPeriod),
|
|
259
|
+
bmUnit: filter.bmUnit
|
|
260
|
+
}),
|
|
261
|
+
/** Physical notifications over a window (`/datasets/PN/stream`). */
|
|
262
|
+
pnStream: async (w) => this.list("datasets/PN/stream", { ...windowQuery(w), bmUnit: w.bmUnit }),
|
|
263
|
+
/** Maximum export limits (MELS). */
|
|
264
|
+
mels: async (w) => this.list("datasets/MELS/stream", { ...windowQuery(w), bmUnit: w.bmUnit }),
|
|
265
|
+
/** Minimum import limits (MILS). */
|
|
266
|
+
mils: async (w) => this.list("datasets/MILS/stream", { ...windowQuery(w), bmUnit: w.bmUnit })
|
|
267
|
+
};
|
|
268
|
+
/** Reference data. */
|
|
269
|
+
reference = {
|
|
270
|
+
/** All BM units (~3,100). Capacities are strings; `fuelType` is null for many non-generators. */
|
|
271
|
+
bmUnits: async () => this.list("reference/bmunits/all"),
|
|
272
|
+
/** Fuel type codes used across the API. */
|
|
273
|
+
fuelTypes: async () => this.list("reference/fueltypes/all"),
|
|
274
|
+
/** BM units matching a filter, e.g. `{ fuelType: ["WIND"] }` (287 units). */
|
|
275
|
+
units: async (filter = {}) => {
|
|
276
|
+
const party = filter.leadParty?.toLowerCase();
|
|
277
|
+
return (await this.reference.bmUnits()).filter(
|
|
278
|
+
(u) => (!filter.fuelType || u.fuelType != null && filter.fuelType.includes(u.fuelType)) && (!filter.gspGroupId || u.gspGroupId != null && filter.gspGroupId.includes(u.gspGroupId)) && (!party || (u.leadPartyName ?? "").toLowerCase().includes(party))
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
/** Market-wide balancing views (one settlement period per call) and settlement prices. */
|
|
283
|
+
balancing = {
|
|
284
|
+
/** System buy/sell prices for a settlement day, or one period of it (DISEBSP). */
|
|
285
|
+
systemPrices: async (settlementDate, settlementPeriod) => this.data(
|
|
286
|
+
`balancing/settlement/system-prices/${periodPath(settlementDate, settlementPeriod)}`
|
|
287
|
+
),
|
|
288
|
+
/** Market index (MID) prices and volumes. */
|
|
289
|
+
marketIndex: async (w) => this.data("balancing/pricing/market-index", {
|
|
290
|
+
...windowQuery(w),
|
|
291
|
+
dataProviders: w.dataProviders
|
|
292
|
+
}),
|
|
293
|
+
/** Every acceptance in one settlement period. */
|
|
294
|
+
acceptances: async (settlementDate, settlementPeriod, filter = {}) => this.data("balancing/acceptances/all", {
|
|
295
|
+
...periodQuery(settlementDate, settlementPeriod),
|
|
296
|
+
bmUnit: filter.bmUnit
|
|
297
|
+
}),
|
|
298
|
+
/** Every bid-offer pair in one settlement period. */
|
|
299
|
+
bidOffers: async (settlementDate, settlementPeriod, filter = {}) => this.data("balancing/bid-offer/all", {
|
|
300
|
+
...periodQuery(settlementDate, settlementPeriod),
|
|
301
|
+
bmUnit: filter.bmUnit
|
|
302
|
+
}),
|
|
303
|
+
/**
|
|
304
|
+
* Settled acceptance volumes (BOAV) for a settlement day, or one period of it: one row per
|
|
305
|
+
* acceptance per period, `totalVolumeAccepted` in MWh (negative for bids). Long unit lists
|
|
306
|
+
* are split across requests.
|
|
307
|
+
*/
|
|
308
|
+
acceptanceVolumes: async (bidOffer, settlementDate, filter = {}) => this.perUnitChunks(
|
|
309
|
+
`balancing/settlement/acceptance/volumes/all/${bidOffer}/${periodPath(settlementDate, filter.settlementPeriod)}`,
|
|
310
|
+
filter.bmUnit
|
|
311
|
+
),
|
|
312
|
+
/**
|
|
313
|
+
* Indicative bid/offer cashflows (EBOCF) for a settlement day or one period: one row per unit
|
|
314
|
+
* per period, `totalCashflow` in £ paid to the unit (positive for a wind bid at a negative
|
|
315
|
+
* price).
|
|
316
|
+
*/
|
|
317
|
+
cashflows: async (bidOffer, settlementDate, filter = {}) => this.perUnitChunks(
|
|
318
|
+
`balancing/settlement/indicative/cashflows/all/${bidOffer}/${periodPath(settlementDate, filter.settlementPeriod)}`,
|
|
319
|
+
filter.bmUnit
|
|
320
|
+
),
|
|
321
|
+
/** PN / QPN / MILS / MELS for every unit in one settlement period. */
|
|
322
|
+
physical: async (dataset, settlementDate, settlementPeriod, filter = {}) => this.data("balancing/physical/all", {
|
|
323
|
+
dataset,
|
|
324
|
+
...periodQuery(settlementDate, settlementPeriod),
|
|
325
|
+
bmUnit: filter.bmUnit
|
|
326
|
+
})
|
|
327
|
+
};
|
|
328
|
+
/** Wind curtailment: SO-flagged and energy bid acceptances per unit per period, with £. */
|
|
329
|
+
curtailment = {
|
|
330
|
+
/**
|
|
331
|
+
* Curtailment for a GB settlement day (Europe/London), per unit and per settlement period.
|
|
332
|
+
* Defaults to every WIND unit; the reference list plus three requests per 200 units.
|
|
333
|
+
*/
|
|
334
|
+
forDay: async (settlementDate, opts = {}) => curtailmentForDay(this, settlementDate, opts)
|
|
335
|
+
};
|
|
336
|
+
/** `{data}` endpoints with an optional unit filter, chunked so URLs stay short. */
|
|
337
|
+
async perUnitChunks(path, bmUnit) {
|
|
338
|
+
if (!bmUnit?.length) return this.data(path);
|
|
339
|
+
const parts = await Promise.all(
|
|
340
|
+
chunk(bmUnit, MAX_UNITS_PER_REQUEST).map((ids) => this.data(path, { bmUnit: ids }))
|
|
341
|
+
);
|
|
342
|
+
return parts.flat();
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
function periodPath(settlementDate, settlementPeriod) {
|
|
346
|
+
if (settlementPeriod !== void 0) core.assertSettlementPeriod(settlementPeriod);
|
|
347
|
+
const p = settlementPeriod === void 0 ? "" : `/${settlementPeriod}`;
|
|
348
|
+
return `${core.formatDate(settlementDate)}${p}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
exports.BASE_URL = BASE_URL;
|
|
352
|
+
exports.DEFAULT_CACHE_TTL_MS = DEFAULT_CACHE_TTL_MS;
|
|
353
|
+
exports.DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT;
|
|
354
|
+
exports.ElexonClient = ElexonClient;
|
|
355
|
+
exports.MAX_UNITS_PER_REQUEST = MAX_UNITS_PER_REQUEST;
|
|
356
|
+
exports.aggregateCurtailment = aggregateCurtailment;
|
|
357
|
+
exports.curtailmentForDay = curtailmentForDay;
|
|
358
|
+
exports.unitId = unitId;
|
|
359
|
+
//# sourceMappingURL=index.cjs.map
|
|
360
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/curtailment.ts","../src/client.ts"],"names":["formatDate","settlementPeriodsInDay","UkEnergyError","formatDateTime","assertSettlementPeriod","HttpClient"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Wind curtailment from settlement data.\n *\n * A curtailment is a *bid acceptance*: NESO accepts a unit's bid to reduce output below its\n * physical notification. Three Elexon feeds describe one:\n *\n * - BOAV (`balancing.acceptanceVolumes(\"bid\", day)`): the settled volume of each acceptance in\n * each settlement period, in MWh (negative for bids). This is the quantity curtailed.\n * - EBOCF (`balancing.cashflows(\"bid\", day)`): the £ paid to the unit per period. Wind bids are\n * priced negative (the unit pays to keep generating, so is *paid* to stop), hence the\n * cashflow is positive and is what consumers fund through BSUoS.\n * - BOALF (`datasets.boalf`): the acceptance's level profile and flags. `soFlag` marks\n * actions taken for system reasons (transmission constraints: the classic \"wind farm paid\n * to switch off because the cable to England is full\"); unflagged acceptances are energy\n * balancing. `rrFlag`/`storFlag`/`deemedBoFlag` are counted, not excluded.\n *\n * `curtailmentForDay` joins the three by (unit, period) and (acceptance number) and adds the\n * unit's fuel type, GSP group and capacity from the reference list.\n *\n * Contains BMRS data © Elexon Limited copyright and database right 2026.\n */\nimport { type DateInput, formatDate, settlementPeriodsInDay, UkEnergyError } from \"@uk-energy/core\";\nimport { chunk, type ElexonClient, MAX_UNITS_PER_REQUEST, unitId } from \"./client.js\";\nimport type { AcceptanceVolumeRow, BmUnit, BoalfRow, CashflowRow } from \"./types.js\";\n\nexport interface CurtailmentOptions {\n /** Units to include. Default: every unit whose `fuelType` is in `fuelType`. */\n bmUnit?: string[];\n /** Used when `bmUnit` is omitted. Default `[\"WIND\"]`. */\n fuelType?: string[];\n /** Restrict to a range of settlement periods (1-based, inclusive). Default: the whole day. */\n settlementPeriodFrom?: number;\n settlementPeriodTo?: number;\n}\n\n/** One unit in one settlement period. */\nexport interface CurtailmentPeriod {\n settlementDate: string;\n settlementPeriod: number;\n /** UTC start of the period, from the API. */\n startTime: string;\n bmUnit: string;\n nationalGridBmUnit: string | null;\n leadPartyName: string | null;\n fuelType: string | null;\n gspGroupId: string | null;\n /** `generationCapacity` from the reference list, MW; null when unknown. */\n capacityMw: number | null;\n /** MWh curtailed in the period (positive). */\n curtailedMwh: number;\n /** Of which from SO-flagged (system / constraint) acceptances. */\n soFlaggedMwh: number;\n /**\n * £ paid to the unit for the period's bids (EBOCF); null when no cashflow row was published\n * yet. Cashflow rows for periods with no settled bid volume are not reported anywhere.\n */\n cashflowGbp: number | null;\n /** Distinct acceptances contributing to the period. */\n acceptances: number;\n /** Acceptances in the period that BOALF reported with `soFlag: true`. */\n soFlaggedAcceptances: number;\n}\n\n/** A unit's totals over the day. */\nexport interface CurtailmentUnit {\n bmUnit: string;\n nationalGridBmUnit: string | null;\n leadPartyName: string | null;\n fuelType: string | null;\n gspGroupId: string | null;\n capacityMw: number | null;\n curtailedMwh: number;\n soFlaggedMwh: number;\n cashflowGbp: number;\n /** Settlement periods with any curtailment. */\n periods: number;\n /** Distinct acceptances over the day (one acceptance can span several periods). */\n acceptances: number;\n}\n\nexport interface CurtailmentReport {\n settlementDate: string;\n settlementPeriodFrom: number;\n settlementPeriodTo: number;\n /** Units that were queried (whether or not they were curtailed). */\n unitsQueried: number;\n /** Rows ordered by settlement period then unit id; only (unit, period) pairs with volume. */\n periods: CurtailmentPeriod[];\n /** Per-unit totals, largest `curtailedMwh` first. */\n units: CurtailmentUnit[];\n total: {\n curtailedMwh: number;\n soFlaggedMwh: number;\n cashflowGbp: number;\n /** Units with any curtailment. */\n units: number;\n /** Distinct acceptances. */\n acceptances: number;\n };\n}\n\nconst DEFAULT_FUEL_TYPES = [\"WIND\"];\n\n/** Code-point order (not locale order) so TS and Python sort \"C__X\" and \"CD\" the same way. */\nfunction compareIds(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\nfunction num(s: string | null | undefined): number | null {\n if (s == null || s === \"\") return null;\n const n = Number(s);\n return Number.isFinite(n) ? n : null;\n}\n\n/** Pure aggregation over already-fetched rows; exported for tests and for callers with their own data. */\nexport function aggregateCurtailment(\n settlementDate: string,\n range: { from: number; to: number },\n units: readonly BmUnit[],\n volumes: readonly AcceptanceVolumeRow[],\n cashflows: readonly CashflowRow[],\n boalf: readonly BoalfRow[],\n): Omit<CurtailmentReport, \"unitsQueried\"> {\n const byId = new Map<string, BmUnit>();\n for (const u of units) {\n if (u.elexonBmUnit) byId.set(u.elexonBmUnit, u);\n if (u.nationalGridBmUnit) byId.set(u.nationalGridBmUnit, u);\n }\n const soByAcceptance = new Map<number, boolean>();\n for (const r of boalf) {\n if (r.acceptanceNumber != null) {\n soByAcceptance.set(\n r.acceptanceNumber,\n (soByAcceptance.get(r.acceptanceNumber) ?? false) || r.soFlag === true,\n );\n }\n }\n const cashByKey = new Map<string, number>();\n for (const c of cashflows) {\n const sp = c.settlementPeriod;\n if (!c.bmUnit || sp === undefined || sp < range.from || sp > range.to) continue;\n const key = `${c.bmUnit}|${sp}`;\n cashByKey.set(key, (cashByKey.get(key) ?? 0) + (c.totalCashflow ?? 0));\n }\n\n const periods = new Map<string, CurtailmentPeriod>();\n const seen = new Map<string, Set<number>>();\n for (const v of volumes) {\n const sp = v.settlementPeriod;\n const acceptance = v.acceptanceId;\n if (!v.bmUnit || sp === undefined || acceptance === undefined) continue;\n if (sp < range.from || sp > range.to) continue;\n const mwh = -(v.totalVolumeAccepted ?? 0);\n if (!(mwh > 0)) continue;\n const key = `${v.bmUnit}|${sp}`;\n let p = periods.get(key);\n let ids = seen.get(key);\n if (!p || !ids) {\n const ref = byId.get(v.bmUnit);\n p = {\n settlementDate: v.settlementDate ?? settlementDate,\n settlementPeriod: sp,\n startTime: v.startTime ?? \"\",\n bmUnit: v.bmUnit,\n nationalGridBmUnit: v.nationalGridBmUnit ?? ref?.nationalGridBmUnit ?? null,\n leadPartyName: v.leadPartyName ?? ref?.leadPartyName ?? null,\n fuelType: ref?.fuelType ?? null,\n gspGroupId: ref?.gspGroupId ?? null,\n capacityMw: num(ref?.generationCapacity),\n curtailedMwh: 0,\n soFlaggedMwh: 0,\n cashflowGbp: cashByKey.get(key) ?? null,\n acceptances: 0,\n soFlaggedAcceptances: 0,\n };\n ids = new Set();\n periods.set(key, p);\n seen.set(key, ids);\n }\n p.curtailedMwh += mwh;\n const so = soByAcceptance.get(acceptance) === true;\n if (so) p.soFlaggedMwh += mwh;\n if (!ids.has(acceptance)) {\n ids.add(acceptance);\n p.acceptances += 1;\n if (so) p.soFlaggedAcceptances += 1;\n }\n }\n\n const rows = [...periods.values()].sort(\n (a, b) => a.settlementPeriod - b.settlementPeriod || compareIds(a.bmUnit, b.bmUnit),\n );\n\n const unitTotals = new Map<string, CurtailmentUnit>();\n const unitAcceptances = new Map<string, Set<number>>();\n const allAcceptances = new Set<number>();\n const total = { curtailedMwh: 0, soFlaggedMwh: 0, cashflowGbp: 0, units: 0, acceptances: 0 };\n for (const p of rows) {\n let u = unitTotals.get(p.bmUnit);\n if (!u) {\n u = {\n bmUnit: p.bmUnit,\n nationalGridBmUnit: p.nationalGridBmUnit,\n leadPartyName: p.leadPartyName,\n fuelType: p.fuelType,\n gspGroupId: p.gspGroupId,\n capacityMw: p.capacityMw,\n curtailedMwh: 0,\n soFlaggedMwh: 0,\n cashflowGbp: 0,\n periods: 0,\n acceptances: 0,\n };\n unitTotals.set(p.bmUnit, u);\n }\n u.curtailedMwh += p.curtailedMwh;\n u.soFlaggedMwh += p.soFlaggedMwh;\n u.cashflowGbp += p.cashflowGbp ?? 0;\n u.periods += 1;\n const ids = seen.get(`${p.bmUnit}|${p.settlementPeriod}`) ?? new Set<number>();\n let acc = unitAcceptances.get(p.bmUnit);\n if (!acc) {\n acc = new Set();\n unitAcceptances.set(p.bmUnit, acc);\n }\n for (const id of ids) {\n acc.add(id);\n allAcceptances.add(id);\n }\n u.acceptances = acc.size;\n total.curtailedMwh += p.curtailedMwh;\n total.soFlaggedMwh += p.soFlaggedMwh;\n total.cashflowGbp += p.cashflowGbp ?? 0;\n }\n total.units = unitTotals.size;\n total.acceptances = allAcceptances.size;\n\n return {\n settlementDate,\n settlementPeriodFrom: range.from,\n settlementPeriodTo: range.to,\n periods: rows,\n units: [...unitTotals.values()].sort((a, b) => b.curtailedMwh - a.curtailedMwh),\n total,\n };\n}\n\nexport async function curtailmentForDay(\n client: ElexonClient,\n settlementDate: DateInput,\n opts: CurtailmentOptions = {},\n): Promise<CurtailmentReport> {\n const day = formatDate(settlementDate);\n const last = settlementPeriodsInDay(day);\n const from = opts.settlementPeriodFrom ?? 1;\n const to = opts.settlementPeriodTo ?? last;\n if (from < 1 || to > last || from > to) {\n throw new UkEnergyError(`Settlement periods ${from}-${to} are outside 1-${last} for ${day}`);\n }\n\n const units = await client.reference.bmUnits();\n let ids: string[];\n if (opts.bmUnit) {\n ids = opts.bmUnit;\n } else {\n const fuel = opts.fuelType ?? DEFAULT_FUEL_TYPES;\n ids = units\n .filter((u) => u.fuelType != null && fuel.includes(u.fuelType))\n .map(unitId)\n .filter((id) => id !== \"\");\n }\n ids = [...new Set(ids)]; // the reference list has duplicates (T_WLNYO-4 twice on 20 Oct 2026)\n if (ids.length === 0) throw new UkEnergyError(\"No BM units to query\");\n\n // BOALF is fetched for the whole day, not just from..to: an acceptance's level rows are\n // keyed by the periods they span, so one that started earlier would otherwise lose its flag.\n const [volumes, cashflows, boalfParts] = await Promise.all([\n client.balancing.acceptanceVolumes(\"bid\", day, { bmUnit: ids }),\n client.balancing.cashflows(\"bid\", day, { bmUnit: ids }),\n Promise.all(\n chunk(ids, MAX_UNITS_PER_REQUEST).map((bmUnit) =>\n client.datasets.boalf({\n from: day,\n to: day,\n settlementPeriodFrom: 1,\n settlementPeriodTo: last,\n bmUnit,\n }),\n ),\n ),\n ]);\n const boalf = boalfParts.flat();\n\n return {\n unitsQueried: ids.length,\n ...aggregateCurtailment(day, { from, to }, units, volumes, cashflows, boalf),\n };\n}\n","import {\n assertSettlementPeriod,\n type DateInput,\n formatDate,\n formatDateTime,\n HttpClient,\n type HttpClientOptions,\n type Query,\n type RateLimit,\n UkEnergyError,\n} from \"@uk-energy/core\";\nimport {\n type CurtailmentOptions,\n type CurtailmentReport,\n curtailmentForDay,\n} from \"./curtailment.js\";\nimport type {\n AcceptanceRow,\n AcceptanceVolumeRow,\n BidOffer,\n BidOfferRow,\n BmUnit,\n BoalfRow,\n BodRow,\n CashflowRow,\n DatasetQuery,\n DatasetRow,\n FuelinstRow,\n MarketIndexRow,\n MelsRow,\n PhysicalDataset,\n PhysicalRow,\n PnRow,\n StreamDataset,\n SystemPriceRow,\n} from \"./types.js\";\n\nexport const BASE_URL = \"https://data.elexon.co.uk/bmrs/api/v1\";\n\n/**\n * No limit is published and none was observed: 310 requests at ~120 req/s on 20 Oct 2026 all\n * returned 200 with no rate-limit headers (build-board d02). 10 rps / burst 20 keeps us a\n * polite order of magnitude below that; raise it per client if you need to.\n */\nexport const DEFAULT_RATE_LIMIT: RateLimit = { requestsPerSecond: 10, burst: 20 };\n\n/** Most datasets settle within minutes; a short default TTL avoids hammering on retries. */\nexport const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000;\n\nexport type ElexonClientOptions = Partial<Omit<HttpClientOptions, \"baseUrl\">> & {\n baseUrl?: string;\n};\n\n/**\n * Time window for the dataset endpoints. With `settlementPeriodFrom`/`To` set, the API treats\n * `from`/`to` as settlement *dates* (time ignored) and filters by period instead.\n */\nexport interface SettlementWindow {\n from: DateInput;\n to: DateInput;\n settlementPeriodFrom?: number;\n settlementPeriodTo?: number;\n}\n\nexport interface UnitFilter {\n /** Elexon BM unit ids, e.g. \"T_ARCHW-1\". Omit for all units. */\n bmUnit?: string[];\n}\n\n/** Client-side filter over the BM unit reference list. All given fields must match. */\nexport interface UnitsFilter {\n /** e.g. [\"WIND\"]. */\n fuelType?: string[];\n /** GSP group ids, e.g. [\"_P\", \"_N\"] (north / south Scotland). Most wind units have none. */\n gspGroupId?: string[];\n /** Case-insensitive substring of `leadPartyName`. */\n leadParty?: string;\n}\n\n/** The id to query a unit by: `elexonBmUnit`, or `nationalGridBmUnit` for units without one (48 wind units). */\nexport function unitId(u: BmUnit): string {\n return u.elexonBmUnit || u.nationalGridBmUnit || \"\";\n}\n\n/** The API accepts the whole wind fleet (287 ids, ~4.9 KB URL); chunk well below that. */\nexport const MAX_UNITS_PER_REQUEST = 200;\n\nexport function chunk<T>(items: readonly T[], size: number): T[][] {\n const out: T[][] = [];\n for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));\n return out;\n}\n\nexport type FuelinstQuery = (\n | { publishDateTimeFrom: DateInput; publishDateTimeTo: DateInput }\n | { settlementDateFrom: DateInput; settlementDateTo: DateInput; settlementPeriod?: number[] }\n) & { fuelType?: string[] };\n\ninterface Envelope<T> {\n data: T;\n metadata?: { datasets?: string[] };\n}\n\nfunction windowQuery(w: SettlementWindow): Query {\n const q: Query = { from: formatDateTime(w.from), to: formatDateTime(w.to) };\n if (w.settlementPeriodFrom !== undefined) {\n assertSettlementPeriod(w.settlementPeriodFrom);\n q.settlementPeriodFrom = w.settlementPeriodFrom;\n }\n if (w.settlementPeriodTo !== undefined) {\n assertSettlementPeriod(w.settlementPeriodTo);\n q.settlementPeriodTo = w.settlementPeriodTo;\n }\n return q;\n}\n\nfunction periodQuery(settlementDate: DateInput, settlementPeriod: number): Query {\n assertSettlementPeriod(settlementPeriod);\n return { settlementDate: formatDate(settlementDate), settlementPeriod };\n}\n\n/**\n * Client for Elexon Insights (BMRS). Methods return the API's own row shapes, typed from its\n * OpenAPI spec and unwrapped from the `{ data }` envelope where there is one.\n *\n * const ex = new ElexonClient();\n * const rows = await ex.datasets.boalf({ from: \"2026-10-15T00:00Z\", to: \"2026-10-15T01:00Z\" });\n * const wind = (await ex.reference.bmUnits()).filter((u) => u.fuelType === \"WIND\");\n *\n * Contains BMRS data © Elexon Limited copyright and database right 2026. Keep the attribution.\n */\nexport class ElexonClient {\n readonly http: HttpClient;\n\n constructor(opts: ElexonClientOptions = {}) {\n this.http = new HttpClient({\n baseUrl: BASE_URL,\n rateLimit: DEFAULT_RATE_LIMIT,\n cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,\n ...opts,\n });\n }\n\n /** Endpoints returning a bare array. */\n private async list<T>(path: string, query?: Query): Promise<T[]> {\n const res = await this.http.get<T[]>(path, query);\n if (!Array.isArray(res)) throw new UkEnergyError(`Unexpected response shape for ${path}`);\n return res;\n }\n\n /** Endpoints returning `{ data: [...] }`. */\n private async data<T>(path: string, query?: Query): Promise<T[]> {\n const res = await this.http.get<Envelope<T[]>>(path, query);\n if (!res || !Array.isArray(res.data)) {\n throw new UkEnergyError(`Unexpected response shape for ${path}`);\n }\n return res.data;\n }\n\n /** `/datasets/{code}` — raw datasets. The `/stream` variants are used: lighter, no envelope. */\n readonly datasets = {\n /**\n * Any stream dataset (77 codes: BOALF, BOD, FUELINST, PN, MELS, WINDFOR, FREQ, …), with the\n * query parameters typed from the spec. Values are sent as given (strings in API format,\n * \"YYYY-MM-DDTHH:MMZ\" or \"YYYY-MM-DD\"); the named methods below format and validate dates.\n */\n stream: async <D extends StreamDataset>(\n dataset: D,\n query: DatasetQuery<D>,\n ): Promise<DatasetRow<D>[]> =>\n this.list<DatasetRow<D>>(`datasets/${dataset}/stream`, query as unknown as Query),\n\n /** Bid-offer acceptance levels (BOALF). Curtailment shows as `soFlag: true` bids. */\n boalf: async (w: SettlementWindow & UnitFilter): Promise<BoalfRow[]> =>\n this.list(\"datasets/BOALF/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n\n /** Bid-offer data (BOD): submitted bid and offer prices per pair. */\n bod: async (w: SettlementWindow & UnitFilter): Promise<BodRow[]> =>\n this.list(\"datasets/BOD/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n\n /** Generation by fuel type at 5-minute resolution (FUELINST). */\n fuelinst: async (q: FuelinstQuery): Promise<FuelinstRow[]> => {\n const query: Query = { fuelType: q.fuelType };\n if (\"publishDateTimeFrom\" in q) {\n query.publishDateTimeFrom = formatDateTime(q.publishDateTimeFrom);\n query.publishDateTimeTo = formatDateTime(q.publishDateTimeTo);\n } else {\n query.settlementDateFrom = formatDate(q.settlementDateFrom);\n query.settlementDateTo = formatDate(q.settlementDateTo);\n if (q.settlementPeriod) {\n for (const p of q.settlementPeriod) assertSettlementPeriod(p);\n query.settlementPeriod = q.settlementPeriod;\n }\n }\n return this.list(\"datasets/FUELINST/stream\", query);\n },\n\n /** Physical notifications for one settlement period (`/datasets/PN` takes exactly one). */\n pn: async (\n settlementDate: DateInput,\n settlementPeriod: number,\n filter: UnitFilter = {},\n ): Promise<PnRow[]> =>\n this.data(\"datasets/PN\", {\n ...periodQuery(settlementDate, settlementPeriod),\n bmUnit: filter.bmUnit,\n }),\n\n /** Physical notifications over a window (`/datasets/PN/stream`). */\n pnStream: async (w: SettlementWindow & UnitFilter): Promise<PnRow[]> =>\n this.list(\"datasets/PN/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n\n /** Maximum export limits (MELS). */\n mels: async (w: SettlementWindow & UnitFilter): Promise<MelsRow[]> =>\n this.list(\"datasets/MELS/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n\n /** Minimum import limits (MILS). */\n mils: async (w: SettlementWindow & UnitFilter): Promise<MelsRow[]> =>\n this.list(\"datasets/MILS/stream\", { ...windowQuery(w), bmUnit: w.bmUnit }),\n };\n\n /** Reference data. */\n readonly reference = {\n /** All BM units (~3,100). Capacities are strings; `fuelType` is null for many non-generators. */\n bmUnits: async (): Promise<BmUnit[]> => this.list(\"reference/bmunits/all\"),\n /** Fuel type codes used across the API. */\n fuelTypes: async (): Promise<string[]> => this.list(\"reference/fueltypes/all\"),\n /** BM units matching a filter, e.g. `{ fuelType: [\"WIND\"] }` (287 units). */\n units: async (filter: UnitsFilter = {}): Promise<BmUnit[]> => {\n const party = filter.leadParty?.toLowerCase();\n return (await this.reference.bmUnits()).filter(\n (u) =>\n (!filter.fuelType || (u.fuelType != null && filter.fuelType.includes(u.fuelType))) &&\n (!filter.gspGroupId ||\n (u.gspGroupId != null && filter.gspGroupId.includes(u.gspGroupId))) &&\n (!party || (u.leadPartyName ?? \"\").toLowerCase().includes(party)),\n );\n },\n };\n\n /** Market-wide balancing views (one settlement period per call) and settlement prices. */\n readonly balancing = {\n /** System buy/sell prices for a settlement day, or one period of it (DISEBSP). */\n systemPrices: async (\n settlementDate: DateInput,\n settlementPeriod?: number,\n ): Promise<SystemPriceRow[]> =>\n this.data(\n `balancing/settlement/system-prices/${periodPath(settlementDate, settlementPeriod)}`,\n ),\n\n /** Market index (MID) prices and volumes. */\n marketIndex: async (\n w: SettlementWindow & { dataProviders?: string[] },\n ): Promise<MarketIndexRow[]> =>\n this.data(\"balancing/pricing/market-index\", {\n ...windowQuery(w),\n dataProviders: w.dataProviders,\n }),\n\n /** Every acceptance in one settlement period. */\n acceptances: async (\n settlementDate: DateInput,\n settlementPeriod: number,\n filter: UnitFilter = {},\n ): Promise<AcceptanceRow[]> =>\n this.data(\"balancing/acceptances/all\", {\n ...periodQuery(settlementDate, settlementPeriod),\n bmUnit: filter.bmUnit,\n }),\n\n /** Every bid-offer pair in one settlement period. */\n bidOffers: async (\n settlementDate: DateInput,\n settlementPeriod: number,\n filter: UnitFilter = {},\n ): Promise<BidOfferRow[]> =>\n this.data(\"balancing/bid-offer/all\", {\n ...periodQuery(settlementDate, settlementPeriod),\n bmUnit: filter.bmUnit,\n }),\n\n /**\n * Settled acceptance volumes (BOAV) for a settlement day, or one period of it: one row per\n * acceptance per period, `totalVolumeAccepted` in MWh (negative for bids). Long unit lists\n * are split across requests.\n */\n acceptanceVolumes: async (\n bidOffer: BidOffer,\n settlementDate: DateInput,\n filter: UnitFilter & { settlementPeriod?: number } = {},\n ): Promise<AcceptanceVolumeRow[]> =>\n this.perUnitChunks(\n `balancing/settlement/acceptance/volumes/all/${bidOffer}/${periodPath(settlementDate, filter.settlementPeriod)}`,\n filter.bmUnit,\n ),\n /**\n * Indicative bid/offer cashflows (EBOCF) for a settlement day or one period: one row per unit\n * per period, `totalCashflow` in £ paid to the unit (positive for a wind bid at a negative\n * price).\n */\n cashflows: async (\n bidOffer: BidOffer,\n settlementDate: DateInput,\n filter: UnitFilter & { settlementPeriod?: number } = {},\n ): Promise<CashflowRow[]> =>\n this.perUnitChunks(\n `balancing/settlement/indicative/cashflows/all/${bidOffer}/${periodPath(settlementDate, filter.settlementPeriod)}`,\n filter.bmUnit,\n ),\n /** PN / QPN / MILS / MELS for every unit in one settlement period. */\n physical: async (\n dataset: PhysicalDataset,\n settlementDate: DateInput,\n settlementPeriod: number,\n filter: UnitFilter = {},\n ): Promise<PhysicalRow[]> =>\n this.data(\"balancing/physical/all\", {\n dataset,\n ...periodQuery(settlementDate, settlementPeriod),\n bmUnit: filter.bmUnit,\n }),\n };\n\n /** Wind curtailment: SO-flagged and energy bid acceptances per unit per period, with £. */\n readonly curtailment = {\n /**\n * Curtailment for a GB settlement day (Europe/London), per unit and per settlement period.\n * Defaults to every WIND unit; the reference list plus three requests per 200 units.\n */\n forDay: async (\n settlementDate: DateInput,\n opts: CurtailmentOptions = {},\n ): Promise<CurtailmentReport> => curtailmentForDay(this, settlementDate, opts),\n };\n\n /** `{data}` endpoints with an optional unit filter, chunked so URLs stay short. */\n private async perUnitChunks<T>(path: string, bmUnit?: string[]): Promise<T[]> {\n if (!bmUnit?.length) return this.data<T>(path);\n const parts = await Promise.all(\n chunk(bmUnit, MAX_UNITS_PER_REQUEST).map((ids) => this.data<T>(path, { bmUnit: ids })),\n );\n return parts.flat();\n }\n}\n\nfunction periodPath(settlementDate: DateInput, settlementPeriod?: number): string {\n if (settlementPeriod !== undefined) assertSettlementPeriod(settlementPeriod);\n const p = settlementPeriod === undefined ? \"\" : `/${settlementPeriod}`;\n return `${formatDate(settlementDate)}${p}`;\n}\n"]}
|