- Introduced a new `system_health_constants.py` file to define thresholds and constants for system health metrics. - Refactored existing system health callbacks into modular components, enhancing maintainability and clarity. - Implemented dynamic loading of time range options in `charts.py`, improving flexibility in time range selection. - Added detailed documentation for new callback functions, ensuring clarity on their purpose and usage. - Enhanced error handling and logging practices across the new modules to ensure robust monitoring and debugging capabilities. These changes significantly improve the architecture and maintainability of the system health monitoring features, aligning with project standards for modularity and performance.
301 lines
14 KiB
Python
301 lines
14 KiB
Python
"""
|
|
Chart-related callbacks for the dashboard.
|
|
"""
|
|
|
|
from dash import Output, Input, State, Patch, ctx, html, no_update, dcc
|
|
import dash_bootstrap_components as dbc
|
|
from datetime import datetime, timedelta
|
|
from utils.logger import get_logger
|
|
from components.charts import (
|
|
create_strategy_chart,
|
|
create_chart_with_indicators,
|
|
create_error_chart,
|
|
)
|
|
from dashboard.components.data_analysis import get_market_statistics
|
|
from components.charts.config import get_all_example_strategies
|
|
from database.connection import DatabaseManager
|
|
from components.charts.builder import ChartBuilder
|
|
from components.charts.utils import prepare_chart_data
|
|
import pandas as pd
|
|
import io
|
|
from utils.time_range_utils import load_time_range_options
|
|
|
|
logger = get_logger("default_logger")
|
|
|
|
|
|
def calculate_time_range(time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals):
|
|
"""Calculate days_back and status message based on time range controls."""
|
|
try:
|
|
# Load time range options dynamically
|
|
time_range_options = load_time_range_options()
|
|
predefined_ranges = [option['value'] for option in time_range_options if option['value'] not in ['custom', 'realtime']]
|
|
|
|
time_map = {}
|
|
|
|
# Dynamically create time_map from loaded options
|
|
for option in time_range_options:
|
|
value = option['value']
|
|
label = option['label']
|
|
if value.endswith('h'):
|
|
days_back_fractional = int(value[:-1]) / 24
|
|
elif value.endswith('d'):
|
|
days_back_fractional = int(value[:-1])
|
|
else:
|
|
continue # Skip custom and realtime, and any other unexpected values
|
|
|
|
time_map[value] = (days_back_fractional, label)
|
|
|
|
if time_range_quick in predefined_ranges:
|
|
# Ensure the selected time_range_quick exists in our dynamically created time_map
|
|
if time_range_quick not in time_map:
|
|
logger.warning(f"Selected time range quick option '{time_range_quick}' not found in time_map. Defaulting to 7 days.")
|
|
return 7, f"⚠️ Invalid time range selected. Defaulting to 7 days."
|
|
|
|
days_back_fractional, label = time_map[time_range_quick]
|
|
mode_text = "🔒 Locked" if analysis_mode == 'locked' else "🔴 Live"
|
|
status = f"{label} | {mode_text}"
|
|
days_back = days_back_fractional if days_back_fractional < 1 else int(days_back_fractional)
|
|
return days_back, status
|
|
|
|
if time_range_quick == 'custom' and custom_start_date and custom_end_date:
|
|
start_date = datetime.fromisoformat(custom_start_date.split('T')[0])
|
|
end_date = datetime.fromisoformat(custom_end_date.split('T')[0])
|
|
days_diff = (end_date - start_date).days
|
|
status = f"📅 Custom Range: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')} ({days_diff} days)"
|
|
return max(1, days_diff), status
|
|
|
|
if time_range_quick == 'realtime':
|
|
mode_text = "🔒 Analysis Mode" if analysis_mode == 'locked' else "🔴 Real-time Updates"
|
|
status = f"📈 Real-time Mode | {mode_text} (Default: Last 7 Days)"
|
|
return 7, status
|
|
|
|
mode_text = "🔒 Analysis Mode" if analysis_mode == 'locked' else "🔴 Live"
|
|
default_label = "📅 Default (Last 7 Days)"
|
|
if time_range_quick == 'custom' and not (custom_start_date and custom_end_date):
|
|
default_label = "⏳ Select Custom Dates"
|
|
status = f"{default_label} | {mode_text}"
|
|
return 7, status
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error calculating time range: {e}. Defaulting to 7 days.")
|
|
return 7, f"⚠️ Error in time range. Defaulting to 7 days."
|
|
|
|
|
|
def register_chart_callbacks(app):
|
|
"""Register chart-related callbacks."""
|
|
|
|
@app.callback(
|
|
[Output('price-chart', 'figure'),
|
|
Output('time-range-status', 'children'),
|
|
Output('chart-data-store', 'data')],
|
|
[Input('symbol-dropdown', 'value'),
|
|
Input('timeframe-dropdown', 'value'),
|
|
Input('overlay-indicators-checklist', 'value'),
|
|
Input('subplot-indicators-checklist', 'value'),
|
|
Input('strategy-dropdown', 'value'),
|
|
Input('time-range-quick-select', 'value'),
|
|
Input('custom-date-range', 'start_date'),
|
|
Input('custom-date-range', 'end_date'),
|
|
Input('analysis-mode-toggle', 'value'),
|
|
Input('interval-component', 'n_intervals')],
|
|
[State('price-chart', 'relayoutData'),
|
|
State('price-chart', 'figure')]
|
|
)
|
|
def update_price_chart(symbol, timeframe, overlay_indicators, subplot_indicators, selected_strategy,
|
|
time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals,
|
|
relayout_data, current_figure):
|
|
"""Updates the main price chart based on user selections and time range.
|
|
|
|
Args:
|
|
symbol (str): The selected trading symbol.
|
|
timeframe (str): The selected chart timeframe (e.g., '1h', '1d').
|
|
overlay_indicators (list): List of selected overlay indicators.
|
|
subplot_indicators (list): List of selected subplot indicators.
|
|
selected_strategy (str): The selected trading strategy.
|
|
time_range_quick (str): Quick time range selection (e.g., '7d', 'custom').
|
|
custom_start_date (str): Custom start date for the chart.
|
|
custom_end_date (str): Custom end date for the chart.
|
|
analysis_mode (str): The current analysis mode ('locked' or 'live').
|
|
n_intervals (int): Interval component trigger.
|
|
relayout_data (dict): Data from chart relayout events (e.g., zoom/pan).
|
|
current_figure (dict): The current chart figure data.
|
|
|
|
Returns:
|
|
tuple: A tuple containing:
|
|
- dash.Dash.figure: The updated price chart figure.
|
|
- str: The status message for the time range.
|
|
- str: JSON string of the chart data.
|
|
"""
|
|
try:
|
|
triggered_id = ctx.triggered_id
|
|
if triggered_id == 'interval-component' and analysis_mode == 'locked':
|
|
return no_update, no_update, no_update
|
|
|
|
days_back, status_message = calculate_time_range(
|
|
time_range_quick, custom_start_date, custom_end_date, analysis_mode, n_intervals
|
|
)
|
|
|
|
chart_df = pd.DataFrame()
|
|
if selected_strategy and selected_strategy != 'basic':
|
|
fig, chart_df = create_strategy_chart(symbol, timeframe, selected_strategy, days_back=days_back)
|
|
else:
|
|
fig, chart_df = create_chart_with_indicators(
|
|
symbol=symbol, timeframe=timeframe,
|
|
overlay_indicators=overlay_indicators or [], subplot_indicators=subplot_indicators or [],
|
|
days_back=days_back
|
|
)
|
|
|
|
stored_data = None
|
|
if chart_df is not None and not chart_df.empty:
|
|
stored_data = chart_df.to_json(orient='split', date_format='iso')
|
|
|
|
if relayout_data and 'xaxis.range' in relayout_data:
|
|
fig.update_layout(xaxis=dict(range=relayout_data['xaxis.range']), yaxis=dict(range=relayout_data.get('yaxis.range')))
|
|
|
|
return fig, status_message, stored_data
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating price chart: {e}", exc_info=True)
|
|
error_fig = create_error_chart(f"Error loading chart: {str(e)}")
|
|
return error_fig, f"❌ Error: {str(e)}", None
|
|
|
|
@app.callback(
|
|
Output('analysis-mode-toggle', 'value'),
|
|
Input('price-chart', 'relayoutData'),
|
|
State('analysis-mode-toggle', 'value'),
|
|
prevent_initial_call=True
|
|
)
|
|
def auto_lock_chart_on_interaction(relayout_data, current_mode):
|
|
"""Automatically locks the chart to 'analysis' mode upon user interaction (zoom/pan).
|
|
|
|
Args:
|
|
relayout_data (dict): Data from chart relayout events (e.g., zoom/pan).
|
|
current_mode (str): The current analysis mode ('locked' or 'live').
|
|
|
|
Returns:
|
|
str: The new analysis mode ('locked') if interaction occurred and not already locked, else no_update.
|
|
"""
|
|
if relayout_data and 'xaxis.range' in relayout_data and current_mode != 'locked':
|
|
return 'locked'
|
|
return no_update
|
|
|
|
@app.callback(
|
|
Output('market-stats', 'children'),
|
|
[Input('chart-data-store', 'data')],
|
|
[State('symbol-dropdown', 'value'),
|
|
State('timeframe-dropdown', 'value')]
|
|
)
|
|
def update_market_stats(stored_data, symbol, timeframe):
|
|
"""Updates the market statistics display based on the stored chart data.
|
|
|
|
Args:
|
|
stored_data (str): JSON string of the chart data.
|
|
symbol (str): The selected trading symbol.
|
|
timeframe (str): The selected chart timeframe.
|
|
|
|
Returns:
|
|
dbc.Alert or html.Div: An alert message or the market statistics component.
|
|
"""
|
|
if not stored_data:
|
|
return dbc.Alert("Statistics will be available once chart data is loaded.", color="info")
|
|
try:
|
|
df = pd.read_json(io.StringIO(stored_data), orient='split')
|
|
if df.empty:
|
|
return dbc.Alert("Not enough data to calculate statistics.", color="warning")
|
|
return get_market_statistics(df, symbol, timeframe)
|
|
except Exception as e:
|
|
logger.error(f"Error updating market stats from stored data: {e}", exc_info=True)
|
|
return dbc.Alert(f"Error loading statistics: {e}", color="danger")
|
|
|
|
@app.callback(
|
|
Output("download-chart-data", "data"),
|
|
[Input("export-csv-btn", "n_clicks"),
|
|
Input("export-json-btn", "n_clicks")],
|
|
[State("chart-data-store", "data"),
|
|
State("symbol-dropdown", "value"),
|
|
State("timeframe-dropdown", "value")],
|
|
prevent_initial_call=True,
|
|
)
|
|
def export_chart_data(csv_clicks, json_clicks, stored_data, symbol, timeframe):
|
|
"""Exports chart data to CSV or JSON based on button clicks.
|
|
|
|
Args:
|
|
csv_clicks (int): Number of clicks on the export CSV button.
|
|
json_clicks (int): Number of clicks on the export JSON button.
|
|
stored_data (str): JSON string of the chart data.
|
|
symbol (str): The selected trading symbol.
|
|
timeframe (str): The selected chart timeframe.
|
|
|
|
Returns:
|
|
dict: Data for download (filename and content) or no_update.
|
|
"""
|
|
triggered_id = ctx.triggered_id
|
|
if not triggered_id or not stored_data:
|
|
return no_update
|
|
try:
|
|
df = pd.read_json(io.StringIO(stored_data), orient='split')
|
|
if df.empty:
|
|
return no_update
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
filename_base = f"chart_data_{symbol}_{timeframe}_{timestamp}"
|
|
if triggered_id == "export-csv-btn":
|
|
return dcc.send_data_frame(df.to_csv, f"{filename_base}.csv", index=False)
|
|
elif triggered_id == "export-json-btn":
|
|
return dict(content=df.to_json(orient='records', date_format='iso'), filename=f"{filename_base}.json")
|
|
except Exception as e:
|
|
logger.error(f"Error exporting chart data from store: {e}", exc_info=True)
|
|
return no_update
|
|
|
|
@app.callback(
|
|
[Output('overlay-indicators-checklist', 'value'),
|
|
Output('subplot-indicators-checklist', 'value')],
|
|
[Input('strategy-dropdown', 'value')]
|
|
)
|
|
def update_indicators_from_strategy(selected_strategy):
|
|
"""Updates the overlay and subplot indicators based on the selected strategy.
|
|
|
|
Args:
|
|
selected_strategy (str): The currently selected trading strategy.
|
|
|
|
Returns:
|
|
tuple: A tuple containing lists of overlay indicators and subplot indicators.
|
|
"""
|
|
if not selected_strategy or selected_strategy == 'basic':
|
|
return [], []
|
|
try:
|
|
all_strategies = get_all_example_strategies()
|
|
if selected_strategy in all_strategies:
|
|
strategy_example = all_strategies[selected_strategy]
|
|
config = strategy_example.config
|
|
overlay_indicators = config.overlay_indicators or []
|
|
subplot_indicators = []
|
|
for subplot_config in config.subplot_configs or []:
|
|
subplot_indicators.extend(subplot_config.indicators or [])
|
|
return overlay_indicators, subplot_indicators
|
|
else:
|
|
return [], []
|
|
except Exception as e:
|
|
logger.error(f"Error loading strategy indicators: {e}", exc_info=True)
|
|
return [], []
|
|
|
|
@app.callback(
|
|
[Output('custom-date-range', 'start_date'),
|
|
Output('custom-date-range', 'end_date'),
|
|
Output('time-range-quick-select', 'value')],
|
|
[Input('clear-date-range-btn', 'n_clicks')],
|
|
prevent_initial_call=True
|
|
)
|
|
def clear_custom_date_range(n_clicks):
|
|
"""Clears the custom date range and resets the quick time range selection.
|
|
|
|
Args:
|
|
n_clicks (int): Number of clicks on the clear date range button.
|
|
|
|
Returns:
|
|
tuple: A tuple containing None for start and end dates, and '7d' for quick select, or no_update.
|
|
"""
|
|
if n_clicks and n_clicks > 0:
|
|
return None, None, '7d'
|
|
return no_update, no_update, no_update
|
|
|
|
logger.info("Chart callback: Chart callbacks registered successfully") |