Time and data pitfalls¶
What goes wrong when reading PSE data by hand, and what this package does about it. Everything below was checked against the live API and the legacy export on 26 October 2026.
The API stamps the end of the interval¶
Each row of the reports API carries dtime_utc, the end of its interval in UTC, and business_date, the
trading day. For the first quarter-hour of 26 October 2025 (00:00-00:15 Polish time) the API returns
dtime_utc = "2025-10-25 22:15:00". Treating dtime_utc as the start shifts every series by one interval
(15 minutes, or one hour for hourly reports such as cmbp-tp).
The package computes the start as end minus the report resolution and returns both, in UTC and in Polish time:
| column | first quarter-hour of 2025-10-26 |
|---|---|
start_utc |
2025-10-25 22:00:00+00:00 |
end_utc |
2025-10-25 22:15:00+00:00 |
start_local |
2025-10-26 00:00:00+02:00 |
end_local |
2025-10-26 00:15:00+02:00 |
business_date |
2025-10-26 |
business_date is the local date of the interval start. A consequence of end stamps: for his-wlk-cal,
kse-load, cmbu-tu and mbu-tu the API also returns the last quarter-hour of business date 2024-06-13
(stamped 2024-06-13 22:00:00 UTC). The package starts these reports on 2024-06-14 like the others.
Daylight saving time¶
- The autumn day has 100 quarter-hours (25 hours), the spring day 92 (23 hours). Hourly reports have 25 and 23 rows.
- The local text fields
dtimeandperiodare ambiguous or broken on the autumn day: the repeated hour is written02a:15, and one period reads03:00 - 02a:15. Onlydtime_utcis unambiguous; the package ignores the text fields. start_localandend_localare tz-aware, so the two 02:00 hours of the autumn day are different values (02:00+02:00and02:00+01:00).- In the legacy export the extra autumn hour is labelled
2Aand the spring day has no hour2. Rows are placed by their label (1, 2, 2A, 3, ...), so a missing row leaves a gap instead of shifting the rest of the day.
Paging¶
The API follows OData conventions of Microsoft Data API builder: $filter, $select, $orderby, $first and a
nextLink with an opaque $after cursor.
- Without
$firsta page has 100 rows; the largest accepted$firstis 100,000 (more gives HTTP 400). The package asks for 20,000 rows per page and followsnextLinkuntil the end, refusing to loop. - The cursor is a keyset over the entity key (for most reports
dtime_utc). For the anonymous balancing capacity offerspopmb-rmbthe key is(dtime_utc, pom, com, comrr, reserve_type), but identical offers exist and some fields are empty, and the cursor skips rows at page boundaries: 8 of 8,286 rows of 2025-10-02 were lost with 1,000-row pages, 26 with 250-row pages. The package requests such reports one business date at a time in one page and keeps identical rows. Paging ofgen-jw,przeplywy-mocy,his-wlk-calandcrb-rozlwas checked complete. - Unknown fields or reports give HTTP 400/404 with a JSON error; the message is passed on in
PseApiError.
Hourly products in quarter-hour rows¶
RCE (rce-pln) and the SDAC price (csdac-pln) were hourly until delivery day 30 September 2025, but the API
returns 96 rows per day since June 2024: the hourly price is repeated in the four quarter-hours. Quarter-hourly
prices start on 1 October 2025. pse.to_hourly(frame) averages complete hours, which gives the hourly price before
October 2025 and the mean of the four quarter-hour prices after.
Revisions¶
Some reports are republished after corrections. The latest republication of each month, observed for September 2025 to October 2026:
| Report | Last republication after the month end |
|---|---|
rce-pln, csdac-pln, cmbp-tp, mbp-tp, pdgopkd, popmb-rmb |
none (published before delivery) |
cmbu-tu, mbu-tu |
about 1 day |
his-wlk-cal, kse-load, przeplywy-mocy, gen-jw |
usually 1 day; up to about 31 days, plus a bulk republication of 2024-2025 data on 2026-01-18 |
crb-rozl, eb-rozl |
about 46-48 days, once 106 days |
poze-redoze |
up to about 70 days |
en-rozl |
in steps of about 48, 108 and 228 days |
The cache uses these windows (Report.revision_days) so a month is not frozen before PSE stops correcting it.
For backtests where the information available at a past moment matters, note that the API only serves the latest
version of each row.
Stitching the legacy export and the API¶
- The two sources do not overlap: the legacy export ends on 2024-06-13 and the API starts on 2024-06-14. The seam is local midnight of 14 June 2024 (2024-06-13 22:00 UTC).
- Time stamps: legacy hour
1is 00:00-01:00 local time; the API stamps quarter-hour ends in UTC. Using the legacy hour label as a start in UTC, or the API end as a start, shifts one side by one or two hours. Both are converted to interval starts here. - Level: API demand (
his-wlk-caldemand, identical tokse-loadload_actual) runs below the legacy "Krajowe zapotrzebowanie na moc"; matched by weekday and hour over two weeks either side of the seam the ratio is about 1.007.pse.history(..., rescale=True)applies it; by default nothing is rescaled. See history. - The imbalance price changed with the market reform: legacy
CROand APIcen_cost(CEN) are not stitched.
Legacy export limits¶
- One calendar month per request at most: longer ranges get HTTP 429. One-day ranges are rejected; the package widens them to two days and trims the result.
- The server takes about a minute for a month and answers HTTP 429 to quick series of requests. The package waits 15 seconds between requests and backs off on 429.
- Files are cp1250 with
;separators and decimal commas; missing values are-. Dates areYYYY-MM-DDin some reports andYYYYMMDDin others. Columns change over time (Generacja PIandGeneracja IRZuntil 2020,CORin the price report until 2020).
pandas versions¶
Time columns are always datetime64[ns, tz], whichever resolution pandas used when parsing (pandas 3 parses text
to microseconds by default). Cached files written with one pandas version read the same with another.