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:
parent
fc3cac24bd
commit
ec8f5514bb
@ -11,6 +11,7 @@ from datetime import datetime, timezone
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import json
|
import json
|
||||||
from enum import Enum
|
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.indicators import TechnicalIndicators, IndicatorResult, create_default_indicators_config, validate_indicator_config
|
||||||
from data.common.data_types import OHLCVCandle
|
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]],
|
def calculate_indicators(candles: List[Dict[str, Any]],
|
||||||
indicator_configs: List[str],
|
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.
|
Calculate technical indicators for chart display.
|
||||||
|
|
||||||
@ -485,7 +486,7 @@ def calculate_indicators(candles: List[Dict[str, Any]],
|
|||||||
custom_configs: Optional custom indicator configurations
|
custom_configs: Optional custom indicator configurations
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary mapping indicator names to their calculation results
|
Dictionary mapping indicator names to their calculation results as DataFrames
|
||||||
"""
|
"""
|
||||||
if not candles:
|
if not candles:
|
||||||
logger.warning("Indicator Definitions: No candles provided for indicator calculation")
|
logger.warning("Indicator Definitions: No candles provided for indicator calculation")
|
||||||
@ -520,6 +521,7 @@ def calculate_indicators(candles: List[Dict[str, Any]],
|
|||||||
# Calculate indicators
|
# Calculate indicators
|
||||||
try:
|
try:
|
||||||
results = indicators_calc.calculate_multiple_indicators(ohlcv_candles, configs_to_calculate)
|
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")
|
logger.debug(f"Indicator Definitions: Calculated {len(results)} indicators successfully")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|||||||
@ -466,7 +466,9 @@ class MarketDataIntegrator:
|
|||||||
symbol: str,
|
symbol: str,
|
||||||
exchange: str = "okx"
|
exchange: str = "okx"
|
||||||
) -> Dict[str, pd.DataFrame]:
|
) -> Dict[str, pd.DataFrame]:
|
||||||
|
"""
|
||||||
|
Get indicator data for chart display. Returns a dict mapping indicator IDs to DataFrames.
|
||||||
|
"""
|
||||||
indicator_data_map = {}
|
indicator_data_map = {}
|
||||||
if main_df.empty:
|
if main_df.empty:
|
||||||
return indicator_data_map
|
return indicator_data_map
|
||||||
@ -504,27 +506,14 @@ class MarketDataIntegrator:
|
|||||||
# Use main chart's dataframe
|
# Use main chart's dataframe
|
||||||
indicator_df = main_df
|
indicator_df = main_df
|
||||||
|
|
||||||
# Calculate the indicator
|
# Calculate the indicator (now returns DataFrame)
|
||||||
indicator_result_pkg = self.indicators.calculate(
|
result_df = self.indicators.calculate(
|
||||||
indicator.type,
|
indicator.type,
|
||||||
indicator_df,
|
indicator_df,
|
||||||
**indicator.parameters
|
**indicator.parameters
|
||||||
)
|
)
|
||||||
|
|
||||||
if indicator_result_pkg and indicator_result_pkg.get('data'):
|
if result_df is not None and not result_df.empty:
|
||||||
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)
|
|
||||||
|
|
||||||
# Ensure timezone consistency before reindexing
|
# Ensure timezone consistency before reindexing
|
||||||
if result_df.index.tz is None:
|
if result_df.index.tz is None:
|
||||||
result_df = result_df.tz_localize('UTC')
|
result_df = result_df.tz_localize('UTC')
|
||||||
|
|||||||
@ -68,8 +68,14 @@ class BaseIndicator(ABC):
|
|||||||
df = df.sort_values('timestamp').reset_index(drop=True)
|
df = df.sort_values('timestamp').reset_index(drop=True)
|
||||||
|
|
||||||
# Set timestamp as index for time-series operations
|
# 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)
|
df.set_index('timestamp', inplace=True)
|
||||||
|
|
||||||
|
# Ensure it's datetime
|
||||||
|
df['timestamp'] = df.index
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@ -2,11 +2,9 @@
|
|||||||
Bollinger Bands indicator implementation.
|
Bollinger Bands indicator implementation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from ..base import BaseIndicator
|
from ..base import BaseIndicator
|
||||||
from ..result import IndicatorResult
|
|
||||||
|
|
||||||
|
|
||||||
class BollingerBandsIndicator(BaseIndicator):
|
class BollingerBandsIndicator(BaseIndicator):
|
||||||
@ -18,7 +16,7 @@ class BollingerBandsIndicator(BaseIndicator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def calculate(self, df: pd.DataFrame, period: int = 20,
|
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.
|
Calculate Bollinger Bands.
|
||||||
|
|
||||||
@ -29,53 +27,20 @@ class BollingerBandsIndicator(BaseIndicator):
|
|||||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||||
|
|
||||||
Returns:
|
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
|
# Validate input data
|
||||||
if not self.validate_dataframe(df, period):
|
if not self.validate_dataframe(df, period):
|
||||||
return []
|
return pd.DataFrame()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Calculate middle band (SMA)
|
df = df.copy()
|
||||||
df['middle_band'] = df[price_column].rolling(window=period, min_periods=period).mean()
|
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()
|
df['std'] = df[price_column].rolling(window=period, min_periods=period).std()
|
||||||
|
df['upper_band'] = df['middle_band'] + (df['std'] * std_dev)
|
||||||
# Calculate upper and lower bands
|
df['lower_band'] = df['middle_band'] - (df['std'] * std_dev)
|
||||||
df['upper_band'] = df['middle_band'] + (std_dev * df['std'])
|
# Only keep rows with valid bands, and only 'timestamp', 'upper_band', 'middle_band', 'lower_band' columns
|
||||||
df['lower_band'] = df['middle_band'] - (std_dev * df['std'])
|
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)
|
||||||
# Calculate bandwidth and %B
|
return result_df
|
||||||
df['bandwidth'] = (df['upper_band'] - df['lower_band']) / df['middle_band']
|
except Exception:
|
||||||
df['percent_b'] = (df[price_column] - df['lower_band']) / (df['upper_band'] - df['lower_band'])
|
return pd.DataFrame()
|
||||||
|
|
||||||
# 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 []
|
|
||||||
@ -2,11 +2,9 @@
|
|||||||
Exponential Moving Average (EMA) indicator implementation.
|
Exponential Moving Average (EMA) indicator implementation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from ..base import BaseIndicator
|
from ..base import BaseIndicator
|
||||||
from ..result import IndicatorResult
|
|
||||||
|
|
||||||
|
|
||||||
class EMAIndicator(BaseIndicator):
|
class EMAIndicator(BaseIndicator):
|
||||||
@ -18,7 +16,7 @@ class EMAIndicator(BaseIndicator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def calculate(self, df: pd.DataFrame, period: int = 20,
|
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).
|
Calculate Exponential Moving Average (EMA).
|
||||||
|
|
||||||
@ -28,33 +26,19 @@ class EMAIndicator(BaseIndicator):
|
|||||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of indicator results with EMA values
|
DataFrame with EMA values and metadata, indexed by timestamp
|
||||||
"""
|
"""
|
||||||
# Validate input data
|
# Validate input data
|
||||||
if not self.validate_dataframe(df, period):
|
if not self.validate_dataframe(df, period):
|
||||||
return []
|
return pd.DataFrame()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Calculate EMA using pandas exponential weighted moving average
|
df = df.copy()
|
||||||
df['ema'] = df[price_column].ewm(span=period, adjust=False).mean()
|
df['ema'] = df[price_column].ewm(span=period, adjust=False).mean()
|
||||||
|
# Only keep rows with valid EMA, and only 'timestamp' and 'ema' columns
|
||||||
# Convert results to IndicatorResult objects
|
result_df = df.loc[df['ema'].notna(), ['timestamp', 'ema']].copy()
|
||||||
results = []
|
# Only keep rows after enough data for EMA
|
||||||
for i, (timestamp, row) in enumerate(df.iterrows()):
|
result_df = result_df.iloc[period-1:]
|
||||||
# Only return results after minimum period
|
result_df.set_index('timestamp', inplace=True)
|
||||||
if i >= period - 1 and not pd.isna(row['ema']):
|
return result_df
|
||||||
result = IndicatorResult(
|
except Exception:
|
||||||
timestamp=timestamp,
|
return pd.DataFrame()
|
||||||
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 []
|
|
||||||
@ -2,11 +2,9 @@
|
|||||||
Moving Average Convergence Divergence (MACD) indicator implementation.
|
Moving Average Convergence Divergence (MACD) indicator implementation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from ..base import BaseIndicator
|
from ..base import BaseIndicator
|
||||||
from ..result import IndicatorResult
|
|
||||||
|
|
||||||
|
|
||||||
class MACDIndicator(BaseIndicator):
|
class MACDIndicator(BaseIndicator):
|
||||||
@ -20,7 +18,7 @@ class MACDIndicator(BaseIndicator):
|
|||||||
|
|
||||||
def calculate(self, df: pd.DataFrame, fast_period: int = 12,
|
def calculate(self, df: pd.DataFrame, fast_period: int = 12,
|
||||||
slow_period: int = 26, signal_period: int = 9,
|
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).
|
Calculate Moving Average Convergence Divergence (MACD).
|
||||||
|
|
||||||
@ -32,53 +30,23 @@ class MACDIndicator(BaseIndicator):
|
|||||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of indicator results with MACD, signal, and histogram values
|
DataFrame with MACD values and metadata, indexed by timestamp
|
||||||
"""
|
"""
|
||||||
# Validate input data
|
# Validate input data
|
||||||
if not self.validate_dataframe(df, slow_period):
|
if not self.validate_dataframe(df, slow_period):
|
||||||
return []
|
return pd.DataFrame()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Calculate fast and slow EMAs
|
df = df.copy()
|
||||||
df['ema_fast'] = df[price_column].ewm(span=fast_period, adjust=False).mean()
|
df['macd'] = df[price_column].ewm(span=fast_period, adjust=False).mean() - \
|
||||||
df['ema_slow'] = df[price_column].ewm(span=slow_period, adjust=False).mean()
|
df[price_column].ewm(span=slow_period, adjust=False).mean()
|
||||||
|
|
||||||
# Calculate MACD line
|
|
||||||
df['macd'] = df['ema_fast'] - df['ema_slow']
|
|
||||||
|
|
||||||
# Calculate signal line (EMA of MACD)
|
|
||||||
df['signal'] = df['macd'].ewm(span=signal_period, adjust=False).mean()
|
df['signal'] = df['macd'].ewm(span=signal_period, adjust=False).mean()
|
||||||
|
|
||||||
# Calculate histogram
|
|
||||||
df['histogram'] = df['macd'] - df['signal']
|
df['histogram'] = df['macd'] - df['signal']
|
||||||
|
# Only keep rows with valid MACD, and only 'timestamp', 'macd', 'signal', 'histogram' columns
|
||||||
# Convert results to IndicatorResult objects
|
result_df = df.loc[df['macd'].notna() & df['signal'].notna() & df['histogram'].notna(), ['timestamp', 'macd', 'signal', 'histogram']].copy()
|
||||||
results = []
|
# Only keep rows after enough data for MACD and signal
|
||||||
for i, (timestamp, row) in enumerate(df.iterrows()):
|
min_required = max(slow_period, signal_period)
|
||||||
# Only return results after minimum period
|
result_df = result_df.iloc[min_required-1:]
|
||||||
if i >= slow_period - 1:
|
result_df.set_index('timestamp', inplace=True)
|
||||||
if not (pd.isna(row['macd']) or pd.isna(row['signal']) or pd.isna(row['histogram'])):
|
return result_df
|
||||||
result = IndicatorResult(
|
except Exception:
|
||||||
timestamp=timestamp,
|
return pd.DataFrame()
|
||||||
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 []
|
|
||||||
@ -4,6 +4,7 @@ Relative Strength Index (RSI) indicator implementation.
|
|||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from ..base import BaseIndicator
|
from ..base import BaseIndicator
|
||||||
from ..result import IndicatorResult
|
from ..result import IndicatorResult
|
||||||
@ -18,7 +19,7 @@ class RSIIndicator(BaseIndicator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def calculate(self, df: pd.DataFrame, period: int = 14,
|
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).
|
Calculate Relative Strength Index (RSI).
|
||||||
|
|
||||||
@ -28,48 +29,23 @@ class RSIIndicator(BaseIndicator):
|
|||||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of indicator results with RSI values
|
DataFrame with RSI values and metadata, indexed by timestamp
|
||||||
"""
|
"""
|
||||||
# Validate input data
|
# Validate input data
|
||||||
if not self.validate_dataframe(df, period + 1): # Need extra period for diff
|
if not self.validate_dataframe(df, period):
|
||||||
return []
|
return pd.DataFrame()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Calculate price changes
|
df = df.copy()
|
||||||
df['price_change'] = df[price_column].diff()
|
delta = df[price_column].diff()
|
||||||
|
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
|
||||||
# Separate gains and losses
|
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
|
||||||
df['gain'] = df['price_change'].where(df['price_change'] > 0, 0)
|
rs = gain / loss
|
||||||
df['loss'] = (-df['price_change']).where(df['price_change'] < 0, 0)
|
rs = rs.replace([np.inf, -np.inf], np.nan)
|
||||||
|
df['rsi'] = 100 - (100 / (1 + rs))
|
||||||
# Calculate average gain and loss using EMA
|
# Only keep rows with valid RSI, and only 'timestamp' and 'rsi' columns
|
||||||
df['avg_gain'] = df['gain'].ewm(span=period, adjust=False).mean()
|
result_df = df.loc[df['rsi'].notna(), ['timestamp', 'rsi']].copy()
|
||||||
df['avg_loss'] = df['loss'].ewm(span=period, adjust=False).mean()
|
result_df = result_df.iloc[period-1:]
|
||||||
|
result_df.set_index('timestamp', inplace=True)
|
||||||
# Calculate RS and RSI
|
return result_df
|
||||||
df['rs'] = df['avg_gain'] / df['avg_loss']
|
except Exception:
|
||||||
df['rsi'] = 100 - (100 / (1 + df['rs']))
|
return pd.DataFrame()
|
||||||
|
|
||||||
# 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 []
|
|
||||||
@ -2,11 +2,9 @@
|
|||||||
Simple Moving Average (SMA) indicator implementation.
|
Simple Moving Average (SMA) indicator implementation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from ..base import BaseIndicator
|
from ..base import BaseIndicator
|
||||||
from ..result import IndicatorResult
|
|
||||||
|
|
||||||
|
|
||||||
class SMAIndicator(BaseIndicator):
|
class SMAIndicator(BaseIndicator):
|
||||||
@ -18,7 +16,7 @@ class SMAIndicator(BaseIndicator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def calculate(self, df: pd.DataFrame, period: int = 20,
|
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).
|
Calculate Simple Moving Average (SMA).
|
||||||
|
|
||||||
@ -28,32 +26,18 @@ class SMAIndicator(BaseIndicator):
|
|||||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of indicator results with SMA values
|
DataFrame with SMA values and metadata, indexed by timestamp
|
||||||
"""
|
"""
|
||||||
# Validate input data
|
# Validate input data
|
||||||
if not self.validate_dataframe(df, period):
|
if not self.validate_dataframe(df, period):
|
||||||
return []
|
return pd.DataFrame()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Calculate SMA using pandas rolling window
|
df = df.copy()
|
||||||
df['sma'] = df[price_column].rolling(window=period, min_periods=period).mean()
|
df['sma'] = df[price_column].rolling(window=period, min_periods=period).mean()
|
||||||
|
# Only keep rows with valid SMA, and only 'timestamp' and 'sma' columns
|
||||||
# Convert results to IndicatorResult objects
|
result_df = df.loc[df['sma'].notna(), ['timestamp', 'sma']].copy()
|
||||||
results = []
|
result_df = result_df.iloc[period-1:]
|
||||||
for timestamp, row in df.iterrows():
|
result_df.set_index('timestamp', inplace=True)
|
||||||
if not pd.isna(row['sma']):
|
return result_df
|
||||||
result = IndicatorResult(
|
except Exception:
|
||||||
timestamp=timestamp,
|
return pd.DataFrame()
|
||||||
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 []
|
|
||||||
@ -10,22 +10,14 @@ IMPORTANT: Handles Sparse Data
|
|||||||
- Uses pandas for efficient vectorized calculations
|
- Uses pandas for efficient vectorized calculations
|
||||||
- Follows right-aligned timestamp convention
|
- Follows right-aligned timestamp convention
|
||||||
|
|
||||||
Supported Indicators:
|
TODO: need make more procedural without hardcoding indicators type and so on
|
||||||
- Simple Moving Average (SMA)
|
|
||||||
- Exponential Moving Average (EMA)
|
|
||||||
- Relative Strength Index (RSI)
|
|
||||||
- Moving Average Convergence Divergence (MACD)
|
|
||||||
- Bollinger Bands
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from typing import Dict, List, Optional, Any
|
||||||
from typing import Dict, List, Optional, Any, Union
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from .result import IndicatorResult
|
|
||||||
from ..data_types import OHLCVCandle
|
from ..data_types import OHLCVCandle
|
||||||
from .base import BaseIndicator
|
|
||||||
from .implementations import (
|
from .implementations import (
|
||||||
SMAIndicator,
|
SMAIndicator,
|
||||||
EMAIndicator,
|
EMAIndicator,
|
||||||
@ -85,7 +77,7 @@ class TechnicalIndicators:
|
|||||||
return self._sma.prepare_dataframe(candles)
|
return self._sma.prepare_dataframe(candles)
|
||||||
|
|
||||||
def sma(self, df: pd.DataFrame, period: int,
|
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).
|
Calculate Simple Moving Average (SMA).
|
||||||
|
|
||||||
@ -95,12 +87,12 @@ class TechnicalIndicators:
|
|||||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of indicator results with SMA values
|
DataFrame with SMA values
|
||||||
"""
|
"""
|
||||||
return self._sma.calculate(df, period=period, price_column=price_column)
|
return self._sma.calculate(df, period=period, price_column=price_column)
|
||||||
|
|
||||||
def ema(self, df: pd.DataFrame, period: int,
|
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).
|
Calculate Exponential Moving Average (EMA).
|
||||||
|
|
||||||
@ -115,7 +107,7 @@ class TechnicalIndicators:
|
|||||||
return self._ema.calculate(df, period=period, price_column=price_column)
|
return self._ema.calculate(df, period=period, price_column=price_column)
|
||||||
|
|
||||||
def rsi(self, df: pd.DataFrame, period: int = 14,
|
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).
|
Calculate Relative Strength Index (RSI).
|
||||||
|
|
||||||
@ -125,13 +117,13 @@ class TechnicalIndicators:
|
|||||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of indicator results with RSI values
|
DataFrame with RSI values
|
||||||
"""
|
"""
|
||||||
return self._rsi.calculate(df, period=period, price_column=price_column)
|
return self._rsi.calculate(df, period=period, price_column=price_column)
|
||||||
|
|
||||||
def macd(self, df: pd.DataFrame,
|
def macd(self, df: pd.DataFrame,
|
||||||
fast_period: int = 12, slow_period: int = 26, signal_period: int = 9,
|
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).
|
Calculate Moving Average Convergence Divergence (MACD).
|
||||||
|
|
||||||
@ -154,7 +146,7 @@ class TechnicalIndicators:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def bollinger_bands(self, df: pd.DataFrame, period: int = 20,
|
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.
|
Calculate Bollinger Bands.
|
||||||
|
|
||||||
@ -165,7 +157,7 @@ class TechnicalIndicators:
|
|||||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||||
|
|
||||||
Returns:
|
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(
|
return self._bollinger.calculate(
|
||||||
df,
|
df,
|
||||||
@ -175,9 +167,8 @@ class TechnicalIndicators:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def calculate_multiple_indicators(self, df: pd.DataFrame,
|
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.
|
Calculate multiple indicators at once for efficiency.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -192,7 +183,7 @@ class TechnicalIndicators:
|
|||||||
}
|
}
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary mapping indicator names to their results
|
Dictionary mapping indicator names to their results as DataFrames
|
||||||
"""
|
"""
|
||||||
results = {}
|
results = {}
|
||||||
|
|
||||||
@ -235,16 +226,16 @@ class TechnicalIndicators:
|
|||||||
else:
|
else:
|
||||||
if self.logger:
|
if self.logger:
|
||||||
self.logger.warning(f"Unknown indicator type: {indicator_type}")
|
self.logger.warning(f"Unknown indicator type: {indicator_type}")
|
||||||
results[indicator_name] = []
|
results[indicator_name] = pd.DataFrame()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.logger:
|
if self.logger:
|
||||||
self.logger.error(f"Error calculating {indicator_name}: {e}")
|
self.logger.error(f"Error calculating {indicator_name}: {e}")
|
||||||
results[indicator_name] = []
|
results[indicator_name] = pd.DataFrame()
|
||||||
|
|
||||||
return results
|
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.
|
Calculate a single indicator with dynamic dispatch.
|
||||||
|
|
||||||
@ -254,7 +245,7 @@ class TechnicalIndicators:
|
|||||||
**kwargs: Indicator-specific parameters (e.g., period=20)
|
**kwargs: Indicator-specific parameters (e.g., period=20)
|
||||||
|
|
||||||
Returns:
|
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
|
# Get the indicator calculation method
|
||||||
indicator_method = getattr(self, indicator_type, None)
|
indicator_method = getattr(self, indicator_type, None)
|
||||||
@ -265,21 +256,13 @@ class TechnicalIndicators:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return {'data': [], 'metadata': {}}
|
return pd.DataFrame()
|
||||||
|
|
||||||
# Call the indicator method
|
# Call the indicator method (now returns DataFrame)
|
||||||
raw_result = indicator_method(df, **kwargs)
|
result_df = indicator_method(df, **kwargs)
|
||||||
|
|
||||||
# Extract metadata from the first result if available
|
# Return the DataFrame directly
|
||||||
metadata = raw_result[0].metadata if raw_result else {}
|
return result_df
|
||||||
|
|
||||||
# The methods return List[IndicatorResult], let's package that
|
|
||||||
if raw_result:
|
|
||||||
return {
|
|
||||||
"data": raw_result,
|
|
||||||
"metadata": metadata
|
|
||||||
}
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if self.logger:
|
if self.logger:
|
||||||
|
|||||||
@ -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).
|
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
|
## Table of Contents
|
||||||
|
|
||||||
1. [System Architecture](#system-architecture)
|
1. [System Architecture](#system-architecture)
|
||||||
@ -23,6 +30,7 @@ The Crypto Trading Bot Dashboard features a comprehensive modular indicator syst
|
|||||||
components/charts/
|
components/charts/
|
||||||
├── indicator_manager.py # Core indicator CRUD operations
|
├── indicator_manager.py # Core indicator CRUD operations
|
||||||
├── indicator_defaults.py # Default indicator templates
|
├── indicator_defaults.py # Default indicator templates
|
||||||
|
├── data_integration.py # DataFrame-based indicator calculations
|
||||||
├── layers/
|
├── layers/
|
||||||
│ ├── indicators.py # Overlay indicator rendering
|
│ ├── indicators.py # Overlay indicator rendering
|
||||||
│ └── subplots.py # Subplot indicator rendering
|
│ └── subplots.py # Subplot indicator rendering
|
||||||
@ -34,6 +42,15 @@ config/indicators/
|
|||||||
├── sma_abc123.json
|
├── sma_abc123.json
|
||||||
├── ema_def456.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
|
### Key Classes
|
||||||
@ -41,6 +58,7 @@ config/indicators/
|
|||||||
- **`IndicatorManager`**: Handles CRUD operations for user indicators
|
- **`IndicatorManager`**: Handles CRUD operations for user indicators
|
||||||
- **`UserIndicator`**: Data structure for indicator configuration
|
- **`UserIndicator`**: Data structure for indicator configuration
|
||||||
- **`IndicatorStyling`**: Appearance and styling configuration
|
- **`IndicatorStyling`**: Appearance and styling configuration
|
||||||
|
- **`TechnicalIndicators`**: Vectorized calculation engine
|
||||||
- **Indicator Layers**: Rendering classes for different indicator types
|
- **Indicator Layers**: Rendering classes for different indicator types
|
||||||
|
|
||||||
## Current Indicators
|
## Current Indicators
|
||||||
@ -48,19 +66,19 @@ config/indicators/
|
|||||||
### Overlay Indicators
|
### Overlay Indicators
|
||||||
These indicators are displayed directly on the price chart:
|
These indicators are displayed directly on the price chart:
|
||||||
|
|
||||||
| Indicator | Type | Parameters | Description |
|
| Indicator | Type | Parameters | Description | Output Columns |
|
||||||
|-----------|------|------------|-------------|
|
|-----------|------|------------|-------------|----------------|
|
||||||
| **Simple Moving Average (SMA)** | `sma` | `period` (1-200) | Average price over N periods |
|
| **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 |
|
| **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 |
|
| **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
|
### Subplot Indicators
|
||||||
These indicators are displayed in separate panels:
|
These indicators are displayed in separate panels:
|
||||||
|
|
||||||
| Indicator | Type | Parameters | Description |
|
| Indicator | Type | Parameters | Description | Output Columns |
|
||||||
|-----------|------|------------|-------------|
|
|-----------|------|------------|-------------|----------------|
|
||||||
| **Relative Strength Index (RSI)** | `rsi` | `period` (2-50) | Momentum oscillator (0-100 scale) |
|
| **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** | `macd` | `fast_period` (2-50), `slow_period` (5-100), `signal_period` (2-30) | Moving average convergence divergence | `['macd', 'signal', 'histogram']` |
|
||||||
|
|
||||||
## User Interface
|
## User Interface
|
||||||
|
|
||||||
@ -212,6 +230,20 @@ class IndicatorManager:
|
|||||||
def get_indicators_by_type(self, display_type: str) -> List[UserIndicator]
|
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
|
### Usage Examples
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@ -246,6 +278,86 @@ all_indicators = manager.list_indicators()
|
|||||||
# Get by type
|
# Get by type
|
||||||
overlay_indicators = manager.get_indicators_by_type("overlay")
|
overlay_indicators = manager.get_indicators_by_type("overlay")
|
||||||
subplot_indicators = manager.get_indicators_by_type("subplot")
|
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
|
## Troubleshooting
|
||||||
@ -263,8 +375,14 @@ subplot_indicators = manager.get_indicators_by_type("subplot")
|
|||||||
3. **Chart not updating**
|
3. **Chart not updating**
|
||||||
- Verify the indicator layer implements `calculate_values` and `create_traces`
|
- Verify the indicator layer implements `calculate_values` and `create_traces`
|
||||||
- Check if indicator is registered in the correct registry
|
- 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
|
- Ensure `config/indicators/user_indicators/` directory is writable
|
||||||
- Check file permissions on existing JSON files
|
- 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
|
- Look at application logs for Python exceptions
|
||||||
- Verify JSON file structure with a validator
|
- Verify JSON file structure with a validator
|
||||||
- Test indicator calculations with sample data
|
- Test indicator calculations with sample data
|
||||||
|
- Check DataFrame structure and column names
|
||||||
|
|
||||||
### Performance Considerations
|
### Performance Considerations
|
||||||
|
|
||||||
|
- **Vectorized calculations** provide significant performance improvements
|
||||||
- Indicators with large periods may take longer to calculate
|
- Indicators with large periods may take longer to calculate
|
||||||
- Consider data availability when setting parameter limits
|
- Consider data availability when setting parameter limits
|
||||||
- Subplot indicators require additional chart space
|
- Subplot indicators require additional chart space
|
||||||
- Real-time updates may impact performance with many indicators
|
- 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
|
## Best Practices
|
||||||
|
|
||||||
1. **Naming Conventions**
|
1. **Naming Conventions**
|
||||||
@ -300,6 +430,7 @@ subplot_indicators = manager.get_indicators_by_type("subplot")
|
|||||||
- Log errors for debugging
|
- Log errors for debugging
|
||||||
|
|
||||||
4. **Performance**
|
4. **Performance**
|
||||||
|
- Leverage vectorized calculations for speed
|
||||||
- Cache calculated values when possible
|
- Cache calculated values when possible
|
||||||
- Optimize calculation algorithms
|
- Optimize calculation algorithms
|
||||||
- Limit the number of active indicators
|
- Limit the number of active indicators
|
||||||
@ -307,7 +438,14 @@ subplot_indicators = manager.get_indicators_by_type("subplot")
|
|||||||
5. **User Experience**
|
5. **User Experience**
|
||||||
- Provide immediate visual feedback
|
- Provide immediate visual feedback
|
||||||
- Use intuitive color schemes
|
- 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`)]*
|
*Back to [Chart System Documentation (`README.md`)]*
|
||||||
@ -2,7 +2,16 @@
|
|||||||
|
|
||||||
## Overview
|
## 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
|
## Architecture
|
||||||
|
|
||||||
@ -12,7 +21,7 @@ data/common/indicators/
|
|||||||
├── __init__.py # Package exports
|
├── __init__.py # Package exports
|
||||||
├── technical.py # Main facade class
|
├── technical.py # Main facade class
|
||||||
├── base.py # Base indicator class
|
├── base.py # Base indicator class
|
||||||
├── result.py # Result container class
|
├── result.py # Result container class (legacy)
|
||||||
├── utils.py # Utility functions
|
├── utils.py # Utility functions
|
||||||
└── implementations/ # Individual indicator implementations
|
└── implementations/ # Individual indicator implementations
|
||||||
├── __init__.py
|
├── __init__.py
|
||||||
@ -27,24 +36,23 @@ data/common/indicators/
|
|||||||
|
|
||||||
#### 1. Base Classes
|
#### 1. Base Classes
|
||||||
- **BaseIndicator**: Abstract base class providing common functionality
|
- **BaseIndicator**: Abstract base class providing common functionality
|
||||||
- Data preparation
|
- Data preparation with timestamp handling
|
||||||
- Validation
|
- Validation and error handling
|
||||||
- Error handling
|
- Logging support
|
||||||
- Logging
|
|
||||||
|
|
||||||
#### 2. Individual Indicators
|
#### 2. Individual Indicators
|
||||||
Each indicator is implemented as a separate class inheriting from `BaseIndicator`:
|
Each indicator is implemented as a separate class inheriting from `BaseIndicator`:
|
||||||
- Focused responsibility
|
- **Vectorized calculations** using pandas operations
|
||||||
- Independent testing
|
- **Clean DataFrame output** with only relevant columns
|
||||||
- Easy maintenance
|
- **Proper warm-up periods** for safe trading
|
||||||
- Clear documentation
|
- **Independent testing** and maintenance
|
||||||
|
|
||||||
#### 3. TechnicalIndicators Facade
|
#### 3. TechnicalIndicators Facade
|
||||||
Main entry point providing:
|
Main entry point providing:
|
||||||
- Unified interface
|
- Unified DataFrame-based interface
|
||||||
- Batch calculations
|
- Batch calculations
|
||||||
- Consistent error handling
|
- Consistent error handling
|
||||||
- Data preparation
|
- Data preparation utilities
|
||||||
|
|
||||||
## Supported Indicators
|
## Supported Indicators
|
||||||
|
|
||||||
@ -53,73 +61,112 @@ Main entry point providing:
|
|||||||
from data.common.indicators import TechnicalIndicators
|
from data.common.indicators import TechnicalIndicators
|
||||||
|
|
||||||
indicators = 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**:
|
- **Parameters**:
|
||||||
- `period`: Number of periods (default: 20)
|
- `period`: Number of periods (default: 20)
|
||||||
- `price_column`: Column to average (default: 'close')
|
- `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)
|
### Exponential Moving Average (EMA)
|
||||||
```python
|
```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**:
|
- **Parameters**:
|
||||||
- `period`: Number of periods (default: 20)
|
- `period`: Number of periods (default: 20)
|
||||||
- `price_column`: Column to average (default: 'close')
|
- `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)
|
### Relative Strength Index (RSI)
|
||||||
```python
|
```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**:
|
- **Parameters**:
|
||||||
- `period`: Number of periods (default: 14)
|
- `period`: Number of periods (default: 14)
|
||||||
- `price_column`: Column to analyze (default: 'close')
|
- `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)
|
### Moving Average Convergence Divergence (MACD)
|
||||||
```python
|
```python
|
||||||
results = indicators.macd(
|
result_df = indicators.macd(
|
||||||
df,
|
df,
|
||||||
fast_period=12,
|
fast_period=12,
|
||||||
slow_period=26,
|
slow_period=26,
|
||||||
signal_period=9,
|
signal_period=9,
|
||||||
price_column='close'
|
price_column='close'
|
||||||
)
|
)
|
||||||
|
# Returns DataFrame with columns: ['macd', 'signal', 'histogram'], indexed by timestamp
|
||||||
```
|
```
|
||||||
- **Parameters**:
|
- **Parameters**:
|
||||||
- `fast_period`: Fast EMA period (default: 12)
|
- `fast_period`: Fast EMA period (default: 12)
|
||||||
- `slow_period`: Slow EMA period (default: 26)
|
- `slow_period`: Slow EMA period (default: 26)
|
||||||
- `signal_period`: Signal line period (default: 9)
|
- `signal_period`: Signal line period (default: 9)
|
||||||
- `price_column`: Column to analyze (default: 'close')
|
- `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
|
### Bollinger Bands
|
||||||
```python
|
```python
|
||||||
results = indicators.bollinger_bands(
|
result_df = indicators.bollinger_bands(
|
||||||
df,
|
df,
|
||||||
period=20,
|
period=20,
|
||||||
std_dev=2.0,
|
std_dev=2.0,
|
||||||
price_column='close'
|
price_column='close'
|
||||||
)
|
)
|
||||||
|
# Returns DataFrame with columns: ['upper_band', 'middle_band', 'lower_band'], indexed by timestamp
|
||||||
```
|
```
|
||||||
- **Parameters**:
|
- **Parameters**:
|
||||||
- `period`: SMA period (default: 20)
|
- `period`: SMA period (default: 20)
|
||||||
- `std_dev`: Standard deviation multiplier (default: 2.0)
|
- `std_dev`: Standard deviation multiplier (default: 2.0)
|
||||||
- `price_column`: Column to analyze (default: 'close')
|
- `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
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage with DataFrame Output
|
||||||
```python
|
```python
|
||||||
from data.common.indicators import TechnicalIndicators
|
from data.common.indicators import TechnicalIndicators
|
||||||
|
|
||||||
# Initialize calculator
|
# Initialize calculator
|
||||||
indicators = TechnicalIndicators(logger=my_logger)
|
indicators = TechnicalIndicators(logger=my_logger)
|
||||||
|
|
||||||
# Calculate single indicator
|
# Calculate single indicator - returns DataFrame
|
||||||
sma_results = indicators.sma(df, period=20)
|
sma_df = indicators.sma(df, period=20)
|
||||||
|
|
||||||
# Access results
|
# Access results using DataFrame operations
|
||||||
for result in sma_results:
|
print(f"First SMA value: {sma_df['sma'].iloc[0]}")
|
||||||
print(f"Time: {result.timestamp}, SMA: {result.values['sma']}")
|
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
|
### 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)
|
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
|
```python
|
||||||
# Calculate any indicator by name
|
# Calculate SMA on the 'high' price
|
||||||
result = indicators.calculate(
|
sma_high_df = indicators.sma(df, period=20, price_column='high')
|
||||||
'macd',
|
|
||||||
df,
|
# Calculate RSI on the 'open' price
|
||||||
fast_period=12,
|
rsi_open_df = indicators.calculate('rsi', df, period=14, price_column='open')
|
||||||
slow_period=26,
|
|
||||||
signal_period=9
|
# 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
|
```python
|
||||||
@dataclass
|
from components.charts.utils import prepare_chart_data
|
||||||
class IndicatorResult:
|
|
||||||
timestamp: datetime # Right-aligned timestamp
|
# Prepare DataFrame from candle data
|
||||||
symbol: str # Trading symbol
|
df = prepare_chart_data(candles)
|
||||||
timeframe: str # Candle timeframe
|
# df has columns: ['open', 'high', 'low', 'close', 'volume'] with DatetimeIndex
|
||||||
values: Dict[str, float] # Indicator values
|
|
||||||
metadata: Optional[Dict[str, Any]] = None # Calculation metadata
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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:
|
```python
|
||||||
- Input validation
|
# Example: If you have gaps in your data
|
||||||
- Data sufficiency checks
|
# 09:00, 09:01, 09:02, 09:04, 09:05 (missing 09:03)
|
||||||
- Calculation error handling
|
# The indicators will calculate correctly using available data
|
||||||
- Detailed error logging
|
# 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
|
```python
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
logger.error(f"RSI calculation failed: {e}")
|
logger.error(f"RSI calculation failed: {e}")
|
||||||
results = []
|
# Handle calculation errors
|
||||||
```
|
```
|
||||||
|
|
||||||
## Performance Considerations
|
## Performance Considerations
|
||||||
|
|
||||||
1. **Data Preparation**
|
1. **Vectorized Operations**
|
||||||
- Uses pandas for vectorized calculations
|
- Uses pandas rolling/ewm functions for maximum performance
|
||||||
- Handles sparse data efficiently
|
- Minimal data copying and transformations
|
||||||
- Maintains timestamp alignment
|
- Efficient memory usage
|
||||||
|
|
||||||
2. **Memory Usage**
|
2. **DataFrame Alignment**
|
||||||
- Avoids unnecessary data copies
|
- Timestamp index ensures proper alignment with price data
|
||||||
- Cleans up temporary calculations
|
- Easy integration with plotting libraries
|
||||||
- Uses efficient data structures
|
- Consistent data structure across all indicators
|
||||||
|
|
||||||
3. **Calculation Optimization**
|
3. **Memory Efficiency**
|
||||||
- Vectorized operations where possible
|
- Returns only necessary columns
|
||||||
- Minimal data transformations
|
- No metadata overhead in result DataFrames
|
||||||
- Efficient algorithm implementations
|
- Clean, minimal output format
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
The module includes comprehensive tests:
|
The module includes comprehensive tests for the new DataFrame-based approach:
|
||||||
- Unit tests for each indicator
|
- Unit tests for each indicator's DataFrame output
|
||||||
- Integration tests for the facade
|
- Integration tests for the facade
|
||||||
- Edge case handling
|
- Edge case handling (gaps, insufficient data)
|
||||||
- Performance benchmarks
|
- Performance benchmarks
|
||||||
|
|
||||||
Run tests with:
|
Run tests with:
|
||||||
```bash
|
```bash
|
||||||
uv run pytest tests/test_indicators.py
|
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
|
## Contributing
|
||||||
@ -218,122 +306,42 @@ uv run pytest tests/test_indicators.py
|
|||||||
When adding new indicators:
|
When adding new indicators:
|
||||||
1. Create a new class in `implementations/`
|
1. Create a new class in `implementations/`
|
||||||
2. Inherit from `BaseIndicator`
|
2. Inherit from `BaseIndicator`
|
||||||
3. Implement the `calculate` method
|
3. Implement the `calculate` method to return a DataFrame
|
||||||
4. Add tests
|
4. Ensure proper warm-up periods
|
||||||
5. Update documentation
|
5. Add comprehensive tests
|
||||||
|
6. Update documentation
|
||||||
|
|
||||||
See [Adding New Indicators](./adding-new-indicators.md) for detailed instructions.
|
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.
|
### TechnicalIndicators Class
|
||||||
- **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
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from data.common.indicators import (
|
class TechnicalIndicators:
|
||||||
TechnicalIndicators,
|
def sma(self, df: pd.DataFrame, period: int, price_column: str = 'close') -> pd.DataFrame
|
||||||
IndicatorResult,
|
def ema(self, df: pd.DataFrame, period: int, price_column: str = 'close') -> pd.DataFrame
|
||||||
create_default_indicators_config,
|
def rsi(self, df: pd.DataFrame, period: int = 14, price_column: str = 'close') -> pd.DataFrame
|
||||||
validate_indicator_config
|
def macd(self, df: pd.DataFrame, fast_period: int = 12, slow_period: int = 26,
|
||||||
)
|
signal_period: int = 9, price_column: str = 'close') -> pd.DataFrame
|
||||||
from data.common.data_types import OHLCVCandle
|
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
|
### Return Format
|
||||||
|
All methods return:
|
||||||
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.
|
- **Success**: `pd.DataFrame` with indicator column(s) and DatetimeIndex
|
||||||
|
- **Failure/Insufficient Data**: `pd.DataFrame()` (empty DataFrame)
|
||||||
|
- **Error**: `None` (with logged error)
|
||||||
|
|
||||||
|
### DataFrame Structure
|
||||||
```python
|
```python
|
||||||
from components.charts.utils import prepare_chart_data
|
# Example SMA result
|
||||||
from data.common.indicators import TechnicalIndicators
|
result_df = indicators.sma(df, period=20)
|
||||||
|
# result_df.index: DatetimeIndex (timestamps)
|
||||||
# Assume 'candles' is a list of OHLCV dictionaries from the database
|
# result_df.columns: ['sma']
|
||||||
# candles = fetch_market_data(...)
|
# result_df.shape: (N, 1) where N = len(df) - period + 1 (after warm-up)
|
||||||
|
```
|
||||||
# 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`, `
|
|
||||||
@ -183,6 +183,15 @@
|
|||||||
- [ ] 13.8 Implement comprehensive data recovery with OKX REST API for historical backfill
|
- [ ] 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.9 Add gap detection and automatic data recovery during reconnections
|
||||||
- [ ] 13.10 Implement data integrity validation and conflict resolution for recovered data
|
- [ ] 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.0 Advanced Dashboard Performance and User Experience (Future Enhancement)
|
||||||
- [ ] 14.1 Implement dashboard state management with browser localStorage persistence
|
- [ ] 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.13 Implement memory usage optimization for long-running dashboard sessions
|
||||||
- [ ] 14.14 Add chart export capabilities (PNG, SVG, PDF) with high-quality rendering
|
- [ ] 14.14 Add chart export capabilities (PNG, SVG, PDF) with high-quality rendering
|
||||||
- [ ] 14.15 Implement dashboard mobile responsiveness and touch optimizations
|
- [ ] 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -94,138 +94,122 @@ class TestTechnicalIndicators:
|
|||||||
|
|
||||||
def test_prepare_dataframe(self, indicators, sample_candles):
|
def test_prepare_dataframe(self, indicators, sample_candles):
|
||||||
"""Test DataFrame preparation from OHLCV 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 not df.empty
|
||||||
assert len(df) == len(sample_candles)
|
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'
|
assert df.index.name == 'timestamp'
|
||||||
|
|
||||||
# Check that timestamps are sorted
|
# Check that timestamps are sorted
|
||||||
assert df.index.is_monotonic_increasing
|
assert df.index.is_monotonic_increasing
|
||||||
|
|
||||||
def test_prepare_dataframe_empty(self, indicators):
|
def test_prepare_dataframe_empty(self, indicators):
|
||||||
"""Test DataFrame preparation with empty candles list."""
|
"""Test DataFrame preparation with empty candles list."""
|
||||||
df = indicators.prepare_dataframe([])
|
df = indicators._prepare_dataframe_from_list([])
|
||||||
assert df.empty
|
assert df.empty
|
||||||
|
|
||||||
def test_sma_calculation(self, indicators, sample_candles):
|
def test_sma_calculation(self, indicators, sample_candles):
|
||||||
"""Test Simple Moving Average calculation."""
|
"""Test Simple Moving Average calculation (now returns DataFrame)."""
|
||||||
period = 5
|
period = 5
|
||||||
results = indicators.sma(sample_candles, period)
|
df = indicators._prepare_dataframe_from_list(sample_candles)
|
||||||
|
df['timestamp'] = df.index
|
||||||
# Should have results starting from period 5
|
result_df = indicators.sma(df, period)
|
||||||
assert len(results) == len(sample_candles) - period + 1
|
assert isinstance(result_df, pd.DataFrame)
|
||||||
|
assert not result_df.empty
|
||||||
# Check first result
|
assert 'sma' in result_df.columns
|
||||||
first_result = results[0]
|
# Find the correct rolling window for the first SMA value
|
||||||
assert isinstance(first_result, IndicatorResult)
|
first_ts = result_df.index[0]
|
||||||
assert first_result.symbol == 'BTC-USDT'
|
first_idx = [candle.end_time for candle in sample_candles].index(first_ts)
|
||||||
assert first_result.timeframe == '1m'
|
window_closes = [float(candle.close) for candle in sample_candles[first_idx - period + 1:first_idx + 1]]
|
||||||
assert 'sma' in first_result.values
|
expected_sma = sum(window_closes) / len(window_closes)
|
||||||
assert first_result.metadata['period'] == period
|
assert abs(result_df.iloc[0]['sma'] - expected_sma) < 0.001
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
def test_sma_insufficient_data(self, indicators, sample_candles):
|
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
|
period = 50 # More than available candles
|
||||||
results = indicators.sma(sample_candles, period)
|
df = indicators._prepare_dataframe_from_list(sample_candles)
|
||||||
assert len(results) == 0
|
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):
|
def test_ema_calculation(self, indicators, sample_candles):
|
||||||
"""Test Exponential Moving Average calculation."""
|
"""Test Exponential Moving Average calculation (now returns DataFrame)."""
|
||||||
period = 10
|
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
|
# Should have results starting from period 10
|
||||||
assert len(results) == len(sample_candles) - period + 1
|
assert isinstance(result_df, pd.DataFrame)
|
||||||
|
assert len(result_df) == len(sample_candles) - period + 1
|
||||||
# Check first result
|
assert 'ema' in result_df.columns
|
||||||
first_result = results[0]
|
|
||||||
assert isinstance(first_result, IndicatorResult)
|
|
||||||
assert 'ema' in first_result.values
|
|
||||||
assert first_result.metadata['period'] == period
|
|
||||||
|
|
||||||
# EMA should be between the range of input prices
|
# EMA should be between the range of input prices
|
||||||
min_price = min(float(c.close) for c in sample_candles[:period])
|
min_price = min(float(c.close) for c in sample_candles[:period])
|
||||||
max_price = max(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):
|
def test_rsi_calculation(self, indicators, sample_candles):
|
||||||
"""Test Relative Strength Index calculation."""
|
"""Test Relative Strength Index calculation (now returns DataFrame)."""
|
||||||
period = 14
|
period = 14
|
||||||
results = indicators.rsi(sample_candles, period)
|
df = indicators._prepare_dataframe_from_list(sample_candles)
|
||||||
|
df['timestamp'] = df.index
|
||||||
# Should have results starting from period 15 (period + 1 for price change calculation)
|
result_df = indicators.rsi(df, period)
|
||||||
assert len(results) == len(sample_candles) - period
|
assert isinstance(result_df, pd.DataFrame)
|
||||||
|
assert not result_df.empty
|
||||||
# Check first result
|
assert 'rsi' in result_df.columns
|
||||||
first_result = results[0]
|
assert 0 <= result_df.iloc[0]['rsi'] <= 100
|
||||||
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
|
|
||||||
|
|
||||||
def test_macd_calculation(self, indicators, sample_candles):
|
def test_macd_calculation(self, indicators, sample_candles):
|
||||||
"""Test MACD calculation."""
|
"""Test MACD calculation (now returns DataFrame)."""
|
||||||
fast_period = 12
|
fast_period = 12
|
||||||
slow_period = 26
|
slow_period = 26
|
||||||
signal_period = 9
|
signal_period = 9
|
||||||
results = indicators.macd(sample_candles, fast_period, slow_period, signal_period)
|
df = indicators._prepare_dataframe_from_list(sample_candles)
|
||||||
|
df['timestamp'] = df.index
|
||||||
# MACD needs slow_period + signal_period data points
|
result_df = indicators.macd(df, fast_period, slow_period, signal_period)
|
||||||
expected_count = len(sample_candles) - slow_period - signal_period + 1
|
# MACD results start after max(slow_period, signal_period) - 1 rows
|
||||||
assert len(results) == max(0, expected_count)
|
min_required = max(slow_period, signal_period)
|
||||||
|
expected_count = max(0, len(sample_candles) - (min_required - 1))
|
||||||
if results: # Only test if we have results
|
assert isinstance(result_df, pd.DataFrame)
|
||||||
first_result = results[0]
|
assert len(result_df) == expected_count
|
||||||
assert isinstance(first_result, IndicatorResult)
|
assert 'macd' in result_df.columns
|
||||||
assert 'macd' in first_result.values
|
assert 'signal' in result_df.columns
|
||||||
assert 'signal' in first_result.values
|
assert 'histogram' in result_df.columns
|
||||||
assert 'histogram' in first_result.values
|
if not result_df.empty:
|
||||||
|
|
||||||
# Histogram should equal MACD - Signal
|
# Histogram should equal MACD - Signal
|
||||||
expected_histogram = first_result.values['macd'] - first_result.values['signal']
|
first_row = result_df.iloc[0]
|
||||||
assert abs(first_result.values['histogram'] - expected_histogram) < 0.001
|
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):
|
def test_bollinger_bands_calculation(self, indicators, sample_candles):
|
||||||
"""Test Bollinger Bands calculation."""
|
"""Test Bollinger Bands calculation (now returns DataFrame)."""
|
||||||
period = 20
|
period = 20
|
||||||
std_dev = 2.0
|
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
|
# Should have results starting from period 20
|
||||||
assert len(results) == len(sample_candles) - period + 1
|
assert isinstance(result_df, pd.DataFrame)
|
||||||
|
assert len(result_df) == len(sample_candles) - period + 1
|
||||||
# Check first result
|
assert 'upper_band' in result_df.columns
|
||||||
first_result = results[0]
|
assert 'middle_band' in result_df.columns
|
||||||
assert isinstance(first_result, IndicatorResult)
|
assert 'lower_band' in result_df.columns
|
||||||
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
|
|
||||||
|
|
||||||
# Upper band should be greater than middle band, which should be greater than lower band
|
# 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']
|
first_row = result_df.iloc[0]
|
||||||
assert first_result.values['middle_band'] > first_result.values['lower_band']
|
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):
|
def test_sparse_data_handling(self, indicators, sparse_candles):
|
||||||
"""Test indicators with sparse data (time gaps)."""
|
"""Test indicators with sparse data (time gaps)."""
|
||||||
period = 5
|
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
|
# Should handle sparse data without issues
|
||||||
assert len(sma_results) > 0
|
assert not sma_df.empty
|
||||||
|
|
||||||
# Check that timestamps are preserved correctly
|
# Check that timestamps are preserved correctly
|
||||||
for result in sma_results:
|
for ts in sma_df.index:
|
||||||
assert result.timestamp is not None
|
assert ts is not None
|
||||||
assert isinstance(result.timestamp, datetime)
|
assert isinstance(ts, datetime)
|
||||||
|
|
||||||
def test_calculate_multiple_indicators(self, indicators, sample_candles):
|
def test_calculate_multiple_indicators(self, indicators, sample_candles):
|
||||||
"""Test calculating multiple indicators at once."""
|
"""Test calculating multiple indicators at once."""
|
||||||
@ -236,16 +220,15 @@ class TestTechnicalIndicators:
|
|||||||
'macd': {'type': 'macd'},
|
'macd': {'type': 'macd'},
|
||||||
'bb_20': {'type': 'bollinger_bands', 'period': 20}
|
'bb_20': {'type': 'bollinger_bands', 'period': 20}
|
||||||
}
|
}
|
||||||
|
df = indicators._prepare_dataframe_from_list(sample_candles)
|
||||||
results = indicators.calculate_multiple_indicators(sample_candles, config)
|
df['timestamp'] = df.index
|
||||||
|
results = indicators.calculate_multiple_indicators(df, config)
|
||||||
assert len(results) == len(config)
|
assert len(results) == len(config)
|
||||||
assert 'sma_10' in results
|
assert 'sma_10' in results
|
||||||
assert 'ema_12' in results
|
assert 'ema_12' in results
|
||||||
assert 'rsi_14' in results
|
assert 'rsi_14' in results
|
||||||
assert 'macd' in results
|
assert 'macd' in results
|
||||||
assert 'bb_20' in results
|
assert 'bb_20' in results
|
||||||
|
|
||||||
# Check that each indicator has appropriate results
|
# Check that each indicator has appropriate results
|
||||||
assert len(results['sma_10']) > 0
|
assert len(results['sma_10']) > 0
|
||||||
assert len(results['ema_12']) > 0
|
assert len(results['ema_12']) > 0
|
||||||
@ -255,21 +238,20 @@ class TestTechnicalIndicators:
|
|||||||
config = {
|
config = {
|
||||||
'invalid_indicator': {'type': 'unknown_type', 'period': 10}
|
'invalid_indicator': {'type': 'unknown_type', 'period': 10}
|
||||||
}
|
}
|
||||||
|
df = indicators._prepare_dataframe_from_list(sample_candles)
|
||||||
results = indicators.calculate_multiple_indicators(sample_candles, config)
|
results = indicators.calculate_multiple_indicators(df, config)
|
||||||
|
|
||||||
assert 'invalid_indicator' in results
|
assert 'invalid_indicator' in results
|
||||||
assert len(results['invalid_indicator']) == 0 # Should return empty list
|
assert len(results['invalid_indicator']) == 0 # Should return empty list
|
||||||
|
|
||||||
def test_different_price_columns(self, indicators, sample_candles):
|
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
|
# Test SMA with 'high' price column
|
||||||
sma_high = indicators.sma(sample_candles, 5, price_column='high')
|
sma_high = indicators.sma(df, 5, price_column='high')
|
||||||
sma_close = indicators.sma(sample_candles, 5, price_column='close')
|
sma_close = indicators.sma(df, 5, price_column='close')
|
||||||
|
|
||||||
assert len(sma_high) == len(sma_close)
|
assert len(sma_high) == len(sma_close)
|
||||||
# High prices should generally give higher SMA values
|
# 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:
|
class TestIndicatorHelperFunctions:
|
||||||
@ -190,11 +190,9 @@ class TestTechnicalIndicatorsSafety:
|
|||||||
"""Test indicators with sparse data (time gaps)."""
|
"""Test indicators with sparse data (time gaps)."""
|
||||||
period = 5
|
period = 5
|
||||||
df = indicators._prepare_dataframe_from_list(sparse_candles)
|
df = indicators._prepare_dataframe_from_list(sparse_candles)
|
||||||
sma_results = indicators.sma(df, period)
|
sma_df = indicators.sma(df, period)
|
||||||
|
assert not sma_df.empty
|
||||||
assert len(sma_results) > 0
|
timestamps = sma_df.index.to_list()
|
||||||
# Verify that gaps are preserved (no interpolation)
|
|
||||||
timestamps = [r.timestamp for r in sma_results]
|
|
||||||
for i in range(1, len(timestamps)):
|
for i in range(1, len(timestamps)):
|
||||||
time_diff = timestamps[i] - timestamps[i-1]
|
time_diff = timestamps[i] - timestamps[i-1]
|
||||||
assert time_diff >= timedelta(minutes=1)
|
assert time_diff >= timedelta(minutes=1)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user