- Introduced dynamic generation of parameter fields and callback handling for indicators, enhancing modularity and maintainability. - Updated `config_utils.py` with new utility functions to load indicator templates and generate dynamic outputs and states for parameter fields. - Refactored `indicators.py` to utilize these utilities, streamlining the callback logic and improving user experience by reducing hardcoded elements. - Modified `indicator_modal.py` to create parameter fields dynamically based on JSON templates, eliminating the need for manual updates when adding new indicators. - Added documentation outlining the new data-driven architecture for indicators, improving clarity and guidance for future development. These changes significantly enhance the flexibility and scalability of the indicator system, aligning with project goals for maintainability and performance.
12 KiB
Technical Indicators Module
Overview
The Technical Indicators module provides a vectorized, DataFrame-centric system for calculating technical analysis indicators. It is designed to handle sparse OHLCV data efficiently using pandas for high-performance calculations, making it ideal for real-time trading applications and chart visualization.
Key Features
- DataFrame-Centric Design: All indicators return pandas DataFrames with timestamp index for easy alignment and plotting
- Vectorized Calculations: Leverages pandas and numpy for high-speed computation
- Clean Output: Returns only relevant indicator columns (e.g.,
'sma','ema','rsi') with timestamp index - Safe Trading: Proper warm-up periods ensure no early/invalid values are returned
- Gap Handling: Maintains timestamp alignment without interpolation for trading integrity
- Modular Architecture: Clear separation between calculation logic and result formatting
Architecture
Package Structure
data/common/indicators/
├── __init__.py # Package exports
├── technical.py # Main facade class
├── base.py # Base indicator class
├── result.py # Result container class (legacy)
├── utils.py # Utility functions
└── implementations/ # Individual indicator implementations
├── __init__.py
├── sma.py # Simple Moving Average
├── ema.py # Exponential Moving Average
├── rsi.py # Relative Strength Index
├── macd.py # MACD
└── bollinger.py # Bollinger Bands
Key Components
1. Base Classes
- BaseIndicator: Abstract base class providing common functionality
- Data preparation with timestamp handling
- Validation and error handling
- Logging support
2. Individual Indicators
Each indicator is implemented as a separate class inheriting from BaseIndicator:
- Vectorized calculations using pandas operations
- Clean DataFrame output with only relevant columns
- Proper warm-up periods for safe trading
- Independent testing and maintenance
3. TechnicalIndicators Facade
Main entry point providing:
- Unified DataFrame-based interface
- Batch calculations
- Consistent error handling
- Data preparation utilities
Supported Indicators
Simple Moving Average (SMA)
from data.common.indicators import TechnicalIndicators
indicators = TechnicalIndicators()
result_df = indicators.sma(df, period=20, price_column='close')
# Returns DataFrame with columns: ['sma'], indexed by timestamp
- Parameters:
period: Number of periods (default: 20)price_column: Column to average (default: 'close')
- Returns: DataFrame with
'sma'column, indexed by timestamp - Warm-up: First
period-1values are excluded for safety
Exponential Moving Average (EMA)
result_df = indicators.ema(df, period=12, price_column='close')
# Returns DataFrame with columns: ['ema'], indexed by timestamp
- Parameters:
period: Number of periods (default: 20)price_column: Column to average (default: 'close')
- Returns: DataFrame with
'ema'column, indexed by timestamp - Warm-up: First
period-1values are excluded for safety
Relative Strength Index (RSI)
result_df = indicators.rsi(df, period=14, price_column='close')
# Returns DataFrame with columns: ['rsi'], indexed by timestamp
- Parameters:
period: Number of periods (default: 14)price_column: Column to analyze (default: 'close')
- Returns: DataFrame with
'rsi'column, indexed by timestamp - Warm-up: First
period-1values are excluded for safety
Moving Average Convergence Divergence (MACD)
result_df = indicators.macd(
df,
fast_period=12,
slow_period=26,
signal_period=9,
price_column='close'
)
# Returns DataFrame with columns: ['macd', 'signal', 'histogram'], indexed by timestamp
- Parameters:
fast_period: Fast EMA period (default: 12)slow_period: Slow EMA period (default: 26)signal_period: Signal line period (default: 9)price_column: Column to analyze (default: 'close')
- Returns: DataFrame with
'macd','signal','histogram'columns, indexed by timestamp - Warm-up: First
max(slow_period, signal_period)-1values are excluded for safety
Bollinger Bands
result_df = indicators.bollinger_bands(
df,
period=20,
std_dev=2.0,
price_column='close'
)
# Returns DataFrame with columns: ['upper_band', 'middle_band', 'lower_band'], indexed by timestamp
- Parameters:
period: SMA period (default: 20)std_dev: Standard deviation multiplier (default: 2.0)price_column: Column to analyze (default: 'close')
- Returns: DataFrame with
'upper_band','middle_band','lower_band'columns, indexed by timestamp - Warm-up: First
period-1values are excluded for safety
Usage Examples
Basic Usage with DataFrame Output
from data.common.indicators import TechnicalIndicators
# Initialize calculator
indicators = TechnicalIndicators(logger=my_logger)
# Calculate single indicator - returns DataFrame
sma_df = indicators.sma(df, period=20)
# Access results using DataFrame operations
print(f"First SMA value: {sma_df['sma'].iloc[0]}")
print(f"Latest SMA value: {sma_df['sma'].iloc[-1]}")
print(f"All SMA values: {sma_df['sma'].tolist()}")
# Plotting integration
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(
x=sma_df.index,
y=sma_df['sma'],
name='SMA 20',
line=dict(color='blue')
))
Using the Dynamic calculate Method
# Calculate any indicator by type name
rsi_df = indicators.calculate('rsi', df, period=14)
if rsi_df is not None and not rsi_df.empty:
print(f"RSI range: {rsi_df['rsi'].min():.2f} - {rsi_df['rsi'].max():.2f}")
# MACD with custom parameters
macd_df = indicators.calculate('macd', df, fast_period=10, slow_period=30, signal_period=8)
if macd_df is not None and not macd_df.empty:
print(f"MACD signal line: {macd_df['signal'].iloc[-1]:.4f}")
Batch Calculations
# Configure multiple indicators
config = {
'sma_20': {'type': 'sma', 'period': 20},
'ema_12': {'type': 'ema', 'period': 12},
'rsi_14': {'type': 'rsi', 'period': 14},
'macd': {
'type': 'macd',
'fast_period': 12,
'slow_period': 26,
'signal_period': 9
}
}
# Calculate all at once - returns dict of DataFrames
results = indicators.calculate_multiple_indicators(df, config)
# Access individual results
sma_df = results['sma_20'] # DataFrame with 'sma' column
ema_df = results['ema_12'] # DataFrame with 'ema' column
rsi_df = results['rsi_14'] # DataFrame with 'rsi' column
macd_df = results['macd'] # DataFrame with 'macd', 'signal', 'histogram' columns
Working with Different Price Columns
# Calculate SMA on the 'high' price
sma_high_df = indicators.sma(df, period=20, price_column='high')
# Calculate RSI on the 'open' price
rsi_open_df = indicators.calculate('rsi', df, period=14, price_column='open')
# All results are DataFrames with the same structure
assert 'sma' in sma_high_df.columns
assert 'rsi' in rsi_open_df.columns
Data Handling and Best Practices
DataFrame Preparation
from components.charts.utils import prepare_chart_data
# Prepare DataFrame from candle data
df = prepare_chart_data(candles)
# df has columns: ['open', 'high', 'low', 'close', 'volume'] with DatetimeIndex
Gap Handling
The system handles data gaps naturally:
- No interpolation: Gaps in timestamps are preserved
- Rolling calculations: Use only available data points
- Safe trading: No artificial data is introduced
# Example: If you have gaps in your data
# 09:00, 09:01, 09:02, 09:04, 09:05 (missing 09:03)
# The indicators will calculate correctly using available data
# No interpolation or filling of gaps
Warm-up Periods
All indicators implement proper warm-up periods for safe trading:
- SMA/EMA/RSI/BB: First
period-1values excluded - MACD: First
max(slow_period, signal_period)-1values excluded - Result: Only reliable, fully-calculated values are returned
Error Handling
try:
result_df = indicators.rsi(df, period=14)
if result_df is not None and not result_df.empty:
# Process results
pass
else:
# Handle insufficient data
logger.warning("Insufficient data for RSI calculation")
except Exception as e:
logger.error(f"RSI calculation failed: {e}")
# Handle calculation errors
Performance Considerations
-
Vectorized Operations
- Uses pandas rolling/ewm functions for maximum performance
- Minimal data copying and transformations
- Efficient memory usage
-
DataFrame Alignment
- Timestamp index ensures proper alignment with price data
- Easy integration with plotting libraries
- Consistent data structure across all indicators
-
Memory Efficiency
- Returns only necessary columns
- No metadata overhead in result DataFrames
- Clean, minimal output format
Testing
The module includes comprehensive tests for the new DataFrame-based approach:
- Unit tests for each indicator's DataFrame output
- Integration tests for the facade
- Edge case handling (gaps, insufficient data)
- Performance benchmarks
Run tests with:
uv run pytest tests/test_indicators.py
uv run pytest tests/test_indicators_safety.py
Migration from Legacy Format
If you were using the old List[IndicatorResult] format:
Old Style:
results = indicators.sma(df, period=20)
for result in results:
print(f"Time: {result.timestamp}, SMA: {result.values['sma']}")
New Style:
result_df = indicators.sma(df, period=20)
for timestamp, row in result_df.iterrows():
print(f"Time: {timestamp}, SMA: {row['sma']}")
Contributing
When adding new indicators:
- Create a new class in
implementations/. - Inherit from
BaseIndicator. - Implement the
calculatemethod to return a DataFrame. - Ensure proper warm-up periods.
- Add comprehensive tests.
- Create a corresponding JSON template file in
config/indicators/templates/to define its parameters, display properties, and styling for UI integration. - Update documentation in
docs/guides/adding-new-indicators.md.
See Adding New Indicators for detailed instructions.
API Reference
TechnicalIndicators Class
class TechnicalIndicators:
def sma(self, df: pd.DataFrame, period: int, price_column: str = 'close') -> pd.DataFrame
def ema(self, df: pd.DataFrame, period: int, price_column: str = 'close') -> pd.DataFrame
def rsi(self, df: pd.DataFrame, period: int = 14, price_column: str = 'close') -> pd.DataFrame
def macd(self, df: pd.DataFrame, fast_period: int = 12, slow_period: int = 26,
signal_period: int = 9, price_column: str = 'close') -> pd.DataFrame
def bollinger_bands(self, df: pd.DataFrame, period: int = 20, std_dev: float = 2.0,
price_column: str = 'close') -> pd.DataFrame
def calculate(self, indicator_type: str, df: pd.DataFrame, **kwargs) -> Optional[pd.DataFrame]
def calculate_multiple_indicators(self, df: pd.DataFrame,
indicators_config: Dict[str, Dict[str, Any]]) -> Dict[str, pd.DataFrame]
Return Format
All methods return:
- Success:
pd.DataFramewith indicator column(s) and DatetimeIndex - Failure/Insufficient Data:
pd.DataFrame()(empty DataFrame) - Error:
None(with logged error)
DataFrame Structure
# Example SMA result
result_df = indicators.sma(df, period=20)
# result_df.index: DatetimeIndex (timestamps)
# result_df.columns: ['sma']
# result_df.shape: (N, 1) where N = len(df) - period + 1 (after warm-up)