"""
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'
',
'"]
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}
',
'
",
"
",
]
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"]