213 lines
9.6 KiB
Python
Raw Normal View History

"""
Chart-related callbacks for the dashboard.
"""
from dash import Output, Input, State, Patch, ctx, html, no_update, dcc
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
2025-06-04 17:03:35 +08:00
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:
predefined_ranges = ['1h', '4h', '6h', '12h', '1d', '3d', '7d', '30d']
if time_range_quick in predefined_ranges:
time_map = {
'1h': (1/24, '🕐 Last 1 Hour'), '4h': (4/24, '🕐 Last 4 Hours'), '6h': (6/24, '🕐 Last 6 Hours'),
'12h': (12/24, '🕐 Last 12 Hours'), '1d': (1, '📅 Last 1 Day'), '3d': (3, '📅 Last 3 Days'),
'7d': (7, '📅 Last 7 Days'), '30d': (30, '📅 Last 30 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):
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):
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):
if not stored_data:
return html.Div("Statistics will be available once chart data is loaded.")
try:
df = pd.read_json(io.StringIO(stored_data), orient='split')
if df.empty:
return html.Div("Not enough data to calculate statistics.")
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 html.Div(f"Error loading statistics: {e}", style={'color': 'red'})
@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):
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):
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):
if n_clicks and n_clicks > 0:
return None, None, '7d'
return no_update, no_update, no_update
2025-06-04 17:03:35 +08:00
logger.info("Chart callback: Chart callbacks registered successfully")