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

@@ -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")

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.
"""
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([

View File

@@ -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(