tuning prodeje
Some checks failed
CI and deploy / migration-check (push) Failing after 18s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-29 22:45:02 +02:00
parent 230351b38a
commit 877f5b6180
4 changed files with 150 additions and 60 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_BATT_EXPORT_SHORTFALL_PENALTY_CZK_KWH = 80.0
PRE_NEG_BATT_EXPORT_MIN_SELL_CZK_KWH = 1.0
PLANNER_BUILD_TAG = "2026-05-29-neg-prep-infeasible-relax-v40b"
PLANNER_BUILD_TAG = "2026-05-29-evening-peak-only-export-v41"
# Po t_detach v prep: necpát PV do bat (měkké; tvrdý hold přes soc_target z rampy).
NEG_SELL_POST_DETACH_BCPV_DISCOURAGE_CZK_KWH = 250.0
# Večer před neg dnem: výboj do sítě (měkký shortfall na ge_bat).
@@ -1632,10 +1632,38 @@ def _evening_push_discharge_budget_wh(
return min(available_wh, exportable_full_wh * buf)
def _slot_evening_push_profitable(
slot: PlanningSlot,
*,
charge_acquisition_czk_kwh: float,
min_spread: float,
) -> bool:
"""Push večerní špičky: spot marže (acq+spread), ne fixní buy z konstantního horizontu."""
return float(slot.sell_price) > float(charge_acquisition_czk_kwh) + float(min_spread)
def _evening_push_peak_candidates(slots: list[PlanningSlot]) -> list[int]:
"""
Kandidáti tvrdého večerního push: sloty na **max sell** v nočním úseku
(ne široké pásmo peakdegrad — ten rozplizňoval export do levnějších slotů).
"""
candidates: list[int] = []
for seg in _night_export_window_segments(slots):
if not seg:
continue
seg_peak = max(float(slots[t].sell_price) for t in seg)
if seg_peak <= 0.0:
continue
for t in seg:
if float(slots[t].sell_price) >= seg_peak - 1e-6:
candidates.append(t)
return candidates
def _evening_battery_export_push_indices(
slots: list[PlanningSlot],
*,
profitable_export_ts: set[int],
charge_acquisition_czk_kwh: float,
degrad_czk_kwh: float,
current_soc_wh: float,
min_soc_wh: float,
@@ -1645,27 +1673,21 @@ def _evening_battery_export_push_indices(
evening_start_hour: int = 17,
) -> list[int]:
"""
Noční push: plný ge_bat v tolika nejdražších peak-band slotech, kolik unese Wh rozpočet.
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.
Noční push: plný ge_bat v tolika nejdražších peak slotech (shodná max sell v úseku),
kolik unese Wh rozpočet. Řazení sell desc; přidávat sloty dokud kumulované Wh ≤ push_budget.
per_slot_discharge_wh: volající předá min(BMS, export cap) × účinnost × 0,25 h.
"""
_ = 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 t in peak_ts
and t in profitable_export_ts
and float(s.sell_price) >= 0.0
for t in _evening_push_peak_candidates(slots)
if _slot_evening_push_profitable(
slots[t],
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
min_spread=degrad_czk_kwh,
)
]
if not candidates:
return []
@@ -1740,22 +1762,23 @@ def _evening_push_hysteresis_active(
def _evening_early_export_penalty_indices(
slots: list[PlanningSlot],
*,
profitable_export_ts: set[int],
discharge_export_slots: set[int],
evening_push_ts: set[int],
exempt_ts: set[int] | None = None,
) -> set[int]:
"""ge_bat=0 pro profitable noční sloty pod peakeps mimo evening_push (v38: i po prvním push)."""
"""
ge_bat=0 v nočním okně mimo tvrdý evening_push (a mimo pre-neg / neg-evening větve).
"""
exempt = exempt_ts or set()
out: set[int] = set()
for t_ev, s_ev in enumerate(slots):
if not _in_night_battery_export_window(s_ev):
continue
if t_ev not in profitable_export_ts or t_ev not in discharge_export_slots:
if t_ev not in discharge_export_slots:
continue
if t_ev in evening_push_ts:
if t_ev in evening_push_ts or t_ev in exempt:
continue
peak_sell = _night_peak_sell_czk_kwh(slots, t_ev)
if float(s_ev.sell_price) < peak_sell - EVENING_PEAK_SELL_EPS_CZK_KWH:
out.add(t_ev)
out.add(t_ev)
return out
@@ -2392,7 +2415,7 @@ def solve_dispatch(
computed_evening_push_ts = set(
_evening_battery_export_push_indices(
slots,
profitable_export_ts=profitable_export_ts_pre,
charge_acquisition_czk_kwh=charge_acquisition_czk_kwh,
degrad_czk_kwh=float(degradation_cost_effective),
current_soc_wh=float(current_soc_wh),
min_soc_wh=float(min_soc_wh),
@@ -2406,12 +2429,6 @@ def solve_dispatch(
evening_push_hysteresis_retained = True
else:
evening_push_ts = computed_evening_push_ts
evening_early_export_penalty_ts = _evening_early_export_penalty_indices(
slots,
profitable_export_ts=profitable_export_ts_pre,
discharge_export_slots=discharge_export_slots,
evening_push_ts=evening_push_ts,
)
last_pos_sell_pre_neg_buy = _last_non_negative_sell_before_neg_buy(
slots, first_neg_buy_idx
)
@@ -2421,6 +2438,19 @@ def solve_dispatch(
pre_neg_buy_empty_ts = _pre_neg_buy_empty_discharge_indices(
slots, first_neg_buy_idx, last_pos_sell_pre_neg_buy
)
if om == "AUTO":
evening_export_exempt_ts = (
set(morning_pre_neg_export_ts)
| set(pre_neg_buy_discharge_ts)
| set(pre_neg_buy_empty_ts)
| set(neg_evening_push_ts)
)
evening_early_export_penalty_ts = _evening_early_export_penalty_indices(
slots,
discharge_export_slots=discharge_export_slots,
evening_push_ts=evening_push_ts,
exempt_ts=evening_export_exempt_ts,
)
pre_neg_buy_soc_ceiling_wh = _pre_neg_buy_soc_ceiling_wh(
slots,
first_neg_buy_idx=first_neg_buy_idx,
@@ -2530,6 +2560,9 @@ def solve_dispatch(
continue
if t in evening_push_ts:
continue
if _in_night_battery_export_window(slots[t]):
# Večerní export jen v tvrdém push; jinak by shortfall rozplizňoval ge_bat.
continue
if _battery_export_push_defer_to_pv(slots[t]):
continue
if not _slot_profitable_battery_export(