zasadni uprava LP planneru
Some checks failed
CI and deploy / migration-check (push) Failing after 24s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-21 11:18:09 +02:00
parent d984716f69
commit 08f1b6741a
7 changed files with 330 additions and 123 deletions

View File

@@ -63,6 +63,12 @@ declare
v_degrad_czk_kwh numeric;
v_ref_buy_czk_kwh numeric;
v_purchase_pricing_mode text;
v_lookahead_slots int := 4;
v_grid_charge_cap_am int := 6;
v_grid_charge_cap_pm int := 6;
v_buy_lookahead_eps numeric := 0.05;
v_grid_slots_am int := 0;
v_grid_slots_pm int := 0;
begin
drop table if exists _ems_plan_slot_wk;
create temp table _ems_plan_slot_wk on commit drop as
@@ -243,9 +249,56 @@ begin
v_per_slot_discharge_wh := v_max_discharge_w * v_discharge_eff * 0.25;
v_energy_to_fill := v_soc_max_wh - p_current_soc_wh;
v_exportable := v_soc_max_wh - v_min_soc_wh;
v_grid_target_wh := greatest(v_energy_to_fill, 0) * v_charge_buf;
-- Rozpočet masek: buffer neinfluje počet slotů nad skutečný deficit; nad reserve jen deficit.
if p_current_soc_wh >= v_reserve_wh then
v_grid_target_wh := greatest(v_energy_to_fill, 0);
else
v_grid_target_wh := least(
greatest(v_energy_to_fill, 0) * v_charge_buf,
greatest(v_energy_to_fill, 0)
);
end if;
v_discharge_target_wh := v_exportable * v_discharge_buf;
-- Referenční nákup pro arbitráž (celý horizont, ne jen allow_charge).
select coalesce(min(wk.buy_price), 0)
into v_ref_buy_czk_kwh
from _ems_plan_slot_wk wk;
-- Lookahead min buy (VT→NT) a store_score pro vrstvu A.
alter table _ems_plan_slot_wk
add column if not exists future_sell_lookahead numeric,
add column if not exists buy_min_next_n numeric,
add column if not exists store_score numeric;
update _ems_plan_slot_wk wk
set
future_sell_lookahead = coalesce(
(
select max(w2.sell_price)
from _ems_plan_slot_wk w2
where w2.slot_ord > wk.slot_ord
),
wk.sell_price
),
buy_min_next_n = (
select min(w2.buy_price)
from _ems_plan_slot_wk w2
where w2.slot_ord > wk.slot_ord
and w2.slot_ord <= wk.slot_ord + v_lookahead_slots
),
store_score =
coalesce(
(
select max(w2.sell_price)
from _ems_plan_slot_wk w2
where w2.slot_ord > wk.slot_ord
),
wk.sell_price
)
- wk.sell_price
- greatest(0::numeric, wk.buy_price - wk.sell_price);
-- AM/PM rozpočet grid charging (Europe/Prague 0012 vs 1224).
-- Chybějící segment dostane celý budget.
select
@@ -269,18 +322,14 @@ begin
v_chg_pm_wh := v_grid_target_wh - v_chg_am_wh;
end if;
-- charge mask: dvě nezávislé vrstvy
-- charge mask: dvě nezávislé vrstvy (tenký anti-mikrocyklus, ekonomika z cen)
--
-- A) PV-surplus sloty (pv_surplus_w > 0): ranking dle sell_price ASC.
-- Nejlevnější PV-surplus sloty vybereme, dokud kumulativní
-- PV surplus nepokryje charge target (energy_to_fill × charge_buf).
-- Zbylé PV-surplus sloty mají allow_charge = false → PV jde do sítě.
-- Toto je hlavní mechanismus proti mikro-cyklování z PV:
-- v drahých slotech se PV prodává přímo, nabíjení jen v levných.
-- A) PV-surplus: ranking store_score DESC (future_sell sell max(0,buysell)).
-- Sloty s nejvyšší hodnotou uložení vs export pokrývají charge target.
-- Zbylé PV-surplus → allow_charge=false (PV jen do sítě / bc≤surplus v LP).
--
-- B) Non-PV sloty (pv_surplus_w <= 0): AM/PM budget, OTE-first (jen spot nákup).
-- U purchase_pricing_mode = fixed se grid nabíjení neplánuje — buy je
-- v každém slotu stejný, cyklus ze sítě by byl čistá ztráta; nabíjení jen z FVE.
-- B) Non-PV grid: jen spot, buy ≤ ref_buy+degrad, buy ≤ min(next N)+ε,
-- cap K slotů AM/PM; nikdy při sell < buy degrad (ztrátový slot).
if v_charge_buf <= 0 then
update _ems_plan_slot_wk wk set allow_charge = true;
elsif v_energy_to_fill <= 0 then
@@ -288,13 +337,14 @@ begin
else
update _ems_plan_slot_wk wk set allow_charge = false;
-- A) PV-surplus: cheapest sell_price first
-- A) PV-surplus: nejvyšší store_score (ukládat FVE vs exportovat)
v_cum := 0;
for r_slot in
select wk.slot_ord, wk.pv_surplus_w
from _ems_plan_slot_wk wk
where wk.pv_surplus_w > 0
order by wk.sell_price, wk.slot_ord
and wk.sell_price >= wk.buy_price - v_degrad_czk_kwh
order by wk.store_score desc nulls last, wk.slot_ord
loop
exit when v_cum >= v_grid_target_wh;
update _ems_plan_slot_wk wk set allow_charge = true where wk.slot_ord = r_slot.slot_ord;
@@ -302,49 +352,54 @@ begin
end loop;
if v_purchase_pricing_mode <> 'fixed' then
-- B) Non-PV AM: OTE-first, then predicted, ordered by buy_price
-- B) Non-PV AM: OTE-first, levný buy + lookahead, cap slotů
v_cum := 0;
v_grid_slots_am := 0;
for r_slot in
select wk.slot_ord
from _ems_plan_slot_wk wk
where wk.pv_surplus_w <= 0
and extract(hour from wk.interval_start at time zone 'Europe/Prague') < 12
and wk.buy_price <= v_ref_buy_czk_kwh + v_degrad_czk_kwh
and (
wk.buy_min_next_n is null
or wk.buy_price <= wk.buy_min_next_n + v_buy_lookahead_eps
)
order by wk.is_predicted_price::int, wk.buy_price, wk.slot_ord
loop
exit when v_cum >= v_chg_am_wh;
exit when v_per_slot_charge_wh <= 0;
exit when v_grid_slots_am >= v_grid_charge_cap_am;
update _ems_plan_slot_wk wk set allow_charge = true where wk.slot_ord = r_slot.slot_ord;
v_cum := v_cum + v_per_slot_charge_wh;
v_grid_slots_am := v_grid_slots_am + 1;
end loop;
-- B) Non-PV PM: OTE-first, then predicted, ordered by buy_price
-- B) Non-PV PM
v_cum := 0;
v_grid_slots_pm := 0;
for r_slot in
select wk.slot_ord
from _ems_plan_slot_wk wk
where wk.pv_surplus_w <= 0
and extract(hour from wk.interval_start at time zone 'Europe/Prague') >= 12
and wk.buy_price <= v_ref_buy_czk_kwh + v_degrad_czk_kwh
and (
wk.buy_min_next_n is null
or wk.buy_price <= wk.buy_min_next_n + v_buy_lookahead_eps
)
order by wk.is_predicted_price::int, wk.buy_price, wk.slot_ord
loop
exit when v_cum >= v_chg_pm_wh;
exit when v_per_slot_charge_wh <= 0;
exit when v_grid_slots_pm >= v_grid_charge_cap_pm;
update _ems_plan_slot_wk wk set allow_charge = true where wk.slot_ord = r_slot.slot_ord;
v_cum := v_cum + v_per_slot_charge_wh;
v_grid_slots_pm := v_grid_slots_pm + 1;
end loop;
end if;
end if;
-- Referenční nákup pro arbitráž exportu: nejlevnější buy mezi sloty, kde lze nabíjet
-- (ne buy ve stejném slotu — střídač nekupuje a neprodává současně).
select coalesce(
min(wk.buy_price) filter (where wk.allow_charge),
min(wk.buy_price)
)
into v_ref_buy_czk_kwh
from _ems_plan_slot_wk wk;
v_ref_buy_czk_kwh := coalesce(v_ref_buy_czk_kwh, 0);
-- discharge-export mask
if v_discharge_buf <= 0 then
update _ems_plan_slot_wk wk set allow_discharge_export = true;
@@ -464,9 +519,9 @@ $fn$;
comment on function ems.fn_load_planning_slots_full is
'15min sloty s cenami, forecastem, baseline a maskami proti mikro-cyklu (charge/discharge-export). '
'Charge mask: PV-surplus sloty rankované dle sell_price ASC nejlevnější pokrývají charge target, zbytek → PV do sítě; '
'non-PV sloty dle buy_price s AM/PM rozpočtem 50/50 a OTE-first prioritou (is_predicted_price::int ASC). '
'Discharge-export mask: nejdražší sell_price sloty globálně. '
'Charge mask A: PV-surplus dle store_score DESC (future_sellsellmax(0,buysell)); zbytek → PV export. '
'Charge mask B: non-PV jen spot, buy≤ref_buy+degrad, lookahead min buy v N slotech, cap 6 slotů AM/PM. '
'ref_buy = min(buy) horizontu. Discharge-export: nejdražší sell kde sell>ref_buy+degrad (spot). '
'Strop SoC pro výpočet energie k dobití: coalesce(planner_max_soc_percent, max_soc_percent). '
'Denní safety vstupy: night_baseload_* (20:0006:00 Europe/Prague), safety_soc_target_wh (619), '
'lookahead max buy/sell pro měkké LP penalizace.';