tradusai/analysis/indicators.py
2025-12-02 22:54:03 +08:00

225 lines
6.6 KiB
Python

"""
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