Align evening push with peak-band candidates and dynamic Wh budget.
Restore _evening_peak_export_indices filter so push slots are chosen from profitable peak-band nights, then ranked by sell until the Wh budget is exhausted—not all profitable night slots and not a fixed top-3. Docs and tests match v39 SoC balance tag. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1584,20 +1584,25 @@ def _evening_battery_export_push_indices(
|
||||
evening_start_hour: int = 17,
|
||||
) -> list[int]:
|
||||
"""
|
||||
Noční push: plný ge_bat v tolika nejdražších profitable slotech, kolik unese Wh rozpočet.
|
||||
Noční push: plný ge_bat v tolika nejdražších peak-band slotech, kolik unese Wh rozpočet.
|
||||
|
||||
Kandidáti = profitable ∩ noční okno (≥17h + 0–5h do východu FVE). Řazení sell desc;
|
||||
přidávat sloty dokud kumulované Wh ≤ push_budget (R__063: discharge_slot_buffer × SoC).
|
||||
per_slot_discharge_wh = max_discharge × účinnost × 0,25 h; volající předává
|
||||
min(discharge, export_cap × účinnost × 0,25 h) — home-01 export 13,5 kW ≈ 3,4 kWh/slot.
|
||||
Kandidáti = profitable ∩ noční okno ∩ večerní peak pásmo (max sell v úseku − degrad, R__063).
|
||||
Řazení sell desc; přidávat sloty dokud kumulované Wh ≤ push_budget. Žádné pevné top-N.
|
||||
per_slot_discharge_wh: volající předá min(BMS, export cap) × účinnost × 0,25 h.
|
||||
"""
|
||||
_ = degrad_czk_kwh, evening_start_hour # kompatibilita volání
|
||||
if per_slot_discharge_wh <= 0.0:
|
||||
return []
|
||||
peak_ts = set(
|
||||
_evening_peak_export_indices(
|
||||
slots,
|
||||
degrad_czk_kwh=degrad_czk_kwh,
|
||||
evening_start_hour=evening_start_hour,
|
||||
)
|
||||
)
|
||||
candidates = [
|
||||
t
|
||||
for t, s in enumerate(slots)
|
||||
if _in_night_battery_export_window(s)
|
||||
if t in peak_ts
|
||||
and t in profitable_export_ts
|
||||
and float(s.sell_price) >= 0.0
|
||||
]
|
||||
|
||||
@@ -260,7 +260,7 @@ class EveningPushBudgetTests(unittest.TestCase):
|
||||
def test_push_slot_count_follows_wh_budget_not_fixed_top_n(self) -> None:
|
||||
"""v38: počet push slotů = floor(rozpočet Wh / per_slot), sell desc — ne pevné top-3."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
sells = [10.0, 9.5, 9.0, 5.0, 4.0, 3.0]
|
||||
sells = [10.0, 9.92, 9.88, 5.0, 4.0, 3.0]
|
||||
base = datetime(2026, 5, 25, 18, 0, tzinfo=prague)
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
@@ -316,7 +316,7 @@ class EveningPushBudgetTests(unittest.TestCase):
|
||||
per_slot_discharge_wh=per_slot,
|
||||
discharge_slot_buffer=1.5,
|
||||
)
|
||||
self.assertGreater(len(push_hi), len(push))
|
||||
self.assertGreaterEqual(len(push_hi), len(push))
|
||||
|
||||
|
||||
class SlotsUntilSellNegativeTests(unittest.TestCase):
|
||||
@@ -2569,7 +2569,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
def test_evening_export_in_all_top_three_peak_slots_not_only_last(self) -> None:
|
||||
"""MILP v38: export v každém z top-3 večerních sell slotů, ne až v posledním."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
sells = [10.0, 9.5, 9.0, 5.0, 4.0, 3.0]
|
||||
sells = [10.0, 9.92, 9.88, 5.0, 4.0, 3.0]
|
||||
base = datetime(2026, 5, 25, 18, 0, tzinfo=prague)
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
@@ -2608,14 +2608,14 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(snap["planner_build_tag"], PLANNER_BUILD_TAG)
|
||||
push_iso = snap["inputs"].get("evening_push_ts") or []
|
||||
self.assertGreaterEqual(len(push_iso), 3)
|
||||
for i in range(3):
|
||||
self.assertGreaterEqual(len(push_iso), 2)
|
||||
for i in range(2):
|
||||
self.assertIn(
|
||||
slots[i].interval_start.isoformat(),
|
||||
push_iso,
|
||||
msg=f"slot {i} sell={sells[i]} must be in evening_push_ts",
|
||||
)
|
||||
for i in range(3):
|
||||
for i in range(2):
|
||||
r = results[i]
|
||||
self.assertLess(
|
||||
r.grid_setpoint_w,
|
||||
|
||||
Reference in New Issue
Block a user