2025-06-02 13:42:00 +08:00
# Technical Indicators Module
## Overview
2025-06-09 16:28:16 +08:00
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
2025-06-07 01:32:21 +08:00
2025-06-07 14:12:37 +08:00
## Architecture
2025-06-07 01:32:21 +08:00
2025-06-07 14:12:37 +08:00
### Package Structure
2025-06-07 01:32:21 +08:00
```
data/common/indicators/
2025-06-07 14:12:37 +08:00
├── __init__ .py # Package exports
├── technical.py # Main facade class
├── base.py # Base indicator class
2025-06-09 16:28:16 +08:00
├── result.py # Result container class (legacy)
2025-06-07 14:12:37 +08:00
├── 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
2025-06-09 16:28:16 +08:00
- Data preparation with timestamp handling
- Validation and error handling
- Logging support
2025-06-07 14:12:37 +08:00
#### 2. Individual Indicators
Each indicator is implemented as a separate class inheriting from `BaseIndicator` :
2025-06-09 16:28:16 +08:00
- **Vectorized calculations** using pandas operations
- **Clean DataFrame output** with only relevant columns
- **Proper warm-up periods** for safe trading
- **Independent testing** and maintenance
2025-06-07 14:12:37 +08:00
#### 3. TechnicalIndicators Facade
Main entry point providing:
2025-06-09 16:28:16 +08:00
- Unified DataFrame-based interface
2025-06-07 14:12:37 +08:00
- Batch calculations
- Consistent error handling
2025-06-09 16:28:16 +08:00
- Data preparation utilities
2025-06-07 14:12:37 +08:00
## Supported Indicators
### Simple Moving Average (SMA)
```python
from data.common.indicators import TechnicalIndicators
indicators = TechnicalIndicators()
2025-06-09 16:28:16 +08:00
result_df = indicators.sma(df, period=20, price_column='close')
# Returns DataFrame with columns: ['sma'], indexed by timestamp
2025-06-07 14:12:37 +08:00
```
- **Parameters**:
- `period` : Number of periods (default: 20)
- `price_column` : Column to average (default: 'close')
2025-06-09 16:28:16 +08:00
- **Returns**: DataFrame with `'sma'` column, indexed by timestamp
- **Warm-up**: First `period-1` values are excluded for safety
2025-06-07 14:12:37 +08:00
### Exponential Moving Average (EMA)
```python
2025-06-09 16:28:16 +08:00
result_df = indicators.ema(df, period=12, price_column='close')
# Returns DataFrame with columns: ['ema'], indexed by timestamp
2025-06-07 14:12:37 +08:00
```
- **Parameters**:
- `period` : Number of periods (default: 20)
- `price_column` : Column to average (default: 'close')
2025-06-09 16:28:16 +08:00
- **Returns**: DataFrame with `'ema'` column, indexed by timestamp
- **Warm-up**: First `period-1` values are excluded for safety
2025-06-07 14:12:37 +08:00
### Relative Strength Index (RSI)
```python
2025-06-09 16:28:16 +08:00
result_df = indicators.rsi(df, period=14, price_column='close')
# Returns DataFrame with columns: ['rsi'], indexed by timestamp
2025-06-07 14:12:37 +08:00
```
- **Parameters**:
- `period` : Number of periods (default: 14)
- `price_column` : Column to analyze (default: 'close')
2025-06-09 16:28:16 +08:00
- **Returns**: DataFrame with `'rsi'` column, indexed by timestamp
- **Warm-up**: First `period-1` values are excluded for safety
2025-06-07 14:12:37 +08:00
### Moving Average Convergence Divergence (MACD)
```python
2025-06-09 16:28:16 +08:00
result_df = indicators.macd(
2025-06-07 14:12:37 +08:00
df,
fast_period=12,
slow_period=26,
signal_period=9,
price_column='close'
)
2025-06-09 16:28:16 +08:00
# Returns DataFrame with columns: ['macd', 'signal', 'histogram'], indexed by timestamp
2025-06-07 14:12:37 +08:00
```
- **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')
2025-06-09 16:28:16 +08:00
- **Returns**: DataFrame with `'macd'` , `'signal'` , `'histogram'` columns, indexed by timestamp
- **Warm-up**: First `max(slow_period, signal_period)-1` values are excluded for safety
2025-06-07 14:12:37 +08:00
### Bollinger Bands
```python
2025-06-09 16:28:16 +08:00
result_df = indicators.bollinger_bands(
2025-06-07 14:12:37 +08:00
df,
period=20,
std_dev=2.0,
price_column='close'
)
2025-06-09 16:28:16 +08:00
# Returns DataFrame with columns: ['upper_band', 'middle_band', 'lower_band'], indexed by timestamp
2025-06-07 14:12:37 +08:00
```
- **Parameters**:
- `period` : SMA period (default: 20)
- `std_dev` : Standard deviation multiplier (default: 2.0)
- `price_column` : Column to analyze (default: 'close')
2025-06-09 16:28:16 +08:00
- **Returns**: DataFrame with `'upper_band'` , `'middle_band'` , `'lower_band'` columns, indexed by timestamp
- **Warm-up**: First `period-1` values are excluded for safety
2025-06-07 14:12:37 +08:00
## Usage Examples
2025-06-09 16:28:16 +08:00
### Basic Usage with DataFrame Output
2025-06-07 14:12:37 +08:00
```python
from data.common.indicators import TechnicalIndicators
# Initialize calculator
indicators = TechnicalIndicators(logger=my_logger)
2025-06-09 16:28:16 +08:00
# Calculate single indicator - returns DataFrame
sma_df = indicators.sma(df, period=20)
2025-06-07 14:12:37 +08:00
2025-06-09 16:28:16 +08:00
# 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
```python
# 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}")
2025-06-07 14:12:37 +08:00
```
### Batch Calculations
```python
# 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
}
}
2025-06-09 16:28:16 +08:00
# Calculate all at once - returns dict of DataFrames
2025-06-07 14:12:37 +08:00
results = indicators.calculate_multiple_indicators(df, config)
2025-06-09 16:28:16 +08:00
# 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
2025-06-07 14:12:37 +08:00
```
2025-06-09 16:28:16 +08:00
### Working with Different Price Columns
2025-06-07 14:12:37 +08:00
```python
2025-06-09 16:28:16 +08:00
# 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
2025-06-07 14:12:37 +08:00
```
2025-06-09 16:28:16 +08:00
## Data Handling and Best Practices
2025-06-07 14:12:37 +08:00
2025-06-09 16:28:16 +08:00
### DataFrame Preparation
2025-06-07 14:12:37 +08:00
```python
2025-06-09 16:28:16 +08:00
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
2025-06-07 14:12:37 +08:00
```
2025-06-09 16:28:16 +08:00
### 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
2025-06-07 14:12:37 +08:00
2025-06-09 16:28:16 +08:00
```python
# 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
```
2025-06-07 14:12:37 +08:00
2025-06-09 16:28:16 +08:00
### Warm-up Periods
All indicators implement proper warm-up periods for safe trading:
- **SMA/EMA/RSI/BB**: First `period-1` values excluded
- **MACD**: First `max(slow_period, signal_period)-1` values excluded
- **Result**: Only reliable, fully-calculated values are returned
### Error Handling
2025-06-07 14:12:37 +08:00
```python
try:
2025-06-09 16:28:16 +08:00
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")
2025-06-07 14:12:37 +08:00
except Exception as e:
logger.error(f"RSI calculation failed: {e}")
2025-06-09 16:28:16 +08:00
# Handle calculation errors
2025-06-07 14:12:37 +08:00
```
## Performance Considerations
2025-06-09 16:28:16 +08:00
1. **Vectorized Operations**
- Uses pandas rolling/ewm functions for maximum performance
- Minimal data copying and transformations
- Efficient memory usage
2025-06-07 14:12:37 +08:00
2025-06-09 16:28:16 +08:00
2. **DataFrame Alignment**
- Timestamp index ensures proper alignment with price data
- Easy integration with plotting libraries
- Consistent data structure across all indicators
2025-06-07 14:12:37 +08:00
2025-06-09 16:28:16 +08:00
3. **Memory Efficiency**
- Returns only necessary columns
- No metadata overhead in result DataFrames
- Clean, minimal output format
2025-06-07 14:12:37 +08:00
## Testing
2025-06-09 16:28:16 +08:00
The module includes comprehensive tests for the new DataFrame-based approach:
- Unit tests for each indicator's DataFrame output
2025-06-07 14:12:37 +08:00
- Integration tests for the facade
2025-06-09 16:28:16 +08:00
- Edge case handling (gaps, insufficient data)
2025-06-07 14:12:37 +08:00
- Performance benchmarks
Run tests with:
```bash
uv run pytest tests/test_indicators.py
2025-06-09 16:28:16 +08:00
uv run pytest tests/test_indicators_safety.py
2025-06-07 01:32:21 +08:00
```
2025-06-02 13:42:00 +08:00
2025-06-09 16:28:16 +08:00
## Migration from Legacy Format
2025-06-02 13:42:00 +08:00
2025-06-09 16:28:16 +08:00
If you were using the old `List[IndicatorResult]` format:
2025-06-07 01:32:21 +08:00
2025-06-09 16:28:16 +08:00
### Old Style:
2025-06-07 01:32:21 +08:00
```python
2025-06-09 16:28:16 +08:00
results = indicators.sma(df, period=20)
for result in results:
print(f"Time: {result.timestamp}, SMA: {result.values['sma']}")
2025-06-07 01:32:21 +08:00
```
2025-06-09 16:28:16 +08:00
### New Style:
2025-06-02 13:42:00 +08:00
```python
2025-06-09 16:28:16 +08:00
result_df = indicators.sma(df, period=20)
for timestamp, row in result_df.iterrows():
print(f"Time: {timestamp}, SMA: {row['sma']}")
2025-06-06 15:06:17 +08:00
```
2025-06-02 13:42:00 +08:00
2025-06-09 16:28:16 +08:00
## Contributing
2025-06-02 13:42:00 +08:00
2025-06-09 16:28:16 +08:00
When adding new indicators:
2025-06-11 19:09:52 +08:00
1. Create a new class in `implementations/` .
2. Inherit from `BaseIndicator` .
3. Implement the `calculate` method to return a DataFrame.
4. Ensure proper warm-up periods.
5. Add comprehensive tests.
6. Create a corresponding **JSON template file** in `config/indicators/templates/` to define its parameters, display properties, and styling for UI integration.
7. Update documentation in `docs/guides/adding-new-indicators.md` .
2025-06-02 13:42:00 +08:00
2025-06-09 16:28:16 +08:00
See [Adding New Indicators ](./adding-new-indicators.md ) for detailed instructions.
2025-06-02 13:42:00 +08:00
2025-06-09 16:28:16 +08:00
## API Reference
2025-06-02 13:42:00 +08:00
2025-06-09 16:28:16 +08:00
### TechnicalIndicators Class
2025-06-02 13:42:00 +08:00
2025-06-06 15:06:17 +08:00
```python
2025-06-09 16:28:16 +08:00
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]
2025-06-02 13:42:00 +08:00
```
2025-06-09 16:28:16 +08:00
### Return Format
All methods return:
- **Success**: `pd.DataFrame` with indicator column(s) and DatetimeIndex
- **Failure/Insufficient Data**: `pd.DataFrame()` (empty DataFrame)
- **Error**: `None` (with logged error)
2025-06-02 13:42:00 +08:00
2025-06-09 16:28:16 +08:00
### DataFrame Structure
2025-06-02 13:42:00 +08:00
```python
2025-06-09 16:28:16 +08:00
# 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)
```