Files
ems/docs/04-modules/market-prices.md
Dusan Vojacek 8b4af663d8 Initial commit
Made-with: Cursor
2026-03-20 13:27:44 +01:00

129 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:0014: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ů