From 1ead4d9638e509d47e10fd4963805f63c5bd18d6 Mon Sep 17 00:00:00 2001 From: Youssef Aitousarrah Date: Tue, 10 Feb 2026 22:40:08 -0800 Subject: [PATCH] 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 --- .streamlit/config.toml | 7 + tradingagents/ui/pages/home.py | 69 ++- tradingagents/ui/pages/performance.py | 57 ++- tradingagents/ui/pages/portfolio.py | 53 ++- tradingagents/ui/pages/settings.py | 5 +- tradingagents/ui/pages/todays_picks.py | 25 +- tradingagents/ui/theme.py | 596 +++++++++++++++++++++++++ 7 files changed, 751 insertions(+), 61 deletions(-) create mode 100644 .streamlit/config.toml create mode 100644 tradingagents/ui/theme.py diff --git a/.streamlit/config.toml b/.streamlit/config.toml new file mode 100644 index 00000000..56f3f893 --- /dev/null +++ b/.streamlit/config.toml @@ -0,0 +1,7 @@ +[theme] +base = "dark" +primaryColor = "#22c55e" +backgroundColor = "#0a0e17" +secondaryBackgroundColor = "#111827" +textColor = "#e2e8f0" +font = "monospace" diff --git a/tradingagents/ui/pages/home.py b/tradingagents/ui/pages/home.py index 1f88f180..47678e66 100644 --- a/tradingagents/ui/pages/home.py +++ b/tradingagents/ui/pages/home.py @@ -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.") diff --git a/tradingagents/ui/pages/performance.py b/tradingagents/ui/pages/performance.py index f5ae1f20..130d5bc0 100644 --- a/tradingagents/ui/pages/performance.py +++ b/tradingagents/ui/pages/performance.py @@ -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) diff --git a/tradingagents/ui/pages/portfolio.py b/tradingagents/ui/pages/portfolio.py index 09655251..074b20dd 100644 --- a/tradingagents/ui/pages/portfolio.py +++ b/tradingagents/ui/pages/portfolio.py @@ -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", diff --git a/tradingagents/ui/pages/settings.py b/tradingagents/ui/pages/settings.py index a8c491ae..5508af90 100644 --- a/tradingagents/ui/pages/settings.py +++ b/tradingagents/ui/pages/settings.py @@ -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", {}) diff --git a/tradingagents/ui/pages/todays_picks.py b/tradingagents/ui/pages/todays_picks.py index 4f8ddff5..525e61f4 100644 --- a/tradingagents/ui/pages/todays_picks.py +++ b/tradingagents/ui/pages/todays_picks.py @@ -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 diff --git a/tradingagents/ui/theme.py b/tradingagents/ui/theme.py new file mode 100644 index 00000000..41ee4954 --- /dev/null +++ b/tradingagents/ui/theme.py @@ -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""" + +""" + + +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, +) -> 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""" +
+
+
+ {ticker} + #{rank} +
+
+
+ {strategy} + Score {score} + Conf {confidence}/10 +
+
+
+
Entry
+
{entry_str}
+
+
+
Score
+
{score}
+
+
+
Confidence
+
{confidence}/10
+
+
+
Strategy
+
{strategy.upper()}
+
+
+
{reason}
+
+
+
+
+ """ + + +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"]