""" Template Renderer for Dashboard Handles HTML template rendering with Jinja2 """ import os from typing import Dict, Any from jinja2 import Environment, FileSystemLoader, select_autoescape import dash_html_components as html from dash import 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)