BIG CLEANUP
This commit is contained in:
@ -1 +0,0 @@
|
||||
# Web module for trading system dashboard
|
@ -1,253 +0,0 @@
|
||||
"""
|
||||
Dashboard Fix
|
||||
|
||||
This module provides fixes for the trading dashboard to address:
|
||||
1. Trade display issues
|
||||
2. P&L calculation and display
|
||||
3. Position tracking and synchronization
|
||||
|
||||
Apply these fixes by importing and applying the patch in the dashboard initialization
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DashboardFix:
|
||||
"""Fixes for the Dashboard class"""
|
||||
|
||||
@staticmethod
|
||||
def apply_fixes(dashboard):
|
||||
"""Apply all fixes to the dashboard"""
|
||||
logger.info("Applying Dashboard fixes...")
|
||||
|
||||
# Apply fixes
|
||||
DashboardFix._fix_trade_display(dashboard)
|
||||
DashboardFix._fix_position_sync(dashboard)
|
||||
DashboardFix._fix_pnl_calculation(dashboard)
|
||||
DashboardFix._add_trade_validation(dashboard)
|
||||
|
||||
logger.info("Dashboard fixes applied successfully")
|
||||
return dashboard
|
||||
|
||||
@staticmethod
|
||||
def _fix_trade_display(dashboard):
|
||||
"""Fix trade display to ensure accurate information"""
|
||||
# Store original format_closed_trades_table method
|
||||
if hasattr(dashboard.component_manager, 'format_closed_trades_table'):
|
||||
original_format_closed_trades = dashboard.component_manager.format_closed_trades_table
|
||||
|
||||
def format_closed_trades_table_fixed(self, closed_trades, trading_stats=None):
|
||||
"""Fixed closed trades table formatter with accurate P&L calculation"""
|
||||
# Recalculate P&L for each trade to ensure accuracy
|
||||
for trade in closed_trades:
|
||||
# Skip if already validated
|
||||
if getattr(trade, 'pnl_validated', False):
|
||||
continue
|
||||
|
||||
# Handle both trade objects and dictionary formats
|
||||
if hasattr(trade, 'entry_price'):
|
||||
# This is a trade object
|
||||
entry_price = getattr(trade, 'entry_price', 0)
|
||||
exit_price = getattr(trade, 'exit_price', 0)
|
||||
size = getattr(trade, 'size', 0)
|
||||
side = getattr(trade, 'side', 'UNKNOWN')
|
||||
fees = getattr(trade, 'fees', 0)
|
||||
else:
|
||||
# This is a dictionary format
|
||||
entry_price = trade.get('entry_price', 0)
|
||||
exit_price = trade.get('exit_price', 0)
|
||||
size = trade.get('size', trade.get('quantity', 0))
|
||||
side = trade.get('side', 'UNKNOWN')
|
||||
fees = trade.get('fees', 0)
|
||||
|
||||
# Recalculate P&L
|
||||
if side == 'LONG' or side == 'BUY':
|
||||
pnl = (exit_price - entry_price) * size
|
||||
else: # SHORT or SELL
|
||||
pnl = (entry_price - exit_price) * size
|
||||
|
||||
# Update P&L value
|
||||
if hasattr(trade, 'entry_price'):
|
||||
trade.pnl = pnl
|
||||
trade.net_pnl = pnl - fees
|
||||
trade.pnl_validated = True
|
||||
else:
|
||||
trade['pnl'] = pnl
|
||||
trade['net_pnl'] = pnl - fees
|
||||
trade['pnl_validated'] = True
|
||||
|
||||
# Call original method with validated trades
|
||||
return original_format_closed_trades(closed_trades, trading_stats)
|
||||
|
||||
# Apply the patch
|
||||
dashboard.component_manager.format_closed_trades_table = format_closed_trades_table_fixed.__get__(dashboard.component_manager)
|
||||
logger.info("Trade display fix applied")
|
||||
|
||||
@staticmethod
|
||||
def _fix_position_sync(dashboard):
|
||||
"""Fix position synchronization to ensure accurate position tracking"""
|
||||
# Store original _sync_position_from_executor method
|
||||
if hasattr(dashboard, '_sync_position_from_executor'):
|
||||
original_sync_position = dashboard._sync_position_from_executor
|
||||
|
||||
def sync_position_from_executor_fixed(self, symbol):
|
||||
"""Fixed position sync with validation and logging"""
|
||||
try:
|
||||
# Call original sync method
|
||||
result = original_sync_position(symbol)
|
||||
|
||||
# Add validation and logging
|
||||
if self.trading_executor and hasattr(self.trading_executor, 'positions'):
|
||||
if symbol in self.trading_executor.positions:
|
||||
position = self.trading_executor.positions[symbol]
|
||||
|
||||
# Log position details for debugging
|
||||
logger.debug(f"Position sync for {symbol}: "
|
||||
f"Side={position.side}, "
|
||||
f"Size={position.size}, "
|
||||
f"Entry=${position.entry_price:.2f}")
|
||||
|
||||
# Validate position data
|
||||
if position.entry_price <= 0:
|
||||
logger.warning(f"Invalid entry price for {symbol}: ${position.entry_price:.2f}")
|
||||
|
||||
# Store last sync time
|
||||
if not hasattr(self, 'last_position_sync'):
|
||||
self.last_position_sync = {}
|
||||
|
||||
self.last_position_sync[symbol] = time.time()
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in sync_position_from_executor_fixed: {e}")
|
||||
return None
|
||||
|
||||
# Apply the patch
|
||||
dashboard._sync_position_from_executor = sync_position_from_executor_fixed.__get__(dashboard)
|
||||
logger.info("Position sync fix applied")
|
||||
|
||||
@staticmethod
|
||||
def _fix_pnl_calculation(dashboard):
|
||||
"""Fix P&L calculation to ensure accuracy"""
|
||||
# Add a method to recalculate P&L for all closed trades
|
||||
def recalculate_all_pnl(self):
|
||||
"""Recalculate P&L for all closed trades"""
|
||||
if not hasattr(self, 'closed_trades') or not self.closed_trades:
|
||||
return
|
||||
|
||||
for trade in self.closed_trades:
|
||||
# Handle both trade objects and dictionary formats
|
||||
if hasattr(trade, 'entry_price'):
|
||||
# This is a trade object
|
||||
entry_price = getattr(trade, 'entry_price', 0)
|
||||
exit_price = getattr(trade, 'exit_price', 0)
|
||||
size = getattr(trade, 'size', 0)
|
||||
side = getattr(trade, 'side', 'UNKNOWN')
|
||||
fees = getattr(trade, 'fees', 0)
|
||||
else:
|
||||
# This is a dictionary format
|
||||
entry_price = trade.get('entry_price', 0)
|
||||
exit_price = trade.get('exit_price', 0)
|
||||
size = trade.get('size', trade.get('quantity', 0))
|
||||
side = trade.get('side', 'UNKNOWN')
|
||||
fees = trade.get('fees', 0)
|
||||
|
||||
# Recalculate P&L
|
||||
if side == 'LONG' or side == 'BUY':
|
||||
pnl = (exit_price - entry_price) * size
|
||||
else: # SHORT or SELL
|
||||
pnl = (entry_price - exit_price) * size
|
||||
|
||||
# Update P&L value
|
||||
if hasattr(trade, 'entry_price'):
|
||||
trade.pnl = pnl
|
||||
trade.net_pnl = pnl - fees
|
||||
else:
|
||||
trade['pnl'] = pnl
|
||||
trade['net_pnl'] = pnl - fees
|
||||
|
||||
logger.info(f"Recalculated P&L for {len(self.closed_trades)} closed trades")
|
||||
|
||||
# Add the method
|
||||
dashboard.recalculate_all_pnl = recalculate_all_pnl.__get__(dashboard)
|
||||
|
||||
# Call it once to fix existing trades
|
||||
dashboard.recalculate_all_pnl()
|
||||
|
||||
logger.info("P&L calculation fix applied")
|
||||
|
||||
@staticmethod
|
||||
def _add_trade_validation(dashboard):
|
||||
"""Add trade validation to prevent invalid trades"""
|
||||
# Store original _on_trade_closed method if it exists
|
||||
original_on_trade_closed = getattr(dashboard, '_on_trade_closed', None)
|
||||
|
||||
if original_on_trade_closed:
|
||||
def on_trade_closed_fixed(self, trade_data):
|
||||
"""Fixed trade closed handler with validation"""
|
||||
try:
|
||||
# Validate trade data
|
||||
is_valid = True
|
||||
validation_errors = []
|
||||
|
||||
# Check for required fields
|
||||
required_fields = ['symbol', 'side', 'entry_price', 'exit_price', 'size']
|
||||
for field in required_fields:
|
||||
if field not in trade_data:
|
||||
is_valid = False
|
||||
validation_errors.append(f"Missing required field: {field}")
|
||||
|
||||
# Check for valid prices
|
||||
if 'entry_price' in trade_data and trade_data['entry_price'] <= 0:
|
||||
is_valid = False
|
||||
validation_errors.append(f"Invalid entry price: {trade_data['entry_price']}")
|
||||
|
||||
if 'exit_price' in trade_data and trade_data['exit_price'] <= 0:
|
||||
is_valid = False
|
||||
validation_errors.append(f"Invalid exit price: {trade_data['exit_price']}")
|
||||
|
||||
# Check for valid size
|
||||
if 'size' in trade_data and trade_data['size'] <= 0:
|
||||
is_valid = False
|
||||
validation_errors.append(f"Invalid size: {trade_data['size']}")
|
||||
|
||||
# If invalid, log errors and skip
|
||||
if not is_valid:
|
||||
logger.warning(f"Invalid trade data: {validation_errors}")
|
||||
return
|
||||
|
||||
# Calculate correct P&L
|
||||
if 'side' in trade_data and 'entry_price' in trade_data and 'exit_price' in trade_data and 'size' in trade_data:
|
||||
side = trade_data['side']
|
||||
entry_price = trade_data['entry_price']
|
||||
exit_price = trade_data['exit_price']
|
||||
size = trade_data['size']
|
||||
|
||||
if side == 'LONG' or side == 'BUY':
|
||||
pnl = (exit_price - entry_price) * size
|
||||
else: # SHORT or SELL
|
||||
pnl = (entry_price - exit_price) * size
|
||||
|
||||
# Update P&L in trade data
|
||||
trade_data['pnl'] = pnl
|
||||
|
||||
# Calculate net P&L (after fees)
|
||||
fees = trade_data.get('fees', 0)
|
||||
trade_data['net_pnl'] = pnl - fees
|
||||
|
||||
# Call original method with validated data
|
||||
return original_on_trade_closed(trade_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in on_trade_closed_fixed: {e}")
|
||||
|
||||
# Apply the patch
|
||||
dashboard._on_trade_closed = on_trade_closed_fixed.__get__(dashboard)
|
||||
logger.info("Trade validation fix applied")
|
||||
else:
|
||||
logger.warning("_on_trade_closed method not found, skipping trade validation fix")
|
@ -1,331 +0,0 @@
|
||||
"""
|
||||
Dashboard Data Model
|
||||
Provides structured data for template rendering
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetricData:
|
||||
"""Individual metric for the dashboard"""
|
||||
id: str
|
||||
label: str
|
||||
value: str
|
||||
format_type: str = "text" # text, currency, percentage
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradingControlsData:
|
||||
"""Trading controls configuration"""
|
||||
buy_text: str = "BUY"
|
||||
sell_text: str = "SELL"
|
||||
clear_text: str = "Clear Session"
|
||||
leverage: int = 10
|
||||
leverage_min: int = 1
|
||||
leverage_max: int = 50
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecentDecisionData:
|
||||
"""Recent AI decision data"""
|
||||
timestamp: str
|
||||
action: str
|
||||
symbol: str
|
||||
confidence: float
|
||||
price: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class COBLevelData:
|
||||
"""Order book level data"""
|
||||
side: str # 'bid' or 'ask'
|
||||
size: str
|
||||
price: str
|
||||
total: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class COBData:
|
||||
"""Complete order book data for a symbol"""
|
||||
symbol: str
|
||||
content_id: str
|
||||
total_usd: str
|
||||
total_crypto: str
|
||||
levels: List[COBLevelData] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelData:
|
||||
"""Model status data"""
|
||||
name: str
|
||||
status: str # 'training', 'idle', 'loading'
|
||||
status_text: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrainingMetricData:
|
||||
"""Training metric data"""
|
||||
name: str
|
||||
value: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PerformanceStatData:
|
||||
"""Performance statistic data"""
|
||||
name: str
|
||||
value: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClosedTradeData:
|
||||
"""Closed trade data"""
|
||||
time: str
|
||||
symbol: str
|
||||
side: str
|
||||
size: str
|
||||
entry_price: str
|
||||
exit_price: str
|
||||
pnl: float
|
||||
duration: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChartData:
|
||||
"""Chart configuration data"""
|
||||
title: str = "Price Chart & Signals"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DashboardModel:
|
||||
"""Complete dashboard data model"""
|
||||
title: str = "Live Scalping Dashboard"
|
||||
subtitle: str = "Real-time Trading with AI Models"
|
||||
refresh_interval: int = 1000
|
||||
|
||||
# Main sections
|
||||
metrics: List[MetricData] = field(default_factory=list)
|
||||
chart: ChartData = field(default_factory=ChartData)
|
||||
trading_controls: TradingControlsData = field(default_factory=TradingControlsData)
|
||||
recent_decisions: List[RecentDecisionData] = field(default_factory=list)
|
||||
cob_data: List[COBData] = field(default_factory=list)
|
||||
models: List[ModelData] = field(default_factory=list)
|
||||
training_metrics: List[TrainingMetricData] = field(default_factory=list)
|
||||
performance_stats: List[PerformanceStatData] = field(default_factory=list)
|
||||
closed_trades: List[ClosedTradeData] = field(default_factory=list)
|
||||
|
||||
|
||||
class DashboardDataBuilder:
|
||||
"""Builder class to construct dashboard data from various sources"""
|
||||
|
||||
def __init__(self):
|
||||
self.model = DashboardModel()
|
||||
|
||||
def set_basic_info(self, title: str = None, subtitle: str = None, refresh_interval: int = None):
|
||||
"""Set basic dashboard information"""
|
||||
if title:
|
||||
self.model.title = title
|
||||
if subtitle:
|
||||
self.model.subtitle = subtitle
|
||||
if refresh_interval:
|
||||
self.model.refresh_interval = refresh_interval
|
||||
return self
|
||||
|
||||
def add_metric(self, id: str, label: str, value: Any, format_type: str = "text"):
|
||||
"""Add a metric to the dashboard"""
|
||||
formatted_value = self._format_value(value, format_type)
|
||||
metric = MetricData(id=id, label=label, value=formatted_value, format_type=format_type)
|
||||
self.model.metrics.append(metric)
|
||||
return self
|
||||
|
||||
def set_trading_controls(self, leverage: int = None, leverage_range: tuple = None):
|
||||
"""Configure trading controls"""
|
||||
if leverage:
|
||||
self.model.trading_controls.leverage = leverage
|
||||
if leverage_range:
|
||||
self.model.trading_controls.leverage_min = leverage_range[0]
|
||||
self.model.trading_controls.leverage_max = leverage_range[1]
|
||||
return self
|
||||
|
||||
def add_recent_decision(self, timestamp: datetime, action: str, symbol: str,
|
||||
confidence: float, price: float):
|
||||
"""Add a recent AI decision"""
|
||||
decision = RecentDecisionData(
|
||||
timestamp=timestamp.strftime("%H:%M:%S"),
|
||||
action=action,
|
||||
symbol=symbol,
|
||||
confidence=round(confidence * 100, 1),
|
||||
price=round(price, 4)
|
||||
)
|
||||
self.model.recent_decisions.append(decision)
|
||||
return self
|
||||
|
||||
def add_cob_data(self, symbol: str, content_id: str, total_usd: float,
|
||||
total_crypto: float, levels: List[Dict]):
|
||||
"""Add COB data for a symbol"""
|
||||
cob_levels = []
|
||||
for level in levels:
|
||||
cob_level = COBLevelData(
|
||||
side=level.get('side', 'bid'),
|
||||
size=self._format_value(level.get('size', 0), 'number'),
|
||||
price=self._format_value(level.get('price', 0), 'currency'),
|
||||
total=self._format_value(level.get('total', 0), 'currency')
|
||||
)
|
||||
cob_levels.append(cob_level)
|
||||
|
||||
cob = COBData(
|
||||
symbol=symbol,
|
||||
content_id=content_id,
|
||||
total_usd=self._format_value(total_usd, 'currency'),
|
||||
total_crypto=self._format_value(total_crypto, 'number'),
|
||||
levels=cob_levels
|
||||
)
|
||||
self.model.cob_data.append(cob)
|
||||
return self
|
||||
|
||||
def add_model_status(self, name: str, is_training: bool, is_loading: bool = False):
|
||||
"""Add model status"""
|
||||
if is_loading:
|
||||
status = "loading"
|
||||
status_text = "Loading"
|
||||
elif is_training:
|
||||
status = "training"
|
||||
status_text = "Training"
|
||||
else:
|
||||
status = "idle"
|
||||
status_text = "Idle"
|
||||
|
||||
model = ModelData(name=name, status=status, status_text=status_text)
|
||||
self.model.models.append(model)
|
||||
return self
|
||||
|
||||
def add_training_metric(self, name: str, value: Any):
|
||||
"""Add training metric"""
|
||||
metric = TrainingMetricData(
|
||||
name=name,
|
||||
value=self._format_value(value, 'number')
|
||||
)
|
||||
self.model.training_metrics.append(metric)
|
||||
return self
|
||||
|
||||
def add_performance_stat(self, name: str, value: Any):
|
||||
"""Add performance statistic"""
|
||||
stat = PerformanceStatData(
|
||||
name=name,
|
||||
value=self._format_value(value, 'number')
|
||||
)
|
||||
self.model.performance_stats.append(stat)
|
||||
return self
|
||||
|
||||
def add_closed_trade(self, time: datetime, symbol: str, side: str, size: float,
|
||||
entry_price: float, exit_price: float, pnl: float, duration: str):
|
||||
"""Add closed trade"""
|
||||
trade = ClosedTradeData(
|
||||
time=time.strftime("%H:%M:%S"),
|
||||
symbol=symbol,
|
||||
side=side,
|
||||
size=self._format_value(size, 'number'),
|
||||
entry_price=self._format_value(entry_price, 'currency'),
|
||||
exit_price=self._format_value(exit_price, 'currency'),
|
||||
pnl=round(pnl, 2),
|
||||
duration=duration
|
||||
)
|
||||
self.model.closed_trades.append(trade)
|
||||
return self
|
||||
|
||||
def build(self) -> DashboardModel:
|
||||
"""Build and return the complete dashboard model"""
|
||||
return self.model
|
||||
|
||||
def _format_value(self, value: Any, format_type: str) -> str:
|
||||
"""Format value based on type"""
|
||||
if value is None:
|
||||
return "N/A"
|
||||
|
||||
try:
|
||||
if format_type == "currency":
|
||||
return f"${float(value):,.4f}"
|
||||
elif format_type == "percentage":
|
||||
return f"{float(value):.2f}%"
|
||||
elif format_type == "number":
|
||||
if isinstance(value, int):
|
||||
return f"{value:,}"
|
||||
else:
|
||||
return f"{float(value):,.2f}"
|
||||
else:
|
||||
return str(value)
|
||||
except (ValueError, TypeError):
|
||||
return str(value)
|
||||
|
||||
|
||||
def create_sample_dashboard_data() -> DashboardModel:
|
||||
"""Create sample dashboard data for testing"""
|
||||
builder = DashboardDataBuilder()
|
||||
|
||||
# Basic info
|
||||
builder.set_basic_info(
|
||||
title="Live Scalping Dashboard",
|
||||
subtitle="Real-time Trading with AI Models",
|
||||
refresh_interval=1000
|
||||
)
|
||||
|
||||
# Metrics
|
||||
builder.add_metric("current-price", "Current Price", 3425.67, "currency")
|
||||
builder.add_metric("session-pnl", "Session PnL", 125.34, "currency")
|
||||
builder.add_metric("current-position", "Position", 0.0, "number")
|
||||
builder.add_metric("trade-count", "Trades", 15, "number")
|
||||
builder.add_metric("portfolio-value", "Portfolio", 10250.45, "currency")
|
||||
builder.add_metric("mexc-status", "MEXC Status", "Connected", "text")
|
||||
|
||||
# Trading controls
|
||||
builder.set_trading_controls(leverage=10, leverage_range=(1, 50))
|
||||
|
||||
# Recent decisions
|
||||
builder.add_recent_decision(datetime.now(), "BUY", "ETH/USDT", 0.85, 3425.67)
|
||||
builder.add_recent_decision(datetime.now(), "HOLD", "BTC/USDT", 0.62, 45123.45)
|
||||
|
||||
# COB data
|
||||
eth_levels = [
|
||||
{"side": "ask", "size": 1.5, "price": 3426.12, "total": 5139.18},
|
||||
{"side": "ask", "size": 2.3, "price": 3425.89, "total": 7879.55},
|
||||
{"side": "bid", "size": 1.8, "price": 3425.45, "total": 6165.81},
|
||||
{"side": "bid", "size": 3.2, "price": 3425.12, "total": 10960.38}
|
||||
]
|
||||
builder.add_cob_data("ETH/USDT", "eth-cob-content", 25000.0, 7.3, eth_levels)
|
||||
|
||||
btc_levels = [
|
||||
{"side": "ask", "size": 0.15, "price": 45125.67, "total": 6768.85},
|
||||
{"side": "ask", "size": 0.23, "price": 45123.45, "total": 10378.39},
|
||||
{"side": "bid", "size": 0.18, "price": 45121.23, "total": 8121.82},
|
||||
{"side": "bid", "size": 0.32, "price": 45119.12, "total": 14438.12}
|
||||
]
|
||||
builder.add_cob_data("BTC/USDT", "btc-cob-content", 35000.0, 0.88, btc_levels)
|
||||
|
||||
# Model statuses
|
||||
builder.add_model_status("DQN", True)
|
||||
builder.add_model_status("CNN", True)
|
||||
builder.add_model_status("Transformer", False)
|
||||
builder.add_model_status("COB-RL", True)
|
||||
|
||||
# Training metrics
|
||||
builder.add_training_metric("DQN Loss", 0.0234)
|
||||
builder.add_training_metric("CNN Accuracy", 0.876)
|
||||
builder.add_training_metric("Training Steps", 15420)
|
||||
builder.add_training_metric("Learning Rate", 0.0001)
|
||||
|
||||
# Performance stats
|
||||
builder.add_performance_stat("Win Rate", 68.5)
|
||||
builder.add_performance_stat("Avg Trade", 8.34)
|
||||
builder.add_performance_stat("Max Drawdown", -45.67)
|
||||
builder.add_performance_stat("Sharpe Ratio", 1.82)
|
||||
|
||||
# Closed trades
|
||||
builder.add_closed_trade(
|
||||
datetime.now(), "ETH/USDT", "BUY", 1.5, 3420.45, 3428.12, 11.51, "2m 34s"
|
||||
)
|
||||
builder.add_closed_trade(
|
||||
datetime.now(), "BTC/USDT", "SELL", 0.1, 45150.23, 45142.67, -0.76, "1m 12s"
|
||||
)
|
||||
|
||||
return builder.build()
|
@ -1,753 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Models & Training Progress Panel - Clean Implementation
|
||||
Displays real-time model status, training metrics, and performance data
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime, timedelta
|
||||
from dash import html, dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ModelsTrainingPanel:
|
||||
"""Clean implementation of the Models & Training Progress panel"""
|
||||
|
||||
def __init__(self, orchestrator=None):
|
||||
self.orchestrator = orchestrator
|
||||
self.last_update = None
|
||||
|
||||
def create_panel(self) -> html.Div:
|
||||
"""Create the main Models & Training Progress panel"""
|
||||
try:
|
||||
# Get fresh data from orchestrator
|
||||
panel_data = self._gather_panel_data()
|
||||
|
||||
# Build the panel components
|
||||
content = []
|
||||
|
||||
# Header with refresh button
|
||||
content.append(self._create_header())
|
||||
|
||||
# Models section
|
||||
if panel_data.get('models'):
|
||||
content.append(self._create_models_section(panel_data['models']))
|
||||
else:
|
||||
content.append(self._create_no_models_message())
|
||||
|
||||
# Training status section
|
||||
if panel_data.get('training_status'):
|
||||
content.append(self._create_training_status_section(panel_data['training_status']))
|
||||
|
||||
# Performance metrics section
|
||||
if panel_data.get('performance_metrics'):
|
||||
content.append(self._create_performance_section(panel_data['performance_metrics']))
|
||||
|
||||
return html.Div(content, id="training-metrics")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating models training panel: {e}")
|
||||
return html.Div([
|
||||
html.P(f"Error loading training panel: {str(e)}", className="text-danger small")
|
||||
], id="training-metrics")
|
||||
|
||||
def _gather_panel_data(self) -> Dict[str, Any]:
|
||||
"""Gather all data needed for the panel from orchestrator and other sources"""
|
||||
data = {
|
||||
'models': {},
|
||||
'training_status': {},
|
||||
'performance_metrics': {},
|
||||
'last_update': datetime.now().strftime('%H:%M:%S')
|
||||
}
|
||||
|
||||
if not self.orchestrator:
|
||||
logger.warning("No orchestrator available for training panel")
|
||||
return data
|
||||
|
||||
try:
|
||||
# Get model registry information
|
||||
if hasattr(self.orchestrator, 'model_registry') and self.orchestrator.model_registry:
|
||||
registered_models = self.orchestrator.model_registry.get_all_models()
|
||||
for model_name, model_info in registered_models.items():
|
||||
data['models'][model_name] = self._extract_model_data(model_name, model_info)
|
||||
|
||||
# Add decision fusion model if it exists (check multiple sources)
|
||||
decision_fusion_added = False
|
||||
|
||||
# Check if it's in the model registry
|
||||
if hasattr(self.orchestrator, 'model_registry') and self.orchestrator.model_registry:
|
||||
registered_models = self.orchestrator.model_registry.get_all_models()
|
||||
if 'decision_fusion' in registered_models:
|
||||
data['models']['decision_fusion'] = self._extract_decision_fusion_data()
|
||||
decision_fusion_added = True
|
||||
|
||||
# If not in registry, check if decision fusion network exists
|
||||
if not decision_fusion_added and hasattr(self.orchestrator, 'decision_fusion_network') and self.orchestrator.decision_fusion_network:
|
||||
data['models']['decision_fusion'] = self._extract_decision_fusion_data()
|
||||
decision_fusion_added = True
|
||||
|
||||
# If still not added, check if decision fusion is enabled
|
||||
if not decision_fusion_added and hasattr(self.orchestrator, 'decision_fusion_enabled') and self.orchestrator.decision_fusion_enabled:
|
||||
data['models']['decision_fusion'] = self._extract_decision_fusion_data()
|
||||
decision_fusion_added = True
|
||||
|
||||
# Add COB RL model if it exists but wasn't captured in registry
|
||||
if 'cob_rl_model' not in data['models'] and hasattr(self.orchestrator, 'cob_rl_model'):
|
||||
data['models']['cob_rl_model'] = self._extract_cob_rl_data()
|
||||
|
||||
# Get training status
|
||||
data['training_status'] = self._extract_training_status()
|
||||
|
||||
# Get performance metrics
|
||||
data['performance_metrics'] = self._extract_performance_metrics()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error gathering panel data: {e}")
|
||||
data['error'] = str(e)
|
||||
|
||||
return data
|
||||
|
||||
def _extract_model_data(self, model_name: str, model_info: Any) -> Dict[str, Any]:
|
||||
"""Extract relevant data for a single model"""
|
||||
try:
|
||||
model_data = {
|
||||
'name': model_name,
|
||||
'status': 'unknown',
|
||||
'parameters': 0,
|
||||
'last_prediction': {},
|
||||
'training_enabled': True,
|
||||
'inference_enabled': True,
|
||||
'checkpoint_loaded': False,
|
||||
'loss_metrics': {},
|
||||
'timing_metrics': {}
|
||||
}
|
||||
|
||||
# Get model status from orchestrator - check if model is actually loaded and active
|
||||
if hasattr(self.orchestrator, 'get_model_state'):
|
||||
model_state = self.orchestrator.get_model_state(model_name)
|
||||
model_data['status'] = 'active' if model_state else 'inactive'
|
||||
|
||||
# Check actual inference activity from logs/statistics
|
||||
if hasattr(self.orchestrator, 'get_model_statistics'):
|
||||
stats = self.orchestrator.get_model_statistics()
|
||||
if stats and model_name in stats:
|
||||
model_stats = stats[model_name]
|
||||
# Check if model has recent activity (last prediction exists)
|
||||
if hasattr(model_stats, 'last_prediction') and model_stats.last_prediction:
|
||||
model_data['status'] = 'active'
|
||||
elif hasattr(model_stats, 'inferences_per_second') and getattr(model_stats, 'inferences_per_second', 0) > 0:
|
||||
model_data['status'] = 'active'
|
||||
else:
|
||||
model_data['status'] = 'registered' # Registered but not actively inferencing
|
||||
else:
|
||||
model_data['status'] = 'inactive'
|
||||
|
||||
# Check if model is in registry (fallback)
|
||||
if hasattr(self.orchestrator, 'model_registry') and self.orchestrator.model_registry:
|
||||
registered_models = self.orchestrator.model_registry.get_all_models()
|
||||
if model_name in registered_models and model_data['status'] == 'unknown':
|
||||
model_data['status'] = 'registered'
|
||||
|
||||
# Get toggle states
|
||||
if hasattr(self.orchestrator, 'get_model_toggle_state'):
|
||||
toggle_state = self.orchestrator.get_model_toggle_state(model_name)
|
||||
if isinstance(toggle_state, dict):
|
||||
model_data['training_enabled'] = toggle_state.get('training_enabled', True)
|
||||
model_data['inference_enabled'] = toggle_state.get('inference_enabled', True)
|
||||
|
||||
# Get model statistics
|
||||
if hasattr(self.orchestrator, 'get_model_statistics'):
|
||||
stats = self.orchestrator.get_model_statistics()
|
||||
if stats and model_name in stats:
|
||||
model_stats = stats[model_name]
|
||||
|
||||
# Handle both dict and object formats
|
||||
def safe_get(obj, key, default=None):
|
||||
if hasattr(obj, key):
|
||||
return getattr(obj, key, default)
|
||||
elif isinstance(obj, dict):
|
||||
return obj.get(key, default)
|
||||
else:
|
||||
return default
|
||||
|
||||
# Extract loss metrics
|
||||
model_data['loss_metrics'] = {
|
||||
'current_loss': safe_get(model_stats, 'current_loss'),
|
||||
'best_loss': safe_get(model_stats, 'best_loss'),
|
||||
'loss_5ma': safe_get(model_stats, 'loss_5ma'),
|
||||
'improvement': safe_get(model_stats, 'improvement', 0)
|
||||
}
|
||||
|
||||
# Extract timing metrics
|
||||
model_data['timing_metrics'] = {
|
||||
'last_inference': safe_get(model_stats, 'last_inference'),
|
||||
'last_training': safe_get(model_stats, 'last_training'),
|
||||
'inferences_per_second': safe_get(model_stats, 'inferences_per_second', 0),
|
||||
'predictions_24h': safe_get(model_stats, 'predictions_24h', 0)
|
||||
}
|
||||
|
||||
# Extract last prediction
|
||||
last_pred = safe_get(model_stats, 'last_prediction')
|
||||
if last_pred:
|
||||
model_data['last_prediction'] = {
|
||||
'action': safe_get(last_pred, 'action', 'NONE'),
|
||||
'confidence': safe_get(last_pred, 'confidence', 0),
|
||||
'timestamp': safe_get(last_pred, 'timestamp', 'N/A'),
|
||||
'predicted_price': safe_get(last_pred, 'predicted_price'),
|
||||
'price_change': safe_get(last_pred, 'price_change')
|
||||
}
|
||||
|
||||
# Extract model parameters count
|
||||
model_data['parameters'] = safe_get(model_stats, 'parameters', 0)
|
||||
|
||||
# Check checkpoint status from orchestrator model states (more reliable)
|
||||
checkpoint_loaded = False
|
||||
checkpoint_failed = False
|
||||
if hasattr(self.orchestrator, 'model_states'):
|
||||
model_state_mapping = {
|
||||
'dqn_agent': 'dqn',
|
||||
'enhanced_cnn': 'cnn',
|
||||
'cob_rl_model': 'cob_rl',
|
||||
'extrema_trainer': 'extrema_trainer'
|
||||
}
|
||||
state_key = model_state_mapping.get(model_name, model_name)
|
||||
if state_key in self.orchestrator.model_states:
|
||||
checkpoint_loaded = self.orchestrator.model_states[state_key].get('checkpoint_loaded', False)
|
||||
checkpoint_failed = self.orchestrator.model_states[state_key].get('checkpoint_failed', False)
|
||||
|
||||
# If not found in model states, check model stats as fallback
|
||||
if not checkpoint_loaded and not checkpoint_failed:
|
||||
checkpoint_loaded = safe_get(model_stats, 'checkpoint_loaded', False)
|
||||
|
||||
model_data['checkpoint_loaded'] = checkpoint_loaded
|
||||
model_data['checkpoint_failed'] = checkpoint_failed
|
||||
|
||||
# Extract signal generation statistics and real performance data
|
||||
model_data['signal_stats'] = {
|
||||
'buy_signals': safe_get(model_stats, 'buy_signals_count', 0),
|
||||
'sell_signals': safe_get(model_stats, 'sell_signals_count', 0),
|
||||
'hold_signals': safe_get(model_stats, 'hold_signals_count', 0),
|
||||
'total_signals': safe_get(model_stats, 'total_signals', 0),
|
||||
'accuracy': safe_get(model_stats, 'accuracy', 0),
|
||||
'win_rate': safe_get(model_stats, 'win_rate', 0)
|
||||
}
|
||||
|
||||
# Extract real performance metrics from logs
|
||||
# For DQN: we see "Performance: 81.9% (158/193)" in logs
|
||||
if model_name == 'dqn_agent':
|
||||
model_data['signal_stats']['accuracy'] = 81.9 # From logs
|
||||
model_data['signal_stats']['total_signals'] = 193 # From logs
|
||||
model_data['signal_stats']['correct_predictions'] = 158 # From logs
|
||||
elif model_name == 'enhanced_cnn':
|
||||
model_data['signal_stats']['accuracy'] = 65.3 # From logs
|
||||
model_data['signal_stats']['total_signals'] = 193 # From logs
|
||||
model_data['signal_stats']['correct_predictions'] = 126 # From logs
|
||||
|
||||
return model_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting data for model {model_name}: {e}")
|
||||
return {'name': model_name, 'status': 'error', 'error': str(e)}
|
||||
|
||||
def _extract_decision_fusion_data(self) -> Dict[str, Any]:
|
||||
"""Extract data for the decision fusion model"""
|
||||
try:
|
||||
decision_data = {
|
||||
'name': 'decision_fusion',
|
||||
'status': 'active',
|
||||
'parameters': 0,
|
||||
'last_prediction': {},
|
||||
'training_enabled': True,
|
||||
'inference_enabled': True,
|
||||
'checkpoint_loaded': False,
|
||||
'loss_metrics': {},
|
||||
'timing_metrics': {},
|
||||
'signal_stats': {}
|
||||
}
|
||||
|
||||
# Check if decision fusion is actually enabled and working
|
||||
if hasattr(self.orchestrator, 'decision_fusion_enabled'):
|
||||
decision_data['status'] = 'active' if self.orchestrator.decision_fusion_enabled else 'registered'
|
||||
|
||||
# Check if decision fusion network exists
|
||||
if hasattr(self.orchestrator, 'decision_fusion_network') and self.orchestrator.decision_fusion_network:
|
||||
decision_data['status'] = 'active'
|
||||
# Get network parameters
|
||||
if hasattr(self.orchestrator.decision_fusion_network, 'parameters'):
|
||||
decision_data['parameters'] = sum(p.numel() for p in self.orchestrator.decision_fusion_network.parameters())
|
||||
|
||||
# Check decision fusion mode
|
||||
if hasattr(self.orchestrator, 'decision_fusion_mode'):
|
||||
decision_data['mode'] = self.orchestrator.decision_fusion_mode
|
||||
if self.orchestrator.decision_fusion_mode == 'neural':
|
||||
decision_data['status'] = 'active'
|
||||
elif self.orchestrator.decision_fusion_mode == 'programmatic':
|
||||
decision_data['status'] = 'active' # Still active, just using programmatic mode
|
||||
|
||||
# Get decision fusion statistics
|
||||
if hasattr(self.orchestrator, 'get_decision_fusion_stats'):
|
||||
stats = self.orchestrator.get_decision_fusion_stats()
|
||||
if stats:
|
||||
decision_data['loss_metrics']['current_loss'] = stats.get('recent_loss')
|
||||
decision_data['timing_metrics']['decisions_per_second'] = stats.get('decisions_per_second', 0)
|
||||
decision_data['signal_stats'] = {
|
||||
'buy_decisions': stats.get('buy_decisions', 0),
|
||||
'sell_decisions': stats.get('sell_decisions', 0),
|
||||
'hold_decisions': stats.get('hold_decisions', 0),
|
||||
'total_decisions': stats.get('total_decisions', 0),
|
||||
'consensus_rate': stats.get('consensus_rate', 0)
|
||||
}
|
||||
|
||||
# Get decision fusion network parameters
|
||||
if hasattr(self.orchestrator, 'decision_fusion') and self.orchestrator.decision_fusion:
|
||||
if hasattr(self.orchestrator.decision_fusion, 'parameters'):
|
||||
decision_data['parameters'] = sum(p.numel() for p in self.orchestrator.decision_fusion.parameters())
|
||||
|
||||
# Check for decision fusion checkpoint status
|
||||
if hasattr(self.orchestrator, 'model_states') and 'decision_fusion' in self.orchestrator.model_states:
|
||||
df_state = self.orchestrator.model_states['decision_fusion']
|
||||
decision_data['checkpoint_loaded'] = df_state.get('checkpoint_loaded', False)
|
||||
|
||||
return decision_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting decision fusion data: {e}")
|
||||
return {'name': 'decision_fusion', 'status': 'error', 'error': str(e)}
|
||||
|
||||
def _extract_cob_rl_data(self) -> Dict[str, Any]:
|
||||
"""Extract data for the COB RL model"""
|
||||
try:
|
||||
cob_data = {
|
||||
'name': 'cob_rl_model',
|
||||
'status': 'registered', # Usually registered but not actively inferencing
|
||||
'parameters': 0,
|
||||
'last_prediction': {},
|
||||
'training_enabled': True,
|
||||
'inference_enabled': True,
|
||||
'checkpoint_loaded': False,
|
||||
'loss_metrics': {},
|
||||
'timing_metrics': {},
|
||||
'signal_stats': {}
|
||||
}
|
||||
|
||||
# Check if COB RL has actual statistics
|
||||
if hasattr(self.orchestrator, 'get_model_statistics'):
|
||||
stats = self.orchestrator.get_model_statistics()
|
||||
if stats and 'cob_rl_model' in stats:
|
||||
cob_stats = stats['cob_rl_model']
|
||||
# Use the safe_get function from above
|
||||
def safe_get(obj, key, default=None):
|
||||
if hasattr(obj, key):
|
||||
return getattr(obj, key, default)
|
||||
elif isinstance(obj, dict):
|
||||
return obj.get(key, default)
|
||||
else:
|
||||
return default
|
||||
|
||||
cob_data['parameters'] = safe_get(cob_stats, 'parameters', 356647429) # Known COB RL size
|
||||
cob_data['status'] = 'active' if safe_get(cob_stats, 'inferences_per_second', 0) > 0 else 'registered'
|
||||
|
||||
# Extract metrics if available
|
||||
cob_data['loss_metrics'] = {
|
||||
'current_loss': safe_get(cob_stats, 'current_loss'),
|
||||
'best_loss': safe_get(cob_stats, 'best_loss'),
|
||||
}
|
||||
|
||||
return cob_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting COB RL data: {e}")
|
||||
return {'name': 'cob_rl_model', 'status': 'error', 'error': str(e)}
|
||||
|
||||
def _extract_training_status(self) -> Dict[str, Any]:
|
||||
"""Extract overall training status"""
|
||||
try:
|
||||
status = {
|
||||
'active_sessions': 0,
|
||||
'total_training_steps': 0,
|
||||
'is_training': False,
|
||||
'last_update': 'N/A'
|
||||
}
|
||||
|
||||
# Check if enhanced training system is available
|
||||
if hasattr(self.orchestrator, 'enhanced_training') and self.orchestrator.enhanced_training:
|
||||
enhanced_stats = self.orchestrator.enhanced_training.get_training_statistics()
|
||||
if enhanced_stats:
|
||||
status.update({
|
||||
'is_training': enhanced_stats.get('is_training', False),
|
||||
'training_iteration': enhanced_stats.get('training_iteration', 0),
|
||||
'experience_buffer_size': enhanced_stats.get('experience_buffer_size', 0),
|
||||
'last_update': datetime.now().strftime('%H:%M:%S')
|
||||
})
|
||||
|
||||
return status
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting training status: {e}")
|
||||
return {'error': str(e)}
|
||||
|
||||
def _extract_performance_metrics(self) -> Dict[str, Any]:
|
||||
"""Extract performance metrics"""
|
||||
try:
|
||||
metrics = {
|
||||
'decision_fusion_active': False,
|
||||
'cob_integration_active': False,
|
||||
'symbols_tracking': 0,
|
||||
'recent_decisions': 0
|
||||
}
|
||||
|
||||
# Check decision fusion status
|
||||
if hasattr(self.orchestrator, 'decision_fusion_enabled'):
|
||||
metrics['decision_fusion_active'] = self.orchestrator.decision_fusion_enabled
|
||||
|
||||
# Check COB integration
|
||||
if hasattr(self.orchestrator, 'cob_integration') and self.orchestrator.cob_integration:
|
||||
metrics['cob_integration_active'] = True
|
||||
if hasattr(self.orchestrator.cob_integration, 'symbols'):
|
||||
metrics['symbols_tracking'] = len(self.orchestrator.cob_integration.symbols)
|
||||
|
||||
return metrics
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting performance metrics: {e}")
|
||||
return {'error': str(e)}
|
||||
|
||||
def _create_header(self) -> html.Div:
|
||||
"""Create the panel header with title and refresh button"""
|
||||
return html.Div([
|
||||
html.H6([
|
||||
html.I(className="fas fa-brain me-2 text-primary"),
|
||||
"Models & Training Progress"
|
||||
], className="mb-2"),
|
||||
html.Button([
|
||||
html.I(className="fas fa-sync-alt me-1"),
|
||||
"Refresh"
|
||||
], id="refresh-training-metrics-btn", className="btn btn-sm btn-outline-primary mb-2")
|
||||
], className="d-flex justify-content-between align-items-start")
|
||||
|
||||
def _create_models_section(self, models_data: Dict[str, Any]) -> html.Div:
|
||||
"""Create the models section showing each loaded model"""
|
||||
model_cards = []
|
||||
|
||||
for model_name, model_data in models_data.items():
|
||||
if model_data.get('error'):
|
||||
# Error card
|
||||
model_cards.append(html.Div([
|
||||
html.Strong(f"{model_name.upper()}", className="text-danger"),
|
||||
html.P(f"Error: {model_data['error']}", className="text-danger small mb-0")
|
||||
], className="border border-danger rounded p-2 mb-2"))
|
||||
else:
|
||||
model_cards.append(self._create_model_card(model_name, model_data))
|
||||
|
||||
return html.Div([
|
||||
html.H6([
|
||||
html.I(className="fas fa-microchip me-2 text-success"),
|
||||
f"Loaded Models ({len(models_data)})"
|
||||
], className="mb-2"),
|
||||
html.Div(model_cards)
|
||||
])
|
||||
|
||||
def _create_model_card(self, model_name: str, model_data: Dict[str, Any]) -> html.Div:
|
||||
"""Create a card for a single model"""
|
||||
# Status styling
|
||||
status = model_data.get('status', 'unknown')
|
||||
if status == 'active':
|
||||
status_class = "text-success"
|
||||
status_icon = "fas fa-check-circle"
|
||||
status_text = "ACTIVE"
|
||||
elif status == 'registered':
|
||||
status_class = "text-warning"
|
||||
status_icon = "fas fa-circle"
|
||||
status_text = "REGISTERED"
|
||||
elif status == 'inactive':
|
||||
status_class = "text-muted"
|
||||
status_icon = "fas fa-pause-circle"
|
||||
status_text = "INACTIVE"
|
||||
else:
|
||||
status_class = "text-danger"
|
||||
status_icon = "fas fa-exclamation-circle"
|
||||
status_text = "UNKNOWN"
|
||||
|
||||
# Model size formatting
|
||||
params = model_data.get('parameters', 0)
|
||||
if params > 1e9:
|
||||
size_str = f"{params/1e9:.1f}B"
|
||||
elif params > 1e6:
|
||||
size_str = f"{params/1e6:.1f}M"
|
||||
elif params > 1e3:
|
||||
size_str = f"{params/1e3:.1f}K"
|
||||
else:
|
||||
size_str = str(params)
|
||||
|
||||
# Last prediction info
|
||||
last_pred = model_data.get('last_prediction', {})
|
||||
pred_action = last_pred.get('action', 'NONE')
|
||||
pred_confidence = last_pred.get('confidence', 0)
|
||||
pred_time = last_pred.get('timestamp', 'N/A')
|
||||
|
||||
# Loss metrics
|
||||
loss_metrics = model_data.get('loss_metrics', {})
|
||||
current_loss = loss_metrics.get('current_loss')
|
||||
loss_class = "text-success" if current_loss and current_loss < 0.1 else "text-warning" if current_loss and current_loss < 0.5 else "text-danger"
|
||||
|
||||
# Timing metrics
|
||||
timing = model_data.get('timing_metrics', {})
|
||||
|
||||
return html.Div([
|
||||
# Header with model name and status
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.I(className=f"{status_icon} me-2 {status_class}"),
|
||||
html.Strong(f"{model_name.upper()}", className=status_class),
|
||||
html.Span(f" - {status_text}", className=f"{status_class} small ms-1"),
|
||||
html.Span(f" ({size_str})", className="text-muted small ms-2"),
|
||||
# Show mode for decision fusion
|
||||
*([html.Span(f" [{model_data.get('mode', 'unknown').upper()}]", className="text-info small ms-1")] if model_name == 'decision_fusion' and model_data.get('mode') else []),
|
||||
html.Span(
|
||||
" [CKPT]" if model_data.get('checkpoint_loaded')
|
||||
else " [FAILED]" if model_data.get('checkpoint_failed')
|
||||
else " [FRESH]",
|
||||
className=f"small {'text-success' if model_data.get('checkpoint_loaded') else 'text-danger' if model_data.get('checkpoint_failed') else 'text-warning'} ms-1"
|
||||
)
|
||||
], style={"flex": "1"}),
|
||||
|
||||
# Toggle switches with pattern matching IDs
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Label("Inf", className="text-muted small me-1", style={"font-size": "10px"}),
|
||||
dcc.Checklist(
|
||||
id={'type': 'model-toggle', 'model': model_name, 'toggle_type': 'inference'},
|
||||
options=[{"label": "", "value": True}],
|
||||
value=[True] if model_data.get('inference_enabled', True) else [],
|
||||
className="form-check-input me-2",
|
||||
style={"transform": "scale(0.7)"}
|
||||
)
|
||||
], className="d-flex align-items-center me-2"),
|
||||
html.Div([
|
||||
html.Label("Trn", className="text-muted small me-1", style={"font-size": "10px"}),
|
||||
dcc.Checklist(
|
||||
id={'type': 'model-toggle', 'model': model_name, 'toggle_type': 'training'},
|
||||
options=[{"label": "", "value": True}],
|
||||
value=[True] if model_data.get('training_enabled', True) else [],
|
||||
className="form-check-input",
|
||||
style={"transform": "scale(0.7)"}
|
||||
)
|
||||
], className="d-flex align-items-center")
|
||||
], className="d-flex")
|
||||
], className="d-flex align-items-center mb-2"),
|
||||
|
||||
# Model metrics
|
||||
html.Div([
|
||||
# Last prediction
|
||||
html.Div([
|
||||
html.Span("Last: ", className="text-muted small"),
|
||||
html.Span(f"{pred_action}",
|
||||
className=f"small fw-bold {'text-success' if pred_action == 'BUY' else 'text-danger' if pred_action == 'SELL' else 'text-warning'}"),
|
||||
html.Span(f" ({pred_confidence:.1f}%)", className="text-muted small"),
|
||||
html.Span(f" @ {pred_time}", className="text-muted small")
|
||||
], className="mb-1"),
|
||||
|
||||
# Loss information
|
||||
html.Div([
|
||||
html.Span("Loss: ", className="text-muted small"),
|
||||
html.Span(f"{current_loss:.4f}" if current_loss is not None else "N/A",
|
||||
className=f"small fw-bold {loss_class}"),
|
||||
*([
|
||||
html.Span(" | Best: ", className="text-muted small"),
|
||||
html.Span(f"{loss_metrics.get('best_loss', 0):.4f}", className="text-success small")
|
||||
] if loss_metrics.get('best_loss') is not None else [])
|
||||
], className="mb-1"),
|
||||
|
||||
# Timing information
|
||||
html.Div([
|
||||
html.Span("Rate: ", className="text-muted small"),
|
||||
html.Span(f"{timing.get('inferences_per_second', 0):.2f}/s", className="text-info small"),
|
||||
html.Span(" | 24h: ", className="text-muted small"),
|
||||
html.Span(f"{timing.get('predictions_24h', 0)}", className="text-primary small")
|
||||
], className="mb-1"),
|
||||
|
||||
# Last activity times
|
||||
html.Div([
|
||||
html.Span("Last Inf: ", className="text-muted small"),
|
||||
html.Span(f"{timing.get('last_inference', 'N/A')}", className="text-info small"),
|
||||
html.Span(" | Train: ", className="text-muted small"),
|
||||
html.Span(f"{timing.get('last_training', 'N/A')}", className="text-warning small")
|
||||
], className="mb-1"),
|
||||
|
||||
# Signal generation statistics
|
||||
*self._create_signal_stats_display(model_data.get('signal_stats', {})),
|
||||
|
||||
# Performance metrics
|
||||
*self._create_performance_metrics_display(model_data)
|
||||
])
|
||||
], className="border rounded p-2 mb-2",
|
||||
style={"backgroundColor": "rgba(255,255,255,0.05)" if status == 'active' else "rgba(128,128,128,0.1)"})
|
||||
|
||||
def _create_no_models_message(self) -> html.Div:
|
||||
"""Create message when no models are loaded"""
|
||||
return html.Div([
|
||||
html.H6([
|
||||
html.I(className="fas fa-exclamation-triangle me-2 text-warning"),
|
||||
"No Models Loaded"
|
||||
], className="mb-2"),
|
||||
html.P("No machine learning models are currently loaded. Check orchestrator status.",
|
||||
className="text-muted small")
|
||||
])
|
||||
|
||||
def _create_training_status_section(self, training_status: Dict[str, Any]) -> html.Div:
|
||||
"""Create the training status section"""
|
||||
if training_status.get('error'):
|
||||
return html.Div([
|
||||
html.Hr(),
|
||||
html.H6([
|
||||
html.I(className="fas fa-exclamation-triangle me-2 text-danger"),
|
||||
"Training Status Error"
|
||||
], className="mb-2"),
|
||||
html.P(f"Error: {training_status['error']}", className="text-danger small")
|
||||
])
|
||||
|
||||
is_training = training_status.get('is_training', False)
|
||||
|
||||
return html.Div([
|
||||
html.Hr(),
|
||||
html.H6([
|
||||
html.I(className="fas fa-brain me-2 text-secondary"),
|
||||
"Training Status"
|
||||
], className="mb-2"),
|
||||
|
||||
html.Div([
|
||||
html.Span("Status: ", className="text-muted small"),
|
||||
html.Span("ACTIVE" if is_training else "INACTIVE",
|
||||
className=f"small fw-bold {'text-success' if is_training else 'text-warning'}"),
|
||||
html.Span(f" | Iteration: {training_status.get('training_iteration', 0):,}",
|
||||
className="text-info small ms-2")
|
||||
], className="mb-1"),
|
||||
|
||||
html.Div([
|
||||
html.Span("Buffer: ", className="text-muted small"),
|
||||
html.Span(f"{training_status.get('experience_buffer_size', 0):,}",
|
||||
className="text-success small"),
|
||||
html.Span(" | Updated: ", className="text-muted small"),
|
||||
html.Span(f"{training_status.get('last_update', 'N/A')}",
|
||||
className="text-muted small")
|
||||
], className="mb-0")
|
||||
])
|
||||
|
||||
def _create_performance_section(self, performance_metrics: Dict[str, Any]) -> html.Div:
|
||||
"""Create the performance metrics section"""
|
||||
if performance_metrics.get('error'):
|
||||
return html.Div([
|
||||
html.Hr(),
|
||||
html.P(f"Performance metrics error: {performance_metrics['error']}",
|
||||
className="text-danger small")
|
||||
])
|
||||
|
||||
return html.Div([
|
||||
html.Hr(),
|
||||
html.H6([
|
||||
html.I(className="fas fa-chart-line me-2 text-primary"),
|
||||
"System Performance"
|
||||
], className="mb-2"),
|
||||
|
||||
html.Div([
|
||||
html.Span("Decision Fusion: ", className="text-muted small"),
|
||||
html.Span("ON" if performance_metrics.get('decision_fusion_active') else "OFF",
|
||||
className=f"small {'text-success' if performance_metrics.get('decision_fusion_active') else 'text-muted'}"),
|
||||
html.Span(" | COB: ", className="text-muted small"),
|
||||
html.Span("ON" if performance_metrics.get('cob_integration_active') else "OFF",
|
||||
className=f"small {'text-success' if performance_metrics.get('cob_integration_active') else 'text-muted'}")
|
||||
], className="mb-1"),
|
||||
|
||||
html.Div([
|
||||
html.Span("Tracking: ", className="text-muted small"),
|
||||
html.Span(f"{performance_metrics.get('symbols_tracking', 0)} symbols",
|
||||
className="text-info small"),
|
||||
html.Span(" | Decisions: ", className="text-muted small"),
|
||||
html.Span(f"{performance_metrics.get('recent_decisions', 0):,}",
|
||||
className="text-primary small")
|
||||
], className="mb-0")
|
||||
])
|
||||
|
||||
def _create_signal_stats_display(self, signal_stats: Dict[str, Any]) -> List[html.Div]:
|
||||
"""Create display elements for signal generation statistics"""
|
||||
if not signal_stats or not any(signal_stats.values()):
|
||||
return []
|
||||
|
||||
buy_signals = signal_stats.get('buy_signals', 0)
|
||||
sell_signals = signal_stats.get('sell_signals', 0)
|
||||
hold_signals = signal_stats.get('hold_signals', 0)
|
||||
total_signals = signal_stats.get('total_signals', 0)
|
||||
|
||||
if total_signals == 0:
|
||||
return []
|
||||
|
||||
# Calculate percentages - ensure all values are numeric
|
||||
buy_signals = buy_signals or 0
|
||||
sell_signals = sell_signals or 0
|
||||
hold_signals = hold_signals or 0
|
||||
total_signals = total_signals or 0
|
||||
|
||||
buy_pct = (buy_signals / total_signals * 100) if total_signals > 0 else 0
|
||||
sell_pct = (sell_signals / total_signals * 100) if total_signals > 0 else 0
|
||||
hold_pct = (hold_signals / total_signals * 100) if total_signals > 0 else 0
|
||||
|
||||
return [
|
||||
html.Div([
|
||||
html.Span("Signals: ", className="text-muted small"),
|
||||
html.Span(f"B:{buy_signals}({buy_pct:.0f}%)", className="text-success small"),
|
||||
html.Span(" | ", className="text-muted small"),
|
||||
html.Span(f"S:{sell_signals}({sell_pct:.0f}%)", className="text-danger small"),
|
||||
html.Span(" | ", className="text-muted small"),
|
||||
html.Span(f"H:{hold_signals}({hold_pct:.0f}%)", className="text-warning small")
|
||||
], className="mb-1"),
|
||||
|
||||
html.Div([
|
||||
html.Span("Total: ", className="text-muted small"),
|
||||
html.Span(f"{total_signals:,}", className="text-primary small fw-bold"),
|
||||
*([
|
||||
html.Span(" | Accuracy: ", className="text-muted small"),
|
||||
html.Span(f"{signal_stats.get('accuracy', 0):.1f}%",
|
||||
className=f"small fw-bold {'text-success' if signal_stats.get('accuracy', 0) > 60 else 'text-warning' if signal_stats.get('accuracy', 0) > 40 else 'text-danger'}")
|
||||
] if signal_stats.get('accuracy', 0) > 0 else [])
|
||||
], className="mb-1")
|
||||
]
|
||||
|
||||
def _create_performance_metrics_display(self, model_data: Dict[str, Any]) -> List[html.Div]:
|
||||
"""Create display elements for performance metrics"""
|
||||
elements = []
|
||||
|
||||
# Win rate and accuracy
|
||||
signal_stats = model_data.get('signal_stats', {})
|
||||
loss_metrics = model_data.get('loss_metrics', {})
|
||||
|
||||
# Safely get numeric values
|
||||
win_rate = signal_stats.get('win_rate', 0) or 0
|
||||
accuracy = signal_stats.get('accuracy', 0) or 0
|
||||
|
||||
if win_rate > 0 or accuracy > 0:
|
||||
|
||||
elements.append(html.Div([
|
||||
html.Span("Performance: ", className="text-muted small"),
|
||||
*([
|
||||
html.Span(f"Win: {win_rate:.1f}%",
|
||||
className=f"small fw-bold {'text-success' if win_rate > 55 else 'text-warning' if win_rate > 45 else 'text-danger'}"),
|
||||
html.Span(" | ", className="text-muted small")
|
||||
] if win_rate > 0 else []),
|
||||
*([
|
||||
html.Span(f"Acc: {accuracy:.1f}%",
|
||||
className=f"small fw-bold {'text-success' if accuracy > 60 else 'text-warning' if accuracy > 40 else 'text-danger'}")
|
||||
] if accuracy > 0 else [])
|
||||
], className="mb-1"))
|
||||
|
||||
# Loss improvement
|
||||
if loss_metrics.get('improvement', 0) != 0:
|
||||
improvement = loss_metrics.get('improvement', 0)
|
||||
elements.append(html.Div([
|
||||
html.Span("Improvement: ", className="text-muted small"),
|
||||
html.Span(f"{improvement:+.1f}%",
|
||||
className=f"small fw-bold {'text-success' if improvement > 0 else 'text-danger'}")
|
||||
], className="mb-1"))
|
||||
|
||||
return elements
|
@ -1,384 +0,0 @@
|
||||
"""
|
||||
Template Renderer for Dashboard
|
||||
Handles HTML template rendering with Jinja2
|
||||
"""
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from dash import html, dcc
|
||||
import plotly.graph_objects as go
|
||||
|
||||
from .dashboard_model import DashboardModel, DashboardDataBuilder
|
||||
|
||||
|
||||
class DashboardTemplateRenderer:
|
||||
"""Renders dashboard templates using Jinja2"""
|
||||
|
||||
def __init__(self, template_dir: str = "web/templates"):
|
||||
"""Initialize the template renderer"""
|
||||
self.template_dir = template_dir
|
||||
|
||||
# Create Jinja2 environment
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader(template_dir),
|
||||
autoescape=select_autoescape(['html', 'xml'])
|
||||
)
|
||||
|
||||
# Add custom filters
|
||||
self.env.filters['currency'] = self._currency_filter
|
||||
self.env.filters['percentage'] = self._percentage_filter
|
||||
self.env.filters['number'] = self._number_filter
|
||||
|
||||
def render_dashboard(self, model: DashboardModel) -> html.Div:
|
||||
"""Render the complete dashboard using the template"""
|
||||
try:
|
||||
# Convert model to dict for template
|
||||
template_data = self._model_to_dict(model)
|
||||
|
||||
# Render template
|
||||
template = self.env.get_template('dashboard.html')
|
||||
rendered_html = template.render(**template_data)
|
||||
|
||||
# Convert to Dash components
|
||||
return self._convert_to_dash_components(model)
|
||||
|
||||
except Exception as e:
|
||||
# Fallback to basic layout if template fails
|
||||
return self._create_fallback_layout(str(e))
|
||||
|
||||
def _model_to_dict(self, model: DashboardModel) -> Dict[str, Any]:
|
||||
"""Convert dashboard model to dictionary for template rendering"""
|
||||
return {
|
||||
'title': model.title,
|
||||
'subtitle': model.subtitle,
|
||||
'refresh_interval': model.refresh_interval,
|
||||
'metrics': [self._dataclass_to_dict(m) for m in model.metrics],
|
||||
'chart': self._dataclass_to_dict(model.chart),
|
||||
'trading_controls': self._dataclass_to_dict(model.trading_controls),
|
||||
'recent_decisions': [self._dataclass_to_dict(d) for d in model.recent_decisions],
|
||||
'cob_data': [self._dataclass_to_dict(c) for c in model.cob_data],
|
||||
'models': [self._dataclass_to_dict(m) for m in model.models],
|
||||
'training_metrics': [self._dataclass_to_dict(m) for m in model.training_metrics],
|
||||
'performance_stats': [self._dataclass_to_dict(s) for s in model.performance_stats],
|
||||
'closed_trades': [self._dataclass_to_dict(t) for t in model.closed_trades]
|
||||
}
|
||||
|
||||
def _dataclass_to_dict(self, obj) -> Dict[str, Any]:
|
||||
"""Convert dataclass to dictionary"""
|
||||
if hasattr(obj, '__dict__'):
|
||||
result = {}
|
||||
for key, value in obj.__dict__.items():
|
||||
if hasattr(value, '__dict__'):
|
||||
result[key] = self._dataclass_to_dict(value)
|
||||
elif isinstance(value, list):
|
||||
result[key] = [self._dataclass_to_dict(item) if hasattr(item, '__dict__') else item for item in value]
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
return obj
|
||||
|
||||
def _convert_to_dash_components(self, model: DashboardModel) -> html.Div:
|
||||
"""Convert template model to Dash components"""
|
||||
return html.Div([
|
||||
# Header
|
||||
html.Div([
|
||||
html.H1(model.title, className="text-center"),
|
||||
html.P(model.subtitle, className="text-center text-muted")
|
||||
], className="row mb-3"),
|
||||
|
||||
# Metrics Row
|
||||
html.Div([
|
||||
html.Div([
|
||||
self._create_metric_card(metric)
|
||||
], className="col-md-2") for metric in model.metrics
|
||||
], className="row mb-3"),
|
||||
|
||||
# Main Content Row
|
||||
html.Div([
|
||||
# Price Chart
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.H5(model.chart.title)
|
||||
], className="card-header"),
|
||||
html.Div([
|
||||
dcc.Graph(id="price-chart", style={"height": "500px"})
|
||||
], className="card-body")
|
||||
], className="card")
|
||||
], className="col-md-8"),
|
||||
|
||||
# Trading Controls & Recent Decisions
|
||||
html.Div([
|
||||
# Trading Controls
|
||||
self._create_trading_controls(model.trading_controls),
|
||||
# Recent Decisions
|
||||
self._create_recent_decisions(model.recent_decisions)
|
||||
], className="col-md-4")
|
||||
], className="row mb-3"),
|
||||
|
||||
# COB Data and Models Row
|
||||
html.Div([
|
||||
# COB Ladders
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Div([
|
||||
self._create_cob_card(cob)
|
||||
], className="col-md-6") for cob in model.cob_data
|
||||
], className="row")
|
||||
], className="col-md-7"),
|
||||
|
||||
# Models & Training
|
||||
html.Div([
|
||||
self._create_training_panel(model)
|
||||
], className="col-md-5")
|
||||
], className="row mb-3"),
|
||||
|
||||
# Closed Trades Row
|
||||
html.Div([
|
||||
html.Div([
|
||||
self._create_closed_trades_table(model.closed_trades)
|
||||
], className="col-12")
|
||||
], className="row"),
|
||||
|
||||
# Auto-refresh interval
|
||||
dcc.Interval(id='interval-component', interval=model.refresh_interval, n_intervals=0)
|
||||
|
||||
], className="container-fluid")
|
||||
|
||||
def _create_metric_card(self, metric) -> html.Div:
|
||||
"""Create a metric card component"""
|
||||
return html.Div([
|
||||
html.Div(metric.value, className="metric-value", id=metric.id),
|
||||
html.Div(metric.label, className="metric-label")
|
||||
], className="metric-card")
|
||||
|
||||
def _create_trading_controls(self, controls) -> html.Div:
|
||||
"""Create trading controls component"""
|
||||
return html.Div([
|
||||
html.Div([
|
||||
html.H6("Manual Trading")
|
||||
], className="card-header"),
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Button(controls.buy_text, id="manual-buy-btn",
|
||||
className="btn btn-success w-100")
|
||||
], className="col-6"),
|
||||
html.Div([
|
||||
html.Button(controls.sell_text, id="manual-sell-btn",
|
||||
className="btn btn-danger w-100")
|
||||
], className="col-6")
|
||||
], className="row mb-2"),
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Label([
|
||||
f"Leverage: ",
|
||||
html.Span(f"{controls.leverage}x", id="leverage-display")
|
||||
], className="form-label"),
|
||||
dcc.Slider(
|
||||
id="leverage-slider",
|
||||
min=controls.leverage_min,
|
||||
max=controls.leverage_max,
|
||||
value=controls.leverage,
|
||||
step=1,
|
||||
marks={i: str(i) for i in range(controls.leverage_min, controls.leverage_max + 1, 10)}
|
||||
)
|
||||
], className="col-12")
|
||||
], className="row mb-2"),
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Button(controls.clear_text, id="clear-session-btn",
|
||||
className="btn btn-warning w-100")
|
||||
], className="col-12")
|
||||
], className="row")
|
||||
], className="card-body")
|
||||
], className="card mb-3")
|
||||
|
||||
def _create_recent_decisions(self, decisions) -> html.Div:
|
||||
"""Create recent decisions component"""
|
||||
decision_items = []
|
||||
for decision in decisions:
|
||||
border_class = {
|
||||
'BUY': 'border-success bg-success bg-opacity-10',
|
||||
'SELL': 'border-danger bg-danger bg-opacity-10'
|
||||
}.get(decision.action, 'border-secondary bg-secondary bg-opacity-10')
|
||||
|
||||
decision_items.append(
|
||||
html.Div([
|
||||
html.Small(decision.timestamp, className="text-muted"),
|
||||
html.Br(),
|
||||
html.Strong(f"{decision.action} - {decision.symbol}"),
|
||||
html.Br(),
|
||||
html.Small(f"Confidence: {decision.confidence}% | Price: ${decision.price}")
|
||||
], className=f"mb-2 p-2 border-start border-3 {border_class}")
|
||||
)
|
||||
|
||||
return html.Div([
|
||||
html.Div([
|
||||
html.H6("Recent AI Decisions")
|
||||
], className="card-header"),
|
||||
html.Div([
|
||||
html.Div(decision_items, id="recent-decisions")
|
||||
], className="card-body", style={"max-height": "300px", "overflow-y": "auto"})
|
||||
], className="card")
|
||||
|
||||
def _create_cob_card(self, cob) -> html.Div:
|
||||
"""Create COB ladder card"""
|
||||
return html.Div([
|
||||
html.Div([
|
||||
html.H6(f"{cob.symbol} Order Book"),
|
||||
html.Small(f"Total: {cob.total_usd} USD | {cob.total_crypto} {cob.symbol.split('/')[0]}",
|
||||
className="text-muted")
|
||||
], className="card-header"),
|
||||
html.Div([
|
||||
html.Div(id=cob.content_id, className="cob-ladder")
|
||||
], className="card-body p-2")
|
||||
], className="card")
|
||||
|
||||
def _create_training_panel(self, model: DashboardModel) -> html.Div:
|
||||
"""Create training panel component"""
|
||||
# Model status indicators
|
||||
model_status_items = []
|
||||
for model_item in model.models:
|
||||
status_class = f"status-{model_item.status}"
|
||||
model_status_items.append(
|
||||
html.Span(f"{model_item.name}: {model_item.status_text}",
|
||||
className=f"model-status {status_class}")
|
||||
)
|
||||
|
||||
# Training metrics
|
||||
training_items = []
|
||||
for metric in model.training_metrics:
|
||||
training_items.append(
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Small(f"{metric.name}:")
|
||||
], className="col-6"),
|
||||
html.Div([
|
||||
html.Small(metric.value, className="fw-bold")
|
||||
], className="col-6")
|
||||
], className="row mb-1")
|
||||
)
|
||||
|
||||
# Performance stats
|
||||
performance_items = []
|
||||
for stat in model.performance_stats:
|
||||
performance_items.append(
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Small(f"{stat.name}:")
|
||||
], className="col-8"),
|
||||
html.Div([
|
||||
html.Small(stat.value, className="fw-bold")
|
||||
], className="col-4")
|
||||
], className="row mb-1")
|
||||
)
|
||||
|
||||
return html.Div([
|
||||
html.Div([
|
||||
html.H6("Models & Training Progress")
|
||||
], className="card-header"),
|
||||
html.Div([
|
||||
html.Div([
|
||||
# Model Status
|
||||
html.Div([
|
||||
html.H6("Model Status"),
|
||||
html.Div(model_status_items)
|
||||
], className="mb-3"),
|
||||
|
||||
# Training Metrics
|
||||
html.Div([
|
||||
html.H6("Training Metrics"),
|
||||
html.Div(training_items, id="training-metrics")
|
||||
], className="mb-3"),
|
||||
|
||||
# Performance Stats
|
||||
html.Div([
|
||||
html.H6("Performance"),
|
||||
html.Div(performance_items)
|
||||
], className="mb-3")
|
||||
])
|
||||
], className="card-body training-panel")
|
||||
], className="card")
|
||||
|
||||
def _create_closed_trades_table(self, trades) -> html.Div:
|
||||
"""Create closed trades table"""
|
||||
trade_rows = []
|
||||
for trade in trades:
|
||||
pnl_class = "trade-profit" if trade.pnl > 0 else "trade-loss"
|
||||
side_class = "bg-success" if trade.side == "BUY" else "bg-danger"
|
||||
|
||||
trade_rows.append(
|
||||
html.Tr([
|
||||
html.Td(trade.time),
|
||||
html.Td(trade.symbol),
|
||||
html.Td([
|
||||
html.Span(trade.side, className=f"badge {side_class}")
|
||||
]),
|
||||
html.Td(trade.size),
|
||||
html.Td(trade.entry_price),
|
||||
html.Td(trade.exit_price),
|
||||
html.Td(f"${trade.pnl}", className=pnl_class),
|
||||
html.Td(trade.duration)
|
||||
])
|
||||
)
|
||||
|
||||
return html.Div([
|
||||
html.Div([
|
||||
html.H6("Recent Closed Trades")
|
||||
], className="card-header"),
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Table([
|
||||
html.Thead([
|
||||
html.Tr([
|
||||
html.Th("Time"),
|
||||
html.Th("Symbol"),
|
||||
html.Th("Side"),
|
||||
html.Th("Size"),
|
||||
html.Th("Entry"),
|
||||
html.Th("Exit"),
|
||||
html.Th("PnL"),
|
||||
html.Th("Duration")
|
||||
])
|
||||
]),
|
||||
html.Tbody(trade_rows)
|
||||
], className="table table-sm", id="closed-trades-table")
|
||||
])
|
||||
], className="card-body closed-trades")
|
||||
], className="card")
|
||||
|
||||
def _create_fallback_layout(self, error_msg: str) -> html.Div:
|
||||
"""Create fallback layout if template rendering fails"""
|
||||
return html.Div([
|
||||
html.Div([
|
||||
html.H1("Dashboard Error", className="text-center text-danger"),
|
||||
html.P(f"Template rendering failed: {error_msg}", className="text-center"),
|
||||
html.P("Using fallback layout.", className="text-center text-muted")
|
||||
], className="container mt-5")
|
||||
])
|
||||
|
||||
# Jinja2 custom filters
|
||||
def _currency_filter(self, value) -> str:
|
||||
"""Format value as currency"""
|
||||
try:
|
||||
return f"${float(value):,.4f}"
|
||||
except (ValueError, TypeError):
|
||||
return str(value)
|
||||
|
||||
def _percentage_filter(self, value) -> str:
|
||||
"""Format value as percentage"""
|
||||
try:
|
||||
return f"{float(value):.2f}%"
|
||||
except (ValueError, TypeError):
|
||||
return str(value)
|
||||
|
||||
def _number_filter(self, value) -> str:
|
||||
"""Format value as number"""
|
||||
try:
|
||||
if isinstance(value, int):
|
||||
return f"{value:,}"
|
||||
else:
|
||||
return f"{float(value):,.2f}"
|
||||
except (ValueError, TypeError):
|
||||
return str(value)
|
File diff suppressed because it is too large
Load Diff
@ -1,173 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TensorBoard Component for Dashboard
|
||||
|
||||
This module provides a Dash component that embeds TensorBoard in the dashboard.
|
||||
"""
|
||||
|
||||
import dash
|
||||
from dash import html, dcc
|
||||
import dash_bootstrap_components as dbc
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def create_tensorboard_tab(tensorboard_url: str = "http://localhost:6006") -> html.Div:
|
||||
"""
|
||||
Create a dashboard tab that embeds TensorBoard
|
||||
|
||||
Args:
|
||||
tensorboard_url: URL of the TensorBoard server
|
||||
|
||||
Returns:
|
||||
html.Div: Dash component containing TensorBoard iframe
|
||||
"""
|
||||
return html.Div([
|
||||
dbc.Alert([
|
||||
html.I(className="fas fa-chart-line me-2"),
|
||||
"TensorBoard Training Visualization",
|
||||
html.A(
|
||||
"Open in New Window",
|
||||
href=tensorboard_url,
|
||||
target="_blank",
|
||||
className="ms-2 btn btn-sm btn-primary"
|
||||
)
|
||||
], color="info", className="mb-3"),
|
||||
|
||||
# TensorBoard iframe
|
||||
html.Iframe(
|
||||
src=tensorboard_url,
|
||||
style={
|
||||
'width': '100%',
|
||||
'height': '800px',
|
||||
'border': 'none'
|
||||
}
|
||||
),
|
||||
|
||||
# Training metrics summary
|
||||
html.Div([
|
||||
html.H5("Training Metrics Summary", className="mt-3"),
|
||||
html.Div(id="training-metrics-summary", className="mt-2")
|
||||
], className="mt-3")
|
||||
])
|
||||
|
||||
def create_training_metrics_card() -> dbc.Card:
|
||||
"""
|
||||
Create a card displaying key training metrics
|
||||
|
||||
Returns:
|
||||
dbc.Card: Dash Bootstrap card component
|
||||
"""
|
||||
return dbc.Card([
|
||||
dbc.CardHeader([
|
||||
html.I(className="fas fa-brain me-2"),
|
||||
"Training Metrics"
|
||||
]),
|
||||
dbc.CardBody([
|
||||
dbc.Row([
|
||||
dbc.Col([
|
||||
html.H6("Model Status"),
|
||||
html.Div(id="model-training-status", children="Initializing...")
|
||||
], width=6),
|
||||
dbc.Col([
|
||||
html.H6("Training Progress"),
|
||||
dbc.Progress(id="training-progress-bar", value=0, className="mb-2"),
|
||||
html.Div(id="training-progress-text", children="0%")
|
||||
], width=6)
|
||||
], className="mb-3"),
|
||||
|
||||
dbc.Row([
|
||||
dbc.Col([
|
||||
html.H6("Loss"),
|
||||
html.Div(id="training-loss-value", children="N/A")
|
||||
], width=4),
|
||||
dbc.Col([
|
||||
html.H6("Reward"),
|
||||
html.Div(id="training-reward-value", children="N/A")
|
||||
], width=4),
|
||||
dbc.Col([
|
||||
html.H6("State Quality"),
|
||||
html.Div(id="training-state-quality", children="N/A")
|
||||
], width=4)
|
||||
], className="mb-3"),
|
||||
|
||||
dbc.Row([
|
||||
dbc.Col([
|
||||
html.A(
|
||||
dbc.Button([
|
||||
html.I(className="fas fa-chart-line me-2"),
|
||||
"Open TensorBoard"
|
||||
], color="primary", size="sm", className="w-100"),
|
||||
href="http://localhost:6006",
|
||||
target="_blank"
|
||||
)
|
||||
], width=12)
|
||||
])
|
||||
])
|
||||
], className="mb-3")
|
||||
|
||||
def create_tensorboard_status_indicator(tensorboard_url: str = "http://localhost:6006") -> html.Div:
|
||||
"""
|
||||
Create a status indicator for TensorBoard
|
||||
|
||||
Args:
|
||||
tensorboard_url: URL of the TensorBoard server
|
||||
|
||||
Returns:
|
||||
html.Div: Dash component showing TensorBoard status
|
||||
"""
|
||||
return html.Div([
|
||||
dbc.Button([
|
||||
html.I(className="fas fa-chart-line me-2"),
|
||||
"TensorBoard"
|
||||
],
|
||||
id="tensorboard-status-button",
|
||||
color="success",
|
||||
size="sm",
|
||||
href=tensorboard_url,
|
||||
target="_blank",
|
||||
external_link=True,
|
||||
className="ms-2")
|
||||
], id="tensorboard-status-container")
|
||||
|
||||
def update_training_metrics_card(metrics: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Update training metrics card with latest data
|
||||
|
||||
Args:
|
||||
metrics: Dictionary of training metrics
|
||||
|
||||
Returns:
|
||||
Dict: Dictionary of Dash component updates
|
||||
"""
|
||||
# Extract metrics
|
||||
training_active = metrics.get("training_active", False)
|
||||
loss = metrics.get("loss", None)
|
||||
reward = metrics.get("reward", None)
|
||||
state_quality = metrics.get("state_quality", None)
|
||||
progress = metrics.get("progress", 0)
|
||||
|
||||
# Format values
|
||||
loss_str = f"{loss:.4f}" if loss is not None else "N/A"
|
||||
reward_str = f"{reward:.4f}" if reward is not None else "N/A"
|
||||
state_quality_str = f"{state_quality:.1%}" if state_quality is not None else "N/A"
|
||||
progress_str = f"{progress:.1%}"
|
||||
|
||||
# Determine status
|
||||
if training_active:
|
||||
status = "Training Active"
|
||||
status_class = "text-success"
|
||||
else:
|
||||
status = "Training Inactive"
|
||||
status_class = "text-warning"
|
||||
|
||||
# Return updates
|
||||
return {
|
||||
"model-training-status": html.Span(status, className=status_class),
|
||||
"training-progress-bar": progress * 100,
|
||||
"training-progress-text": progress_str,
|
||||
"training-loss-value": loss_str,
|
||||
"training-reward-value": reward_str,
|
||||
"training-state-quality": state_quality_str
|
||||
}
|
@ -1,203 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TensorBoard Integration for Dashboard
|
||||
|
||||
This module provides integration between the trading dashboard and TensorBoard,
|
||||
allowing training metrics to be visualized in real-time.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class TensorBoardIntegration:
|
||||
"""
|
||||
TensorBoard integration for dashboard
|
||||
|
||||
Provides methods to start TensorBoard server and access training metrics
|
||||
"""
|
||||
|
||||
def __init__(self, log_dir: str = "runs", port: int = 6006):
|
||||
"""
|
||||
Initialize TensorBoard integration
|
||||
|
||||
Args:
|
||||
log_dir: Directory containing TensorBoard logs
|
||||
port: Port to run TensorBoard on
|
||||
"""
|
||||
self.log_dir = log_dir
|
||||
self.port = port
|
||||
self.process = None
|
||||
self.url = f"http://localhost:{port}"
|
||||
self.is_running = False
|
||||
self.latest_metrics = {}
|
||||
|
||||
# Create log directory if it doesn't exist
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
def start_tensorboard(self, open_browser: bool = False) -> bool:
|
||||
"""
|
||||
Start TensorBoard server in a separate process
|
||||
|
||||
Args:
|
||||
open_browser: Whether to open browser automatically
|
||||
|
||||
Returns:
|
||||
bool: True if TensorBoard was started successfully
|
||||
"""
|
||||
if self.is_running:
|
||||
logger.info("TensorBoard is already running")
|
||||
return True
|
||||
|
||||
try:
|
||||
# Check if TensorBoard is available
|
||||
try:
|
||||
import tensorboard
|
||||
logger.info(f"TensorBoard version {tensorboard.__version__} available")
|
||||
except ImportError:
|
||||
logger.warning("TensorBoard not installed. Install with: pip install tensorboard")
|
||||
return False
|
||||
|
||||
# Check if log directory exists and has content
|
||||
log_dir_path = Path(self.log_dir)
|
||||
if not log_dir_path.exists():
|
||||
logger.warning(f"Log directory {self.log_dir} does not exist")
|
||||
os.makedirs(self.log_dir, exist_ok=True)
|
||||
logger.info(f"Created log directory {self.log_dir}")
|
||||
|
||||
# Start TensorBoard process
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"tensorboard.main",
|
||||
"--logdir", self.log_dir,
|
||||
"--port", str(self.port),
|
||||
"--reload_interval", "5", # Reload data every 5 seconds
|
||||
"--reload_multifile", "true" # Better handling of multiple log files
|
||||
]
|
||||
|
||||
logger.info(f"Starting TensorBoard: {' '.join(cmd)}")
|
||||
|
||||
# Start process without capturing output
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Wait a moment for TensorBoard to start
|
||||
time.sleep(2)
|
||||
|
||||
# Check if process is running
|
||||
if self.process.poll() is None:
|
||||
self.is_running = True
|
||||
logger.info(f"TensorBoard started at {self.url}")
|
||||
|
||||
# Open browser if requested
|
||||
if open_browser:
|
||||
try:
|
||||
webbrowser.open(self.url)
|
||||
logger.info("Browser opened automatically")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not open browser: {e}")
|
||||
|
||||
# Start monitoring thread
|
||||
threading.Thread(target=self._monitor_process, daemon=True).start()
|
||||
|
||||
return True
|
||||
else:
|
||||
stdout, stderr = self.process.communicate()
|
||||
logger.error(f"TensorBoard failed to start: {stderr}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting TensorBoard: {e}")
|
||||
return False
|
||||
|
||||
def _monitor_process(self):
|
||||
"""Monitor TensorBoard process and capture output"""
|
||||
try:
|
||||
while self.process and self.process.poll() is None:
|
||||
# Read output line by line
|
||||
for line in iter(self.process.stdout.readline, ''):
|
||||
if line:
|
||||
line = line.strip()
|
||||
if line:
|
||||
logger.debug(f"TensorBoard: {line}")
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
# Process has ended
|
||||
self.is_running = False
|
||||
logger.info("TensorBoard process has ended")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error monitoring TensorBoard process: {e}")
|
||||
|
||||
def stop_tensorboard(self):
|
||||
"""Stop TensorBoard server"""
|
||||
if self.process and self.process.poll() is None:
|
||||
try:
|
||||
self.process.terminate()
|
||||
self.process.wait(timeout=5)
|
||||
logger.info("TensorBoard stopped")
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
logger.warning("TensorBoard process killed after timeout")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping TensorBoard: {e}")
|
||||
|
||||
self.is_running = False
|
||||
|
||||
def get_tensorboard_url(self) -> str:
|
||||
"""Get TensorBoard URL"""
|
||||
return self.url
|
||||
|
||||
def is_tensorboard_running(self) -> bool:
|
||||
"""Check if TensorBoard is running"""
|
||||
if self.process:
|
||||
return self.process.poll() is None
|
||||
return False
|
||||
|
||||
def get_latest_metrics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get latest training metrics from TensorBoard
|
||||
|
||||
This is a placeholder - in a real implementation, you would
|
||||
parse TensorBoard event files to extract metrics
|
||||
"""
|
||||
# In a real implementation, you would parse TensorBoard event files
|
||||
# For now, return placeholder data
|
||||
return {
|
||||
"training_active": self.is_running,
|
||||
"tensorboard_url": self.url,
|
||||
"metrics_available": self.is_running
|
||||
}
|
||||
|
||||
# Singleton instance
|
||||
_tensorboard_integration = None
|
||||
|
||||
def get_tensorboard_integration(log_dir: str = "runs", port: int = 6006) -> TensorBoardIntegration:
|
||||
"""
|
||||
Get TensorBoard integration singleton instance
|
||||
|
||||
Args:
|
||||
log_dir: Directory containing TensorBoard logs
|
||||
port: Port to run TensorBoard on
|
||||
|
||||
Returns:
|
||||
TensorBoardIntegration: Singleton instance
|
||||
"""
|
||||
global _tensorboard_integration
|
||||
if _tensorboard_integration is None:
|
||||
_tensorboard_integration = TensorBoardIntegration(log_dir, port)
|
||||
return _tensorboard_integration
|
Reference in New Issue
Block a user