added clean dashboard - reimplementation as other is 10k lines

This commit is contained in:
Dobromir Popov
2025-06-25 01:51:23 +03:00
parent 60c462802d
commit 939b223f1b
4 changed files with 1342 additions and 0 deletions

72
run_clean_dashboard.py Normal file
View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
Run Clean Trading Dashboard
Simple runner for the modular dashboard implementation
"""
import logging
import sys
import os
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def main():
"""Main function to run the clean dashboard"""
try:
logger.info("Starting Clean Trading Dashboard...")
# Import core components
from core.data_provider import DataProvider
from core.orchestrator import TradingOrchestrator
from core.trading_executor import TradingExecutor
# Import clean dashboard
from web.clean_dashboard import create_clean_dashboard
# Initialize components
logger.info("Initializing trading components...")
data_provider = DataProvider()
# Try to use enhanced orchestrator if available
try:
from core.enhanced_orchestrator import EnhancedTradingOrchestrator
orchestrator = EnhancedTradingOrchestrator(
data_provider=data_provider,
symbols=['ETH/USDT', 'BTC/USDT']
)
logger.info("Using Enhanced Trading Orchestrator")
except ImportError:
orchestrator = TradingOrchestrator(data_provider=data_provider)
logger.info("Using Standard Trading Orchestrator")
trading_executor = TradingExecutor()
# Create and run dashboard
logger.info("Creating clean dashboard...")
dashboard = create_clean_dashboard(
data_provider=data_provider,
orchestrator=orchestrator,
trading_executor=trading_executor
)
logger.info("Dashboard created successfully!")
logger.info("Starting server on http://127.0.0.1:8051")
# Run the dashboard
dashboard.run_server(host='127.0.0.1', port=8051, debug=False)
except KeyboardInterrupt:
logger.info("Dashboard stopped by user")
except Exception as e:
logger.error(f"Error running clean dashboard: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

797
web/clean_dashboard.py Normal file
View File

@ -0,0 +1,797 @@
"""
Clean Trading Dashboard - Modular Implementation
Uses layout and component managers to reduce file size and improve maintainability
"""
import dash
from dash import Dash, dcc, html, Input, Output, State
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, timezone
import pytz
import logging
import json
import time
import threading
from typing import Dict, List, Optional, Any
import os
# Setup logger
logger = logging.getLogger(__name__)
# Import core components
from core.config import get_config
from core.data_provider import DataProvider
from core.orchestrator import TradingOrchestrator
from core.trading_executor import TradingExecutor
# Import layout and component managers
from web.layout_manager import DashboardLayoutManager
from web.component_manager import DashboardComponentManager
# Import optional components
try:
from core.enhanced_orchestrator import EnhancedTradingOrchestrator
ENHANCED_RL_AVAILABLE = True
except ImportError:
ENHANCED_RL_AVAILABLE = False
logger.warning("Enhanced RL components not available")
try:
from core.cob_integration import COBIntegration
from core.multi_exchange_cob_provider import COBSnapshot
COB_INTEGRATION_AVAILABLE = True
except ImportError:
COB_INTEGRATION_AVAILABLE = False
logger.warning("COB integration not available")
class CleanTradingDashboard:
"""Clean, modular trading dashboard implementation"""
def __init__(self, data_provider: DataProvider = None, orchestrator: TradingOrchestrator = None, trading_executor: TradingExecutor = None):
self.config = get_config()
# Initialize components
self.data_provider = data_provider or DataProvider()
self.orchestrator = orchestrator
self.trading_executor = trading_executor
# Initialize layout and component managers
self.layout_manager = DashboardLayoutManager(
starting_balance=self._get_initial_balance(),
trading_executor=self.trading_executor
)
self.component_manager = DashboardComponentManager()
# Dashboard state
self.recent_decisions = []
self.closed_trades = []
self.current_prices = {}
self.session_pnl = 0.0
self.total_fees = 0.0
self.current_position = None
# WebSocket streaming
self.ws_price_cache = {}
self.is_streaming = False
self.tick_cache = []
# COB data cache
self.cob_cache = {
'ETH/USDT': {'last_update': 0, 'data': None, 'updates_count': 0},
'BTC/USDT': {'last_update': 0, 'data': None, 'updates_count': 0}
}
# Initialize timezone
timezone_name = self.config.get('system', {}).get('timezone', 'Europe/Sofia')
self.timezone = pytz.timezone(timezone_name)
# Create Dash app
self.app = Dash(__name__, external_stylesheets=[
'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'
])
# Setup layout and callbacks
self._setup_layout()
self._setup_callbacks()
# Start data streams
self._initialize_streaming()
logger.info("Clean Trading Dashboard initialized")
def _get_initial_balance(self) -> float:
"""Get initial balance from trading executor or default"""
try:
if self.trading_executor and hasattr(self.trading_executor, 'get_balance'):
balance = self.trading_executor.get_balance()
if balance and balance > 0:
return balance
except Exception as e:
logger.warning(f"Error getting balance: {e}")
return 100.0 # Default balance
def _setup_layout(self):
"""Setup the dashboard layout using layout manager"""
self.app.layout = self.layout_manager.create_main_layout()
def _setup_callbacks(self):
"""Setup dashboard callbacks"""
@self.app.callback(
[Output('current-price', 'children'),
Output('session-pnl', 'children'),
Output('current-position', 'children'),
Output('portfolio-value', 'children'),
Output('total-fees', 'children'),
Output('trade-count', 'children'),
Output('mexc-status', 'children')],
[Input('interval-component', 'n_intervals')]
)
def update_metrics(n):
"""Update key metrics"""
try:
# Get current price
current_price = self._get_current_price('ETH/USDT')
price_str = f"${current_price:.2f}" if current_price else "Loading..."
# Calculate session P&L
session_pnl_str = f"${self.session_pnl:.2f}"
session_pnl_class = "text-success" if self.session_pnl >= 0 else "text-danger"
# Current position
position_str = "No Position"
if self.current_position:
side = self.current_position.get('side', 'UNKNOWN')
size = self.current_position.get('size', 0)
entry_price = self.current_position.get('price', 0)
position_str = f"{side} {size:.3f} @ ${entry_price:.2f}"
# Portfolio value
initial_balance = self._get_initial_balance()
portfolio_value = initial_balance + self.session_pnl
portfolio_str = f"${portfolio_value:.2f}"
# Total fees
fees_str = f"${self.total_fees:.3f}"
# Trade count
trade_count = len(self.closed_trades)
trade_str = f"{trade_count} Trades"
# MEXC status
mexc_status = "SIM"
if self.trading_executor:
if hasattr(self.trading_executor, 'trading_enabled') and self.trading_executor.trading_enabled:
if hasattr(self.trading_executor, 'simulation_mode') and not self.trading_executor.simulation_mode:
mexc_status = "LIVE"
return price_str, session_pnl_str, position_str, portfolio_str, fees_str, trade_str, mexc_status
except Exception as e:
logger.error(f"Error updating metrics: {e}")
return "Error", "$0.00", "Error", "$100.00", "$0.00", "0", "ERROR"
@self.app.callback(
Output('recent-decisions', 'children'),
[Input('interval-component', 'n_intervals')]
)
def update_recent_decisions(n):
"""Update recent trading signals"""
try:
return self.component_manager.format_trading_signals(self.recent_decisions)
except Exception as e:
logger.error(f"Error updating decisions: {e}")
return [html.P(f"Error: {str(e)}", className="text-danger")]
@self.app.callback(
Output('price-chart', 'figure'),
[Input('interval-component', 'n_intervals')]
)
def update_price_chart(n):
"""Update price chart every second (1000ms interval)"""
try:
return self._create_price_chart('ETH/USDT')
except Exception as e:
logger.error(f"Error updating chart: {e}")
return go.Figure().add_annotation(text=f"Chart Error: {str(e)}",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False)
@self.app.callback(
Output('closed-trades-table', 'children'),
[Input('interval-component', 'n_intervals')]
)
def update_closed_trades(n):
"""Update closed trades table"""
try:
return self.component_manager.format_closed_trades_table(self.closed_trades)
except Exception as e:
logger.error(f"Error updating trades table: {e}")
return html.P(f"Error: {str(e)}", className="text-danger")
@self.app.callback(
[Output('cob-status-content', 'children'),
Output('eth-cob-content', 'children'),
Output('btc-cob-content', 'children')],
[Input('interval-component', 'n_intervals')]
)
def update_cob_data(n):
"""Update COB data displays"""
try:
# COB Status
cob_status = self._get_cob_status()
status_components = self.component_manager.format_system_status(cob_status)
# ETH/USDT COB
eth_cob = self._get_cob_snapshot('ETH/USDT')
eth_components = self.component_manager.format_cob_data(eth_cob, 'ETH/USDT')
# BTC/USDT COB
btc_cob = self._get_cob_snapshot('BTC/USDT')
btc_components = self.component_manager.format_cob_data(btc_cob, 'BTC/USDT')
return status_components, eth_components, btc_components
except Exception as e:
logger.error(f"Error updating COB data: {e}")
error_msg = html.P(f"Error: {str(e)}", className="text-danger")
return error_msg, error_msg, error_msg
@self.app.callback(
Output('training-metrics', 'children'),
[Input('interval-component', 'n_intervals')]
)
def update_training_metrics(n):
"""Update training metrics"""
try:
metrics_data = self._get_training_metrics()
return self.component_manager.format_training_metrics(metrics_data)
except Exception as e:
logger.error(f"Error updating training metrics: {e}")
return [html.P(f"Error: {str(e)}", className="text-danger")]
# Manual trading buttons
@self.app.callback(
Output('manual-buy-btn', 'children'),
[Input('manual-buy-btn', 'n_clicks')],
prevent_initial_call=True
)
def handle_manual_buy(n_clicks):
"""Handle manual buy button"""
if n_clicks:
self._execute_manual_trade('BUY')
return [html.I(className="fas fa-arrow-up me-1"), "BUY"]
@self.app.callback(
Output('manual-sell-btn', 'children'),
[Input('manual-sell-btn', 'n_clicks')],
prevent_initial_call=True
)
def handle_manual_sell(n_clicks):
"""Handle manual sell button"""
if n_clicks:
self._execute_manual_trade('SELL')
return [html.I(className="fas fa-arrow-down me-1"), "SELL"]
# Clear session button
@self.app.callback(
Output('clear-session-btn', 'children'),
[Input('clear-session-btn', 'n_clicks')],
prevent_initial_call=True
)
def handle_clear_session(n_clicks):
"""Handle clear session button"""
if n_clicks:
self._clear_session()
return [html.I(className="fas fa-trash me-1"), "Clear Session"]
def _get_current_price(self, symbol: str) -> Optional[float]:
"""Get current price for symbol"""
try:
# Try WebSocket cache first
ws_symbol = symbol.replace('/', '')
if ws_symbol in self.ws_price_cache:
return self.ws_price_cache[ws_symbol]
# Fallback to data provider
if symbol in self.current_prices:
return self.current_prices[symbol]
# Get fresh price from data provider
df = self.data_provider.get_historical_data(symbol, '1m', limit=1)
if df is not None and not df.empty:
price = float(df['close'].iloc[-1])
self.current_prices[symbol] = price
return price
except Exception as e:
logger.warning(f"Error getting current price for {symbol}: {e}")
return None
def _create_price_chart(self, symbol: str) -> go.Figure:
"""Create 1-minute main chart with 1-second mini chart - Updated every second"""
try:
# Get 1-minute data (main chart) - FIXED for real-time updates
# First try to create 1m bars from WebSocket 1s data
ws_data_raw = self._get_websocket_chart_data(symbol, 'raw')
if ws_data_raw is not None and len(ws_data_raw) > 60:
# Resample 1s data to 1m bars for real-time updating 1m chart
df_main = ws_data_raw.resample('1min').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}).dropna().tail(180) # Last 3 hours
main_source = "WebSocket 1m (Real-time)"
else:
# Fallback to historical 1-minute data (3 hours)
df_main = self.data_provider.get_historical_data(symbol, '1m', limit=180)
main_source = "Historical 1m"
# Get 1-second data (mini chart)
ws_data_1s = self._get_websocket_chart_data(symbol, '1s')
if df_main is None or df_main.empty:
return go.Figure().add_annotation(text="No data available",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False)
# Create chart with 3 subplots: Main 1m chart, Mini 1s chart, Volume
if ws_data_1s is not None and len(ws_data_1s) > 5:
fig = make_subplots(
rows=3, cols=1,
shared_xaxes=True,
vertical_spacing=0.05,
subplot_titles=(
f'{symbol} - {main_source} ({len(df_main)} bars)',
f'1s Mini Chart ({len(ws_data_1s)} bars)',
'Volume'
),
row_heights=[0.5, 0.25, 0.25]
)
has_mini_chart = True
else:
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.08,
subplot_titles=(f'{symbol} - {main_source} ({len(df_main)} bars)', 'Volume'),
row_heights=[0.7, 0.3]
)
has_mini_chart = False
# Main 1-minute candlestick chart
fig.add_trace(
go.Candlestick(
x=df_main.index,
open=df_main['open'],
high=df_main['high'],
low=df_main['low'],
close=df_main['close'],
name=f'{symbol} 1m',
increasing_line_color='#26a69a',
decreasing_line_color='#ef5350',
increasing_fillcolor='#26a69a',
decreasing_fillcolor='#ef5350'
),
row=1, col=1
)
# Mini 1-second chart (if available)
if has_mini_chart:
fig.add_trace(
go.Scatter(
x=ws_data_1s.index,
y=ws_data_1s['close'],
mode='lines',
name='1s Price',
line=dict(color='#ffa726', width=1),
showlegend=False
),
row=2, col=1
)
# Volume bars (bottom subplot)
volume_row = 3 if has_mini_chart else 2
fig.add_trace(
go.Bar(
x=df_main.index,
y=df_main['volume'],
name='Volume',
marker_color='rgba(100,150,200,0.6)',
showlegend=False
),
row=volume_row, col=1
)
# Update layout
chart_height = 500 if has_mini_chart else 400
fig.update_layout(
title=f'{symbol} Live Chart - {main_source} (Updated Every Second)',
template='plotly_dark',
showlegend=False,
height=chart_height,
margin=dict(l=50, r=50, t=60, b=50),
xaxis_rangeslider_visible=False
)
# Update axes
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)')
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)')
chart_info = f"1m bars: {len(df_main)}"
if has_mini_chart:
chart_info += f", 1s ticks: {len(ws_data_1s)}"
logger.debug(f"[CHART] Created combined chart - {chart_info}")
return fig
except Exception as e:
logger.error(f"Error creating chart for {symbol}: {e}")
return go.Figure().add_annotation(text=f"Chart Error: {str(e)}",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False)
def _get_websocket_chart_data(self, symbol: str, timeframe: str = '1m') -> Optional[pd.DataFrame]:
"""Get WebSocket chart data - supports both 1m and 1s timeframes"""
try:
if not hasattr(self, 'tick_cache') or not self.tick_cache:
return None
# Filter ticks for symbol
symbol_ticks = [tick for tick in self.tick_cache if tick.get('symbol') == symbol.replace('/', '')]
if len(symbol_ticks) < 10:
return None
# Convert to DataFrame
df = pd.DataFrame(symbol_ticks)
df['datetime'] = pd.to_datetime(df['datetime'])
df.set_index('datetime', inplace=True)
# Get the price column (could be 'price', 'close', or 'c')
price_col = None
for col in ['price', 'close', 'c']:
if col in df.columns:
price_col = col
break
if price_col is None:
logger.warning(f"No price column found in WebSocket data for {symbol}")
return None
# Create OHLC bars based on requested timeframe
if timeframe == '1s':
df_resampled = df[price_col].resample('1s').ohlc()
# For 1s data, keep last 300 seconds (5 minutes)
max_bars = 300
elif timeframe == 'raw':
# Return raw 1s kline data for resampling to 1m in chart creation
df_resampled = df[['open', 'high', 'low', 'close', 'volume']].copy()
# Keep last 3+ hours of 1s data for 1m resampling
max_bars = 200 * 60 # 200 minutes worth of 1s data
else: # 1m
df_resampled = df[price_col].resample('1min').ohlc()
# For 1m data, keep last 180 minutes (3 hours)
max_bars = 180
if timeframe == '1s':
df_resampled.columns = ['open', 'high', 'low', 'close']
# Handle volume data
if timeframe == '1s':
# FIXED: Better volume calculation for 1s
if 'volume' in df.columns and df['volume'].sum() > 0:
df_resampled['volume'] = df['volume'].resample('1s').sum()
else:
# Use tick count as volume proxy with some randomization for variety
import random
tick_counts = df[price_col].resample('1s').count()
df_resampled['volume'] = tick_counts * (50 + random.randint(0, 100))
# For 1m timeframe, volume is already in the raw data
# Remove any NaN rows and limit to max bars
df_resampled = df_resampled.dropna().tail(max_bars)
if len(df_resampled) < 5:
logger.debug(f"Insufficient {timeframe} data for {symbol}: {len(df_resampled)} bars")
return None
logger.debug(f"[WS-CHART] Created {len(df_resampled)} {timeframe} OHLC bars for {symbol}")
return df_resampled
except Exception as e:
logger.warning(f"Error getting WebSocket chart data: {e}")
return None
def _get_cob_status(self) -> Dict:
"""Get COB integration status"""
try:
status = {
'trading_enabled': bool(self.trading_executor and getattr(self.trading_executor, 'trading_enabled', False)),
'simulation_mode': bool(self.trading_executor and getattr(self.trading_executor, 'simulation_mode', True)),
'data_provider_status': 'Active',
'websocket_status': 'Connected' if self.is_streaming else 'Disconnected',
'cob_status': 'Active' if COB_INTEGRATION_AVAILABLE else 'Inactive'
}
if self.orchestrator and hasattr(self.orchestrator, 'cob_integration'):
cob_integration = self.orchestrator.cob_integration
if cob_integration and hasattr(cob_integration, 'is_active'):
status['cob_status'] = 'Active' if cob_integration.is_active else 'Inactive'
return status
except Exception as e:
logger.error(f"Error getting COB status: {e}")
return {'error': str(e)}
def _get_cob_snapshot(self, symbol: str) -> Optional[Any]:
"""Get COB snapshot for symbol"""
try:
if not COB_INTEGRATION_AVAILABLE:
return None
if self.orchestrator and hasattr(self.orchestrator, 'cob_integration'):
cob_integration = self.orchestrator.cob_integration
if cob_integration and hasattr(cob_integration, 'get_latest_snapshot'):
return cob_integration.get_latest_snapshot(symbol)
return None
except Exception as e:
logger.warning(f"Error getting COB snapshot for {symbol}: {e}")
return None
def _get_training_metrics(self) -> Dict:
"""Get training metrics data"""
try:
metrics = {}
# CNN metrics
if hasattr(self, 'williams_structure') and self.williams_structure:
cnn_stats = getattr(self.williams_structure, 'get_training_stats', lambda: {})()
if cnn_stats:
metrics['cnn_metrics'] = cnn_stats
# RL metrics
if ENHANCED_RL_AVAILABLE and self.orchestrator:
if hasattr(self.orchestrator, 'get_rl_stats'):
rl_stats = self.orchestrator.get_rl_stats()
if rl_stats:
metrics['rl_metrics'] = rl_stats
return metrics
except Exception as e:
logger.error(f"Error getting training metrics: {e}")
return {'error': str(e)}
def _execute_manual_trade(self, action: str):
"""Execute manual trading action"""
try:
if not self.trading_executor:
logger.warning("No trading executor available")
return
symbol = 'ETH/USDT'
current_price = self._get_current_price(symbol)
if not current_price:
logger.warning("No current price available for manual trade")
return
# Create manual trading decision
decision = {
'timestamp': datetime.now().strftime('%H:%M:%S'),
'action': action,
'confidence': 100.0, # Manual trades have 100% confidence
'price': current_price,
'executed': False,
'blocked': False,
'manual': True
}
# Execute through trading executor
if hasattr(self.trading_executor, 'execute_trade'):
result = self.trading_executor.execute_trade(symbol, action, 0.01) # Small size for testing
if result:
decision['executed'] = True
logger.info(f"Manual {action} executed at ${current_price:.2f}")
else:
decision['blocked'] = True
decision['block_reason'] = "Execution failed"
# Add to recent decisions
self.recent_decisions.append(decision)
# Keep only last 20 decisions
if len(self.recent_decisions) > 20:
self.recent_decisions = self.recent_decisions[-20:]
except Exception as e:
logger.error(f"Error executing manual {action}: {e}")
def _clear_session(self):
"""Clear session data"""
try:
# Reset session metrics
self.session_pnl = 0.0
self.total_fees = 0.0
self.closed_trades = []
self.recent_decisions = []
logger.info("Session data cleared")
except Exception as e:
logger.error(f"Error clearing session: {e}")
def _initialize_streaming(self):
"""Initialize data streaming"""
try:
# Start WebSocket streaming
self._start_websocket_streaming()
# Start data collection thread
self._start_data_collection()
logger.info("Data streaming initialized")
except Exception as e:
logger.error(f"Error initializing streaming: {e}")
def _start_websocket_streaming(self):
"""Start WebSocket streaming for real-time data"""
try:
def ws_worker():
try:
import websocket
import json
def on_message(ws, message):
try:
data = json.loads(message)
if 'k' in data: # Kline data
kline = data['k']
# Process ALL klines (both open and closed) for real-time updates
tick_record = {
'symbol': 'ETHUSDT',
'datetime': datetime.fromtimestamp(int(kline['t']) / 1000),
'open': float(kline['o']),
'high': float(kline['h']),
'low': float(kline['l']),
'close': float(kline['c']),
'price': float(kline['c']), # For compatibility
'volume': float(kline['v']), # Real volume data!
'is_closed': kline['x'] # Track if kline is closed
}
# Update current price every second
current_price = float(kline['c'])
self.ws_price_cache['ETHUSDT'] = current_price
self.current_prices['ETH/USDT'] = current_price
# Add to tick cache (keep last 1000 klines for charts)
# For real-time updates, we need more data points
self.tick_cache.append(tick_record)
if len(self.tick_cache) > 1000:
self.tick_cache = self.tick_cache[-1000:]
status = "CLOSED" if kline['x'] else "LIVE"
logger.debug(f"[WS] {status} kline: {current_price:.2f}, Vol: {tick_record['volume']:.0f} (cache: {len(self.tick_cache)})")
except Exception as e:
logger.warning(f"WebSocket message error: {e}")
def on_error(ws, error):
logger.error(f"WebSocket error: {error}")
self.is_streaming = False
def on_close(ws, close_status_code, close_msg):
logger.warning("WebSocket connection closed")
self.is_streaming = False
def on_open(ws):
logger.info("WebSocket connected")
self.is_streaming = True
# Binance WebSocket - Use kline stream for OHLCV data
ws_url = "wss://stream.binance.com:9443/ws/ethusdt@kline_1s"
ws = websocket.WebSocketApp(
ws_url,
on_message=on_message,
on_error=on_error,
on_close=on_close,
on_open=on_open
)
ws.run_forever()
except Exception as e:
logger.error(f"WebSocket worker error: {e}")
self.is_streaming = False
# Start WebSocket thread
ws_thread = threading.Thread(target=ws_worker, daemon=True)
ws_thread.start()
except Exception as e:
logger.error(f"Error starting WebSocket: {e}")
def _start_data_collection(self):
"""Start background data collection"""
try:
def data_worker():
while True:
try:
# Update recent decisions from orchestrator
if self.orchestrator and hasattr(self.orchestrator, 'get_recent_decisions'):
decisions = self.orchestrator.get_recent_decisions('ETH/USDT')
if decisions:
self.recent_decisions = decisions[-20:] # Keep last 20
# Update closed trades
if self.trading_executor and hasattr(self.trading_executor, 'get_closed_trades'):
trades = self.trading_executor.get_closed_trades()
if trades:
self.closed_trades = trades
# Update session metrics
self._update_session_metrics()
time.sleep(5) # Update every 5 seconds
except Exception as e:
logger.warning(f"Data collection error: {e}")
time.sleep(10) # Wait longer on error
# Start data collection thread
data_thread = threading.Thread(target=data_worker, daemon=True)
data_thread.start()
except Exception as e:
logger.error(f"Error starting data collection: {e}")
def _update_session_metrics(self):
"""Update session P&L and metrics"""
try:
# Calculate session P&L from closed trades
if self.closed_trades:
self.session_pnl = sum(trade.get('pnl', 0) for trade in self.closed_trades)
self.total_fees = sum(trade.get('fees', 0) for trade in self.closed_trades)
# Update current position
if self.trading_executor and hasattr(self.trading_executor, 'get_current_position'):
position = self.trading_executor.get_current_position()
self.current_position = position
except Exception as e:
logger.warning(f"Error updating session metrics: {e}")
def run_server(self, host='127.0.0.1', port=8051, debug=False):
"""Run the dashboard server"""
logger.info(f"Starting Clean Trading Dashboard at http://{host}:{port}")
self.app.run(host=host, port=port, debug=debug)
def stop(self):
"""Stop the dashboard and cleanup resources"""
try:
self.is_streaming = False
logger.info("Clean Trading Dashboard stopped")
except Exception as e:
logger.error(f"Error stopping dashboard: {e}")
# Factory function for easy creation
def create_clean_dashboard(data_provider=None, orchestrator=None, trading_executor=None):
"""Create a clean trading dashboard instance"""
return CleanTradingDashboard(
data_provider=data_provider,
orchestrator=orchestrator,
trading_executor=trading_executor
)

252
web/component_manager.py Normal file
View File

@ -0,0 +1,252 @@
"""
Dashboard Component Manager - Clean Trading Dashboard
Manages the formatting and creation of dashboard components
"""
from dash import html
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class DashboardComponentManager:
"""Manages dashboard component formatting and creation"""
def __init__(self):
pass
def format_trading_signals(self, recent_decisions):
"""Format trading signals for display"""
try:
if not recent_decisions:
return [html.P("No recent signals", className="text-muted small")]
signals = []
for decision in recent_decisions[-10:]: # Last 10 signals
timestamp = decision.get('timestamp', 'Unknown')
action = decision.get('action', 'UNKNOWN')
confidence = decision.get('confidence', 0)
price = decision.get('price', 0)
executed = decision.get('executed', False)
blocked = decision.get('blocked', False)
manual = decision.get('manual', False)
# Determine signal style
if executed:
badge_class = "bg-success"
status = ""
elif blocked:
badge_class = "bg-danger"
status = ""
else:
badge_class = "bg-warning"
status = ""
action_color = "text-success" if action == "BUY" else "text-danger"
manual_indicator = " [M]" if manual else ""
signal_div = html.Div([
html.Span(f"{timestamp}", className="small text-muted me-2"),
html.Span(f"{status}", className=f"badge {badge_class} me-2"),
html.Span(f"{action}{manual_indicator}", className=f"{action_color} fw-bold me-2"),
html.Span(f"({confidence:.1f}%)", className="small text-muted me-2"),
html.Span(f"${price:.2f}", className="small")
], className="mb-1")
signals.append(signal_div)
return signals
except Exception as e:
logger.error(f"Error formatting trading signals: {e}")
return [html.P(f"Error: {str(e)}", className="text-danger small")]
def format_closed_trades_table(self, closed_trades):
"""Format closed trades table for display"""
try:
if not closed_trades:
return html.P("No closed trades", className="text-muted small")
# Create table headers
headers = html.Thead([
html.Tr([
html.Th("Time", className="small"),
html.Th("Side", className="small"),
html.Th("Size", className="small"),
html.Th("Entry", className="small"),
html.Th("Exit", className="small"),
html.Th("P&L", className="small"),
html.Th("Fees", className="small")
])
])
# Create table rows
rows = []
for trade in closed_trades[-20:]: # Last 20 trades
entry_time = trade.get('entry_time', 'Unknown')
side = trade.get('side', 'UNKNOWN')
size = trade.get('size', 0)
entry_price = trade.get('entry_price', 0)
exit_price = trade.get('exit_price', 0)
pnl = trade.get('pnl', 0)
fees = trade.get('fees', 0)
# Format time
if isinstance(entry_time, datetime):
time_str = entry_time.strftime('%H:%M:%S')
else:
time_str = str(entry_time)
# Determine P&L color
pnl_class = "text-success" if pnl >= 0 else "text-danger"
side_class = "text-success" if side == "BUY" else "text-danger"
row = html.Tr([
html.Td(time_str, className="small"),
html.Td(side, className=f"small {side_class}"),
html.Td(f"{size:.3f}", className="small"),
html.Td(f"${entry_price:.2f}", className="small"),
html.Td(f"${exit_price:.2f}", className="small"),
html.Td(f"${pnl:.2f}", className=f"small {pnl_class}"),
html.Td(f"${fees:.3f}", className="small text-muted")
])
rows.append(row)
tbody = html.Tbody(rows)
return html.Table([headers, tbody], className="table table-sm table-striped")
except Exception as e:
logger.error(f"Error formatting closed trades: {e}")
return html.P(f"Error: {str(e)}", className="text-danger small")
def format_system_status(self, status_data):
"""Format system status for display"""
try:
if not status_data or 'error' in status_data:
return [html.P("Status unavailable", className="text-muted small")]
status_items = []
# Trading status
trading_enabled = status_data.get('trading_enabled', False)
simulation_mode = status_data.get('simulation_mode', True)
if trading_enabled:
if simulation_mode:
status_items.append(html.Div([
html.I(className="fas fa-play-circle text-success me-2"),
html.Span("Trading: SIMULATION", className="text-warning")
], className="mb-1"))
else:
status_items.append(html.Div([
html.I(className="fas fa-play-circle text-success me-2"),
html.Span("Trading: LIVE", className="text-success fw-bold")
], className="mb-1"))
else:
status_items.append(html.Div([
html.I(className="fas fa-pause-circle text-danger me-2"),
html.Span("Trading: DISABLED", className="text-danger")
], className="mb-1"))
# Data provider status
data_status = status_data.get('data_provider_status', 'Unknown')
status_items.append(html.Div([
html.I(className="fas fa-database text-info me-2"),
html.Span(f"Data: {data_status}", className="small")
], className="mb-1"))
# WebSocket status
ws_status = status_data.get('websocket_status', 'Unknown')
ws_class = "text-success" if ws_status == "Connected" else "text-danger"
status_items.append(html.Div([
html.I(className="fas fa-wifi text-info me-2"),
html.Span(f"WebSocket: {ws_status}", className=f"small {ws_class}")
], className="mb-1"))
# COB status
cob_status = status_data.get('cob_status', 'Unknown')
cob_class = "text-success" if cob_status == "Active" else "text-warning"
status_items.append(html.Div([
html.I(className="fas fa-layer-group text-info me-2"),
html.Span(f"COB: {cob_status}", className=f"small {cob_class}")
], className="mb-1"))
return status_items
except Exception as e:
logger.error(f"Error formatting system status: {e}")
return [html.P(f"Error: {str(e)}", className="text-danger small")]
def format_cob_data(self, cob_snapshot, symbol):
"""Format COB data for display"""
try:
if not cob_snapshot:
return [html.P("No COB data", className="text-muted small")]
# Basic COB info
cob_info = []
# Symbol and update count
cob_info.append(html.Div([
html.Strong(f"{symbol}", className="text-info"),
html.Span(" - COB Snapshot", className="small text-muted")
], className="mb-2"))
# Mock COB data display (since we don't have real COB structure)
cob_info.append(html.Div([
html.Div([
html.I(className="fas fa-chart-bar text-success me-2"),
html.Span("Order Book: Active", className="small")
], className="mb-1"),
html.Div([
html.I(className="fas fa-coins text-warning me-2"),
html.Span("Liquidity: Good", className="small")
], className="mb-1"),
html.Div([
html.I(className="fas fa-balance-scale text-info me-2"),
html.Span("Imbalance: Neutral", className="small")
])
]))
return cob_info
except Exception as e:
logger.error(f"Error formatting COB data: {e}")
return [html.P(f"Error: {str(e)}", className="text-danger small")]
def format_training_metrics(self, metrics_data):
"""Format training metrics for display"""
try:
if not metrics_data or 'error' in metrics_data:
return [html.P("No training data", className="text-muted small")]
metrics_info = []
# CNN metrics
if 'cnn_metrics' in metrics_data:
cnn_data = metrics_data['cnn_metrics']
metrics_info.append(html.Div([
html.Strong("CNN Model", className="text-primary"),
html.Br(),
html.Span(f"Status: Active", className="small text-success")
], className="mb-2"))
# RL metrics
if 'rl_metrics' in metrics_data:
rl_data = metrics_data['rl_metrics']
metrics_info.append(html.Div([
html.Strong("RL Model", className="text-warning"),
html.Br(),
html.Span(f"Status: Training", className="small text-info")
], className="mb-2"))
# Default message if no metrics
if not metrics_info:
metrics_info.append(html.P("Training metrics not available", className="text-muted small"))
return metrics_info
except Exception as e:
logger.error(f"Error formatting training metrics: {e}")
return [html.P(f"Error: {str(e)}", className="text-danger small")]

221
web/layout_manager.py Normal file
View File

@ -0,0 +1,221 @@
"""
Dashboard Layout Manager - Clean Trading Dashboard
Manages the layout and structure of the trading dashboard
"""
import dash
from dash import dcc, html
from datetime import datetime
class DashboardLayoutManager:
"""Manages dashboard layout and structure"""
def __init__(self, starting_balance: float = 100.0, trading_executor=None):
self.starting_balance = starting_balance
self.trading_executor = trading_executor
def create_main_layout(self):
"""Create the main dashboard layout"""
return html.Div([
self._create_header(),
self._create_interval_component(),
self._create_main_content()
], className="container-fluid")
def _create_header(self):
"""Create the dashboard header"""
trading_mode = "SIMULATION" if (not self.trading_executor or
getattr(self.trading_executor, 'simulation_mode', True)) else "LIVE"
return html.Div([
html.H2([
html.I(className="fas fa-chart-line me-2"),
"Clean Trading Dashboard"
], className="text-light mb-0"),
html.P(
f"Ultra-Fast Updates • Portfolio: ${self.starting_balance:,.0f}{trading_mode}",
className="text-light mb-0 opacity-75 small"
)
], className="bg-dark p-2 mb-2")
def _create_interval_component(self):
"""Create the auto-refresh interval component"""
return dcc.Interval(
id='interval-component',
interval=1000, # Update every 1 second for maximum responsiveness
n_intervals=0
)
def _create_main_content(self):
"""Create the main content area"""
return html.Div([
self._create_metrics_and_signals_row(),
self._create_charts_row(),
self._create_analytics_row(),
self._create_performance_row()
])
def _create_metrics_and_signals_row(self):
"""Create the top row with key metrics and recent signals"""
return html.Div([
# Left side - Key metrics (compact cards)
self._create_metrics_grid(),
# Right side - Recent Signals & Model Training
self._create_signals_and_training_panels()
], className="d-flex mb-3")
def _create_metrics_grid(self):
"""Create the metrics grid with compact cards"""
metrics_cards = [
("current-price", "Live Price", "text-success"),
("session-pnl", "Session P&L", ""),
("total-fees", "Total Fees", "text-warning"),
("current-position", "Position", "text-info"),
("trade-count", "Trades", "text-warning"),
("portfolio-value", "Portfolio", "text-secondary"),
("mexc-status", "MEXC API", "text-info")
]
cards = []
for card_id, label, text_class in metrics_cards:
card = html.Div([
html.Div([
html.H5(id=card_id, className=f"{text_class} mb-0 small"),
html.P(label, className="text-muted mb-0 tiny")
], className="card-body text-center p-2")
], className="card bg-light", style={"height": "60px"})
cards.append(card)
return html.Div(
cards,
style={
"display": "grid",
"gridTemplateColumns": "repeat(4, 1fr)",
"gap": "8px",
"width": "60%"
}
)
def _create_signals_and_training_panels(self):
"""Create the signals and training panels"""
return html.Div([
# Recent Trading Signals Column (50%)
html.Div([
html.Div([
html.H6([
html.I(className="fas fa-robot me-2"),
"Recent Trading Signals"
], className="card-title mb-2"),
html.Div(id="recent-decisions", style={"height": "160px", "overflowY": "auto"})
], className="card-body p-2")
], className="card", style={"width": "48%"}),
# Model Training + COB Buckets Column (50%)
html.Div([
html.Div([
html.H6([
html.I(className="fas fa-brain me-2"),
"Training Progress & COB $1 Buckets"
], className="card-title mb-2"),
html.Div(id="training-metrics", style={"height": "160px", "overflowY": "auto"})
], className="card-body p-2")
], className="card", style={"width": "48%", "marginLeft": "4%"}),
], style={"width": "48%", "marginLeft": "2%", "display": "flex"})
def _create_charts_row(self):
"""Create the charts row with price chart and manual trading buttons"""
return html.Div([
html.Div([
html.Div([
# Chart header with manual trading buttons
html.Div([
html.H6([
html.I(className="fas fa-chart-candlestick me-2"),
"Live 1m Price Chart (3h) + 1s Mini Chart (5min) - Updated Every Second"
], className="card-title mb-0"),
html.Div([
html.Button([
html.I(className="fas fa-arrow-up me-1"),
"BUY"
], id="manual-buy-btn", className="btn btn-success btn-sm me-2",
style={"fontSize": "10px", "padding": "2px 8px"}),
html.Button([
html.I(className="fas fa-arrow-down me-1"),
"SELL"
], id="manual-sell-btn", className="btn btn-danger btn-sm",
style={"fontSize": "10px", "padding": "2px 8px"})
], className="d-flex")
], className="d-flex justify-content-between align-items-center mb-2"),
html.Div([
dcc.Graph(id="price-chart", style={"height": "500px"})
])
], className="card-body p-2")
], className="card")
])
def _create_analytics_row(self):
"""Create the analytics row with COB data and system status"""
return html.Div([
# COB Status
html.Div([
html.Div([
html.H6([
html.I(className="fas fa-server me-2"),
"System Status"
], className="card-title mb-2"),
html.Div(id="cob-status-content")
], className="card-body p-2")
], className="card", style={"width": "32%"}),
# ETH/USDT COB
html.Div([
html.Div([
html.H6([
html.I(className="fab fa-ethereum me-2"),
"ETH/USDT COB"
], className="card-title mb-2"),
html.Div(id="eth-cob-content")
], className="card-body p-2")
], className="card", style={"width": "32%", "marginLeft": "2%"}),
# BTC/USDT COB
html.Div([
html.Div([
html.H6([
html.I(className="fab fa-bitcoin me-2"),
"BTC/USDT COB"
], className="card-title mb-2"),
html.Div(id="btc-cob-content")
], className="card-body p-2")
], className="card", style={"width": "32%", "marginLeft": "2%"})
], className="d-flex mb-3")
def _create_performance_row(self):
"""Create the performance row with closed trades and session controls"""
return html.Div([
# Closed Trades Table
html.Div([
html.Div([
html.H6([
html.I(className="fas fa-history me-2"),
"Closed Trades"
], className="card-title mb-2"),
html.Div(id="closed-trades-table", style={"height": "200px", "overflowY": "auto"})
], className="card-body p-2")
], className="card", style={"width": "70%"}),
# Session Controls
html.Div([
html.Div([
html.H6([
html.I(className="fas fa-cog me-2"),
"Session Controls"
], className="card-title mb-2"),
html.Button([
html.I(className="fas fa-trash me-1"),
"Clear Session"
], id="clear-session-btn", className="btn btn-warning btn-sm w-100")
], className="card-body p-2")
], className="card", style={"width": "28%", "marginLeft": "2%"})
], className="d-flex")