feat(best_price,peak_price): add optional extension to VERY_CHEAP/VERY_EXPENSIVE intervals

After period detection, optionally walk left/right from each period boundary
to absorb adjacent VERY_CHEAP (best price) or VERY_EXPENSIVE (peak price)
intervals (step 7.5 in the pipeline).

New constants: CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP, CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS,
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE, CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS.
Defaults: off / 4 intervals (1 hour per side). Hard maximum: 12 intervals (3 hours).

Config stored under "extension_settings" section, reflected in period hash
for correct cache invalidation.

New module: coordinator/period_handlers/shape_extension.py handles the
boundary walk, stat recalculation, and extension_intervals_added bookkeeping.

Impact: Users can opt-in to wider best/peak price windows that include
extreme-level adjacent intervals, reducing missed very cheap/expensive slots
at period edges.
This commit is contained in:
Julian Pawlowski 2026-04-11 21:24:44 +00:00
parent 447dc907e6
commit b7f1efce1f
12 changed files with 10006 additions and 9485 deletions

View file

@ -12,7 +12,9 @@ import voluptuous as vol
from custom_components.tibber_prices.const import (
BEST_PRICE_MAX_LEVEL_OPTIONS,
CONF_AVERAGE_SENSOR_DISPLAY,
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
CONF_BEST_PRICE_FLEX,
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS,
CONF_BEST_PRICE_MAX_LEVEL,
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
@ -23,7 +25,9 @@ from custom_components.tibber_prices.const import (
CONF_EXTENDED_DESCRIPTIONS,
CONF_MIN_PERIODS_BEST,
CONF_MIN_PERIODS_PEAK,
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
CONF_PEAK_PRICE_FLEX,
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
CONF_PEAK_PRICE_MIN_LEVEL,
@ -49,7 +53,9 @@ from custom_components.tibber_prices.const import (
CONF_VOLATILITY_THRESHOLD_MODERATE,
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
DEFAULT_AVERAGE_SENSOR_DISPLAY,
DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
DEFAULT_BEST_PRICE_FLEX,
DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS,
DEFAULT_BEST_PRICE_MAX_LEVEL,
DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
@ -59,7 +65,9 @@ from custom_components.tibber_prices.const import (
DEFAULT_EXTENDED_DESCRIPTIONS,
DEFAULT_MIN_PERIODS_BEST,
DEFAULT_MIN_PERIODS_PEAK,
DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
DEFAULT_PEAK_PRICE_FLEX,
DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
DEFAULT_PEAK_PRICE_MIN_LEVEL,
@ -86,6 +94,7 @@ from custom_components.tibber_prices.const import (
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
DISPLAY_MODE_BASE,
DISPLAY_MODE_SUBUNIT,
MAX_EXTENSION_INTERVALS,
MAX_GAP_COUNT,
MAX_MIN_PERIOD_LENGTH,
MAX_MIN_PERIODS,
@ -618,6 +627,7 @@ def get_best_price_schema(
period_settings = options.get("period_settings", {})
flexibility_settings = options.get("flexibility_settings", {})
relaxation_settings = options.get("relaxation_and_target_periods", {})
extension_settings = options.get("extension_settings", {})
# Get current values for override display
min_period_length = int(
@ -633,6 +643,12 @@ def get_best_price_schema(
enable_min_periods = relaxation_settings.get(CONF_ENABLE_MIN_PERIODS_BEST, DEFAULT_ENABLE_MIN_PERIODS_BEST)
min_periods = int(relaxation_settings.get(CONF_MIN_PERIODS_BEST, DEFAULT_MIN_PERIODS_BEST))
relaxation_attempts = int(relaxation_settings.get(CONF_RELAXATION_ATTEMPTS_BEST, DEFAULT_RELAXATION_ATTEMPTS_BEST))
extend_to_very_cheap = bool(
extension_settings.get(CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP, DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP)
)
max_extension_intervals_best = int(
extension_settings.get(CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS, DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS)
)
# Build section schemas with optional override warnings
period_warning = get_section_override_warning("best_price", "period_settings", overrides, translations) or {}
@ -754,6 +770,28 @@ def get_best_price_schema(
vol.Schema(relaxation_fields),
{"collapsed": True},
),
vol.Required("extension_settings"): section(
vol.Schema(
{
vol.Optional(
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
default=extend_to_very_cheap,
): BooleanSelector(selector.BooleanSelectorConfig()),
vol.Optional(
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS,
default=max_extension_intervals_best,
): NumberSelector(
NumberSelectorConfig(
min=1,
max=MAX_EXTENSION_INTERVALS,
step=1,
mode=NumberSelectorMode.SLIDER,
)
),
}
),
{"collapsed": True},
),
}
)
@ -779,6 +817,7 @@ def get_peak_price_schema(
period_settings = options.get("period_settings", {})
flexibility_settings = options.get("flexibility_settings", {})
relaxation_settings = options.get("relaxation_and_target_periods", {})
extension_settings = options.get("extension_settings", {})
# Get current values for override display
min_period_length = int(
@ -794,6 +833,12 @@ def get_peak_price_schema(
enable_min_periods = relaxation_settings.get(CONF_ENABLE_MIN_PERIODS_PEAK, DEFAULT_ENABLE_MIN_PERIODS_PEAK)
min_periods = int(relaxation_settings.get(CONF_MIN_PERIODS_PEAK, DEFAULT_MIN_PERIODS_PEAK))
relaxation_attempts = int(relaxation_settings.get(CONF_RELAXATION_ATTEMPTS_PEAK, DEFAULT_RELAXATION_ATTEMPTS_PEAK))
extend_to_very_expensive = bool(
extension_settings.get(CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE, DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE)
)
max_extension_intervals_peak = int(
extension_settings.get(CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS, DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS)
)
# Build section schemas with optional override warnings
period_warning = get_section_override_warning("peak_price", "period_settings", overrides, translations) or {}
@ -915,6 +960,28 @@ def get_peak_price_schema(
vol.Schema(relaxation_fields),
{"collapsed": True},
),
vol.Required("extension_settings"): section(
vol.Schema(
{
vol.Optional(
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
default=extend_to_very_expensive,
): BooleanSelector(selector.BooleanSelectorConfig()),
vol.Optional(
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
default=max_extension_intervals_peak,
): NumberSelector(
NumberSelectorConfig(
min=1,
max=MAX_EXTENSION_INTERVALS,
step=1,
mode=NumberSelectorMode.SLIDER,
)
),
}
),
{"collapsed": True},
),
}
)

View file

@ -68,6 +68,10 @@ CONF_RELAXATION_ATTEMPTS_BEST = "relaxation_attempts_best"
CONF_ENABLE_MIN_PERIODS_PEAK = "enable_min_periods_peak"
CONF_MIN_PERIODS_PEAK = "min_periods_peak"
CONF_RELAXATION_ATTEMPTS_PEAK = "relaxation_attempts_peak"
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP = "best_price_extend_to_very_cheap"
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS = "best_price_max_extension_intervals"
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE = "peak_price_extend_to_very_expensive"
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS = "peak_price_max_extension_intervals"
ATTRIBUTION = "Data provided by Tibber"
@ -131,6 +135,10 @@ DEFAULT_RELAXATION_ATTEMPTS_BEST = 11 # Default: 11 steps allows escalation fro
DEFAULT_ENABLE_MIN_PERIODS_PEAK = True # Default: minimum periods feature enabled for peak price
DEFAULT_MIN_PERIODS_PEAK = 2 # Default: require at least 2 peak price periods (when enabled)
DEFAULT_RELAXATION_ATTEMPTS_PEAK = 11 # Default: 11 steps allows escalation from 20% to 50% (3% increment per step)
DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP = False # Default: disabled (opt-in feature)
DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS = 4 # Default: up to 4 intervals (1 hour) per side
DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE = False # Default: disabled (opt-in feature)
DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS = 4 # Default: up to 4 intervals (1 hour) per side
# Validation limits (used in GUI schemas and server-side validation)
# These ensure consistency between frontend and backend validation
@ -139,6 +147,7 @@ MAX_DISTANCE_PERCENTAGE = 50 # Maximum distance from average percentage (GUI sl
MAX_GAP_COUNT = 8 # Maximum gap count for level filtering (GUI slider limit)
MAX_MIN_PERIODS = 10 # Maximum number of minimum periods per day (GUI slider limit)
MAX_RELAXATION_ATTEMPTS = 12 # Maximum relaxation attempts (GUI slider limit)
MAX_EXTENSION_INTERVALS = 12 # Maximum extension intervals per side (GUI slider limit = 3 hours)
MIN_PERIOD_LENGTH = 15 # Minimum period length in minutes (1 quarter hour)
MAX_MIN_PERIOD_LENGTH = 180 # Maximum for minimum period length setting (3 hours - realistic for required minimum)
@ -407,6 +416,13 @@ def get_default_options(currency_code: str | None) -> dict[str, Any]:
CONF_MIN_PERIODS_PEAK: DEFAULT_MIN_PERIODS_PEAK,
CONF_RELAXATION_ATTEMPTS_PEAK: DEFAULT_RELAXATION_ATTEMPTS_PEAK,
},
# Nested section: Extension settings (shared by best/peak price)
"extension_settings": {
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP: DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS: DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS,
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE: DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS: DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
},
}

View file

@ -28,6 +28,9 @@ from .outlier_filtering import filter_price_outliers
# Re-export relaxation
from .relaxation import calculate_periods_with_relaxation
# Re-export shape extension
from .shape_extension import extend_periods_for_shape
# Re-export constants and types
from .types import (
ALL_DAY_PATTERNS,
@ -80,5 +83,6 @@ __all__ = [
"calculate_periods",
"calculate_periods_with_relaxation",
"detect_day_patterns",
"extend_periods_for_shape",
"filter_price_outliers",
]

View file

@ -25,6 +25,7 @@ from .period_building import (
from .period_statistics import (
extract_period_summaries,
)
from .shape_extension import extend_periods_for_shape
from .types import TibberPricesThresholdConfig
# Flex limits to prevent degenerate behavior (see docs/development/period-calculation-theory.md)
@ -209,6 +210,20 @@ def calculate_periods(
time=time,
)
# Step 7.5: Extend periods into adjacent VERY_CHEAP / VERY_EXPENSIVE intervals
# This is an opt-in feature (disabled by default) that adds contiguous
# extreme-level intervals on each side of an already-found period.
if config.extend_to_extreme and config.max_extension_intervals > 0:
period_summaries = extend_periods_for_shape(
period_summaries,
all_prices_sorted,
price_context,
reverse_sort=reverse_sort,
max_extension_intervals=config.max_extension_intervals,
thresholds=thresholds,
time=time,
)
# Step 8: Cross-day extension for late-night periods
# If a best-price period ends near midnight and tomorrow has continued low prices,
# extend the period across midnight to give users the full cheap window

View file

@ -0,0 +1,258 @@
"""
Shape-based period extension: extend periods into adjacent VERY_CHEAP/VERY_EXPENSIVE intervals.
After periods are identified by the core algorithm, this module optionally extends
each period's boundaries to include any directly-adjacent intervals that carry the
most extreme price level relevant to the period type:
- Best price periods extend into VERY_CHEAP neighbouring intervals
- Peak price periods extend into VERY_EXPENSIVE neighbouring intervals
Extension is purely additive and opt-in (disabled by default). It does not affect
the core period-finding logic; periods that would not normally be found are not
created by this step.
"""
from __future__ import annotations
import statistics
from datetime import timedelta
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.const import (
PRICE_LEVEL_VERY_CHEAP,
PRICE_LEVEL_VERY_EXPENSIVE,
)
from custom_components.tibber_prices.utils.price import (
aggregate_period_levels,
aggregate_period_ratings,
)
from .period_statistics import (
calculate_aggregated_rating_difference,
calculate_period_price_diff,
calculate_period_price_statistics,
)
if TYPE_CHECKING:
from datetime import datetime
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
from .types import TibberPricesThresholdConfig
_INTERVAL_DURATION = timedelta(minutes=15)
def extend_periods_for_shape( # noqa: PLR0913 - Extension requires all context params
periods: list[dict[str, Any]],
all_prices: list[dict[str, Any]],
price_context: dict[str, Any],
*,
reverse_sort: bool,
max_extension_intervals: int,
thresholds: TibberPricesThresholdConfig,
time: TibberPricesTimeService,
) -> list[dict[str, Any]]:
"""
Extend each period into adjacent VERY_CHEAP or VERY_EXPENSIVE intervals.
For best price periods (reverse_sort=False): extend into VERY_CHEAP neighbours.
For peak price periods (reverse_sort=True): extend into VERY_EXPENSIVE neighbours.
Only intervals that are directly contiguous with the period and carry the
target level are added. At most *max_extension_intervals* are consumed on
each side independently. Period statistics are fully recalculated after
any extension.
Args:
periods: Period summary dicts from ``extract_period_summaries``.
all_prices: All enriched price intervals (yesterday + today + tomorrow).
price_context: Dict with ``ref_prices`` and ``avg_prices`` per calendar day.
reverse_sort: ``True`` for peak price, ``False`` for best price.
max_extension_intervals: Maximum extra intervals that may be added per side.
thresholds: Threshold configuration for level / rating aggregation.
time: Time-service instance used to resolve ``startsAt`` timestamps.
Returns:
Updated list of period dicts, potentially with extended boundaries and
recalculated statistics. Unmodified periods are returned as-is.
"""
if not periods or max_extension_intervals <= 0:
return periods
target_level = PRICE_LEVEL_VERY_EXPENSIVE if reverse_sort else PRICE_LEVEL_VERY_CHEAP
# Build a lookup dict: local datetime → full interval dict
interval_index: dict[datetime, dict[str, Any]] = {}
for iv in all_prices:
t = time.get_interval_time(iv)
if t is not None:
interval_index[t] = iv
return [
_extend_period_edges(
period,
interval_index,
target_level=target_level,
max_intervals=max_extension_intervals,
thresholds=thresholds,
price_context=price_context,
)
for period in periods
]
# ── private helpers ────────────────────────────────────────────────────────────
def _extend_period_edges( # noqa: PLR0913, PLR0912, PLR0915 - Period edge extension requires many args, branches, and statements
period: dict[str, Any],
interval_index: dict[datetime, dict[str, Any]],
*,
target_level: str,
max_intervals: int,
thresholds: TibberPricesThresholdConfig,
price_context: dict[str, Any],
) -> dict[str, Any]:
"""
Consume adjacent target-level intervals on both edges of a period.
The original period dict is never mutated; a new dict is returned.
If no extension is possible, the original dict is returned unchanged.
Args:
period: Period summary dict with ``start`` and ``end`` datetime keys.
interval_index: Lookup map of ``{starts_at_datetime: interval_dict}``.
target_level: ``"VERY_CHEAP"`` or ``"VERY_EXPENSIVE"``.
max_intervals: Maximum intervals that may be added on each side.
thresholds: Threshold config for aggregation helpers.
price_context: Reference prices / averages per calendar day.
Returns:
Extended (or original) period summary dict.
"""
start: datetime = period["start"]
end: datetime = period["end"]
# ``end`` is the exclusive boundary: the last included interval starts at
# ``end - _INTERVAL_DURATION``.
# ── walk LEFT (earlier than period start) ─────────────────────────────────
left_additions: list[dict[str, Any]] = []
cursor = start - _INTERVAL_DURATION
for _ in range(max_intervals):
iv = interval_index.get(cursor)
if iv is None or iv.get("level") != target_level:
break
left_additions.insert(0, iv)
cursor -= _INTERVAL_DURATION
# ── walk RIGHT (later than period end) ────────────────────────────────────
right_additions: list[dict[str, Any]] = []
cursor = end # first interval AFTER the period
for _ in range(max_intervals):
iv = interval_index.get(cursor)
if iv is None or iv.get("level") != target_level:
break
right_additions.append(iv)
cursor += _INTERVAL_DURATION
total_added = len(left_additions) + len(right_additions)
if total_added == 0:
return period
# ── rebuild full interval list for the extended period ────────────────────
original_intervals = _collect_original_intervals(start, end, interval_index)
all_period_intervals = left_additions + original_intervals + right_additions
# ── recalculate boundaries ────────────────────────────────────────────────
new_start = start - _INTERVAL_DURATION * len(left_additions)
new_end = end + _INTERVAL_DURATION * len(right_additions)
new_duration_minutes = int((new_end - new_start).total_seconds() // 60)
new_interval_count = len(all_period_intervals)
# ── recalculate price statistics ──────────────────────────────────────────
price_stats = calculate_period_price_statistics(all_period_intervals)
period_price_diff, period_price_diff_pct = calculate_period_price_diff(
price_stats["price_mean"], new_start, price_context
)
rating_diff_pct = calculate_aggregated_rating_difference(all_period_intervals)
# ── recalculate level / rating aggregates ─────────────────────────────────
new_level = aggregate_period_levels(all_period_intervals)
new_rating: str | None = None
if thresholds.threshold_low is not None and thresholds.threshold_high is not None:
new_rating, _ = aggregate_period_ratings(
all_period_intervals,
thresholds.threshold_low,
thresholds.threshold_high,
)
# ── recalculate volatility (coefficient of variation) ────────────────────
prices_for_vol = [float(p["total"]) for p in all_period_intervals if "total" in p]
cv_pct: float | None = None
if len(prices_for_vol) >= 2: # noqa: PLR2004
mean_p = statistics.mean(prices_for_vol)
if mean_p > 0:
cv_pct = round(statistics.stdev(prices_for_vol) / mean_p * 100, 1)
# ── assemble updated period dict (keep structural fields, update statistics) ─
reverse_sort = target_level == PRICE_LEVEL_VERY_EXPENSIVE
updated: dict[str, Any] = {
**period,
# Time fields
"start": new_start,
"end": new_end,
"duration_minutes": new_duration_minutes,
# Core decision attributes
"level": new_level,
"rating_level": new_rating,
"rating_difference_%": rating_diff_pct,
# Price statistics
"price_mean": price_stats["price_mean"],
"price_median": price_stats["price_median"],
"price_min": price_stats["price_min"],
"price_max": price_stats["price_max"],
"price_spread": price_stats["price_spread"],
"price_coefficient_variation_%": cv_pct,
# Detail
"period_interval_count": new_interval_count,
# Extension metadata
"extension_intervals_added": total_added,
}
# Refresh period price diff (replaces old value from base period)
if reverse_sort:
updated.pop("period_price_diff_from_daily_min", None)
updated.pop("period_price_diff_from_daily_min_%", None)
if period_price_diff is not None:
updated["period_price_diff_from_daily_max"] = period_price_diff
if period_price_diff_pct is not None:
updated["period_price_diff_from_daily_max_%"] = period_price_diff_pct
else:
updated.pop("period_price_diff_from_daily_max", None)
updated.pop("period_price_diff_from_daily_max_%", None)
if period_price_diff is not None:
updated["period_price_diff_from_daily_min"] = period_price_diff
if period_price_diff_pct is not None:
updated["period_price_diff_from_daily_min_%"] = period_price_diff_pct
return updated
def _collect_original_intervals(
start: datetime,
end: datetime,
interval_index: dict[datetime, dict[str, Any]],
) -> list[dict[str, Any]]:
"""Reconstruct the ordered interval list for an existing period from the index."""
result: list[dict[str, Any]] = []
cursor = start
while cursor < end:
iv = interval_index.get(cursor)
if iv is not None:
result.append(iv)
cursor += _INTERVAL_DURATION
return result

View file

@ -56,6 +56,8 @@ class TibberPricesPeriodConfig(NamedTuple):
threshold_volatility_very_high: float = DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH
level_filter: str | None = None # "any", "cheap", "expensive", etc. or None
gap_count: int = 0 # Number of allowed consecutive deviating intervals
extend_to_extreme: bool = False # Extend periods into adjacent VERY_CHEAP/VERY_EXPENSIVE intervals
max_extension_intervals: int = 0 # Max intervals this extension may add per side (0 = disabled)
class TibberPricesPeriodData(NamedTuple):

View file

@ -225,6 +225,41 @@ class TibberPricesPeriodCalculator:
"min_period_length": int(min_period_length),
}
# Extension settings (stored in 'extension_settings' nested section)
if reverse_sort:
extend_to_extreme = bool(
self._get_option(
_const.CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
"extension_settings",
_const.DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
)
)
max_extension_intervals = int(
self._get_option(
_const.CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
"extension_settings",
_const.DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
)
)
else:
extend_to_extreme = bool(
self._get_option(
_const.CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
"extension_settings",
_const.DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
)
)
max_extension_intervals = int(
self._get_option(
_const.CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS,
"extension_settings",
_const.DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS,
)
)
config["extend_to_extreme"] = extend_to_extreme
config["max_extension_intervals"] = max_extension_intervals
# Cache the result
self._config_cache[cache_key] = config
self._config_cache_valid = True
@ -702,6 +737,8 @@ class TibberPricesPeriodCalculator:
threshold_volatility_very_high=threshold_volatility_very_high,
level_filter=max_level_best,
gap_count=gap_count_best,
extend_to_extreme=best_config["extend_to_extreme"],
max_extension_intervals=best_config["max_extension_intervals"],
)
best_periods = calculate_periods_with_relaxation(
all_prices,
@ -783,6 +820,8 @@ class TibberPricesPeriodCalculator:
threshold_volatility_very_high=threshold_volatility_very_high,
level_filter=min_level_peak,
gap_count=gap_count_peak,
extend_to_extreme=peak_config["extend_to_extreme"],
max_extension_intervals=peak_config["max_extension_intervals"],
)
peak_periods = calculate_periods_with_relaxation(
all_prices,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff