Merge commit 'd49a473ed6f4aef55bfdd47d6370e53582be6b7b' into cleanup
This commit is contained in:
@@ -1 +0,0 @@
|
||||
# Web module for trading system dashboard
|
||||
File diff suppressed because it is too large
Load Diff
@@ -356,7 +356,8 @@ class COBDashboardServer:
|
||||
if mid_price <= 0:
|
||||
return
|
||||
|
||||
now = datetime.now()
|
||||
from datetime import timezone
|
||||
now = datetime.now(timezone.utc)
|
||||
current_second = now.replace(microsecond=0)
|
||||
|
||||
# Get or create current candle
|
||||
@@ -377,7 +378,7 @@ class COBDashboardServer:
|
||||
if current_second > current_candle['timestamp']:
|
||||
# Close previous candle
|
||||
finished_candle = {
|
||||
'timestamp': current_candle['timestamp'].isoformat(),
|
||||
'timestamp': current_candle['timestamp'].isoformat(), # UTC ISO8601
|
||||
'open': current_candle['open'],
|
||||
'high': current_candle['high'],
|
||||
'low': current_candle['low'],
|
||||
|
||||
@@ -45,6 +45,10 @@ class DashboardComponentManager:
|
||||
blocked = decision.get('blocked', False)
|
||||
manual = decision.get('manual', False)
|
||||
|
||||
# FILTER OUT INVALID PRICES - Skip signals with price 0 or None
|
||||
if price is None or price <= 0:
|
||||
continue
|
||||
|
||||
# Determine signal style
|
||||
if executed:
|
||||
badge_class = "bg-success"
|
||||
@@ -136,7 +140,8 @@ class DashboardComponentManager:
|
||||
# Create table headers
|
||||
headers = html.Thead([
|
||||
html.Tr([
|
||||
html.Th("Time", className="small"),
|
||||
html.Th("Entry Time", className="small"),
|
||||
html.Th("Exit Time", className="small"),
|
||||
html.Th("Side", className="small"),
|
||||
html.Th("Size", className="small"),
|
||||
html.Th("Entry", className="small"),
|
||||
@@ -154,6 +159,7 @@ class DashboardComponentManager:
|
||||
if hasattr(trade, 'entry_time'):
|
||||
# This is a trade object
|
||||
entry_time = getattr(trade, 'entry_time', 'Unknown')
|
||||
exit_time = getattr(trade, 'exit_time', 'Unknown')
|
||||
side = getattr(trade, 'side', 'UNKNOWN')
|
||||
size = getattr(trade, 'size', 0)
|
||||
entry_price = getattr(trade, 'entry_price', 0)
|
||||
@@ -164,6 +170,7 @@ class DashboardComponentManager:
|
||||
else:
|
||||
# This is a dictionary format
|
||||
entry_time = trade.get('entry_time', 'Unknown')
|
||||
exit_time = trade.get('exit_time', 'Unknown')
|
||||
side = trade.get('side', 'UNKNOWN')
|
||||
size = trade.get('quantity', trade.get('size', 0)) # Try 'quantity' first, then 'size'
|
||||
entry_price = trade.get('entry_price', 0)
|
||||
@@ -172,24 +179,41 @@ class DashboardComponentManager:
|
||||
fees = trade.get('fees', 0)
|
||||
hold_time_seconds = trade.get('hold_time_seconds', 0.0)
|
||||
|
||||
# Format time
|
||||
# Format entry time
|
||||
if isinstance(entry_time, datetime):
|
||||
time_str = entry_time.strftime('%H:%M:%S')
|
||||
entry_time_str = entry_time.strftime('%H:%M:%S')
|
||||
else:
|
||||
time_str = str(entry_time)
|
||||
entry_time_str = str(entry_time)
|
||||
|
||||
# Format exit time
|
||||
if isinstance(exit_time, datetime):
|
||||
exit_time_str = exit_time.strftime('%H:%M:%S')
|
||||
else:
|
||||
exit_time_str = str(exit_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"
|
||||
|
||||
# Calculate position size in USD
|
||||
position_size_usd = size * entry_price
|
||||
|
||||
# Get leverage from trade or use default
|
||||
leverage = trade.get('leverage', 1.0) if not hasattr(trade, 'entry_time') else getattr(trade, 'leverage', 1.0)
|
||||
|
||||
# Calculate leveraged PnL (already included in pnl value, but ensure it's displayed correctly)
|
||||
# Ensure fees are subtracted from PnL for accurate profitability
|
||||
net_pnl = pnl - fees
|
||||
|
||||
row = html.Tr([
|
||||
html.Td(time_str, className="small"),
|
||||
html.Td(entry_time_str, className="small"),
|
||||
html.Td(exit_time_str, className="small"),
|
||||
html.Td(side, className=f"small {side_class}"),
|
||||
html.Td(f"{size:.3f}", className="small"),
|
||||
html.Td(f"${position_size_usd:.2f}", className="small"), # Show size in USD
|
||||
html.Td(f"${entry_price:.2f}", className="small"),
|
||||
html.Td(f"${exit_price:.2f}", className="small"),
|
||||
html.Td(f"{hold_time_seconds:.0f}", className="small text-info"),
|
||||
html.Td(f"${pnl:.2f}", className=f"small {pnl_class}"),
|
||||
html.Td(f"${net_pnl:.2f}", className=f"small {pnl_class}"), # Show net PnL after fees
|
||||
html.Td(f"${fees:.3f}", className="small text-muted")
|
||||
])
|
||||
rows.append(row)
|
||||
@@ -272,37 +296,92 @@ class DashboardComponentManager:
|
||||
logger.error(f"Error formatting system status: {e}")
|
||||
return [html.P(f"Error: {str(e)}", className="text-danger small")]
|
||||
|
||||
<<<<<<< HEAD
|
||||
def format_cob_data(self, cob_snapshot, symbol, cumulative_imbalance_stats=None, cob_mode="Unknown", imbalance_ma_data=None):
|
||||
"""Format COB data into a split view with summary, imbalance stats, and a compact ladder."""
|
||||
=======
|
||||
def format_cob_data(self, cob_snapshot, symbol, cumulative_imbalance_stats=None, cob_mode="Unknown", update_info: dict = None):
|
||||
"""Format COB data into a split view with summary, imbalance stats, and a compact ladder.
|
||||
update_info can include keys: 'update_rate', 'aggregated_1s', 'recent_ticks'.
|
||||
"""
|
||||
>>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b
|
||||
try:
|
||||
if not cob_snapshot:
|
||||
return html.Div([
|
||||
html.H6(f"{symbol} COB", className="mb-2"),
|
||||
html.P("No COB data available", className="text-muted small"),
|
||||
html.P(f"Mode: {cob_mode}", className="text-muted small"),
|
||||
html.P(f"Update: {(update_info or {}).get('update_rate', 'n/a')}", className="text-muted small")
|
||||
])
|
||||
|
||||
# Defensive: If cob_snapshot is a list, log and return error
|
||||
if isinstance(cob_snapshot, list):
|
||||
logger.error(f"COB snapshot for {symbol} is a list, expected object. Data: {cob_snapshot}")
|
||||
return html.Div([
|
||||
html.H6(f"{symbol} COB", className="mb-2"),
|
||||
html.P("Invalid COB data format (list)", className="text-danger small"),
|
||||
html.P(f"Mode: {cob_mode}", className="text-muted small"),
|
||||
html.P(f"Update: {(update_info or {}).get('update_rate', 'n/a')}", className="text-muted small")
|
||||
])
|
||||
|
||||
# Debug: Log the type and structure of cob_snapshot
|
||||
logger.debug(f"COB snapshot type for {symbol}: {type(cob_snapshot)}")
|
||||
|
||||
# Handle case where cob_snapshot is a list (error case)
|
||||
if isinstance(cob_snapshot, list):
|
||||
logger.error(f"COB snapshot is a list for {symbol}, expected object or dict")
|
||||
return html.Div([
|
||||
html.H6(f"{symbol} COB", className="mb-2"),
|
||||
html.P("Invalid COB data format (list)", className="text-danger small"),
|
||||
html.P(f"Mode: {cob_mode}", className="text-muted small")
|
||||
])
|
||||
|
||||
# Handle both old format (with stats attribute) and new format (direct attributes)
|
||||
if hasattr(cob_snapshot, 'stats'):
|
||||
# Normalize snapshot to support dict-based COB (bids/asks arrays) and legacy object formats
|
||||
stats = {}
|
||||
bids = []
|
||||
asks = []
|
||||
mid_price = 0
|
||||
spread_bps = 0
|
||||
imbalance = 0
|
||||
|
||||
if isinstance(cob_snapshot, dict):
|
||||
stats = cob_snapshot.get('stats', {}) if isinstance(cob_snapshot.get('stats', {}), dict) else {}
|
||||
mid_price = float(stats.get('mid_price', 0) or 0)
|
||||
spread_bps = float(stats.get('spread_bps', 0) or 0)
|
||||
imbalance = float(stats.get('imbalance', 0) or 0)
|
||||
bids = cob_snapshot.get('bids', []) or []
|
||||
asks = cob_snapshot.get('asks', []) or []
|
||||
elif hasattr(cob_snapshot, 'stats'):
|
||||
# Old format with stats attribute
|
||||
<<<<<<< HEAD
|
||||
stats = cob_snapshot.stats
|
||||
mid_price = stats.get('mid_price', 0)
|
||||
spread_bps = stats.get('spread_bps', 0)
|
||||
imbalance = stats.get('imbalance', 0)
|
||||
bids = getattr(cob_snapshot, 'consolidated_bids', [])
|
||||
asks = getattr(cob_snapshot, 'consolidated_asks', [])
|
||||
=======
|
||||
stats = cob_snapshot.stats if isinstance(cob_snapshot.stats, dict) else {}
|
||||
mid_price = float((stats or {}).get('mid_price', 0) or 0)
|
||||
spread_bps = float((stats or {}).get('spread_bps', 0) or 0)
|
||||
imbalance = float((stats or {}).get('imbalance', 0) or 0)
|
||||
bids = getattr(cob_snapshot, 'consolidated_bids', []) or []
|
||||
asks = getattr(cob_snapshot, 'consolidated_asks', []) or []
|
||||
>>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b
|
||||
else:
|
||||
# New COBSnapshot format with direct attributes
|
||||
mid_price = getattr(cob_snapshot, 'volume_weighted_mid', 0)
|
||||
spread_bps = getattr(cob_snapshot, 'spread_bps', 0)
|
||||
imbalance = getattr(cob_snapshot, 'liquidity_imbalance', 0)
|
||||
bids = getattr(cob_snapshot, 'consolidated_bids', [])
|
||||
asks = getattr(cob_snapshot, 'consolidated_asks', [])
|
||||
# New object-like snapshot with direct attributes
|
||||
mid_price = float(getattr(cob_snapshot, 'volume_weighted_mid', 0) or 0)
|
||||
spread_bps = float(getattr(cob_snapshot, 'spread_bps', 0) or 0)
|
||||
imbalance = float(getattr(cob_snapshot, 'liquidity_imbalance', 0) or 0)
|
||||
bids = getattr(cob_snapshot, 'consolidated_bids', []) or []
|
||||
asks = getattr(cob_snapshot, 'consolidated_asks', []) or []
|
||||
|
||||
if mid_price == 0 or not bids or not asks:
|
||||
return html.Div([
|
||||
html.H6(f"{symbol} COB", className="mb-2"),
|
||||
html.P("Awaiting valid order book data...", className="text-muted small")
|
||||
html.P("Awaiting valid order book data...", className="text-muted small"),
|
||||
html.P(f"Mode: {cob_mode}", className="text-muted small"),
|
||||
html.P(f"Update: {(update_info or {}).get('update_rate', 'n/a')}", className="text-muted small")
|
||||
])
|
||||
|
||||
# Create stats dict for compatibility with existing code
|
||||
@@ -315,17 +394,83 @@ class DashboardComponentManager:
|
||||
'bid_levels': len(bids),
|
||||
'ask_levels': len(asks)
|
||||
}
|
||||
# Show staleness if provided via provider (age_ms)
|
||||
try:
|
||||
age_ms = None
|
||||
if hasattr(cob_snapshot, 'stats') and isinstance(cob_snapshot.stats, dict):
|
||||
age_ms = cob_snapshot.stats.get('age_ms')
|
||||
if age_ms is not None:
|
||||
stats['age_ms'] = age_ms
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- Left Panel: Overview and Stats ---
|
||||
<<<<<<< HEAD
|
||||
overview_panel = self._create_cob_overview_panel(symbol, stats, cumulative_imbalance_stats, cob_mode, imbalance_ma_data)
|
||||
=======
|
||||
# Prepend update info to overview
|
||||
overview_panel = self._create_cob_overview_panel(symbol, stats, cumulative_imbalance_stats, cob_mode)
|
||||
if update_info and update_info.get('update_rate'):
|
||||
# Wrap with a small header line for update rate
|
||||
overview_panel = html.Div([
|
||||
html.Div(html.Small(f"Update: {update_info['update_rate']}", className="text-muted"), className="mb-1"),
|
||||
overview_panel
|
||||
])
|
||||
>>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b
|
||||
|
||||
# --- Right Panel: Compact Ladder ---
|
||||
# --- Right Panel: Compact Ladder with optional exchange stats ---
|
||||
exchange_stats = (update_info or {}).get('exchanges') if isinstance(update_info, dict) else None
|
||||
ladder_panel = self._create_cob_ladder_panel(bids, asks, mid_price, symbol)
|
||||
if exchange_stats:
|
||||
# Render a tiny exchange contribution summary above ladder
|
||||
try:
|
||||
rows = []
|
||||
for ex, stats_ex in exchange_stats.items():
|
||||
rows.append(html.Small(f"{ex}: {stats_ex.get('bids',0)}/{stats_ex.get('asks',0)}", className="text-muted me-2"))
|
||||
ladder_panel = html.Div([
|
||||
html.Div(rows, className="mb-1"),
|
||||
ladder_panel
|
||||
])
|
||||
except Exception:
|
||||
pass
|
||||
# Append small extras line from aggregated_1s and recent_ticks (robust to numeric fields)
|
||||
extras = []
|
||||
if update_info:
|
||||
agg = (update_info.get('aggregated_1s') or [])
|
||||
if agg and isinstance(agg[-1], dict):
|
||||
last = agg[-1]
|
||||
spread_field = last.get('spread', {})
|
||||
if isinstance(spread_field, dict):
|
||||
avg_spread = spread_field.get('average_bps', 0)
|
||||
elif isinstance(spread_field, (int, float)):
|
||||
avg_spread = spread_field
|
||||
else:
|
||||
avg_spread = 0
|
||||
imb_field = last.get('imbalance', {})
|
||||
if isinstance(imb_field, dict):
|
||||
avg_imb = imb_field.get('average', 0)
|
||||
elif isinstance(imb_field, (int, float)):
|
||||
avg_imb = imb_field
|
||||
else:
|
||||
avg_imb = 0
|
||||
tick_count = last.get('tick_count', 0)
|
||||
extras.append(html.Small(f"1s agg: {tick_count} ticks, spread {avg_spread:.1f} bps, imb {avg_imb:.2f}", className="text-muted"))
|
||||
recent = (update_info.get('recent_ticks') or [])
|
||||
if recent:
|
||||
extras.append(html.Small(f"Recent ticks: {len(recent)}", className="text-muted ms-2"))
|
||||
extras_div = html.Div(extras, className="mb-1") if extras else None
|
||||
|
||||
return dbc.Row([
|
||||
dbc.Col(overview_panel, width=5, className="pe-1"),
|
||||
dbc.Col(ladder_panel, width=7, className="ps-1")
|
||||
], className="g-0") # g-0 removes gutters
|
||||
# Heatmap is rendered in dedicated tiles (avoid duplicate component IDs)
|
||||
heatmap_graph = None
|
||||
|
||||
children = [dbc.Col(overview_panel, width=5, className="pe-1")]
|
||||
right_children = []
|
||||
# Do not append inline heatmap here to prevent duplicate IDs
|
||||
right_children.append(ladder_panel)
|
||||
if extras_div:
|
||||
right_children.insert(0, extras_div)
|
||||
children.append(dbc.Col(html.Div(right_children), width=7, className="ps-1"))
|
||||
return dbc.Row(children, className="g-0") # g-0 removes gutters
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error formatting split COB data: {e}")
|
||||
@@ -347,12 +492,16 @@ class DashboardComponentManager:
|
||||
mode_color = "text-success" if cob_mode == "WS" else "text-warning" if cob_mode == "REST" else "text-muted"
|
||||
mode_icon = "fas fa-wifi" if cob_mode == "WS" else "fas fa-globe" if cob_mode == "REST" else "fas fa-question"
|
||||
|
||||
imbalance_stats_display = []
|
||||
if cumulative_imbalance_stats:
|
||||
imbalance_stats_display.append(html.H6("Cumulative Imbalance", className="mt-3 mb-2 small text-muted text-uppercase"))
|
||||
for period, value in cumulative_imbalance_stats.items():
|
||||
imbalance_stats_display.append(self._create_imbalance_stat_row(period, value))
|
||||
|
||||
def _safe_imb(stats_val, key, fallback):
|
||||
try:
|
||||
if isinstance(stats_val, dict):
|
||||
return float(stats_val.get(key, fallback))
|
||||
if isinstance(stats_val, (int, float)):
|
||||
return float(stats_val)
|
||||
except Exception:
|
||||
pass
|
||||
return float(fallback)
|
||||
|
||||
return html.Div([
|
||||
html.H6(f"{symbol} - COB Overview", className="mb-2"),
|
||||
html.Div([
|
||||
@@ -371,7 +520,17 @@ class DashboardComponentManager:
|
||||
html.Span(imbalance_text, className=f"fw-bold small {imbalance_color}")
|
||||
]),
|
||||
|
||||
html.Div(imbalance_stats_display),
|
||||
# Multi-timeframe imbalance metrics (single display, not duplicate)
|
||||
html.Div([
|
||||
html.Strong("Timeframe Imbalances:", className="small d-block mt-2 mb-1")
|
||||
]),
|
||||
|
||||
html.Div([
|
||||
self._create_timeframe_imbalance("1s", _safe_imb(cumulative_imbalance_stats, '1s', imbalance)),
|
||||
self._create_timeframe_imbalance("5s", _safe_imb(cumulative_imbalance_stats, '5s', imbalance)),
|
||||
self._create_timeframe_imbalance("15s", _safe_imb(cumulative_imbalance_stats, '15s', imbalance)),
|
||||
self._create_timeframe_imbalance("60s", _safe_imb(cumulative_imbalance_stats, '60s', imbalance)),
|
||||
], className="d-flex justify-content-between mb-2"),
|
||||
|
||||
# COB Imbalance Moving Averages
|
||||
html.Div([
|
||||
@@ -415,6 +574,22 @@ class DashboardComponentManager:
|
||||
html.Div(title, className="small text-muted"),
|
||||
html.Div(value, className="fw-bold")
|
||||
], className="text-center")
|
||||
|
||||
def _create_timeframe_imbalance(self, timeframe, value):
|
||||
"""Helper for creating timeframe imbalance indicators."""
|
||||
color = "text-success" if value > 0 else "text-danger" if value < 0 else "text-muted"
|
||||
icon = "fas fa-chevron-up" if value > 0 else "fas fa-chevron-down" if value < 0 else "fas fa-minus"
|
||||
|
||||
# Format the value with sign and 2 decimal places
|
||||
formatted_value = f"{value:+.2f}"
|
||||
|
||||
return html.Div([
|
||||
html.Div(timeframe, className="small text-muted"),
|
||||
html.Div([
|
||||
html.I(className=f"{icon} me-1"),
|
||||
html.Span(formatted_value, className="small")
|
||||
], className=color)
|
||||
], className="text-center")
|
||||
|
||||
def _create_cob_ladder_panel(self, bids, asks, mid_price, symbol=""):
|
||||
"""Creates Bookmap-style COB display with horizontal bars extending from center price."""
|
||||
@@ -425,6 +600,7 @@ class DashboardComponentManager:
|
||||
def aggregate_buckets(orders):
|
||||
buckets = {}
|
||||
for order in orders:
|
||||
<<<<<<< HEAD
|
||||
# Handle both dictionary format and ConsolidatedOrderBookLevel objects
|
||||
if hasattr(order, 'price'):
|
||||
price = order.price
|
||||
@@ -434,6 +610,30 @@ class DashboardComponentManager:
|
||||
price = order.get('price', 0)
|
||||
size = order.get('total_size', order.get('size', 0))
|
||||
volume_usd = order.get('total_volume_usd', size * price)
|
||||
=======
|
||||
# Handle multiple formats: object, dict, or [price, size]
|
||||
price = 0.0
|
||||
size = 0.0
|
||||
volume_usd = 0.0
|
||||
try:
|
||||
if hasattr(order, 'price'):
|
||||
# ConsolidatedOrderBookLevel object
|
||||
price = float(getattr(order, 'price', 0) or 0)
|
||||
size = float(getattr(order, 'total_size', getattr(order, 'size', 0)) or 0)
|
||||
volume_usd = float(getattr(order, 'total_volume_usd', price * size) or (price * size))
|
||||
elif isinstance(order, dict):
|
||||
price = float(order.get('price', 0) or 0)
|
||||
size = float(order.get('total_size', order.get('size', 0)) or 0)
|
||||
volume_usd = float(order.get('total_volume_usd', price * size) or (price * size))
|
||||
elif isinstance(order, (list, tuple)) and len(order) >= 2:
|
||||
price = float(order[0] or 0)
|
||||
size = float(order[1] or 0)
|
||||
volume_usd = price * size
|
||||
else:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
>>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b
|
||||
|
||||
if price > 0:
|
||||
bucket_key = round(price / bucket_size) * bucket_size
|
||||
@@ -773,7 +973,15 @@ class DashboardComponentManager:
|
||||
def format_training_metrics(self, metrics_data):
|
||||
"""Format training metrics for display - Enhanced with loaded models"""
|
||||
try:
|
||||
# DEBUG: Log what we're receiving
|
||||
logger.info(f"format_training_metrics received: {type(metrics_data)}")
|
||||
if metrics_data:
|
||||
logger.info(f"Metrics keys: {list(metrics_data.keys()) if isinstance(metrics_data, dict) else 'Not a dict'}")
|
||||
if isinstance(metrics_data, dict) and 'loaded_models' in metrics_data:
|
||||
logger.info(f"Loaded models: {list(metrics_data['loaded_models'].keys())}")
|
||||
|
||||
if not metrics_data or 'error' in metrics_data:
|
||||
logger.warning(f"No training data or error in metrics_data: {metrics_data}")
|
||||
return [html.P("No training data", className="text-muted small")]
|
||||
|
||||
content = []
|
||||
@@ -824,6 +1032,7 @@ class DashboardComponentManager:
|
||||
checkpoint_status = "LOADED" if model_info.get('checkpoint_loaded', False) else "FRESH"
|
||||
|
||||
# Model card
|
||||
logger.info(f"Creating model card for {model_name} with toggles: inference={model_info.get('inference_enabled', True)}, training={model_info.get('training_enabled', True)}")
|
||||
model_card = html.Div([
|
||||
# Header with model name and toggle
|
||||
html.Div([
|
||||
@@ -835,16 +1044,29 @@ class DashboardComponentManager:
|
||||
html.Span(f" [{checkpoint_status}]", className=f"small {'text-success' if checkpoint_status == 'LOADED' else 'text-warning'} ms-1")
|
||||
], style={"flex": "1"}),
|
||||
|
||||
# Activation toggle (if easy to implement)
|
||||
# Inference and Training toggles
|
||||
html.Div([
|
||||
dcc.Checklist(
|
||||
id=f"toggle-{model_name}",
|
||||
options=[{"label": "", "value": "active"}],
|
||||
value=["active"] if is_active else [],
|
||||
className="form-check-input",
|
||||
style={"transform": "scale(0.8)"}
|
||||
)
|
||||
], className="form-check form-switch")
|
||||
html.Div([
|
||||
html.Label("Inf", className="text-muted small me-1", style={"font-size": "10px"}),
|
||||
dcc.Checklist(
|
||||
id=f"{model_name}-inference-toggle",
|
||||
options=[{"label": "", "value": True}],
|
||||
value=[True] if model_info.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=f"{model_name}-training-toggle",
|
||||
options=[{"label": "", "value": True}],
|
||||
value=[True] if model_info.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-1"),
|
||||
|
||||
# Model metrics
|
||||
@@ -883,7 +1105,11 @@ class DashboardComponentManager:
|
||||
html.Br(),
|
||||
html.Span(f"Rate: {model_info.get('timing', {}).get('inferences_per_second', '0.00')}/s", className="text-success small"),
|
||||
html.Span(" | ", className="text-muted small"),
|
||||
html.Span(f"24h: {model_info.get('timing', {}).get('predictions_24h', 0)}", className="text-primary small")
|
||||
html.Span(f"24h: {model_info.get('timing', {}).get('predictions_24h', 0)}", className="text-primary small"),
|
||||
html.Br(),
|
||||
html.Span(f"Avg Inf: {model_info.get('timing', {}).get('average_inference_time_ms', 'N/A')}ms", className="text-info small"),
|
||||
html.Span(" | ", className="text-muted small"),
|
||||
html.Span(f"Avg Train: {model_info.get('timing', {}).get('average_training_time_ms', 'N/A')}ms", className="text-warning small")
|
||||
], className="mb-1"),
|
||||
|
||||
# Loss metrics with improvement tracking
|
||||
@@ -1078,10 +1304,15 @@ class DashboardComponentManager:
|
||||
html.Span(f"{enhanced_stats['recent_validation_score']:.3f}", className="text-primary small fw-bold")
|
||||
], className="mb-1"))
|
||||
|
||||
logger.info(f"format_training_metrics returning {len(content)} components")
|
||||
for i, component in enumerate(content[:3]): # Log first 3 components
|
||||
logger.info(f" Component {i}: {type(component)}")
|
||||
return content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error formatting training metrics: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
return [html.P(f"Error: {str(e)}", className="text-danger small")]
|
||||
|
||||
def _format_cnn_pivot_prediction(self, model_info):
|
||||
|
||||
@@ -10,11 +10,13 @@ from datetime import datetime
|
||||
class DashboardLayoutManager:
|
||||
"""Manages dashboard layout and structure"""
|
||||
|
||||
def __init__(self, starting_balance: float = 100.0, trading_executor=None):
|
||||
def __init__(self, starting_balance: float = 100.0, trading_executor=None, dashboard=None):
|
||||
self.starting_balance = starting_balance
|
||||
self.trading_executor = trading_executor
|
||||
self.dashboard = dashboard
|
||||
|
||||
def create_main_layout(self):
|
||||
<<<<<<< HEAD
|
||||
"""Create the main dashboard layout with dark theme"""
|
||||
return html.Div([
|
||||
self._create_header(),
|
||||
@@ -27,6 +29,35 @@ class DashboardLayoutManager:
|
||||
"minHeight": "100vh",
|
||||
"color": "#f8f9fa"
|
||||
})
|
||||
=======
|
||||
"""Create the main dashboard layout"""
|
||||
try:
|
||||
print("Creating main layout...")
|
||||
header = self._create_header()
|
||||
print("Header created")
|
||||
interval_component = self._create_interval_component()
|
||||
print("Interval component created")
|
||||
main_content = self._create_main_content()
|
||||
print("Main content created")
|
||||
|
||||
layout = html.Div([
|
||||
header,
|
||||
interval_component,
|
||||
main_content
|
||||
], className="container-fluid")
|
||||
|
||||
print("Main layout created successfully")
|
||||
return layout
|
||||
except Exception as e:
|
||||
print(f"Error creating main layout: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# Return a simple error layout
|
||||
return html.Div([
|
||||
html.H1("Dashboard Error", className="text-danger"),
|
||||
html.P(f"Error creating layout: {str(e)}", className="text-danger")
|
||||
])
|
||||
>>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b
|
||||
|
||||
def _create_prediction_tracking_section(self):
|
||||
"""Create prediction tracking and model performance section"""
|
||||
@@ -248,7 +279,7 @@ class DashboardLayoutManager:
|
||||
"Clean Trading Dashboard"
|
||||
], className="text-light mb-0"),
|
||||
html.P(
|
||||
f"Ultra-Fast Updates • Portfolio: ${self.starting_balance:,.0f} • {trading_mode}",
|
||||
f"Ultra-Fast Updates • Live Account Balance Sync • {trading_mode}",
|
||||
className="text-light mb-0 opacity-75 small"
|
||||
)
|
||||
], className="bg-dark p-2 mb-2")
|
||||
@@ -261,6 +292,7 @@ class DashboardLayoutManager:
|
||||
], className="bg-dark p-2 mb-2")
|
||||
|
||||
def _create_interval_component(self):
|
||||
<<<<<<< HEAD
|
||||
"""Create the auto-refresh interval component"""
|
||||
return html.Div([
|
||||
dcc.Interval(
|
||||
@@ -273,6 +305,32 @@ class DashboardLayoutManager:
|
||||
interval=60000, # Update every 60 seconds for chained inference
|
||||
n_intervals=0
|
||||
)
|
||||
=======
|
||||
"""Create the auto-refresh interval components with different frequencies"""
|
||||
return html.Div([
|
||||
# Fast interval for critical updates (2 seconds - reduced from 1s)
|
||||
dcc.Interval(
|
||||
id='interval-component',
|
||||
interval=2000, # Update every 2000 ms (0.5 Hz) - OPTIMIZED
|
||||
n_intervals=0
|
||||
),
|
||||
# Slow interval for non-critical updates (10 seconds - increased from 5s)
|
||||
dcc.Interval(
|
||||
id='slow-interval-component',
|
||||
interval=10000, # Update every 10 seconds (0.1 Hz) - OPTIMIZED
|
||||
n_intervals=0,
|
||||
disabled=False
|
||||
),
|
||||
# Fast interval for testing (5 seconds)
|
||||
dcc.Interval(
|
||||
id='fast-interval-component',
|
||||
interval=5000, # Update every 5 seconds for testing
|
||||
n_intervals=0,
|
||||
disabled=False
|
||||
),
|
||||
# WebSocket-based updates for high-frequency data (no interval needed)
|
||||
html.Div(id='websocket-updates-container', style={'display': 'none'})
|
||||
>>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b
|
||||
])
|
||||
|
||||
def _create_main_content(self):
|
||||
@@ -297,6 +355,25 @@ class DashboardLayoutManager:
|
||||
|
||||
def _create_metrics_grid(self):
|
||||
"""Create the metrics grid with compact cards"""
|
||||
# Get exchange name dynamically
|
||||
exchange_name = "Exchange"
|
||||
if self.trading_executor:
|
||||
if hasattr(self.trading_executor, 'primary_name'):
|
||||
exchange_name = self.trading_executor.primary_name.upper()
|
||||
elif hasattr(self.trading_executor, 'exchange') and self.trading_executor.exchange:
|
||||
# Try to get exchange name from exchange interface
|
||||
exchange_class_name = self.trading_executor.exchange.__class__.__name__
|
||||
if 'Bybit' in exchange_class_name:
|
||||
exchange_name = "BYBIT"
|
||||
elif 'Mexc' in exchange_class_name or 'MEXC' in exchange_class_name:
|
||||
exchange_name = "MEXC"
|
||||
elif 'Binance' in exchange_class_name:
|
||||
exchange_name = "BINANCE"
|
||||
elif 'Deribit' in exchange_class_name:
|
||||
exchange_name = "DERIBIT"
|
||||
else:
|
||||
exchange_name = "EXCHANGE"
|
||||
|
||||
metrics_cards = [
|
||||
("current-price", "Live Price", "text-success"),
|
||||
("session-pnl", "Session P&L", ""),
|
||||
@@ -304,7 +381,9 @@ class DashboardLayoutManager:
|
||||
# ("leverage-info", "Leverage", "text-primary"),
|
||||
("trade-count", "Trades", "text-warning"),
|
||||
("portfolio-value", "Portfolio", "text-secondary"),
|
||||
("mexc-status", "MEXC API", "text-info")
|
||||
("profitability-multiplier", "Profit Boost", "text-primary"),
|
||||
("cob-websocket-status", "COB WebSocket", "text-warning"),
|
||||
("mexc-status", f"{exchange_name} API", "text-info")
|
||||
]
|
||||
|
||||
cards = []
|
||||
@@ -356,7 +435,64 @@ class DashboardLayoutManager:
|
||||
"Session Controls"
|
||||
], className="card-title mb-2"),
|
||||
|
||||
# Trading Agent Mode Toggle
|
||||
html.Div([
|
||||
html.Label([
|
||||
html.I(className="fas fa-robot me-1"),
|
||||
"Trading Agent: ",
|
||||
html.Span(
|
||||
id="trading-mode-display",
|
||||
children="LIVE" if getattr(self.dashboard, 'trading_mode_live', False) else "SIM",
|
||||
className="fw-bold text-danger" if getattr(self.dashboard, 'trading_mode_live', False) else "fw-bold text-warning"
|
||||
)
|
||||
], className="form-label small mb-1"),
|
||||
dcc.Checklist(
|
||||
id='trading-mode-switch',
|
||||
options=[{'label': '', 'value': 'live'}],
|
||||
value=['live'] if getattr(self.dashboard, 'trading_mode_live', False) else [],
|
||||
className="form-check-input"
|
||||
),
|
||||
html.Small("SIM = Simulation Mode, LIVE = Real Trading", className="text-muted d-block")
|
||||
], className="mb-2"),
|
||||
|
||||
# Cold Start Training Toggle
|
||||
html.Div([
|
||||
html.Label([
|
||||
html.I(className="fas fa-fire me-1"),
|
||||
"Cold Start Training: ",
|
||||
html.Span(
|
||||
id="cold-start-display",
|
||||
children="ON" if getattr(self.dashboard, 'cold_start_enabled', True) else "OFF",
|
||||
className="fw-bold text-success" if getattr(self.dashboard, 'cold_start_enabled', True) else "fw-bold text-secondary"
|
||||
)
|
||||
], className="form-label small mb-1"),
|
||||
dcc.Checklist(
|
||||
id='cold-start-switch',
|
||||
options=[{'label': '', 'value': 'enabled'}],
|
||||
value=['enabled'] if getattr(self.dashboard, 'cold_start_enabled', True) else [],
|
||||
className="form-check-input"
|
||||
),
|
||||
html.Small("Excessive training during cold start", className="text-muted d-block")
|
||||
], className="mb-2"),
|
||||
|
||||
html.Hr(className="my-2"),
|
||||
|
||||
# Leverage Control
|
||||
html.Div([
|
||||
html.Label([
|
||||
html.I(className="fas fa-compass me-1"),
|
||||
"Show Pivot Points: ",
|
||||
html.Span(id="pivots-display", children="ON", className="fw-bold text-success")
|
||||
], className="form-label small mb-1"),
|
||||
dcc.Checklist(
|
||||
id='show-pivots-switch',
|
||||
options=[{'label': '', 'value': 'enabled'}],
|
||||
value=['enabled'],
|
||||
className="form-check-input"
|
||||
),
|
||||
html.Small("Toggle pivot overlays on the chart", className="text-muted d-block")
|
||||
], className="mb-2"),
|
||||
|
||||
html.Div([
|
||||
html.Label([
|
||||
html.I(className="fas fa-sliders-h me-1"),
|
||||
@@ -455,6 +591,37 @@ class DashboardLayoutManager:
|
||||
html.I(className="fas fa-save me-1"),
|
||||
"Store All Models"
|
||||
], id="store-models-btn", className="btn btn-info btn-sm w-100 mt-2"),
|
||||
html.Button([
|
||||
html.I(className="fas fa-arrows-rotate me-1"),
|
||||
"Sync Positions/Orders"
|
||||
], id="manual-sync-btn", className="btn btn-primary btn-sm w-100 mt-2"),
|
||||
|
||||
# Text Export Controls
|
||||
html.Hr(className="my-2"),
|
||||
html.Small("Text Export & LLM", className="text-muted d-block mb-1"),
|
||||
html.Div([
|
||||
html.Button([
|
||||
html.I(className="fas fa-file-export me-1"),
|
||||
"Start Text Export"
|
||||
], id="start-text-export-btn", className="btn btn-success btn-sm me-1", style={"fontSize": "10px"}),
|
||||
html.Button([
|
||||
html.I(className="fas fa-stop me-1"),
|
||||
"Stop"
|
||||
], id="stop-text-export-btn", className="btn btn-danger btn-sm", style={"fontSize": "10px"})
|
||||
], className="d-flex mb-2"),
|
||||
html.Div([
|
||||
html.Button([
|
||||
html.I(className="fas fa-robot me-1"),
|
||||
"Start LLM"
|
||||
], id="start-llm-btn", className="btn btn-info btn-sm me-1", style={"fontSize": "10px"}),
|
||||
html.Button([
|
||||
html.I(className="fas fa-stop me-1"),
|
||||
"Stop"
|
||||
], id="stop-llm-btn", className="btn btn-warning btn-sm", style={"fontSize": "10px"})
|
||||
], className="d-flex mb-2"),
|
||||
html.Small(id="text-export-status", children="Export: Stopped", className="text-muted d-block"),
|
||||
html.Small(id="llm-status", children="LLM: Stopped", className="text-muted d-block"),
|
||||
|
||||
html.Hr(className="my-2"),
|
||||
html.Small("System Status", className="text-muted d-block mb-1"),
|
||||
html.Div([
|
||||
@@ -478,11 +645,12 @@ class DashboardLayoutManager:
|
||||
return html.Div([
|
||||
html.Div([
|
||||
html.Div([
|
||||
# Chart header with manual trading buttons
|
||||
# Chart header with manual trading buttons and live price
|
||||
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"
|
||||
"Live Price (WebSocket): ",
|
||||
html.Span(id="chart-current-price", className="ms-2 text-primary", style={"fontWeight": "bold", "fontSize": "1.2em"})
|
||||
], className="card-title mb-0"),
|
||||
html.Div([
|
||||
html.Button([
|
||||
@@ -493,20 +661,33 @@ class DashboardLayoutManager:
|
||||
html.Button([
|
||||
html.I(className="fas fa-arrow-down me-1"),
|
||||
"SELL"
|
||||
], id="manual-sell-btn", className="btn btn-danger btn-sm",
|
||||
], id="manual-sell-btn", className="btn btn-danger btn-sm me-2",
|
||||
style={"fontSize": "10px", "padding": "2px 8px"}),
|
||||
html.Button([
|
||||
html.I(className="fas fa-times me-1"),
|
||||
"CLOSE"
|
||||
], id="manual-close-btn", className="btn btn-secondary 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"})
|
||||
]),
|
||||
html.Hr(className="my-2"),
|
||||
html.Div([
|
||||
html.H6([
|
||||
html.I(className="fas fa-chart-line me-2"),
|
||||
"Polymarket vs CoinGecko (BTC/ETH, next 5 days)"
|
||||
], className="card-title mb-2"),
|
||||
dcc.Graph(id="polymarket-eth-btc-chart", style={"height": "350px"})
|
||||
])
|
||||
], className="card-body p-2")
|
||||
], className="card")
|
||||
])
|
||||
|
||||
def _create_cob_and_trades_row(self):
|
||||
"""Creates the row for COB ladders, closed trades, and model status - REORGANIZED LAYOUT"""
|
||||
"""Creates the row for COB ladders, closed trades, pending orders, and model status"""
|
||||
return html.Div([
|
||||
# Top row: COB Ladders (left) and Models/Training (right)
|
||||
html.Div([
|
||||
@@ -531,38 +712,77 @@ class DashboardLayoutManager:
|
||||
], className="d-flex")
|
||||
], style={"width": "60%"}),
|
||||
|
||||
# Right side: Models & Training Progress (40% width) - MOVED UP
|
||||
# Right side: Models & Training Progress (40% width)
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.H6([
|
||||
html.I(className="fas fa-brain me-2"),
|
||||
"Models & Training Progress",
|
||||
], className="card-title mb-2"),
|
||||
html.Div([
|
||||
html.H6([
|
||||
html.I(className="fas fa-brain me-2"),
|
||||
"Models & Training Progress",
|
||||
], className="card-title 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")
|
||||
], className="d-flex justify-content-between align-items-center mb-2"),
|
||||
html.Div(
|
||||
id="training-metrics",
|
||||
style={"height": "300px", "overflowY": "auto"}, # Increased height
|
||||
style={"height": "300px", "overflowY": "auto"},
|
||||
),
|
||||
], className="card-body p-2")
|
||||
], className="card")
|
||||
], style={"width": "38%", "marginLeft": "2%"}),
|
||||
], className="d-flex mb-3"),
|
||||
|
||||
# Bottom row: Closed Trades (full width) - MOVED BELOW COB
|
||||
|
||||
# Mini COB Heatmaps (ETH and BTC)
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.H6([
|
||||
html.I(className="fas fa-history me-2"),
|
||||
"Recent Closed Trades",
|
||||
], className="card-title mb-2"),
|
||||
html.Div(
|
||||
id="closed-trades-table",
|
||||
style={"height": "200px", "overflowY": "auto"}, # Reduced height
|
||||
),
|
||||
], className="card-body p-2")
|
||||
], className="card")
|
||||
])
|
||||
html.H6([html.I(className="fas fa-fire me-2"), "ETH Heatmap"], className="card-title mb-2"),
|
||||
dcc.Graph(id='cob-heatmap-eth', config={'displayModeBar': False}, style={"height": "220px"})
|
||||
], className="card-body p-2", style={"flex": "1"}),
|
||||
html.Div([
|
||||
html.H6([html.I(className="fas fa-fire me-2"), "BTC Heatmap"], className="card-title mb-2"),
|
||||
dcc.Graph(id='cob-heatmap-btc', config={'displayModeBar': False}, style={"height": "220px"})
|
||||
], className="card-body p-2", style={"flex": "1", "marginLeft": "1rem"})
|
||||
], className="d-flex")
|
||||
], className="card"),
|
||||
|
||||
# Second row: Pending Orders (left) and Closed Trades (right)
|
||||
html.Div([
|
||||
# Left side: Pending Orders (40% width)
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.H6([
|
||||
html.I(className="fas fa-clock me-2"),
|
||||
"Pending Orders & Position Sync",
|
||||
], className="card-title mb-2"),
|
||||
html.Div(
|
||||
id="pending-orders-content",
|
||||
style={"height": "200px", "overflowY": "auto"},
|
||||
),
|
||||
], className="card-body p-2")
|
||||
], className="card")
|
||||
], style={"width": "40%"}),
|
||||
|
||||
# Right side: Closed Trades (58% width)
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.H6([
|
||||
html.I(className="fas fa-history me-2"),
|
||||
"Recent 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": "58%", "marginLeft": "2%"}),
|
||||
], className="d-flex")
|
||||
])
|
||||
|
||||
def _create_analytics_and_performance_row(self):
|
||||
@@ -618,6 +838,9 @@ class DashboardLayoutManager:
|
||||
], className="card-body p-2")
|
||||
], className="card", style={"width": "30%", "marginLeft": "2%"})
|
||||
], className="d-flex")
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
|
||||
|
||||
=======
|
||||
>>>>>>> d49a473ed6f4aef55bfdd47d6370e53582be6b7b
|
||||
|
||||
290
web/models_training_panel.py
Normal file
290
web/models_training_panel.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""
|
||||
Models & Training Progress Panel - Clean Implementation
|
||||
Displays real-time model status, training metrics, and performance data
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from dash import html, dcc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModelsTrainingPanel:
|
||||
"""Clean implementation of the Models & Training Progress panel.
|
||||
- Uses orchestrator.get_model_statistics_summary() and model_states
|
||||
- No synthetic or placeholder data; shows empty when unavailable
|
||||
- Avoids duplicate class definitions and invalid imports
|
||||
"""
|
||||
|
||||
def __init__(self, orchestrator=None):
|
||||
self.orchestrator = orchestrator
|
||||
|
||||
def create_panel(self) -> Any:
|
||||
try:
|
||||
data = self._gather_data()
|
||||
|
||||
content: List[html.Div] = []
|
||||
content.append(self._create_models_section(data.get("models", {})))
|
||||
|
||||
if data.get("training_status"):
|
||||
content.append(self._create_training_status_section(data["training_status"]))
|
||||
|
||||
if data.get("system_metrics"):
|
||||
content.append(self._create_system_metrics_section(data["system_metrics"]))
|
||||
|
||||
# Return children (to be assigned to 'training-metrics' container)
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating models training panel: {e}")
|
||||
return [
|
||||
html.P(f"Error loading training panel: {str(e)}", className="text-danger small")
|
||||
]
|
||||
|
||||
def _gather_data(self) -> Dict[str, Any]:
|
||||
result: Dict[str, Any] = {"models": {}, "training_status": {}, "system_metrics": {}}
|
||||
if not self.orchestrator:
|
||||
return result
|
||||
try:
|
||||
# Model statistics summary (serializable)
|
||||
stats_summary: Dict[str, Dict[str, Any]] = {}
|
||||
if hasattr(self.orchestrator, "get_model_statistics_summary"):
|
||||
stats = self.orchestrator.get_model_statistics_summary()
|
||||
if isinstance(stats, dict):
|
||||
stats_summary = stats
|
||||
|
||||
# Model states (for best_loss and checkpoint flags) - use get_model_states() for updated checkpoint info
|
||||
model_states: Dict[str, Dict[str, Any]] = {}
|
||||
if hasattr(self.orchestrator, "get_model_states"):
|
||||
try:
|
||||
model_states = self.orchestrator.get_model_states() or {}
|
||||
except Exception as e:
|
||||
logger.debug(f"Error getting model states: {e}")
|
||||
model_states = getattr(self.orchestrator, "model_states", {}) or {}
|
||||
|
||||
# Build models block from stats_summary
|
||||
for model_name, s in stats_summary.items():
|
||||
model_info: Dict[str, Any] = {}
|
||||
try:
|
||||
# Use actual model name - no mapping needed
|
||||
display_name = model_name.upper()
|
||||
# Status: active if we have any inference info
|
||||
total_inf = int(s.get("total_inferences", 0) or 0)
|
||||
status = "active" if (total_inf > 0 or s.get("last_inference_time")) else "registered"
|
||||
|
||||
# Last prediction
|
||||
last_pred_action = s.get("last_prediction")
|
||||
last_confidence = s.get("last_confidence")
|
||||
last_inference_time = s.get("last_inference_time")
|
||||
|
||||
# Loss metrics
|
||||
loss_metrics = {
|
||||
"current_loss": s.get("current_loss"),
|
||||
"best_loss": s.get("best_loss"),
|
||||
}
|
||||
|
||||
# Timing metrics
|
||||
timing_metrics = {
|
||||
"inferences_per_second": s.get("inference_rate_per_second"),
|
||||
"last_inference": last_inference_time,
|
||||
"last_training": s.get("last_training_time"),
|
||||
}
|
||||
|
||||
# Checkpoint flags from orchestrator.model_states - map external names to internal keys
|
||||
ckpt_loaded = False
|
||||
ckpt_failed = False
|
||||
checkpoint_filename = None
|
||||
|
||||
# Map external model names to internal model state keys
|
||||
state_key_map = {
|
||||
"dqn_agent": "dqn",
|
||||
"enhanced_cnn": "cnn",
|
||||
"extrema_trainer": "extrema_trainer",
|
||||
"decision": "decision",
|
||||
"cob_rl_model": "cob_rl"
|
||||
}
|
||||
state_key = state_key_map.get(model_name, model_name)
|
||||
|
||||
if model_states and state_key in model_states:
|
||||
ckpt_loaded = bool(model_states[state_key].get("checkpoint_loaded", False))
|
||||
ckpt_failed = bool(model_states[state_key].get("checkpoint_failed", False))
|
||||
checkpoint_filename = model_states[state_key].get("checkpoint_filename")
|
||||
|
||||
# If model started fresh, mark as stored after first successful autosave/best_loss update
|
||||
if not ckpt_loaded:
|
||||
cur = model_states[state_key].get("current_loss")
|
||||
best = model_states[state_key].get("best_loss")
|
||||
if isinstance(cur, (int, float)) and isinstance(best, (int, float)) and best is not None and cur is not None and cur <= best:
|
||||
ckpt_loaded = True
|
||||
|
||||
model_info = {
|
||||
"name": display_name,
|
||||
"status": status,
|
||||
"parameters": 0, # unknown; do not synthesize
|
||||
"last_prediction": {
|
||||
"action": last_pred_action if last_pred_action is not None else "",
|
||||
"confidence": last_confidence if last_confidence is not None else None,
|
||||
"timestamp": last_inference_time if last_inference_time else "",
|
||||
},
|
||||
"training_enabled": True,
|
||||
"inference_enabled": True,
|
||||
"routing_enabled": True,
|
||||
"checkpoint_loaded": ckpt_loaded,
|
||||
"checkpoint_failed": ckpt_failed,
|
||||
"checkpoint_filename": checkpoint_filename,
|
||||
"loss_metrics": loss_metrics,
|
||||
"timing_metrics": timing_metrics,
|
||||
"signal_stats": {},
|
||||
}
|
||||
except Exception as e:
|
||||
logger.debug(f"Error building model info for {model_name}: {e}")
|
||||
model_info = {"name": model_name, "status": "error", "error": str(e)}
|
||||
|
||||
result["models"][model_name] = model_info
|
||||
|
||||
# Enhanced training system status
|
||||
training_status: Dict[str, Any] = {}
|
||||
try:
|
||||
if hasattr(self.orchestrator, "get_enhanced_training_stats"):
|
||||
training_status = self.orchestrator.get_enhanced_training_stats() or {}
|
||||
except Exception as e:
|
||||
logger.debug(f"Enhanced training stats unavailable: {e}")
|
||||
result["training_status"] = training_status
|
||||
|
||||
# System metrics (decision fusion, cob integration)
|
||||
system_metrics = {
|
||||
"decision_fusion_active": bool(getattr(self.orchestrator, "decision_fusion_enabled", False)),
|
||||
"cob_integration_active": bool(getattr(self.orchestrator, "cob_integration", None) is not None),
|
||||
"symbols_tracking": len(getattr(getattr(self.orchestrator, "cob_integration", None), "symbols", []) or []),
|
||||
}
|
||||
result["system_metrics"] = system_metrics
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error gathering panel data: {e}")
|
||||
return result
|
||||
|
||||
def _create_models_section(self, models: Dict[str, Any]) -> html.Div:
|
||||
cards = [self._create_model_card(name, info) for name, info in models.items()]
|
||||
return html.Div([
|
||||
html.H6([html.I(className="fas fa-microchip me-2 text-success"), f"Loaded Models ({len(models)})"], className="mb-2"),
|
||||
html.Div(cards)
|
||||
])
|
||||
|
||||
def _create_model_card(self, model_name: str, model_data: Dict[str, Any]) -> html.Div:
|
||||
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"
|
||||
|
||||
last_pred = model_data.get("last_prediction", {})
|
||||
pred_action = last_pred.get("action") or "NONE"
|
||||
pred_confidence = last_pred.get("confidence")
|
||||
pred_time = last_pred.get("timestamp") or "N/A"
|
||||
|
||||
loss_metrics = model_data.get("loss_metrics", {})
|
||||
current_loss = loss_metrics.get("current_loss")
|
||||
best_loss = loss_metrics.get("best_loss")
|
||||
loss_class = (
|
||||
"text-success" if (isinstance(current_loss, (int, float)) and current_loss < 0.1)
|
||||
else "text-warning" if (isinstance(current_loss, (int, float)) and current_loss < 0.5)
|
||||
else "text-danger"
|
||||
) if current_loss is not None else "text-muted"
|
||||
|
||||
timing = model_data.get("timing_metrics", {})
|
||||
rate = timing.get("inferences_per_second")
|
||||
|
||||
# Tooltip title showing checkpoint info (if any)
|
||||
title_parts: List[str] = [f"{status_text}"]
|
||||
ckpt_name = model_data.get('checkpoint_filename')
|
||||
if ckpt_name:
|
||||
title_parts.append(f"CKPT: {ckpt_name}")
|
||||
header_title = " | ".join(title_parts)
|
||||
|
||||
return html.Div([
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.I(className=f"{status_icon} me-2 {status_class}"),
|
||||
html.Strong(f"{model_name.upper()}", className=status_class, title=header_title),
|
||||
html.Span(f" - {status_text}", className=f"{status_class} small ms-1"),
|
||||
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"}),
|
||||
html.Div([
|
||||
html.Div([
|
||||
html.Label("Inf", className="text-muted small me-1", style={"fontSize": "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={"fontSize": "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 me-2"),
|
||||
html.Div([
|
||||
html.Label("Route", className="text-muted small me-1", style={"fontSize": "10px"}),
|
||||
dcc.Checklist(id={'type': 'model-toggle', 'model': model_name, 'toggle_type': 'routing'}, options=[{"label": "", "value": True}], value=[True] if model_data.get('routing_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"),
|
||||
|
||||
html.Div([
|
||||
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}%)" if isinstance(pred_confidence, (int, float)) else "", className="text-muted small"),
|
||||
html.Span(f" @ {pred_time}", className="text-muted small")
|
||||
], className="mb-1"),
|
||||
html.Div([
|
||||
html.Span("Loss: ", className="text-muted small"),
|
||||
html.Span(f"{current_loss:.4f}" if isinstance(current_loss, (int, float)) else "", className=f"small fw-bold {loss_class}"),
|
||||
*([html.Span(" | Best: ", className="text-muted small"), html.Span(f"{best_loss:.4f}", className="text-success small")] if isinstance(best_loss, (int, float)) else [])
|
||||
], className="mb-1"),
|
||||
html.Div([
|
||||
html.Span("Rate: ", className="text-muted small"),
|
||||
html.Span(f"{rate:.2f}/s" if isinstance(rate, (int, float)) else "", className="text-info small")
|
||||
], className="mb-1"),
|
||||
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"),
|
||||
])
|
||||
], className="border rounded p-2 mb-2")
|
||||
|
||||
def _create_training_status_section(self, status: Dict[str, Any]) -> html.Div:
|
||||
if not status or status.get("status") == "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: {status.get('error', 'Unknown')}", className="text-danger small")
|
||||
])
|
||||
is_enabled = status.get("training_enabled", 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("Enabled: ", className="text-muted small"),
|
||||
html.Span("ON" if is_enabled else "OFF", className=f"small fw-bold {'text-success' if is_enabled else 'text-warning'}"),
|
||||
], className="mb-1"),
|
||||
])
|
||||
|
||||
def _create_system_metrics_section(self, metrics: Dict[str, Any]) -> html.Div:
|
||||
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 metrics.get("decision_fusion_active") else "OFF", className=f"small {'text-success' if metrics.get('decision_fusion_active') else 'text-muted'}"),
|
||||
html.Span(" | COB: ", className="text-muted small"),
|
||||
html.Span("ON" if metrics.get("cob_integration_active") else "OFF", className=f"small {'text-success' if metrics.get('cob_integration_active') else 'text-muted'}"),
|
||||
], className="mb-1"),
|
||||
html.Div([
|
||||
html.Span("Tracking: ", className="text-muted small"),
|
||||
html.Span(f"{metrics.get('symbols_tracking', 0)} symbols", className="text-info small"),
|
||||
], className="mb-0"),
|
||||
])
|
||||
Reference in New Issue
Block a user