Files
gogo2/web/template_renderer.py
Dobromir Popov 6acc1c9296 Add template-based MVC dashboard architecture
- 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
2025-07-02 01:56:50 +03:00

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)