mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-08 15:26:38 +00:00
fix: detect recent Energy-Charts source cadence (#1315)
Infer interval coverage from consistent recent original price spacings while preserving history and forecasting resolution policies. Reuse source data until a successful refresh and cover cadence transitions, fallback, and history repair. Fixes #1279
This commit is contained in:
@@ -98,6 +98,35 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
|
||||
"""Return the unique identifier for the Energy-Charts provider."""
|
||||
return "ElecPriceEnergyCharts"
|
||||
|
||||
def _coverage_resolution_seconds(self, source_series: pd.Series) -> int:
|
||||
"""Infer interval coverage from original prices without changing forecast resolution.
|
||||
|
||||
The caller excludes timestamps beyond ``highest_orig_datetime``. Within the last
|
||||
24 hours, four equal spacings among the final five differences establish a recent
|
||||
cadence. This recognizes five consecutive points at a new resolution while tolerating
|
||||
one exceptional gap. Only positive intervals dividing one hour are supported, as in
|
||||
the shared resolution helper.
|
||||
|
||||
Ambiguous or insufficient agreement falls back to the median resolution of these
|
||||
24 hours; fewer than two distinct timestamps fall back to hourly coverage.
|
||||
"""
|
||||
if source_series.empty:
|
||||
return 3600
|
||||
|
||||
recent_series = source_series.sort_index()
|
||||
recent_series = recent_series[
|
||||
recent_series.index >= recent_series.index[-1] - pd.Timedelta(hours=24)
|
||||
]
|
||||
index = pd.DatetimeIndex(recent_series.index).drop_duplicates()
|
||||
deltas = index.to_series().diff().dropna().dt.total_seconds().tail(5)
|
||||
counts = deltas.value_counts()
|
||||
if not counts.empty and counts.iloc[0] >= 4:
|
||||
resolution = float(counts.index[0])
|
||||
if resolution > 0 and 3600 % resolution == 0:
|
||||
return int(resolution)
|
||||
|
||||
return self._resolution_seconds(recent_series)
|
||||
|
||||
def _has_complete_published_horizon(
|
||||
self, *, now: pd.Timestamp, resolution_seconds: int
|
||||
) -> bool:
|
||||
@@ -247,12 +276,21 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
|
||||
|
||||
# Determine if update is needed and what start date is really necessary
|
||||
needs_update = False
|
||||
raw_series: Optional[pd.Series] = None
|
||||
if self.highest_orig_datetime:
|
||||
raw_history = await self.key_to_raw_series(
|
||||
source_end = to_datetime(self.highest_orig_datetime).add(seconds=1)
|
||||
raw_series = await self.key_to_raw_series(
|
||||
key="elecprice_marketprice_raw_wh",
|
||||
start_datetime=start_datetime,
|
||||
end_datetime=gross_start_datetime,
|
||||
end_datetime=max(gross_start_datetime, source_end),
|
||||
)
|
||||
# Preserve the history window even during an outage when the latest original
|
||||
# timestamp precedes EMS start. Reuse this read for coverage and forecasting,
|
||||
# but exclude the extrapolated tail from both resolution estimates.
|
||||
raw_history = raw_series[
|
||||
(raw_series.index >= pd.Timestamp(start_datetime))
|
||||
& (raw_series.index < pd.Timestamp(gross_start_datetime))
|
||||
]
|
||||
raw_series = raw_series[raw_series.index <= pd.Timestamp(self.highest_orig_datetime)]
|
||||
|
||||
if raw_history.empty:
|
||||
# We need the default start date (35 days in past)
|
||||
@@ -271,16 +309,7 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
|
||||
# Use default start date in case of forced update
|
||||
needs_update = True
|
||||
else:
|
||||
# The latest source data may have a different resolution than
|
||||
# the history before ems_start_datetime. Use its final 24 hours
|
||||
# so older, finer intervals cannot dominate the median, and
|
||||
# exclude the predicted tail.
|
||||
source_series = await self.key_to_raw_series(
|
||||
key="elecprice_marketprice_raw_wh",
|
||||
start_datetime=to_datetime(self.highest_orig_datetime).subtract(hours=24),
|
||||
end_datetime=to_datetime(self.highest_orig_datetime).add(seconds=1),
|
||||
)
|
||||
source_resolution_seconds = self._resolution_seconds(source_series)
|
||||
source_resolution_seconds = self._coverage_resolution_seconds(raw_series)
|
||||
if not self._has_complete_published_horizon(
|
||||
now=now, resolution_seconds=source_resolution_seconds
|
||||
):
|
||||
@@ -311,6 +340,8 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
|
||||
raise ValueError("No Energy-Charts electricity price data available")
|
||||
self.highest_orig_datetime = to_datetime(series_data.index.max())
|
||||
await self.key_from_series("elecprice_marketprice_raw_wh", series_data)
|
||||
# Reload after a successful fetch so prediction sees the new source data.
|
||||
raw_series = None
|
||||
# Newly fetched data widens the window that needs its gross
|
||||
# (fee-inclusive) values recomputed.
|
||||
gross_start_datetime = to_datetime(series_data.index.min())
|
||||
@@ -335,10 +366,11 @@ class ElecPriceEnergyCharts(ElecPriceProvider):
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
raw_series = await self.key_to_raw_series(
|
||||
key="elecprice_marketprice_raw_wh",
|
||||
end_datetime=to_datetime(self.highest_orig_datetime).add(seconds=1),
|
||||
)
|
||||
if raw_series is None:
|
||||
raw_series = await self.key_to_raw_series(
|
||||
key="elecprice_marketprice_raw_wh",
|
||||
end_datetime=to_datetime(self.highest_orig_datetime).add(seconds=1),
|
||||
)
|
||||
resolution_seconds = self._resolution_seconds(raw_series)
|
||||
slots_per_hour = 3600 // resolution_seconds
|
||||
|
||||
|
||||
Reference in New Issue
Block a user