From fc0761fb2a5555e40f06f154fe645411ff3bce90 Mon Sep 17 00:00:00 2001 From: Dusan Vojacek Date: Thu, 21 May 2026 15:04:24 +0200 Subject: [PATCH] dalsi ladeni --- backend/services/planning_engine.py | 17 ++++- backend/tests/test_planning_dispatch_milp.py | 73 +++++++++++++++++++ .../planning-arbitrage-accounting.md | 4 +- docs/04-modules/planning.md | 1 + 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/backend/services/planning_engine.py b/backend/services/planning_engine.py index b3c3a6d..414d251 100644 --- a/backend/services/planning_engine.py +++ b/backend/services/planning_engine.py @@ -1095,16 +1095,31 @@ def solve_dispatch( 0.0, float(s.pv_a_forecast_w) + float(s.pv_b_forecast_w) - load_t, ) + # Mezi-slotová arbitráž: ráno prodat FVE (sell > acquisition), odpoledne levně + # nabít ze sítě — necpát přebytek do baterie jen protože sell < buy ve stejném slotu. + cross_slot_pv_export = ( + t not in charge_slots + and pv_surplus_w > 0 + and sell_t >= charge_acquisition_czk_kwh + min_spread + and any(ts in charge_slots for ts in range(t + 1, T)) + ) # Ztrátový export FVE (sell ≪ buy): zakázat jen pokud jde energii do baterie. - # Výjimky: plná baterie (ventil), neriťitelné pv_b s přebytkem. + # Výjimky: plná baterie (ventil), neriťitelné pv_b s přebytkem, cross-slot výše. if sell_t < buy_t - min_spread: block_loss_pv_export = not ( float(s.pv_b_forecast_w) > 0 and pv_surplus_w > 0 ) if t == 0 and current_soc_wh >= float(battery.soc_max_wh) - soc_headroom_wh: block_loss_pv_export = False + if cross_slot_pv_export: + block_loss_pv_export = False if block_loss_pv_export: prob += ge_pv[t] == 0 + if cross_slot_pv_export: + prob += bc[t] == 0 + prob += ge_pv[t] >= min( + pv_surplus_w, float(grid.max_export_power_w) + ) # Drahý nákup oproti horizontu: import jen na load + EV + TČ, ne na grid-nabíjení. if buy_t >= 0 and buy_t > ref_buy_horizon + min_spread: prob += gi[t] <= load_t + ev_cap_t + hp_rated_w diff --git a/backend/tests/test_planning_dispatch_milp.py b/backend/tests/test_planning_dispatch_milp.py index 77c8ad8..8a8f684 100644 --- a/backend/tests/test_planning_dispatch_milp.py +++ b/backend/tests/test_planning_dispatch_milp.py @@ -1420,6 +1420,79 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase): ) self.assertLess(evening.battery_setpoint_w, -500) + def test_morning_pv_export_when_sell_above_acquisition_and_later_grid_charge(self) -> None: + """Ráno sell>acquisition a sell min(buy téhož dne)+degrad` — ne globální min zítra). Lookahead VT→NT jen před tímto oknem. **`charge_acquisition`:** vážený `buy` jen u `allow_grid_charge` (ne FVE vrstva A). + - **LP (AUTO):** pokud `sell ≥ charge_acquisition + degrad` a později existuje `allow_charge`, přebytek FVE jde **do sítě** (`ge_pv`), ne do baterie — i když ve stejném slotu `sell < buy` (ranní VT výkup vs odpolední NT nákup). Viz [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md) §2.2. - **`ref_buy_min` (brána exportu):** `min(buy_price)` horizontu — jen „existuje levný nákup?“, **ne** průměrná cena nabití přes hodiny. Export sloty: `sell > ref_buy_min + degradation` (spot). Viz [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md). - Pokud `energy_to_fill <= 0` nebo `charge_slot_buffer = 0`: všechny sloty povoleny. - **LP ekonomické guardy** (`solve_dispatch`, AUTO): pokud `sell < buy − degradation` → `ge_pv=0` (výjimka: plná baterie, přebytek **pv_b**). Pokud `buy > min(buy)+degradation` → `gi` jen na load+EV+TČ. Viz `planning_engine.py` sekce po slot pre-selection.