Files
ems/scripts/analysis/ote_arbitrage_proxy.sql
Dusan Vojacek fd06811753
All checks were successful
deploy / deploy (push) Successful in 25s
test / smoke-test (push) Successful in 6s
tune microcycling
2026-04-13 00:49:36 +02:00

105 lines
3.5 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =============================================================
-- 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:0024:00 a 0:008: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;