a dalsi pokus o opravu
This commit is contained in:
@@ -71,7 +71,7 @@ NEG_BUY_CHARGE_SHORTFALL_PENALTY_CZK_KWH = 100.0
|
||||
PRE_NEG_CHARGE_PENALTY_CZK_KWH = 400.0
|
||||
PRE_NEG_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.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).
|
||||
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).
|
||||
@@ -1660,20 +1660,12 @@ def _slot_evening_push_profitable(
|
||||
*,
|
||||
charge_acquisition_czk_kwh: float,
|
||||
min_spread: float,
|
||||
spot_push_sell_ge_buy: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Push večerní špičky: acq+spread (historická zásoba).
|
||||
Spot (home-01): navíc sell >= buy−spread — jinak vývoz za 3 Kč a noc import za 5 Kč.
|
||||
Fixní tarif (KV1): jen acq+spread (sell často < konstantní buy).
|
||||
Push večerní špičky: sell > acq+spread (zásoba z levného nabití).
|
||||
Večer sell<buy je OK — vyprázdnění před neg dnem / FVE; problém je až nocí import za 5 Kč.
|
||||
"""
|
||||
sell_t = float(slot.sell_price)
|
||||
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
|
||||
return float(slot.sell_price) > float(charge_acquisition_czk_kwh) + float(min_spread)
|
||||
|
||||
|
||||
def _evening_push_segment_candidates(
|
||||
@@ -1682,7 +1674,6 @@ def _evening_push_segment_candidates(
|
||||
*,
|
||||
charge_acquisition_czk_kwh: float,
|
||||
min_spread: float,
|
||||
spot_push_sell_ge_buy: bool = False,
|
||||
discharge_export_ok: set[int] | None = None,
|
||||
) -> list[int]:
|
||||
"""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],
|
||||
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
||||
min_spread=min_spread,
|
||||
spot_push_sell_ge_buy=spot_push_sell_ge_buy,
|
||||
):
|
||||
continue
|
||||
out.append(t)
|
||||
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(
|
||||
slots: list[PlanningSlot],
|
||||
discharge_export_ok: set[int] | None = None,
|
||||
@@ -1758,7 +1781,6 @@ def _evening_battery_export_push_indices(
|
||||
soc_max_wh: float,
|
||||
per_slot_discharge_wh: float,
|
||||
discharge_slot_buffer: float,
|
||||
spot_push_sell_ge_buy: bool = False,
|
||||
discharge_export_ok: set[int] | None = None,
|
||||
evening_start_hour: int = 17,
|
||||
) -> list[int]:
|
||||
@@ -1792,7 +1814,6 @@ def _evening_battery_export_push_indices(
|
||||
seg,
|
||||
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
||||
min_spread=degrad_czk_kwh,
|
||||
spot_push_sell_ge_buy=spot_push_sell_ge_buy,
|
||||
discharge_export_ok=discharge_export_ok,
|
||||
)
|
||||
if not candidates:
|
||||
@@ -2496,6 +2517,7 @@ def solve_dispatch(
|
||||
evening_push_ts: set[int] = set()
|
||||
evening_early_export_penalty_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
|
||||
if om == "AUTO":
|
||||
per_slot_discharge_wh_pre = max(
|
||||
@@ -2520,7 +2542,6 @@ def solve_dispatch(
|
||||
soc_max_wh=float(battery.soc_max_wh),
|
||||
per_slot_discharge_wh=per_slot_push_wh_pre,
|
||||
discharge_slot_buffer=discharge_buf_pre,
|
||||
spot_push_sell_ge_buy=not purchase_fixed_pre,
|
||||
discharge_export_ok=discharge_export_slots,
|
||||
)
|
||||
)
|
||||
@@ -2557,6 +2578,10 @@ def solve_dispatch(
|
||||
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
|
||||
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(
|
||||
slots,
|
||||
first_neg_buy_idx=first_neg_buy_idx,
|
||||
@@ -3739,7 +3764,11 @@ def solve_dispatch(
|
||||
# Strict: síť jen EV+TČ; baseload z baterie/FVE.
|
||||
# Relaxed: síť smí baseload jen mimo night_self_consume (v46).
|
||||
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:
|
||||
prob += gi[t] <= ev_cap_t + hp[t] + float(s.load_baseline_w)
|
||||
|
||||
@@ -2796,45 +2796,12 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
self.assertLessEqual(len(push), 4)
|
||||
self.assertEqual(push, [0, 1, 2, 3][: len(push)])
|
||||
|
||||
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")
|
||||
base = datetime(2026, 5, 30, 20, 0, tzinfo=prague)
|
||||
slots = [
|
||||
PlanningSlot(
|
||||
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),
|
||||
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(
|
||||
interval_start=datetime(
|
||||
2026, 5, 30, 20, 0, tzinfo=ZoneInfo("Europe/Prague")
|
||||
).astimezone(timezone.utc),
|
||||
buy_price=5.5,
|
||||
sell_price=3.3,
|
||||
pv_a_forecast_w=0,
|
||||
@@ -2844,39 +2811,9 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
||||
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,
|
||||
slot, charge_acquisition_czk_kwh=0.61, min_spread=0.15
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user