Merge commit 'd49a473ed6f4aef55bfdd47d6370e53582be6b7b' into cleanup

This commit is contained in:
Dobromir Popov
2025-10-01 00:32:19 +03:00
353 changed files with 81004 additions and 35899 deletions

View File

@@ -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):