prepsani s opusem dle planu
This commit is contained in:
25
db/migration/V081__planning_interval_economics.sql
Normal file
25
db/migration/V081__planning_interval_economics.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- Rozsireni ekonomickeho rozpadu planu (audit transparence: cashflow vs arbitraz vs penalizace vs bonus).
|
||||
-- Drive byl v planning_interval jen expected_cost_czk = gi*buy - ge*sell (bez penalizaci a bez acquisition).
|
||||
|
||||
alter table ems.planning_interval
|
||||
add column if not exists cashflow_czk numeric,
|
||||
add column if not exists battery_arbitrage_czk numeric,
|
||||
add column if not exists penalty_czk numeric,
|
||||
add column if not exists green_bonus_czk numeric;
|
||||
|
||||
comment on column ems.planning_interval.cashflow_czk is
|
||||
'Net penezni tok ze site v slotu: gi*buy_price*h - ge*sell_price*h (Kc). '
|
||||
'Kladne = platba EMS, zaporne = prijem. Shodne s expected_cost_czk (ponechano jako legacy).';
|
||||
|
||||
comment on column ems.planning_interval.battery_arbitrage_czk is
|
||||
'Marze z exportu baterie do site: ge_bat * (sell_price - acquisition_used) * h (Kc). '
|
||||
'Kladne = zisk arbitraze (cena prodeje > vazeny nakup zasoby).';
|
||||
|
||||
comment on column ems.planning_interval.penalty_czk is
|
||||
'Soucet penalizaci v slotu (Kc): shortfall (peak_export, pv_charge, neg_sell_dump) + safety_deficit '
|
||||
'+ curtailment + commitment. Neviditelne v cashflow_czk, ale solver je optimalizuje.';
|
||||
|
||||
comment on column ems.planning_interval.green_bonus_czk is
|
||||
'Planovany zeleny bonus z vyroby poli s active green_bonus_czk_kwh (Kc). '
|
||||
'pv_*_forecast_solver_w * green_bonus_czk_kwh * h, scitano pres vsechna pole se zelenym bonusem '
|
||||
'platnym v slotu (ems.asset_pv_array.green_bonus_*).';
|
||||
@@ -24,6 +24,7 @@ DECLARE
|
||||
v_ev JSONB;
|
||||
v_fc JSONB;
|
||||
v_ov JSONB;
|
||||
v_econ JSONB;
|
||||
BEGIN
|
||||
IF p_site_id IS NULL THEN
|
||||
RETURN jsonb_build_object('error', 'site_id_required');
|
||||
@@ -89,6 +90,49 @@ BEGIN
|
||||
AND pi.interval_start < v_win_end
|
||||
) t;
|
||||
|
||||
select jsonb_build_object(
|
||||
'window_start_utc', v_slot,
|
||||
'window_end_utc', v_win_end,
|
||||
'total_import_kwh', coalesce(sum(
|
||||
case when pi.grid_setpoint_w > 0
|
||||
then pi.grid_setpoint_w * 0.25 / 1000.0 else 0 end
|
||||
), 0),
|
||||
'total_export_kwh', coalesce(sum(
|
||||
case when pi.grid_setpoint_w < 0
|
||||
then -pi.grid_setpoint_w * 0.25 / 1000.0 else 0 end
|
||||
), 0),
|
||||
'total_buy_cost_czk', coalesce(sum(
|
||||
case when pi.grid_setpoint_w > 0
|
||||
then pi.grid_setpoint_w * pi.effective_buy_price * 0.25 / 1000.0
|
||||
else 0 end
|
||||
), 0),
|
||||
'total_sell_revenue_czk', coalesce(sum(
|
||||
case when pi.grid_setpoint_w < 0
|
||||
then -pi.grid_setpoint_w * pi.effective_sell_price * 0.25 / 1000.0
|
||||
else 0 end
|
||||
), 0),
|
||||
'total_cashflow_czk', coalesce(sum(pi.cashflow_czk), 0),
|
||||
'total_battery_arbitrage_czk', coalesce(sum(pi.battery_arbitrage_czk), 0),
|
||||
'total_penalty_czk', coalesce(sum(pi.penalty_czk), 0),
|
||||
'total_green_bonus_czk', coalesce(sum(pi.green_bonus_czk), 0),
|
||||
'net_economic_czk',
|
||||
coalesce(-sum(pi.cashflow_czk), 0)
|
||||
+ coalesce(sum(pi.battery_arbitrage_czk), 0)
|
||||
- coalesce(sum(pi.penalty_czk), 0)
|
||||
+ coalesce(sum(pi.green_bonus_czk), 0),
|
||||
'neg_sell_export_slots', count(*) filter (
|
||||
where pi.effective_sell_price < 0 and pi.grid_setpoint_w < -500
|
||||
),
|
||||
'first_grid_charge_slot_utc', min(pi.interval_start) filter (
|
||||
where pi.grid_setpoint_w > 500
|
||||
)
|
||||
)
|
||||
into v_econ
|
||||
from ems.planning_interval pi
|
||||
where pi.run_id = v_run.id
|
||||
and pi.interval_start >= v_slot
|
||||
and pi.interval_start < v_win_end;
|
||||
|
||||
SELECT to_jsonb(m.*) || jsonb_build_object('mode_name', d.name)
|
||||
INTO v_mode
|
||||
FROM ems.site_operating_mode m
|
||||
@@ -170,6 +214,7 @@ BEGIN
|
||||
'ev_sessions_open', v_ev,
|
||||
'forecast_correction_log_recent', v_fc,
|
||||
'site_overrides_active_in_window', v_ov,
|
||||
'economics_summary', v_econ,
|
||||
'ai_readme', jsonb_build_object(
|
||||
'purpose',
|
||||
'Data stačí k vysvětlení „proč plán v dalších hodinách vypadá takto“: ceny v řádcích intervalů, vstupy (baseline, PV), výstupy (bat/grid/EV/TČ), režim a síťové limity.',
|
||||
|
||||
@@ -68,7 +68,11 @@ begin
|
||||
is_predicted_price,
|
||||
load_baseline_w,
|
||||
pv_a_forecast_raw_w, pv_b_forecast_raw_w,
|
||||
pv_a_forecast_solver_w, pv_b_forecast_solver_w
|
||||
pv_a_forecast_solver_w, pv_b_forecast_solver_w,
|
||||
cashflow_czk,
|
||||
battery_arbitrage_czk,
|
||||
penalty_czk,
|
||||
green_bonus_czk
|
||||
) values (
|
||||
v_run_id,
|
||||
(r.value->>'interval_start')::timestamptz,
|
||||
@@ -94,7 +98,11 @@ begin
|
||||
(r.value->>'pv_a_forecast_raw_w')::int,
|
||||
(r.value->>'pv_b_forecast_raw_w')::int,
|
||||
(r.value->>'pv_a_forecast_solver_w')::int,
|
||||
(r.value->>'pv_b_forecast_solver_w')::int
|
||||
(r.value->>'pv_b_forecast_solver_w')::int,
|
||||
nullif(r.value->>'cashflow_czk', '')::numeric,
|
||||
nullif(r.value->>'battery_arbitrage_czk', '')::numeric,
|
||||
nullif(r.value->>'penalty_czk', '')::numeric,
|
||||
nullif(r.value->>'green_bonus_czk', '')::numeric
|
||||
);
|
||||
else
|
||||
insert into ems.planning_interval (
|
||||
@@ -109,7 +117,11 @@ begin
|
||||
heat_pump_enabled, heat_pump_setpoint_w,
|
||||
pv_a_curtailed_w, expected_cost_czk,
|
||||
effective_buy_price, effective_sell_price,
|
||||
is_predicted_price
|
||||
is_predicted_price,
|
||||
cashflow_czk,
|
||||
battery_arbitrage_czk,
|
||||
penalty_czk,
|
||||
green_bonus_czk
|
||||
) values (
|
||||
v_run_id,
|
||||
(r.value->>'interval_start')::timestamptz,
|
||||
@@ -130,7 +142,11 @@ begin
|
||||
(r.value->>'expected_cost_czk')::numeric,
|
||||
(r.value->>'effective_buy_price')::numeric,
|
||||
(r.value->>'effective_sell_price')::numeric,
|
||||
coalesce((r.value->>'is_predicted_price')::boolean, false)
|
||||
coalesce((r.value->>'is_predicted_price')::boolean, false),
|
||||
nullif(r.value->>'cashflow_czk', '')::numeric,
|
||||
nullif(r.value->>'battery_arbitrage_czk', '')::numeric,
|
||||
nullif(r.value->>'penalty_czk', '')::numeric,
|
||||
nullif(r.value->>'green_bonus_czk', '')::numeric
|
||||
);
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
@@ -32,7 +32,11 @@ returns table (
|
||||
future_sell_opportunity_czk_kwh numeric,
|
||||
is_daytime_pv_surplus_slot boolean,
|
||||
charge_acquisition_buy_czk_kwh numeric,
|
||||
charge_acquisition_cutoff_at timestamptz
|
||||
charge_acquisition_cutoff_at timestamptz,
|
||||
min_buy_before_cutoff_czk_kwh numeric,
|
||||
pv_charge_wh_ahead numeric,
|
||||
neg_buy_wh_ahead numeric,
|
||||
grid_charge_suppressed_reason text
|
||||
)
|
||||
language plpgsql
|
||||
volatile
|
||||
@@ -92,6 +96,14 @@ declare
|
||||
v_est_pv_cost numeric;
|
||||
v_export_window_start timestamptz;
|
||||
v_plan_day_prague date;
|
||||
v_acq_v2 numeric;
|
||||
v_acq_prev numeric := -999;
|
||||
v_iter int;
|
||||
v_affected int;
|
||||
v_cum_allowed numeric;
|
||||
v_pv_ahead_total numeric;
|
||||
v_target_deficit numeric;
|
||||
r_unlock record;
|
||||
begin
|
||||
v_plan_day_prague := (p_from at time zone 'Europe/Prague')::date;
|
||||
drop table if exists _ems_plan_slot_wk;
|
||||
@@ -310,7 +322,11 @@ begin
|
||||
add column if not exists buy_min_next_n numeric,
|
||||
add column if not exists store_score numeric,
|
||||
add column if not exists allow_grid_charge boolean default false,
|
||||
add column if not exists export_window_start_at timestamptz;
|
||||
add column if not exists export_window_start_at timestamptz,
|
||||
add column if not exists min_buy_before_cutoff numeric,
|
||||
add column if not exists pv_charge_wh_ahead numeric,
|
||||
add column if not exists neg_buy_wh_ahead numeric,
|
||||
add column if not exists grid_charge_suppressed_reason text;
|
||||
|
||||
-- První výkupní okno **per kalendářní den** (Prague). Globální min přes dny by
|
||||
-- zablokoval NT grid nabíjení (včerejší večerní peak → dnešní 00–06 už „po okně“).
|
||||
@@ -512,6 +528,127 @@ begin
|
||||
update _ems_plan_slot_wk wk
|
||||
set allow_charge = true, allow_grid_charge = true
|
||||
where wk.buy_price < 0;
|
||||
|
||||
-- Self-konzistentni filtr vrstvy B (spot): vyloucit drahe grid sloty, pokud PV / buy<0
|
||||
-- alternativa pokryje deficit SoC pred prvnim exportem.
|
||||
update _ems_plan_slot_wk wk
|
||||
set pv_charge_wh_ahead = sub.pv_wh_ahead,
|
||||
neg_buy_wh_ahead = sub.neg_buy_wh_ahead,
|
||||
min_buy_before_cutoff = sub.min_buy_ahead
|
||||
from (
|
||||
select
|
||||
wk.slot_ord,
|
||||
least(
|
||||
coalesce(sum(
|
||||
case
|
||||
when w2.slot_ord >= wk.slot_ord
|
||||
and (v_first_neg_sell_ord is null or w2.slot_ord < v_first_neg_sell_ord)
|
||||
and w2.pv_surplus_w > 0
|
||||
and (w2.sell_price < 0 or w2.buy_price < 0)
|
||||
then least(w2.pv_surplus_w::numeric, v_max_charge_w)
|
||||
* v_charge_eff * 0.25
|
||||
else 0
|
||||
end
|
||||
), 0),
|
||||
v_soc_max_wh - p_current_soc_wh
|
||||
) as pv_wh_ahead,
|
||||
coalesce(sum(
|
||||
case
|
||||
when w2.slot_ord >= wk.slot_ord
|
||||
and (v_first_neg_sell_ord is null or w2.slot_ord < v_first_neg_sell_ord)
|
||||
and w2.buy_price < 0
|
||||
then v_per_slot_charge_wh
|
||||
else 0
|
||||
end
|
||||
), 0) as neg_buy_wh_ahead,
|
||||
min(
|
||||
case
|
||||
when w2.slot_ord > wk.slot_ord
|
||||
and (v_first_neg_sell_ord is null or w2.slot_ord < v_first_neg_sell_ord)
|
||||
then w2.buy_price
|
||||
else null
|
||||
end
|
||||
) as min_buy_ahead
|
||||
from _ems_plan_slot_wk wk
|
||||
cross join _ems_plan_slot_wk w2
|
||||
group by wk.slot_ord
|
||||
) sub
|
||||
where wk.slot_ord = sub.slot_ord;
|
||||
|
||||
v_iter := 0;
|
||||
loop
|
||||
v_iter := v_iter + 1;
|
||||
exit when v_iter > 5;
|
||||
|
||||
select coalesce(
|
||||
sum(wk.buy_price * v_per_slot_charge_wh)
|
||||
filter (
|
||||
where wk.allow_grid_charge
|
||||
and (v_first_neg_sell_ord is null or wk.slot_ord < v_first_neg_sell_ord)
|
||||
)
|
||||
/ nullif(sum(v_per_slot_charge_wh)
|
||||
filter (
|
||||
where wk.allow_grid_charge
|
||||
and (v_first_neg_sell_ord is null or wk.slot_ord < v_first_neg_sell_ord)
|
||||
), 0),
|
||||
v_ref_buy_czk_kwh
|
||||
)
|
||||
into v_acq_v2
|
||||
from _ems_plan_slot_wk wk;
|
||||
|
||||
exit when abs(v_acq_v2 - v_acq_prev) < 0.05;
|
||||
v_acq_prev := v_acq_v2;
|
||||
|
||||
update _ems_plan_slot_wk wk
|
||||
set allow_charge = false,
|
||||
allow_grid_charge = false,
|
||||
grid_charge_suppressed_reason =
|
||||
case
|
||||
when wk.pv_charge_wh_ahead + wk.neg_buy_wh_ahead
|
||||
>= greatest(0, v_soc_max_wh - p_current_soc_wh) * 0.6
|
||||
then 'cheaper_pv_ahead'
|
||||
else 'cheaper_neg_buy_ahead'
|
||||
end
|
||||
where wk.allow_grid_charge
|
||||
and wk.buy_price > v_acq_v2 - v_degrad_czk_kwh
|
||||
and wk.buy_price >= 0
|
||||
and (
|
||||
wk.pv_charge_wh_ahead + wk.neg_buy_wh_ahead
|
||||
>= greatest(0, v_soc_max_wh - p_current_soc_wh) * 0.6
|
||||
);
|
||||
|
||||
get diagnostics v_affected = row_count;
|
||||
exit when v_affected = 0;
|
||||
end loop;
|
||||
|
||||
select coalesce(sum(v_per_slot_charge_wh) filter (where wk.allow_grid_charge), 0)
|
||||
into v_cum_allowed
|
||||
from _ems_plan_slot_wk wk;
|
||||
|
||||
select coalesce(min(wk.pv_charge_wh_ahead), 0)
|
||||
into v_pv_ahead_total
|
||||
from _ems_plan_slot_wk wk
|
||||
where wk.slot_ord = 0;
|
||||
|
||||
v_target_deficit := greatest(0, v_soc_max_wh - p_current_soc_wh) - v_pv_ahead_total;
|
||||
|
||||
if v_cum_allowed < v_target_deficit * 0.6 then
|
||||
for r_unlock in
|
||||
select wk.slot_ord
|
||||
from _ems_plan_slot_wk wk
|
||||
where wk.grid_charge_suppressed_reason is not null
|
||||
and wk.buy_price < 2 * v_acq_v2
|
||||
order by wk.buy_price, wk.slot_ord
|
||||
loop
|
||||
update _ems_plan_slot_wk wk
|
||||
set allow_charge = true,
|
||||
allow_grid_charge = true,
|
||||
grid_charge_suppressed_reason = 'safety_failsafe_unlock'
|
||||
where wk.slot_ord = r_unlock.slot_ord;
|
||||
v_cum_allowed := v_cum_allowed + v_per_slot_charge_wh;
|
||||
exit when v_cum_allowed >= v_target_deficit * 0.6;
|
||||
end loop;
|
||||
end if;
|
||||
elsif exists (
|
||||
select 1
|
||||
from _ems_plan_slot_wk w2
|
||||
@@ -925,7 +1062,11 @@ begin
|
||||
(
|
||||
extract(hour from w.interval_start at time zone 'Europe/Prague') between 6 and 18
|
||||
and w.pv_surplus_w > 0
|
||||
) as is_daytime_pv_surplus_slot
|
||||
) as is_daytime_pv_surplus_slot,
|
||||
w.min_buy_before_cutoff as min_buy_before_cutoff_czk_kwh,
|
||||
coalesce(w.pv_charge_wh_ahead, 0) as pv_charge_wh_ahead,
|
||||
coalesce(w.neg_buy_wh_ahead, 0) as neg_buy_wh_ahead,
|
||||
w.grid_charge_suppressed_reason
|
||||
from _ems_plan_slot_wk w
|
||||
cross join night_tot nt
|
||||
)
|
||||
@@ -949,7 +1090,11 @@ begin
|
||||
e.future_sell_opportunity_czk_kwh,
|
||||
e.is_daytime_pv_surplus_slot,
|
||||
v_charge_acquisition as charge_acquisition_buy_czk_kwh,
|
||||
v_acquisition_cutoff as charge_acquisition_cutoff_at
|
||||
v_acquisition_cutoff as charge_acquisition_cutoff_at,
|
||||
e.min_buy_before_cutoff_czk_kwh,
|
||||
e.pv_charge_wh_ahead,
|
||||
e.neg_buy_wh_ahead,
|
||||
e.grid_charge_suppressed_reason
|
||||
from enriched e
|
||||
order by e.slot_ord;
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user