diff --git a/custom_components/tibber_prices/config_flow_handlers/options_flow.py b/custom_components/tibber_prices/config_flow_handlers/options_flow.py index c1ef500..f1981fd 100644 --- a/custom_components/tibber_prices/config_flow_handlers/options_flow.py +++ b/custom_components/tibber_prices/config_flow_handlers/options_flow.py @@ -33,6 +33,8 @@ from custom_components.tibber_prices.config_flow_handlers.validators import ( validate_price_rating_thresholds, validate_price_trend_falling, validate_price_trend_rising, + validate_price_trend_strongly_falling, + validate_price_trend_strongly_rising, validate_relaxation_attempts, validate_volatility_threshold_high, validate_volatility_threshold_moderate, @@ -54,6 +56,8 @@ from custom_components.tibber_prices.const import ( CONF_PRICE_RATING_THRESHOLD_LOW, CONF_PRICE_TREND_THRESHOLD_FALLING, CONF_PRICE_TREND_THRESHOLD_RISING, + CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING, + CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING, CONF_RELAXATION_ATTEMPTS_BEST, CONF_RELAXATION_ATTEMPTS_PEAK, CONF_VOLATILITY_THRESHOLD_HIGH, @@ -493,6 +497,34 @@ class TibberPricesOptionsFlowHandler(OptionsFlow): ): errors[CONF_PRICE_TREND_THRESHOLD_FALLING] = "invalid_price_trend_falling" + # Validate strongly rising trend threshold + if CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING in user_input and not validate_price_trend_strongly_rising( + user_input[CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING] + ): + errors[CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING] = "invalid_price_trend_strongly_rising" + + # Validate strongly falling trend threshold + if CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING in user_input and not validate_price_trend_strongly_falling( + user_input[CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING] + ): + errors[CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING] = "invalid_price_trend_strongly_falling" + + # Cross-validation: Ensure rising < strongly_rising and falling > strongly_falling + if not errors: + rising = user_input.get(CONF_PRICE_TREND_THRESHOLD_RISING) + strongly_rising = user_input.get(CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING) + falling = user_input.get(CONF_PRICE_TREND_THRESHOLD_FALLING) + strongly_falling = user_input.get(CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING) + + if rising is not None and strongly_rising is not None and rising >= strongly_rising: + errors[CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING] = ( + "invalid_trend_strongly_rising_less_than_rising" + ) + if falling is not None and strongly_falling is not None and falling <= strongly_falling: + errors[CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING] = ( + "invalid_trend_strongly_falling_greater_than_falling" + ) + if not errors: # Store flat data directly in options (no section wrapping) self._options.update(user_input) diff --git a/custom_components/tibber_prices/config_flow_handlers/schemas.py b/custom_components/tibber_prices/config_flow_handlers/schemas.py index 0adfd3d..c6c126b 100644 --- a/custom_components/tibber_prices/config_flow_handlers/schemas.py +++ b/custom_components/tibber_prices/config_flow_handlers/schemas.py @@ -35,6 +35,8 @@ from custom_components.tibber_prices.const import ( CONF_PRICE_RATING_THRESHOLD_LOW, CONF_PRICE_TREND_THRESHOLD_FALLING, CONF_PRICE_TREND_THRESHOLD_RISING, + CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING, + CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING, CONF_RELAXATION_ATTEMPTS_BEST, CONF_RELAXATION_ATTEMPTS_PEAK, CONF_VIRTUAL_TIME_OFFSET_DAYS, @@ -66,6 +68,8 @@ from custom_components.tibber_prices.const import ( DEFAULT_PRICE_RATING_THRESHOLD_LOW, DEFAULT_PRICE_TREND_THRESHOLD_FALLING, DEFAULT_PRICE_TREND_THRESHOLD_RISING, + DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING, + DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING, DEFAULT_RELAXATION_ATTEMPTS_BEST, DEFAULT_RELAXATION_ATTEMPTS_PEAK, DEFAULT_VIRTUAL_TIME_OFFSET_DAYS, @@ -86,6 +90,8 @@ from custom_components.tibber_prices.const import ( MAX_PRICE_RATING_THRESHOLD_LOW, MAX_PRICE_TREND_FALLING, MAX_PRICE_TREND_RISING, + MAX_PRICE_TREND_STRONGLY_FALLING, + MAX_PRICE_TREND_STRONGLY_RISING, MAX_RELAXATION_ATTEMPTS, MAX_VOLATILITY_THRESHOLD_HIGH, MAX_VOLATILITY_THRESHOLD_MODERATE, @@ -99,6 +105,8 @@ from custom_components.tibber_prices.const import ( MIN_PRICE_RATING_THRESHOLD_LOW, MIN_PRICE_TREND_FALLING, MIN_PRICE_TREND_RISING, + MIN_PRICE_TREND_STRONGLY_FALLING, + MIN_PRICE_TREND_STRONGLY_RISING, MIN_RELAXATION_ATTEMPTS, MIN_VOLATILITY_THRESHOLD_HIGH, MIN_VOLATILITY_THRESHOLD_MODERATE, @@ -745,6 +753,23 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema: mode=NumberSelectorMode.SLIDER, ), ), + vol.Optional( + CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING, + default=int( + options.get( + CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING, + DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING, + ) + ), + ): NumberSelector( + NumberSelectorConfig( + min=MIN_PRICE_TREND_STRONGLY_RISING, + max=MAX_PRICE_TREND_STRONGLY_RISING, + step=1, + unit_of_measurement="%", + mode=NumberSelectorMode.SLIDER, + ), + ), vol.Optional( CONF_PRICE_TREND_THRESHOLD_FALLING, default=int( @@ -762,6 +787,23 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema: mode=NumberSelectorMode.SLIDER, ), ), + vol.Optional( + CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING, + default=int( + options.get( + CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING, + DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING, + ) + ), + ): NumberSelector( + NumberSelectorConfig( + min=MIN_PRICE_TREND_STRONGLY_FALLING, + max=MAX_PRICE_TREND_STRONGLY_FALLING, + step=1, + unit_of_measurement="%", + mode=NumberSelectorMode.SLIDER, + ), + ), } ) diff --git a/custom_components/tibber_prices/config_flow_handlers/validators.py b/custom_components/tibber_prices/config_flow_handlers/validators.py index a935b40..fcd2b26 100644 --- a/custom_components/tibber_prices/config_flow_handlers/validators.py +++ b/custom_components/tibber_prices/config_flow_handlers/validators.py @@ -20,6 +20,8 @@ from custom_components.tibber_prices.const import ( MAX_PRICE_RATING_THRESHOLD_LOW, MAX_PRICE_TREND_FALLING, MAX_PRICE_TREND_RISING, + MAX_PRICE_TREND_STRONGLY_FALLING, + MAX_PRICE_TREND_STRONGLY_RISING, MAX_RELAXATION_ATTEMPTS, MAX_VOLATILITY_THRESHOLD_HIGH, MAX_VOLATILITY_THRESHOLD_MODERATE, @@ -30,6 +32,8 @@ from custom_components.tibber_prices.const import ( MIN_PRICE_RATING_THRESHOLD_LOW, MIN_PRICE_TREND_FALLING, MIN_PRICE_TREND_RISING, + MIN_PRICE_TREND_STRONGLY_FALLING, + MIN_PRICE_TREND_STRONGLY_RISING, MIN_RELAXATION_ATTEMPTS, MIN_VOLATILITY_THRESHOLD_HIGH, MIN_VOLATILITY_THRESHOLD_MODERATE, @@ -337,3 +341,31 @@ def validate_price_trend_falling(threshold: int) -> bool: """ return MIN_PRICE_TREND_FALLING <= threshold <= MAX_PRICE_TREND_FALLING + + +def validate_price_trend_strongly_rising(threshold: int) -> bool: + """ + Validate strongly rising price trend threshold. + + Args: + threshold: Strongly rising trend threshold percentage (2 to 100) + + Returns: + True if threshold is valid (MIN_PRICE_TREND_STRONGLY_RISING to MAX_PRICE_TREND_STRONGLY_RISING) + + """ + return MIN_PRICE_TREND_STRONGLY_RISING <= threshold <= MAX_PRICE_TREND_STRONGLY_RISING + + +def validate_price_trend_strongly_falling(threshold: int) -> bool: + """ + Validate strongly falling price trend threshold. + + Args: + threshold: Strongly falling trend threshold percentage (-100 to -2) + + Returns: + True if threshold is valid (MIN_PRICE_TREND_STRONGLY_FALLING to MAX_PRICE_TREND_STRONGLY_FALLING) + + """ + return MIN_PRICE_TREND_STRONGLY_FALLING <= threshold <= MAX_PRICE_TREND_STRONGLY_FALLING diff --git a/custom_components/tibber_prices/const.py b/custom_components/tibber_prices/const.py index ea61b66..614eb2b 100644 --- a/custom_components/tibber_prices/const.py +++ b/custom_components/tibber_prices/const.py @@ -50,6 +50,8 @@ CONF_PRICE_LEVEL_GAP_TOLERANCE = "price_level_gap_tolerance" CONF_AVERAGE_SENSOR_DISPLAY = "average_sensor_display" # "median" or "mean" CONF_PRICE_TREND_THRESHOLD_RISING = "price_trend_threshold_rising" CONF_PRICE_TREND_THRESHOLD_FALLING = "price_trend_threshold_falling" +CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING = "price_trend_threshold_strongly_rising" +CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING = "price_trend_threshold_strongly_falling" CONF_VOLATILITY_THRESHOLD_MODERATE = "volatility_threshold_moderate" CONF_VOLATILITY_THRESHOLD_HIGH = "volatility_threshold_high" CONF_VOLATILITY_THRESHOLD_VERY_HIGH = "volatility_threshold_very_high" @@ -101,6 +103,10 @@ DEFAULT_PRICE_LEVEL_GAP_TOLERANCE = 1 # Max consecutive intervals to smooth out DEFAULT_AVERAGE_SENSOR_DISPLAY = "median" # Default: show median in state, mean in attributes DEFAULT_PRICE_TREND_THRESHOLD_RISING = 3 # Default trend threshold for rising prices (%) DEFAULT_PRICE_TREND_THRESHOLD_FALLING = -3 # Default trend threshold for falling prices (%, negative value) +# Strong trend thresholds default to 2x the base threshold. +# These are independently configurable to allow fine-tuning of "strongly" detection. +DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING = 6 # Default strong rising threshold (%) +DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING = -6 # Default strong falling threshold (%, negative value) # Default volatility thresholds (relative values using coefficient of variation) # Coefficient of variation = (standard_deviation / mean) * 100% # These thresholds are unitless and work across different price levels @@ -161,6 +167,11 @@ MIN_PRICE_TREND_RISING = 1 # Minimum rising trend threshold MAX_PRICE_TREND_RISING = 50 # Maximum rising trend threshold MIN_PRICE_TREND_FALLING = -50 # Minimum falling trend threshold (negative) MAX_PRICE_TREND_FALLING = -1 # Maximum falling trend threshold (negative) +# Strong trend thresholds have higher ranges to allow detection of significant moves +MIN_PRICE_TREND_STRONGLY_RISING = 2 # Minimum strongly rising threshold (must be > rising) +MAX_PRICE_TREND_STRONGLY_RISING = 100 # Maximum strongly rising threshold +MIN_PRICE_TREND_STRONGLY_FALLING = -100 # Minimum strongly falling threshold (negative) +MAX_PRICE_TREND_STRONGLY_FALLING = -2 # Maximum strongly falling threshold (must be < falling) # Gap count and relaxation limits MIN_GAP_COUNT = 0 # Minimum gap count @@ -447,6 +458,14 @@ VOLATILITY_MODERATE = "MODERATE" VOLATILITY_HIGH = "HIGH" VOLATILITY_VERY_HIGH = "VERY_HIGH" +# Price trend constants (calculated values with 5-level scale) +# Used by trend sensors: momentary, short-term, mid-term, long-term +PRICE_TREND_STRONGLY_FALLING = "strongly_falling" +PRICE_TREND_FALLING = "falling" +PRICE_TREND_STABLE = "stable" +PRICE_TREND_RISING = "rising" +PRICE_TREND_STRONGLY_RISING = "strongly_rising" + # Sensor options (lowercase versions for ENUM device class) # NOTE: These constants define the valid enum options, but they are not used directly # in sensor/definitions.py due to import timing issues. Instead, the options are defined inline @@ -472,6 +491,15 @@ VOLATILITY_OPTIONS = [ VOLATILITY_VERY_HIGH.lower(), ] +# Trend options for enum sensors (lowercase versions for ENUM device class) +PRICE_TREND_OPTIONS = [ + PRICE_TREND_STRONGLY_FALLING, + PRICE_TREND_FALLING, + PRICE_TREND_STABLE, + PRICE_TREND_RISING, + PRICE_TREND_STRONGLY_RISING, +] + # Valid options for best price maximum level filter # Sorted from cheap to expensive: user selects "up to how expensive" BEST_PRICE_MAX_LEVEL_OPTIONS = [ @@ -514,6 +542,16 @@ PRICE_RATING_MAPPING = { PRICE_RATING_HIGH: 1, } +# Mapping for comparing price trends (used for sorting and automation comparisons) +# Values range from -2 (strongly falling) to +2 (strongly rising), with 0 = stable +PRICE_TREND_MAPPING = { + PRICE_TREND_STRONGLY_FALLING: -2, + PRICE_TREND_FALLING: -1, + PRICE_TREND_STABLE: 0, + PRICE_TREND_RISING: 1, + PRICE_TREND_STRONGLY_RISING: 2, +} + # Icon mapping for price levels (dynamic icons based on level) PRICE_LEVEL_ICON_MAPPING = { PRICE_LEVEL_VERY_CHEAP: "mdi:gauge-empty", diff --git a/custom_components/tibber_prices/entity_utils/icons.py b/custom_components/tibber_prices/entity_utils/icons.py index 5da61ae..9bec4d7 100644 --- a/custom_components/tibber_prices/entity_utils/icons.py +++ b/custom_components/tibber_prices/entity_utils/icons.py @@ -85,19 +85,25 @@ def get_dynamic_icon( def get_trend_icon(key: str, value: Any) -> str | None: - """Get icon for trend sensors.""" + """Get icon for trend sensors using 5-level trend scale.""" # Handle next_price_trend_change TIMESTAMP sensor differently # (icon based on attributes, not value which is a timestamp) if key == "next_price_trend_change": return None # Will be handled by sensor's icon property using attributes - if not key.startswith("price_trend_") or not isinstance(value, str): + if not key.startswith("price_trend_") and key != "current_price_trend": return None + if not isinstance(value, str): + return None + + # 5-level trend icons: strongly uses double arrows, normal uses single trend_icons = { - "rising": "mdi:trending-up", - "falling": "mdi:trending-down", - "stable": "mdi:trending-neutral", + "strongly_rising": "mdi:chevron-double-up", # Strong upward movement + "rising": "mdi:trending-up", # Normal upward trend + "stable": "mdi:trending-neutral", # No significant change + "falling": "mdi:trending-down", # Normal downward trend + "strongly_falling": "mdi:chevron-double-down", # Strong downward movement } return trend_icons.get(value) diff --git a/custom_components/tibber_prices/sensor/calculators/trend.py b/custom_components/tibber_prices/sensor/calculators/trend.py index 7e7ecfd..6b08e5b 100644 --- a/custom_components/tibber_prices/sensor/calculators/trend.py +++ b/custom_components/tibber_prices/sensor/calculators/trend.py @@ -105,6 +105,8 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): # Get configured thresholds from options threshold_rising = self.config.get("price_trend_threshold_rising", 5.0) threshold_falling = self.config.get("price_trend_threshold_falling", -5.0) + threshold_strongly_rising = self.config.get("price_trend_threshold_strongly_rising", 6.0) + threshold_strongly_falling = self.config.get("price_trend_threshold_strongly_falling", -6.0) volatility_threshold_moderate = self.config.get("volatility_threshold_moderate", 15.0) volatility_threshold_high = self.config.get("volatility_threshold_high", 30.0) @@ -115,11 +117,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): lookahead_intervals = self.coordinator.time.minutes_to_intervals(hours * 60) # Calculate trend with volatility-adaptive thresholds - trend_state, diff_pct = calculate_price_trend( + trend_state, diff_pct, trend_value = calculate_price_trend( current_interval_price, future_mean, threshold_rising=threshold_rising, threshold_falling=threshold_falling, + threshold_strongly_rising=threshold_strongly_rising, + threshold_strongly_falling=threshold_strongly_falling, volatility_adjustment=True, # Always enabled lookahead_intervals=lookahead_intervals, all_intervals=all_intervals, @@ -127,11 +131,14 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): volatility_threshold_high=volatility_threshold_high, ) - # Determine icon color based on trend state + # Determine icon color based on trend state (5-level scale) + # Strongly rising/falling uses more intense colors icon_color = { - "rising": "var(--error-color)", # Red/Orange for rising prices (expensive) - "falling": "var(--success-color)", # Green for falling prices (cheaper) + "strongly_rising": "var(--error-color)", # Red for strongly rising (very expensive) + "rising": "var(--warning-color)", # Orange/Yellow for rising prices "stable": "var(--state-icon-color)", # Default gray for stable prices + "falling": "var(--success-color)", # Green for falling prices (cheaper) + "strongly_falling": "var(--success-color)", # Green for strongly falling (great deal) }.get(trend_state, "var(--state-icon-color)") # Convert prices to display currency unit based on configuration @@ -140,6 +147,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): # Store attributes in sensor-specific dictionary AND cache the trend value self._trend_attributes = { "timestamp": next_interval_start, + "trend_value": trend_value, f"trend_{hours}h_%": round(diff_pct, 1), f"next_{hours}h_avg": round(future_mean * factor, 2), "interval_count": lookahead_intervals, @@ -414,6 +422,8 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): return { "rising": self.config.get("price_trend_threshold_rising", 5.0), "falling": self.config.get("price_trend_threshold_falling", -5.0), + "strongly_rising": self.config.get("price_trend_threshold_strongly_rising", 6.0), + "strongly_falling": self.config.get("price_trend_threshold_strongly_falling", -6.0), "moderate": self.config.get("volatility_threshold_moderate", 15.0), "high": self.config.get("volatility_threshold_high", 30.0), } @@ -428,7 +438,7 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): current_index: Index of current interval Returns: - Momentum direction: "rising", "falling", or "stable" + Momentum direction: "strongly_rising", "rising", "stable", "falling", or "strongly_falling" """ # Look back 1 hour (4 intervals) for quick reaction @@ -451,15 +461,25 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): weighted_sum = sum(price * weight for price, weight in zip(trailing_prices, weights, strict=True)) weighted_avg = weighted_sum / sum(weights) - # Calculate momentum with 3% threshold + # Calculate momentum with thresholds + # Using same logic as 5-level trend: 3% for normal, 6% (2x) for strong momentum_threshold = 0.03 - diff = (current_price - weighted_avg) / weighted_avg + strong_momentum_threshold = 0.06 + diff = (current_price - weighted_avg) / abs(weighted_avg) if weighted_avg != 0 else 0 - if diff > momentum_threshold: - return "rising" - if diff < -momentum_threshold: - return "falling" - return "stable" + # Determine momentum level based on thresholds + if diff >= strong_momentum_threshold: + momentum = "strongly_rising" + elif diff > momentum_threshold: + momentum = "rising" + elif diff <= -strong_momentum_threshold: + momentum = "strongly_falling" + elif diff < -momentum_threshold: + momentum = "falling" + else: + momentum = "stable" + + return momentum def _combine_momentum_with_future( self, @@ -472,43 +492,60 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): """ Combine momentum analysis with future outlook to determine final trend. + Uses 5-level scale: strongly_rising, rising, stable, falling, strongly_falling. + Momentum intensity is preserved when future confirms the trend direction. + Args: - current_momentum: Current momentum direction (rising/falling/stable) + current_momentum: Current momentum direction (5-level scale) current_price: Current interval price future_mean: Average price in future window context: Dict with all_intervals, current_index, lookahead_intervals, thresholds Returns: - Final trend direction: "rising", "falling", or "stable" + Final trend direction (5-level scale) """ - if current_momentum == "rising": - # We're in uptrend - does it continue? - return "rising" if future_mean >= current_price * 0.98 else "falling" - - if current_momentum == "falling": - # We're in downtrend - does it continue? - return "falling" if future_mean <= current_price * 1.02 else "rising" - - # current_momentum == "stable" - what's coming? + # Use calculate_price_trend for consistency with 5-level logic all_intervals = context["all_intervals"] current_index = context["current_index"] lookahead_intervals = context["lookahead_intervals"] thresholds = context["thresholds"] lookahead_for_volatility = all_intervals[current_index : current_index + lookahead_intervals] - trend_state, _ = calculate_price_trend( + future_trend, _, _ = calculate_price_trend( current_price, future_mean, threshold_rising=thresholds["rising"], threshold_falling=thresholds["falling"], + threshold_strongly_rising=thresholds["strongly_rising"], + threshold_strongly_falling=thresholds["strongly_falling"], volatility_adjustment=True, lookahead_intervals=lookahead_intervals, all_intervals=lookahead_for_volatility, volatility_threshold_moderate=thresholds["moderate"], volatility_threshold_high=thresholds["high"], ) - return trend_state + + # Check if momentum and future trend are aligned (same direction) + momentum_rising = current_momentum in ("rising", "strongly_rising") + momentum_falling = current_momentum in ("falling", "strongly_falling") + future_rising = future_trend in ("rising", "strongly_rising") + future_falling = future_trend in ("falling", "strongly_falling") + + if momentum_rising and future_rising: + # Both indicate rising - use the stronger signal + if current_momentum == "strongly_rising" or future_trend == "strongly_rising": + return "strongly_rising" + return "rising" + + if momentum_falling and future_falling: + # Both indicate falling - use the stronger signal + if current_momentum == "strongly_falling" or future_trend == "strongly_falling": + return "strongly_falling" + return "falling" + + # Conflicting signals or stable momentum - trust future trend calculation + return future_trend def _calculate_standard_trend( self, @@ -534,11 +571,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): current_price = float(current_interval["total"]) standard_lookahead_volatility = all_intervals[current_index : current_index + standard_lookahead] - current_trend_3h, _ = calculate_price_trend( + current_trend_3h, _, _ = calculate_price_trend( current_price, standard_future_mean, threshold_rising=thresholds["rising"], threshold_falling=thresholds["falling"], + threshold_strongly_rising=thresholds["strongly_rising"], + threshold_strongly_falling=thresholds["strongly_falling"], volatility_adjustment=True, lookahead_intervals=standard_lookahead, all_intervals=standard_lookahead_volatility, @@ -606,11 +645,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): # Calculate trend at this past point lookahead_for_volatility = all_intervals[i : i + intervals_in_3h] - trend_state, _ = calculate_price_trend( + trend_state, _, _ = calculate_price_trend( price, future_mean, threshold_rising=thresholds["rising"], threshold_falling=thresholds["falling"], + threshold_strongly_rising=thresholds["strongly_rising"], + threshold_strongly_falling=thresholds["strongly_falling"], volatility_adjustment=True, lookahead_intervals=intervals_in_3h, all_intervals=lookahead_for_volatility, @@ -678,11 +719,13 @@ class TibberPricesTrendCalculator(TibberPricesBaseCalculator): # Calculate trend at this future point lookahead_for_volatility = all_intervals[i : i + intervals_in_3h] - trend_state, _ = calculate_price_trend( + trend_state, _, _ = calculate_price_trend( current_price, future_mean, threshold_rising=thresholds["rising"], threshold_falling=thresholds["falling"], + threshold_strongly_rising=thresholds["strongly_rising"], + threshold_strongly_falling=thresholds["strongly_falling"], volatility_adjustment=True, lookahead_intervals=intervals_in_3h, all_intervals=lookahead_for_volatility, diff --git a/custom_components/tibber_prices/sensor/core.py b/custom_components/tibber_prices/sensor/core.py index 8e953bf..030a7cd 100644 --- a/custom_components/tibber_prices/sensor/core.py +++ b/custom_components/tibber_prices/sensor/core.py @@ -987,11 +987,13 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor): key = self.entity_description.key value = self.native_value - # Icon mapping for trend directions + # Icon mapping for trend directions (5-level scale) trend_icons = { + "strongly_rising": "mdi:chevron-double-up", "rising": "mdi:trending-up", - "falling": "mdi:trending-down", "stable": "mdi:trending-neutral", + "falling": "mdi:trending-down", + "strongly_falling": "mdi:chevron-double-down", } # Special handling for next_price_trend_change: Icon based on direction attribute diff --git a/custom_components/tibber_prices/sensor/definitions.py b/custom_components/tibber_prices/sensor/definitions.py index a0dba17..f5ba9fc 100644 --- a/custom_components/tibber_prices/sensor/definitions.py +++ b/custom_components/tibber_prices/sensor/definitions.py @@ -548,7 +548,7 @@ FUTURE_TREND_SENSORS = ( icon="mdi:trending-up", # Dynamic: trending-up/trending-down/trending-neutral based on current trend device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics - options=["rising", "falling", "stable"], + options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), # Next trend change sensor (when will trend change?) @@ -570,7 +570,7 @@ FUTURE_TREND_SENSORS = ( icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics - options=["rising", "falling", "stable"], + options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( @@ -580,7 +580,7 @@ FUTURE_TREND_SENSORS = ( icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics - options=["rising", "falling", "stable"], + options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( @@ -590,7 +590,7 @@ FUTURE_TREND_SENSORS = ( icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics - options=["rising", "falling", "stable"], + options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( @@ -600,7 +600,7 @@ FUTURE_TREND_SENSORS = ( icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics - options=["rising", "falling", "stable"], + options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), SensorEntityDescription( @@ -610,7 +610,7 @@ FUTURE_TREND_SENSORS = ( icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics - options=["rising", "falling", "stable"], + options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=True, ), # Disabled by default: 6h, 8h, 12h @@ -621,7 +621,7 @@ FUTURE_TREND_SENSORS = ( icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics - options=["rising", "falling", "stable"], + options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -631,7 +631,7 @@ FUTURE_TREND_SENSORS = ( icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics - options=["rising", "falling", "stable"], + options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=False, ), SensorEntityDescription( @@ -641,7 +641,7 @@ FUTURE_TREND_SENSORS = ( icon="mdi:trending-up", # Dynamic: shows trending-up/trending-down/trending-neutral based on trend value device_class=SensorDeviceClass.ENUM, state_class=None, # Enum values: no statistics - options=["rising", "falling", "stable"], + options=["strongly_falling", "falling", "stable", "rising", "strongly_rising"], entity_registry_enabled_default=False, ), ) diff --git a/custom_components/tibber_prices/translations/de.json b/custom_components/tibber_prices/translations/de.json index d79bbd6..7e56e3c 100644 --- a/custom_components/tibber_prices/translations/de.json +++ b/custom_components/tibber_prices/translations/de.json @@ -283,14 +283,18 @@ }, "price_trend": { "title": "📈 Preistrend-Schwellenwerte", - "description": "**Konfiguriere Schwellenwerte für Preistrend-Sensoren. Diese Sensoren vergleichen den aktuellen Preis mit dem Durchschnitt der nächsten N Stunden, um festzustellen, ob die Preise steigen, fallen oder stabil sind.**", + "description": "**Konfiguriere Schwellenwerte für Preistrend-Sensoren.** Diese Sensoren vergleichen den aktuellen Preis mit dem Durchschnitt der nächsten N Stunden, um festzustellen, ob die Preise steigen, fallen oder stabil sind.\n\n**5-Stufen-Skala:** Nutzt stark_fallend (-2), fallend (-1), stabil (0), steigend (+1), stark_steigend (+2) für Automations-Vergleiche über das trend_value Attribut.", "data": { "price_trend_threshold_rising": "Steigend-Schwelle", - "price_trend_threshold_falling": "Fallend-Schwelle" + "price_trend_threshold_strongly_rising": "Stark steigend-Schwelle", + "price_trend_threshold_falling": "Fallend-Schwelle", + "price_trend_threshold_strongly_falling": "Stark fallend-Schwelle" }, "data_description": { - "price_trend_threshold_rising": "Prozentwert, um wie viel der Durchschnitt der nächsten N Stunden über dem aktuellen Preis liegen muss, damit der Trend als 'steigend' gilt. Beispiel: 5 bedeutet Durchschnitt ist mindestens 5% höher → Preise werden steigen. Typische Werte: 5-15%. Standard: 5%", - "price_trend_threshold_falling": "Prozentwert (negativ), um wie viel der Durchschnitt der nächsten N Stunden unter dem aktuellen Preis liegen muss, damit der Trend als 'fallend' gilt. Beispiel: -5 bedeutet Durchschnitt ist mindestens 5% niedriger → Preise werden fallen. Typische Werte: -5 bis -15%. Standard: -5%" + "price_trend_threshold_rising": "Prozentwert, um wie viel der Durchschnitt der nächsten N Stunden über dem aktuellen Preis liegen muss, damit der Trend als 'steigend' gilt. Beispiel: 3 bedeutet Durchschnitt ist mindestens 3% höher → Preise werden steigen. Typische Werte: 3-10%. Standard: 3%", + "price_trend_threshold_strongly_rising": "Prozentwert für 'stark steigend'-Trend. Muss höher sein als die steigend-Schwelle. Beispiel: 6 bedeutet Durchschnitt ist mindestens 6% höher → Preise werden deutlich steigen. Typische Werte: 6-15%. Standard: 6%", + "price_trend_threshold_falling": "Prozentwert (negativ), um wie viel der Durchschnitt der nächsten N Stunden unter dem aktuellen Preis liegen muss, damit der Trend als 'fallend' gilt. Beispiel: -3 bedeutet Durchschnitt ist mindestens 3% niedriger → Preise werden fallen. Typische Werte: -3 bis -10%. Standard: -3%", + "price_trend_threshold_strongly_falling": "Prozentwert (negativ) für 'stark fallend'-Trend. Muss niedriger (negativer) sein als die fallend-Schwelle. Beispiel: -6 bedeutet Durchschnitt ist mindestens 6% niedriger → Preise werden deutlich fallen. Typische Werte: -6 bis -15%. Standard: -6%" }, "submit": "↩ Speichern & Zurück" }, @@ -356,7 +360,11 @@ "invalid_volatility_threshold_very_high": "Sehr hohe Volatilitätsschwelle muss zwischen 35% und 80% liegen", "invalid_volatility_thresholds": "Schwellenwerte müssen aufsteigend sein: moderat < hoch < sehr hoch", "invalid_price_trend_rising": "Steigender Trendschwellenwert muss zwischen 1% und 50% liegen", - "invalid_price_trend_falling": "Fallender Trendschwellenwert muss zwischen -50% und -1% liegen" + "invalid_price_trend_falling": "Fallender Trendschwellenwert muss zwischen -50% und -1% liegen", + "invalid_price_trend_strongly_rising": "Stark steigender Trendschwellenwert muss zwischen 2% und 100% liegen", + "invalid_price_trend_strongly_falling": "Stark fallender Trendschwellenwert muss zwischen -100% und -2% liegen", + "invalid_trend_strongly_rising_less_than_rising": "Stark steigend-Schwelle muss größer als steigend-Schwelle sein", + "invalid_trend_strongly_falling_greater_than_falling": "Stark fallend-Schwelle muss kleiner (negativer) als fallend-Schwelle sein" }, "abort": { "entry_not_found": "Tibber Konfigurationseintrag nicht gefunden.", @@ -592,73 +600,91 @@ "price_trend_1h": { "name": "Preistrend (1h)", "state": { + "strongly_rising": "Stark steigend", "rising": "Steigend", + "stable": "Stabil", "falling": "Fallend", - "stable": "Stabil" + "strongly_falling": "Stark fallend" } }, "price_trend_2h": { "name": "Preistrend (2h)", "state": { + "strongly_rising": "Stark steigend", "rising": "Steigend", + "stable": "Stabil", "falling": "Fallend", - "stable": "Stabil" + "strongly_falling": "Stark fallend" } }, "price_trend_3h": { "name": "Preistrend (3h)", "state": { + "strongly_rising": "Stark steigend", "rising": "Steigend", + "stable": "Stabil", "falling": "Fallend", - "stable": "Stabil" + "strongly_falling": "Stark fallend" } }, "price_trend_4h": { "name": "Preistrend (4h)", "state": { + "strongly_rising": "Stark steigend", "rising": "Steigend", + "stable": "Stabil", "falling": "Fallend", - "stable": "Stabil" + "strongly_falling": "Stark fallend" } }, "price_trend_5h": { "name": "Preistrend (5h)", "state": { + "strongly_rising": "Stark steigend", "rising": "Steigend", + "stable": "Stabil", "falling": "Fallend", - "stable": "Stabil" + "strongly_falling": "Stark fallend" } }, "price_trend_6h": { "name": "Preistrend (6h)", "state": { + "strongly_rising": "Stark steigend", "rising": "Steigend", + "stable": "Stabil", "falling": "Fallend", - "stable": "Stabil" + "strongly_falling": "Stark fallend" } }, "price_trend_8h": { "name": "Preistrend (8h)", "state": { + "strongly_rising": "Stark steigend", "rising": "Steigend", + "stable": "Stabil", "falling": "Fallend", - "stable": "Stabil" + "strongly_falling": "Stark fallend" } }, "price_trend_12h": { "name": "Preistrend (12h)", "state": { + "strongly_rising": "Stark steigend", "rising": "Steigend", + "stable": "Stabil", "falling": "Fallend", - "stable": "Stabil" + "strongly_falling": "Stark fallend" } }, "current_price_trend": { "name": "Aktueller Preistrend", "state": { + "strongly_rising": "Stark steigend", "rising": "Steigend", + "stable": "Stabil", "falling": "Fallend", - "stable": "Stabil" + "strongly_falling": "Stark fallend" } }, "next_price_trend_change": { diff --git a/custom_components/tibber_prices/translations/en.json b/custom_components/tibber_prices/translations/en.json index 44afe69..0375f38 100644 --- a/custom_components/tibber_prices/translations/en.json +++ b/custom_components/tibber_prices/translations/en.json @@ -294,14 +294,18 @@ }, "price_trend": { "title": "📈 Price Trend Thresholds", - "description": "**Configure thresholds for price trend sensors. These sensors compare current price with the average of the next N hours to determine if prices are rising, falling, or stable.**", + "description": "**Configure thresholds for price trend sensors.** These sensors compare current price with the average of the next N hours to determine if prices are rising, falling, or stable.\n\n**5-Level Scale:** Uses strongly_falling (-2), falling (-1), stable (0), rising (+1), strongly_rising (+2) for automation comparisons via trend_value attribute.", "data": { "price_trend_threshold_rising": "Rising Threshold", - "price_trend_threshold_falling": "Falling Threshold" + "price_trend_threshold_strongly_rising": "Strongly Rising Threshold", + "price_trend_threshold_falling": "Falling Threshold", + "price_trend_threshold_strongly_falling": "Strongly Falling Threshold" }, "data_description": { - "price_trend_threshold_rising": "Percentage that the average of the next N hours must be above the current price to qualify as 'rising' trend. Example: 5 means average is at least 5% higher → prices will rise. Typical values: 5-15%. Default: 5%", - "price_trend_threshold_falling": "Percentage (negative) that the average of the next N hours must be below the current price to qualify as 'falling' trend. Example: -5 means average is at least 5% lower → prices will fall. Typical values: -5 to -15%. Default: -5%" + "price_trend_threshold_rising": "Percentage that the average of the next N hours must be above the current price to qualify as 'rising' trend. Example: 3 means average is at least 3% higher → prices will rise. Typical values: 3-10%. Default: 3%", + "price_trend_threshold_strongly_rising": "Percentage for 'strongly rising' trend. Must be higher than rising threshold. Example: 6 means average is at least 6% higher → prices will rise significantly. Typical values: 6-15%. Default: 6%", + "price_trend_threshold_falling": "Percentage (negative) that the average of the next N hours must be below the current price to qualify as 'falling' trend. Example: -3 means average is at least 3% lower → prices will fall. Typical values: -3 to -10%. Default: -3%", + "price_trend_threshold_strongly_falling": "Percentage (negative) for 'strongly falling' trend. Must be lower (more negative) than falling threshold. Example: -6 means average is at least 6% lower → prices will fall significantly. Typical values: -6 to -15%. Default: -6%" }, "submit": "↩ Save & Back" }, @@ -356,7 +360,11 @@ "invalid_volatility_threshold_very_high": "Very high volatility threshold must be between 35% and 80%", "invalid_volatility_thresholds": "Thresholds must be in ascending order: moderate < high < very high", "invalid_price_trend_rising": "Rising trend threshold must be between 1% and 50%", - "invalid_price_trend_falling": "Falling trend threshold must be between -50% and -1%" + "invalid_price_trend_falling": "Falling trend threshold must be between -50% and -1%", + "invalid_price_trend_strongly_rising": "Strongly rising trend threshold must be between 2% and 100%", + "invalid_price_trend_strongly_falling": "Strongly falling trend threshold must be between -100% and -2%", + "invalid_trend_strongly_rising_less_than_rising": "Strongly rising threshold must be greater than rising threshold", + "invalid_trend_strongly_falling_greater_than_falling": "Strongly falling threshold must be less (more negative) than falling threshold" }, "abort": { "entry_not_found": "Tibber configuration entry not found.", @@ -592,73 +600,91 @@ "price_trend_1h": { "name": "Price Trend (1h)", "state": { + "strongly_rising": "Strongly Rising", "rising": "Rising", + "stable": "Stable", "falling": "Falling", - "stable": "Stable" + "strongly_falling": "Strongly Falling" } }, "price_trend_2h": { "name": "Price Trend (2h)", "state": { + "strongly_rising": "Strongly Rising", "rising": "Rising", + "stable": "Stable", "falling": "Falling", - "stable": "Stable" + "strongly_falling": "Strongly Falling" } }, "price_trend_3h": { "name": "Price Trend (3h)", "state": { + "strongly_rising": "Strongly Rising", "rising": "Rising", + "stable": "Stable", "falling": "Falling", - "stable": "Stable" + "strongly_falling": "Strongly Falling" } }, "price_trend_4h": { "name": "Price Trend (4h)", "state": { + "strongly_rising": "Strongly Rising", "rising": "Rising", + "stable": "Stable", "falling": "Falling", - "stable": "Stable" + "strongly_falling": "Strongly Falling" } }, "price_trend_5h": { "name": "Price Trend (5h)", "state": { + "strongly_rising": "Strongly Rising", "rising": "Rising", + "stable": "Stable", "falling": "Falling", - "stable": "Stable" + "strongly_falling": "Strongly Falling" } }, "price_trend_6h": { "name": "Price Trend (6h)", "state": { + "strongly_rising": "Strongly Rising", "rising": "Rising", + "stable": "Stable", "falling": "Falling", - "stable": "Stable" + "strongly_falling": "Strongly Falling" } }, "price_trend_8h": { "name": "Price Trend (8h)", "state": { + "strongly_rising": "Strongly Rising", "rising": "Rising", + "stable": "Stable", "falling": "Falling", - "stable": "Stable" + "strongly_falling": "Strongly Falling" } }, "price_trend_12h": { "name": "Price Trend (12h)", "state": { + "strongly_rising": "Strongly Rising", "rising": "Rising", + "stable": "Stable", "falling": "Falling", - "stable": "Stable" + "strongly_falling": "Strongly Falling" } }, "current_price_trend": { "name": "Current Price Trend", "state": { + "strongly_rising": "Strongly Rising", "rising": "Rising", + "stable": "Stable", "falling": "Falling", - "stable": "Stable" + "strongly_falling": "Strongly Falling" } }, "next_price_trend_change": { diff --git a/custom_components/tibber_prices/translations/nb.json b/custom_components/tibber_prices/translations/nb.json index 15eaaba..be73459 100644 --- a/custom_components/tibber_prices/translations/nb.json +++ b/custom_components/tibber_prices/translations/nb.json @@ -283,14 +283,18 @@ }, "price_trend": { "title": "📈 Pristrendterskler", - "description": "**Konfigurer terskler for pristrendsensorer. Disse sensorene sammenligner nåværende pris med gjennomsnittet av de neste N timene for å bestemme om prisene stiger, faller eller er stabile.**", + "description": "**Konfigurer terskler for pristrendsensorer. Disse sensorene sammenligner nåværende pris med gjennomsnittet av de neste N timene for å bestemme om prisene stiger sterkt, stiger, er stabile, faller eller faller sterkt.**", "data": { "price_trend_threshold_rising": "Stigende terskel", - "price_trend_threshold_falling": "Fallende terskel" + "price_trend_threshold_strongly_rising": "Sterkt stigende terskel", + "price_trend_threshold_falling": "Fallende terskel", + "price_trend_threshold_strongly_falling": "Sterkt fallende terskel" }, "data_description": { - "price_trend_threshold_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'stigende' trend. Eksempel: 5 betyr gjennomsnittet er minst 5% høyere → prisene vil stige. Typiske verdier: 5-15%. Standard: 5%", - "price_trend_threshold_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'synkende' trend. Eksempel: -5 betyr gjennomsnittet er minst 5% lavere → prisene vil falle. Typiske verdier: -5 til -15%. Standard: -5%" + "price_trend_threshold_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'stigende' trend. Eksempel: 3 betyr gjennomsnittet er minst 3% høyere → prisene vil stige. Typiske verdier: 3-10%. Standard: 3%", + "price_trend_threshold_strongly_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'sterkt stigende' trend. Må være høyere enn stigende terskel. Typiske verdier: 6-20%. Standard: 6%", + "price_trend_threshold_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'synkende' trend. Eksempel: -3 betyr gjennomsnittet er minst 3% lavere → prisene vil falle. Typiske verdier: -3 til -10%. Standard: -3%", + "price_trend_threshold_strongly_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'sterkt synkende' trend. Må være lavere (mer negativ) enn fallende terskel. Typiske verdier: -6 til -20%. Standard: -6%" }, "submit": "↩ Lagre & tilbake" }, @@ -356,7 +360,11 @@ "invalid_volatility_threshold_very_high": "Svært høy volatilitetsgrense må være mellom 35% og 80%", "invalid_volatility_thresholds": "Grensene må være i stigende rekkefølge: moderat < høy < svært høy", "invalid_price_trend_rising": "Stigende trendgrense må være mellom 1% og 50%", - "invalid_price_trend_falling": "Fallende trendgrense må være mellom -50% og -1%" + "invalid_price_trend_falling": "Fallende trendgrense må være mellom -50% og -1%", + "invalid_price_trend_strongly_rising": "Sterkt stigende trendgrense må være mellom 2% og 100%", + "invalid_price_trend_strongly_falling": "Sterkt fallende trendgrense må være mellom -100% og -2%", + "invalid_trend_strongly_rising_less_than_rising": "Sterkt stigende-grense må være høyere enn stigende-grense", + "invalid_trend_strongly_falling_greater_than_falling": "Sterkt fallende-grense må være lavere (mer negativ) enn fallende-grense" }, "abort": { "entry_not_found": "Tibber-konfigurasjonsoppføring ikke funnet.", @@ -592,73 +600,91 @@ "price_trend_1h": { "name": "Pristrend (1t)", "state": { + "strongly_rising": "Sterkt stigende", "rising": "Stigende", + "stable": "Stabil", "falling": "Fallende", - "stable": "Stabil" + "strongly_falling": "Sterkt fallende" } }, "price_trend_2h": { "name": "Pristrend (2t)", "state": { + "strongly_rising": "Sterkt stigende", "rising": "Stigende", + "stable": "Stabil", "falling": "Fallende", - "stable": "Stabil" + "strongly_falling": "Sterkt fallende" } }, "price_trend_3h": { "name": "Pristrend (3t)", "state": { + "strongly_rising": "Sterkt stigende", "rising": "Stigende", + "stable": "Stabil", "falling": "Fallende", - "stable": "Stabil" + "strongly_falling": "Sterkt fallende" } }, "price_trend_4h": { "name": "Pristrend (4t)", "state": { + "strongly_rising": "Sterkt stigende", "rising": "Stigende", + "stable": "Stabil", "falling": "Fallende", - "stable": "Stabil" + "strongly_falling": "Sterkt fallende" } }, "price_trend_5h": { "name": "Pristrend (5t)", "state": { + "strongly_rising": "Sterkt stigende", "rising": "Stigende", + "stable": "Stabil", "falling": "Fallende", - "stable": "Stabil" + "strongly_falling": "Sterkt fallende" } }, "price_trend_6h": { "name": "Pristrend (6t)", "state": { + "strongly_rising": "Sterkt stigende", "rising": "Stigende", + "stable": "Stabil", "falling": "Fallende", - "stable": "Stabil" + "strongly_falling": "Sterkt fallende" } }, "price_trend_8h": { "name": "Pristrend (8t)", "state": { + "strongly_rising": "Sterkt stigende", "rising": "Stigende", + "stable": "Stabil", "falling": "Fallende", - "stable": "Stabil" + "strongly_falling": "Sterkt fallende" } }, "price_trend_12h": { "name": "Pristrend (12t)", "state": { + "strongly_rising": "Sterkt stigende", "rising": "Stigende", + "stable": "Stabil", "falling": "Fallende", - "stable": "Stabil" + "strongly_falling": "Sterkt fallende" } }, "current_price_trend": { "name": "Nåværende pristrend", "state": { + "strongly_rising": "Sterkt stigende", "rising": "Stigende", + "stable": "Stabil", "falling": "Fallende", - "stable": "Stabil" + "strongly_falling": "Sterkt fallende" } }, "next_price_trend_change": { diff --git a/custom_components/tibber_prices/translations/nl.json b/custom_components/tibber_prices/translations/nl.json index 226b9f3..ca4841b 100644 --- a/custom_components/tibber_prices/translations/nl.json +++ b/custom_components/tibber_prices/translations/nl.json @@ -283,14 +283,18 @@ }, "price_trend": { "title": "📈 Prijstrend Drempelwaarden", - "description": "**Configureer drempelwaarden voor prijstrend sensoren. Deze sensoren vergelijken de huidige prijs met het gemiddelde van de volgende N uur om te bepalen of prijzen stijgen, dalen of stabiel zijn.**", + "description": "**Configureer drempelwaarden voor prijstrend sensoren. Deze sensoren vergelijken de huidige prijs met het gemiddelde van de volgende N uur om te bepalen of prijzen sterk stijgen, stijgen, stabiel zijn, dalen of sterk dalen.**", "data": { "price_trend_threshold_rising": "Stijgende Drempel", - "price_trend_threshold_falling": "Dalende Drempel" + "price_trend_threshold_strongly_rising": "Sterk Stijgende Drempel", + "price_trend_threshold_falling": "Dalende Drempel", + "price_trend_threshold_strongly_falling": "Sterk Dalende Drempel" }, "data_description": { - "price_trend_threshold_rising": "Percentage dat het gemiddelde van de volgende N uur boven de huidige prijs moet zijn om te kwalificeren als 'stijgende' trend. Voorbeeld: 5 betekent dat het gemiddelde minimaal 5% hoger is → prijzen zullen stijgen. Typische waarden: 5-15%. Standaard: 5%", - "price_trend_threshold_falling": "Percentage (negatief) dat het gemiddelde van de volgende N uur onder de huidige prijs moet zijn om te kwalificeren als 'dalende' trend. Voorbeeld: -5 betekent dat het gemiddelde minimaal 5% lager is → prijzen zullen dalen. Typische waarden: -5 tot -15%. Standaard: -5%" + "price_trend_threshold_rising": "Percentage dat het gemiddelde van de volgende N uur boven de huidige prijs moet zijn om te kwalificeren als 'stijgende' trend. Voorbeeld: 3 betekent dat het gemiddelde minimaal 3% hoger is → prijzen zullen stijgen. Typische waarden: 3-10%. Standaard: 3%", + "price_trend_threshold_strongly_rising": "Percentage dat het gemiddelde van de volgende N uur boven de huidige prijs moet zijn om te kwalificeren als 'sterk stijgende' trend. Moet hoger zijn dan stijgende drempel. Typische waarden: 6-20%. Standaard: 6%", + "price_trend_threshold_falling": "Percentage (negatief) dat het gemiddelde van de volgende N uur onder de huidige prijs moet zijn om te kwalificeren als 'dalende' trend. Voorbeeld: -3 betekent dat het gemiddelde minimaal 3% lager is → prijzen zullen dalen. Typische waarden: -3 tot -10%. Standaard: -3%", + "price_trend_threshold_strongly_falling": "Percentage (negatief) dat het gemiddelde van de volgende N uur onder de huidige prijs moet zijn om te kwalificeren als 'sterk dalende' trend. Moet lager (meer negatief) zijn dan dalende drempel. Typische waarden: -6 tot -20%. Standaard: -6%" }, "submit": "↩ Opslaan & Terug" }, @@ -356,7 +360,11 @@ "invalid_volatility_threshold_very_high": "Zeer hoge volatiliteit drempel moet tussen 35% en 80% zijn", "invalid_volatility_thresholds": "Drempelwaarden moeten in oplopende volgorde zijn: gematigd < hoog < zeer hoog", "invalid_price_trend_rising": "Stijgende trend drempel moet tussen 1% en 50% zijn", - "invalid_price_trend_falling": "Dalende trend drempel moet tussen -50% en -1% zijn" + "invalid_price_trend_falling": "Dalende trend drempel moet tussen -50% en -1% zijn", + "invalid_price_trend_strongly_rising": "Sterk stijgende trend drempel moet tussen 2% en 100% zijn", + "invalid_price_trend_strongly_falling": "Sterk dalende trend drempel moet tussen -100% en -2% zijn", + "invalid_trend_strongly_rising_less_than_rising": "Sterk stijgende drempel moet hoger zijn dan stijgende drempel", + "invalid_trend_strongly_falling_greater_than_falling": "Sterk dalende drempel moet lager (meer negatief) zijn dan dalende drempel" }, "abort": { "entry_not_found": "Tibber-configuratie-item niet gevonden.", @@ -592,73 +600,91 @@ "price_trend_1h": { "name": "Prijstrend (1u)", "state": { + "strongly_rising": "Sterk stijgend", "rising": "Stijgend", + "stable": "Stabiel", "falling": "Dalend", - "stable": "Stabiel" + "strongly_falling": "Sterk dalend" } }, "price_trend_2h": { "name": "Prijstrend (2u)", "state": { + "strongly_rising": "Sterk stijgend", "rising": "Stijgend", + "stable": "Stabiel", "falling": "Dalend", - "stable": "Stabiel" + "strongly_falling": "Sterk dalend" } }, "price_trend_3h": { "name": "Prijstrend (3u)", "state": { + "strongly_rising": "Sterk stijgend", "rising": "Stijgend", + "stable": "Stabiel", "falling": "Dalend", - "stable": "Stabiel" + "strongly_falling": "Sterk dalend" } }, "price_trend_4h": { "name": "Prijstrend (4u)", "state": { + "strongly_rising": "Sterk stijgend", "rising": "Stijgend", + "stable": "Stabiel", "falling": "Dalend", - "stable": "Stabiel" + "strongly_falling": "Sterk dalend" } }, "price_trend_5h": { "name": "Prijstrend (5u)", "state": { + "strongly_rising": "Sterk stijgend", "rising": "Stijgend", + "stable": "Stabiel", "falling": "Dalend", - "stable": "Stabiel" + "strongly_falling": "Sterk dalend" } }, "price_trend_6h": { "name": "Prijstrend (6u)", "state": { + "strongly_rising": "Sterk stijgend", "rising": "Stijgend", + "stable": "Stabiel", "falling": "Dalend", - "stable": "Stabiel" + "strongly_falling": "Sterk dalend" } }, "price_trend_8h": { "name": "Prijstrend (8u)", "state": { + "strongly_rising": "Sterk stijgend", "rising": "Stijgend", + "stable": "Stabiel", "falling": "Dalend", - "stable": "Stabiel" + "strongly_falling": "Sterk dalend" } }, "price_trend_12h": { "name": "Prijstrend (12u)", "state": { + "strongly_rising": "Sterk stijgend", "rising": "Stijgend", + "stable": "Stabiel", "falling": "Dalend", - "stable": "Stabiel" + "strongly_falling": "Sterk dalend" } }, "current_price_trend": { "name": "Huidige Prijstrend", "state": { + "strongly_rising": "Sterk stijgend", "rising": "Stijgend", + "stable": "Stabiel", "falling": "Dalend", - "stable": "Stabiel" + "strongly_falling": "Sterk dalend" } }, "next_price_trend_change": { diff --git a/custom_components/tibber_prices/translations/sv.json b/custom_components/tibber_prices/translations/sv.json index a9422fb..78dab82 100644 --- a/custom_components/tibber_prices/translations/sv.json +++ b/custom_components/tibber_prices/translations/sv.json @@ -283,14 +283,18 @@ }, "price_trend": { "title": "📈 Pristrendtrösklar", - "description": "**Konfigurera tröskelvärden för pristrendsensorer. Dessa sensorer jämför aktuellt pris med genomsnittet av de nästa N timmarna för att bestämma om priserna stiger, faller eller är stabila.**", + "description": "**Konfigurera tröskelvärden för pristrendsensorer. Dessa sensorer jämför aktuellt pris med genomsnittet av de nästa N timmarna för att bestämma om priserna stiger kraftigt, stiger, är stabila, faller eller faller kraftigt.**", "data": { "price_trend_threshold_rising": "Stigande tröskel", - "price_trend_threshold_falling": "Fallande tröskel" + "price_trend_threshold_strongly_rising": "Kraftigt stigande tröskel", + "price_trend_threshold_falling": "Fallande tröskel", + "price_trend_threshold_strongly_falling": "Kraftigt fallande tröskel" }, "data_description": { - "price_trend_threshold_rising": "Procentandel som genomsnittet av de nästa N timmarna måste vara över det aktuella priset för att kvalificera som 'stigande' trend. Exempel: 5 betyder att genomsnittet är minst 5% högre → priserna kommer att stiga. Typiska värden: 5-15%. Standard: 5%", - "price_trend_threshold_falling": "Procentandel (negativ) som genomsnittet av de nästa N timmarna måste vara under det aktuella priset för att kvalificera som 'fallande' trend. Exempel: -5 betyder att genomsnittet är minst 5% lägre → priserna kommer att falla. Typiska värden: -5 till -15%. Standard: -5%" + "price_trend_threshold_rising": "Procentandel som genomsnittet av de nästa N timmarna måste vara över det aktuella priset för att kvalificera som 'stigande' trend. Exempel: 3 betyder att genomsnittet är minst 3% högre → priserna kommer att stiga. Typiska värden: 3-10%. Standard: 3%", + "price_trend_threshold_strongly_rising": "Procentandel som genomsnittet av de nästa N timmarna måste vara över det aktuella priset för att kvalificera som 'kraftigt stigande' trend. Måste vara högre än stigande tröskel. Typiska värden: 6-20%. Standard: 6%", + "price_trend_threshold_falling": "Procentandel (negativ) som genomsnittet av de nästa N timmarna måste vara under det aktuella priset för att kvalificera som 'fallande' trend. Exempel: -3 betyder att genomsnittet är minst 3% lägre → priserna kommer att falla. Typiska värden: -3 till -10%. Standard: -3%", + "price_trend_threshold_strongly_falling": "Procentandel (negativ) som genomsnittet av de nästa N timmarna måste vara under det aktuella priset för att kvalificera som 'kraftigt fallande' trend. Måste vara lägre (mer negativ) än fallande tröskel. Typiska värden: -6 till -20%. Standard: -6%" }, "submit": "↩ Spara & tillbaka" }, @@ -356,7 +360,11 @@ "invalid_volatility_threshold_very_high": "Mycket hög volatilitetströskel måste vara mellan 35% och 80%", "invalid_volatility_thresholds": "Trösklar måste vara i stigande ordning: måttlig < hög < mycket hög", "invalid_price_trend_rising": "Stigande trendtröskel måste vara mellan 1% och 50%", - "invalid_price_trend_falling": "Fallande trendtröskel måste vara mellan -50% och -1%" + "invalid_price_trend_falling": "Fallande trendtröskel måste vara mellan -50% och -1%", + "invalid_price_trend_strongly_rising": "Kraftigt stigande trendtröskel måste vara mellan 2% och 100%", + "invalid_price_trend_strongly_falling": "Kraftigt fallande trendtröskel måste vara mellan -100% och -2%", + "invalid_trend_strongly_rising_less_than_rising": "Kraftigt stigande-tröskel måste vara högre än stigande-tröskel", + "invalid_trend_strongly_falling_greater_than_falling": "Kraftigt fallande-tröskel måste vara lägre (mer negativ) än fallande-tröskel" }, "abort": { "entry_not_found": "Tibber-konfigurationspost hittades inte.", @@ -592,73 +600,91 @@ "price_trend_1h": { "name": "Pristrend (1h)", "state": { + "strongly_rising": "Kraftigt stigande", "rising": "Stigande", + "stable": "Stabil", "falling": "Fallande", - "stable": "Stabil" + "strongly_falling": "Kraftigt fallande" } }, "price_trend_2h": { "name": "Pristrend (2h)", "state": { + "strongly_rising": "Kraftigt stigande", "rising": "Stigande", + "stable": "Stabil", "falling": "Fallande", - "stable": "Stabil" + "strongly_falling": "Kraftigt fallande" } }, "price_trend_3h": { "name": "Pristrend (3h)", "state": { + "strongly_rising": "Kraftigt stigande", "rising": "Stigande", + "stable": "Stabil", "falling": "Fallande", - "stable": "Stabil" + "strongly_falling": "Kraftigt fallande" } }, "price_trend_4h": { "name": "Pristrend (4h)", "state": { + "strongly_rising": "Kraftigt stigande", "rising": "Stigande", + "stable": "Stabil", "falling": "Fallande", - "stable": "Stabil" + "strongly_falling": "Kraftigt fallande" } }, "price_trend_5h": { "name": "Pristrend (5h)", "state": { + "strongly_rising": "Kraftigt stigande", "rising": "Stigande", + "stable": "Stabil", "falling": "Fallande", - "stable": "Stabil" + "strongly_falling": "Kraftigt fallande" } }, "price_trend_6h": { "name": "Pristrend (6h)", "state": { + "strongly_rising": "Kraftigt stigande", "rising": "Stigande", + "stable": "Stabil", "falling": "Fallande", - "stable": "Stabil" + "strongly_falling": "Kraftigt fallande" } }, "price_trend_8h": { "name": "Pristrend (8h)", "state": { + "strongly_rising": "Kraftigt stigande", "rising": "Stigande", + "stable": "Stabil", "falling": "Fallande", - "stable": "Stabil" + "strongly_falling": "Kraftigt fallande" } }, "price_trend_12h": { "name": "Pristrend (12h)", "state": { + "strongly_rising": "Kraftigt stigande", "rising": "Stigande", + "stable": "Stabil", "falling": "Fallande", - "stable": "Stabil" + "strongly_falling": "Kraftigt fallande" } }, "current_price_trend": { "name": "Aktuell pristrend", "state": { + "strongly_rising": "Kraftigt stigande", "rising": "Stigande", + "stable": "Stabil", "falling": "Fallande", - "stable": "Stabil" + "strongly_falling": "Kraftigt fallande" } }, "next_price_trend_change": { diff --git a/custom_components/tibber_prices/utils/price.py b/custom_components/tibber_prices/utils/price.py index 1eee2a8..f4adc0f 100644 --- a/custom_components/tibber_prices/utils/price.py +++ b/custom_components/tibber_prices/utils/price.py @@ -20,6 +20,12 @@ from custom_components.tibber_prices.const import ( PRICE_LEVEL_MAPPING, PRICE_LEVEL_NORMAL, PRICE_RATING_NORMAL, + PRICE_TREND_FALLING, + PRICE_TREND_MAPPING, + PRICE_TREND_RISING, + PRICE_TREND_STABLE, + PRICE_TREND_STRONGLY_FALLING, + PRICE_TREND_STRONGLY_RISING, VOLATILITY_HIGH, VOLATILITY_LOW, VOLATILITY_MODERATE, @@ -1130,15 +1136,27 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v threshold_rising: float = 3.0, threshold_falling: float = -3.0, *, + threshold_strongly_rising: float = 6.0, + threshold_strongly_falling: float = -6.0, volatility_adjustment: bool = True, lookahead_intervals: int | None = None, all_intervals: list[dict[str, Any]] | None = None, volatility_threshold_moderate: float = DEFAULT_VOLATILITY_THRESHOLD_MODERATE, volatility_threshold_high: float = DEFAULT_VOLATILITY_THRESHOLD_HIGH, -) -> tuple[str, float]: +) -> tuple[str, float, int]: """ Calculate price trend by comparing current price with future average. + Uses a 5-level trend scale with integer values for automation comparisons: + - strongly_falling (-2): difference <= strongly_falling_threshold + - falling (-1): difference <= falling_threshold + - stable (0): difference between thresholds + - rising (+1): difference >= rising_threshold + - strongly_rising (+2): difference >= strongly_rising_threshold + + The strong thresholds are independently configurable (not derived from base + thresholds), allowing fine-grained control over trend sensitivity. + Supports volatility-adaptive thresholds: when enabled, the effective threshold is adjusted based on price volatility in the lookahead period. This makes the trend detection more sensitive during stable periods and less noisy during @@ -1152,6 +1170,8 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v future_average: Average price of future intervals threshold_rising: Base threshold for rising trend (%, positive, default 3%) threshold_falling: Base threshold for falling trend (%, negative, default -3%) + threshold_strongly_rising: Threshold for strongly rising (%, positive, default 6%) + threshold_strongly_falling: Threshold for strongly falling (%, negative, default -6%) volatility_adjustment: Enable volatility-adaptive thresholds (default True) lookahead_intervals: Number of intervals in trend period for volatility calc all_intervals: Price intervals (today + tomorrow) for volatility calculation @@ -1159,9 +1179,10 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v volatility_threshold_high: User-configured high volatility threshold (%) Returns: - Tuple of (trend_state, difference_percentage) - trend_state: "rising" | "falling" | "stable" + Tuple of (trend_state, difference_percentage, trend_value) + trend_state: PRICE_TREND_* constant (e.g., "strongly_rising") difference_percentage: % change from current to future ((future - current) / current * 100) + trend_value: Integer value from -2 to +2 for automation comparisons Note: Volatility adjustment factor: @@ -1172,12 +1193,13 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v """ if current_interval_price == 0: # Avoid division by zero - return stable trend - return "stable", 0.0 + return PRICE_TREND_STABLE, 0.0, PRICE_TREND_MAPPING[PRICE_TREND_STABLE] # Apply volatility adjustment if enabled and data available effective_rising = threshold_rising effective_falling = threshold_falling - volatility_factor = 1.0 + effective_strongly_rising = threshold_strongly_rising + effective_strongly_falling = threshold_strongly_falling if volatility_adjustment and lookahead_intervals and all_intervals: volatility_factor = _calculate_lookahead_volatility_factor( @@ -1185,22 +1207,25 @@ def calculate_price_trend( # noqa: PLR0913 - All parameters are necessary for v ) effective_rising = threshold_rising * volatility_factor effective_falling = threshold_falling * volatility_factor + effective_strongly_rising = threshold_strongly_rising * volatility_factor + effective_strongly_falling = threshold_strongly_falling * volatility_factor # Calculate percentage difference from current to future # CRITICAL: Use abs() for negative prices to get correct percentage direction # Example: current=-10, future=-5 → diff=5, pct=5/abs(-10)*100=+50% (correctly shows rising) - if current_interval_price == 0: - # Edge case: avoid division by zero - diff_pct = 0.0 - else: - diff_pct = ((future_average - current_interval_price) / abs(current_interval_price)) * 100 + diff_pct = ((future_average - current_interval_price) / abs(current_interval_price)) * 100 - # Determine trend based on effective thresholds - if diff_pct >= effective_rising: - trend = "rising" + # Determine trend based on effective thresholds (5-level scale) + # Check "strongly" conditions first (more extreme), then regular conditions + if diff_pct >= effective_strongly_rising: + trend = PRICE_TREND_STRONGLY_RISING + elif diff_pct >= effective_rising: + trend = PRICE_TREND_RISING + elif diff_pct <= effective_strongly_falling: + trend = PRICE_TREND_STRONGLY_FALLING elif diff_pct <= effective_falling: - trend = "falling" + trend = PRICE_TREND_FALLING else: - trend = "stable" + trend = PRICE_TREND_STABLE - return trend, diff_pct + return trend, diff_pct, PRICE_TREND_MAPPING[trend] diff --git a/tests/test_percentage_calculations.py b/tests/test_percentage_calculations.py index f512382..78c4ccf 100644 --- a/tests/test_percentage_calculations.py +++ b/tests/test_percentage_calculations.py @@ -99,20 +99,26 @@ def test_bug10_trend_diff_negative_current_price() -> None: future_average = -0.05 threshold_rising = 10.0 threshold_falling = -10.0 + threshold_strongly_rising = 20.0 + threshold_strongly_falling = -20.0 - trend, diff_pct = calculate_price_trend( + trend, diff_pct, trend_value = calculate_price_trend( current_interval_price=current_interval_price, future_average=future_average, threshold_rising=threshold_rising, threshold_falling=threshold_falling, + threshold_strongly_rising=threshold_strongly_rising, + threshold_strongly_falling=threshold_strongly_falling, volatility_adjustment=False, # Disable to simplify test ) # Difference: -5 - (-10) = 5 ct # Percentage: 5 / abs(-10) * 100 = +50% (correctly shows rising) + # With 5-level scale: +50% >= 20% strongly_rising threshold => strongly_rising assert diff_pct > 0, "Percentage should be positive (price rising toward zero)" assert diff_pct == pytest.approx(50.0, abs=0.1), "Should be +50%" - assert trend == "rising", "Trend should be 'rising' (above 10% threshold)" + assert trend == "strongly_rising", "Trend should be 'strongly_rising' (above strongly_rising threshold)" + assert trend_value == 2, "Trend value should be 2 for strongly_rising" def test_bug10_trend_diff_negative_falling_deeper() -> None: @@ -126,20 +132,26 @@ def test_bug10_trend_diff_negative_falling_deeper() -> None: future_average = -0.15 # -15 ct (more negative = cheaper) threshold_rising = 10.0 threshold_falling = -10.0 + threshold_strongly_rising = 20.0 + threshold_strongly_falling = -20.0 - trend, diff_pct = calculate_price_trend( + trend, diff_pct, trend_value = calculate_price_trend( current_interval_price=current_interval_price, future_average=future_average, threshold_rising=threshold_rising, threshold_falling=threshold_falling, + threshold_strongly_rising=threshold_strongly_rising, + threshold_strongly_falling=threshold_strongly_falling, volatility_adjustment=False, ) # Difference: -15 - (-10) = -5 ct # Percentage: -5 / abs(-10) * 100 = -50% (correctly shows falling) + # With 5-level scale: -50% <= -20% strongly_falling threshold => strongly_falling assert diff_pct < 0, "Percentage should be negative (price falling deeper)" assert diff_pct == pytest.approx(-50.0, abs=0.1), "Should be -50%" - assert trend == "falling", "Trend should be 'falling' (below -10% threshold)" + assert trend == "strongly_falling", "Trend should be 'strongly_falling' (below strongly_falling threshold)" + assert trend_value == -2, "Trend value should be -2 for strongly_falling" def test_bug10_trend_diff_zero_current_price() -> None: @@ -152,18 +164,23 @@ def test_bug10_trend_diff_zero_current_price() -> None: future_average = 0.05 threshold_rising = 10.0 threshold_falling = -10.0 + threshold_strongly_rising = 20.0 + threshold_strongly_falling = -20.0 - trend, diff_pct = calculate_price_trend( + trend, diff_pct, trend_value = calculate_price_trend( current_interval_price=current_interval_price, future_average=future_average, threshold_rising=threshold_rising, threshold_falling=threshold_falling, + threshold_strongly_rising=threshold_strongly_rising, + threshold_strongly_falling=threshold_strongly_falling, volatility_adjustment=False, ) # Edge case: current=0 → diff_pct should be 0.0 (avoid division by zero) assert diff_pct == 0.0, "Should return 0.0 to avoid division by zero" assert trend == "stable", "Should be stable when diff is 0%" + assert trend_value == 0, "Trend value should be 0 for stable" def test_bug10_trend_diff_positive_prices_unchanged() -> None: @@ -176,19 +193,25 @@ def test_bug10_trend_diff_positive_prices_unchanged() -> None: future_average = 0.15 # 15 ct (rising) threshold_rising = 10.0 threshold_falling = -10.0 + threshold_strongly_rising = 20.0 + threshold_strongly_falling = -20.0 - trend, diff_pct = calculate_price_trend( + trend, diff_pct, trend_value = calculate_price_trend( current_interval_price=current_interval_price, future_average=future_average, threshold_rising=threshold_rising, threshold_falling=threshold_falling, + threshold_strongly_rising=threshold_strongly_rising, + threshold_strongly_falling=threshold_strongly_falling, volatility_adjustment=False, ) # Difference: 15 - 10 = 5 ct # Percentage: 5 / 10 * 100 = +50% + # With 5-level scale: +50% >= 20% strongly_rising threshold => strongly_rising assert diff_pct == pytest.approx(50.0, abs=0.1), "Should be +50%" - assert trend == "rising", "Should be rising" + assert trend == "strongly_rising", "Should be strongly_rising (above strongly_rising threshold)" + assert trend_value == 2, "Trend value should be 2 for strongly_rising" def test_bug11_later_half_diff_calculation_note() -> None: