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:
Vasily.onl
2025-06-06 15:06:17 +08:00
parent 58a754414a
commit b49e39dcb4
19 changed files with 417 additions and 247 deletions

View File

@@ -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]]: