# 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ě (OTE CZ publikuje po hodinách → konvertujeme na 15min replikací) --- ## 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:00–14:00 středoevropského času. ### Formát dat OTE CZ OTE publikuje hodinové ceny v EUR/MWh. Konverzní kroky: 1. Stáhnout XML/JSON feed nebo scrape HTML tabulky 2. Převést EUR/MWh → CZK/kWh (kurz ČNB nebo fixní koeficient dle konfigurace) 3. Rozložit hodinový interval na 4× 15min sloty (stejná hodnota) 4. Uložit do `market_interval_price` ### Alternativní API - **OTE XML feed:** `https://www.ote-cr.cz/pubapi/v1/market-data/dam?date=YYYY-MM-DD&market=DAM&type=FIN` - Autentikace: nepotřebná pro veřejná data --- ## Kdo stahuje data **Python service: `price_importer`** Samostatný modul (ne součást FastAPI, ale může být volán z ní jako task). ### Kdy se spouští | Trigger | Čas | Popis | |---|---|---| | Scheduled (cron) | každý den 14:00 CET | Stažení cen na zítřek (D+1) | | Scheduled (cron) | každý den 00:05 CET | Kontrola – ověření že dnešní data jsou v DB | | Manual trigger | na vyžádání | API endpoint `POST /admin/import-prices?date=YYYY-MM-DD` | | Retry | při chybě, 3× s backoffem | Automatický opakovaný pokus | ### Logika importu ```python # 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 z OTE API raw_data = ote_client.fetch_dam_prices(date) # vrátí list hodinových cen v EUR/MWh # 3. Konvertovat EUR/MWh → CZK/kWh eur_czk_rate = get_exchange_rate() # z konfigurace nebo ČNB API czk_per_kwh = [(price_eur_mwh * eur_czk_rate) / 1000 for price in raw_data] # 4. Rozložit na 15min intervaly (1 hodina = 4 sloty se stejnou cenou) intervals = expand_hourly_to_15min(czk_per_kwh, date) # 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 | --- ## Konfigurace (env proměnné) ```env OTE_API_URL=https://www.ote-cr.cz/pubapi/v1/market-data/dam 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ů