""" Technical indicator calculation engine """ import logging import pandas as pd import numpy as np from ta import trend, momentum, volatility, volume from .config import config logger = logging.getLogger(__name__) class TechnicalIndicators: """Calculate technical indicators for market analysis""" @staticmethod def add_all_indicators(df: pd.DataFrame) -> pd.DataFrame: """ Add all technical indicators to DataFrame Args: df: DataFrame with OHLCV data Returns: DataFrame with all indicators added """ # Minimum data needed for indicators is ~60 candles # (based on EMA_SLOW=50 + some buffer) MIN_CANDLES = 60 if df.empty or len(df) < MIN_CANDLES: logger.warning(f"Insufficient data for indicators: {len(df)} candles (min: {MIN_CANDLES})") return df df = df.copy() # Trend indicators df = TechnicalIndicators.add_trend_indicators(df) # Momentum indicators df = TechnicalIndicators.add_momentum_indicators(df) # Volatility indicators df = TechnicalIndicators.add_volatility_indicators(df) # Volume indicators df = TechnicalIndicators.add_volume_indicators(df) return df @staticmethod def add_trend_indicators(df: pd.DataFrame) -> pd.DataFrame: """Add trend-following indicators""" # EMAs df[f'ema_{config.EMA_FAST}'] = trend.EMAIndicator( df['close'], window=config.EMA_FAST ).ema_indicator() df[f'ema_{config.EMA_SLOW}'] = trend.EMAIndicator( df['close'], window=config.EMA_SLOW ).ema_indicator() # MACD macd = trend.MACD( df['close'], window_slow=config.MACD_SLOW, window_fast=config.MACD_FAST, window_sign=config.MACD_SIGNAL ) df['macd'] = macd.macd() df['macd_signal'] = macd.macd_signal() df['macd_hist'] = macd.macd_diff() # ADX (trend strength) adx = trend.ADXIndicator( df['high'], df['low'], df['close'], window=config.ADX_PERIOD ) df['adx'] = adx.adx() df['dmp'] = adx.adx_pos() # +DI df['dmn'] = adx.adx_neg() # -DI return df @staticmethod def add_momentum_indicators(df: pd.DataFrame) -> pd.DataFrame: """Add momentum indicators""" # RSI df['rsi'] = momentum.RSIIndicator( df['close'], window=config.RSI_PERIOD ).rsi() # Stochastic stoch = momentum.StochasticOscillator( df['high'], df['low'], df['close'], window=14, smooth_window=3 ) df['stoch_k'] = stoch.stoch() df['stoch_d'] = stoch.stoch_signal() # Williams %R df['willr'] = momentum.WilliamsRIndicator( df['high'], df['low'], df['close'], lbp=14 ).williams_r() return df @staticmethod def add_volatility_indicators(df: pd.DataFrame) -> pd.DataFrame: """Add volatility indicators""" # ATR (Average True Range) df['atr'] = volatility.AverageTrueRange( df['high'], df['low'], df['close'], window=config.ATR_PERIOD ).average_true_range() # Bollinger Bands bbands = volatility.BollingerBands( df['close'], window=config.BB_PERIOD, window_dev=config.BB_STD ) df['bb_upper'] = bbands.bollinger_hband() df['bb_middle'] = bbands.bollinger_mavg() df['bb_lower'] = bbands.bollinger_lband() df['bb_width'] = bbands.bollinger_wband() # Historical Volatility (20-period) df['hist_vol'] = df['close'].pct_change().rolling(20).std() * np.sqrt(24 * 365) * 100 return df @staticmethod def add_volume_indicators(df: pd.DataFrame) -> pd.DataFrame: """Add volume-based indicators""" # Volume SMA df['volume_ma'] = df['volume'].rolling(window=config.VOLUME_MA_PERIOD).mean() # Volume ratio df['volume_ratio'] = df['volume'] / df['volume_ma'] # OBV (On-Balance Volume) df['obv'] = volume.OnBalanceVolumeIndicator( df['close'], df['volume'] ).on_balance_volume() # VWAP (Volume Weighted Average Price) - for intraday if 'quote_volume' in df.columns: df['vwap'] = (df['quote_volume'].cumsum() / df['volume'].cumsum()) return df @staticmethod def calculate_price_changes(df: pd.DataFrame) -> dict: """ Calculate price changes over various periods Returns: Dict with price change percentages """ if df.empty: return {} latest_close = df['close'].iloc[-1] changes = {} for periods, label in [(1, '1candle'), (5, '5candles'), (20, '20candles')]: if len(df) > periods: old_close = df['close'].iloc[-periods - 1] change_pct = ((latest_close - old_close) / old_close) * 100 changes[label] = round(change_pct, 2) return changes @staticmethod def get_latest_indicators(df: pd.DataFrame) -> dict: """ Extract latest indicator values for analysis Returns: Dict with latest indicator values """ if df.empty: return {} latest = df.iloc[-1] indicators = { # Trend 'ema_20': round(latest.get(f'ema_{config.EMA_FAST}', 0), 2), 'ema_50': round(latest.get(f'ema_{config.EMA_SLOW}', 0), 2), 'macd': round(latest.get('macd', 0), 4), 'macd_signal': round(latest.get('macd_signal', 0), 4), 'macd_hist': round(latest.get('macd_hist', 0), 4), 'adx': round(latest.get('adx', 0), 1), # Momentum 'rsi': round(latest.get('rsi', 0), 1), 'stoch_k': round(latest.get('stoch_k', 0), 1), 'willr': round(latest.get('willr', 0), 1), # Volatility 'atr': round(latest.get('atr', 0), 2), 'bb_upper': round(latest.get('bb_upper', 0), 2), 'bb_lower': round(latest.get('bb_lower', 0), 2), 'bb_width': round(latest.get('bb_width', 0), 4), 'hist_vol': round(latest.get('hist_vol', 0), 2), # Volume 'volume_ratio': round(latest.get('volume_ratio', 0), 2), 'obv': int(latest.get('obv', 0)), # Price 'close': round(latest['close'], 2), 'high': round(latest['high'], 2), 'low': round(latest['low'], 2), } return indicators