- 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.
1523 lines
63 KiB
Python
1523 lines
63 KiB
Python
#!/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() |