Files
ems/docs/04-modules/planning-arbitrage-accounting.md
Dusan Vojacek 649c9e9510
Some checks failed
CI and deploy / migration-check (push) Failing after 22s
CI and deploy / deploy (push) Has been skipped
uz me to nebavi
2026-05-21 15:17:09 +02:00

8.4 KiB
Raw Blame History

Plánování: arbitráž a účtování energie (mezi sloty vs. v jednom slotu)

Účel: Trvalá poznámka pro implementaci i pro agenty — aby se neopakovala chyba „řešit arbitráž přes buy a sell ve stejném 15min slotu“ nebo přes min(buy) celého horizontu jako nákupní cenu uložené energie.

Související: planning.md, planning_engine.py (solve_dispatch), R__063_fn_load_planning_slots_full.sql.


1. Co uživatel / provoz očekává (správný model)

Arbitráž baterie je časový posun:

  1. V levných hodinách (může jich být více za sebou) nabít z site — např. home-01: baterie 64 kWh, import z site typicky až 17 kW → za 15 min až ~4,25 kWh ze sítě na slot, ale klidně 816 slotů (24 h) dokud cena sedí.
  2. V drahých / výkupních hodinách (jiný čas) stejnou energii prodat do sítě nebo ušetřit drahý import domu.

Ekonomický přínos je přibližně:

zisk ≈ (efektivní sell ve výprodejním okně)
       (efektivní buy v nabíjecím okně)
       degradace cyklu / účinnost

Není to rozhodnutí „v tomto jednom 15min okně koupím za 7 Kč a prodám za 4,6 Kč“ — ve výprodejním slotu se nekupuje energie určená k exportu z baterie; ta byla nabitá dříve za jinou cenu.


2. Co dělá dnešní LP (a proč to arbitráž láme)

2.1 Účelová funkce je po slotech

V solve_dispatch je v každém slotu t zhruba:

náklad  += gi[t] × buy[t]
výnos   -= ge[t] × sell[t]

Energetická bilance je také per slot (15 min). Když solver v evening slotu zvýší ge_bat (export baterie), bilance často vyžaduje současně gi (síť krmí dům) a bd/ge_bat. Marginalně pak vypadá každá vyvezená kWh jako:

  • „koupeno“ za buy[t] večer (např. 7 Kč/kWh),
  • „prodáno“ za sell[t] večer (např. 4,6 Kč/kWh),

→ v jednom okně ztráta, i když energie v baterii pochází z poledních 0,7 Kč/kWh.

Závěr: Samotné opravy typu „přidat ge_bat × (sell ref_buy)nestačí, pokud ref_buy je jedna čísla z jednoho slotu — pořád myslíme příliš v rámci jednoho okna. Cíl je oddělit nákupní okno od výprodejního okna v ekonomice modelu.

2.2 Guardy sell < buy ve stejném slotu

Tvrdé zákazy typu ge_pv = 0 když sell[t] < buy[t] brání ztrátovému exportu FVE v tomže slotu (prodat za 3 Kč při VT nákupu 5 Kč).

Výjimka (AUTO, od 2026-05): pokud je v budoucnu allow_charge (levný nákup), solver povolí FVE export i při sell[t] < buy[t], ale jen když (sell[t] min_buy_charge) ≥ (future_sell_opportunity charge_acquisition) + degrad — tj. prodat teď a později levně dobít překoná uložení PV na večerní špičku. Při odpoledním sell ~1,4 Kč a večer ~5,5 Kč export se nevnucuje (energie do baterie). Implementace: solve_dispatch() v planning_engine.py.

Pro baterii stejný test v exportním slotu nesmí být jediná logika arbitráže — večer téměř vždy sell[t] < buy[t] (VT/NT vs výkupní marže), přesto má smysl vybíjet do sítě energii nabitou v levném okně.


3. Proč min(buy) přes celý horizont není nákupní cena zásoby

min(buy_price) v horizontu je jeden 15min slot (nejlevnější čtvrthodina).

home-01 (typicky) Hodnota
Kapacita baterie 64 kWh
Max import ze site 17 kW
Max energie ze sítě / slot (15 min) 17 kW × 0,25 h ≈ 4,25 kWh
Počet slotů na „plné“ grid nabíjení ~15 slotů (≈ 64/4,25), tedy hodiny

Min buy tedy popisuje špičku v jednom čtvrthodině, ne průměrnou cenu energie, kterou skutečně natankujeme přes dlouhé nabíjecí okno.

Použití min(buy) jako „acquisition cost“ pro večerní export:

  • podhodnotí skutečný náklad, pokud nabíjíme i v druhém/třetím levném slotu s vyšší cenou;
  • neříká nic o tom, kolik energie v levném pásmu vůbec nabít — to řeší masky allow_charge a rozpočet Wh, ne jedna čísla.

Kde je min(buy) dnes OK: hrubá brána („existuje v horizontu levný nákup?“), výběr slotů pro vrstvu B (buy ≤ min + ε), ne jako jediná proměnná pro výpočet zisku z baterie.


4. Co používat místo toho (směr návrhu)

Pojem Význam Poznámka
buy_charge_window Nákupní cena energie do baterie Odvozená z množiny nabíjecích slotů (allow_charge / skutečný bc+gi), ne z jednoho minima
sell_discharge_window Výkup při vybíjení do sítě Např. průměr / percentil sell v allow_discharge_export slotech
Spread sell_discharge buy_charge degradace Rozhoduje, zda má smysl večer ge_bat

Příklady výpočtu buy_charge (zvolit jednu politiku v implementaci):

  1. Průměr přes allow_charge sloty (vážený 0/1 — všechny povolené sloty stejně).
  2. Průměr přes N nejlevnějších slotů, kde N = počet slotů potřebných na dobití:
    ceil(energy_to_fill_wh / (max_charge_w × η × 0,25 h)).
  3. Vážený průměr sum(buy[t] × charge_wh[t]) / sum(charge_wh[t]) z výsledku LP (až po solve — iterace nebo aproximace před solve z masky).

Pro home-01 při nabíjení 11:0014:00 za ~0,70,9 Kč a výprodeji 19:0022:00 za ~3,55,5 Kč je spread řádově 24 Kč/kWh — to LP dnes nevidí, pokud účtuje večerní buy[t].


5. Co nedělat v dalších iteracích

  • Navrhovat „opravu arbitráže“ jen jako sell[t] min(buy horizontu) v objective — min buy je jeden slot, nabíjení je více hodin.
  • Zaměňovat stejnoslotové buy/sell s mezi-slotovou arbitráží — uživatel to explicitně považuje za nesmysl.
  • Očekávat, že zvýšení allow_discharge_export samo spustí večerní SELL, když objective pořád trestá export při buy[t] > sell[t] ve stejném slotu.

6. Implementace (2026-05) a backlog

Hotovo (jednoduchá varianta před solve)

  1. ems.fn_load_planning_slots_full (R__063): sloupce
    charge_acquisition_buy_czk_kwh, charge_acquisition_cutoff_at.
  2. Vážený průměr buy v allow_charge slotech před prvním allow_discharge_export (cutoff):
    Σ(buy × per_slot_charge_wh) / Σ(per_slot_charge_wh) — bez future_sell z odpolední FVE (jinak acquisition nafukovala večerní export).
  3. solve_dispatch: v exportních slotech (allow_discharge_export) přičíst k objective
    + ge_bat[t] × charge_acquisition × INTERVAL_H/1000 (náklad uložené energie), ponechat ge×sell. Snapshot v solver_params.inputs.
  4. FVE opportunity: varianta B — lookahead future_sell_opportunity, ne jen sell[t] v odhadu PV Wh.

Zbývá / vylepšit

  1. Iterace po solve: přepočítat acquisition z plánovaných bc+gi / pv místo odhadu z masek — viz docs/05-todo.md.
  2. Objective: explicitní ge_bat × (sell acquisition degrad) vs současné ge×sell + +ge_bat×acquisition (ekvivalentní jen pokud ge_pv v exportním slotu ≈ 0).
  3. Masky: více charge slotů ∝ energy_to_fill / per_slot_wh — viz planning.md.
  4. Bilance / guardy: zpřesnit, aby večerní export nebyl vázaný na falešný gi×buy v tomže slotu.

7. Ověření po změně (home-01)

-- levné okno: víc allow_charge, rozumný buy_charge (~0.71.0)
select interval_start at time zone 'Europe/Prague' as t,
       buy_price, allow_charge
from ems.fn_load_planning_slots_full(2, <from>, <to>, <soc_wh>)
where buy_price < 1.2
order by 1;

-- večer: BATTERY_SELL, záporný grid_setpoint
select interval_start at time zone 'Europe/Prague' as t,
       effective_buy_price, effective_sell_price,
       battery_setpoint_w, grid_setpoint_w, export_mode
from ems.planning_interval
where run_id = <active_run_id>
  and extract(hour from interval_start at time zone 'Europe/Prague') between 19 and 22;

Očekávání: SoC před večerem 7090 % po levném pásmu; večer export do sítě v špičce sell, ne jen ~2 kW do domu.


Poslední aktualizace: 2026-05 — charge_acquisition před 1. exportem + LP ge_bat náklad; iterace po solve v TODO.