Refactor technical indicators to return DataFrames and enhance documentation

- 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.
This commit is contained in:
Vasily.onl 2025-06-09 16:28:16 +08:00
parent fc3cac24bd
commit ec8f5514bb
14 changed files with 542 additions and 542 deletions

View File

@ -11,6 +11,7 @@ from datetime import datetime, timezone
from decimal import Decimal
import json
from enum import Enum
import pandas as pd
from data.common.indicators import TechnicalIndicators, IndicatorResult, create_default_indicators_config, validate_indicator_config
from data.common.data_types import OHLCVCandle
@ -475,7 +476,7 @@ def convert_database_candles_to_ohlcv(candles: List[Dict[str, Any]]) -> List[OHL
def calculate_indicators(candles: List[Dict[str, Any]],
indicator_configs: List[str],
custom_configs: Optional[Dict[str, ChartIndicatorConfig]] = None) -> Dict[str, List[IndicatorResult]]:
custom_configs: Optional[Dict[str, ChartIndicatorConfig]] = None) -> Dict[str, pd.DataFrame]:
"""
Calculate technical indicators for chart display.
@ -485,7 +486,7 @@ def calculate_indicators(candles: List[Dict[str, Any]],
custom_configs: Optional custom indicator configurations
Returns:
Dictionary mapping indicator names to their calculation results
Dictionary mapping indicator names to their calculation results as DataFrames
"""
if not candles:
logger.warning("Indicator Definitions: No candles provided for indicator calculation")
@ -520,6 +521,7 @@ def calculate_indicators(candles: List[Dict[str, Any]],
# Calculate indicators
try:
results = indicators_calc.calculate_multiple_indicators(ohlcv_candles, configs_to_calculate)
# results is now a dict of DataFrames
logger.debug(f"Indicator Definitions: Calculated {len(results)} indicators successfully")
return results

View File

@ -466,7 +466,9 @@ class MarketDataIntegrator:
symbol: str,
exchange: str = "okx"
) -> Dict[str, pd.DataFrame]:
"""
Get indicator data for chart display. Returns a dict mapping indicator IDs to DataFrames.
"""
indicator_data_map = {}
if main_df.empty:
return indicator_data_map
@ -504,27 +506,14 @@ class MarketDataIntegrator:
# Use main chart's dataframe
indicator_df = main_df
# Calculate the indicator
indicator_result_pkg = self.indicators.calculate(
# Calculate the indicator (now returns DataFrame)
result_df = self.indicators.calculate(
indicator.type,
indicator_df,
**indicator.parameters
)
if indicator_result_pkg and indicator_result_pkg.get('data'):
indicator_results = indicator_result_pkg['data']
if not indicator_results:
self.logger.warning(f"Indicator '{indicator.name}' produced no results.")
continue
result_df = pd.DataFrame([
{'timestamp': r.timestamp, **r.values}
for r in indicator_results
])
result_df['timestamp'] = pd.to_datetime(result_df['timestamp'])
result_df.set_index('timestamp', inplace=True)
if result_df is not None and not result_df.empty:
# Ensure timezone consistency before reindexing
if result_df.index.tz is None:
result_df = result_df.tz_localize('UTC')

View File

@ -68,8 +68,14 @@ class BaseIndicator(ABC):
df = df.sort_values('timestamp').reset_index(drop=True)
# Set timestamp as index for time-series operations
df['timestamp'] = pd.to_datetime(df['timestamp'])
# Set as index, but keep as column
df.set_index('timestamp', inplace=True)
# Ensure it's datetime
df['timestamp'] = df.index
return df
@abstractmethod

View File

@ -2,11 +2,9 @@
Bollinger Bands indicator implementation.
"""
from typing import List
import pandas as pd
from ..base import BaseIndicator
from ..result import IndicatorResult
class BollingerBandsIndicator(BaseIndicator):
@ -18,7 +16,7 @@ class BollingerBandsIndicator(BaseIndicator):
"""
def calculate(self, df: pd.DataFrame, period: int = 20,
std_dev: float = 2.0, price_column: str = 'close') -> List[IndicatorResult]:
std_dev: float = 2.0, price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Bollinger Bands.
@ -29,53 +27,20 @@ class BollingerBandsIndicator(BaseIndicator):
price_column: Price column to use ('open', 'high', 'low', 'close')
Returns:
List of indicator results with upper band, middle band (SMA), and lower band
DataFrame with Bollinger Bands values and metadata, indexed by timestamp
"""
# Validate input data
if not self.validate_dataframe(df, period):
return []
return pd.DataFrame()
try:
# Calculate middle band (SMA)
df = df.copy()
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 []
df['upper_band'] = df['middle_band'] + (df['std'] * std_dev)
df['lower_band'] = df['middle_band'] - (df['std'] * std_dev)
# Only keep rows with valid bands, and only 'timestamp', 'upper_band', 'middle_band', 'lower_band' columns
result_df = df.loc[df['middle_band'].notna() & df['upper_band'].notna() & df['lower_band'].notna(), ['timestamp', 'upper_band', 'middle_band', 'lower_band']].copy()
result_df.set_index('timestamp', inplace=True)
return result_df
except Exception:
return pd.DataFrame()

View File

@ -2,11 +2,9 @@
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):
@ -18,7 +16,7 @@ class EMAIndicator(BaseIndicator):
"""
def calculate(self, df: pd.DataFrame, period: int = 20,
price_column: str = 'close') -> List[IndicatorResult]:
price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Exponential Moving Average (EMA).
@ -28,33 +26,19 @@ class EMAIndicator(BaseIndicator):
price_column: Price column to use ('open', 'high', 'low', 'close')
Returns:
List of indicator results with EMA values
DataFrame with EMA values and metadata, indexed by timestamp
"""
# Validate input data
if not self.validate_dataframe(df, period):
return []
return pd.DataFrame()
try:
# Calculate EMA using pandas exponential weighted moving average
df = df.copy()
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 []
# Only keep rows with valid EMA, and only 'timestamp' and 'ema' columns
result_df = df.loc[df['ema'].notna(), ['timestamp', 'ema']].copy()
# Only keep rows after enough data for EMA
result_df = result_df.iloc[period-1:]
result_df.set_index('timestamp', inplace=True)
return result_df
except Exception:
return pd.DataFrame()

View File

@ -2,11 +2,9 @@
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):
@ -20,7 +18,7 @@ class MACDIndicator(BaseIndicator):
def calculate(self, df: pd.DataFrame, fast_period: int = 12,
slow_period: int = 26, signal_period: int = 9,
price_column: str = 'close') -> List[IndicatorResult]:
price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Moving Average Convergence Divergence (MACD).
@ -32,53 +30,23 @@ class MACDIndicator(BaseIndicator):
price_column: Price column to use ('open', 'high', 'low', 'close')
Returns:
List of indicator results with MACD, signal, and histogram values
DataFrame with MACD values and metadata, indexed by timestamp
"""
# Validate input data
if not self.validate_dataframe(df, slow_period):
return []
return pd.DataFrame()
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 = df.copy()
df['macd'] = df[price_column].ewm(span=fast_period, adjust=False).mean() - \
df[price_column].ewm(span=slow_period, adjust=False).mean()
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 []
# Only keep rows with valid MACD, and only 'timestamp', 'macd', 'signal', 'histogram' columns
result_df = df.loc[df['macd'].notna() & df['signal'].notna() & df['histogram'].notna(), ['timestamp', 'macd', 'signal', 'histogram']].copy()
# Only keep rows after enough data for MACD and signal
min_required = max(slow_period, signal_period)
result_df = result_df.iloc[min_required-1:]
result_df.set_index('timestamp', inplace=True)
return result_df
except Exception:
return pd.DataFrame()

View File

@ -4,6 +4,7 @@ Relative Strength Index (RSI) indicator implementation.
from typing import List
import pandas as pd
import numpy as np
from ..base import BaseIndicator
from ..result import IndicatorResult
@ -18,7 +19,7 @@ class RSIIndicator(BaseIndicator):
"""
def calculate(self, df: pd.DataFrame, period: int = 14,
price_column: str = 'close') -> List[IndicatorResult]:
price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Relative Strength Index (RSI).
@ -28,48 +29,23 @@ class RSIIndicator(BaseIndicator):
price_column: Price column to use ('open', 'high', 'low', 'close')
Returns:
List of indicator results with RSI values
DataFrame with RSI values and metadata, indexed by timestamp
"""
# Validate input data
if not self.validate_dataframe(df, period + 1): # Need extra period for diff
return []
if not self.validate_dataframe(df, period):
return pd.DataFrame()
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 []
df = df.copy()
delta = df[price_column].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
rs = rs.replace([np.inf, -np.inf], np.nan)
df['rsi'] = 100 - (100 / (1 + rs))
# Only keep rows with valid RSI, and only 'timestamp' and 'rsi' columns
result_df = df.loc[df['rsi'].notna(), ['timestamp', 'rsi']].copy()
result_df = result_df.iloc[period-1:]
result_df.set_index('timestamp', inplace=True)
return result_df
except Exception:
return pd.DataFrame()

View File

@ -2,11 +2,9 @@
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):
@ -18,7 +16,7 @@ class SMAIndicator(BaseIndicator):
"""
def calculate(self, df: pd.DataFrame, period: int = 20,
price_column: str = 'close') -> List[IndicatorResult]:
price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Simple Moving Average (SMA).
@ -28,32 +26,18 @@ class SMAIndicator(BaseIndicator):
price_column: Price column to use ('open', 'high', 'low', 'close')
Returns:
List of indicator results with SMA values
DataFrame with SMA values and metadata, indexed by timestamp
"""
# Validate input data
if not self.validate_dataframe(df, period):
return []
return pd.DataFrame()
try:
# Calculate SMA using pandas rolling window
df = df.copy()
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 []
# Only keep rows with valid SMA, and only 'timestamp' and 'sma' columns
result_df = df.loc[df['sma'].notna(), ['timestamp', 'sma']].copy()
result_df = result_df.iloc[period-1:]
result_df.set_index('timestamp', inplace=True)
return result_df
except Exception:
return pd.DataFrame()

View File

@ -10,22 +10,14 @@ IMPORTANT: Handles Sparse Data
- Uses pandas for efficient vectorized calculations
- Follows right-aligned timestamp convention
Supported Indicators:
- Simple Moving Average (SMA)
- Exponential Moving Average (EMA)
- Relative Strength Index (RSI)
- Moving Average Convergence Divergence (MACD)
- Bollinger Bands
TODO: need make more procedural without hardcoding indicators type and so on
"""
from datetime import datetime
from typing import Dict, List, Optional, Any, Union
from typing import Dict, List, Optional, Any
import pandas as pd
import numpy as np
from .result import IndicatorResult
from ..data_types import OHLCVCandle
from .base import BaseIndicator
from .implementations import (
SMAIndicator,
EMAIndicator,
@ -85,7 +77,7 @@ class TechnicalIndicators:
return self._sma.prepare_dataframe(candles)
def sma(self, df: pd.DataFrame, period: int,
price_column: str = 'close') -> List[IndicatorResult]:
price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Simple Moving Average (SMA).
@ -95,12 +87,12 @@ class TechnicalIndicators:
price_column: Price column to use ('open', 'high', 'low', 'close')
Returns:
List of indicator results with SMA values
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') -> List[IndicatorResult]:
price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Exponential Moving Average (EMA).
@ -115,7 +107,7 @@ class TechnicalIndicators:
return self._ema.calculate(df, period=period, price_column=price_column)
def rsi(self, df: pd.DataFrame, period: int = 14,
price_column: str = 'close') -> List[IndicatorResult]:
price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Relative Strength Index (RSI).
@ -125,13 +117,13 @@ class TechnicalIndicators:
price_column: Price column to use ('open', 'high', 'low', 'close')
Returns:
List of indicator results with RSI values
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') -> List[IndicatorResult]:
price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Moving Average Convergence Divergence (MACD).
@ -154,7 +146,7 @@ class TechnicalIndicators:
)
def bollinger_bands(self, df: pd.DataFrame, period: int = 20,
std_dev: float = 2.0, price_column: str = 'close') -> List[IndicatorResult]:
std_dev: float = 2.0, price_column: str = 'close') -> pd.DataFrame:
"""
Calculate Bollinger Bands.
@ -165,7 +157,7 @@ class TechnicalIndicators:
price_column: Price column to use ('open', 'high', 'low', 'close')
Returns:
List of indicator results with upper band, middle band (SMA), and lower band
DataFrame with upper band, middle band (SMA), and lower band
"""
return self._bollinger.calculate(
df,
@ -175,9 +167,8 @@ class TechnicalIndicators:
)
def calculate_multiple_indicators(self, df: pd.DataFrame,
indicators_config: Dict[str, Dict[str, Any]]) -> Dict[str, List[IndicatorResult]]:
indicators_config: Dict[str, Dict[str, Any]]) -> Dict[str, pd.DataFrame]:
"""
TODO: need make more procedural without hardcoding indicators type and so on
Calculate multiple indicators at once for efficiency.
Args:
@ -192,7 +183,7 @@ class TechnicalIndicators:
}
Returns:
Dictionary mapping indicator names to their results
Dictionary mapping indicator names to their results as DataFrames
"""
results = {}
@ -235,16 +226,16 @@ class TechnicalIndicators:
else:
if self.logger:
self.logger.warning(f"Unknown indicator type: {indicator_type}")
results[indicator_name] = []
results[indicator_name] = pd.DataFrame()
except Exception as e:
if self.logger:
self.logger.error(f"Error calculating {indicator_name}: {e}")
results[indicator_name] = []
results[indicator_name] = pd.DataFrame()
return results
def calculate(self, indicator_type: str, df: pd.DataFrame, **kwargs) -> Optional[Dict[str, Any]]:
def calculate(self, indicator_type: str, df: pd.DataFrame, **kwargs) -> Optional[pd.DataFrame]:
"""
Calculate a single indicator with dynamic dispatch.
@ -254,7 +245,7 @@ class TechnicalIndicators:
**kwargs: Indicator-specific parameters (e.g., period=20)
Returns:
A dictionary containing the indicator results, or None if the type is unknown.
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)
@ -265,21 +256,13 @@ class TechnicalIndicators:
try:
if df.empty:
return {'data': [], 'metadata': {}}
return pd.DataFrame()
# Call the indicator method
raw_result = indicator_method(df, **kwargs)
# Call the indicator method (now returns DataFrame)
result_df = indicator_method(df, **kwargs)
# Extract metadata from the first result if available
metadata = raw_result[0].metadata if raw_result else {}
# The methods return List[IndicatorResult], let's package that
if raw_result:
return {
"data": raw_result,
"metadata": metadata
}
return None
# Return the DataFrame directly
return result_df
except Exception as e:
if self.logger:

View File

@ -4,6 +4,13 @@
The Crypto Trading Bot Dashboard features a comprehensive modular indicator system that allows users to create, customize, and manage technical indicators for chart analysis. The system supports both overlay indicators (displayed on the main price chart) and subplot indicators (displayed in separate panels below the main chart).
**Key Features:**
- **Vectorized Calculations**: High-performance pandas-based indicator calculations
- **Clean DataFrame Output**: Returns only relevant indicator columns with timestamp index
- **Safe Trading**: Proper warm-up periods ensure no early/invalid values
- **Gap Handling**: Maintains timestamp alignment without interpolation
- **Real-time Integration**: Seamless integration with chart visualization
## Table of Contents
1. [System Architecture](#system-architecture)
@ -23,6 +30,7 @@ The Crypto Trading Bot Dashboard features a comprehensive modular indicator syst
components/charts/
├── indicator_manager.py # Core indicator CRUD operations
├── indicator_defaults.py # Default indicator templates
├── data_integration.py # DataFrame-based indicator calculations
├── layers/
│ ├── indicators.py # Overlay indicator rendering
│ └── subplots.py # Subplot indicator rendering
@ -34,6 +42,15 @@ config/indicators/
├── sma_abc123.json
├── ema_def456.json
└── ...
data/common/indicators/
├── technical.py # Vectorized indicator calculations
├── implementations/ # Individual indicator implementations
├── sma.py # Simple Moving Average
├── ema.py # Exponential Moving Average
├── rsi.py # Relative Strength Index
├── macd.py # MACD
└── bollinger.py # Bollinger Bands
```
### Key Classes
@ -41,6 +58,7 @@ config/indicators/
- **`IndicatorManager`**: Handles CRUD operations for user indicators
- **`UserIndicator`**: Data structure for indicator configuration
- **`IndicatorStyling`**: Appearance and styling configuration
- **`TechnicalIndicators`**: Vectorized calculation engine
- **Indicator Layers**: Rendering classes for different indicator types
## Current Indicators
@ -48,19 +66,19 @@ config/indicators/
### Overlay Indicators
These indicators are displayed directly on the price chart:
| Indicator | Type | Parameters | Description |
|-----------|------|------------|-------------|
| **Simple Moving Average (SMA)** | `sma` | `period` (1-200) | Average price over N periods |
| **Exponential Moving Average (EMA)** | `ema` | `period` (1-200) | Weighted average giving more weight to recent prices |
| **Bollinger Bands** | `bollinger_bands` | `period` (5-100), `std_dev` (0.5-5.0) | Price channels based on standard deviation |
| Indicator | Type | Parameters | Description | Output Columns |
|-----------|------|------------|-------------|----------------|
| **Simple Moving Average (SMA)** | `sma` | `period` (1-200) | Average price over N periods | `['sma']` |
| **Exponential Moving Average (EMA)** | `ema` | `period` (1-200) | Weighted average giving more weight to recent prices | `['ema']` |
| **Bollinger Bands** | `bollinger_bands` | `period` (5-100), `std_dev` (0.5-5.0) | Price channels based on standard deviation | `['upper_band', 'middle_band', 'lower_band']` |
### Subplot Indicators
These indicators are displayed in separate panels:
| Indicator | Type | Parameters | Description |
|-----------|------|------------|-------------|
| **Relative Strength Index (RSI)** | `rsi` | `period` (2-50) | Momentum oscillator (0-100 scale) |
| **MACD** | `macd` | `fast_period` (2-50), `slow_period` (5-100), `signal_period` (2-30) | Moving average convergence divergence |
| Indicator | Type | Parameters | Description | Output Columns |
|-----------|------|------------|-------------|----------------|
| **Relative Strength Index (RSI)** | `rsi` | `period` (2-50) | Momentum oscillator (0-100 scale) | `['rsi']` |
| **MACD** | `macd` | `fast_period` (2-50), `slow_period` (5-100), `signal_period` (2-30) | Moving average convergence divergence | `['macd', 'signal', 'histogram']` |
## User Interface
@ -212,6 +230,20 @@ class IndicatorManager:
def get_indicators_by_type(self, display_type: str) -> List[UserIndicator]
```
### TechnicalIndicators Class (Vectorized Calculations)
```python
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]
```
### Usage Examples
```python
@ -246,6 +278,86 @@ all_indicators = manager.list_indicators()
# Get by type
overlay_indicators = manager.get_indicators_by_type("overlay")
subplot_indicators = manager.get_indicators_by_type("subplot")
# Calculate indicators (vectorized)
from data.common.indicators import TechnicalIndicators
indicators = TechnicalIndicators()
# Calculate SMA - returns DataFrame with 'sma' column
sma_df = indicators.sma(df, period=20)
print(f"SMA values: {sma_df['sma'].tolist()}")
# Calculate MACD - returns DataFrame with 'macd', 'signal', 'histogram' columns
macd_df = indicators.macd(df, fast_period=12, slow_period=26, signal_period=9)
print(f"MACD signal: {macd_df['signal'].iloc[-1]}")
```
## Data Integration and Chart Rendering
### DataFrame-Based Calculations
The system now uses vectorized DataFrame calculations for high performance:
```python
# In data_integration.py
def get_indicator_data(self, main_df: pd.DataFrame, indicator_configs: List[UserIndicator]) -> Dict[str, pd.DataFrame]:
"""
Calculate indicator data using vectorized operations.
Returns:
Dict mapping indicator_id to DataFrame with indicator values
"""
indicator_data_map = {}
for config in indicator_configs:
indicator = self.indicator_manager.load_indicator(config.id)
if not indicator:
continue
# Calculate using vectorized methods
result_df = self.indicators.calculate(
indicator.type,
indicator_df,
**indicator.parameters
)
if result_df is not None and not result_df.empty:
indicator_data_map[config.id] = result_df
return indicator_data_map
```
### Chart Integration
Indicators are rendered using their clean DataFrame output:
```python
# In layers/indicators.py
def create_traces(self, indicator_data: pd.DataFrame, styling: IndicatorStyling) -> List[go.Scatter]:
"""
Create plotly traces from indicator DataFrame.
"""
traces = []
# For SMA/EMA - single line
if 'sma' in indicator_data.columns:
traces.append(go.Scatter(
x=indicator_data.index,
y=indicator_data['sma'],
name=self.indicator.name,
line=dict(color=styling.color, width=styling.line_width),
opacity=styling.opacity
))
# For MACD - multiple lines
elif 'macd' in indicator_data.columns:
traces.extend([
go.Scatter(x=indicator_data.index, y=indicator_data['macd'], name='MACD'),
go.Scatter(x=indicator_data.index, y=indicator_data['signal'], name='Signal'),
go.Bar(x=indicator_data.index, y=indicator_data['histogram'], name='Histogram')
])
return traces
```
## Troubleshooting
@ -263,8 +375,14 @@ subplot_indicators = manager.get_indicators_by_type("subplot")
3. **Chart not updating**
- Verify the indicator layer implements `calculate_values` and `create_traces`
- Check if indicator is registered in the correct registry
- Ensure DataFrame output is not empty
4. **File permission errors**
4. **Empty indicator results**
- Check if DataFrame has sufficient data (warm-up periods)
- Verify timestamp column is present and properly formatted
- Check for gaps in data that might affect calculations
5. **File permission errors**
- Ensure `config/indicators/user_indicators/` directory is writable
- Check file permissions on existing JSON files
@ -274,14 +392,26 @@ subplot_indicators = manager.get_indicators_by_type("subplot")
- Look at application logs for Python exceptions
- Verify JSON file structure with a validator
- Test indicator calculations with sample data
- Check DataFrame structure and column names
### Performance Considerations
- **Vectorized calculations** provide significant performance improvements
- Indicators with large periods may take longer to calculate
- Consider data availability when setting parameter limits
- Subplot indicators require additional chart space
- Real-time updates may impact performance with many indicators
### Warm-up Periods
All indicators implement proper warm-up periods for safe trading:
- **SMA/EMA/RSI/BB**: First `period-1` values are excluded
- **MACD**: First `max(slow_period, signal_period)-1` values are excluded
- **Result**: Only reliable, fully-calculated values are returned
This ensures that no early/invalid values are used for trading decisions.
## Best Practices
1. **Naming Conventions**
@ -300,6 +430,7 @@ subplot_indicators = manager.get_indicators_by_type("subplot")
- Log errors for debugging
4. **Performance**
- Leverage vectorized calculations for speed
- Cache calculated values when possible
- Optimize calculation algorithms
- Limit the number of active indicators
@ -307,7 +438,14 @@ subplot_indicators = manager.get_indicators_by_type("subplot")
5. **User Experience**
- Provide immediate visual feedback
- Use intuitive color schemes
- Group related indicators logically
- Group related indicators logically
6. **Data Handling**
- Respect warm-up periods for safe trading
- Handle data gaps without interpolation
- Maintain timestamp alignment
- Use clean DataFrame output for plotting
---
*Back to [Chart System Documentation (`README.md`)]*

View File

@ -2,7 +2,16 @@
## Overview
The Technical Indicators module provides a modular, extensible system for calculating technical analysis indicators. It is designed to handle sparse OHLCV data efficiently, making it ideal for real-time trading applications.
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
@ -12,7 +21,7 @@ data/common/indicators/
├── __init__.py # Package exports
├── technical.py # Main facade class
├── base.py # Base indicator class
├── result.py # Result container class
├── result.py # Result container class (legacy)
├── utils.py # Utility functions
└── implementations/ # Individual indicator implementations
├── __init__.py
@ -27,24 +36,23 @@ data/common/indicators/
#### 1. Base Classes
- **BaseIndicator**: Abstract base class providing common functionality
- Data preparation
- Validation
- Error handling
- Logging
- 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`:
- Focused responsibility
- Independent testing
- Easy maintenance
- Clear documentation
- **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 interface
- Unified DataFrame-based interface
- Batch calculations
- Consistent error handling
- Data preparation
- Data preparation utilities
## Supported Indicators
@ -53,73 +61,112 @@ Main entry point providing:
from data.common.indicators import TechnicalIndicators
indicators = TechnicalIndicators()
results = indicators.sma(df, period=20, price_column='close')
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)
```python
results = indicators.ema(df, period=12, price_column='close')
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)
```python
results = indicators.rsi(df, period=14, price_column='close')
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)
```python
results = indicators.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
```python
results = indicators.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
### Basic Usage with DataFrame Output
```python
from data.common.indicators import TechnicalIndicators
# Initialize calculator
indicators = TechnicalIndicators(logger=my_logger)
# Calculate single indicator
sma_results = indicators.sma(df, period=20)
# Calculate single indicator - returns DataFrame
sma_df = indicators.sma(df, period=20)
# Access results
for result in sma_results:
print(f"Time: {result.timestamp}, SMA: {result.values['sma']}")
# 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}")
```
### Batch Calculations
@ -137,80 +184,121 @@ config = {
}
}
# Calculate all at once
# 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
```
### Dynamic Indicator Selection
### Working with Different Price Columns
```python
# Calculate any indicator by name
result = indicators.calculate(
'macd',
df,
fast_period=12,
slow_period=26,
signal_period=9
)
# 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 Structures
## Data Handling and Best Practices
### IndicatorResult
### DataFrame Preparation
```python
@dataclass
class IndicatorResult:
timestamp: datetime # Right-aligned timestamp
symbol: str # Trading symbol
timeframe: str # Candle timeframe
values: Dict[str, float] # Indicator values
metadata: Optional[Dict[str, Any]] = None # Calculation metadata
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
```
## Error Handling
### 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
The module provides comprehensive error handling:
- Input validation
- Data sufficiency checks
- Calculation error handling
- Detailed error logging
```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
```
Example:
### 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
```python
try:
results = indicators.rsi(df, period=14)
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}")
results = []
# Handle calculation errors
```
## Performance Considerations
1. **Data Preparation**
- Uses pandas for vectorized calculations
- Handles sparse data efficiently
- Maintains timestamp alignment
1. **Vectorized Operations**
- Uses pandas rolling/ewm functions for maximum performance
- Minimal data copying and transformations
- Efficient memory usage
2. **Memory Usage**
- Avoids unnecessary data copies
- Cleans up temporary calculations
- Uses efficient data structures
2. **DataFrame Alignment**
- Timestamp index ensures proper alignment with price data
- Easy integration with plotting libraries
- Consistent data structure across all indicators
3. **Calculation Optimization**
- Vectorized operations where possible
- Minimal data transformations
- Efficient algorithm implementations
3. **Memory Efficiency**
- Returns only necessary columns
- No metadata overhead in result DataFrames
- Clean, minimal output format
## Testing
The module includes comprehensive tests:
- Unit tests for each indicator
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
- Edge case handling (gaps, insufficient data)
- Performance benchmarks
Run tests with:
```bash
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:
```python
results = indicators.sma(df, period=20)
for result in results:
print(f"Time: {result.timestamp}, SMA: {result.values['sma']}")
```
### New Style:
```python
result_df = indicators.sma(df, period=20)
for timestamp, row in result_df.iterrows():
print(f"Time: {timestamp}, SMA: {row['sma']}")
```
## Contributing
@ -218,122 +306,42 @@ uv run pytest tests/test_indicators.py
When adding new indicators:
1. Create a new class in `implementations/`
2. Inherit from `BaseIndicator`
3. Implement the `calculate` method
4. Add tests
5. Update documentation
3. Implement the `calculate` method to return a DataFrame
4. Ensure proper warm-up periods
5. Add comprehensive tests
6. Update documentation
See [Adding New Indicators](./adding-new-indicators.md) for detailed instructions.
## Key Features
## API Reference
- **DataFrame-Centric Design**: Operates directly on pandas DataFrames for performance and simplicity.
- **Vectorized Calculations**: Leverages pandas and numpy for high-speed computation.
- **Flexible `calculate` Method**: A single entry point for calculating any supported indicator by name.
- **Standardized Output**: All methods return a DataFrame containing the calculated indicator values, indexed by timestamp.
- **Modular Architecture**: Clear separation between calculation logic, result types, and utilities.
## Usage Examples
### Importing the Required Components
### TechnicalIndicators Class
```python
from data.common.indicators import (
TechnicalIndicators,
IndicatorResult,
create_default_indicators_config,
validate_indicator_config
)
from data.common.data_types import OHLCVCandle
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]
```
### Preparing the DataFrame
Before you can calculate indicators, you need a properly formatted pandas DataFrame. The `prepare_chart_data` utility is the recommended way to create one from a list of candle dictionaries.
### 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
```python
from components.charts.utils import prepare_chart_data
from data.common.indicators import TechnicalIndicators
# Assume 'candles' is a list of OHLCV dictionaries from the database
# candles = fetch_market_data(...)
# Prepare the DataFrame
df = prepare_chart_data(candles)
# df is now ready for indicator calculations
# It has a DatetimeIndex and the necessary OHLCV columns.
```
### Basic Indicator Calculation
Once you have a prepared DataFrame, you can calculate indicators directly.
```python
# Initialize the calculator
indicators = TechnicalIndicators()
# Calculate a Simple Moving Average
sma_df = indicators.sma(df, period=20)
# Calculate an Exponential Moving Average
ema_df = indicators.ema(df, period=12)
# sma_df and ema_df are pandas DataFrames containing the results.
```
### Using the `calculate` Method
The most flexible way to compute an indicator is with the `calculate` method, which accepts the indicator type as a string.
```python
# Calculate RSI using the generic method
rsi_pkg = indicators.calculate('rsi', df, period=14)
if rsi_pkg:
rsi_df = rsi_pkg['data']
# Calculate MACD with custom parameters
macd_pkg = indicators.calculate('macd', df, fast_period=10, slow_period=30, signal_period=8)
if macd_pkg:
macd_df = macd_pkg['data']
```
### Using Different Price Columns
You can specify which price column (`open`, `high`, `low`, or `close`) to use for the calculation.
```python
# 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_pkg = indicators.calculate('rsi', df, period=14, price_column='open')
```
## Indicator Details
The following details the parameters and the columns returned in the result DataFrame for each indicator.
### Simple Moving Average (SMA)
- **Parameters**: `period` (int), `price_column` (str, default: 'close')
- **Returned Columns**: `sma`
### Exponential Moving Average (EMA)
- **Parameters**: `period` (int), `price_column` (str, default: 'close')
- **Returned Columns**: `ema`
### Relative Strength Index (RSI)
- **Parameters**: `period` (int), `price_column` (str, default: 'close')
- **Returned Columns**: `rsi`
### MACD (Moving Average Convergence Divergence)
- **Parameters**: `fast_period` (int), `slow_period` (int), `signal_period` (int), `price_column` (str, default: 'close')
- **Returned Columns**: `macd`, `signal`, `histogram`
### Bollinger Bands
- **Parameters**: `period` (int), `std_dev` (float), `price_column` (str, default: 'close')
- **Returned Columns**: `upper_band`, `
# 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)
```

View File

@ -183,6 +183,15 @@
- [ ] 13.8 Implement comprehensive data recovery with OKX REST API for historical backfill
- [ ] 13.9 Add gap detection and automatic data recovery during reconnections
- [ ] 13.10 Implement data integrity validation and conflict resolution for recovered data
- [ ] 13.11 **Gap-Based Trading Strategy Implementation**
- [ ] 13.11.1 Implement gap detection algorithms for various timeframes (overnight, weekend, intraday)
- [ ] 13.11.2 Create gap classification system (breakaway, exhaustion, continuation, common gaps)
- [ ] 13.11.3 Develop gap-filling probability models using historical data analysis
- [ ] 13.11.4 Implement gap-based entry/exit strategies with risk management
- [ ] 13.11.5 Add gap visualization tools in dashboard for manual analysis
- [ ] 13.11.6 Create automated gap trading bots with configurable parameters
- [ ] 13.11.7 Implement gap-based backtesting scenarios and performance metrics
- [ ] 13.11.8 Add gap alert system for real-time gap detection and notification
- [ ] 14.0 Advanced Dashboard Performance and User Experience (Future Enhancement)
- [ ] 14.1 Implement dashboard state management with browser localStorage persistence
@ -200,6 +209,14 @@
- [ ] 14.13 Implement memory usage optimization for long-running dashboard sessions
- [ ] 14.14 Add chart export capabilities (PNG, SVG, PDF) with high-quality rendering
- [ ] 14.15 Implement dashboard mobile responsiveness and touch optimizations
- [ ] 14.16 **Advanced Gap Analysis Dashboard Features**
- [ ] 14.16.1 Create dedicated gap analysis panel with gap statistics and trends
- [ ] 14.16.2 Implement interactive gap charts showing gap size, frequency, and fill rates
- [ ] 14.16.3 Add gap pattern recognition and historical comparison tools
- [ ] 14.16.4 Create gap-based alert system with customizable thresholds
- [ ] 14.16.5 Implement gap trading performance analytics and reporting
- [ ] 14.16.6 Add gap-based strategy backtesting interface
- [ ] 14.16.7 Create gap risk assessment tools and position sizing recommendations

View File

@ -94,138 +94,122 @@ class TestTechnicalIndicators:
def test_prepare_dataframe(self, indicators, sample_candles):
"""Test DataFrame preparation from OHLCV candles."""
df = indicators.prepare_dataframe(sample_candles)
df = indicators._prepare_dataframe_from_list(sample_candles)
assert not df.empty
assert len(df) == len(sample_candles)
assert list(df.columns) == ['symbol', 'timeframe', 'open', 'high', 'low', 'close', 'volume', 'trade_count']
assert list(df.columns) == ['symbol', 'timeframe', 'open', 'high', 'low', 'close', 'volume', 'trade_count', 'timestamp']
assert df.index.name == 'timestamp'
# Check that timestamps are sorted
assert df.index.is_monotonic_increasing
def test_prepare_dataframe_empty(self, indicators):
"""Test DataFrame preparation with empty candles list."""
df = indicators.prepare_dataframe([])
df = indicators._prepare_dataframe_from_list([])
assert df.empty
def test_sma_calculation(self, indicators, sample_candles):
"""Test Simple Moving Average calculation."""
"""Test Simple Moving Average calculation (now returns DataFrame)."""
period = 5
results = indicators.sma(sample_candles, period)
# Should have results starting from period 5
assert len(results) == len(sample_candles) - period + 1
# Check first result
first_result = results[0]
assert isinstance(first_result, IndicatorResult)
assert first_result.symbol == 'BTC-USDT'
assert first_result.timeframe == '1m'
assert 'sma' in first_result.values
assert first_result.metadata['period'] == period
# Verify SMA calculation manually for first result
first_5_closes = [float(candle.close) for candle in sample_candles[:5]]
expected_sma = sum(first_5_closes) / len(first_5_closes)
assert abs(first_result.values['sma'] - expected_sma) < 0.001
df = indicators._prepare_dataframe_from_list(sample_candles)
df['timestamp'] = df.index
result_df = indicators.sma(df, period)
assert isinstance(result_df, pd.DataFrame)
assert not result_df.empty
assert 'sma' in result_df.columns
# Find the correct rolling window for the first SMA value
first_ts = result_df.index[0]
first_idx = [candle.end_time for candle in sample_candles].index(first_ts)
window_closes = [float(candle.close) for candle in sample_candles[first_idx - period + 1:first_idx + 1]]
expected_sma = sum(window_closes) / len(window_closes)
assert abs(result_df.iloc[0]['sma'] - expected_sma) < 0.001
def test_sma_insufficient_data(self, indicators, sample_candles):
"""Test SMA with insufficient data."""
"""Test SMA with insufficient data (now returns DataFrame)."""
period = 50 # More than available candles
results = indicators.sma(sample_candles, period)
assert len(results) == 0
df = indicators._prepare_dataframe_from_list(sample_candles)
df['timestamp'] = df.index
result_df = indicators.sma(df, period)
assert isinstance(result_df, pd.DataFrame)
assert result_df.empty
def test_ema_calculation(self, indicators, sample_candles):
"""Test Exponential Moving Average calculation."""
"""Test Exponential Moving Average calculation (now returns DataFrame)."""
period = 10
results = indicators.ema(sample_candles, period)
df = indicators._prepare_dataframe_from_list(sample_candles)
df['timestamp'] = df.index
result_df = indicators.ema(df, period)
# Should have results starting from period 10
assert len(results) == len(sample_candles) - period + 1
# Check first result
first_result = results[0]
assert isinstance(first_result, IndicatorResult)
assert 'ema' in first_result.values
assert first_result.metadata['period'] == period
assert isinstance(result_df, pd.DataFrame)
assert len(result_df) == len(sample_candles) - period + 1
assert 'ema' in result_df.columns
# EMA should be between the range of input prices
min_price = min(float(c.close) for c in sample_candles[:period])
max_price = max(float(c.close) for c in sample_candles[:period])
assert min_price <= first_result.values['ema'] <= max_price
assert min_price <= result_df.iloc[0]['ema'] <= max_price
def test_rsi_calculation(self, indicators, sample_candles):
"""Test Relative Strength Index calculation."""
"""Test Relative Strength Index calculation (now returns DataFrame)."""
period = 14
results = indicators.rsi(sample_candles, period)
# Should have results starting from period 15 (period + 1 for price change calculation)
assert len(results) == len(sample_candles) - period
# Check first result
first_result = results[0]
assert isinstance(first_result, IndicatorResult)
assert 'rsi' in first_result.values
assert 0 <= first_result.values['rsi'] <= 100 # RSI should be between 0 and 100
assert first_result.metadata['period'] == period
df = indicators._prepare_dataframe_from_list(sample_candles)
df['timestamp'] = df.index
result_df = indicators.rsi(df, period)
assert isinstance(result_df, pd.DataFrame)
assert not result_df.empty
assert 'rsi' in result_df.columns
assert 0 <= result_df.iloc[0]['rsi'] <= 100
def test_macd_calculation(self, indicators, sample_candles):
"""Test MACD calculation."""
"""Test MACD calculation (now returns DataFrame)."""
fast_period = 12
slow_period = 26
signal_period = 9
results = indicators.macd(sample_candles, fast_period, slow_period, signal_period)
# MACD needs slow_period + signal_period data points
expected_count = len(sample_candles) - slow_period - signal_period + 1
assert len(results) == max(0, expected_count)
if results: # Only test if we have results
first_result = results[0]
assert isinstance(first_result, IndicatorResult)
assert 'macd' in first_result.values
assert 'signal' in first_result.values
assert 'histogram' in first_result.values
df = indicators._prepare_dataframe_from_list(sample_candles)
df['timestamp'] = df.index
result_df = indicators.macd(df, fast_period, slow_period, signal_period)
# MACD results start after max(slow_period, signal_period) - 1 rows
min_required = max(slow_period, signal_period)
expected_count = max(0, len(sample_candles) - (min_required - 1))
assert isinstance(result_df, pd.DataFrame)
assert len(result_df) == expected_count
assert 'macd' in result_df.columns
assert 'signal' in result_df.columns
assert 'histogram' in result_df.columns
if not result_df.empty:
# Histogram should equal MACD - Signal
expected_histogram = first_result.values['macd'] - first_result.values['signal']
assert abs(first_result.values['histogram'] - expected_histogram) < 0.001
first_row = result_df.iloc[0]
expected_histogram = first_row['macd'] - first_row['signal']
assert abs(first_row['histogram'] - expected_histogram) < 0.001
def test_bollinger_bands_calculation(self, indicators, sample_candles):
"""Test Bollinger Bands calculation."""
"""Test Bollinger Bands calculation (now returns DataFrame)."""
period = 20
std_dev = 2.0
results = indicators.bollinger_bands(sample_candles, period, std_dev)
df = indicators._prepare_dataframe_from_list(sample_candles)
df['timestamp'] = df.index
result_df = indicators.bollinger_bands(df, period, std_dev)
# Should have results starting from period 20
assert len(results) == len(sample_candles) - period + 1
# Check first result
first_result = results[0]
assert isinstance(first_result, IndicatorResult)
assert 'upper_band' in first_result.values
assert 'middle_band' in first_result.values
assert 'lower_band' in first_result.values
assert 'bandwidth' in first_result.values
assert 'percent_b' in first_result.values
assert isinstance(result_df, pd.DataFrame)
assert len(result_df) == len(sample_candles) - period + 1
assert 'upper_band' in result_df.columns
assert 'middle_band' in result_df.columns
assert 'lower_band' in result_df.columns
# Upper band should be greater than middle band, which should be greater than lower band
assert first_result.values['upper_band'] > first_result.values['middle_band']
assert first_result.values['middle_band'] > first_result.values['lower_band']
first_row = result_df.iloc[0]
assert first_row['upper_band'] > first_row['middle_band']
assert first_row['middle_band'] > first_row['lower_band']
def test_sparse_data_handling(self, indicators, sparse_candles):
"""Test indicators with sparse data (time gaps)."""
period = 5
sma_results = indicators.sma(sparse_candles, period)
df = indicators._prepare_dataframe_from_list(sparse_candles)
df['timestamp'] = df.index
sma_df = indicators.sma(df, period)
# Should handle sparse data without issues
assert len(sma_results) > 0
assert not sma_df.empty
# Check that timestamps are preserved correctly
for result in sma_results:
assert result.timestamp is not None
assert isinstance(result.timestamp, datetime)
for ts in sma_df.index:
assert ts is not None
assert isinstance(ts, datetime)
def test_calculate_multiple_indicators(self, indicators, sample_candles):
"""Test calculating multiple indicators at once."""
@ -236,16 +220,15 @@ class TestTechnicalIndicators:
'macd': {'type': 'macd'},
'bb_20': {'type': 'bollinger_bands', 'period': 20}
}
results = indicators.calculate_multiple_indicators(sample_candles, config)
df = indicators._prepare_dataframe_from_list(sample_candles)
df['timestamp'] = df.index
results = indicators.calculate_multiple_indicators(df, config)
assert len(results) == len(config)
assert 'sma_10' in results
assert 'ema_12' in results
assert 'rsi_14' in results
assert 'macd' in results
assert 'bb_20' in results
# Check that each indicator has appropriate results
assert len(results['sma_10']) > 0
assert len(results['ema_12']) > 0
@ -255,21 +238,20 @@ class TestTechnicalIndicators:
config = {
'invalid_indicator': {'type': 'unknown_type', 'period': 10}
}
results = indicators.calculate_multiple_indicators(sample_candles, config)
df = indicators._prepare_dataframe_from_list(sample_candles)
results = indicators.calculate_multiple_indicators(df, config)
assert 'invalid_indicator' in results
assert len(results['invalid_indicator']) == 0 # Should return empty list
def test_different_price_columns(self, indicators, sample_candles):
"""Test indicators with different price columns."""
"""Test indicators with different price columns (now returns DataFrame)."""
df = indicators._prepare_dataframe_from_list(sample_candles)
# Test SMA with 'high' price column
sma_high = indicators.sma(sample_candles, 5, price_column='high')
sma_close = indicators.sma(sample_candles, 5, price_column='close')
sma_high = indicators.sma(df, 5, price_column='high')
sma_close = indicators.sma(df, 5, price_column='close')
assert len(sma_high) == len(sma_close)
# High prices should generally give higher SMA values
assert sma_high[0].values['sma'] >= sma_close[0].values['sma']
assert sma_high.iloc[0]['sma'] >= sma_close.iloc[0]['sma']
class TestIndicatorHelperFunctions:

View File

@ -190,11 +190,9 @@ class TestTechnicalIndicatorsSafety:
"""Test indicators with sparse data (time gaps)."""
period = 5
df = indicators._prepare_dataframe_from_list(sparse_candles)
sma_results = indicators.sma(df, period)
assert len(sma_results) > 0
# Verify that gaps are preserved (no interpolation)
timestamps = [r.timestamp for r in sma_results]
sma_df = indicators.sma(df, period)
assert not sma_df.empty
timestamps = sma_df.index.to_list()
for i in range(1, len(timestamps)):
time_diff = timestamps[i] - timestamps[i-1]
assert time_diff >= timedelta(minutes=1)