urpava KV1 vyliti v maxu v noci
This commit is contained in:
@@ -30,6 +30,7 @@ from services.planning_engine import (
|
||||
_pre_neg_pv_export_bundle,
|
||||
_pre_neg_pv_export_forecast_cushion_ok_for_day,
|
||||
_prague_calendar_date,
|
||||
_prague_hour,
|
||||
_pre_neg_buy_soc_ceiling_wh,
|
||||
_pre_neg_peak_sell_idx,
|
||||
_pre_neg_pv_export_forecast_cushion_ok,
|
||||
@@ -2836,6 +2837,73 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
self.assertLessEqual(len(push), 4)
|
||||
self.assertEqual(push, [0, 1, 2, 3][: len(push)])
|
||||
|
||||
def test_kv1_evening_push_profitable_vs_morning_zone_peak(self) -> None:
|
||||
"""v52: KV1 večer ≥ ranní max (5–11) − degrad; pod prahem ne."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
neg = datetime(2026, 5, 31, 8, 15, tzinfo=prague).astimezone(timezone.utc)
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
interval_start=datetime(2026, 5, 31, 6, 0, tzinfo=prague).astimezone(timezone.utc),
|
||||
buy_price=6.35,
|
||||
sell_price=2.55,
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=400,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
),
|
||||
PlanningSlot(
|
||||
interval_start=neg,
|
||||
buy_price=6.35,
|
||||
sell_price=-0.3,
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=400,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
),
|
||||
]
|
||||
eve = PlanningSlot(
|
||||
interval_start=datetime(2026, 5, 30, 22, 0, tzinfo=prague).astimezone(timezone.utc),
|
||||
buy_price=6.35,
|
||||
sell_price=3.30,
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=400,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
)
|
||||
self.assertTrue(
|
||||
_slot_evening_push_profitable(
|
||||
eve,
|
||||
charge_acquisition_czk_kwh=6.35,
|
||||
min_spread=0.3,
|
||||
slots=slots,
|
||||
first_neg_sell_idx=1,
|
||||
kv1_evening_push=True,
|
||||
)
|
||||
)
|
||||
eve_low = PlanningSlot(
|
||||
interval_start=eve.interval_start,
|
||||
buy_price=6.35,
|
||||
sell_price=2.20,
|
||||
pv_a_forecast_w=0,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=400,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
)
|
||||
self.assertFalse(
|
||||
_slot_evening_push_profitable(
|
||||
eve_low,
|
||||
charge_acquisition_czk_kwh=6.35,
|
||||
min_spread=0.3,
|
||||
slots=slots,
|
||||
first_neg_sell_idx=1,
|
||||
kv1_evening_push=True,
|
||||
)
|
||||
)
|
||||
|
||||
def test_evening_push_ok_when_sell_below_buy_vs_acq(self) -> None:
|
||||
"""v47: večer sell<buy ale >acq — push pro vyprázdnění před neg dnem."""
|
||||
slot = PlanningSlot(
|
||||
@@ -3817,6 +3885,106 @@ class PreNegativeSellExportTests(unittest.TestCase):
|
||||
self.assertGreater(abs(res[1].grid_setpoint_w), 500)
|
||||
self.assertLess(abs(res[1].battery_setpoint_w), 500)
|
||||
|
||||
def test_kv1_evening_push_when_sell_above_morning_peak_not_at_dawn(self) -> None:
|
||||
"""v52: večer sell ≥ ranní max (5–11) před sell<0 — push, ne až úsvit za horší cenu."""
|
||||
prague = ZoneInfo("Europe/Prague")
|
||||
base = datetime(2026, 5, 30, 23, 0, tzinfo=prague)
|
||||
specs: list[tuple[int, float, float]] = []
|
||||
for i in range(40):
|
||||
local = base + timedelta(minutes=15 * i)
|
||||
h = local.hour
|
||||
if h >= 23 or h <= 2:
|
||||
sell = 3.25 if i % 2 == 0 else 3.10
|
||||
pv_a = 0
|
||||
elif h == 4:
|
||||
sell = 2.80
|
||||
pv_a = 900
|
||||
elif 5 <= h <= 7:
|
||||
sell = 2.55
|
||||
pv_a = 1200
|
||||
elif h == 8 and local.minute == 15:
|
||||
sell = -0.20
|
||||
pv_a = 4000
|
||||
elif h >= 8:
|
||||
sell = -0.30
|
||||
pv_a = 4500
|
||||
else:
|
||||
sell = 3.00
|
||||
pv_a = 0
|
||||
specs.append((pv_a, sell))
|
||||
slots: list[PlanningSlot] = []
|
||||
for i, (pv_a, sell) in enumerate(specs):
|
||||
local = base + timedelta(minutes=15 * i)
|
||||
slots.append(
|
||||
PlanningSlot(
|
||||
interval_start=local.astimezone(timezone.utc),
|
||||
buy_price=6.3525,
|
||||
sell_price=sell,
|
||||
pv_a_forecast_w=pv_a,
|
||||
pv_b_forecast_w=0,
|
||||
load_baseline_w=400,
|
||||
ev1_connected=False,
|
||||
ev2_connected=False,
|
||||
allow_charge=sell < 0,
|
||||
allow_discharge_export=sell >= 0,
|
||||
charge_acquisition_buy_czk_kwh=6.3525,
|
||||
)
|
||||
)
|
||||
battery = _battery(uc_wh=12_500.0, min_pct=10.0, arb_pct=30.0)
|
||||
battery.reserve_soc_percent = 30.0
|
||||
battery.max_discharge_power_w = 6250
|
||||
battery.discharge_slot_buffer = 1.5
|
||||
hp = SimpleNamespace(rated_heating_power_w=0, tuv_min_temp_c=45.0, tuv_target_temp_c=55.0)
|
||||
grid = SimpleNamespace(
|
||||
max_import_power_w=17_000,
|
||||
max_export_power_w=8000,
|
||||
block_export_on_negative_sell=True,
|
||||
purchase_pricing_mode="fixed",
|
||||
)
|
||||
vehicles = [
|
||||
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
|
||||
SimpleNamespace(max_charge_power_w=0, battery_capacity_kwh=1.0, default_target_soc_pct=80.0),
|
||||
]
|
||||
res, _, snap = solve_dispatch(
|
||||
slots,
|
||||
battery,
|
||||
hp,
|
||||
grid,
|
||||
[None, None],
|
||||
vehicles,
|
||||
0.62 * battery.soc_max_wh,
|
||||
50.0,
|
||||
operating_mode="AUTO",
|
||||
)
|
||||
self.assertTrue(snap["inputs"].get("kv1_evening_push_morning_peak_rule"))
|
||||
push_iso = set(snap["inputs"].get("evening_push_ts") or [])
|
||||
self.assertTrue(
|
||||
any(
|
||||
slots[i].interval_start.isoformat() in push_iso
|
||||
for i in range(4)
|
||||
if _in_evening_push_hour_window(slots[i])
|
||||
),
|
||||
"večerní push musí zahrnout slot ≥17h",
|
||||
)
|
||||
eve_idx = next(
|
||||
i
|
||||
for i in range(len(slots))
|
||||
if _in_evening_push_hour_window(slots[i])
|
||||
and float(slots[i].sell_price) >= 3.0
|
||||
)
|
||||
self.assertLess(
|
||||
res[eve_idx].grid_setpoint_w,
|
||||
-2000,
|
||||
"večer ~3,3 Kč: vývoz do sítě, ne jen úsvit",
|
||||
)
|
||||
dawn_idx = next(i for i, s in enumerate(slots) if _prague_hour(s) == 4)
|
||||
if slots[dawn_idx].interval_start.isoformat() not in push_iso:
|
||||
self.assertLess(
|
||||
abs(res[dawn_idx].battery_setpoint_w),
|
||||
4000,
|
||||
"úsvit není jediný velký bat export pokud večer push proběhl",
|
||||
)
|
||||
|
||||
|
||||
class Home01PvStoreValueTests(unittest.TestCase):
|
||||
"""FVE: spot sell<0 → nabít/vent B; sell>=0 → LP volí export vs bc (ne tvrdý curtail)."""
|
||||
|
||||
Reference in New Issue
Block a user