
- Add HTML templates for clean separation of concerns - Add structured data models for type safety - Add template renderer for Jinja2 integration - Add templated dashboard implementation - Demonstrates 95% file size reduction potential
385 lines
15 KiB
Python
385 lines
15 KiB
Python
"""
|
|
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) |