gb-price-estimate / code /hour23.py
x2q's picture
Initial release 2026-11-03
e49f88c verified
Raw History Blame Contribute Delete
2.7 kB
"""The missing last hour of Portugal's and Ireland's newest day.
Both zones run one hour behind CET, so the day-ahead auction (CET day) leaves the last local hour (23:00-24:00 = 00:00-01:00 CET of the next market day) of the
newest settled day without a price until the next auction is published (~11:45-12:45 UTC). The estimate of that ONE hour uses what is known when it is missing:
the same day's hours 0..22 and every earlier day.
persist the price of hour 22 of the same day the naive method
med28 hour 22 + the median of (hour 23 - hour 22) over the last 28 complete days "the last settled hour and the typical same-hour step"
Which of the two a zone publishes is decided by backtest_hour23.py (hour23_gate.json): the one with the lower error out of sample, and only if it is not worse than
the best naive method (persistence of hour 22, yesterday's hour 23, last week's hour 23) on the whole backtest AND on the hold-out. The band is the
rolling residual band of the EU model (calibrate.py: 1.2 x the 10th / 90th percentile of the method's own error over the last 180 days).
Pure functions on a day x hour matrix (rows = days in order, 24 columns, NaN where there is no price).
"""
import numpy as np
METHODS = ("persist", "med28")
STEP_DAYS = 28
BAND_DAYS = 180
BAND_SCALE = 1.2
MIN_STEPS = 10
def step_median(P, i, n=STEP_DAYS):
"""Median of (hour 23 - hour 22) over the n complete days before day index i, or NaN with fewer than MIN_STEPS."""
s = (P[max(0, i - n):i, 23] - P[max(0, i - n):i, 22])
s = s[np.isfinite(s)]
return float(np.median(s)) if len(s) >= MIN_STEPS else np.nan
def estimate(P, i, method):
"""The estimate of P[i, 23] from P[i, :23] and the days before i, or NaN when hour 22 is missing (or the history is too short for med28)."""
p22 = P[i, 22]
if not np.isfinite(p22):
return np.nan
if method == "persist":
return float(p22)
if method == "med28":
st = step_median(P, i)
return float(p22 + st) if np.isfinite(st) else np.nan
raise ValueError(method)
def naive(P, i):
"""The naive candidates for P[i, 23]: persistence of hour 22, yesterday's hour 23, last week's hour 23."""
return {"persist": P[i, 22], "yday": P[i - 1, 23] if i >= 1 else np.nan, "lag7": P[i - 7, 23] if i >= 7 else np.nan}
def band_offsets(res, scale=BAND_SCALE):
"""(lo, hi) from the residuals (actual - estimate) of the window: scale x P10 and scale x P90."""
r = np.asarray(res, float)
r = r[np.isfinite(r)]
if len(r) < 60:
return None
return round(float(np.percentile(r, 10)) * scale, 2), round(float(np.percentile(r, 90)) * scale, 2)