9.9 KiB
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:
- 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ě 8–16 slotů (2–4 h) dokud cena sedí.
- 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í | až ~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_chargea 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):
- Průměr přes
allow_chargesloty (vážený 0/1 — všechny povolené sloty stejně). - 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)). - 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:00–14:00 za ~0,7–0,9 Kč a výprodeji 19:00–22:00 za ~3,5–5,5 Kč je spread řádově 2–4 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/sells mezi-slotovou arbitráží — uživatel to explicitně považuje za nesmysl. - Očekávat, že zvýšení
allow_discharge_exportsamo spustí večerní SELL, když objective pořád trestá export přibuy[t] > sell[t]ve stejném slotu.
6. Implementace (LP-first přestavba, 2026-05)
Hotovo
ems.fn_load_planning_slots_full(R__063): grid B = nejlevnější sloty v AM/PM do Wh rozpočtu; nevyčerpaný AM rozpočet přejde do PM (odpolední NT za ~0,5 Kč může nabíjet i po ranním dobití).grid_target × charge_slot_buffer, cap slotů též × buffer. A = PV jen pokudsell ≥ future_sell_lookahead − degrad.solve_dispatch(AUTO): objectivegi×buy − ge_pv×sell − ge_bat×sell + ge_bat×acquisition(export bat. jen vallow_discharge_export). Odstraněn cross-slot guardge_pv ≥ surplus/bc=0dleexport_refill_net.- Guard FVE:
ge_pv=0přisell < future_sell_opportunity − degradjen pokudsell < 0(spot) nebo fixní tarif — usell ≥ 0spot neblokuje export FVE kvůli budoucímu peak sell (solver export vs. nabíjení; baterii šetříge_bat). Přisell < 0home-01:ge_pv=0/ ventil pole B. Tag2026-05-28-pv-positive-sell-solver-v29. solve_dispatch_two_pass: pass 1 → váženýbuyzbc+givallow_charge→ pass 2; volárun_daily_plan/run_rolling_replanv AUTO. Snapshot:acquisition_pass1_czk_kwh,acquisition_pass2_czk_kwh,two_pass_enabled.- Regrese:
Home01RegressionTestsvbackend/tests/test_planning_dispatch_milp.py; masky vtest_planning_charge_slot_selection.py. - Load-first (Deye, AUTO, tvrdý v34):
pv_ld/pv_sp,bc_pv/bc_gi;bc_pv + ge_pv ≤ pv_sp;gi ≤ bc_gi + max(0, max_load − pv_forecast); připv ≥ load + 500 Wpv_ld ≥ load. Plná bilancepv_a + pv_b + gi + bd = load + ev + hp + bc + ge. TestLoadFirstDispatchTests. - Self-konzistentní vrstva B (
R__063, 2026-05): iterativní filtr v plpgsql — vyloučí drahé grid sloty, pokudpv_charge_wh_ahead + neg_buy_wh_ahead >= 60 % deficitu SoC(levnější alternativa dál v horizontu). Failsafe unlock pokud výsledek nepokryje safety target. Důsledek:acquisition_pass1 ~ acquisition_pass2v drtivé většině případů. Nové debug sloupce:min_buy_before_cutoff_czk_kwh,pv_charge_wh_ahead,neg_buy_wh_ahead,grid_charge_suppressed_reason(cheaper_pv_ahead/cheaper_neg_buy_ahead/safety_failsafe_unlock). - Ekonomická transparentnost plánu (
V081, 2026-05):planning_interval—cashflow_czk,battery_arbitrage_czk,penalty_czk,green_bonus_czk;fn_plan_explain_bundle→economics_summary; post-processing vsolve_dispatch().
Co dál neřešit ad-hoc
- Další Python
if sell < buyguardy — ekonomiku drží LP + acquisition + masky rozpočtu slotů. - Multi-period inventory model (větší projekt) — mimo tuto vlnu.
7. Ověření po změně (home-01)
-- levné okno: víc allow_charge, rozumný buy_charge (~0.7–1.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 70–90 % 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-27 — self-konzistentní grid maska B (v12), ekonomické sloupce v planning_interval, economics_summary v explain bundle. Po deployi: PLANNER_BUILD_TAG=2026-05-27-self-consistent-grid-mask-v12, solver_params.objective_terms[].grid_charge_suppressed_reason.