228 lines
7.7 KiB
PL/PgSQL
228 lines
7.7 KiB
PL/PgSQL
-- =============================================================
|
||
-- R__042_fn_predict_negative_prices.sql
|
||
-- Predikce oken se zvýšeným rizikem záporné spotové ceny (OTE).
|
||
-- Volat denně po importu cen a po forecastu FVE; výsledky ukládá do
|
||
-- ems.predicted_negative_price_window.
|
||
-- =============================================================
|
||
|
||
CREATE OR REPLACE FUNCTION ems.fn_predict_negative_price_windows(
|
||
p_site_id INT,
|
||
p_days_ahead INT DEFAULT 7
|
||
)
|
||
RETURNS TABLE (
|
||
predicted_date DATE,
|
||
window_start_hour INT,
|
||
window_end_hour INT,
|
||
probability_pct INT,
|
||
expected_min_price NUMERIC(10, 4),
|
||
reason TEXT
|
||
)
|
||
LANGUAGE plpgsql
|
||
VOLATILE
|
||
AS $$
|
||
DECLARE
|
||
v_start DATE;
|
||
v_end DATE;
|
||
v_days INT;
|
||
BEGIN
|
||
v_days := COALESCE(p_days_ahead, 7);
|
||
IF v_days < 1 OR v_days > 60 THEN
|
||
RAISE EXCEPTION 'p_days_ahead must be between 1 and 60, got %', p_days_ahead;
|
||
END IF;
|
||
|
||
v_start := (CURRENT_TIMESTAMP AT TIME ZONE 'Europe/Prague')::DATE + 1;
|
||
v_end := (CURRENT_TIMESTAMP AT TIME ZONE 'Europe/Prague')::DATE + v_days;
|
||
|
||
IF NOT EXISTS (SELECT 1 FROM ems.site s WHERE s.id = p_site_id) THEN
|
||
RAISE EXCEPTION 'site not found: %', p_site_id;
|
||
END IF;
|
||
|
||
DELETE FROM ems.predicted_negative_price_window p
|
||
WHERE p.site_id = p_site_id
|
||
AND p.predicted_date BETWEEN v_start AND v_end;
|
||
|
||
RETURN QUERY
|
||
WITH hist_price AS (
|
||
SELECT
|
||
EXTRACT(DOW FROM mip.interval_start AT TIME ZONE 'Europe/Prague')::INT AS dow,
|
||
EXTRACT(HOUR FROM mip.interval_start AT TIME ZONE 'Europe/Prague')::INT AS hour,
|
||
COUNT(*)::INT AS total_slots,
|
||
COUNT(*) FILTER (WHERE mip.buy_raw_price_czk_kwh < 0)::INT AS neg_slots,
|
||
AVG(mip.buy_raw_price_czk_kwh) AS avg_price,
|
||
MIN(mip.buy_raw_price_czk_kwh) AS min_price
|
||
FROM ems.market_interval_price mip
|
||
WHERE mip.market_source IN ('OTE_CZ', 'OTE_CZ_DAM')
|
||
AND mip.interval_start >= NOW() - INTERVAL '6 months'
|
||
GROUP BY 1, 2
|
||
HAVING COUNT(*) >= 4
|
||
),
|
||
latest_run AS (
|
||
SELECT fpr.id
|
||
FROM ems.forecast_pv_run fpr
|
||
WHERE fpr.site_id = p_site_id
|
||
AND fpr.status = 'ok'
|
||
ORDER BY fpr.created_at DESC NULLS LAST
|
||
LIMIT 1
|
||
),
|
||
slot_power_hist AS (
|
||
SELECT
|
||
fpi.interval_start,
|
||
SUM(fpi.power_w)::BIGINT AS total_w
|
||
FROM ems.forecast_pv_interval fpi
|
||
INNER JOIN ems.forecast_pv_run fpr ON fpr.id = fpi.run_id
|
||
WHERE fpr.site_id = p_site_id
|
||
AND fpr.status = 'ok'
|
||
AND fpi.interval_start >= NOW() - INTERVAL '6 months'
|
||
GROUP BY fpi.interval_start
|
||
),
|
||
pv_max_by_hour AS (
|
||
SELECT
|
||
EXTRACT(HOUR FROM sp.interval_start AT TIME ZONE 'Europe/Prague')::INT AS hour,
|
||
MAX(sp.total_w)::BIGINT AS hist_max_w
|
||
FROM slot_power_hist sp
|
||
GROUP BY 1
|
||
),
|
||
pred_slot AS (
|
||
SELECT
|
||
fpi.interval_start,
|
||
SUM(fpi.power_w)::BIGINT AS total_w
|
||
FROM ems.forecast_pv_interval fpi
|
||
INNER JOIN latest_run lr ON lr.id = fpi.run_id
|
||
GROUP BY fpi.interval_start
|
||
),
|
||
pred_by_hour AS (
|
||
SELECT
|
||
(ps.interval_start AT TIME ZONE 'Europe/Prague')::DATE AS d,
|
||
EXTRACT(HOUR FROM ps.interval_start AT TIME ZONE 'Europe/Prague')::INT AS hour,
|
||
MAX(ps.total_w)::BIGINT AS pred_max_w
|
||
FROM pred_slot ps
|
||
GROUP BY 1, 2
|
||
),
|
||
future_days AS (
|
||
SELECT gs::DATE AS d
|
||
FROM generate_series(v_start, v_end, INTERVAL '1 day') AS gs
|
||
),
|
||
hourly_base AS (
|
||
SELECT
|
||
fd.d AS predicted_date,
|
||
h.hour,
|
||
EXTRACT(DOW FROM fd.d)::INT AS dow,
|
||
LEAST(
|
||
100,
|
||
GREATEST(
|
||
0,
|
||
(100.0 * hp.neg_slots / NULLIF(hp.total_slots, 0))::INT
|
||
)
|
||
) AS base_prob,
|
||
hp.min_price
|
||
FROM future_days fd
|
||
CROSS JOIN generate_series(0, 23) AS h (hour)
|
||
INNER JOIN hist_price hp
|
||
ON hp.dow = EXTRACT(DOW FROM fd.d)::INT
|
||
AND hp.hour = h.hour
|
||
),
|
||
hourly_adj AS (
|
||
SELECT
|
||
hb.predicted_date,
|
||
hb.hour,
|
||
hb.dow,
|
||
hb.base_prob,
|
||
hb.min_price,
|
||
CASE
|
||
WHEN pm.hist_max_w IS NULL OR pm.hist_max_w <= 0 THEN hb.base_prob
|
||
WHEN ph.pred_max_w IS NULL THEN hb.base_prob
|
||
WHEN ph.pred_max_w::NUMERIC > 0.8 * pm.hist_max_w::NUMERIC THEN
|
||
LEAST(100, hb.base_prob + 15)
|
||
WHEN ph.pred_max_w::NUMERIC < 0.4 * pm.hist_max_w::NUMERIC THEN
|
||
GREATEST(0, hb.base_prob - 20)
|
||
ELSE hb.base_prob
|
||
END AS probability_pct
|
||
FROM hourly_base hb
|
||
LEFT JOIN pred_by_hour ph
|
||
ON ph.d = hb.predicted_date
|
||
AND ph.hour = hb.hour
|
||
LEFT JOIN pv_max_by_hour pm ON pm.hour = hb.hour
|
||
),
|
||
qualified AS (
|
||
SELECT
|
||
ha.predicted_date,
|
||
ha.hour,
|
||
ha.probability_pct,
|
||
ha.min_price AS expected_hour_min,
|
||
ha.hour
|
||
- ROW_NUMBER() OVER (
|
||
PARTITION BY ha.predicted_date, ha.probability_pct
|
||
ORDER BY ha.hour
|
||
) AS grp
|
||
FROM hourly_adj ha
|
||
WHERE ha.probability_pct >= 30
|
||
),
|
||
windows AS (
|
||
SELECT
|
||
q.predicted_date,
|
||
q.probability_pct,
|
||
MIN(q.hour) AS window_start_hour,
|
||
MAX(q.hour) AS window_end_hour,
|
||
MIN(q.expected_hour_min) AS expected_min_price
|
||
FROM qualified q
|
||
GROUP BY q.predicted_date, q.probability_pct, q.grp
|
||
),
|
||
final_rows AS (
|
||
SELECT
|
||
w.predicted_date,
|
||
w.window_start_hour,
|
||
w.window_end_hour,
|
||
w.probability_pct,
|
||
ROUND(w.expected_min_price::NUMERIC, 4) AS expected_min_price,
|
||
CASE
|
||
WHEN w.probability_pct >= 70 THEN
|
||
'Historicky vysoká FVE výroba v tento čas – velmi pravděpodobné'
|
||
WHEN w.probability_pct >= 50 THEN
|
||
'Víkendový vzor + dobrá předpověď FVE'
|
||
ELSE
|
||
'Možné na základě historických dat'
|
||
END AS reason
|
||
FROM windows w
|
||
WHERE w.probability_pct >= 25
|
||
),
|
||
ins AS (
|
||
INSERT INTO ems.predicted_negative_price_window (
|
||
site_id,
|
||
predicted_date,
|
||
window_start_hour,
|
||
window_end_hour,
|
||
probability_pct,
|
||
expected_min_price,
|
||
reason
|
||
)
|
||
SELECT
|
||
p_site_id,
|
||
fr.predicted_date,
|
||
fr.window_start_hour,
|
||
fr.window_end_hour,
|
||
fr.probability_pct,
|
||
fr.expected_min_price,
|
||
fr.reason
|
||
FROM final_rows fr
|
||
RETURNING
|
||
predicted_negative_price_window.predicted_date,
|
||
predicted_negative_price_window.window_start_hour,
|
||
predicted_negative_price_window.window_end_hour,
|
||
predicted_negative_price_window.probability_pct,
|
||
predicted_negative_price_window.expected_min_price,
|
||
predicted_negative_price_window.reason
|
||
)
|
||
SELECT
|
||
ins.predicted_date,
|
||
ins.window_start_hour,
|
||
ins.window_end_hour,
|
||
ins.probability_pct,
|
||
ins.expected_min_price,
|
||
ins.reason
|
||
FROM ins;
|
||
END;
|
||
$$;
|
||
|
||
COMMENT ON FUNCTION ems.fn_predict_negative_price_windows(INT, INT) IS
|
||
'Predikuje okna se zvýšeným rizikem záporné nákupní ceny z historie OTE (6 měsíců), upraví podle forecastu FVE a zapíše do ems.predicted_negative_price_window. Volat denně po importu cen a forecastu.';
|