""" Supertrend Indicator State This module implements incremental Supertrend calculation that maintains constant memory usage and provides identical results to traditional batch calculations. Supertrend is used by the DefaultStrategy for trend detection. """ from typing import Dict, Union, Optional from .base import OHLCIndicatorState from .atr import ATRState class SupertrendState(OHLCIndicatorState): """ Incremental Supertrend calculation state. Supertrend is a trend-following indicator that uses Average True Range (ATR) to calculate dynamic support and resistance levels. It provides clear trend direction signals: +1 for uptrend, -1 for downtrend. The calculation involves: 1. Calculate ATR for the given period 2. Calculate basic upper and lower bands using ATR and multiplier 3. Calculate final upper and lower bands with trend logic 4. Determine trend direction based on price vs bands Attributes: period (int): ATR period for Supertrend calculation multiplier (float): Multiplier for ATR in band calculation atr_state (ATRState): ATR calculation state previous_close (float): Previous period's close price previous_trend (int): Previous trend direction (+1 or -1) final_upper_band (float): Current final upper band final_lower_band (float): Current final lower band Example: supertrend = SupertrendState(period=10, multiplier=3.0) # Add OHLC data incrementally ohlc = {'open': 100, 'high': 105, 'low': 98, 'close': 103} result = supertrend.update(ohlc) trend = result['trend'] # +1 or -1 supertrend_value = result['supertrend'] # Supertrend line value """ def __init__(self, period: int = 10, multiplier: float = 3.0): """ Initialize Supertrend state. Args: period: ATR period for Supertrend calculation (default: 10) multiplier: Multiplier for ATR in band calculation (default: 3.0) Raises: ValueError: If period is not positive or multiplier is not positive """ super().__init__(period) if multiplier <= 0: raise ValueError(f"Multiplier must be positive, got {multiplier}") self.multiplier = multiplier self.atr_state = ATRState(period) # State variables self.previous_close = None self.previous_trend = None # Don't assume initial trend, let first calculation determine it self.final_upper_band = None self.final_lower_band = None # Current values self.current_trend = None self.current_supertrend = None self.is_initialized = True def update(self, ohlc_data: Dict[str, float]) -> Dict[str, float]: """ Update Supertrend with new OHLC data. Args: ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys Returns: Dictionary with 'trend', 'supertrend', 'upper_band', 'lower_band' keys Raises: ValueError: If OHLC data is invalid TypeError: If ohlc_data is not a dictionary """ # Validate input if not isinstance(ohlc_data, dict): raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}") self.validate_input(ohlc_data) high = float(ohlc_data['high']) low = float(ohlc_data['low']) close = float(ohlc_data['close']) # Update ATR atr_value = self.atr_state.update(ohlc_data) # Calculate HL2 (typical price) hl2 = (high + low) / 2.0 # Calculate basic upper and lower bands basic_upper_band = hl2 + (self.multiplier * atr_value) basic_lower_band = hl2 - (self.multiplier * atr_value) # Calculate final upper band if self.final_upper_band is None or basic_upper_band < self.final_upper_band or self.previous_close > self.final_upper_band: final_upper_band = basic_upper_band else: final_upper_band = self.final_upper_band # Calculate final lower band if self.final_lower_band is None or basic_lower_band > self.final_lower_band or self.previous_close < self.final_lower_band: final_lower_band = basic_lower_band else: final_lower_band = self.final_lower_band # Determine trend if self.previous_close is None: # First calculation - match original logic # If close <= upper_band, trend is -1 (downtrend), else trend is 1 (uptrend) trend = -1 if close <= basic_upper_band else 1 else: # Trend logic for subsequent calculations if self.previous_trend == 1 and close <= final_lower_band: trend = -1 elif self.previous_trend == -1 and close >= final_upper_band: trend = 1 else: trend = self.previous_trend # Calculate Supertrend value if trend == 1: supertrend_value = final_lower_band else: supertrend_value = final_upper_band # Store current state self.previous_close = close self.previous_trend = trend self.final_upper_band = final_upper_band self.final_lower_band = final_lower_band self.current_trend = trend self.current_supertrend = supertrend_value self.values_received += 1 # Prepare result result = { 'trend': trend, 'supertrend': supertrend_value, 'upper_band': final_upper_band, 'lower_band': final_lower_band, 'atr': atr_value } self._current_values = result return result def is_warmed_up(self) -> bool: """ Check if Supertrend has enough data for reliable values. Returns: True if ATR state is warmed up """ return self.atr_state.is_warmed_up() def reset(self) -> None: """Reset Supertrend state to initial conditions.""" self.atr_state.reset() self.previous_close = None self.previous_trend = None self.final_upper_band = None self.final_lower_band = None self.current_trend = None self.current_supertrend = None self.values_received = 0 self._current_values = {} def get_current_value(self) -> Optional[Dict[str, float]]: """ Get current Supertrend values without updating. Returns: Dictionary with current Supertrend values, or None if not warmed up """ if not self.is_warmed_up(): return None return self._current_values.copy() if self._current_values else None def get_current_trend(self) -> int: """ Get current trend direction. Returns: Current trend (+1 for uptrend, -1 for downtrend, 0 if not warmed up) """ return self.current_trend if self.current_trend is not None else 0 def get_current_supertrend_value(self) -> Optional[float]: """ Get current Supertrend line value. Returns: Current Supertrend value, or None if not warmed up """ return self.current_supertrend def get_state_summary(self) -> dict: """Get detailed state summary for debugging.""" base_summary = super().get_state_summary() base_summary.update({ 'multiplier': self.multiplier, 'previous_close': self.previous_close, 'previous_trend': self.previous_trend, 'current_trend': self.current_trend, 'current_supertrend': self.current_supertrend, 'final_upper_band': self.final_upper_band, 'final_lower_band': self.final_lower_band, 'atr_state': self.atr_state.get_state_summary() }) return base_summary class SupertrendCollection: """ Collection of multiple Supertrend indicators for meta-trend calculation. This class manages multiple Supertrend indicators with different parameters and provides meta-trend calculation based on their agreement. """ def __init__(self, supertrend_configs: list): """ Initialize collection of Supertrend indicators. Args: supertrend_configs: List of (period, multiplier) tuples """ self.supertrends = [] self.configs = supertrend_configs for period, multiplier in supertrend_configs: supertrend = SupertrendState(period=period, multiplier=multiplier) self.supertrends.append(supertrend) def update(self, ohlc_data: Dict[str, float]) -> Dict[str, Union[int, list]]: """ Update all Supertrend indicators and calculate meta-trend. Args: ohlc_data: OHLC data dictionary Returns: Dictionary with 'meta_trend' and 'trends' keys """ trends = [] # Update each Supertrend and collect trends for supertrend in self.supertrends: result = supertrend.update(ohlc_data) trends.append(result['trend']) # Calculate meta-trend meta_trend = self.get_current_meta_trend() return { 'meta_trend': meta_trend, 'trends': trends } def is_warmed_up(self) -> bool: """Check if all Supertrend indicators are warmed up.""" return all(st.is_warmed_up() for st in self.supertrends) def reset(self) -> None: """Reset all Supertrend indicators.""" for supertrend in self.supertrends: supertrend.reset() def get_current_meta_trend(self) -> int: """ Calculate current meta-trend from all Supertrend indicators. Meta-trend logic: - If all trends agree, return that trend - If trends disagree, return 0 (neutral) Returns: Meta-trend value (1, -1, or 0) """ if not self.is_warmed_up(): return 0 trends = [st.get_current_trend() for st in self.supertrends] # Check if all trends agree if all(trend == trends[0] for trend in trends): return trends[0] # All agree: return the common trend else: return 0 # Neutral when trends disagree def get_state_summary(self) -> dict: """Get detailed state summary for all Supertrend indicators.""" return { 'configs': self.configs, 'meta_trend': self.get_current_meta_trend(), 'is_warmed_up': self.is_warmed_up(), 'supertrends': [st.get_state_summary() for st in self.supertrends] }