105 lines
3.5 KiB
SQL
105 lines
3.5 KiB
SQL
-- =============================================================
|
||
-- Hrubý odhad „kolik by přineslo přesunout ~30 kWh/den“ z levných
|
||
-- slotů (OTE sell < práh) do večerních/ranních špiček.
|
||
--
|
||
-- Zjednodušení:
|
||
-- • Kalendářní den v Europe/Prague.
|
||
-- • Bereme jen dny, kde existuje aspoň jeden 15min slot s sell < :cheap_thr.
|
||
-- • „Levná“ strana: průměrná OTE sell ve všech slotech toho dne s sell < :cheap_thr.
|
||
-- • „Drahá“ strana: okno večer+ráno (18:00–24:00 a 0:00–8:00), seřadíme sloty
|
||
-- podle ceny DESC, vyhodíme :drop_top nejvyšších (malá baterie je už „sežere“),
|
||
-- vezmeme dalších :take_slot 15min intervalů → :take_slot × 0,25 h × 12 kW = 30 kWh
|
||
-- při 12 kW a take_slot = 10.
|
||
-- • Hrubý přínos (Kč/den) ≈ :shift_kwh * (avg_peak - avg_cheap); bez účinnosti baterie.
|
||
--
|
||
-- Uprav parametry v nejníže (date_trunc rozsah, práhy, počty slotů).
|
||
-- Spuštění: psql -v ON_ERROR_STOP=1 -f scripts/analysis/ote_arbitrage_proxy.sql
|
||
-- =============================================================
|
||
|
||
WITH params AS (
|
||
SELECT
|
||
0.3::numeric AS cheap_thr,
|
||
0::int AS drop_top, -- vynechat N nejdražších 15min ve večer+ráno okně
|
||
12::int AS take_slot, -- dalších N slotů = 2,5 h při 15 min
|
||
30::numeric AS shift_kwh -- objem energie pro hrubý spread (volitelně = take_slot * 0.25 * 12)
|
||
),
|
||
slots AS (
|
||
SELECT
|
||
interval_start,
|
||
(interval_start AT TIME ZONE 'Europe/Prague')::date AS d,
|
||
(interval_start AT TIME ZONE 'Europe/Prague')::time AS t,
|
||
sell_raw_price_czk_kwh::numeric AS sell
|
||
FROM ems.market_interval_price
|
||
WHERE market_source = 'OTE_CZ'
|
||
AND interval_start >= TIMESTAMPTZ '2025-04-01 Europe/Prague'
|
||
AND interval_start < TIMESTAMPTZ '2026-04-01 Europe/Prague'
|
||
),
|
||
days_cheap AS (
|
||
SELECT s.d
|
||
FROM slots s
|
||
GROUP BY s.d
|
||
HAVING MIN(s.sell) < (SELECT cheap_thr FROM params)
|
||
),
|
||
cheap_side AS (
|
||
SELECT
|
||
s.d,
|
||
AVG(s.sell) AS avg_cheap
|
||
FROM slots s
|
||
INNER JOIN days_cheap dc ON dc.d = s.d
|
||
WHERE s.sell < (SELECT cheap_thr FROM params)
|
||
GROUP BY s.d
|
||
),
|
||
evening_morning AS (
|
||
SELECT
|
||
s.d,
|
||
s.sell,
|
||
s.t
|
||
FROM slots s
|
||
INNER JOIN days_cheap dc ON dc.d = s.d
|
||
WHERE s.t >= TIME '18:00'
|
||
OR s.t < TIME '08:00'
|
||
),
|
||
ranked AS (
|
||
SELECT
|
||
em.d,
|
||
em.sell,
|
||
ROW_NUMBER() OVER (PARTITION BY em.d ORDER BY em.sell DESC, em.t) AS rn
|
||
FROM evening_morning em
|
||
),
|
||
peak_pick AS (
|
||
SELECT
|
||
r.d,
|
||
r.sell
|
||
FROM ranked r
|
||
WHERE r.rn > (SELECT drop_top FROM params)
|
||
AND r.rn <= (SELECT drop_top + take_slot FROM params)
|
||
),
|
||
peak_side AS (
|
||
SELECT d, AVG(sell) AS avg_peak
|
||
FROM peak_pick
|
||
GROUP BY d
|
||
),
|
||
per_day AS (
|
||
SELECT
|
||
c.d,
|
||
c.avg_cheap,
|
||
p.avg_peak,
|
||
(SELECT shift_kwh FROM params) * (p.avg_peak - c.avg_cheap) AS rough_kc_day
|
||
FROM cheap_side c
|
||
INNER JOIN peak_side p ON p.d = c.d
|
||
),
|
||
period AS (
|
||
SELECT
|
||
(TIMESTAMPTZ '2026-04-01 Europe/Prague' - TIMESTAMPTZ '2024-04-01 Europe/Prague')
|
||
AS len
|
||
)
|
||
SELECT
|
||
COUNT(*)::int AS days_qualifying,
|
||
ROUND(SUM(pd.rough_kc_day)::numeric, 2) AS spread_kc_sum_period,
|
||
ROUND(AVG(pd.rough_kc_day)::numeric, 4) AS spread_kc_avg_per_qualifying_day,
|
||
ROUND(
|
||
(SUM(pd.rough_kc_day) / NULLIF(EXTRACT(EPOCH FROM (SELECT len FROM period)) / 86400.0, 0) * 365.0)::numeric,
|
||
2
|
||
) AS spread_kc_naive_per_solar_year
|
||
FROM per_day pd;
|