indicators refactor
This commit is contained in:
20
data/common/indicators/implementations/__init__.py
Normal file
20
data/common/indicators/implementations/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Technical indicator implementations package.
|
||||
|
||||
This package contains individual implementations of technical indicators,
|
||||
each in its own module for better maintainability and separation of concerns.
|
||||
"""
|
||||
|
||||
from .sma import SMAIndicator
|
||||
from .ema import EMAIndicator
|
||||
from .rsi import RSIIndicator
|
||||
from .macd import MACDIndicator
|
||||
from .bollinger import BollingerBandsIndicator
|
||||
|
||||
__all__ = [
|
||||
'SMAIndicator',
|
||||
'EMAIndicator',
|
||||
'RSIIndicator',
|
||||
'MACDIndicator',
|
||||
'BollingerBandsIndicator'
|
||||
]
|
||||
81
data/common/indicators/implementations/bollinger.py
Normal file
81
data/common/indicators/implementations/bollinger.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
Bollinger Bands indicator implementation.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
import pandas as pd
|
||||
|
||||
from ..base import BaseIndicator
|
||||
from ..result import IndicatorResult
|
||||
|
||||
|
||||
class BollingerBandsIndicator(BaseIndicator):
|
||||
"""
|
||||
Bollinger Bands technical indicator.
|
||||
|
||||
Calculates a set of lines plotted two standard deviations away from a simple moving average.
|
||||
Handles sparse data appropriately without interpolation.
|
||||
"""
|
||||
|
||||
def calculate(self, df: pd.DataFrame, period: int = 20,
|
||||
std_dev: float = 2.0, price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
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:
|
||||
List of indicator results with upper band, middle band (SMA), and lower band
|
||||
"""
|
||||
# Validate input data
|
||||
if not self.validate_dataframe(df, period):
|
||||
return []
|
||||
|
||||
try:
|
||||
# Calculate middle band (SMA)
|
||||
df['middle_band'] = df[price_column].rolling(window=period, min_periods=period).mean()
|
||||
|
||||
# Calculate standard deviation
|
||||
df['std'] = df[price_column].rolling(window=period, min_periods=period).std()
|
||||
|
||||
# Calculate upper and lower bands
|
||||
df['upper_band'] = df['middle_band'] + (std_dev * df['std'])
|
||||
df['lower_band'] = df['middle_band'] - (std_dev * df['std'])
|
||||
|
||||
# Calculate bandwidth and %B
|
||||
df['bandwidth'] = (df['upper_band'] - df['lower_band']) / df['middle_band']
|
||||
df['percent_b'] = (df[price_column] - df['lower_band']) / (df['upper_band'] - df['lower_band'])
|
||||
|
||||
# Convert results to IndicatorResult objects
|
||||
results = []
|
||||
for timestamp, row in df.iterrows():
|
||||
if not pd.isna(row['middle_band']):
|
||||
result = IndicatorResult(
|
||||
timestamp=timestamp,
|
||||
symbol=row['symbol'],
|
||||
timeframe=row['timeframe'],
|
||||
values={
|
||||
'upper_band': row['upper_band'],
|
||||
'middle_band': row['middle_band'],
|
||||
'lower_band': row['lower_band'],
|
||||
'bandwidth': row['bandwidth'],
|
||||
'percent_b': row['percent_b']
|
||||
},
|
||||
metadata={
|
||||
'period': period,
|
||||
'std_dev': std_dev,
|
||||
'price_column': price_column
|
||||
}
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"Error calculating Bollinger Bands: {e}")
|
||||
return []
|
||||
60
data/common/indicators/implementations/ema.py
Normal file
60
data/common/indicators/implementations/ema.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Exponential Moving Average (EMA) indicator implementation.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
import pandas as pd
|
||||
|
||||
from ..base import BaseIndicator
|
||||
from ..result import IndicatorResult
|
||||
|
||||
|
||||
class EMAIndicator(BaseIndicator):
|
||||
"""
|
||||
Exponential Moving Average (EMA) technical indicator.
|
||||
|
||||
Calculates weighted moving average giving more weight to recent prices.
|
||||
Handles sparse data appropriately without interpolation.
|
||||
"""
|
||||
|
||||
def calculate(self, df: pd.DataFrame, period: int = 20,
|
||||
price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
Calculate Exponential Moving Average (EMA).
|
||||
|
||||
Args:
|
||||
df: DataFrame with OHLCV data
|
||||
period: Number of periods for moving average (default: 20)
|
||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||
|
||||
Returns:
|
||||
List of indicator results with EMA values
|
||||
"""
|
||||
# Validate input data
|
||||
if not self.validate_dataframe(df, period):
|
||||
return []
|
||||
|
||||
try:
|
||||
# Calculate EMA using pandas exponential weighted moving average
|
||||
df['ema'] = df[price_column].ewm(span=period, adjust=False).mean()
|
||||
|
||||
# Convert results to IndicatorResult objects
|
||||
results = []
|
||||
for i, (timestamp, row) in enumerate(df.iterrows()):
|
||||
# Only return results after minimum period
|
||||
if i >= period - 1 and not pd.isna(row['ema']):
|
||||
result = IndicatorResult(
|
||||
timestamp=timestamp,
|
||||
symbol=row['symbol'],
|
||||
timeframe=row['timeframe'],
|
||||
values={'ema': row['ema']},
|
||||
metadata={'period': period, 'price_column': price_column}
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"Error calculating EMA: {e}")
|
||||
return []
|
||||
84
data/common/indicators/implementations/macd.py
Normal file
84
data/common/indicators/implementations/macd.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
Moving Average Convergence Divergence (MACD) indicator implementation.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
import pandas as pd
|
||||
|
||||
from ..base import BaseIndicator
|
||||
from ..result import IndicatorResult
|
||||
|
||||
|
||||
class MACDIndicator(BaseIndicator):
|
||||
"""
|
||||
Moving Average Convergence Divergence (MACD) technical indicator.
|
||||
|
||||
Calculates trend-following momentum indicator that shows the relationship
|
||||
between two moving averages of a security's price.
|
||||
Handles sparse data appropriately without interpolation.
|
||||
"""
|
||||
|
||||
def calculate(self, df: pd.DataFrame, fast_period: int = 12,
|
||||
slow_period: int = 26, signal_period: int = 9,
|
||||
price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
# Validate input data
|
||||
if not self.validate_dataframe(df, slow_period):
|
||||
return []
|
||||
|
||||
try:
|
||||
# Calculate fast and slow EMAs
|
||||
df['ema_fast'] = df[price_column].ewm(span=fast_period, adjust=False).mean()
|
||||
df['ema_slow'] = df[price_column].ewm(span=slow_period, adjust=False).mean()
|
||||
|
||||
# Calculate MACD line
|
||||
df['macd'] = df['ema_fast'] - df['ema_slow']
|
||||
|
||||
# Calculate signal line (EMA of MACD)
|
||||
df['signal'] = df['macd'].ewm(span=signal_period, adjust=False).mean()
|
||||
|
||||
# Calculate histogram
|
||||
df['histogram'] = df['macd'] - df['signal']
|
||||
|
||||
# Convert results to IndicatorResult objects
|
||||
results = []
|
||||
for i, (timestamp, row) in enumerate(df.iterrows()):
|
||||
# Only return results after minimum period
|
||||
if i >= slow_period - 1:
|
||||
if not (pd.isna(row['macd']) or pd.isna(row['signal']) or pd.isna(row['histogram'])):
|
||||
result = IndicatorResult(
|
||||
timestamp=timestamp,
|
||||
symbol=row['symbol'],
|
||||
timeframe=row['timeframe'],
|
||||
values={
|
||||
'macd': row['macd'],
|
||||
'signal': row['signal'],
|
||||
'histogram': row['histogram']
|
||||
},
|
||||
metadata={
|
||||
'fast_period': fast_period,
|
||||
'slow_period': slow_period,
|
||||
'signal_period': signal_period,
|
||||
'price_column': price_column
|
||||
}
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"Error calculating MACD: {e}")
|
||||
return []
|
||||
75
data/common/indicators/implementations/rsi.py
Normal file
75
data/common/indicators/implementations/rsi.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
Relative Strength Index (RSI) indicator implementation.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
import pandas as pd
|
||||
|
||||
from ..base import BaseIndicator
|
||||
from ..result import IndicatorResult
|
||||
|
||||
|
||||
class RSIIndicator(BaseIndicator):
|
||||
"""
|
||||
Relative Strength Index (RSI) technical indicator.
|
||||
|
||||
Measures momentum by comparing the magnitude of recent gains to recent losses.
|
||||
Handles sparse data appropriately without interpolation.
|
||||
"""
|
||||
|
||||
def calculate(self, df: pd.DataFrame, period: int = 14,
|
||||
price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
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:
|
||||
List of indicator results with RSI values
|
||||
"""
|
||||
# Validate input data
|
||||
if not self.validate_dataframe(df, period + 1): # Need extra period for diff
|
||||
return []
|
||||
|
||||
try:
|
||||
# Calculate price changes
|
||||
df['price_change'] = df[price_column].diff()
|
||||
|
||||
# Separate gains and losses
|
||||
df['gain'] = df['price_change'].where(df['price_change'] > 0, 0)
|
||||
df['loss'] = (-df['price_change']).where(df['price_change'] < 0, 0)
|
||||
|
||||
# Calculate average gain and loss using EMA
|
||||
df['avg_gain'] = df['gain'].ewm(span=period, adjust=False).mean()
|
||||
df['avg_loss'] = df['loss'].ewm(span=period, adjust=False).mean()
|
||||
|
||||
# Calculate RS and RSI
|
||||
df['rs'] = df['avg_gain'] / df['avg_loss']
|
||||
df['rsi'] = 100 - (100 / (1 + df['rs']))
|
||||
|
||||
# Handle division by zero
|
||||
df['rsi'] = df['rsi'].fillna(50) # Neutral RSI when no losses
|
||||
|
||||
# Convert results to IndicatorResult objects
|
||||
results = []
|
||||
for i, (timestamp, row) in enumerate(df.iterrows()):
|
||||
# Only return results after minimum period
|
||||
if i >= period and not pd.isna(row['rsi']):
|
||||
result = IndicatorResult(
|
||||
timestamp=timestamp,
|
||||
symbol=row['symbol'],
|
||||
timeframe=row['timeframe'],
|
||||
values={'rsi': row['rsi']},
|
||||
metadata={'period': period, 'price_column': price_column}
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"Error calculating RSI: {e}")
|
||||
return []
|
||||
59
data/common/indicators/implementations/sma.py
Normal file
59
data/common/indicators/implementations/sma.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Simple Moving Average (SMA) indicator implementation.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
import pandas as pd
|
||||
|
||||
from ..base import BaseIndicator
|
||||
from ..result import IndicatorResult
|
||||
|
||||
|
||||
class SMAIndicator(BaseIndicator):
|
||||
"""
|
||||
Simple Moving Average (SMA) technical indicator.
|
||||
|
||||
Calculates the unweighted mean of previous n periods.
|
||||
Handles sparse data appropriately without interpolation.
|
||||
"""
|
||||
|
||||
def calculate(self, df: pd.DataFrame, period: int = 20,
|
||||
price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
Calculate Simple Moving Average (SMA).
|
||||
|
||||
Args:
|
||||
df: DataFrame with OHLCV data
|
||||
period: Number of periods for moving average (default: 20)
|
||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||
|
||||
Returns:
|
||||
List of indicator results with SMA values
|
||||
"""
|
||||
# Validate input data
|
||||
if not self.validate_dataframe(df, period):
|
||||
return []
|
||||
|
||||
try:
|
||||
# Calculate SMA using pandas rolling window
|
||||
df['sma'] = df[price_column].rolling(window=period, min_periods=period).mean()
|
||||
|
||||
# Convert results to IndicatorResult objects
|
||||
results = []
|
||||
for timestamp, row in df.iterrows():
|
||||
if not pd.isna(row['sma']):
|
||||
result = IndicatorResult(
|
||||
timestamp=timestamp,
|
||||
symbol=row['symbol'],
|
||||
timeframe=row['timeframe'],
|
||||
values={'sma': row['sma']},
|
||||
metadata={'period': period, 'price_column': price_column}
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"Error calculating SMA: {e}")
|
||||
return []
|
||||
Reference in New Issue
Block a user