feat: add theme module and fix Streamlit Cloud deployment
- Add tradingagents/ui/theme.py (design system: colors, CSS, Plotly templates) - Add .streamlit/config.toml for dark theme configuration - Fix Plotly duplicate keyword args in performance.py and todays_picks.py - Replace deprecated use_container_width with width="stretch" (Streamlit 1.54+) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ebb42114d
commit
1ead4d9638
|
|
@ -0,0 +1,7 @@
|
|||
[theme]
|
||||
base = "dark"
|
||||
primaryColor = "#22c55e"
|
||||
backgroundColor = "#0a0e17"
|
||||
secondaryBackgroundColor = "#111827"
|
||||
textColor = "#e2e8f0"
|
||||
font = "monospace"
|
||||
|
|
@ -39,7 +39,7 @@ def render() -> None:
|
|||
|
||||
best_strat_name = "N/A"
|
||||
best_strat_wr = 0.0
|
||||
for item in (strategy_metrics or []):
|
||||
for item in strategy_metrics or []:
|
||||
wr = item.get("Win Rate", 0) or 0
|
||||
if wr > best_strat_wr:
|
||||
best_strat_wr = wr
|
||||
|
|
@ -48,11 +48,25 @@ def render() -> None:
|
|||
# ---- KPI Row ----
|
||||
cols = st.columns(5)
|
||||
kpis = [
|
||||
("Win Rate 7d", f"{win_rate_7d:.0f}%", f"+{win_rate_7d - 50:.0f}pp vs 50%" if win_rate_7d >= 50 else f"{win_rate_7d - 50:.0f}pp vs 50%", "green" if win_rate_7d >= 50 else "red"),
|
||||
(
|
||||
"Win Rate 7d",
|
||||
f"{win_rate_7d:.0f}%",
|
||||
(
|
||||
f"+{win_rate_7d - 50:.0f}pp vs 50%"
|
||||
if win_rate_7d >= 50
|
||||
else f"{win_rate_7d - 50:.0f}pp vs 50%"
|
||||
),
|
||||
"green" if win_rate_7d >= 50 else "red",
|
||||
),
|
||||
("Avg Return 7d", f"{avg_return_7d:+.2f}%", "", "green" if avg_return_7d > 0 else "red"),
|
||||
("Open Positions", str(open_count), "", "blue"),
|
||||
("Total Signals", str(total_recs), "", "amber"),
|
||||
("Top Strategy", best_strat_name.upper(), f"{best_strat_wr:.0f}% WR" if best_strat_wr else "", "green" if best_strat_wr >= 60 else "amber"),
|
||||
(
|
||||
"Top Strategy",
|
||||
best_strat_name.upper(),
|
||||
f"{best_strat_wr:.0f}% WR" if best_strat_wr else "",
|
||||
"green" if best_strat_wr >= 60 else "amber",
|
||||
),
|
||||
]
|
||||
for col, (label, value, delta, color) in zip(cols, kpis):
|
||||
with col:
|
||||
|
|
@ -80,21 +94,46 @@ def render() -> None:
|
|||
size="Count",
|
||||
color="Strategy",
|
||||
hover_name="Strategy",
|
||||
hover_data={"Win Rate": ":.1f", "Avg Return": ":.2f", "Count": True, "Strategy": False},
|
||||
hover_data={
|
||||
"Win Rate": ":.1f",
|
||||
"Avg Return": ":.2f",
|
||||
"Count": True,
|
||||
"Strategy": False,
|
||||
},
|
||||
labels={"Win Rate": "Win Rate (%)", "Avg Return": "Avg Return (%)"},
|
||||
size_max=40,
|
||||
)
|
||||
|
||||
fig.add_hline(y=0, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
|
||||
fig.add_vline(x=50, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.4)
|
||||
fig.add_annotation(x=75, y=5, text="WINNERS", showarrow=False, font=dict(size=10, color=COLORS["green"], family="JetBrains Mono"), opacity=0.3)
|
||||
fig.add_annotation(x=25, y=-5, text="LOSERS", showarrow=False, font=dict(size=10, color=COLORS["red"], family="JetBrains Mono"), opacity=0.3)
|
||||
fig.add_annotation(
|
||||
x=75,
|
||||
y=5,
|
||||
text="WINNERS",
|
||||
showarrow=False,
|
||||
font=dict(size=10, color=COLORS["green"], family="JetBrains Mono"),
|
||||
opacity=0.3,
|
||||
)
|
||||
fig.add_annotation(
|
||||
x=25,
|
||||
y=-5,
|
||||
text="LOSERS",
|
||||
showarrow=False,
|
||||
font=dict(size=10, color=COLORS["red"], family="JetBrains Mono"),
|
||||
opacity=0.3,
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
**template,
|
||||
height=380,
|
||||
showlegend=True,
|
||||
legend=dict(bgcolor="rgba(0,0,0,0)", font=dict(size=10), orientation="h", yanchor="bottom", y=-0.25),
|
||||
legend=dict(
|
||||
bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(size=10),
|
||||
orientation="h",
|
||||
yanchor="bottom",
|
||||
y=-0.25,
|
||||
),
|
||||
)
|
||||
st.plotly_chart(fig, width="stretch")
|
||||
else:
|
||||
|
|
@ -118,9 +157,17 @@ def render() -> None:
|
|||
entry = rec.get("entry_price")
|
||||
entry_str = f"${entry:.2f}" if entry else "N/A"
|
||||
|
||||
score_color = COLORS["green"] if score >= 35 else (COLORS["amber"] if score >= 20 else COLORS["text_muted"])
|
||||
score_color = (
|
||||
COLORS["green"]
|
||||
if score >= 35
|
||||
else (COLORS["amber"] if score >= 20 else COLORS["text_muted"])
|
||||
)
|
||||
conf_bar_w = conf * 10
|
||||
conf_color = COLORS["green"] if conf >= 8 else (COLORS["amber"] if conf >= 6 else COLORS["red"])
|
||||
conf_color = (
|
||||
COLORS["green"]
|
||||
if conf >= 8
|
||||
else (COLORS["amber"] if conf >= 6 else COLORS["red"])
|
||||
)
|
||||
|
||||
st.markdown(
|
||||
f"""
|
||||
|
|
@ -156,7 +203,9 @@ def render() -> None:
|
|||
)
|
||||
|
||||
if len(recs) > 6:
|
||||
st.caption(f"+{len(recs) - 6} more signals. Switch to Signals page for the full list.")
|
||||
st.caption(
|
||||
f"+{len(recs) - 6} more signals. Switch to Signals page for the full list."
|
||||
)
|
||||
else:
|
||||
st.info("No signals generated today.")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,18 +11,23 @@ import plotly.graph_objects as go
|
|||
import streamlit as st
|
||||
|
||||
from tradingagents.ui.theme import COLORS, get_plotly_template, page_header
|
||||
from tradingagents.ui.utils import load_performance_database, load_statistics, load_strategy_metrics
|
||||
from tradingagents.ui.utils import load_statistics, load_strategy_metrics
|
||||
|
||||
|
||||
def render() -> None:
|
||||
"""Render the performance analytics page."""
|
||||
st.markdown(page_header("Performance", "Strategy analytics & win/loss breakdown"), unsafe_allow_html=True)
|
||||
st.markdown(
|
||||
page_header("Performance", "Strategy analytics & win/loss breakdown"),
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
strategy_metrics = load_strategy_metrics()
|
||||
stats = load_statistics()
|
||||
|
||||
if not strategy_metrics:
|
||||
st.warning("No performance data available yet. Run the discovery pipeline and track outcomes.")
|
||||
st.warning(
|
||||
"No performance data available yet. Run the discovery pipeline and track outcomes."
|
||||
)
|
||||
return
|
||||
|
||||
template = get_plotly_template()
|
||||
|
|
@ -105,15 +110,17 @@ def render() -> None:
|
|||
df_sorted = df.sort_values("Win Rate", ascending=True)
|
||||
colors = [COLORS["green"] if wr >= 50 else COLORS["red"] for wr in df_sorted["Win Rate"]]
|
||||
|
||||
fig_bar = go.Figure(go.Bar(
|
||||
x=df_sorted["Win Rate"],
|
||||
y=df_sorted["Strategy"],
|
||||
orientation="h",
|
||||
marker_color=colors,
|
||||
text=[f"{wr:.0f}%" for wr in df_sorted["Win Rate"]],
|
||||
textposition="auto",
|
||||
textfont=dict(family="JetBrains Mono", size=11, color=COLORS["text_primary"]),
|
||||
))
|
||||
fig_bar = go.Figure(
|
||||
go.Bar(
|
||||
x=df_sorted["Win Rate"],
|
||||
y=df_sorted["Strategy"],
|
||||
orientation="h",
|
||||
marker_color=colors,
|
||||
text=[f"{wr:.0f}%" for wr in df_sorted["Win Rate"]],
|
||||
textposition="auto",
|
||||
textfont=dict(family="JetBrains Mono", size=11, color=COLORS["text_primary"]),
|
||||
)
|
||||
)
|
||||
|
||||
fig_bar.add_vline(x=50, line_dash="dot", line_color=COLORS["text_muted"], opacity=0.5)
|
||||
|
||||
|
|
@ -158,16 +165,22 @@ def render() -> None:
|
|||
by_strat = stats["by_strategy"]
|
||||
rows = []
|
||||
for strat_name, data in by_strat.items():
|
||||
rows.append({
|
||||
"Strategy": strat_name,
|
||||
"Count": data.get("count", 0),
|
||||
"Win Rate 1d": f"{data.get('win_rate_1d', 0):.0f}%" if "win_rate_1d" in data else "N/A",
|
||||
"Win Rate 7d": f"{data.get('win_rate_7d', 0):.0f}%" if "win_rate_7d" in data else "N/A",
|
||||
"Wins 1d": data.get("wins_1d", 0),
|
||||
"Losses 1d": data.get("losses_1d", 0),
|
||||
"Wins 7d": data.get("wins_7d", 0),
|
||||
"Losses 7d": data.get("losses_7d", 0),
|
||||
})
|
||||
rows.append(
|
||||
{
|
||||
"Strategy": strat_name,
|
||||
"Count": data.get("count", 0),
|
||||
"Win Rate 1d": (
|
||||
f"{data.get('win_rate_1d', 0):.0f}%" if "win_rate_1d" in data else "N/A"
|
||||
),
|
||||
"Win Rate 7d": (
|
||||
f"{data.get('win_rate_7d', 0):.0f}%" if "win_rate_7d" in data else "N/A"
|
||||
),
|
||||
"Wins 1d": data.get("wins_1d", 0),
|
||||
"Losses 1d": data.get("losses_1d", 0),
|
||||
"Wins 7d": data.get("wins_7d", 0),
|
||||
"Losses 7d": data.get("losses_7d", 0),
|
||||
}
|
||||
)
|
||||
|
||||
if rows:
|
||||
st.dataframe(pd.DataFrame(rows), width="stretch", hide_index=True)
|
||||
|
|
|
|||
|
|
@ -30,19 +30,23 @@ def render():
|
|||
st.write("")
|
||||
if st.button("Add Position"):
|
||||
if ticker and entry_price > 0 and shares > 0:
|
||||
from tradingagents.dataflows.discovery.performance.position_tracker import PositionTracker
|
||||
from tradingagents.dataflows.discovery.performance.position_tracker import (
|
||||
PositionTracker,
|
||||
)
|
||||
|
||||
tracker = PositionTracker()
|
||||
pos = tracker.create_position({
|
||||
"ticker": ticker.upper(),
|
||||
"entry_price": entry_price,
|
||||
"shares": shares,
|
||||
"recommendation_date": datetime.now().isoformat(),
|
||||
"pipeline": "manual",
|
||||
"scanner": "manual",
|
||||
"strategy_match": "manual",
|
||||
"confidence": 5,
|
||||
})
|
||||
pos = tracker.create_position(
|
||||
{
|
||||
"ticker": ticker.upper(),
|
||||
"entry_price": entry_price,
|
||||
"shares": shares,
|
||||
"recommendation_date": datetime.now().isoformat(),
|
||||
"pipeline": "manual",
|
||||
"scanner": "manual",
|
||||
"strategy_match": "manual",
|
||||
"confidence": 5,
|
||||
}
|
||||
)
|
||||
tracker.save_position(pos)
|
||||
st.success(f"Added {ticker.upper()}")
|
||||
st.rerun()
|
||||
|
|
@ -74,7 +78,12 @@ def render():
|
|||
summary_kpis = [
|
||||
("Invested", f"${total_invested:,.0f}", "", "blue"),
|
||||
("Current Value", f"${total_current:,.0f}", "", "blue"),
|
||||
("P/L", f"${total_pnl:+,.0f}", f"{total_pnl_pct:+.1f}%", "green" if total_pnl >= 0 else "red"),
|
||||
(
|
||||
"P/L",
|
||||
f"${total_pnl:+,.0f}",
|
||||
f"{total_pnl_pct:+.1f}%",
|
||||
"green" if total_pnl >= 0 else "red",
|
||||
),
|
||||
("Positions", str(len(positions)), "", "amber"),
|
||||
]
|
||||
for col, (label, value, delta, color) in zip(cols, summary_kpis):
|
||||
|
|
@ -145,15 +154,17 @@ def render():
|
|||
data = []
|
||||
for p in positions:
|
||||
pnl = (p["metrics"]["current_price"] - p["entry_price"]) * p.get("shares", 0)
|
||||
data.append({
|
||||
"Ticker": p["ticker"],
|
||||
"Entry": p["entry_price"],
|
||||
"Current": p["metrics"]["current_price"],
|
||||
"Shares": p.get("shares", 0),
|
||||
"P/L": pnl,
|
||||
"P/L %": p["metrics"]["current_return"],
|
||||
"Days": p["metrics"]["days_held"],
|
||||
})
|
||||
data.append(
|
||||
{
|
||||
"Ticker": p["ticker"],
|
||||
"Entry": p["entry_price"],
|
||||
"Current": p["metrics"]["current_price"],
|
||||
"Shares": p.get("shares", 0),
|
||||
"P/L": pnl,
|
||||
"P/L %": p["metrics"]["current_return"],
|
||||
"Days": p["metrics"]["days_held"],
|
||||
}
|
||||
)
|
||||
st.dataframe(
|
||||
pd.DataFrame(data),
|
||||
width="stretch",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ from tradingagents.ui.theme import COLORS, page_header
|
|||
|
||||
def render() -> None:
|
||||
"""Render the configuration page."""
|
||||
st.markdown(page_header("Config", "Pipeline & scanner configuration (read-only)"), unsafe_allow_html=True)
|
||||
st.markdown(
|
||||
page_header("Config", "Pipeline & scanner configuration (read-only)"),
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
config = DEFAULT_CONFIG
|
||||
discovery_config = config.get("discovery", {})
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@ def _load_price_history(ticker: str, period: str) -> pd.DataFrame:
|
|||
return pd.DataFrame()
|
||||
|
||||
data = download_history(
|
||||
ticker, period=period, interval="1d", auto_adjust=True, progress=False,
|
||||
ticker,
|
||||
period=period,
|
||||
interval="1d",
|
||||
auto_adjust=True,
|
||||
progress=False,
|
||||
)
|
||||
if data is None or data.empty:
|
||||
return pd.DataFrame()
|
||||
|
|
@ -62,9 +66,11 @@ def render():
|
|||
# ---- Controls row ----
|
||||
ctrl_cols = st.columns([1, 1, 1, 1])
|
||||
with ctrl_cols[0]:
|
||||
pipelines = sorted(set(
|
||||
(r.get("pipeline") or r.get("strategy_match") or "unknown") for r in recommendations
|
||||
))
|
||||
pipelines = sorted(
|
||||
set(
|
||||
(r.get("pipeline") or r.get("strategy_match") or "unknown") for r in recommendations
|
||||
)
|
||||
)
|
||||
pipeline_filter = st.multiselect("Strategy", pipelines, default=pipelines)
|
||||
with ctrl_cols[1]:
|
||||
min_confidence = st.slider("Min Confidence", 1, 10, 1)
|
||||
|
|
@ -79,7 +85,8 @@ def render():
|
|||
|
||||
# Apply filters
|
||||
filtered = [
|
||||
r for r in recommendations
|
||||
r
|
||||
for r in recommendations
|
||||
if (r.get("pipeline") or r.get("strategy_match") or "unknown") in pipeline_filter
|
||||
and r.get("confidence", 0) >= min_confidence
|
||||
and r.get("final_score", 0) >= min_score
|
||||
|
|
@ -130,7 +137,9 @@ def render():
|
|||
history = _load_price_history(ticker, chart_window)
|
||||
if not history.empty:
|
||||
template = get_plotly_template()
|
||||
fig = px.line(history, x="date", y="close", labels={"date": "", "close": "Price"})
|
||||
fig = px.line(
|
||||
history, x="date", y="close", labels={"date": "", "close": "Price"}
|
||||
)
|
||||
|
||||
# Color line green if trending up, red if down
|
||||
first_close = history["close"].iloc[0]
|
||||
|
|
@ -145,7 +154,9 @@ def render():
|
|||
)
|
||||
fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
|
||||
fig.update_xaxes(showticklabels=False, showgrid=False)
|
||||
fig.update_yaxes(showgrid=True, gridcolor="rgba(42,53,72,0.3)", tickprefix="$")
|
||||
fig.update_yaxes(
|
||||
showgrid=True, gridcolor="rgba(42,53,72,0.3)", tickprefix="$"
|
||||
)
|
||||
st.plotly_chart(fig, width="stretch")
|
||||
|
||||
# Action buttons
|
||||
|
|
|
|||
|
|
@ -0,0 +1,596 @@
|
|||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
# -- 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"""
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=DM+Sans:wght@400;500;600;700&display=swap');
|
||||
|
||||
/* ---- Root overrides ---- */
|
||||
:root {{
|
||||
--bg-primary: {COLORS["bg_primary"]};
|
||||
--bg-secondary: {COLORS["bg_secondary"]};
|
||||
--bg-card: {COLORS["bg_card"]};
|
||||
--border: {COLORS["border"]};
|
||||
--text-primary: {COLORS["text_primary"]};
|
||||
--text-secondary: {COLORS["text_secondary"]};
|
||||
--green: {COLORS["green"]};
|
||||
--red: {COLORS["red"]};
|
||||
--amber: {COLORS["amber"]};
|
||||
--blue: {COLORS["blue"]};
|
||||
}}
|
||||
|
||||
/* ---- Global ---- */
|
||||
.stApp {{
|
||||
background-color: var(--bg-primary) !important;
|
||||
color: var(--text-primary);
|
||||
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}}
|
||||
|
||||
/* Hide default Streamlit chrome */
|
||||
header[data-testid="stHeader"] {{
|
||||
background-color: var(--bg-primary) !important;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}}
|
||||
|
||||
/* ---- Sidebar ---- */
|
||||
section[data-testid="stSidebar"] {{
|
||||
background-color: var(--bg-secondary) !important;
|
||||
border-right: 1px solid var(--border);
|
||||
}}
|
||||
section[data-testid="stSidebar"] .stRadio label {{
|
||||
color: var(--text-secondary) !important;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s ease;
|
||||
}}
|
||||
section[data-testid="stSidebar"] .stRadio label:hover {{
|
||||
background-color: var(--bg-card) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}}
|
||||
section[data-testid="stSidebar"] .stRadio label[data-checked="true"],
|
||||
section[data-testid="stSidebar"] .stRadio div[role="radiogroup"] label:has(input:checked) {{
|
||||
background-color: var(--bg-card) !important;
|
||||
color: var(--green) !important;
|
||||
border-left: 3px solid var(--green);
|
||||
}}
|
||||
|
||||
/* ---- Metric cards ---- */
|
||||
div[data-testid="stMetric"] {{
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem 1.25rem;
|
||||
}}
|
||||
div[data-testid="stMetric"] label {{
|
||||
color: var(--text-secondary) !important;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}}
|
||||
div[data-testid="stMetric"] div[data-testid="stMetricValue"] {{
|
||||
color: var(--text-primary) !important;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 600;
|
||||
}}
|
||||
div[data-testid="stMetric"] div[data-testid="stMetricDelta"] svg {{
|
||||
display: none;
|
||||
}}
|
||||
div[data-testid="stMetric"] div[data-testid="stMetricDelta"] > div {{
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-weight: 500;
|
||||
}}
|
||||
|
||||
/* ---- Custom KPI card classes ---- */
|
||||
.kpi-card {{
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}}
|
||||
.kpi-card::before {{
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}}
|
||||
.kpi-card.green::before {{ background: var(--green); }}
|
||||
.kpi-card.red::before {{ background: var(--red); }}
|
||||
.kpi-card.amber::before {{ background: var(--amber); }}
|
||||
.kpi-card.blue::before {{ background: var(--blue); }}
|
||||
.kpi-label {{
|
||||
color: var(--text-secondary);
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.3rem;
|
||||
}}
|
||||
.kpi-value {{
|
||||
color: var(--text-primary);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}}
|
||||
.kpi-delta {{
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
margin-top: 0.25rem;
|
||||
}}
|
||||
.kpi-delta.positive {{ color: var(--green); }}
|
||||
.kpi-delta.negative {{ color: var(--red); }}
|
||||
.kpi-delta.neutral {{ color: var(--text-muted); }}
|
||||
|
||||
/* ---- Signal card (recommendation) ---- */
|
||||
.signal-card {{
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}}
|
||||
.signal-card:hover {{
|
||||
border-color: {COLORS["border_active"]};
|
||||
box-shadow: 0 0 0 1px {COLORS["border_active"]};
|
||||
}}
|
||||
.signal-header {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}}
|
||||
.signal-ticker {{
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}}
|
||||
.signal-rank {{
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.signal-badges {{
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.75rem;
|
||||
}}
|
||||
.badge {{
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}}
|
||||
.badge-green {{
|
||||
background: {COLORS["green_glow"]};
|
||||
color: var(--green);
|
||||
border: 1px solid {COLORS["green_dim"]};
|
||||
}}
|
||||
.badge-red {{
|
||||
background: {COLORS["red_glow"]};
|
||||
color: var(--red);
|
||||
border: 1px solid {COLORS["red_dim"]};
|
||||
}}
|
||||
.badge-amber {{
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: var(--amber);
|
||||
border: 1px solid {COLORS["amber_dim"]};
|
||||
}}
|
||||
.badge-blue {{
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: var(--blue);
|
||||
border: 1px solid {COLORS["blue_dim"]};
|
||||
}}
|
||||
.badge-muted {{
|
||||
background: rgba(100, 116, 139, 0.15);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid {COLORS["border"]};
|
||||
}}
|
||||
.signal-metrics {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}}
|
||||
.signal-metric {{
|
||||
text-align: center;
|
||||
}}
|
||||
.signal-metric-label {{
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-weight: 600;
|
||||
}}
|
||||
.signal-metric-value {{
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-top: 0.15rem;
|
||||
}}
|
||||
.signal-thesis {{
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text-secondary);
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--border);
|
||||
}}
|
||||
|
||||
/* ---- Confidence bar ---- */
|
||||
.conf-bar {{
|
||||
height: 4px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
}}
|
||||
.conf-fill {{
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}}
|
||||
|
||||
/* ---- Strategy tag colors ---- */
|
||||
.strat-momentum {{ border-left-color: var(--green) !important; }}
|
||||
.strat-insider {{ border-left-color: var(--amber) !important; }}
|
||||
.strat-earnings {{ border-left-color: var(--blue) !important; }}
|
||||
.strat-volume {{ border-left-color: {COLORS["cyan"]} !important; }}
|
||||
.strat-options {{ border-left-color: {COLORS["purple"]} !important; }}
|
||||
|
||||
/* ---- Table styling ---- */
|
||||
.stDataFrame {{
|
||||
background: var(--bg-card) !important;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}}
|
||||
div[data-testid="stDataFrame"] table {{
|
||||
font-family: 'JetBrains Mono', monospace !important;
|
||||
font-size: 0.8rem !important;
|
||||
}}
|
||||
div[data-testid="stDataFrame"] th {{
|
||||
background: var(--bg-secondary) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
font-family: 'DM Sans', sans-serif !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.7rem !important;
|
||||
letter-spacing: 0.05em;
|
||||
}}
|
||||
|
||||
/* ---- Expander ---- */
|
||||
.streamlit-expanderHeader {{
|
||||
background: var(--bg-card) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: 8px !important;
|
||||
font-family: 'DM Sans', sans-serif !important;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary) !important;
|
||||
}}
|
||||
|
||||
/* ---- Buttons ---- */
|
||||
.stButton > button {{
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.15s ease;
|
||||
}}
|
||||
.stButton > button:hover {{
|
||||
border-color: var(--green);
|
||||
color: var(--green);
|
||||
background: {COLORS["green_glow"]};
|
||||
}}
|
||||
|
||||
/* ---- Divider ---- */
|
||||
hr {{
|
||||
border-color: var(--border) !important;
|
||||
opacity: 0.5;
|
||||
}}
|
||||
|
||||
/* ---- Subheader ---- */
|
||||
.stMarkdown h2, .stMarkdown h3 {{
|
||||
font-family: 'DM Sans', sans-serif !important;
|
||||
color: var(--text-primary) !important;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}}
|
||||
|
||||
/* ---- Selectbox & slider ---- */
|
||||
div[data-baseweb="select"] {{
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}}
|
||||
|
||||
/* ---- Scrollbar ---- */
|
||||
::-webkit-scrollbar {{
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}}
|
||||
::-webkit-scrollbar-track {{
|
||||
background: var(--bg-primary);
|
||||
}}
|
||||
::-webkit-scrollbar-thumb {{
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}}
|
||||
::-webkit-scrollbar-thumb:hover {{
|
||||
background: {COLORS["text_muted"]};
|
||||
}}
|
||||
|
||||
/* ---- Section title with mono accent ---- */
|
||||
.section-title {{
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}}
|
||||
.section-title .accent {{
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--green);
|
||||
font-size: 0.85rem;
|
||||
}}
|
||||
|
||||
/* ---- Page header ---- */
|
||||
.page-header {{
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}}
|
||||
.page-header h1 {{
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1.6rem;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
letter-spacing: -0.02em;
|
||||
}}
|
||||
.page-header .subtitle {{
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.25rem;
|
||||
}}
|
||||
|
||||
/* ---- Position row ---- */
|
||||
.pos-row {{
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr repeat(5, 90px);
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
}}
|
||||
.pos-ticker {{
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
}}
|
||||
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
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'<div class="kpi-delta {delta_class}">{delta}</div>' if delta else ""
|
||||
return f"""
|
||||
<div class="kpi-card {color}">
|
||||
<div class="kpi-label">{label}</div>
|
||||
<div class="kpi-value">{value}</div>
|
||||
{delta_html}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def page_header(title: str, subtitle: str = "") -> str:
|
||||
"""Render a page header as HTML."""
|
||||
sub = f'<div class="subtitle">{subtitle}</div>' if subtitle else ""
|
||||
return f"""
|
||||
<div class="page-header">
|
||||
<h1>{title}</h1>
|
||||
{sub}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def signal_card(
|
||||
rank: int,
|
||||
ticker: str,
|
||||
score: int,
|
||||
confidence: int,
|
||||
strategy: str,
|
||||
entry_price: float,
|
||||
reason: 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
|
||||
|
||||
return f"""
|
||||
<div class="signal-card {strat_css}">
|
||||
<div class="signal-header">
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;">
|
||||
<span class="signal-ticker">{ticker}</span>
|
||||
<span class="signal-rank">#{rank}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="signal-badges">
|
||||
<span class="badge {strat_badge}">{strategy}</span>
|
||||
<span class="badge {score_badge}">Score {score}</span>
|
||||
<span class="badge badge-muted">Conf {confidence}/10</span>
|
||||
</div>
|
||||
<div class="signal-metrics">
|
||||
<div class="signal-metric">
|
||||
<div class="signal-metric-label">Entry</div>
|
||||
<div class="signal-metric-value">{entry_str}</div>
|
||||
</div>
|
||||
<div class="signal-metric">
|
||||
<div class="signal-metric-label">Score</div>
|
||||
<div class="signal-metric-value">{score}</div>
|
||||
</div>
|
||||
<div class="signal-metric">
|
||||
<div class="signal-metric-label">Confidence</div>
|
||||
<div class="signal-metric-value">{confidence}/10</div>
|
||||
</div>
|
||||
<div class="signal-metric">
|
||||
<div class="signal-metric-label">Strategy</div>
|
||||
<div class="signal-metric-value" style="font-size:0.75rem;">{strategy.upper()}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="signal-thesis">{reason}</div>
|
||||
<div class="conf-bar">
|
||||
<div class="conf-fill" style="width:{conf_pct}%;background:{bar_color};"></div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
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"]
|
||||
Loading…
Reference in New Issue