Initial commit

Made-with: Cursor
This commit is contained in:
Dusan Vojacek
2026-03-20 13:27:37 +01:00
commit 8b4af663d8
77 changed files with 13337 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
# 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ů