TCPDashboard/app.py
Vasily.onl 476bd67f14 3.4 Implement user-defined indicator management system and enhance chart capabilities
- Introduced a comprehensive user indicator management system in `components/charts/indicator_manager.py`, allowing users to create, edit, and manage custom indicators with JSON persistence.
- Added new default indicators in `components/charts/indicator_defaults.py` to provide users with immediate options for technical analysis.
- Enhanced the chart rendering capabilities by implementing the `create_chart_with_indicators` function in `components/charts/builder.py`, supporting both overlay and subplot indicators.
- Updated the main application layout in `app.py` to include a modal for adding and editing indicators, improving user interaction.
- Enhanced documentation to cover the new indicator system, including a quick guide for adding new indicators and detailed usage examples.
- Added unit tests to ensure the reliability and functionality of the new indicator management features.
2025-06-04 13:01:57 +08:00

1523 lines
63 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Main Dash application for the Crypto Trading Bot Dashboard.
Provides real-time visualization and bot management interface.
"""
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
# Suppress SQLAlchemy logging to reduce verbosity
import logging
logging.getLogger('sqlalchemy').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.dialects').setLevel(logging.WARNING)
logging.getLogger('sqlalchemy.orm').setLevel(logging.WARNING)
import dash
from dash import dcc, html, Input, Output, State, callback
import plotly.graph_objects as go
from datetime import datetime, timedelta
import pandas as pd
# Import project modules
from config.settings import app as app_settings, dashboard as dashboard_settings
from utils.logger import get_logger
from database.connection import DatabaseManager
from components.charts import (
create_candlestick_chart, get_market_statistics,
get_supported_symbols, get_supported_timeframes,
create_data_status_indicator, check_data_availability,
create_error_chart, create_strategy_chart, create_chart_with_indicators
)
from components.charts.config import (
get_available_strategy_names,
get_all_example_strategies,
get_overlay_indicators,
get_subplot_indicators,
get_all_default_indicators,
get_indicators_by_category
)
from components.charts.indicator_manager import get_indicator_manager
from components.charts.indicator_defaults import ensure_default_indicators
# Initialize logger
logger = get_logger("dashboard_app")
# Create the app instance at module level
app = dash.Dash(
__name__,
title="Crypto Trading Bot Dashboard",
update_title="Loading...",
suppress_callback_exceptions=True
)
# Configure app
app.server.secret_key = "crypto-bot-dashboard-secret-key-2024"
logger.info("Initializing Crypto Trading Bot Dashboard")
# Define basic layout
app.layout = html.Div([
# Header
html.Div([
html.H1("🚀 Crypto Trading Bot Dashboard",
style={'margin': '0', 'color': '#2c3e50'}),
html.P("Real-time monitoring and bot management",
style={'margin': '5px 0 0 0', 'color': '#7f8c8d'})
], style={
'padding': '20px',
'background-color': '#ecf0f1',
'border-bottom': '2px solid #bdc3c7'
}),
# Navigation tabs
dcc.Tabs(id="main-tabs", value='market-data', children=[
dcc.Tab(label='📊 Market Data', value='market-data'),
dcc.Tab(label='🤖 Bot Management', value='bot-management'),
dcc.Tab(label='📈 Performance', value='performance'),
dcc.Tab(label='⚙️ System Health', value='system-health'),
], style={'margin': '10px 20px'}),
# Main content area
html.Div(id='tab-content', style={'padding': '20px'}),
# Auto-refresh interval for real-time updates
dcc.Interval(
id='interval-component',
interval=5000, # Update every 5 seconds
n_intervals=0
),
# Store components for data sharing between callbacks
dcc.Store(id='market-data-store'),
dcc.Store(id='bot-status-store'),
# Hidden button for callback compatibility (real button is in market data layout)
html.Button(id='add-indicator-btn', style={'display': 'none'}),
# Add Indicator Modal
html.Div([
dcc.Store(id='edit-indicator-store', data=None), # Store for edit mode - explicitly start with None
# Modal Background
html.Div(
id='indicator-modal-background',
style={
'display': 'none',
'position': 'fixed',
'z-index': '1000',
'left': '0',
'top': '0',
'width': '100%',
'height': '100%',
'background-color': 'rgba(0,0,0,0.5)',
'visibility': 'hidden'
}
),
# Modal Content
html.Div([
html.Div([
# Modal Header
html.Div([
html.H4("📊 Add New Indicator", id="modal-title", style={'margin': '0', 'color': '#2c3e50'}),
html.Button(
"",
id="close-modal-btn",
style={
'background': 'none',
'border': 'none',
'font-size': '24px',
'cursor': 'pointer',
'color': '#999',
'float': 'right'
}
)
], style={'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-bottom': '20px', 'border-bottom': '1px solid #eee', 'padding-bottom': '10px'}),
# Modal Body
html.Div([
# Basic Settings
html.Div([
html.H5("Basic Settings", style={'color': '#2c3e50', 'margin-bottom': '15px'}),
# Indicator Name
html.Div([
html.Label("Indicator Name:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='indicator-name-input',
type='text',
placeholder='e.g., "SMA 30 Custom"',
style={'width': '100%', 'padding': '8px', 'margin-bottom': '10px', 'border': '1px solid #ddd', 'border-radius': '4px'}
)
]),
# Indicator Type
html.Div([
html.Label("Indicator Type:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Dropdown(
id='indicator-type-dropdown',
options=[
{'label': 'Simple Moving Average (SMA)', 'value': 'sma'},
{'label': 'Exponential Moving Average (EMA)', 'value': 'ema'},
{'label': 'Relative Strength Index (RSI)', 'value': 'rsi'},
{'label': 'MACD', 'value': 'macd'},
{'label': 'Bollinger Bands', 'value': 'bollinger_bands'}
],
placeholder='Select indicator type',
style={'margin-bottom': '10px'}
)
]),
# Description
html.Div([
html.Label("Description (Optional):", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Textarea(
id='indicator-description-input',
placeholder='Brief description of this indicator configuration...',
style={'width': '100%', 'height': '60px', 'padding': '8px', 'margin-bottom': '15px', 'border': '1px solid #ddd', 'border-radius': '4px', 'resize': 'vertical'}
)
])
], style={'margin-bottom': '20px'}),
# Parameters Section
html.Div([
html.H5("Parameters", style={'color': '#2c3e50', 'margin-bottom': '15px'}),
# Default message
html.Div(
id='indicator-parameters-message',
children=[html.P("Select an indicator type to configure parameters", style={'color': '#7f8c8d', 'font-style': 'italic'})],
style={'display': 'block'}
),
# SMA Parameters (hidden by default)
html.Div([
html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='sma-period-input',
type='number',
value=20,
min=1, max=200,
style={'width': '100px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'}
),
html.P("Number of periods for Simple Moving Average calculation", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'})
], id='sma-parameters', style={'display': 'none', 'margin-bottom': '10px'}),
# EMA Parameters (hidden by default)
html.Div([
html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='ema-period-input',
type='number',
value=12,
min=1, max=200,
style={'width': '100px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'}
),
html.P("Number of periods for Exponential Moving Average calculation", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'})
], id='ema-parameters', style={'display': 'none', 'margin-bottom': '10px'}),
# RSI Parameters (hidden by default)
html.Div([
html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='rsi-period-input',
type='number',
value=14,
min=2, max=50,
style={'width': '100px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'}
),
html.P("Number of periods for RSI calculation (typically 14)", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'})
], id='rsi-parameters', style={'display': 'none', 'margin-bottom': '10px'}),
# MACD Parameters (hidden by default)
html.Div([
html.Div([
html.Label("Fast Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='macd-fast-period-input',
type='number',
value=12,
min=2, max=50,
style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'}
)
], style={'margin-bottom': '10px'}),
html.Div([
html.Label("Slow Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='macd-slow-period-input',
type='number',
value=26,
min=5, max=100,
style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'}
)
], style={'margin-bottom': '10px'}),
html.Div([
html.Label("Signal Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='macd-signal-period-input',
type='number',
value=9,
min=2, max=30,
style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'}
)
]),
html.P("MACD periods: Fast EMA, Slow EMA, and Signal line", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'})
], id='macd-parameters', style={'display': 'none', 'margin-bottom': '10px'}),
# Bollinger Bands Parameters (hidden by default)
html.Div([
html.Div([
html.Label("Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='bb-period-input',
type='number',
value=20,
min=5, max=100,
style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'}
)
], style={'margin-bottom': '10px'}),
html.Div([
html.Label("Standard Deviation:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='bb-stddev-input',
type='number',
value=2.0,
min=0.5, max=5.0, step=0.1,
style={'width': '80px', 'padding': '8px', 'border': '1px solid #ddd', 'border-radius': '4px'}
)
]),
html.P("Period for middle line (SMA) and standard deviation multiplier", style={'color': '#7f8c8d', 'font-size': '12px', 'margin-top': '5px'})
], id='bb-parameters', style={'display': 'none', 'margin-bottom': '10px'})
], style={'margin-bottom': '20px'}),
# Styling Section
html.Div([
html.H5("Styling", style={'color': '#2c3e50', 'margin-bottom': '15px'}),
html.Div([
# Color Picker
html.Div([
html.Label("Color:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Input(
id='indicator-color-input',
type='text',
value='#007bff',
style={'width': '100px', 'padding': '8px', 'margin-bottom': '10px', 'border': '1px solid #ddd', 'border-radius': '4px'}
)
], style={'width': '48%', 'display': 'inline-block', 'margin-right': '4%'}),
# Line Width
html.Div([
html.Label("Line Width:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Slider(
id='indicator-line-width-slider',
min=1, max=5, step=1, value=2,
marks={i: str(i) for i in range(1, 6)},
tooltip={'placement': 'bottom', 'always_visible': True}
)
], style={'width': '48%', 'display': 'inline-block'})
])
], style={'margin-bottom': '20px'})
]),
# Modal Footer
html.Div([
html.Button(
"Cancel",
id="cancel-indicator-btn",
style={
'background-color': '#6c757d',
'color': 'white',
'border': 'none',
'padding': '10px 20px',
'border-radius': '4px',
'cursor': 'pointer',
'margin-right': '10px'
}
),
html.Button(
"Save Indicator",
id="save-indicator-btn",
style={
'background-color': '#28a745',
'color': 'white',
'border': 'none',
'padding': '10px 20px',
'border-radius': '4px',
'cursor': 'pointer',
'font-weight': 'bold'
}
),
html.Div(id='save-indicator-feedback', style={'margin-top': '10px'})
], style={'text-align': 'right', 'border-top': '1px solid #eee', 'padding-top': '15px'})
], style={
'background-color': 'white',
'margin': '5% auto',
'padding': '30px',
'border-radius': '8px',
'box-shadow': '0 4px 6px rgba(0, 0, 0, 0.1)',
'width': '600px',
'max-width': '90%',
'max-height': '80%',
'overflow-y': 'auto'
})
],
id='indicator-modal',
style={
'display': 'none',
'position': 'fixed',
'z-index': '1001',
'left': '0',
'top': '0',
'width': '100%',
'height': '100%',
'visibility': 'hidden'
})
])
])
def get_market_data_layout():
"""Create the market data visualization layout with indicator controls."""
# Get available symbols and timeframes from database
symbols = get_supported_symbols()
timeframes = get_supported_timeframes()
# Create dropdown options
symbol_options = [{'label': symbol, 'value': symbol} for symbol in symbols]
timeframe_options = [
{'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'},
]
# Filter timeframe options to only show those available in database
available_timeframes = [tf for tf in ['1m', '5m', '15m', '1h', '4h', '1d'] if tf in timeframes]
if not available_timeframes:
available_timeframes = ['1h'] # Default fallback
timeframe_options = [opt for opt in timeframe_options if opt['value'] in available_timeframes]
# Get available strategies and indicators
try:
strategy_names = get_available_strategy_names()
strategy_options = [{'label': name.replace('_', ' ').title(), 'value': name} for name in strategy_names]
# Get user indicators from the new indicator manager
indicator_manager = get_indicator_manager()
# Ensure default indicators exist
ensure_default_indicators()
# Get indicators by display type
overlay_indicators = indicator_manager.get_indicators_by_type('overlay')
subplot_indicators = indicator_manager.get_indicators_by_type('subplot')
# Create checkbox options for overlay indicators
overlay_options = []
for indicator in overlay_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
overlay_options.append({'label': display_name, 'value': indicator.id})
# Create checkbox options for subplot indicators
subplot_options = []
for indicator in subplot_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
subplot_options.append({'label': display_name, 'value': indicator.id})
except Exception as e:
logger.warning(f"Error loading indicator options: {e}")
strategy_options = [{'label': 'Basic Chart', 'value': 'basic'}]
overlay_options = []
subplot_options = []
# Chart Configuration Panel with Add/Edit UI
chart_config_panel = html.Div([
html.H5("🎯 Chart Configuration", style={'color': '#2c3e50', 'margin-bottom': '15px'}),
# Add New Indicator Button
html.Div([
html.Button(
" Add New Indicator",
id="add-indicator-btn-visible",
className="btn btn-primary",
style={
'background-color': '#007bff',
'color': 'white',
'border': 'none',
'padding': '8px 16px',
'border-radius': '4px',
'cursor': 'pointer',
'margin-bottom': '15px',
'font-weight': 'bold'
}
)
]),
# Strategy Selection
html.Div([
html.Label("Strategy Template:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Dropdown(
id='strategy-dropdown',
options=strategy_options,
value=None,
placeholder="Select a strategy template (optional)",
style={'margin-bottom': '15px'}
)
]),
# Indicator Controls with Edit Buttons
html.Div([
# Overlay Indicators
html.Div([
html.Label("Overlay Indicators:", style={'font-weight': 'bold', 'margin-bottom': '10px', 'display': 'block'}),
html.Div([
# Hidden checklist for callback compatibility
dcc.Checklist(
id='overlay-indicators-checklist',
options=overlay_options,
value=[], # Start with no indicators selected
style={'display': 'none'} # Hide the basic checklist
),
# Custom indicator list with edit buttons
html.Div(id='overlay-indicators-list', children=[
# This will be populated dynamically
])
])
], style={'width': '48%', 'display': 'inline-block', 'margin-right': '4%', 'vertical-align': 'top'}),
# Subplot Indicators
html.Div([
html.Label("Subplot Indicators:", style={'font-weight': 'bold', 'margin-bottom': '10px', 'display': 'block'}),
html.Div([
# Hidden checklist for callback compatibility
dcc.Checklist(
id='subplot-indicators-checklist',
options=subplot_options,
value=[], # Start with no indicators selected
style={'display': 'none'} # Hide the basic checklist
),
# Custom indicator list with edit buttons
html.Div(id='subplot-indicators-list', children=[
# This will be populated dynamically
])
])
], style={'width': '48%', 'display': 'inline-block', 'vertical-align': 'top'})
])
], style={
'border': '1px solid #bdc3c7',
'border-radius': '8px',
'padding': '15px',
'background-color': '#f8f9fa',
'margin-bottom': '20px'
})
# Parameter Controls Section
parameter_controls = html.Div([
html.H5("📊 Indicator Parameters", style={'color': '#2c3e50', 'margin-bottom': '15px'}),
# SMA/EMA Period Controls
html.Div([
html.Label("Moving Average Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Slider(
id='ma-period-slider',
min=5, max=200, step=5, value=20,
marks={i: str(i) for i in [5, 20, 50, 100, 200]},
tooltip={'placement': 'bottom', 'always_visible': True}
)
], style={'margin-bottom': '20px'}),
# RSI Period Control
html.Div([
html.Label("RSI Period:", style={'font-weight': 'bold', 'margin-bottom': '5px'}),
dcc.Slider(
id='rsi-period-slider',
min=7, max=30, step=1, value=14,
marks={i: str(i) for i in [7, 14, 21, 30]},
tooltip={'placement': 'bottom', 'always_visible': True}
)
], style={'margin-bottom': '20px'}),
# MACD Parameters
html.Div([
html.Label("MACD Parameters:", style={'font-weight': 'bold', 'margin-bottom': '10px'}),
html.Div([
html.Div([
html.Label("Fast:", style={'font-size': '12px'}),
dcc.Input(
id='macd-fast-input',
type='number',
value=12,
min=5, max=50,
style={'width': '60px', 'margin-left': '5px'}
)
], style={'display': 'inline-block', 'margin-right': '15px'}),
html.Div([
html.Label("Slow:", style={'font-size': '12px'}),
dcc.Input(
id='macd-slow-input',
type='number',
value=26,
min=10, max=100,
style={'width': '60px', 'margin-left': '5px'}
)
], style={'display': 'inline-block', 'margin-right': '15px'}),
html.Div([
html.Label("Signal:", style={'font-size': '12px'}),
dcc.Input(
id='macd-signal-input',
type='number',
value=9,
min=3, max=20,
style={'width': '60px', 'margin-left': '5px'}
)
], style={'display': 'inline-block'})
])
], style={'margin-bottom': '20px'}),
# Bollinger Bands Parameters
html.Div([
html.Label("Bollinger Bands:", style={'font-weight': 'bold', 'margin-bottom': '10px'}),
html.Div([
html.Div([
html.Label("Period:", style={'font-size': '12px'}),
dcc.Input(
id='bb-period-input',
type='number',
value=20,
min=5, max=50,
style={'width': '60px', 'margin-left': '5px'}
)
], style={'display': 'inline-block', 'margin-right': '15px'}),
html.Div([
html.Label("Std Dev:", style={'font-size': '12px'}),
dcc.Input(
id='bb-stddev-input',
type='number',
value=2.0,
min=1.0, max=3.0, step=0.1,
style={'width': '70px', 'margin-left': '5px'}
)
], style={'display': 'inline-block'})
])
])
], style={
'border': '1px solid #bdc3c7',
'border-radius': '8px',
'padding': '15px',
'background-color': '#f8f9fa',
'margin-bottom': '20px'
})
# Auto-update control
auto_update_control = html.Div([
dcc.Checklist(
id='auto-update-checkbox',
options=[{'label': ' Auto-update charts', 'value': 'auto'}],
value=['auto'],
style={'margin-bottom': '10px'}
),
html.Div(id='update-status', style={'font-size': '12px', 'color': '#7f8c8d'})
])
return html.Div([
# Title and basic controls
html.H3("💹 Market Data Visualization", style={'color': '#2c3e50', 'margin-bottom': '20px'}),
# Main chart controls
html.Div([
html.Div([
html.Label("Symbol:", style={'font-weight': 'bold'}),
dcc.Dropdown(
id='symbol-dropdown',
options=symbol_options,
value=symbols[0] if symbols else 'BTC-USDT',
clearable=False,
style={'margin-bottom': '10px'}
)
], style={'width': '48%', 'display': 'inline-block'}),
html.Div([
html.Label("Timeframe:", style={'font-weight': 'bold'}),
dcc.Dropdown(
id='timeframe-dropdown',
options=timeframe_options,
value='1h',
clearable=False,
style={'margin-bottom': '10px'}
)
], style={'width': '48%', 'float': 'right', 'display': 'inline-block'})
], style={'margin-bottom': '20px'}),
# Chart Configuration Panel
chart_config_panel,
# Parameter Controls Section
parameter_controls,
# Auto-update control
auto_update_control,
# Chart
dcc.Graph(id='price-chart'),
# Market statistics
html.Div(id='market-stats', style={'margin-top': '20px'})
])
def get_bot_management_layout():
"""Create the bot management layout."""
return html.Div([
html.H2("🤖 Bot Management", style={'color': '#2c3e50'}),
html.P("Bot management interface will be implemented in Phase 4.0"),
# Placeholder for bot list
html.Div([
html.H3("Active Bots"),
html.Div(id='bot-list', children=[
html.P("No bots currently running", style={'color': '#7f8c8d'})
])
], style={'margin': '20px 0'})
])
def get_performance_layout():
"""Create the performance monitoring layout."""
return html.Div([
html.H2("📈 Performance Analytics", style={'color': '#2c3e50'}),
html.P("Performance analytics will be implemented in Phase 6.0"),
# Placeholder for performance metrics
html.Div([
html.H3("Portfolio Performance"),
html.P("Portfolio tracking coming soon", style={'color': '#7f8c8d'})
], style={'margin': '20px 0'})
])
def get_system_health_layout():
"""Create the system health monitoring layout."""
return html.Div([
html.H2("⚙️ System Health", style={'color': '#2c3e50'}),
# Database status
html.Div([
html.H3("Database Status"),
html.Div(id='database-status')
], style={'margin': '20px 0'}),
# Data collection status
html.Div([
html.H3("Data Collection Status"),
html.Div(id='collection-status')
], style={'margin': '20px 0'}),
# Redis status
html.Div([
html.H3("Redis Status"),
html.Div(id='redis-status')
], style={'margin': '20px 0'})
])
# Tab switching callback
@app.callback(
Output('tab-content', 'children'),
Input('main-tabs', 'value')
)
def render_tab_content(active_tab):
"""Render content based on selected tab."""
if active_tab == 'market-data':
return get_market_data_layout()
elif active_tab == 'bot-management':
return get_bot_management_layout()
elif active_tab == 'performance':
return get_performance_layout()
elif active_tab == 'system-health':
return get_system_health_layout()
else:
return html.Div("Tab not found")
# Market data chart callback
@app.callback(
Output('price-chart', 'figure'),
[Input('symbol-dropdown', 'value'),
Input('timeframe-dropdown', 'value'),
Input('overlay-indicators-checklist', 'value'),
Input('subplot-indicators-checklist', 'value'),
Input('strategy-dropdown', 'value'),
Input('interval-component', 'n_intervals')]
)
def update_price_chart(symbol, timeframe, overlay_indicators, subplot_indicators, selected_strategy, n_intervals):
"""Update the price chart with latest market data and selected indicators."""
try:
# If a strategy is selected, use strategy chart
if selected_strategy and selected_strategy != 'basic':
fig = create_strategy_chart(symbol, timeframe, selected_strategy)
logger.debug(f"Created strategy chart for {symbol} ({timeframe}) with strategy: {selected_strategy}")
else:
# Create chart with dynamically selected indicators
fig = create_chart_with_indicators(
symbol=symbol,
timeframe=timeframe,
overlay_indicators=overlay_indicators or [],
subplot_indicators=subplot_indicators or [],
days_back=7
)
indicator_count = len(overlay_indicators or []) + len(subplot_indicators or [])
logger.debug(f"Created dynamic chart for {symbol} ({timeframe}) with {indicator_count} indicators")
return fig
except Exception as e:
logger.error(f"Error updating price chart: {e}")
return create_error_chart(f"Error loading chart: {str(e)}")
# Strategy selection callback - automatically load strategy indicators
@app.callback(
[Output('overlay-indicators-checklist', 'value'),
Output('subplot-indicators-checklist', 'value')],
[Input('strategy-dropdown', 'value')]
)
def update_indicators_from_strategy(selected_strategy):
"""Update indicator selections when a strategy is chosen."""
if not selected_strategy or selected_strategy == 'basic':
return [], []
try:
# Get strategy configuration
all_strategies = get_all_example_strategies()
if selected_strategy in all_strategies:
strategy_example = all_strategies[selected_strategy]
config = strategy_example.config
# Extract overlay and subplot indicators from strategy
overlay_indicators = config.overlay_indicators or []
# Extract subplot indicators from subplot configs
subplot_indicators = []
for subplot_config in config.subplot_configs or []:
subplot_indicators.extend(subplot_config.indicators or [])
logger.debug(f"Loaded strategy {selected_strategy}: {len(overlay_indicators)} overlays, {len(subplot_indicators)} subplots")
return overlay_indicators, subplot_indicators
else:
logger.warning(f"Strategy {selected_strategy} not found")
return [], []
except Exception as e:
logger.error(f"Error loading strategy indicators: {e}")
return [], []
# Market statistics callback
@app.callback(
Output('market-stats', 'children'),
[Input('symbol-dropdown', 'value'),
Input('interval-component', 'n_intervals')]
)
def update_market_stats(symbol, n_intervals):
"""Update market statistics."""
try:
# Get real market statistics from database
stats = get_market_statistics(symbol)
return html.Div([
html.H3("Market Statistics"),
html.Div([
html.Div([
html.Strong(f"{key}: "),
html.Span(value, style={'color': '#27ae60' if '+' in str(value) else '#e74c3c' if '-' in str(value) else '#2c3e50'})
], style={'margin': '5px 0'}) for key, value in stats.items()
])
])
except Exception as e:
logger.error(f"Error updating market stats: {e}")
return html.Div("Error loading market statistics")
# System health callbacks
@app.callback(
Output('database-status', 'children'),
Input('interval-component', 'n_intervals')
)
def update_database_status(n_intervals):
"""Update database connection status."""
try:
db_manager = DatabaseManager()
# Test database connection
with db_manager.get_session() as session:
# Simple query to test connection
result = session.execute("SELECT 1").fetchone()
if result:
return html.Div([
html.Span("🟢 Connected", style={'color': '#27ae60', 'font-weight': 'bold'}),
html.P(f"Last checked: {datetime.now().strftime('%H:%M:%S')}",
style={'margin': '5px 0', 'color': '#7f8c8d'})
])
else:
return html.Div([
html.Span("🔴 Connection Error", style={'color': '#e74c3c', 'font-weight': 'bold'})
])
except Exception as e:
logger.error(f"Database status check failed: {e}")
return html.Div([
html.Span("🔴 Connection Failed", style={'color': '#e74c3c', 'font-weight': 'bold'}),
html.P(f"Error: {str(e)}", style={'color': '#7f8c8d', 'font-size': '12px'})
])
@app.callback(
Output('data-status', 'children'),
[Input('symbol-dropdown', 'value'),
Input('timeframe-dropdown', 'value'),
Input('interval-component', 'n_intervals')]
)
def update_data_status(symbol, timeframe, n_intervals):
"""Update data collection status."""
try:
# Check real data availability
status = check_data_availability(symbol, timeframe)
return html.Div([
html.H3("Data Collection Status"),
html.Div([
html.Div(
create_data_status_indicator(symbol, timeframe),
style={'margin': '10px 0'}
),
html.P(f"Checking data for {symbol} {timeframe}",
style={'color': '#7f8c8d', 'margin': '5px 0', 'font-style': 'italic'})
], style={'background-color': '#f8f9fa', 'padding': '15px', 'border-radius': '5px'})
])
except Exception as e:
logger.error(f"Error updating data status: {e}")
return html.Div([
html.H3("Data Collection Status"),
html.Div([
html.Span("🔴 Status Check Failed", style={'color': '#e74c3c', 'font-weight': 'bold'}),
html.P(f"Error: {str(e)}", style={'color': '#7f8c8d', 'margin': '5px 0'})
])
])
# Modal control callbacks
@app.callback(
[Output('indicator-modal', 'style'),
Output('indicator-modal-background', 'style')],
[Input('add-indicator-btn', 'n_clicks'),
Input('close-modal-btn', 'n_clicks'),
Input('cancel-indicator-btn', 'n_clicks'),
Input('edit-indicator-store', 'data')]
)
def toggle_indicator_modal(add_clicks, close_clicks, cancel_clicks, edit_data):
"""Toggle the visibility of the add indicator modal."""
# Default hidden styles
hidden_modal_style = {
'display': 'none',
'position': 'fixed',
'z-index': '1001',
'left': '0',
'top': '0',
'width': '100%',
'height': '100%',
'visibility': 'hidden'
}
hidden_background_style = {
'display': 'none',
'position': 'fixed',
'z-index': '1000',
'left': '0',
'top': '0',
'width': '100%',
'height': '100%',
'background-color': 'rgba(0,0,0,0.5)',
'visibility': 'hidden'
}
# Visible styles
visible_modal_style = {
'display': 'block',
'position': 'fixed',
'z-index': '1001',
'left': '0',
'top': '0',
'width': '100%',
'height': '100%',
'visibility': 'visible'
}
visible_background_style = {
'display': 'block',
'position': 'fixed',
'z-index': '1000',
'left': '0',
'top': '0',
'width': '100%',
'height': '100%',
'background-color': 'rgba(0,0,0,0.5)',
'visibility': 'visible'
}
ctx = dash.callback_context
# If no trigger or initial load, return hidden
if not ctx.triggered:
return [hidden_modal_style, hidden_background_style]
triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
# Only open modal if explicitly requested
should_open = False
# Check if add button was clicked (and has a click count > 0)
if triggered_id == 'add-indicator-btn' and add_clicks and add_clicks > 0:
should_open = True
# Check if edit button triggered and should open modal
elif triggered_id == 'edit-indicator-store' and edit_data and edit_data.get('open_modal') and edit_data.get('mode') == 'edit':
should_open = True
# Check if close/cancel buttons were clicked
elif triggered_id in ['close-modal-btn', 'cancel-indicator-btn']:
should_open = False
# Default: don't open
else:
should_open = False
if should_open:
return [visible_modal_style, visible_background_style]
else:
return [hidden_modal_style, hidden_background_style]
# Sync visible button clicks to hidden button
@app.callback(
Output('add-indicator-btn', 'n_clicks'),
Input('add-indicator-btn-visible', 'n_clicks'),
prevent_initial_call=True
)
def sync_add_button_clicks(visible_clicks):
"""Sync clicks from visible button to hidden button."""
return visible_clicks or 0
# Update parameter fields based on indicator type
@app.callback(
[Output('indicator-parameters-message', 'style'),
Output('sma-parameters', 'style'),
Output('ema-parameters', 'style'),
Output('rsi-parameters', 'style'),
Output('macd-parameters', 'style'),
Output('bb-parameters', 'style')],
Input('indicator-type-dropdown', 'value'),
prevent_initial_call=True
)
def update_parameter_fields(indicator_type):
"""Show/hide parameter input fields based on selected indicator type."""
# Default styles
hidden_style = {'display': 'none', 'margin-bottom': '10px'}
visible_style = {'display': 'block', 'margin-bottom': '10px'}
# Default message visibility
message_style = {'display': 'block'} if not indicator_type else {'display': 'none'}
# Initialize all as hidden
sma_style = hidden_style
ema_style = hidden_style
rsi_style = hidden_style
macd_style = hidden_style
bb_style = hidden_style
# Show the relevant parameter section
if indicator_type == 'sma':
sma_style = visible_style
elif indicator_type == 'ema':
ema_style = visible_style
elif indicator_type == 'rsi':
rsi_style = visible_style
elif indicator_type == 'macd':
macd_style = visible_style
elif indicator_type == 'bollinger_bands':
bb_style = visible_style
return message_style, sma_style, ema_style, rsi_style, macd_style, bb_style
# Save indicator callback
@app.callback(
[Output('save-indicator-feedback', 'children'),
Output('overlay-indicators-checklist', 'options'),
Output('subplot-indicators-checklist', 'options')],
Input('save-indicator-btn', 'n_clicks'),
[State('indicator-name-input', 'value'),
State('indicator-type-dropdown', 'value'),
State('indicator-description-input', 'value'),
State('indicator-color-input', 'value'),
State('indicator-line-width-slider', 'value'),
# SMA parameters
State('sma-period-input', 'value'),
# EMA parameters
State('ema-period-input', 'value'),
# RSI parameters
State('rsi-period-input', 'value'),
# MACD parameters
State('macd-fast-period-input', 'value'),
State('macd-slow-period-input', 'value'),
State('macd-signal-period-input', 'value'),
# Bollinger Bands parameters
State('bb-period-input', 'value'),
State('bb-stddev-input', 'value'),
# Edit mode data
State('edit-indicator-store', 'data')],
prevent_initial_call=True
)
def save_new_indicator(n_clicks, name, indicator_type, description, color, line_width,
sma_period, ema_period, rsi_period,
macd_fast, macd_slow, macd_signal,
bb_period, bb_stddev, edit_data):
"""Save a new indicator or update an existing one."""
if not n_clicks or not name or not indicator_type:
return "", dash.no_update, dash.no_update
try:
# Get indicator manager
from components.charts.indicator_manager import get_indicator_manager
manager = get_indicator_manager()
# Collect parameters based on indicator type and actual input values
parameters = {}
if indicator_type == 'sma':
parameters = {'period': sma_period or 20}
elif indicator_type == 'ema':
parameters = {'period': ema_period or 12}
elif indicator_type == 'rsi':
parameters = {'period': rsi_period or 14}
elif indicator_type == 'macd':
parameters = {
'fast_period': macd_fast or 12,
'slow_period': macd_slow or 26,
'signal_period': macd_signal or 9
}
elif indicator_type == 'bollinger_bands':
parameters = {
'period': bb_period or 20,
'std_dev': bb_stddev or 2.0
}
# Check if this is an edit operation
is_edit = edit_data and edit_data.get('mode') == 'edit'
if is_edit:
# Update existing indicator
indicator_id = edit_data.get('indicator_id')
success = manager.update_indicator(
indicator_id,
name=name,
description=description or "",
parameters=parameters,
styling={'color': color or "#007bff", 'line_width': line_width or 2}
)
if success:
success_msg = html.Div([
html.Span("", style={'color': '#28a745'}),
html.Span(f"Indicator '{name}' updated successfully!", style={'color': '#28a745'})
])
else:
error_msg = html.Div([
html.Span("", style={'color': '#dc3545'}),
html.Span("Failed to update indicator. Please try again.", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
else:
# Create new indicator
new_indicator = manager.create_indicator(
name=name,
indicator_type=indicator_type,
parameters=parameters,
description=description or "",
color=color or "#007bff"
)
if not new_indicator:
error_msg = html.Div([
html.Span("", style={'color': '#dc3545'}),
html.Span("Failed to save indicator. Please try again.", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
success_msg = html.Div([
html.Span("", style={'color': '#28a745'}),
html.Span(f"Indicator '{name}' saved successfully!", style={'color': '#28a745'})
])
# Refresh the indicator options
overlay_indicators = manager.get_indicators_by_type('overlay')
subplot_indicators = manager.get_indicators_by_type('subplot')
overlay_options = []
for indicator in overlay_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
overlay_options.append({'label': display_name, 'value': indicator.id})
subplot_options = []
for indicator in subplot_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
subplot_options.append({'label': display_name, 'value': indicator.id})
return success_msg, overlay_options, subplot_options
except Exception as e:
logger.error(f"Error saving indicator: {e}")
error_msg = html.Div([
html.Span("", style={'color': '#dc3545'}),
html.Span(f"Error: {str(e)}", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
# Update custom indicator lists with edit/delete buttons
@app.callback(
[Output('overlay-indicators-list', 'children'),
Output('subplot-indicators-list', 'children')],
[Input('overlay-indicators-checklist', 'options'),
Input('subplot-indicators-checklist', 'options'),
Input('overlay-indicators-checklist', 'value'),
Input('subplot-indicators-checklist', 'value')]
)
def update_custom_indicator_lists(overlay_options, subplot_options, overlay_values, subplot_values):
"""Create custom indicator lists with edit and delete buttons."""
def create_indicator_item(option, is_checked):
"""Create a single indicator item with checkbox and buttons."""
indicator_id = option['value']
indicator_name = option['label']
return html.Div([
# Checkbox and name
html.Div([
dcc.Checklist(
options=[{'label': '', 'value': indicator_id}],
value=[indicator_id] if is_checked else [],
id={'type': 'indicator-checkbox', 'index': indicator_id},
style={'display': 'inline-block', 'margin-right': '8px'}
),
html.Span(indicator_name, style={'display': 'inline-block', 'vertical-align': 'top'})
], style={'display': 'inline-block', 'width': '70%'}),
# Edit and Delete buttons
html.Div([
html.Button(
"✏️",
id={'type': 'edit-indicator-btn', 'index': indicator_id},
title="Edit indicator",
style={
'background': 'none',
'border': 'none',
'cursor': 'pointer',
'margin-left': '5px',
'font-size': '14px',
'color': '#007bff'
}
),
html.Button(
"🗑️",
id={'type': 'delete-indicator-btn', 'index': indicator_id},
title="Delete indicator",
style={
'background': 'none',
'border': 'none',
'cursor': 'pointer',
'margin-left': '5px',
'font-size': '14px',
'color': '#dc3545'
}
)
], style={'display': 'inline-block', 'width': '30%', 'text-align': 'right'})
], style={
'display': 'block',
'padding': '5px 0',
'border-bottom': '1px solid #f0f0f0',
'margin-bottom': '5px'
})
# Create overlay indicators list
overlay_list = []
for option in overlay_options:
is_checked = option['value'] in (overlay_values or [])
overlay_list.append(create_indicator_item(option, is_checked))
# Create subplot indicators list
subplot_list = []
for option in subplot_options:
is_checked = option['value'] in (subplot_values or [])
subplot_list.append(create_indicator_item(option, is_checked))
return overlay_list, subplot_list
# Sync individual indicator checkboxes with main checklist
@app.callback(
Output('overlay-indicators-checklist', 'value', allow_duplicate=True),
[Input({'type': 'indicator-checkbox', 'index': dash.ALL}, 'value')],
[State('overlay-indicators-checklist', 'options')],
prevent_initial_call=True
)
def sync_overlay_indicators(checkbox_values, overlay_options):
"""Sync individual indicator checkboxes with main overlay checklist."""
if not checkbox_values or not overlay_options:
return []
selected_indicators = []
overlay_ids = [opt['value'] for opt in overlay_options]
# Flatten the checkbox values and filter for overlay indicators
for values in checkbox_values:
if values: # values is a list, check if not empty
for indicator_id in values:
if indicator_id in overlay_ids:
selected_indicators.append(indicator_id)
# Remove duplicates
return list(set(selected_indicators))
@app.callback(
Output('subplot-indicators-checklist', 'value', allow_duplicate=True),
[Input({'type': 'indicator-checkbox', 'index': dash.ALL}, 'value')],
[State('subplot-indicators-checklist', 'options')],
prevent_initial_call=True
)
def sync_subplot_indicators(checkbox_values, subplot_options):
"""Sync individual indicator checkboxes with main subplot checklist."""
if not checkbox_values or not subplot_options:
return []
selected_indicators = []
subplot_ids = [opt['value'] for opt in subplot_options]
# Flatten the checkbox values and filter for subplot indicators
for values in checkbox_values:
if values: # values is a list, check if not empty
for indicator_id in values:
if indicator_id in subplot_ids:
selected_indicators.append(indicator_id)
# Remove duplicates
return list(set(selected_indicators))
# Handle delete indicator
@app.callback(
[Output('save-indicator-feedback', 'children', allow_duplicate=True),
Output('overlay-indicators-checklist', 'options', allow_duplicate=True),
Output('subplot-indicators-checklist', 'options', allow_duplicate=True)],
[Input({'type': 'delete-indicator-btn', 'index': dash.ALL}, 'n_clicks')],
[State({'type': 'delete-indicator-btn', 'index': dash.ALL}, 'id')],
prevent_initial_call=True
)
def delete_indicator(delete_clicks, button_ids):
"""Delete an indicator when delete button is clicked."""
ctx = dash.callback_context
if not ctx.triggered or not any(delete_clicks):
return dash.no_update, dash.no_update, dash.no_update
# Find which button was clicked
triggered_id = ctx.triggered[0]['prop_id']
import json
button_info = json.loads(triggered_id.split('.')[0])
indicator_id = button_info['index']
try:
# Get indicator manager and delete the indicator
from components.charts.indicator_manager import get_indicator_manager
manager = get_indicator_manager()
# Load indicator to get its name before deletion
indicator = manager.load_indicator(indicator_id)
indicator_name = indicator.name if indicator else indicator_id
if manager.delete_indicator(indicator_id):
# Refresh the indicator options
overlay_indicators = manager.get_indicators_by_type('overlay')
subplot_indicators = manager.get_indicators_by_type('subplot')
overlay_options = []
for indicator in overlay_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
overlay_options.append({'label': display_name, 'value': indicator.id})
subplot_options = []
for indicator in subplot_indicators:
display_name = f"{indicator.name} ({indicator.type.upper()})"
subplot_options.append({'label': display_name, 'value': indicator.id})
success_msg = html.Div([
html.Span("🗑️ ", style={'color': '#dc3545'}),
html.Span(f"Indicator '{indicator_name}' deleted successfully!", style={'color': '#dc3545'})
])
return success_msg, overlay_options, subplot_options
else:
error_msg = html.Div([
html.Span("", style={'color': '#dc3545'}),
html.Span("Failed to delete indicator.", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
except Exception as e:
logger.error(f"Error deleting indicator: {e}")
error_msg = html.Div([
html.Span("", style={'color': '#dc3545'}),
html.Span(f"Error: {str(e)}", style={'color': '#dc3545'})
])
return error_msg, dash.no_update, dash.no_update
# Handle edit indicator - open modal with existing data
@app.callback(
[Output('modal-title', 'children'),
Output('indicator-name-input', 'value'),
Output('indicator-type-dropdown', 'value'),
Output('indicator-description-input', 'value'),
Output('indicator-color-input', 'value'),
Output('edit-indicator-store', 'data'),
# Add parameter field outputs
Output('sma-period-input', 'value'),
Output('ema-period-input', 'value'),
Output('rsi-period-input', 'value'),
Output('macd-fast-period-input', 'value'),
Output('macd-slow-period-input', 'value'),
Output('macd-signal-period-input', 'value'),
Output('bb-period-input', 'value'),
Output('bb-stddev-input', 'value')],
[Input({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'n_clicks')],
[State({'type': 'edit-indicator-btn', 'index': dash.ALL}, 'id')],
prevent_initial_call=True
)
def edit_indicator(edit_clicks, button_ids):
"""Load indicator data for editing."""
ctx = dash.callback_context
if not ctx.triggered or not any(edit_clicks):
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
# Find which button was clicked
triggered_id = ctx.triggered[0]['prop_id']
import json
button_info = json.loads(triggered_id.split('.')[0])
indicator_id = button_info['index']
try:
# Load the indicator data
from components.charts.indicator_manager import get_indicator_manager
manager = get_indicator_manager()
indicator = manager.load_indicator(indicator_id)
if indicator:
# Store indicator ID for update
edit_data = {'indicator_id': indicator_id, 'mode': 'edit', 'open_modal': True}
# 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
# Update with actual saved values
if indicator.type == 'sma':
sma_period = params.get('period', 20)
elif indicator.type == 'ema':
ema_period = params.get('period', 12)
elif indicator.type == 'rsi':
rsi_period = params.get('period', 14)
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)
elif indicator.type == 'bollinger_bands':
bb_period = params.get('period', 20)
bb_stddev = params.get('std_dev', 2.0)
return (
"✏️ Edit Indicator",
indicator.name,
indicator.type,
indicator.description,
indicator.styling.color,
edit_data,
sma_period,
ema_period,
rsi_period,
macd_fast,
macd_slow,
macd_signal,
bb_period,
bb_stddev
)
else:
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
except Exception as e:
logger.error(f"Error loading indicator for edit: {e}")
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
# Reset modal form when closed
@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-color-input', 'value', allow_duplicate=True),
Output('indicator-line-width-slider', 'value'),
Output('modal-title', 'children', allow_duplicate=True),
Output('edit-indicator-store', 'data', allow_duplicate=True),
# Add parameter field resets
Output('sma-period-input', 'value', allow_duplicate=True),
Output('ema-period-input', 'value', allow_duplicate=True),
Output('rsi-period-input', 'value', allow_duplicate=True),
Output('macd-fast-period-input', 'value', allow_duplicate=True),
Output('macd-slow-period-input', 'value', allow_duplicate=True),
Output('macd-signal-period-input', 'value', allow_duplicate=True),
Output('bb-period-input', 'value', allow_duplicate=True),
Output('bb-stddev-input', 'value', allow_duplicate=True)],
[Input('close-modal-btn', 'n_clicks'),
Input('cancel-indicator-btn', 'n_clicks')],
prevent_initial_call=True
)
def reset_modal_form(close_clicks, cancel_clicks):
"""Reset the modal form when it's closed."""
if close_clicks or cancel_clicks:
return "", None, "", "#007bff", 2, "📊 Add New Indicator", None, 20, 12, 14, 12, 26, 9, 20, 2.0
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
def main():
"""Main function to run the dashboard."""
try:
logger.info("Starting Crypto Trading Bot Dashboard")
logger.info(f"Dashboard will be available at: http://{dashboard_settings.host}:{dashboard_settings.port}")
# Run the app
app.run(
host=dashboard_settings.host,
port=dashboard_settings.port,
debug=dashboard_settings.debug
)
except Exception as e:
logger.error(f"Failed to start dashboard: {e}")
sys.exit(1)
if __name__ == "__main__":
main()