dalsi rozvolneni at vic jedeme arbitraz
This commit is contained in:
@@ -14,7 +14,7 @@ Discharge-export mask:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import date, datetime, timezone, timedelta
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
@@ -27,6 +27,24 @@ _BUY_CHARGE_BAND = 0.40
|
|||||||
_MAX_GRID_CHARGE_CAP = 24
|
_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:
|
def _future_sell(slots: list[PlanningSlot], t: int) -> float:
|
||||||
tail = [float(slots[i].sell_price) for i in range(t + 1, len(slots))]
|
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)
|
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),
|
default=min(float(s.buy_price) for s in slots),
|
||||||
)
|
)
|
||||||
ref_buy_global = 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
|
export_window_start = _export_window_start(slots, degrad)
|
||||||
for s in slots:
|
plan_day = _prague_date(slots[0])
|
||||||
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
|
|
||||||
|
|
||||||
eta = float(getattr(battery, "charge_efficiency", 1.0) or 1.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)
|
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 False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _grid_sort_key(t: int, pred: bool, price: float) -> tuple[int, int, float, int]:
|
def _grid_sort_key(t: int, pred: bool, price: float) -> tuple[int, int, int, float, int]:
|
||||||
before_export = 0
|
today_first = 0 if _prague_date(slots[t]) == plan_day else 1
|
||||||
if export_window_start is not None and slots[t].interval_start < export_window_start:
|
before_export = (
|
||||||
before_export = 0
|
0
|
||||||
else:
|
if export_window_start is not None
|
||||||
before_export = 1
|
and slots[t].interval_start < export_window_start
|
||||||
return (before_export, int(pred), price, t)
|
else 1
|
||||||
|
)
|
||||||
|
return (today_first, before_export, int(pred), price, t)
|
||||||
|
|
||||||
if purchase_pricing_mode != "fixed":
|
if purchase_pricing_mode != "fixed":
|
||||||
am_candidates = [
|
am_candidates = [
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ declare
|
|||||||
v_est_grid_cost numeric;
|
v_est_grid_cost numeric;
|
||||||
v_est_pv_cost numeric;
|
v_est_pv_cost numeric;
|
||||||
v_export_window_start timestamptz;
|
v_export_window_start timestamptz;
|
||||||
|
v_plan_day_prague date;
|
||||||
begin
|
begin
|
||||||
|
v_plan_day_prague := (p_from at time zone 'Europe/Prague')::date;
|
||||||
drop table if exists _ems_plan_slot_wk;
|
drop table if exists _ems_plan_slot_wk;
|
||||||
create temp table _ems_plan_slot_wk on commit drop as
|
create temp table _ems_plan_slot_wk on commit drop as
|
||||||
with
|
with
|
||||||
@@ -289,12 +291,17 @@ begin
|
|||||||
from _ems_plan_slot_wk wk
|
from _ems_plan_slot_wk wk
|
||||||
where extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12;
|
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íjení
|
-- První výkupní okno: sell nad min(buy) **téhož kalendářního dne** (Prague), ne globální
|
||||||
-- před tím má prioritu (dnes PM levně → dnes večer prodáš), ne nejlevnější slot zítra.
|
-- 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)
|
select min(wk.interval_start)
|
||||||
into v_export_window_start
|
into v_export_window_start
|
||||||
from _ems_plan_slot_wk wk
|
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.
|
-- Lookahead min buy (VT→NT) a store_score pro vrstvu A.
|
||||||
alter table _ems_plan_slot_wk
|
alter table _ems_plan_slot_wk
|
||||||
@@ -399,6 +406,11 @@ begin
|
|||||||
or wk.buy_price <= wk.buy_min_next_n + v_buy_lookahead_eps
|
or wk.buy_price <= wk.buy_min_next_n + v_buy_lookahead_eps
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
|
case
|
||||||
|
when (wk.interval_start at time zone 'Europe/Prague')::date = v_plan_day_prague
|
||||||
|
then 0
|
||||||
|
else 1
|
||||||
|
end,
|
||||||
case
|
case
|
||||||
when v_export_window_start is not null
|
when v_export_window_start is not null
|
||||||
and wk.interval_start < v_export_window_start
|
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
|
or wk.buy_price <= wk.buy_min_next_n + v_buy_lookahead_eps
|
||||||
)
|
)
|
||||||
order by
|
order by
|
||||||
|
case
|
||||||
|
when (wk.interval_start at time zone 'Europe/Prague')::date = v_plan_day_prague
|
||||||
|
then 0
|
||||||
|
else 1
|
||||||
|
end,
|
||||||
case
|
case
|
||||||
when v_export_window_start is not null
|
when v_export_window_start is not null
|
||||||
and wk.interval_start < v_export_window_start
|
and wk.interval_start < v_export_window_start
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
- **Terminal SoC shadow price:** v objective je člen `−(avg_buy_prvních_24h × planner_terminal_soc_value_factor / 1000) × soc[T−1]` (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).
|
- **Terminal SoC shadow price:** v objective je člen `−(avg_buy_prvních_24h × planner_terminal_soc_value_factor / 1000) × soc[T−1]` (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.
|
- **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, buy−sell)`; 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).
|
- **PV-surplus (vrstva A):** ranking dle **`store_score DESC`** = `future_sell_opportunity − sell − max(0, buy−sell)`; 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).
|
- **`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.
|
- 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.
|
- **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.
|
||||||
|
|||||||
Reference in New Issue
Block a user