oprava battery hold
This commit is contained in:
@@ -15,6 +15,7 @@ from services.planning_engine import (
|
||||
_dispatch_result_comparison,
|
||||
_evening_battery_export_push_indices,
|
||||
_evening_peak_export_indices,
|
||||
_slot_evening_push_profitable,
|
||||
_evening_push_calendar_segments,
|
||||
_evening_push_discharge_budget_wh,
|
||||
_in_evening_push_hour_window,
|
||||
@@ -2547,7 +2548,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
interval_start=base,
|
||||
buy_price=7.3,
|
||||
buy_price=3.0,
|
||||
sell_price=4.4,
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
@@ -2587,7 +2588,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
def test_evening_battery_export_when_sell_above_acquisition(self) -> None:
|
||||
base = datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc)
|
||||
cheap = (0.75, 0.25)
|
||||
peak = (7.0, 4.8)
|
||||
peak = (3.5, 4.8)
|
||||
slots: list[PlanningSlot] = []
|
||||
for i in range(6):
|
||||
buy, sell = cheap if i < 2 else peak
|
||||
@@ -2701,14 +2702,14 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
self.assertEqual(r.export_mode, "BATTERY_SELL")
|
||||
|
||||
def test_evening_no_spread_export_below_segment_peak_home01(self) -> None:
|
||||
"""home-01 večer: plný export v top push slotech dle rozpočtu Wh, ne v levnějších mimo push."""
|
||||
"""Spot večer sell≥buy: push jen top sell sloty; levnější mimo push bez exportu."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
sells = [3.834, 3.518, 3.204, 3.204, 3.136, 3.020]
|
||||
base = datetime(2026, 5, 29, 20, 15, tzinfo=prague)
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
interval_start=base + timedelta(minutes=15 * i),
|
||||
buy_price=5.5,
|
||||
buy_price=3.0,
|
||||
sell_price=sells[i],
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
@@ -2795,16 +2796,105 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
self.assertLessEqual(len(push), 4)
|
||||
self.assertEqual(push, [0, 1, 2, 3][: len(push)])
|
||||
|
||||
def test_night_self_consume_prefers_battery_over_grid(self) -> None:
|
||||
"""v43: mezi push sloty baterie krmí dům místo importu za ~5 Kč."""
|
||||
def test_home01_evening_no_push_when_sell_below_buy(self) -> None:
|
||||
"""v46: OTE večer sell<buy — žádný push (ne vývoz za 3 Kč při buy 5 Kč)."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
sells = [3.9, 3.8, 3.1, 3.0]
|
||||
base = datetime(2026, 5, 29, 20, 0, tzinfo=prague)
|
||||
base = datetime(2026, 5, 30, 20, 0, tzinfo=prague)
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
interval_start=base + timedelta(minutes=15 * i),
|
||||
buy_price=5.0,
|
||||
sell_price=sells[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,
|
||||
sell_price=3.3,
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=1400,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
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(
|
||||
_slot_evening_push_profitable(
|
||||
ok,
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
def test_night_self_consume_prefers_battery_over_grid(self) -> None:
|
||||
"""v43/v46: mimo push baterie krmí dům, ne import za ~5 Kč."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
base = datetime(2026, 5, 29, 20, 0, tzinfo=prague)
|
||||
slot_specs = [
|
||||
(3.0, 3.9),
|
||||
(3.0, 3.8),
|
||||
(5.0, 3.1),
|
||||
(5.0, 3.0),
|
||||
]
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
interval_start=base + timedelta(minutes=15 * i),
|
||||
buy_price=buy,
|
||||
sell_price=sell,
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=2000,
|
||||
@@ -2813,7 +2903,7 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
allow_discharge_export=True,
|
||||
charge_acquisition_buy_czk_kwh=0.7,
|
||||
)
|
||||
for i in range(4)
|
||||
for i, (buy, sell) in enumerate(slot_specs)
|
||||
]
|
||||
battery = _battery(uc_wh=64_000.0, terminal_soc_value_factor=0.0)
|
||||
battery.max_discharge_power_w = 18_000
|
||||
|
||||
Reference in New Issue
Block a user