Files
ems/docs/04-modules/market-prices.md
Dusan Vojacek 4881966d00
All checks were successful
deploy / deploy (push) Successful in 15s
test / smoke-test (push) Successful in 3s
multisite update dokumentace
2026-04-05 22:11:50 +02:00

7.6 KiB
Raw Blame History

Modul: Market Prices (Spotové ceny OTE CZ)

Co modul dělá

  • Stahuje spotové ceny elektřiny z OTE CZ
  • Ukládá raw data bez vazby na lokalitu (sdílená tabulka)
  • Efektivní ceny (s marží) se dopočítávají per site přes view
  • Granularita: 15 minut nativně (veřejný JSON @@chart-data s time_resolution=PT15M)

Zdroj dat: OTE CZ

URL: https://www.ote-cr.cz/cs/kratkodobe-trhy/elektrina/denni-trh

OTE CZ publikuje denní ceny zpravidla den předem (D-1) okolo 13:0014:00 středoevropského času.

Formát dat OTE CZ (implementace)

Primární zdroj: JSON grafu denního trhu (96 bodů na den):

https://www.ote-cr.cz/cs/kratkodobe-trhy/elektrina/denni-trh/@@chart-data?report_date=YYYY-MM-DD&time_resolution=PT15M

  • Body: data.dataLine[0].point[] s x = 1…96 (15min slot), y = cena.
  • Jednotka ceny se bere z axis.y.legend (typicky EUR/MWh); kód podporuje i CZK/MWh a CZK/kWh.
  • Přepočet do buy_raw_price_czk_kwh: EUR/MWh → × EUR_CZK / 1000; CZK/MWh → / 1000; CZK/kWh beze změny.
  • Časové razítko slotu při importu: závisí na volbě dne; scheduler a globální import používají Europe/Prague. Volitelný parametr site_id v import_ote_prices umožní pro manuální volání bez explicitního data použít site.timezone místo Prahy.

Legacy / alternativa

  • OTE pubapi: https://www.ote-cr.cz/pubapi/v1/market-data/dam?... (hodinová data v projektu už není primární)

Kdo stahuje data

Python service: price_importer

Samostatný modul (ne součást FastAPI, ale může být volán z ní jako task).

Multi-site: jeden import na tick

Tabulka ems.market_interval_price je globální (jeden zdroj OTE_CZ pro celou instalaci). Plánovaný job v backend/app/main.py proto pro dnešek a zítřek v Europe/Prague doplňuje chybějící dny nejednou v cyklu po každé lokalitě, ale jedním HTTP dotazem na OTE a jedním zápisem do DB. Po úspěšném importu se pro každou aktivní ems.site znovu naplní cache predikce záporných cen (fn_predict_negative_price_windows).

Funkce import_ote_prices(db, site_id=None, target_date=…) akceptuje volitelný site_id jen pro odvození „dnes/zítra“, pokud není zadán target_date; při site_id is None se použije Europe/Prague (shodně se schedulerem).

Manuální POST /api/v1/sites/{site_id}/prices/import ponechává site_id v cestě kvůli kompatibilitě s UI (ověření, že lokalita existuje), ale zapisuje stejná sdílená data jako scheduler.

Implementované provozní změny (2026-03)

  • Robustní HTTP fetch přes httpx:
    • oddělené timeouty (connect/read/write/pool),
    • retry s exponential backoff pro transient chyby,
    • detailní chybové kódy (http_status:*, timeout_or_connect:*, db_import:*).
  • API endpoint POST /api/v1/sites/{site_id}/prices/import vrací při chybě konkrétní důvod.
  • Pokud je import volán bez date, importer nejdřív zkusí D+1 a při neúspěchu fallback na dnešní den.

Kdy se spouští

Trigger Čas Popis
Scheduled (cron) každý den 13:30 CET Předběžný pokus importu (D+1 + doplnění dneška)
Scheduled (cron) každý den 14:00 CET Hlavní import OTE (D+1 + doplnění dneška)
Scheduled (cron) každý den 00:05 CET Backfill kontrola úplnosti 96 slotů pro dnešek/zítřek
Manual trigger na vyžádání API endpoint POST /api/v1/sites/{site_id}/prices/import?date=YYYY-MM-DD
Retry při chybě Automatický opakovaný pokus v importéru

Cronové časy v tabulce jsou v Europe/Prague (CET/CEST): AsyncIOScheduler v app/main.pytimezone=ZoneInfo("Europe/Prague"); kontejner backendu má TZ=Europe/Prague v docker-compose.yml. Bez toho by se hodiny/minuty v cronu vyhodnocovaly v UTC a např. „13:30 CET“ by odpovídalo jinému okamžiku na hodinách.

Logika importu

# Pseudologika importu (implementace v price_importer.py)

def import_prices_for_date(date: date, source: str = "OTE_CZ"):
    # 1. Zkontrolovat jestli data pro daný den už existují
    existing = db.query("SELECT COUNT(*) FROM market_interval_price WHERE interval_start::date = %s AND market_source = %s", date, source)
    if existing > 0 and not force_reimport:
        log.info("Data already exist, skipping")
        return

    # 2. Stáhnout chart-data (96× 15 min)
    raw_points = ote_client.fetch_chart_data_15m(date)

    # 3. Detekovat jednotku (EUR/MWh, CZK/MWh, …) a převést na CZK/kWh
    intervals = convert_points_to_czk_kwh(raw_points, date, site_tz)

    # 5. Upsert do DB (idempotentní)
    db.upsert_many("market_interval_price", intervals, conflict_keys=["market_source", "interval_start"])
    log.info(f"Imported {len(intervals)} intervals for {date}")

Struktura DB záznamu

Viz 03-data-model.md → tabulka market_interval_price.

Klíčové body:

  • buy_raw_price_czk_kwh a sell_raw_price_czk_kwh jsou oddělené
  • Pro OTE CZ je v první verzi sell_raw_price = buy_raw_price (reference cena)
  • imported_at slouží pro audit importů

Efektivní ceny per site

Viz view market_vw_site_effective_price v 03-data-model.md.

Marže se konfigurují v site_market_config:

Parametr Typ Příklad
buy_margin_fixed_czk Kč/kWh 0.05 (5 haléřů/kWh)
buy_margin_percent % 2.5
sell_margin_fixed_czk Kč/kWh -0.02 (srážka)
sell_margin_percent % 0

Zelený bonus není součástí fn_effective_sell_price ani view efektivní prodejní ceny jde o samostatný příjem z výroby, viz níže.


Zelený bonus

  • Konfigurace je na ems.asset_pv_array (konkrétní FVE pole), ne na site ani na střídači. Sloupce: green_bonus_czk_kwh, green_bonus_valid_from, green_bonus_valid_to, green_bonus_meter_code.
  • Sazba se typicky mění ročně; verzování je přes valid_from / valid_to (při změně uzavři staré období a zadej novou sazbu s novým valid_from).
  • Výpočet příjmu za interval: ems.fn_green_bonus_revenue(pv_array_id, interval_start, production_wh) kde production_wh je skutečná nebo odhadnutá výroba pole v daném 15min slotu (Wh). Bonus platí z celkové výroby pole (interní spotřeba, baterie, EV, TČ i export) nejde o prodejní cenu.
  • Nezahrnovat do fn_effective_sell_price spot + prodejní marže jsou odděleně od dotace; v auditu se bonus ukládá do audit_interval.green_bonus_czk (plní fn_fill_audit_interval).

Historicky mohou v DB zůstat sloupce green_bonus_* na site_market_config; efektivní prodejní cena je z nich se nepočítá.


Konfigurace (env proměnné)

OTE_API_URL=https://www.ote-cr.cz/cs/kratkodobe-trhy/elektrina/denni-trh/@@chart-data
OTE_IMPORT_HOUR=14          # hodina kdy se spouští denní import
EUR_CZK_RATE=25.0           # fallback kurz pokud ČNB API nedostupné
CNB_API_URL=https://www.cnb.cz/cs/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.xml
PRICE_IMPORT_RETRY_COUNT=3
PRICE_IMPORT_RETRY_BACKOFF_SEC=300

Monitoring a alerting

  • Alert pokud do 16:00 nejsou v DB ceny na zítřek
  • Log každého importu (datum, počet intervalů, zdroj, trvání)
  • Endpoint GET /health/prices?date=YYYY-MM-DD → vrátí počet importovaných intervalů

Otevřené body

  • Kurz EUR/CZK: fixní hodnota vs denní stahování z ČNB rozhodnout před implementací
  • OTE nabízí i intraday ceny zatím neimplementujeme
  • Sell price: OTE nemá oddělenou nákupní a prodejní raw cenu, obě = DAM cena; může se lišit u jiných zdrojů