#!/usr/bin/env python3 """Pobiera ceny z API PSE i zapisuje plik CSV gotowy do wczytania w aplikacji "Dwa magazyny BESS". Użycie (Python 3.8+, bez dodatkowych bibliotek): python3 pobierz_ceny_pse.py 2026-08-01 2026-09-20 python3 pobierz_ceny_pse.py 2026-08-01 2026-09-20 --plik ceny.csv Wynik: jeden wiersz na kwadrans w formacie aplikacji data godzina;cena RDN;CEN np. 2026-09-20 00:00;462,41;1423,4 Czwarta kolumna (cena RDB) jest opcjonalna; PSE jej nie publikuje, więc skrypt jej nie zapisuje w trybie ścisłym doby bez RDB stoją dla magazynu B i strategii A „RDN + RDB”; ceny RDB modelowe aplikacja podstawia tylko w trybie demonstracyjnym. Cena RDN = cena aukcji SDAC dla Polski z raportu PSE "csdac-pln" (po tej cenie rozlicza się zlecenie złożone w aukcji; do 30.09.2025 godzinowa, skrypt wpisuje ją do 4 kwadransów). Tak liczy aplikacja od wersji 11. RCE ("rce-pln", średnia z fixingów RDN TGE) skrypt nie zapisuje; aplikacja pokazuje ją tylko do porównania. CEN = cena niezbilansowania z raportu PSE "crb-rozl" (pole cen_cost). Dodatkowo skrypt wypisuje średnie ceny aukcji mocy bilansujących aFRR dnia poprzedniego (raport "cmbp-tp", godzinowo; godzina bez zakupu liczy się jako 0 zł). Plik nie zawiera cen rezerw, więc te średnie wpisuje się ręcznie w Ustawieniach (stała stawka w górę i stawka w dół w %). Uwaga: CEN publikowana jest z kilkudniowym opóźnieniem; doby bez kompletu danych są pomijane. """ import argparse import datetime as dt import json import sys import time import urllib.parse import urllib.request API = "https://api.raporty.pse.pl/api/" def fetch_day(report, day): """Zwraca wszystkie rekordy raportu dla jednej doby handlowej (z obsługą stronicowania).""" url = API + report + "?" + urllib.parse.urlencode({"$filter": "business_date eq '%s'" % day}) rows = [] while url: for attempt in range(4): try: with urllib.request.urlopen(url, timeout=60) as resp: body = json.load(resp) break except Exception as err: # sieć bywa niestabilna, ponawiamy if attempt == 3: raise print(" ponawiam %s %s (%s)" % (report, day, err), file=sys.stderr) time.sleep(2 ** (attempt + 1)) rows.extend(body.get("value", [])) url = body.get("nextLink") return rows def by_quarter(rows, field): """Mapuje początek kwadransu ("HH:MM", z pola period "00:00 - 00:15") na wartość pola.""" out = {} for r in rows: v = r.get(field) if v is None: continue parts = [x.strip()[:5] for x in str(r.get("period", "")).split("-")] start = parts[0] try: # okres dłuższy niż kwadrans (np. godzinowe ceny sprzed 1.10.2025) rozkładamy na kwadranse h, m = map(int, start.split(":")) eh, em = map(int, parts[1].split(":")) n = max(1, ((eh * 60 + em - h * 60 - m) % 1440) // 15) except (ValueError, IndexError): n = 1 for k in range(n): mm = h * 60 + m + 15 * k if n > 1 else None key = "%02d:%02d" % divmod(mm, 60) if mm is not None else start out.setdefault(key, float(v)) # przy zmianie czasu bierzemy pierwszy kwadrans o danej godzinie return out def pl(v): return ("%.2f" % v).rstrip("0").rstrip(".").replace(".", ",") def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("od", help="pierwsza doba, RRRR-MM-DD") ap.add_argument("do", help="ostatnia doba, RRRR-MM-DD") ap.add_argument("--plik", default="ceny_pse.csv", help="nazwa pliku wynikowego") args = ap.parse_args() d, end = dt.date.fromisoformat(args.od), dt.date.fromisoformat(args.do) lines, days_ok, skipped, afrr = [], 0, [], [] while d <= end: day = d.isoformat() rdn = by_quarter(fetch_day("csdac-pln", day), "csdac_pln") cen = by_quarter(fetch_day("crb-rozl", day), "cen_cost") res = fetch_day("cmbp-tp", day) common = sorted(set(rdn) & set(cen)) if len(common) >= 92: lines += ["%s %s;%s;%s" % (day, q, pl(rdn[q]), pl(cen[q])) for q in common] days_ok += 1 else: skipped.append(day) for r in res: # jeden rekord na godzinę; brak ceny = PSE nie kupiło rezerwy afrr.append((float(r.get("afrr_g") or 0), float(r.get("afrr_d") or 0))) print("%s: RDN %d, CEN %d kwadransów" % (day, len(rdn), len(cen)), file=sys.stderr) d += dt.timedelta(days=1) time.sleep(0.3) with open(args.plik, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") print("\nZapisano %s: %d dób, %d wierszy." % (args.plik, days_ok, len(lines))) if skipped: print("Pominięte doby (brak kompletu danych): " + ", ".join(skipped)) if days_ok < 8: print("UWAGA: aplikacja potrzebuje co najmniej 8 dób (7 na rozgrzanie prognozy).") if afrr: g = sum(a for a, _ in afrr) / len(afrr) dn = sum(b for _, b in afrr) / len(afrr) print("Średnia cena aukcji aFRR: w górę %s, w dół %s zł/MW/h." % (pl(g), pl(dn))) print("W Ustawieniach wybierz Stawka rezerwy: stała stawka, wpisz Stała stawka w górę = %s" " i Stawka w dół = %s%% stawki w górę." % (pl(g), pl(dn / g * 100 if g else 100))) if __name__ == "__main__": main()