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

@ -489,7 +489,12 @@ class ChartBuilder:
if all_indicator_configs: if all_indicator_configs:
indicator_data_map = self.data_integrator.get_indicator_data( 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(): for indicator_id, indicator_df in indicator_data_map.items():
@ -499,7 +504,9 @@ class ChartBuilder:
continue continue
if indicator_df is not None and not indicator_df.empty: 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 # Determine target row for plotting
target_row = 1 # Default to overlay on the main chart target_row = 1 # Default to overlay on the main chart
@ -511,7 +518,7 @@ class ChartBuilder:
if indicator.type == 'bollinger_bands': if indicator.type == 'bollinger_bands':
if all(c in indicator_df.columns for c in ['upper_band', 'lower_band', 'middle_band']): if all(c in indicator_df.columns for c in ['upper_band', 'lower_band', 'middle_band']):
# Prepare data for the filled area # Prepare data for the filled area
x_vals = indicator_df['timestamp'] x_vals = indicator_df.index
y_upper = indicator_df['upper_band'] y_upper = indicator_df['upper_band']
y_lower = indicator_df['lower_band'] y_lower = indicator_df['lower_band']
@ -522,7 +529,7 @@ class ChartBuilder:
# Add the transparent fill trace # Add the transparent fill trace
fig.add_trace(go.Scatter( 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]]), y=pd.concat([y_upper, y_lower[::-1]]),
fill='toself', fill='toself',
fillcolor=fill_color, fillcolor=fill_color,
@ -540,7 +547,7 @@ class ChartBuilder:
for col in indicator_df.columns: for col in indicator_df.columns:
if col != 'timestamp': if col != 'timestamp':
fig.add_trace(go.Scatter( fig.add_trace(go.Scatter(
x=indicator_df['timestamp'], x=indicator_df.index,
y=indicator_df[col], y=indicator_df[col],
mode='lines', mode='lines',
name=f"{indicator.name} ({col})", name=f"{indicator.name} ({col})",

View File

@ -460,8 +460,11 @@ class MarketDataIntegrator:
def get_indicator_data( def get_indicator_data(
self, self,
main_df: pd.DataFrame, main_df: pd.DataFrame,
main_timeframe: str,
indicator_configs: List['IndicatorLayerConfig'], indicator_configs: List['IndicatorLayerConfig'],
indicator_manager: 'IndicatorManager' indicator_manager: 'IndicatorManager',
symbol: str,
exchange: str = "okx"
) -> Dict[str, pd.DataFrame]: ) -> Dict[str, pd.DataFrame]:
indicator_data_map = {} indicator_data_map = {}
@ -477,21 +480,62 @@ class MarketDataIntegrator:
continue continue
try: 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_result_pkg = self.indicators.calculate(
indicator.type, indicator.type,
main_df, indicator_df,
**indicator.parameters **indicator.parameters
) )
if indicator_result_pkg and 'data' in indicator_result_pkg and indicator_result_pkg['data']: if indicator_result_pkg and indicator_result_pkg.get('data'):
# The result is a list of IndicatorResult objects. Convert to DataFrame.
indicator_results = indicator_result_pkg['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([ result_df = pd.DataFrame([
{'timestamp': r.timestamp, **r.values} {'timestamp': r.timestamp, **r.values}
for r in indicator_results 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: else:
self.logger.warning(f"No data returned for indicator '{indicator.name}'") self.logger.warning(f"No data returned for indicator '{indicator.name}'")

View File

@ -60,6 +60,7 @@ class UserIndicator:
display_type: str # DisplayType display_type: str # DisplayType
parameters: Dict[str, Any] parameters: Dict[str, Any]
styling: IndicatorStyling styling: IndicatorStyling
timeframe: Optional[str] = None
visible: bool = True visible: bool = True
created_date: str = "" created_date: str = ""
modified_date: str = "" modified_date: str = ""
@ -82,6 +83,7 @@ class UserIndicator:
'display_type': self.display_type, 'display_type': self.display_type,
'parameters': self.parameters, 'parameters': self.parameters,
'styling': asdict(self.styling), 'styling': asdict(self.styling),
'timeframe': self.timeframe,
'visible': self.visible, 'visible': self.visible,
'created_date': self.created_date, 'created_date': self.created_date,
'modified_date': self.modified_date 'modified_date': self.modified_date
@ -101,6 +103,7 @@ class UserIndicator:
display_type=data['display_type'], display_type=data['display_type'],
parameters=data.get('parameters', {}), parameters=data.get('parameters', {}),
styling=styling, styling=styling,
timeframe=data.get('timeframe'),
visible=data.get('visible', True), visible=data.get('visible', True),
created_date=data.get('created_date', ''), created_date=data.get('created_date', ''),
modified_date=data.get('modified_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], def create_indicator(self, name: str, indicator_type: str, parameters: Dict[str, Any],
description: str = "", color: str = "#007bff", 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. Create a new indicator.
@ -255,6 +258,7 @@ class IndicatorManager:
description: Optional description description: Optional description
color: Color for chart display color: Color for chart display
display_type: overlay or subplot (auto-detected if None) display_type: overlay or subplot (auto-detected if None)
timeframe: Optional timeframe for the indicator
Returns: Returns:
Created UserIndicator instance or None if error Created UserIndicator instance or None if error
@ -278,7 +282,8 @@ class IndicatorManager:
type=indicator_type, type=indicator_type,
display_type=display_type, display_type=display_type,
parameters=parameters, parameters=parameters,
styling=styling styling=styling,
timeframe=timeframe
) )
# Save to file # Save to file
@ -309,16 +314,19 @@ class IndicatorManager:
return False return False
# Update fields # Update fields
for field, value in updates.items(): for key, value in updates.items():
if hasattr(indicator, field): if hasattr(indicator, key):
if field == 'styling' and isinstance(value, dict): if key == 'styling' and isinstance(value, dict):
# Update styling fields # Update nested styling fields
for style_field, style_value in value.items(): for style_key, style_value in value.items():
if hasattr(indicator.styling, style_field): if hasattr(indicator.styling, style_key):
setattr(indicator.styling, style_field, style_value) setattr(indicator.styling, style_key, style_value)
elif key == 'parameters' and isinstance(value, dict):
indicator.parameters.update(value)
else: else:
setattr(indicator, field, value) setattr(indicator, key, value)
# Save updated indicator
return self.save_indicator(indicator) return self.save_indicator(indicator)
except Exception as e: except Exception as e:

View File

@ -139,8 +139,9 @@ def prepare_chart_data(candles: List[Dict[str, Any]]) -> pd.DataFrame:
if col in df.columns: if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce') df[col] = pd.to_numeric(df[col], errors='coerce')
# Sort by timestamp # Sort by timestamp and set it as the index, keeping the column
df = df.sort_values('timestamp').reset_index(drop=True) df = df.sort_values('timestamp')
df.index = pd.to_datetime(df['timestamp'])
# Handle missing volume data # Handle missing volume data
if 'volume' not in df.columns: if 'volume' not in df.columns:

View File

@ -3,6 +3,7 @@
"description": "Bollinger Bands volatility indicator", "description": "Bollinger Bands volatility indicator",
"type": "bollinger_bands", "type": "bollinger_bands",
"display_type": "overlay", "display_type": "overlay",
"timeframe": null,
"default_parameters": { "default_parameters": {
"period": 20, "period": 20,
"std_dev": 2.0 "std_dev": 2.0
@ -20,7 +21,12 @@
"min": 0.5, "min": 0.5,
"max": 5.0, "max": 5.0,
"default": 2.0, "default": 2.0,
"description": "Standard deviation multiplier" "description": "Standard deviation for Bollinger Bands"
},
"timeframe": {
"type": "string",
"default": null,
"description": "Indicator timeframe (e.g., '1h', '4h'). Null for chart timeframe."
} }
}, },
"default_styling": { "default_styling": {

View File

@ -3,6 +3,7 @@
"description": "Exponential Moving Average indicator", "description": "Exponential Moving Average indicator",
"type": "ema", "type": "ema",
"display_type": "overlay", "display_type": "overlay",
"timeframe": null,
"default_parameters": { "default_parameters": {
"period": 12 "period": 12
}, },
@ -13,6 +14,11 @@
"max": 200, "max": 200,
"default": 12, "default": 12,
"description": "Period for EMA calculation" "description": "Period for EMA calculation"
},
"timeframe": {
"type": "string",
"default": null,
"description": "Indicator timeframe (e.g., '1h', '4h'). Null for chart timeframe."
} }
}, },
"default_styling": { "default_styling": {

View File

@ -3,6 +3,7 @@
"description": "Moving Average Convergence Divergence", "description": "Moving Average Convergence Divergence",
"type": "macd", "type": "macd",
"display_type": "subplot", "display_type": "subplot",
"timeframe": null,
"default_parameters": { "default_parameters": {
"fast_period": 12, "fast_period": 12,
"slow_period": 26, "slow_period": 26,
@ -28,11 +29,17 @@
"min": 2, "min": 2,
"max": 30, "max": 30,
"default": 9, "default": 9,
"description": "Signal line period" "description": "Signal line period for MACD"
},
"timeframe": {
"type": "string",
"default": null,
"description": "Indicator timeframe (e.g., '1h', '4h'). Null for chart timeframe."
} }
}, },
"default_styling": { "default_styling": {
"color": "#fd7e14", "color": "#fd7e14",
"line_width": 2 "line_width": 2,
"macd_line_color": "#007bff"
} }
} }

View File

@ -3,6 +3,7 @@
"description": "RSI oscillator indicator", "description": "RSI oscillator indicator",
"type": "rsi", "type": "rsi",
"display_type": "subplot", "display_type": "subplot",
"timeframe": null,
"default_parameters": { "default_parameters": {
"period": 14 "period": 14
}, },
@ -13,6 +14,11 @@
"max": 50, "max": 50,
"default": 14, "default": 14,
"description": "Period for RSI calculation" "description": "Period for RSI calculation"
},
"timeframe": {
"type": "string",
"default": null,
"description": "Indicator timeframe (e.g., '1h', '4h'). Null for chart timeframe."
} }
}, },
"default_styling": { "default_styling": {

View File

@ -3,6 +3,7 @@
"description": "Simple Moving Average indicator", "description": "Simple Moving Average indicator",
"type": "sma", "type": "sma",
"display_type": "overlay", "display_type": "overlay",
"timeframe": null,
"default_parameters": { "default_parameters": {
"period": 20 "period": 20
}, },
@ -13,6 +14,11 @@
"max": 200, "max": 200,
"default": 20, "default": 20,
"description": "Period for SMA calculation" "description": "Period for SMA calculation"
},
"timeframe": {
"type": "string",
"default": null,
"description": "Indicator timeframe (e.g., '1h', '4h'). Null for chart timeframe."
} }
}, },
"default_styling": { "default_styling": {

View File

@ -0,0 +1,20 @@
{
"id": "ema_b869638d",
"name": "EMA 12 (15 minutes)",
"description": "",
"type": "ema",
"display_type": "overlay",
"parameters": {
"period": 12
},
"styling": {
"color": "#007bff",
"line_width": 2,
"opacity": 1.0,
"line_style": "solid"
},
"timeframe": "15m",
"visible": true,
"created_date": "2025-06-06T06:56:54.181578+00:00",
"modified_date": "2025-06-06T06:56:54.181578+00:00"
}

View File

@ -0,0 +1,20 @@
{
"id": "ema_bfbf3a1d",
"name": "EMA 12 (5 minutes)",
"description": "",
"type": "ema",
"display_type": "overlay",
"parameters": {
"period": 12
},
"styling": {
"color": "#007bff",
"line_width": 2,
"opacity": 1.0,
"line_style": "solid"
},
"timeframe": "5m",
"visible": true,
"created_date": "2025-06-06T07:02:34.613543+00:00",
"modified_date": "2025-06-06T07:02:34.613543+00:00"
}

View File

@ -15,7 +15,8 @@
"opacity": 1.0, "opacity": 1.0,
"line_style": "solid" "line_style": "solid"
}, },
"timeframe": "1h",
"visible": true, "visible": true,
"created_date": "2025-06-04T04:16:35.459602+00:00", "created_date": "2025-06-04T04:16:35.459602+00:00",
"modified_date": "2025-06-04T04:16:35.459602+00:00" "modified_date": "2025-06-06T07:03:58.642238+00:00"
} }

View File

@ -96,6 +96,7 @@ def register_indicator_callbacks(app):
[State('indicator-name-input', 'value'), [State('indicator-name-input', 'value'),
State('indicator-type-dropdown', 'value'), State('indicator-type-dropdown', 'value'),
State('indicator-description-input', 'value'), State('indicator-description-input', 'value'),
State('indicator-timeframe-dropdown', 'value'),
State('indicator-color-input', 'value'), State('indicator-color-input', 'value'),
State('indicator-line-width-slider', 'value'), State('indicator-line-width-slider', 'value'),
# SMA parameters # SMA parameters
@ -115,7 +116,7 @@ def register_indicator_callbacks(app):
State('edit-indicator-store', 'data')], State('edit-indicator-store', 'data')],
prevent_initial_call=True prevent_initial_call=True
) )
def save_new_indicator(n_clicks, name, indicator_type, description, color, line_width, def save_new_indicator(n_clicks, name, indicator_type, description, timeframe, color, line_width,
sma_period, ema_period, rsi_period, sma_period, ema_period, rsi_period,
macd_fast, macd_slow, macd_signal, macd_fast, macd_slow, macd_signal,
bb_period, bb_stddev, edit_data): bb_period, bb_stddev, edit_data):
@ -161,7 +162,8 @@ def register_indicator_callbacks(app):
name=name, name=name,
description=description or "", description=description or "",
parameters=parameters, parameters=parameters,
styling={'color': color or "#007bff", 'line_width': line_width or 2} styling={'color': color or "#007bff", 'line_width': line_width or 2},
timeframe=timeframe or None
) )
if success: if success:
@ -176,7 +178,8 @@ def register_indicator_callbacks(app):
indicator_type=indicator_type, indicator_type=indicator_type,
parameters=parameters, parameters=parameters,
description=description or "", description=description or "",
color=color or "#007bff" color=color or "#007bff",
timeframe=timeframe or None
) )
if not new_indicator: if not new_indicator:
@ -384,6 +387,7 @@ def register_indicator_callbacks(app):
Output('indicator-name-input', 'value'), Output('indicator-name-input', 'value'),
Output('indicator-type-dropdown', 'value'), Output('indicator-type-dropdown', 'value'),
Output('indicator-description-input', 'value'), Output('indicator-description-input', 'value'),
Output('indicator-timeframe-dropdown', 'value'),
Output('indicator-color-input', 'value'), Output('indicator-color-input', 'value'),
Output('edit-indicator-store', 'data'), Output('edit-indicator-store', 'data'),
# Add parameter field outputs # Add parameter field outputs
@ -403,7 +407,7 @@ def register_indicator_callbacks(app):
"""Load indicator data for editing.""" """Load indicator data for editing."""
ctx = callback_context ctx = callback_context
if not ctx.triggered or not any(edit_clicks): if not ctx.triggered or not any(edit_clicks):
return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update return [no_update] * 15
# Find which button was clicked # Find which button was clicked
triggered_id = ctx.triggered[0]['prop_id'] triggered_id = ctx.triggered[0]['prop_id']
@ -418,41 +422,42 @@ def register_indicator_callbacks(app):
if indicator: if indicator:
# Store indicator ID for update # Store indicator ID for update
edit_data = {'indicator_id': indicator_id, 'mode': 'edit', 'open_modal': True} edit_data = {'indicator_id': indicator_id, 'mode': 'edit'}
# Extract parameter values based on indicator type # Extract parameter values based on indicator type
params = indicator.parameters params = indicator.parameters
# Default parameter values # Default parameter values
sma_period = 20 sma_period = None
ema_period = 12 ema_period = None
rsi_period = 14 rsi_period = None
macd_fast = 12 macd_fast = None
macd_slow = 26 macd_slow = None
macd_signal = 9 macd_signal = None
bb_period = 20 bb_period = None
bb_stddev = 2.0 bb_stddev = None
# Update with actual saved values # Update with actual saved values
if indicator.type == 'sma': if indicator.type == 'sma':
sma_period = params.get('period', 20) sma_period = params.get('period')
elif indicator.type == 'ema': elif indicator.type == 'ema':
ema_period = params.get('period', 12) ema_period = params.get('period')
elif indicator.type == 'rsi': elif indicator.type == 'rsi':
rsi_period = params.get('period', 14) rsi_period = params.get('period')
elif indicator.type == 'macd': elif indicator.type == 'macd':
macd_fast = params.get('fast_period', 12) macd_fast = params.get('fast_period')
macd_slow = params.get('slow_period', 26) macd_slow = params.get('slow_period')
macd_signal = params.get('signal_period', 9) macd_signal = params.get('signal_period')
elif indicator.type == 'bollinger_bands': elif indicator.type == 'bollinger_bands':
bb_period = params.get('period', 20) bb_period = params.get('period')
bb_stddev = params.get('std_dev', 2.0) bb_stddev = params.get('std_dev')
return ( return (
"✏️ Edit Indicator", f"✏️ Edit Indicator: {indicator.name}",
indicator.name, indicator.name,
indicator.type, indicator.type,
indicator.description, indicator.description,
indicator.timeframe,
indicator.styling.color, indicator.styling.color,
edit_data, edit_data,
sma_period, sma_period,
@ -465,17 +470,18 @@ def register_indicator_callbacks(app):
bb_stddev bb_stddev
) )
else: else:
return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update return [no_update] * 15
except Exception as e: except Exception as e:
logger.error(f"Indicator callback: Error loading indicator for edit: {e}") logger.error(f"Indicator callback: Error loading indicator for edit: {e}")
return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update return [no_update] * 15
# Reset modal form when closed or saved # Reset modal form when closed or saved
@app.callback( @app.callback(
[Output('indicator-name-input', 'value', allow_duplicate=True), [Output('indicator-name-input', 'value', allow_duplicate=True),
Output('indicator-type-dropdown', 'value', allow_duplicate=True), Output('indicator-type-dropdown', 'value', allow_duplicate=True),
Output('indicator-description-input', 'value', allow_duplicate=True), Output('indicator-description-input', 'value', allow_duplicate=True),
Output('indicator-timeframe-dropdown', 'value', allow_duplicate=True),
Output('indicator-color-input', 'value', allow_duplicate=True), Output('indicator-color-input', 'value', allow_duplicate=True),
Output('indicator-line-width-slider', 'value'), Output('indicator-line-width-slider', 'value'),
Output('modal-title', 'children', allow_duplicate=True), Output('modal-title', 'children', allow_duplicate=True),
@ -494,9 +500,7 @@ def register_indicator_callbacks(app):
prevent_initial_call=True prevent_initial_call=True
) )
def reset_modal_form(cancel_clicks, save_clicks): def reset_modal_form(cancel_clicks, save_clicks):
"""Reset the modal form when it's closed or saved.""" """Reset the modal form to its default state."""
if cancel_clicks or save_clicks: return "", "", "", "", "", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0
return "", None, "", "#007bff", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0
return no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update, no_update
logger.info("Indicator callbacks: registered successfully") logger.info("Indicator callbacks: registered successfully")

View File

@ -562,21 +562,28 @@ def get_market_statistics(df: pd.DataFrame, symbol: str, timeframe: str) -> html
""" """
Generate a comprehensive market statistics component from a DataFrame. Generate a comprehensive market statistics component from a DataFrame.
""" """
if df.empty:
return html.Div("No data available for statistics.", className="text-center text-muted")
try: try:
volume_analyzer = VolumeAnalyzer() # Get statistics
price_analyzer = PriceMovementAnalyzer() price_analyzer = PriceMovementAnalyzer()
volume_analyzer = VolumeAnalyzer()
volume_stats = volume_analyzer.get_volume_statistics(df)
price_stats = price_analyzer.get_price_movement_statistics(df) price_stats = price_analyzer.get_price_movement_statistics(df)
volume_stats = volume_analyzer.get_volume_statistics(df)
if 'error' in volume_stats or 'error' in price_stats: # Format key statistics for display
error_msg = volume_stats.get('error') or price_stats.get('error') start_date = df.index.min().strftime('%Y-%m-%d %H:%M')
end_date = df.index.max().strftime('%Y-%m-%d %H:%M')
# Check for errors from analyzers
if 'error' in price_stats or 'error' in volume_stats:
error_msg = price_stats.get('error') or volume_stats.get('error')
return html.Div(f"Error generating statistics: {error_msg}", style={'color': 'red'}) return html.Div(f"Error generating statistics: {error_msg}", style={'color': 'red'})
# Time range for display # Time range for display
start_date = df['timestamp'].min().strftime('%Y-%m-%d %H:%M') days_back = (df.index.max() - df.index.min()).days
end_date = df['timestamp'].max().strftime('%Y-%m-%d %H:%M')
days_back = (df['timestamp'].max() - df['timestamp'].min()).days
time_status = f"📅 Analysis Range: {start_date} to {end_date} (~{days_back} days)" time_status = f"📅 Analysis Range: {start_date} to {end_date} (~{days_back} days)"
return html.Div([ return html.Div([

View File

@ -33,6 +33,27 @@ def create_indicator_modal():
placeholder='Select indicator type', placeholder='Select indicator type',
), width=12) ), width=12)
], className="mb-3"), ], className="mb-3"),
dbc.Row([
dbc.Col(dbc.Label("Timeframe (Optional):"), width=12),
dbc.Col(dcc.Dropdown(
id='indicator-timeframe-dropdown',
options=[
{'label': 'Chart Timeframe', 'value': ''},
{'label': "1 Second", 'value': '1s'},
{'label': "5 Seconds", 'value': '5s'},
{'label': "15 Seconds", 'value': '15s'},
{'label': "30 Seconds", 'value': '30s'},
{'label': '1 Minute', 'value': '1m'},
{'label': '5 Minutes', 'value': '5m'},
{'label': '15 Minutes', 'value': '15m'},
{'label': '1 Hour', 'value': '1h'},
{'label': '4 Hours', 'value': '4h'},
{'label': '1 Day', 'value': '1d'},
],
value='',
placeholder='Defaults to chart timeframe'
), width=12),
], className="mb-3"),
dbc.Row([ dbc.Row([
dbc.Col(dbc.Label("Description (Optional):"), width=12), dbc.Col(dbc.Label("Description (Optional):"), width=12),
dbc.Col(dcc.Textarea( dbc.Col(dcc.Textarea(

View File

@ -74,7 +74,7 @@ class TechnicalIndicators:
if self.logger: if self.logger:
self.logger.info("TechnicalIndicators: Initialized indicator calculator") 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. Convert OHLCV candles to pandas DataFrame for efficient calculations.
@ -112,20 +112,19 @@ class TechnicalIndicators:
return df return df
def sma(self, candles: List[OHLCVCandle], period: int, def sma(self, df: pd.DataFrame, period: int,
price_column: str = 'close') -> List[IndicatorResult]: price_column: str = 'close') -> List[IndicatorResult]:
""" """
Calculate Simple Moving Average (SMA). Calculate Simple Moving Average (SMA).
Args: Args:
candles: List of OHLCV candles df: DataFrame with OHLCV data
period: Number of periods for moving average period: Number of periods for moving average
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 List of indicator results with SMA values
""" """
df = self.prepare_dataframe(candles)
if df.empty or len(df) < period: if df.empty or len(df) < period:
return [] return []
@ -147,20 +146,19 @@ class TechnicalIndicators:
return results return results
def ema(self, candles: List[OHLCVCandle], period: int, def ema(self, df: pd.DataFrame, period: int,
price_column: str = 'close') -> List[IndicatorResult]: price_column: str = 'close') -> List[IndicatorResult]:
""" """
Calculate Exponential Moving Average (EMA). Calculate Exponential Moving Average (EMA).
Args: Args:
candles: List of OHLCV candles df: DataFrame with OHLCV data
period: Number of periods for moving average period: Number of periods for moving average
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 List of indicator results with EMA values
""" """
df = self.prepare_dataframe(candles)
if df.empty or len(df) < period: if df.empty or len(df) < period:
return [] return []
@ -183,20 +181,19 @@ class TechnicalIndicators:
return results 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]: price_column: str = 'close') -> List[IndicatorResult]:
""" """
Calculate Relative Strength Index (RSI). Calculate Relative Strength Index (RSI).
Args: Args:
candles: List of OHLCV candles df: DataFrame with OHLCV data
period: Number of periods for RSI calculation (default 14) period: Number of periods for RSI calculation (default 14)
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 List of indicator results with RSI values
""" """
df = self.prepare_dataframe(candles)
if df.empty or len(df) < period + 1: if df.empty or len(df) < period + 1:
return [] return []
@ -234,14 +231,14 @@ class TechnicalIndicators:
return results 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, fast_period: int = 12, slow_period: int = 26, signal_period: int = 9,
price_column: str = 'close') -> List[IndicatorResult]: price_column: str = 'close') -> List[IndicatorResult]:
""" """
Calculate Moving Average Convergence Divergence (MACD). Calculate Moving Average Convergence Divergence (MACD).
Args: Args:
candles: List of OHLCV candles df: DataFrame with OHLCV data
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 EMA period (default 9) signal_period: Signal line EMA period (default 9)
@ -250,8 +247,7 @@ class TechnicalIndicators:
Returns: Returns:
List of indicator results with MACD, signal, and histogram values List of indicator results with MACD, signal, and histogram values
""" """
df = self.prepare_dataframe(candles) if df.empty or len(df) < slow_period:
if df.empty or len(df) < slow_period + signal_period:
return [] return []
# Calculate fast and slow EMAs # Calculate fast and slow EMAs
@ -271,7 +267,7 @@ class TechnicalIndicators:
results = [] results = []
for i, (timestamp, row) in enumerate(df.iterrows()): for i, (timestamp, row) in enumerate(df.iterrows()):
# Only return results after minimum period # 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'])): if not (pd.isna(row['macd']) or pd.isna(row['signal']) or pd.isna(row['histogram'])):
result = IndicatorResult( result = IndicatorResult(
timestamp=timestamp, timestamp=timestamp,
@ -293,21 +289,20 @@ class TechnicalIndicators:
return results 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]: std_dev: float = 2.0, price_column: str = 'close') -> List[IndicatorResult]:
""" """
Calculate Bollinger Bands. Calculate Bollinger Bands.
Args: Args:
candles: List of OHLCV candles df: DataFrame with OHLCV data
period: Number of periods for moving average (default 20) 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') 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 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: if df.empty or len(df) < period:
return [] return []
@ -417,64 +412,53 @@ class TechnicalIndicators:
def calculate(self, indicator_type: str, candles: Union[pd.DataFrame, List[OHLCVCandle]], **kwargs) -> Optional[Dict[str, Any]]: 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: Args:
indicator_type: The type of indicator to calculate (e.g., 'sma', 'ema'). indicator_type: Name of the indicator (e.g., 'sma', 'ema')
candles: The input data, either a DataFrame or a list of OHLCVCandle objects. candles: List of OHLCV candles or a pre-prepared DataFrame
**kwargs: Keyword arguments for the specific indicator function. **kwargs: Indicator-specific parameters (e.g., period=20)
Returns: Returns:
A dictionary containing the indicator results, or None if the type is unknown. 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. # Get the indicator calculation method
# 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
indicator_method = getattr(self, indicator_type, None) indicator_method = getattr(self, indicator_type, None)
if indicator_method and callable(indicator_method): if not indicator_method:
# We need to construct a proper IndicatorResult object here if self.logger:
# For now, let's adapt to what the methods return self.logger.error(f"TechnicalIndicators: Unknown indicator type '{indicator_type}'")
raw_result = indicator_method(candles, **kwargs) 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 # The methods return List[IndicatorResult], let's package that
if raw_result: if raw_result:
return { return {
"data": raw_result "data": raw_result,
"metadata": metadata
} }
return None return None
if self.logger: except Exception as e:
self.logger.warning(f"TechnicalIndicators: Unknown indicator type '{indicator_type}'") if self.logger:
return None self.logger.error(f"TechnicalIndicators: Error calculating {indicator_type}: {e}")
return None
def create_default_indicators_config() -> Dict[str, Dict[str, Any]]: def create_default_indicators_config() -> Dict[str, Dict[str, Any]]:

View File

@ -1,175 +1,123 @@
# Technical Indicators Module # Technical Indicators Module
The Technical Indicators module provides comprehensive technical analysis capabilities for the TCP Trading Platform. It's designed to handle sparse OHLCV data efficiently and integrates seamlessly with the platform's aggregation strategy. The Technical Indicators module provides a suite of common technical analysis tools. It is designed to work efficiently with pandas DataFrames, which is the standard data structure for time-series analysis in the TCP Trading Platform.
## Overview ## Overview
The module implements five core technical indicators commonly used in trading: The module has been refactored to be **DataFrame-centric**. All calculation methods now expect a pandas DataFrame with a `DatetimeIndex` and the required OHLCV columns (`open`, `high`, `low`, `close`, `volume`). This change simplifies the data pipeline, improves performance through vectorization, and ensures consistency across the platform.
- **Simple Moving Average (SMA)** - Average price over a specified period The module implements five core technical indicators:
- **Exponential Moving Average (EMA)** - Weighted average giving more importance to recent prices
- **Relative Strength Index (RSI)** - Momentum oscillator measuring speed and change of price movements - **Simple Moving Average (SMA)**
- **Moving Average Convergence Divergence (MACD)** - Trend-following momentum indicator - **Exponential Moving Average (EMA)**
- **Bollinger Bands** - Volatility indicator with upper and lower bands around a moving average - **Relative Strength Index (RSI)**
- **Moving Average Convergence Divergence (MACD)**
- **Bollinger Bands**
## Key Features ## Key Features
### Sparse Data Handling - **DataFrame-Centric Design**: Operates directly on pandas DataFrames for performance and simplicity.
- **No Interpolation**: Preserves gaps in timestamp data without artificial interpolation - **Vectorized Calculations**: Leverages pandas and numpy for high-speed computation.
- **Efficient Processing**: Uses pandas for vectorized calculations - **Flexible `calculate` Method**: A single entry point for calculating any supported indicator by name.
- **Right-Aligned Timestamps**: Follows the platform's aggregation strategy convention - **Standardized Output**: All methods return a DataFrame containing the calculated indicator values, indexed by timestamp.
- **Robust Error Handling**: Gracefully handles insufficient data and edge cases
### Performance Optimized
- **Vectorized Calculations**: Leverages pandas and numpy for fast computation
- **Batch Processing**: Calculate multiple indicators simultaneously
- **Memory Efficient**: Processes data in chunks without excessive memory usage
### Flexible Configuration
- **JSON Configuration**: Define indicator parameters via configuration files
- **Multiple Price Columns**: Calculate indicators on open, high, low, or close prices
- **Custom Parameters**: Adjust periods, standard deviations, and other parameters
- **Validation**: Built-in configuration validation
## Usage Examples ## Usage Examples
### Basic Usage ### Preparing the DataFrame
Before you can calculate indicators, you need a properly formatted pandas DataFrame. The `prepare_chart_data` utility is the recommended way to create one from a list of candle dictionaries.
```python ```python
from components.charts.utils import prepare_chart_data
from data.common.indicators import TechnicalIndicators from data.common.indicators import TechnicalIndicators
from data.common.data_types import OHLCVCandle
# Initialize indicators calculator # Assume 'candles' is a list of OHLCV dictionaries from the database
indicators = TechnicalIndicators() # candles = fetch_market_data(...)
# Calculate Simple Moving Average # Prepare the DataFrame
sma_results = indicators.sma(candles, period=20) df = prepare_chart_data(candles)
# Calculate Exponential Moving Average # df is now ready for indicator calculations
ema_results = indicators.ema(candles, period=12) # It has a DatetimeIndex and the necessary OHLCV columns.
# Calculate RSI
rsi_results = indicators.rsi(candles, period=14)
# Calculate MACD
macd_results = indicators.macd(candles, fast_period=12, slow_period=26, signal_period=9)
# Calculate Bollinger Bands
bb_results = indicators.bollinger_bands(candles, period=20, std_dev=2.0)
``` ```
### Multiple Indicators ### Basic Indicator Calculation
Once you have a prepared DataFrame, you can calculate indicators directly.
```python ```python
# Define configuration for multiple indicators # Initialize the calculator
config = { indicators = TechnicalIndicators()
'sma_20': {'type': 'sma', 'period': 20},
'sma_50': {'type': 'sma', 'period': 50},
'ema_12': {'type': 'ema', 'period': 12},
'rsi_14': {'type': 'rsi', 'period': 14},
'macd': {'type': 'macd'},
'bb_20': {'type': 'bollinger_bands', 'period': 20}
}
# Calculate all indicators at once # Calculate a Simple Moving Average
results = indicators.calculate_multiple_indicators(candles, config) sma_df = indicators.sma(df, period=20)
# Access individual indicator results # Calculate an Exponential Moving Average
sma_20_values = results['sma_20'] ema_df = indicators.ema(df, period=12)
rsi_values = results['rsi_14']
macd_values = results['macd'] # 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 ### Using Different Price Columns
```python You can specify which price column (`open`, `high`, `low`, or `close`) to use for the calculation.
# Calculate SMA on high prices instead of close
sma_high = indicators.sma(candles, period=20, price_column='high')
# Calculate EMA on low prices
ema_low = indicators.ema(candles, period=12, price_column='low')
# Calculate RSI on open prices
rsi_open = indicators.rsi(candles, period=14, price_column='open')
```
### Default Configuration
```python ```python
from data.common.indicators import create_default_indicators_config # Calculate SMA on the 'high' price
sma_high_df = indicators.sma(df, period=20, price_column='high')
# Get default configuration # Calculate RSI on the 'open' price
default_config = create_default_indicators_config() rsi_open_pkg = indicators.calculate('rsi', df, period=14, price_column='open')
# Calculate using defaults
results = indicators.calculate_multiple_indicators(candles, default_config)
``` ```
## Indicator Details ## Indicator Details
The following details the parameters and the columns returned in the result DataFrame for each indicator.
### Simple Moving Average (SMA) ### Simple Moving Average (SMA)
Calculates the arithmetic mean of prices over a specified period. - **Parameters**: `period` (int), `price_column` (str, default: 'close')
- **Returned Columns**: `sma`
**Parameters:**
- `period`: Number of periods (default: 20)
- `price_column`: Price column to use (default: 'close')
**Returns:**
- `sma`: Simple moving average value
### Exponential Moving Average (EMA) ### Exponential Moving Average (EMA)
Calculates exponentially weighted moving average, giving more weight to recent prices. - **Parameters**: `period` (int), `price_column` (str, default: 'close')
- **Returned Columns**: `ema`
**Parameters:**
- `period`: Number of periods (default: 20)
- `price_column`: Price column to use (default: 'close')
**Returns:**
- `ema`: Exponential moving average value
### Relative Strength Index (RSI) ### Relative Strength Index (RSI)
Momentum oscillator that measures the speed and change of price movements. - **Parameters**: `period` (int), `price_column` (str, default: 'close')
- **Returned Columns**: `rsi`
**Parameters:**
- `period`: Number of periods (default: 14)
- `price_column`: Price column to use (default: 'close')
**Returns:**
- `rsi`: RSI value (0-100 range)
### MACD (Moving Average Convergence Divergence) ### MACD (Moving Average Convergence Divergence)
Trend-following momentum indicator showing the relationship between two moving averages. - **Parameters**: `fast_period` (int), `slow_period` (int), `signal_period` (int), `price_column` (str, default: 'close')
- **Returned Columns**: `macd`, `signal`, `histogram`
**Parameters:**
- `fast_period`: Fast EMA period (default: 12)
- `slow_period`: Slow EMA period (default: 26)
- `signal_period`: Signal line EMA period (default: 9)
- `price_column`: Price column to use (default: 'close')
**Returns:**
- `macd`: MACD line (fast EMA - slow EMA)
- `signal`: Signal line (EMA of MACD)
- `histogram`: MACD histogram (MACD - Signal)
### Bollinger Bands ### Bollinger Bands
Volatility indicator consisting of a moving average and two standard deviation bands. - **Parameters**: `period` (int), `std_dev` (float), `price_column` (str, default: 'close')
- **Returned Columns**: `upper_band`, `middle_band`, `lower_band`
**Parameters:** ## Integration with the TCP Platform
- `period`: Number of periods for moving average (default: 20)
- `std_dev`: Number of standard deviations (default: 2.0)
- `price_column`: Price column to use (default: 'close')
**Returns:** The refactored `TechnicalIndicators` module is now tightly integrated with the `ChartBuilder`, which handles all data preparation and calculation automatically when indicators are added to a chart. For custom analysis or strategy development, you can use the class directly as shown in the examples above. The key is to always start with a properly prepared DataFrame using `prepare_chart_data`.
- `upper_band`: Upper Bollinger Band
- `middle_band`: Middle band (SMA)
- `lower_band`: Lower Bollinger Band
- `bandwidth`: Band width relative to middle band
- `percent_b`: %B indicator (position within bands)
## Data Structures ## Data Structures

View File

@ -0,0 +1,38 @@
## Relevant Files
- `config/indicators/templates/*.json` - Indicator configuration templates to be updated with the new `timeframe` field.
- `components/charts/indicator_manager.py` - To add `timeframe` to the `UserIndicator` dataclass and related methods.
- `dashboard/layouts/market_data.py` - To add UI elements for selecting the indicator timeframe.
- `dashboard/callbacks/indicators.py` - To handle the new `timeframe` input from the UI.
- `components/charts/data_integration.py` - To implement the core logic for fetching data and calculating indicators on different timeframes.
- `components/charts/builder.py` - To ensure the new indicator data is correctly passed to the chart.
### Notes
- The core of the changes will be in `components/charts/data_integration.py`.
- Careful data alignment (reindexing and forward-filling) will be crucial for correct visualization.
## Tasks
- [x] 1.0 Update Indicator Configuration
- [x] 1.1 Add an optional `timeframe` field to all JSON templates in `config/indicators/templates/`.
- [x] 1.2 Update the `UserIndicator` dataclass in `components/charts/indicator_manager.py` to include `timeframe: Optional[str]`.
- [x] 1.3 Modify `create_indicator` in `IndicatorManager` to accept a `timeframe` parameter.
- [x] 1.4 Update `UserIndicator.from_dict` and `to_dict` to handle the new `timeframe` field.
- [x] 2.0 Implement Multi-Timeframe Data Fetching and Calculation
- [x] 2.1 In `components/charts/data_integration.py`, modify `get_indicator_data`.
- [x] 2.2 If a custom timeframe is present, call `get_market_data_for_indicators` to fetch new data.
- [x] 2.3 If no custom timeframe is set, use the existing `main_df`.
- [x] 2.4 Pass the correct DataFrame to `self.indicators.calculate`.
- [x] 3.0 Align and Merge Indicator Data for Plotting
- [x] 3.1 After calculation, reindex the indicator DataFrame to match the `main_df`'s timestamp index.
- [x] 3.2 Use forward-fill (`ffill`) to handle missing values from reindexing.
- [x] 3.3 Add the aligned data to `indicator_data_map`.
- [x] 4.0 Update UI for Indicator Timeframe Selection
- [x] 4.1 In `dashboard/layouts/market_data.py`, add a `dcc.Dropdown` for timeframe selection in the indicator modal.
- [x] 4.2 In `dashboard/callbacks/indicators.py`, update the save indicator callback to read the timeframe value.
- [x] 4.3 Pass the selected timeframe to `indicator_manager.create_indicator` or `update_indicator`.
- [ ] 5.0 Testing and Validation
- [ ] 5.1 Write unit tests for custom timeframe data fetching and alignment.
- [ ] 5.2 Manually test creating and viewing indicators with various timeframes (higher, lower, and same as chart).
- [ ] 5.3 Verify visual correctness and data integrity on the chart.

View File

@ -0,0 +1,36 @@
## Relevant Files
- `data/common/indicators.py` - This is the primary file to be refactored. The `TechnicalIndicators` class will be modified to be DataFrame-centric.
- `components/charts/utils.py` - The `prepare_chart_data` function in this file needs to be corrected to ensure it properly creates and returns a DataFrame with a `DatetimeIndex`.
- `components/charts/data_integration.py` - This file's `get_indicator_data` method will be simplified to pass the correctly prepared DataFrame to the calculation engine.
- `app_new.py` - The main application file, which will be used to run the dashboard and perform end-to-end testing.
### Notes
- The goal of this refactoring is to create a more robust and maintainable data pipeline for indicator calculations, preventing recurring data type and index errors.
- Pay close attention to ensuring that DataFrames have a consistent `DatetimeIndex` with proper timezone information throughout the pipeline.
## Tasks
- [x] 1.0 Refactor `TechnicalIndicators` Class in `data/common/indicators.py` to be DataFrame-centric.
- [x] 1.1 Modify `sma`, `ema`, `rsi`, `macd`, and `bollinger_bands` methods to accept a pre-formatted DataFrame as their primary input, not a list of candles.
- [x] 1.2 Remove the redundant `prepare_dataframe` call from within each individual indicator method.
- [x] 1.3 Rename `prepare_dataframe` to `_prepare_dataframe_from_list` to signify its new role as a private helper for converting list-based data.
- [x] 1.4 Update the main `calculate` method to be the single point of data preparation, handling both DataFrame and list inputs.
- [x] 2.0 Correct DataFrame Preparation in `components/charts/utils.py`.
- [x] 2.1 Review the `prepare_chart_data` function to identify why the `DatetimeIndex` is being dropped.
- [x] 2.2 Modify the function to ensure it returns a DataFrame with the `timestamp` column correctly set as the index, without a `reset_index()` call at the end.
- [x] 3.0 Simplify Data Flow in `components/charts/data_integration.py`.
- [x] 3.1 In the `get_indicator_data` function, remove the workaround that converts the DataFrame to a list of dictionaries (`to_dict('records')`).
- [x] 3.2 Ensure the function correctly handles both main and custom timeframes, passing the appropriate DataFrame to the calculation engine.
- [x] 3.3 Verify that the final `reindex` operation works correctly with the consistent DataFrame structure.
- [x] 4.0 End-to-End Testing and Validation.
- [x] 4.1 Run the dashboard and test the indicator plotting functionality with both matching and custom timeframes.
- [x] 4.2 Verify that no new errors appear in the console during chart interaction.
- [x] 5.0 Update Indicators documentation to reflect the new DataFrame-centric approach.
- [x] 5.1 Review the documentation in the `/docs` directory related to indicators.
- [x] 5.2 Update the documentation to explain that the calculation engine now uses DataFrames.
- [x] 5.3 Provide clear examples of how to use the refactored `TechnicalIndicators` class.