TCPDashboard/docs/modules/technical-indicators.md
Vasily.onl 3e0e89b826 Refactor indicator management to a data-driven approach
- 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.
2025-06-11 19:09:52 +08:00

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-1 values 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-1 values 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-1 values 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)-1 values 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-1 values 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-1 values excluded
  • MACD: First max(slow_period, signal_period)-1 values 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

  1. Vectorized Operations

    • Uses pandas rolling/ewm functions for maximum performance
    • Minimal data copying and transformations
    • Efficient memory usage
  2. DataFrame Alignment

    • Timestamp index ensures proper alignment with price data
    • Easy integration with plotting libraries
    • Consistent data structure across all indicators
  3. 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:

  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.

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.DataFrame with 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)