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:
@@ -96,6 +96,7 @@ def register_indicator_callbacks(app):
|
||||
[State('indicator-name-input', 'value'),
|
||||
State('indicator-type-dropdown', 'value'),
|
||||
State('indicator-description-input', 'value'),
|
||||
State('indicator-timeframe-dropdown', 'value'),
|
||||
State('indicator-color-input', 'value'),
|
||||
State('indicator-line-width-slider', 'value'),
|
||||
# SMA parameters
|
||||
@@ -115,7 +116,7 @@ def register_indicator_callbacks(app):
|
||||
State('edit-indicator-store', 'data')],
|
||||
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,
|
||||
macd_fast, macd_slow, macd_signal,
|
||||
bb_period, bb_stddev, edit_data):
|
||||
@@ -161,7 +162,8 @@ def register_indicator_callbacks(app):
|
||||
name=name,
|
||||
description=description or "",
|
||||
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:
|
||||
@@ -176,7 +178,8 @@ def register_indicator_callbacks(app):
|
||||
indicator_type=indicator_type,
|
||||
parameters=parameters,
|
||||
description=description or "",
|
||||
color=color or "#007bff"
|
||||
color=color or "#007bff",
|
||||
timeframe=timeframe or None
|
||||
)
|
||||
|
||||
if not new_indicator:
|
||||
@@ -384,6 +387,7 @@ def register_indicator_callbacks(app):
|
||||
Output('indicator-name-input', 'value'),
|
||||
Output('indicator-type-dropdown', 'value'),
|
||||
Output('indicator-description-input', 'value'),
|
||||
Output('indicator-timeframe-dropdown', 'value'),
|
||||
Output('indicator-color-input', 'value'),
|
||||
Output('edit-indicator-store', 'data'),
|
||||
# Add parameter field outputs
|
||||
@@ -403,7 +407,7 @@ def register_indicator_callbacks(app):
|
||||
"""Load indicator data for editing."""
|
||||
ctx = callback_context
|
||||
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
|
||||
triggered_id = ctx.triggered[0]['prop_id']
|
||||
@@ -418,41 +422,42 @@ def register_indicator_callbacks(app):
|
||||
|
||||
if indicator:
|
||||
# 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
|
||||
params = indicator.parameters
|
||||
|
||||
# Default parameter values
|
||||
sma_period = 20
|
||||
ema_period = 12
|
||||
rsi_period = 14
|
||||
macd_fast = 12
|
||||
macd_slow = 26
|
||||
macd_signal = 9
|
||||
bb_period = 20
|
||||
bb_stddev = 2.0
|
||||
sma_period = None
|
||||
ema_period = None
|
||||
rsi_period = None
|
||||
macd_fast = None
|
||||
macd_slow = None
|
||||
macd_signal = None
|
||||
bb_period = None
|
||||
bb_stddev = None
|
||||
|
||||
# Update with actual saved values
|
||||
if indicator.type == 'sma':
|
||||
sma_period = params.get('period', 20)
|
||||
sma_period = params.get('period')
|
||||
elif indicator.type == 'ema':
|
||||
ema_period = params.get('period', 12)
|
||||
ema_period = params.get('period')
|
||||
elif indicator.type == 'rsi':
|
||||
rsi_period = params.get('period', 14)
|
||||
rsi_period = params.get('period')
|
||||
elif indicator.type == 'macd':
|
||||
macd_fast = params.get('fast_period', 12)
|
||||
macd_slow = params.get('slow_period', 26)
|
||||
macd_signal = params.get('signal_period', 9)
|
||||
macd_fast = params.get('fast_period')
|
||||
macd_slow = params.get('slow_period')
|
||||
macd_signal = params.get('signal_period')
|
||||
elif indicator.type == 'bollinger_bands':
|
||||
bb_period = params.get('period', 20)
|
||||
bb_stddev = params.get('std_dev', 2.0)
|
||||
bb_period = params.get('period')
|
||||
bb_stddev = params.get('std_dev')
|
||||
|
||||
return (
|
||||
"✏️ Edit Indicator",
|
||||
f"✏️ Edit Indicator: {indicator.name}",
|
||||
indicator.name,
|
||||
indicator.type,
|
||||
indicator.description,
|
||||
indicator.timeframe,
|
||||
indicator.styling.color,
|
||||
edit_data,
|
||||
sma_period,
|
||||
@@ -465,17 +470,18 @@ def register_indicator_callbacks(app):
|
||||
bb_stddev
|
||||
)
|
||||
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:
|
||||
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
|
||||
@app.callback(
|
||||
[Output('indicator-name-input', 'value', allow_duplicate=True),
|
||||
Output('indicator-type-dropdown', '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-line-width-slider', 'value'),
|
||||
Output('modal-title', 'children', allow_duplicate=True),
|
||||
@@ -494,9 +500,7 @@ def register_indicator_callbacks(app):
|
||||
prevent_initial_call=True
|
||||
)
|
||||
def reset_modal_form(cancel_clicks, save_clicks):
|
||||
"""Reset the modal form when it's closed or saved."""
|
||||
if cancel_clicks or save_clicks:
|
||||
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
|
||||
"""Reset the modal form to its default state."""
|
||||
return "", "", "", "", "", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0
|
||||
|
||||
logger.info("Indicator callbacks: registered successfully")
|
||||
@@ -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.
|
||||
"""
|
||||
if df.empty:
|
||||
return html.Div("No data available for statistics.", className="text-center text-muted")
|
||||
|
||||
try:
|
||||
volume_analyzer = VolumeAnalyzer()
|
||||
# Get statistics
|
||||
price_analyzer = PriceMovementAnalyzer()
|
||||
volume_analyzer = VolumeAnalyzer()
|
||||
|
||||
volume_stats = volume_analyzer.get_volume_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:
|
||||
error_msg = volume_stats.get('error') or price_stats.get('error')
|
||||
# Format key statistics for display
|
||||
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'})
|
||||
|
||||
# Time range for display
|
||||
start_date = df['timestamp'].min().strftime('%Y-%m-%d %H:%M')
|
||||
end_date = df['timestamp'].max().strftime('%Y-%m-%d %H:%M')
|
||||
days_back = (df['timestamp'].max() - df['timestamp'].min()).days
|
||||
days_back = (df.index.max() - df.index.min()).days
|
||||
time_status = f"📅 Analysis Range: {start_date} to {end_date} (~{days_back} days)"
|
||||
|
||||
return html.Div([
|
||||
|
||||
@@ -33,6 +33,27 @@ def create_indicator_modal():
|
||||
placeholder='Select indicator type',
|
||||
), width=12)
|
||||
], 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.Col(dbc.Label("Description (Optional):"), width=12),
|
||||
dbc.Col(dcc.Textarea(
|
||||
|
||||
Reference in New Issue
Block a user