Implement multi-timeframe support for indicators
- Enhanced the `UserIndicator` class to include an optional `timeframe` attribute for custom indicator timeframes. - Updated the `get_indicator_data` method in `MarketDataIntegrator` to fetch and calculate indicators based on the specified timeframe, ensuring proper data alignment and handling. - Modified the `ChartBuilder` to pass the correct DataFrame for plotting indicators with different timeframes. - Added UI elements in the indicator modal for selecting timeframes, improving user experience. - Updated relevant JSON templates to include the new `timeframe` field for all indicators. - Refactored the `prepare_chart_data` function to ensure it returns a DataFrame with a `DatetimeIndex` for consistent calculations. This commit enhances the flexibility and usability of the indicator system, allowing users to analyze data across various timeframes.
This commit is contained in:
@@ -74,7 +74,7 @@ class TechnicalIndicators:
|
||||
if self.logger:
|
||||
self.logger.info("TechnicalIndicators: Initialized indicator calculator")
|
||||
|
||||
def prepare_dataframe(self, candles: List[OHLCVCandle]) -> pd.DataFrame:
|
||||
def _prepare_dataframe_from_list(self, candles: List[OHLCVCandle]) -> pd.DataFrame:
|
||||
"""
|
||||
Convert OHLCV candles to pandas DataFrame for efficient calculations.
|
||||
|
||||
@@ -112,20 +112,19 @@ class TechnicalIndicators:
|
||||
|
||||
return df
|
||||
|
||||
def sma(self, candles: List[OHLCVCandle], period: int,
|
||||
def sma(self, df: pd.DataFrame, period: int,
|
||||
price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
Calculate Simple Moving Average (SMA).
|
||||
|
||||
Args:
|
||||
candles: List of OHLCV candles
|
||||
df: DataFrame with OHLCV data
|
||||
period: Number of periods for moving average
|
||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||
|
||||
Returns:
|
||||
List of indicator results with SMA values
|
||||
"""
|
||||
df = self.prepare_dataframe(candles)
|
||||
if df.empty or len(df) < period:
|
||||
return []
|
||||
|
||||
@@ -147,20 +146,19 @@ class TechnicalIndicators:
|
||||
|
||||
return results
|
||||
|
||||
def ema(self, candles: List[OHLCVCandle], period: int,
|
||||
def ema(self, df: pd.DataFrame, period: int,
|
||||
price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
Calculate Exponential Moving Average (EMA).
|
||||
|
||||
Args:
|
||||
candles: List of OHLCV candles
|
||||
df: DataFrame with OHLCV data
|
||||
period: Number of periods for moving average
|
||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||
|
||||
Returns:
|
||||
List of indicator results with EMA values
|
||||
"""
|
||||
df = self.prepare_dataframe(candles)
|
||||
if df.empty or len(df) < period:
|
||||
return []
|
||||
|
||||
@@ -183,20 +181,19 @@ class TechnicalIndicators:
|
||||
|
||||
return results
|
||||
|
||||
def rsi(self, candles: List[OHLCVCandle], period: int = 14,
|
||||
def rsi(self, df: pd.DataFrame, period: int = 14,
|
||||
price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
Calculate Relative Strength Index (RSI).
|
||||
|
||||
Args:
|
||||
candles: List of OHLCV candles
|
||||
df: DataFrame with OHLCV data
|
||||
period: Number of periods for RSI calculation (default 14)
|
||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||
|
||||
Returns:
|
||||
List of indicator results with RSI values
|
||||
"""
|
||||
df = self.prepare_dataframe(candles)
|
||||
if df.empty or len(df) < period + 1:
|
||||
return []
|
||||
|
||||
@@ -234,14 +231,14 @@ class TechnicalIndicators:
|
||||
|
||||
return results
|
||||
|
||||
def macd(self, candles: List[OHLCVCandle],
|
||||
def macd(self, df: pd.DataFrame,
|
||||
fast_period: int = 12, slow_period: int = 26, signal_period: int = 9,
|
||||
price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
Calculate Moving Average Convergence Divergence (MACD).
|
||||
|
||||
Args:
|
||||
candles: List of OHLCV candles
|
||||
df: DataFrame with OHLCV data
|
||||
fast_period: Fast EMA period (default 12)
|
||||
slow_period: Slow EMA period (default 26)
|
||||
signal_period: Signal line EMA period (default 9)
|
||||
@@ -250,8 +247,7 @@ class TechnicalIndicators:
|
||||
Returns:
|
||||
List of indicator results with MACD, signal, and histogram values
|
||||
"""
|
||||
df = self.prepare_dataframe(candles)
|
||||
if df.empty or len(df) < slow_period + signal_period:
|
||||
if df.empty or len(df) < slow_period:
|
||||
return []
|
||||
|
||||
# Calculate fast and slow EMAs
|
||||
@@ -271,7 +267,7 @@ class TechnicalIndicators:
|
||||
results = []
|
||||
for i, (timestamp, row) in enumerate(df.iterrows()):
|
||||
# Only return results after minimum period
|
||||
if i >= slow_period + signal_period - 1:
|
||||
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,
|
||||
@@ -293,21 +289,20 @@ class TechnicalIndicators:
|
||||
|
||||
return results
|
||||
|
||||
def bollinger_bands(self, candles: List[OHLCVCandle], period: int = 20,
|
||||
def bollinger_bands(self, df: pd.DataFrame, period: int = 20,
|
||||
std_dev: float = 2.0, price_column: str = 'close') -> List[IndicatorResult]:
|
||||
"""
|
||||
Calculate Bollinger Bands.
|
||||
|
||||
Args:
|
||||
candles: List of OHLCV candles
|
||||
df: DataFrame with OHLCV data
|
||||
period: Number of periods for moving average (default 20)
|
||||
std_dev: Number of standard deviations for bands (default 2.0)
|
||||
std_dev: Number of standard deviations (default 2.0)
|
||||
price_column: Price column to use ('open', 'high', 'low', 'close')
|
||||
|
||||
Returns:
|
||||
List of indicator results with upper band, middle band (SMA), and lower band
|
||||
"""
|
||||
df = self.prepare_dataframe(candles)
|
||||
if df.empty or len(df) < period:
|
||||
return []
|
||||
|
||||
@@ -417,64 +412,53 @@ class TechnicalIndicators:
|
||||
|
||||
def calculate(self, indicator_type: str, candles: Union[pd.DataFrame, List[OHLCVCandle]], **kwargs) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Generic method to calculate any supported indicator by type.
|
||||
Calculate a single indicator with dynamic dispatch.
|
||||
|
||||
Args:
|
||||
indicator_type: The type of indicator to calculate (e.g., 'sma', 'ema').
|
||||
candles: The input data, either a DataFrame or a list of OHLCVCandle objects.
|
||||
**kwargs: Keyword arguments for the specific indicator function.
|
||||
indicator_type: Name of the indicator (e.g., 'sma', 'ema')
|
||||
candles: List of OHLCV candles or a pre-prepared DataFrame
|
||||
**kwargs: Indicator-specific parameters (e.g., period=20)
|
||||
|
||||
Returns:
|
||||
A dictionary containing the indicator results, or None if the type is unknown.
|
||||
"""
|
||||
# If input is a DataFrame, convert it to list of OHLCVCandle objects.
|
||||
# This is a temporary adaptation to the existing methods.
|
||||
# Future optimization should standardize on DataFrames.
|
||||
if isinstance(candles, pd.DataFrame):
|
||||
from .data_types import OHLCVCandle
|
||||
|
||||
# Ensure required columns are present
|
||||
required_cols = {'open', 'high', 'low', 'close', 'volume'}
|
||||
if not required_cols.issubset(candles.columns):
|
||||
if self.logger:
|
||||
self.logger.error("Indicators: DataFrame missing required columns for OHLCVCandle conversion.")
|
||||
return None
|
||||
|
||||
symbol = kwargs.get('symbol', 'UNKNOWN')
|
||||
timeframe = kwargs.get('timeframe', 'UNKNOWN')
|
||||
|
||||
candles_list = [
|
||||
OHLCVCandle(
|
||||
symbol=symbol,
|
||||
timeframe=timeframe,
|
||||
start_time=row['timestamp'],
|
||||
end_time=row['timestamp'],
|
||||
open=Decimal(str(row['open'])),
|
||||
high=Decimal(str(row['high'])),
|
||||
low=Decimal(str(row['low'])),
|
||||
close=Decimal(str(row['close'])),
|
||||
volume=Decimal(str(row['volume'])),
|
||||
trade_count=int(row.get('trade_count', 0))
|
||||
) for _, row in candles.iterrows()
|
||||
]
|
||||
candles = candles_list
|
||||
|
||||
# Get the indicator calculation method
|
||||
indicator_method = getattr(self, indicator_type, None)
|
||||
if indicator_method and callable(indicator_method):
|
||||
# We need to construct a proper IndicatorResult object here
|
||||
# For now, let's adapt to what the methods return
|
||||
raw_result = indicator_method(candles, **kwargs)
|
||||
if not indicator_method:
|
||||
if self.logger:
|
||||
self.logger.error(f"TechnicalIndicators: Unknown indicator type '{indicator_type}'")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Prepare DataFrame if input is a list of candles
|
||||
if isinstance(candles, list):
|
||||
df = self._prepare_dataframe_from_list(candles)
|
||||
elif isinstance(candles, pd.DataFrame):
|
||||
df = candles
|
||||
else:
|
||||
raise TypeError("Input 'candles' must be a list of OHLCVCandle objects or a pandas DataFrame.")
|
||||
|
||||
if df.empty:
|
||||
return {'data': [], 'metadata': {}}
|
||||
|
||||
# Call the indicator method
|
||||
raw_result = 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
|
||||
"data": raw_result,
|
||||
"metadata": metadata
|
||||
}
|
||||
return None
|
||||
|
||||
if self.logger:
|
||||
self.logger.warning(f"TechnicalIndicators: Unknown indicator type '{indicator_type}'")
|
||||
return None
|
||||
except Exception as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"TechnicalIndicators: Error calculating {indicator_type}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def create_default_indicators_config() -> Dict[str, Dict[str, Any]]:
|
||||
|
||||
Reference in New Issue
Block a user