a dalsi pokus o opravu
Some checks failed
CI and deploy / migration-check (push) Failing after 19s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-30 22:15:40 +02:00
parent 96d0d52b07
commit 4f67aad4d8
4 changed files with 67 additions and 98 deletions

View File

@@ -71,7 +71,7 @@ NEG_BUY_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 100.0
PRE_NEG_CHARGE_PENALTY_CZK_KWH = 400.0 PRE_NEG_CHARGE_PENALTY_CZK_KWH = 400.0
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0 PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0
PRE_NEG_BATT_EXPORT_MIN_SELL_CZK_KWH = 1.0 PRE_NEG_BATT_EXPORT_MIN_SELL_CZK_KWH = 1.0
PLANNER_BUILD_TAG = "2026-05-30-evening-spot-sell-ge-buy-v46" PLANNER_BUILD_TAG = "2026-05-30-post-push-night-battery-v47"
# Mimo evening_push: preferovat bd pro dům místo gi, když buy >> acq (účinná cena importu). # Mimo evening_push: preferovat bd pro dům místo gi, když buy >> acq (účinná cena importu).
NIGHT_SELF_CONSUME_IMPORT_SURCHARGE_CZK_KWH = 4.0 NIGHT_SELF_CONSUME_IMPORT_SURCHARGE_CZK_KWH = 4.0
# Po t_detach v prep: necpát PV do bat (měkké; tvrdý hold přes soc_target z rampy). # Po t_detach v prep: necpát PV do bat (měkké; tvrdý hold přes soc_target z rampy).
@@ -1660,20 +1660,12 @@ def _slot_evening_push_profitable(
*, *,
charge_acquisition_czk_kwh: float, charge_acquisition_czk_kwh: float,
min_spread: float, min_spread: float,
spot_push_sell_ge_buy: bool = False,
) -> bool: ) -> bool:
""" """
Push večerní špičky: acq+spread (historická zásoba). Push večerní špičky: sell > acq+spread (zásoba z levného nabití).
Spot (home-01): navíc sell >= buyspread — jinak vývoz za 3 Kč a noc import za 5 Kč. Večer sell<buy je OK — vyprázdnění před neg dnem / FVE; problém je až nocí import za 5 Kč.
Fixní tarif (KV1): jen acq+spread (sell často < konstantní buy).
""" """
sell_t = float(slot.sell_price) return float(slot.sell_price) > float(charge_acquisition_czk_kwh) + float(min_spread)
buy_t = float(slot.buy_price)
spread = float(min_spread)
if spot_push_sell_ge_buy and buy_t >= 0.0 and sell_t >= 0.0:
if sell_t < buy_t - spread:
return False
return sell_t > float(charge_acquisition_czk_kwh) + spread
def _evening_push_segment_candidates( def _evening_push_segment_candidates(
@@ -1682,7 +1674,6 @@ def _evening_push_segment_candidates(
*, *,
charge_acquisition_czk_kwh: float, charge_acquisition_czk_kwh: float,
min_spread: float, min_spread: float,
spot_push_sell_ge_buy: bool = False,
discharge_export_ok: set[int] | None = None, discharge_export_ok: set[int] | None = None,
) -> list[int]: ) -> list[int]:
"""Profitable sloty v nočním úseku — výběr pořadí a strop dělá rozpočet Wh (sell desc).""" """Profitable sloty v nočním úseku — výběr pořadí a strop dělá rozpočet Wh (sell desc)."""
@@ -1698,13 +1689,45 @@ def _evening_push_segment_candidates(
slots[t], slots[t],
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh, charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
min_spread=min_spread, min_spread=min_spread,
spot_push_sell_ge_buy=spot_push_sell_ge_buy,
): ):
continue continue
out.append(t) out.append(t)
return out return out
def _post_evening_push_night_self_consume_indices(
slots: list[PlanningSlot],
evening_push_ts: set[int],
) -> set[int]:
"""
Po posledním evening_push daného večera až do rána: dům z baterie, ne import za ~5 Kč.
"""
if not evening_push_ts:
return set()
last_push_by_day: dict[object, int] = {}
for t in evening_push_ts:
last_push_by_day[_prague_calendar_date(slots[t])] = max(
last_push_by_day.get(_prague_calendar_date(slots[t]), -1),
t,
)
out: set[int] = set()
for t, s in enumerate(slots):
day = _prague_calendar_date(s)
t_last = last_push_by_day.get(day)
if t_last is None or t <= t_last:
continue
if t in evening_push_ts:
continue
if not _in_night_battery_export_window(s):
continue
if float(s.buy_price) <= 0.0:
continue
if float(s.load_baseline_w) <= 0:
continue
out.add(t)
return out
def _evening_push_calendar_segments( def _evening_push_calendar_segments(
slots: list[PlanningSlot], slots: list[PlanningSlot],
discharge_export_ok: set[int] | None = None, discharge_export_ok: set[int] | None = None,
@@ -1758,7 +1781,6 @@ def _evening_battery_export_push_indices(
soc_max_wh: float, soc_max_wh: float,
per_slot_discharge_wh: float, per_slot_discharge_wh: float,
discharge_slot_buffer: float, discharge_slot_buffer: float,
spot_push_sell_ge_buy: bool = False,
discharge_export_ok: set[int] | None = None, discharge_export_ok: set[int] | None = None,
evening_start_hour: int = 17, evening_start_hour: int = 17,
) -> list[int]: ) -> list[int]:
@@ -1792,7 +1814,6 @@ def _evening_battery_export_push_indices(
seg, seg,
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh, charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
min_spread=degrad_czk_kwh, min_spread=degrad_czk_kwh,
spot_push_sell_ge_buy=spot_push_sell_ge_buy,
discharge_export_ok=discharge_export_ok, discharge_export_ok=discharge_export_ok,
) )
if not candidates: if not candidates:
@@ -2496,6 +2517,7 @@ def solve_dispatch(
evening_push_ts: set[int] = set() evening_push_ts: set[int] = set()
evening_early_export_penalty_ts: set[int] = set() evening_early_export_penalty_ts: set[int] = set()
night_self_consume_discourage_ts: set[int] = set() night_self_consume_discourage_ts: set[int] = set()
post_evening_push_night_ts: set[int] = set()
evening_push_hysteresis_retained = False evening_push_hysteresis_retained = False
if om == "AUTO": if om == "AUTO":
per_slot_discharge_wh_pre = max( per_slot_discharge_wh_pre = max(
@@ -2520,7 +2542,6 @@ def solve_dispatch(
soc_max_wh=float(battery.soc_max_wh), soc_max_wh=float(battery.soc_max_wh),
per_slot_discharge_wh=per_slot_push_wh_pre, per_slot_discharge_wh=per_slot_push_wh_pre,
discharge_slot_buffer=discharge_buf_pre, discharge_slot_buffer=discharge_buf_pre,
spot_push_sell_ge_buy=not purchase_fixed_pre,
discharge_export_ok=discharge_export_slots, discharge_export_ok=discharge_export_slots,
) )
) )
@@ -2557,6 +2578,10 @@ def solve_dispatch(
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh, charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
min_spread=float(degradation_cost_effective), min_spread=float(degradation_cost_effective),
) )
post_evening_push_night_ts = _post_evening_push_night_self_consume_indices(
slots, evening_push_ts
)
night_self_consume_discourage_ts |= post_evening_push_night_ts
pre_neg_buy_soc_ceiling_wh = _pre_neg_buy_soc_ceiling_wh( pre_neg_buy_soc_ceiling_wh = _pre_neg_buy_soc_ceiling_wh(
slots, slots,
first_neg_buy_idx=first_neg_buy_idx, first_neg_buy_idx=first_neg_buy_idx,
@@ -3739,7 +3764,11 @@ def solve_dispatch(
# Strict: síť jen EV+TČ; baseload z baterie/FVE. # Strict: síť jen EV+TČ; baseload z baterie/FVE.
# Relaxed: síť smí baseload jen mimo night_self_consume (v46). # Relaxed: síť smí baseload jen mimo night_self_consume (v46).
night_self_consume_slot = ( night_self_consume_slot = (
om == "AUTO" and t in night_self_consume_discourage_ts om == "AUTO"
and (
t in night_self_consume_discourage_ts
or t in post_evening_push_night_ts
)
) )
if relaxed_expensive_import and not night_self_consume_slot: if relaxed_expensive_import and not night_self_consume_slot:
prob += gi[t] <= ev_cap_t + hp[t] + float(s.load_baseline_w) prob += gi[t] <= ev_cap_t + hp[t] + float(s.load_baseline_w)

View File

@@ -2796,45 +2796,12 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
self.assertLessEqual(len(push), 4) self.assertLessEqual(len(push), 4)
self.assertEqual(push, [0, 1, 2, 3][: len(push)]) self.assertEqual(push, [0, 1, 2, 3][: len(push)])
def test_home01_evening_no_push_when_sell_below_buy(self) -> None: def test_evening_push_ok_when_sell_below_buy_vs_acq(self) -> None:
"""v46: OTE večer sell<buy — žádný push (ne vývoz za 3 Kč při buy 5 Kč).""" """v47: večer sell<buy ale >acq — push pro vyprázdnění před neg dnem."""
prague = ZoneInfo("Europe/Prague") slot = PlanningSlot(
base = datetime(2026, 5, 30, 20, 0, tzinfo=prague) interval_start=datetime(
slots = [ 2026, 5, 30, 20, 0, tzinfo=ZoneInfo("Europe/Prague")
PlanningSlot( ).astimezone(timezone.utc),
interval_start=base + timedelta(minutes=15 * i),
buy_price=5.5,
sell_price=3.3 - 0.05 * i,
pv_a_forecast_w=0,
pv_b_forecast_w=0,
load_baseline_w=1400,
ev1_connected=False,
ev2_connected=False,
allow_discharge_export=True,
charge_acquisition_buy_czk_kwh=0.61,
)
for i in range(4)
]
battery = _battery(uc_wh=64_000.0, terminal_soc_value_factor=0.0)
per_slot = min(18_000, 13_500) * 0.95 * 0.25
push = _evening_battery_export_push_indices(
slots,
charge_acquisition_czk_kwh=0.61,
degrad_czk_kwh=0.15,
current_soc_wh=0.55 * battery.soc_max_wh,
min_soc_wh=battery.min_soc_wh,
soc_max_wh=battery.soc_max_wh,
per_slot_discharge_wh=per_slot,
discharge_slot_buffer=1.5,
spot_push_sell_ge_buy=True,
)
self.assertEqual(push, [])
def test_spot_evening_push_requires_sell_ge_buy(self) -> None:
"""v46: spot nepush když sell < buy (3 Kč vývoz / 5 Kč nákup)."""
base = datetime(2026, 5, 30, 20, 0, tzinfo=ZoneInfo("Europe/Prague"))
bad = PlanningSlot(
interval_start=base.astimezone(timezone.utc),
buy_price=5.5, buy_price=5.5,
sell_price=3.3, sell_price=3.3,
pv_a_forecast_w=0, pv_a_forecast_w=0,
@@ -2844,39 +2811,9 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
ev2_connected=False, ev2_connected=False,
allow_discharge_export=True, allow_discharge_export=True,
) )
ok = PlanningSlot(
interval_start=base.astimezone(timezone.utc),
buy_price=2.0,
sell_price=4.0,
pv_a_forecast_w=0,
pv_b_forecast_w=0,
load_baseline_w=1400,
ev1_connected=False,
ev2_connected=False,
allow_discharge_export=True,
)
self.assertFalse(
_slot_evening_push_profitable(
bad,
charge_acquisition_czk_kwh=0.61,
min_spread=0.15,
spot_push_sell_ge_buy=True,
)
)
self.assertTrue( self.assertTrue(
_slot_evening_push_profitable( _slot_evening_push_profitable(
ok, slot, charge_acquisition_czk_kwh=0.61, min_spread=0.15
charge_acquisition_czk_kwh=0.61,
min_spread=0.15,
spot_push_sell_ge_buy=True,
)
)
self.assertTrue(
_slot_evening_push_profitable(
bad,
charge_acquisition_czk_kwh=0.61,
min_spread=0.15,
spot_push_sell_ge_buy=False,
) )
) )

View File

@@ -113,11 +113,11 @@ flowchart TD
- **`night_self_consume_discourage`** na **celé** noční okno mimo push; - **`night_self_consume_discourage`** na **celé** noční okno mimo push;
- při `relaxed_neg_prep_window` bez prep shortfall penalizace. - při `relaxed_neg_prep_window` bez prep shortfall penalizace.
6. **v46 — večerní push spot vs. buy:** 6. **v47 po večerním pushu noc z baterie:**
- push jen když **sell ≥ buy spread** (ne vývoz za 3 Kč při buy 5 Kč); - večerní push zůstává **sell > acq+spread** (sell&lt;buy je záměr před neg dnem);
- **`relaxed_expensive_import`** neobchází nocí **bd ≥ load** v `night_self_consume` slotech. - **`post_evening_push_night_ts`:** po pushu **bd ≥ load**, ne import ~5 Kč i při relaxed solve.
**Funkce:** … Tag: **`2026-05-30-evening-spot-sell-ge-buy-v46`**. **Funkce:** … Tag: **`2026-05-30-post-push-night-battery-v47`**.
### Arbitráž baterie — účtování mezi sloty (povinné čtení) ### Arbitráž baterie — účtování mezi sloty (povinné čtení)

View File

@@ -5,15 +5,18 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
--- ---
## 2026-05-30 — Večer: neprodávat pod buy; noc z bat i po relaxed (v46) ## 2026-05-30 — Po večerním pushu noc z baterie, ne import za 5 Kč (v47)
**Problém (v45 běh 20722):** Večer **push export** bat při **sell ~3,3 Kč** a **buy ~5,5 Kč** (acq ~0,61 → LP „zisk“), pak **22:00+ import ~5 Kč** pro dům při **SoC 36 %** (`relaxed_expensive_import` vypnul `bd≥load`). **Záměr uživatele:** Večerní vývoz za **~3 Kč/kWh** (sell&lt;buy) je **správně** — vyprázdnění před neg dnem/FVE. Špatně je **po pushu držet SoC a kupovat dům za ~5 Kč**.
**Změna (v46):** **Problém (v45v46):** Po pushu **SoC ~36 %**, pak **22:00+ grid import** pro baseload; `relaxed_expensive_import` obešel `bd≥load`.
- **`_slot_evening_push_profitable`:** spot (`not purchase_fixed`) vyžaduje **sell ≥ buy spread**; fixní tarif (KV1) jen **acq+spread**.
- **`relaxed_expensive_import`:** v **`night_self_consume_discourage_ts`** pořád **gi jen EV+TČ**, **bd krmí baseload**.
Tag **`2026-05-30-evening-spot-sell-ge-buy-v46`**. **Změna (v47):**
- **Večerní push:** zůstává **sell > acq+spread** (v46 sell≥buy **zrušeno**).
- **`post_evening_push_night_ts`:** po posledním push slotu večera → tvrdé **bd krmí dům** i při `relaxed_expensive_import`.
- **`night_self_consume`** + v45 neg okno beze změny.
Tag **`2026-05-30-post-push-night-battery-v47`**. (v46 na serveru nepoužívat — blokoval večerní push.)
--- ---