""" 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 initialized """ 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 available """ 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 with different parameters. This class manages multiple Supertrend indicators and provides meta-trend calculation based on agreement between different Supertrend configurations. Used by the DefaultStrategy for robust trend detection. Example: # Create collection with three Supertrend indicators collection = SupertrendCollection([ (10, 3.0), # period=10, multiplier=3.0 (11, 2.0), # period=11, multiplier=2.0 (12, 1.0) # period=12, multiplier=1.0 ]) # Update all indicators results = collection.update(ohlc_data) meta_trend = results['meta_trend'] # 1, -1, or 0 (neutral) """ def __init__(self, supertrend_configs: list): """ Initialize Supertrend collection. Args: supertrend_configs: List of (period, multiplier) tuples """ self.supertrends = [] for period, multiplier in supertrend_configs: self.supertrends.append(SupertrendState(period, multiplier)) self.values_received = 0 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 individual trends and meta-trend """ trends = [] results = [] # Update each Supertrend for supertrend in self.supertrends: result = supertrend.update(ohlc_data) trends.append(result['trend']) results.append(result) # Calculate meta-trend: all must agree for directional signal if all(trend == trends[0] for trend in trends): meta_trend = trends[0] # All agree else: meta_trend = 0 # Neutral when trends don't agree self.values_received += 1 return { 'trends': trends, 'meta_trend': meta_trend, 'results': results } 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() self.values_received = 0 def get_current_meta_trend(self) -> int: """ Get current meta-trend without updating. Returns: Current meta-trend: +1, -1, or 0 """ if not self.is_warmed_up(): return 0 trends = [st.get_current_trend() for st in self.supertrends] if all(trend == trends[0] for trend in trends): return trends[0] else: return 0 def get_state_summary(self) -> dict: """Get detailed state summary for all Supertrends.""" return { 'num_supertrends': len(self.supertrends), 'values_received': self.values_received, 'is_warmed_up': self.is_warmed_up(), 'current_meta_trend': self.get_current_meta_trend(), 'supertrends': [st.get_state_summary() for st in self.supertrends] }