""" Trading terminal dark theme for the Streamlit dashboard. Bloomberg/TradingView-inspired aesthetic with green/amber accents. Uses CSS variables for consistency and injects custom fonts. """ import html as _html # -- Color Tokens -- COLORS = { "bg_primary": "#0a0e17", "bg_secondary": "#111827", "bg_card": "#1a2234", "bg_card_hover": "#1f2b42", "bg_input": "#151d2e", "border": "#2a3548", "border_active": "#3b82f6", "text_primary": "#e2e8f0", "text_secondary": "#94a3b8", "text_muted": "#64748b", "green": "#22c55e", "green_dim": "#16a34a", "green_glow": "rgba(34, 197, 94, 0.15)", "red": "#ef4444", "red_dim": "#dc2626", "red_glow": "rgba(239, 68, 68, 0.15)", "amber": "#f59e0b", "amber_dim": "#d97706", "blue": "#3b82f6", "blue_dim": "#2563eb", "cyan": "#06b6d4", "purple": "#a855f7", } def get_plotly_template(): """Return a Plotly layout template matching the terminal theme.""" return dict( paper_bgcolor=COLORS["bg_card"], plot_bgcolor=COLORS["bg_card"], font=dict( family="JetBrains Mono, SF Mono, Menlo, monospace", color=COLORS["text_secondary"], size=11, ), xaxis=dict( gridcolor="rgba(42, 53, 72, 0.5)", zerolinecolor=COLORS["border"], showgrid=True, gridwidth=1, ), yaxis=dict( gridcolor="rgba(42, 53, 72, 0.5)", zerolinecolor=COLORS["border"], showgrid=True, gridwidth=1, ), margin=dict(l=0, r=0, t=32, b=0), hoverlabel=dict( bgcolor=COLORS["bg_secondary"], font_color=COLORS["text_primary"], bordercolor=COLORS["border"], ), colorway=[ COLORS["green"], COLORS["blue"], COLORS["amber"], COLORS["cyan"], COLORS["purple"], COLORS["red"], ], ) GLOBAL_CSS = f""" """ def kpi_card(label: str, value: str, delta: str = "", color: str = "blue") -> str: """Render a custom KPI card as HTML.""" delta_class = ( "positive" if delta.startswith("+") else ("negative" if delta.startswith("-") else "neutral") ) delta_html = f'
{delta}
' if delta else "" return f"""
{label}
{value}
{delta_html}
""" def page_header(title: str, subtitle: str = "") -> str: """Render a page header as HTML.""" sub = f'
{subtitle}
' if subtitle else "" return f""" """ def signal_card( rank: int, ticker: str, score: int, confidence: int, strategy: str, entry_price: float, reason: str, company_name: str = "", description: str = "", risk_level: str = "", ) -> str: """Render a recommendation signal card as HTML.""" # Confidence bar color if confidence >= 8: bar_color = COLORS["green"] elif confidence >= 6: bar_color = COLORS["amber"] else: bar_color = COLORS["red"] # Score badge color if score >= 40: score_badge = "badge-green" elif score >= 25: score_badge = "badge-amber" else: score_badge = "badge-muted" # Strategy badge strat_badge = "badge-blue" strat_css = "" strat_lower = strategy.lower().replace(" ", "_") if "momentum" in strat_lower: strat_badge = "badge-green" strat_css = "strat-momentum" elif "insider" in strat_lower: strat_badge = "badge-amber" strat_css = "strat-insider" elif "earnings" in strat_lower: strat_badge = "badge-blue" strat_css = "strat-earnings" elif "volume" in strat_lower: strat_badge = "badge-blue" strat_css = "strat-volume" entry_str = f"${entry_price:.2f}" if entry_price else "N/A" conf_pct = confidence * 10 # Escape all LLM-generated text to prevent HTML injection / rendering breaks safe_reason = _html.escape(reason) safe_company = _html.escape(company_name) safe_desc = _html.escape(description) safe_strategy = _html.escape(strategy) safe_risk = _html.escape(risk_level) # Risk level badge (built after escaping) risk_badge_html = "" if safe_risk: risk_lower = safe_risk.lower() if risk_lower == "low": risk_badge_html = f'{safe_risk.title()}' elif risk_lower == "moderate": risk_badge_html = f'{safe_risk.title()}' elif risk_lower == "high": risk_badge_html = f'{safe_risk.title()}' elif risk_lower == "speculative": risk_badge_html = f'{safe_risk.title()}' name_html = ( f'{safe_company}' if company_name and company_name != ticker else "" ) desc_html = ( f'
' f'
Company
' f'
{safe_desc}
' f"
" if description else "" ) safe_risk_title = safe_risk.title() if safe_risk else "—" # Build HTML piece-by-piece to avoid blank/whitespace-only lines. # CommonMark terminates a
HTML block at the FIRST blank line, so any # empty interpolation (name_html, desc_html, risk_badge_html = "") would leave # a whitespace-only line that makes Streamlit's markdown parser stop treating # subsequent content as HTML and render it as literal text instead. parts = [ f'
', '
', '
', f'{ticker}', f'#{rank}', ] if name_html: parts.append(name_html) parts += ["
", "
"] if desc_html: parts.append(desc_html) parts += [ '
', f'{safe_strategy}', f'Score {score}', f'Conf {confidence}/10', ] if risk_badge_html: parts.append(risk_badge_html) parts += [ "
", '
', '
', '
Entry
', f'
{entry_str}
', "
", '
', '
Score
', f'
{score}/100
', "
", '
', '
Confidence
', f'
{confidence}/10
', "
", '
', '
Risk
', f'
{safe_risk_title}
', "
", "
", f'
{safe_reason}
', '
', f'
', "
", "
", ] return "\n".join(parts) def pnl_color(value: float) -> str: """Return green/red CSS color based on sign.""" if value > 0: return COLORS["green"] elif value < 0: return COLORS["red"] return COLORS["text_muted"]