tune microcycling
All checks were successful
deploy / deploy (push) Successful in 25s
test / smoke-test (push) Successful in 6s

This commit is contained in:
Dusan Vojacek
2026-04-13 00:49:36 +02:00
parent 3b33594354
commit fd06811753
10 changed files with 587 additions and 62 deletions

View File

@@ -0,0 +1,104 @@
-- =============================================================
-- 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;