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:
@@ -489,7 +489,12 @@ class ChartBuilder:
|
||||
|
||||
if all_indicator_configs:
|
||||
indicator_data_map = self.data_integrator.get_indicator_data(
|
||||
df, all_indicator_configs, indicator_manager
|
||||
main_df=df,
|
||||
main_timeframe=timeframe,
|
||||
indicator_configs=all_indicator_configs,
|
||||
indicator_manager=indicator_manager,
|
||||
symbol=symbol,
|
||||
exchange="okx"
|
||||
)
|
||||
|
||||
for indicator_id, indicator_df in indicator_data_map.items():
|
||||
@@ -499,7 +504,9 @@ class ChartBuilder:
|
||||
continue
|
||||
|
||||
if indicator_df is not None and not indicator_df.empty:
|
||||
final_df = pd.merge(final_df, indicator_df, on='timestamp', how='left')
|
||||
# Add a suffix to the indicator's columns before joining to prevent overlap
|
||||
# when multiple indicators of the same type are added.
|
||||
final_df = final_df.join(indicator_df, how='left', rsuffix=f'_{indicator.id}')
|
||||
|
||||
# Determine target row for plotting
|
||||
target_row = 1 # Default to overlay on the main chart
|
||||
@@ -511,7 +518,7 @@ class ChartBuilder:
|
||||
if indicator.type == 'bollinger_bands':
|
||||
if all(c in indicator_df.columns for c in ['upper_band', 'lower_band', 'middle_band']):
|
||||
# Prepare data for the filled area
|
||||
x_vals = indicator_df['timestamp']
|
||||
x_vals = indicator_df.index
|
||||
y_upper = indicator_df['upper_band']
|
||||
y_lower = indicator_df['lower_band']
|
||||
|
||||
@@ -522,7 +529,7 @@ class ChartBuilder:
|
||||
|
||||
# Add the transparent fill trace
|
||||
fig.add_trace(go.Scatter(
|
||||
x=pd.concat([x_vals, x_vals[::-1]]),
|
||||
x=pd.concat([x_vals.to_series(), x_vals.to_series()[::-1]]),
|
||||
y=pd.concat([y_upper, y_lower[::-1]]),
|
||||
fill='toself',
|
||||
fillcolor=fill_color,
|
||||
@@ -540,7 +547,7 @@ class ChartBuilder:
|
||||
for col in indicator_df.columns:
|
||||
if col != 'timestamp':
|
||||
fig.add_trace(go.Scatter(
|
||||
x=indicator_df['timestamp'],
|
||||
x=indicator_df.index,
|
||||
y=indicator_df[col],
|
||||
mode='lines',
|
||||
name=f"{indicator.name} ({col})",
|
||||
|
||||
@@ -460,8 +460,11 @@ class MarketDataIntegrator:
|
||||
def get_indicator_data(
|
||||
self,
|
||||
main_df: pd.DataFrame,
|
||||
main_timeframe: str,
|
||||
indicator_configs: List['IndicatorLayerConfig'],
|
||||
indicator_manager: 'IndicatorManager'
|
||||
indicator_manager: 'IndicatorManager',
|
||||
symbol: str,
|
||||
exchange: str = "okx"
|
||||
) -> Dict[str, pd.DataFrame]:
|
||||
|
||||
indicator_data_map = {}
|
||||
@@ -477,21 +480,62 @@ class MarketDataIntegrator:
|
||||
continue
|
||||
|
||||
try:
|
||||
# The new `calculate` method in TechnicalIndicators handles DataFrame input
|
||||
# Determine the timeframe and data to use
|
||||
target_timeframe = indicator.timeframe
|
||||
|
||||
if target_timeframe and target_timeframe != main_timeframe:
|
||||
# Custom timeframe: fetch new data
|
||||
days_back = (main_df.index.max() - main_df.index.min()).days + 2 # Add buffer
|
||||
|
||||
raw_candles, _ = self.get_market_data_for_indicators(
|
||||
symbol=symbol,
|
||||
timeframe=target_timeframe,
|
||||
days_back=days_back,
|
||||
exchange=exchange
|
||||
)
|
||||
|
||||
if not raw_candles:
|
||||
self.logger.warning(f"No data for indicator '{indicator.name}' on timeframe {target_timeframe}")
|
||||
continue
|
||||
|
||||
from components.charts.utils import prepare_chart_data
|
||||
indicator_df = prepare_chart_data(raw_candles)
|
||||
else:
|
||||
# Use main chart's dataframe
|
||||
indicator_df = main_df
|
||||
|
||||
# Calculate the indicator
|
||||
indicator_result_pkg = self.indicators.calculate(
|
||||
indicator.type,
|
||||
main_df,
|
||||
indicator_df,
|
||||
**indicator.parameters
|
||||
)
|
||||
|
||||
if indicator_result_pkg and 'data' in indicator_result_pkg and indicator_result_pkg['data']:
|
||||
# The result is a list of IndicatorResult objects. Convert to DataFrame.
|
||||
if indicator_result_pkg and indicator_result_pkg.get('data'):
|
||||
indicator_results = indicator_result_pkg['data']
|
||||
|
||||
if not indicator_results:
|
||||
self.logger.warning(f"Indicator '{indicator.name}' produced no results.")
|
||||
continue
|
||||
|
||||
result_df = pd.DataFrame([
|
||||
{'timestamp': r.timestamp, **r.values}
|
||||
for r in indicator_results
|
||||
])
|
||||
indicator_data_map[indicator.id] = result_df
|
||||
result_df['timestamp'] = pd.to_datetime(result_df['timestamp'])
|
||||
result_df.set_index('timestamp', inplace=True)
|
||||
|
||||
# Ensure timezone consistency before reindexing
|
||||
if result_df.index.tz is None:
|
||||
result_df = result_df.tz_localize('UTC')
|
||||
result_df = result_df.tz_convert(main_df.index.tz)
|
||||
|
||||
# Align data to main_df's index to handle different timeframes
|
||||
if not result_df.index.equals(main_df.index):
|
||||
aligned_df = result_df.reindex(main_df.index, method='ffill')
|
||||
indicator_data_map[indicator.id] = aligned_df
|
||||
else:
|
||||
indicator_data_map[indicator.id] = result_df
|
||||
else:
|
||||
self.logger.warning(f"No data returned for indicator '{indicator.name}'")
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ class UserIndicator:
|
||||
display_type: str # DisplayType
|
||||
parameters: Dict[str, Any]
|
||||
styling: IndicatorStyling
|
||||
timeframe: Optional[str] = None
|
||||
visible: bool = True
|
||||
created_date: str = ""
|
||||
modified_date: str = ""
|
||||
@@ -82,6 +83,7 @@ class UserIndicator:
|
||||
'display_type': self.display_type,
|
||||
'parameters': self.parameters,
|
||||
'styling': asdict(self.styling),
|
||||
'timeframe': self.timeframe,
|
||||
'visible': self.visible,
|
||||
'created_date': self.created_date,
|
||||
'modified_date': self.modified_date
|
||||
@@ -101,6 +103,7 @@ class UserIndicator:
|
||||
display_type=data['display_type'],
|
||||
parameters=data.get('parameters', {}),
|
||||
styling=styling,
|
||||
timeframe=data.get('timeframe'),
|
||||
visible=data.get('visible', True),
|
||||
created_date=data.get('created_date', ''),
|
||||
modified_date=data.get('modified_date', '')
|
||||
@@ -244,7 +247,7 @@ class IndicatorManager:
|
||||
|
||||
def create_indicator(self, name: str, indicator_type: str, parameters: Dict[str, Any],
|
||||
description: str = "", color: str = "#007bff",
|
||||
display_type: str = None) -> Optional[UserIndicator]:
|
||||
display_type: str = None, timeframe: Optional[str] = None) -> Optional[UserIndicator]:
|
||||
"""
|
||||
Create a new indicator.
|
||||
|
||||
@@ -255,6 +258,7 @@ class IndicatorManager:
|
||||
description: Optional description
|
||||
color: Color for chart display
|
||||
display_type: overlay or subplot (auto-detected if None)
|
||||
timeframe: Optional timeframe for the indicator
|
||||
|
||||
Returns:
|
||||
Created UserIndicator instance or None if error
|
||||
@@ -278,7 +282,8 @@ class IndicatorManager:
|
||||
type=indicator_type,
|
||||
display_type=display_type,
|
||||
parameters=parameters,
|
||||
styling=styling
|
||||
styling=styling,
|
||||
timeframe=timeframe
|
||||
)
|
||||
|
||||
# Save to file
|
||||
@@ -309,16 +314,19 @@ class IndicatorManager:
|
||||
return False
|
||||
|
||||
# Update fields
|
||||
for field, value in updates.items():
|
||||
if hasattr(indicator, field):
|
||||
if field == 'styling' and isinstance(value, dict):
|
||||
# Update styling fields
|
||||
for style_field, style_value in value.items():
|
||||
if hasattr(indicator.styling, style_field):
|
||||
setattr(indicator.styling, style_field, style_value)
|
||||
for key, value in updates.items():
|
||||
if hasattr(indicator, key):
|
||||
if key == 'styling' and isinstance(value, dict):
|
||||
# Update nested styling fields
|
||||
for style_key, style_value in value.items():
|
||||
if hasattr(indicator.styling, style_key):
|
||||
setattr(indicator.styling, style_key, style_value)
|
||||
elif key == 'parameters' and isinstance(value, dict):
|
||||
indicator.parameters.update(value)
|
||||
else:
|
||||
setattr(indicator, field, value)
|
||||
setattr(indicator, key, value)
|
||||
|
||||
# Save updated indicator
|
||||
return self.save_indicator(indicator)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -139,9 +139,10 @@ def prepare_chart_data(candles: List[Dict[str, Any]]) -> pd.DataFrame:
|
||||
if col in df.columns:
|
||||
df[col] = pd.to_numeric(df[col], errors='coerce')
|
||||
|
||||
# Sort by timestamp
|
||||
df = df.sort_values('timestamp').reset_index(drop=True)
|
||||
|
||||
# Sort by timestamp and set it as the index, keeping the column
|
||||
df = df.sort_values('timestamp')
|
||||
df.index = pd.to_datetime(df['timestamp'])
|
||||
|
||||
# Handle missing volume data
|
||||
if 'volume' not in df.columns:
|
||||
df['volume'] = 0
|
||||
|
||||
Reference in New Issue
Block a user