dalsi rozvolneni at vic jedeme arbitraz
Some checks failed
CI and deploy / migration-check (push) Failing after 13s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-21 14:54:46 +02:00
parent 3b4d54dcc7
commit 66834ddfa6
3 changed files with 51 additions and 17 deletions

View File

@@ -14,7 +14,7 @@ Discharge-export mask:
from __future__ import annotations
import unittest
from datetime import datetime, timezone, timedelta
from datetime import date, datetime, timezone, timedelta
from types import SimpleNamespace
from zoneinfo import ZoneInfo
@@ -27,6 +27,24 @@ _BUY_CHARGE_BAND = 0.40
_MAX_GRID_CHARGE_CAP = 24
def _prague_date(s: PlanningSlot) -> date:
return s.interval_start.astimezone(_PRAGUE).date()
def _export_window_start(slots: list[PlanningSlot], degrad: float) -> datetime | None:
"""Kopie R__063: sell > min(buy) téhož kalendářního dne (Prague) + degrad."""
result: datetime | None = None
for s in slots:
day = _prague_date(s)
day_min = min(
float(x.buy_price) for x in slots if _prague_date(x) == day
)
if float(s.sell_price) > day_min + degrad:
if result is None or s.interval_start < result:
result = s.interval_start
return result
def _future_sell(slots: list[PlanningSlot], t: int) -> float:
tail = [float(slots[i].sell_price) for i in range(t + 1, len(slots))]
return max(tail) if tail else float(slots[t].sell_price)
@@ -82,11 +100,8 @@ def _select_charge_slots(
default=min(float(s.buy_price) for s in slots),
)
ref_buy_global = min(float(s.buy_price) for s in slots)
export_window_start: datetime | None = None
for s in slots:
if float(s.sell_price) > ref_buy_global + degrad:
if export_window_start is None or s.interval_start < export_window_start:
export_window_start = s.interval_start
export_window_start = _export_window_start(slots, degrad)
plan_day = _prague_date(slots[0])
eta = float(getattr(battery, "charge_efficiency", 1.0) or 1.0)
max_p_w = float(getattr(battery, "max_charge_power_w", 0.0) or 0.0)
@@ -135,13 +150,15 @@ def _select_charge_slots(
return False
return True
def _grid_sort_key(t: int, pred: bool, price: float) -> tuple[int, int, float, int]:
before_export = 0
if export_window_start is not None and slots[t].interval_start < export_window_start:
before_export = 0
else:
before_export = 1
return (before_export, int(pred), price, t)
def _grid_sort_key(t: int, pred: bool, price: float) -> tuple[int, int, int, float, int]:
today_first = 0 if _prague_date(slots[t]) == plan_day else 1
before_export = (
0
if export_window_start is not None
and slots[t].interval_start < export_window_start
else 1
)
return (today_first, before_export, int(pred), price, t)
if purchase_pricing_mode != "fixed":
am_candidates = [

View File

@@ -83,7 +83,9 @@ declare
v_est_grid_cost numeric;
v_est_pv_cost numeric;
v_export_window_start timestamptz;
v_plan_day_prague date;
begin
v_plan_day_prague := (p_from at time zone 'Europe/Prague')::date;
drop table if exists _ems_plan_slot_wk;
create temp table _ems_plan_slot_wk on commit drop as
with
@@ -289,12 +291,17 @@ begin
from _ems_plan_slot_wk wk
where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12;
-- První výkupní okno v horizontu (stejná logika jako discharge maska) — grid nabíje
-- před tím má prioritu (dnes PM levně → dnes večer prodáš), ne nejlevnější slot zítra.
-- První výkupní okno: sell nad min(buy) **téhož kalendářního dne** (Prague), ne globál
-- min(buy) zítra (NT) — jinak okno začne už ~15:30 a dnešní PM grid dostane 1 slot.
select min(wk.interval_start)
into v_export_window_start
from _ems_plan_slot_wk wk
where wk.sell_price > v_ref_buy_czk_kwh + v_degrad_czk_kwh;
where wk.sell_price > (
select coalesce(min(w2.buy_price), wk.buy_price) + v_degrad_czk_kwh
from _ems_plan_slot_wk w2
where (w2.interval_start at time zone 'Europe/Prague')::date
= (wk.interval_start at time zone 'Europe/Prague')::date
);
-- Lookahead min buy (VT→NT) a store_score pro vrstvu A.
alter table _ems_plan_slot_wk
@@ -399,6 +406,11 @@ begin
or wk.buy_price <= wk.buy_min_next_n + v_buy_lookahead_eps
)
order by
case
when (wk.interval_start at time zone 'Europe/Prague')::date = v_plan_day_prague
then 0
else 1
end,
case
when v_export_window_start is not null
and wk.interval_start < v_export_window_start
@@ -433,6 +445,11 @@ begin
or wk.buy_price <= wk.buy_min_next_n + v_buy_lookahead_eps
)
order by
case
when (wk.interval_start at time zone 'Europe/Prague')::date = v_plan_day_prague
then 0
else 1
end,
case
when v_export_window_start is not null
and wk.interval_start < v_export_window_start

View File

@@ -11,7 +11,7 @@
- **Terminal SoC shadow price:** v objective je člen `(avg_buy_prvních_24h × planner_terminal_soc_value_factor / 1000) × soc[T1]` (Kč), kde faktor je **`ems.asset_battery.planner_terminal_soc_value_factor`** přes **`ems.fn_planning_site_context`** (default v DB **0.9**); viz sekci *Tuning pro malé baterie* níže. Účel: konec horizontu nemusí končit zbytečně vyprázdněnou baterií (receding horizon).
- **Masky `allow_charge` / `allow_discharge_export` (tenký anti-mikrocyklus):** generuje `ems.fn_load_planning_slots_full` (`R__063`). Ekonomiku primárně řídí LP podle efektivních cen; masky jen omezují počet slotů pro grid nabíjení / export baterie.
- **PV-surplus (vrstva A):** ranking dle **`store_score DESC`** = `future_sell_opportunity sell max(0, buysell)`; jen sloty s `sell ≥ buy degradation`. Kumulativní PV pokrývá `grid_target` (deficit SoC, nad `reserve_soc` bez násobení `charge_slot_buffer`). Zbytek → `allow_charge=false` (PV jen do sítě / `bc ≤ pv_surplus` v LP).
- **Grid ze sítě (vrstva B, před FVE):** spot, **AM/PM rozpočet Wh 50/50** z `grid_target`. Výběr slotů: nejdřív sloty **před prvním výkupním oknem** (`sell > min(buy)+degrad`), pak teprve zbytek horizontu — aby dnes PM nevyhrál zítra `min(buy)`. Lookahead VT→NT jen mezi sloty před tímto oknem. **`charge_acquisition`:** vážený `buy` jen u `allow_grid_charge` (ne FVE vrstva A).
- **Grid ze sítě (vrstva B, před FVE):** spot, **AM/PM rozpočet Wh 50/50** z `grid_target`. Výběr: nejdřív **kalendářní den plánu** (`p_from` Prague), pak sloty před **výkupním oknem daného dne** (`sell > min(buy téhož dne)+degrad` — ne globální min zítra). Lookahead VT→NT jen před tímto oknem. **`charge_acquisition`:** vážený `buy` jen u `allow_grid_charge` (ne FVE vrstva A).
- **`ref_buy_min` (brána exportu):** `min(buy_price)` horizontu — jen „existuje levný nákup?“, **ne** průměrná cena nabití přes hodiny. Export sloty: `sell > ref_buy_min + degradation` (spot). Viz [`planning-arbitrage-accounting.md`](planning-arbitrage-accounting.md).
- Pokud `energy_to_fill <= 0` nebo `charge_slot_buffer = 0`: všechny sloty povoleny.
- **LP ekonomické guardy** (`solve_dispatch`, AUTO): pokud `sell < buy degradation``ge_pv=0` (výjimka: plná baterie, přebytek **pv_b**). Pokud `buy > min(buy)+degradation``gi` jen na load+EV+TČ. Viz `planning_engine.py` sekce po slot pre-selection.