- Updated all technical indicators to return pandas DataFrames instead of lists, improving consistency and usability. - Modified the `calculate` method in `TechnicalIndicators` to directly return DataFrames with relevant indicator values. - Enhanced the `data_integration.py` to utilize the new DataFrame outputs for better integration with charting. - Updated documentation to reflect the new DataFrame-centric approach, including usage examples and output structures. - Improved error handling to ensure empty DataFrames are returned when insufficient data is available. These changes streamline the indicator calculations and improve the overall architecture, aligning with project standards for maintainability and performance.
270 lines
10 KiB
Python
270 lines
10 KiB
Python
"""
|
|
Technical Indicators Module for OHLCV Data
|
|
|
|
This module provides technical indicator calculations optimized for sparse OHLCV data
|
|
as produced by the TCP Trading Platform's aggregation strategy.
|
|
|
|
IMPORTANT: Handles Sparse Data
|
|
- Missing candles (time gaps) are normal in this system
|
|
- Indicators properly handle gaps without interpolation
|
|
- Uses pandas for efficient vectorized calculations
|
|
- Follows right-aligned timestamp convention
|
|
|
|
TODO: need make more procedural without hardcoding indicators type and so on
|
|
|
|
"""
|
|
|
|
from typing import Dict, List, Optional, Any
|
|
import pandas as pd
|
|
|
|
from ..data_types import OHLCVCandle
|
|
from .implementations import (
|
|
SMAIndicator,
|
|
EMAIndicator,
|
|
RSIIndicator,
|
|
MACDIndicator,
|
|
BollingerBandsIndicator
|
|
)
|
|
|
|
|
|
class TechnicalIndicators:
|
|
"""
|
|
Technical indicator calculator for OHLCV candle data.
|
|
|
|
This class provides vectorized technical indicator calculations
|
|
designed to handle sparse data efficiently. All calculations use
|
|
pandas for performance and handle missing data appropriately.
|
|
|
|
SPARSE DATA HANDLING:
|
|
- Gaps in timestamps are preserved (no interpolation)
|
|
- Indicators calculate only on available data points
|
|
- Periods with insufficient data return NaN
|
|
- Results maintain original timestamp alignment
|
|
"""
|
|
|
|
def __init__(self, logger=None):
|
|
"""
|
|
Initialize technical indicators calculator.
|
|
|
|
Args:
|
|
logger: Optional logger instance
|
|
"""
|
|
self.logger = logger
|
|
|
|
# Initialize individual indicator calculators
|
|
self._sma = SMAIndicator(logger)
|
|
self._ema = EMAIndicator(logger)
|
|
self._rsi = RSIIndicator(logger)
|
|
self._macd = MACDIndicator(logger)
|
|
self._bollinger = BollingerBandsIndicator(logger)
|
|
|
|
if self.logger:
|
|
self.logger.info("TechnicalIndicators: Initialized indicator calculator")
|
|
|
|
def _prepare_dataframe_from_list(self, candles: List[OHLCVCandle]) -> pd.DataFrame:
|
|
"""
|
|
Convert OHLCV candles to pandas DataFrame for efficient calculations.
|
|
|
|
Args:
|
|
candles: List of OHLCV candles (can be sparse)
|
|
|
|
Returns:
|
|
DataFrame with OHLCV data, sorted by timestamp
|
|
"""
|
|
if not candles:
|
|
return pd.DataFrame()
|
|
|
|
return self._sma.prepare_dataframe(candles)
|
|
|
|
def sma(self, df: pd.DataFrame, period: int,
|
|
price_column: str = 'close') -> pd.DataFrame:
|
|
"""
|
|
Calculate Simple Moving Average (SMA).
|
|
|
|
Args:
|
|
df: DataFrame with OHLCV data
|
|
period: Number of periods for moving average
|
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
|
|
|
Returns:
|
|
DataFrame with SMA values
|
|
"""
|
|
return self._sma.calculate(df, period=period, price_column=price_column)
|
|
|
|
def ema(self, df: pd.DataFrame, period: int,
|
|
price_column: str = 'close') -> pd.DataFrame:
|
|
"""
|
|
Calculate Exponential Moving Average (EMA).
|
|
|
|
Args:
|
|
df: DataFrame with OHLCV data
|
|
period: Number of periods for moving average
|
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
|
|
|
Returns:
|
|
List of indicator results with EMA values
|
|
"""
|
|
return self._ema.calculate(df, period=period, price_column=price_column)
|
|
|
|
def rsi(self, df: pd.DataFrame, period: int = 14,
|
|
price_column: str = 'close') -> pd.DataFrame:
|
|
"""
|
|
Calculate Relative Strength Index (RSI).
|
|
|
|
Args:
|
|
df: DataFrame with OHLCV data
|
|
period: Number of periods for RSI calculation (default 14)
|
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
|
|
|
Returns:
|
|
DataFrame with RSI values
|
|
"""
|
|
return self._rsi.calculate(df, period=period, price_column=price_column)
|
|
|
|
def macd(self, df: pd.DataFrame,
|
|
fast_period: int = 12, slow_period: int = 26, signal_period: int = 9,
|
|
price_column: str = 'close') -> pd.DataFrame:
|
|
"""
|
|
Calculate Moving Average Convergence Divergence (MACD).
|
|
|
|
Args:
|
|
df: DataFrame with OHLCV data
|
|
fast_period: Fast EMA period (default 12)
|
|
slow_period: Slow EMA period (default 26)
|
|
signal_period: Signal line EMA period (default 9)
|
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
|
|
|
Returns:
|
|
List of indicator results with MACD, signal, and histogram values
|
|
"""
|
|
return self._macd.calculate(
|
|
df,
|
|
fast_period=fast_period,
|
|
slow_period=slow_period,
|
|
signal_period=signal_period,
|
|
price_column=price_column
|
|
)
|
|
|
|
def bollinger_bands(self, df: pd.DataFrame, period: int = 20,
|
|
std_dev: float = 2.0, price_column: str = 'close') -> pd.DataFrame:
|
|
"""
|
|
Calculate Bollinger Bands.
|
|
|
|
Args:
|
|
df: DataFrame with OHLCV data
|
|
period: Number of periods for moving average (default 20)
|
|
std_dev: Number of standard deviations (default 2.0)
|
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
|
|
|
Returns:
|
|
DataFrame with upper band, middle band (SMA), and lower band
|
|
"""
|
|
return self._bollinger.calculate(
|
|
df,
|
|
period=period,
|
|
std_dev=std_dev,
|
|
price_column=price_column
|
|
)
|
|
|
|
def calculate_multiple_indicators(self, df: pd.DataFrame,
|
|
indicators_config: Dict[str, Dict[str, Any]]) -> Dict[str, pd.DataFrame]:
|
|
"""
|
|
Calculate multiple indicators at once for efficiency.
|
|
|
|
Args:
|
|
df: DataFrame with OHLCV data
|
|
indicators_config: Configuration for indicators to calculate
|
|
Example: {
|
|
'sma_20': {'type': 'sma', 'period': 20},
|
|
'ema_12': {'type': 'ema', 'period': 12},
|
|
'rsi_14': {'type': 'rsi', 'period': 14},
|
|
'macd': {'type': 'macd'},
|
|
'bb_20': {'type': 'bollinger_bands', 'period': 20}
|
|
}
|
|
|
|
Returns:
|
|
Dictionary mapping indicator names to their results as DataFrames
|
|
"""
|
|
results = {}
|
|
|
|
for indicator_name, config in indicators_config.items():
|
|
indicator_type = config.get('type')
|
|
|
|
try:
|
|
if indicator_type == 'sma':
|
|
period = config.get('period', 20)
|
|
price_column = config.get('price_column', 'close')
|
|
results[indicator_name] = self.sma(df, period, price_column)
|
|
|
|
elif indicator_type == 'ema':
|
|
period = config.get('period', 20)
|
|
price_column = config.get('price_column', 'close')
|
|
results[indicator_name] = self.ema(df, period, price_column)
|
|
|
|
elif indicator_type == 'rsi':
|
|
period = config.get('period', 14)
|
|
price_column = config.get('price_column', 'close')
|
|
results[indicator_name] = self.rsi(df, period, price_column)
|
|
|
|
elif indicator_type == 'macd':
|
|
fast_period = config.get('fast_period', 12)
|
|
slow_period = config.get('slow_period', 26)
|
|
signal_period = config.get('signal_period', 9)
|
|
price_column = config.get('price_column', 'close')
|
|
results[indicator_name] = self.macd(
|
|
df, fast_period, slow_period, signal_period, price_column
|
|
)
|
|
|
|
elif indicator_type == 'bollinger_bands':
|
|
period = config.get('period', 20)
|
|
std_dev = config.get('std_dev', 2.0)
|
|
price_column = config.get('price_column', 'close')
|
|
results[indicator_name] = self.bollinger_bands(
|
|
df, period, std_dev, price_column
|
|
)
|
|
|
|
else:
|
|
if self.logger:
|
|
self.logger.warning(f"Unknown indicator type: {indicator_type}")
|
|
results[indicator_name] = pd.DataFrame()
|
|
|
|
except Exception as e:
|
|
if self.logger:
|
|
self.logger.error(f"Error calculating {indicator_name}: {e}")
|
|
results[indicator_name] = pd.DataFrame()
|
|
|
|
return results
|
|
|
|
def calculate(self, indicator_type: str, df: pd.DataFrame, **kwargs) -> Optional[pd.DataFrame]:
|
|
"""
|
|
Calculate a single indicator with dynamic dispatch.
|
|
|
|
Args:
|
|
indicator_type: Name of the indicator (e.g., 'sma', 'ema')
|
|
df: DataFrame with OHLCV data
|
|
**kwargs: Indicator-specific parameters (e.g., period=20)
|
|
|
|
Returns:
|
|
DataFrame with indicator values, or None if the type is unknown or calculation fails.
|
|
"""
|
|
# Get the indicator calculation method
|
|
indicator_method = getattr(self, indicator_type, None)
|
|
if not indicator_method:
|
|
if self.logger:
|
|
self.logger.error(f"Unknown indicator type '{indicator_type}'")
|
|
return None
|
|
|
|
try:
|
|
if df.empty:
|
|
return pd.DataFrame()
|
|
|
|
# Call the indicator method (now returns DataFrame)
|
|
result_df = indicator_method(df, **kwargs)
|
|
|
|
# Return the DataFrame directly
|
|
return result_df
|
|
|
|
except Exception as e:
|
|
if self.logger:
|
|
self.logger.error(f"Error calculating {indicator_type}: {e}")
|
|
return None |