""" CommodiSense Dashboard — Global Commodity Intelligence Engine Dark luxury financial terminal UI. Run: streamlit run dashboard/app.py Deploy: Streamlit Cloud → main file: dashboard/app.py → secret: GROQ_API_KEY """ import sys from datetime import date, datetime, timedelta from pathlib import Path import pandas as pd import plotly.graph_objects as go import streamlit as st ROOT = Path(__file__).parent.parent sys.path.insert(0, str(ROOT)) from data.db import get_conn, init_schema from model.explainer import load_latest_reports, generate_report from model.predictor import predict, SYMBOL_NAMES # ── page config ──────────────────────────────────────────────────────────────── st.set_page_config( page_title="CommodiSense", page_icon="◈", layout="wide", initial_sidebar_state="collapsed", ) # ── design tokens ────────────────────────────────────────────────────────────── C = { "bg": "#060A0F", "surface": "#0D1117", "surface2": "#161B22", "border": "rgba(255,255,255,0.07)", "border_hi": "rgba(255,255,255,0.14)", "up": "#00D97E", "down": "#FF3B55", "stable": "#7A8899", "up_dim": "rgba(0,217,126,0.12)", "down_dim": "rgba(255,59,85,0.12)", "stable_dim": "rgba(122,136,153,0.10)", "accent": "#3D7FFF", "accent_dim": "rgba(61,127,255,0.12)", "gold": "#FFBB00", "text": "#E6EDF3", "text2": "#8B949E", "text3": "#484F58", "conf_high": "#00D97E", "conf_mid": "#FFBB00", "conf_low": "#7A8899", } DIR_COLOR = {"UP": C["up"], "DOWN": C["down"], "STABLE": C["stable"]} DIR_DIM = {"UP": C["up_dim"],"DOWN": C["down_dim"],"STABLE": C["stable_dim"]} DIR_ICON = {"UP": "▲", "DOWN": "▼", "STABLE": "◆"} CONF_COLOR = {"HIGH": C["conf_high"], "MEDIUM": C["conf_mid"], "LOW": C["conf_low"]} ALL_SYMBOLS = list(SYMBOL_NAMES.keys()) # ── CSS ──────────────────────────────────────────────────────────────────────── def _inject_css(): st.markdown(f""" """, unsafe_allow_html=True) # ── data loaders ─────────────────────────────────────────────────────────────── @st.cache_resource def _ensure_schema(): init_schema() @st.cache_resource def _ensure_prices(): """Keep prices up to date on every container start. - If DB is empty: full 5-year backfill from yfinance. - If DB has data but is stale (latest < yesterday): fetch missing days only. Runs once per process lifetime.""" try: import yfinance as yf from datetime import date as _date, timedelta conn = get_conn() row = conn.execute( "SELECT COUNT(*), MAX(date) FROM prices" ).fetchone() conn.close() count, latest_date = row[0], row[1] yesterday = (_date.today() - timedelta(days=1)).isoformat() # Nothing to do if already current if count > 100 and latest_date and str(latest_date) >= yesterday: return # Full backfill for empty DB, incremental top-up otherwise period = "5y" if count < 100 else None start = None if period else (str(latest_date) if latest_date else "2020-01-01") symbols = list(SYMBOL_NAMES.keys()) kwargs = dict(auto_adjust=True, progress=False) if period: kwargs["period"] = period else: kwargs["start"] = start ticker_data = yf.download(symbols, **kwargs) if ticker_data.empty: return conn2 = get_conn() for sym in symbols: try: df = ticker_data.xs(sym, axis=1, level=1) if len(symbols) > 1 else ticker_data.copy() if df is None or df.empty: continue df = df.reset_index() df.columns = [c.lower() for c in df.columns] for _, r in df.iterrows(): try: conn2.execute( "INSERT OR REPLACE INTO prices " "(date,symbol,open,high,low,close,volume,adj_close) " "VALUES (?,?,?,?,?,?,?,?)", [str(r["date"])[:10], sym, float(r.get("open") or 0), float(r.get("high") or 0), float(r.get("low") or 0), float(r.get("close") or 0), float(r.get("volume") or 0), float(r.get("close") or 0)] ) except Exception: pass except Exception: pass conn2.close() except Exception: pass @st.cache_resource def _retag_news(): """Retag all news articles using the expanded keyword lists. Fixes articles with empty tags and re-applies broader matching once per process.""" try: from data.collector_news import COMMODITY_KEYWORDS conn = get_conn() rows = conn.execute( "SELECT id, title, summary FROM news_raw" ).fetchall() updated = 0 for row_id, title, summary in rows: text = ((title or "") + " " + (summary or "")).lower() tags = [sym for sym, kws in COMMODITY_KEYWORDS.items() if any(k in text for k in kws)] if tags: conn.execute( "UPDATE news_raw SET commodity_tags = ? WHERE id = ?", [",".join(tags), row_id] ) updated += 1 conn.close() except Exception: pass @st.cache_data(ttl=3600) def _load_forecast(symbol: str) -> dict: return predict(symbol) @st.cache_data(ttl=3600) def _load_all_forecasts(symbols: tuple) -> dict: return {s: _load_forecast(s) for s in symbols} @st.cache_data(ttl=3600) def _load_price_history(symbol: str, days: int = 90) -> pd.DataFrame: conn = get_conn() df = conn.execute( "SELECT date, open, high, low, close FROM prices " "WHERE symbol = ? AND date >= ? ORDER BY date", [symbol, (date.today() - timedelta(days=days)).isoformat()], ).df() conn.close() return df @st.cache_data(ttl=3600) def _load_sentiment_history(symbol: str, days: int = 60) -> pd.DataFrame: conn = get_conn() df = conn.execute( "SELECT date, sentiment_score, article_count FROM sentiment_daily " "WHERE commodity = ? AND date >= ? ORDER BY date", [symbol, (date.today() - timedelta(days=days)).isoformat()], ).df() conn.close() return df @st.cache_data(ttl=3600) def _load_cot_history(symbol: str, weeks: int = 104) -> pd.DataFrame: conn = get_conn() df = conn.execute( "SELECT date, commercial_net_pct, mm_net_pct, open_interest " "FROM cot_data WHERE symbol = ? ORDER BY date DESC LIMIT ?", [symbol, weeks], ).df() conn.close() return df.sort_values("date").reset_index(drop=True) if not df.empty else df @st.cache_data(ttl=3600) def _load_macro_env() -> dict: conn = get_conn() try: row = conn.execute( "SELECT dxy, vix, treasury_10y, fedfunds, financial_stress, copper_basis " "FROM fred_data WHERE dxy IS NOT NULL ORDER BY date DESC LIMIT 1" ).fetchone() except Exception: row = None conn.close() if row: return {"dxy": row[0], "vix": row[1], "t10y": row[2], "fedfunds": row[3], "stress": row[4], "copper_basis": row[5]} return {} @st.cache_data(ttl=1800) # 30-min cache — live news def _load_recent_news(symbol: str, limit: int = 20) -> pd.DataFrame: """Fetch live news from Yahoo Finance RSS, fall back to DB.""" rows = [] try: import feedparser, urllib.parse # Some tickers need remapping for Yahoo Finance RSS _YF_RSS_MAP = {"USDINR=X": "INR=X"} rss_ticker = _YF_RSS_MAP.get(symbol, symbol) url = f"https://feeds.finance.yahoo.com/rss/2.0/headline?s={urllib.parse.quote(rss_ticker)}®ion=US&lang=en-US" feed = feedparser.parse(url) for entry in feed.entries[:limit]: pub = entry.get("published", "") try: from email.utils import parsedate_to_datetime pub_dt = parsedate_to_datetime(pub).strftime("%Y-%m-%d %H:%M") except Exception: pub_dt = pub[:16] rows.append({ "published_date": pub_dt, "title": entry.get("title", ""), "url": entry.get("link", "#"), "sentiment_score": 0.0, "source": "Yahoo Finance", }) except Exception: pass # Fallback to DB if Yahoo RSS returned nothing if not rows: conn = get_conn() db_df = conn.execute( "SELECT published_date, title, url, sentiment_score FROM news_raw " "WHERE commodity_tags LIKE ? ORDER BY published_date DESC LIMIT ?", [f"%{symbol}%", limit], ).df() conn.close() if not db_df.empty: db_df["source"] = "GDELT" return db_df if rows: return pd.DataFrame(rows) return pd.DataFrame(columns=["published_date", "title", "url", "sentiment_score", "source"]) @st.cache_data(ttl=3600) def _load_weather(symbol: str) -> dict: from signals.weather_features import get_weather_features return get_weather_features(symbol, days=30) @st.cache_data(ttl=3600) def _load_eia_history(series: str, weeks: int = 52) -> pd.DataFrame: conn = get_conn() df = conn.execute( "SELECT date, value, chg_1w, vs_5yr_avg FROM eia_inventory " "WHERE series = ? ORDER BY date DESC LIMIT ?", [series, weeks], ).df() conn.close() return df.sort_values("date").reset_index(drop=True) if not df.empty else df # ── header ───────────────────────────────────────────────────────────────────── def _render_header(): now = datetime.now() st.markdown(f"""
◈ CommodiSense
LIVE
Global Commodity Intelligence
{now.strftime('%a %d %b %Y %H:%M')} UTC
""", unsafe_allow_html=True) # ── ticker strip ─────────────────────────────────────────────────────────────── def _render_ticker(forecasts: dict, horizon_key: str): fk = "forecast_7d" if horizon_key == "7d" else "forecast_30d" items_html = "" for sym in ALL_SYMBOLS: fc = forecasts.get(sym, {}) if "error" in fc or not fc: continue f = fc.get(fk, {}) price = fc.get("current_price", 0) dir_ = f.get("direction", "STABLE") prob = f.get("probability", 0) icon = DIR_ICON.get(dir_, "◆") col = DIR_COLOR.get(dir_, C["stable"]) name = SYMBOL_NAMES.get(sym, sym).upper() items_html += f"""
{sym} {name} ${price:,.2f} {icon} {prob:.0%}
""" # Double for seamless loop st.markdown(f"""
{items_html}{items_html}
""", unsafe_allow_html=True) # ── macro environment bar ────────────────────────────────────────────────────── def _render_macro_bar(): macro = _load_macro_env() if not macro: return def _change_html(val, neutral=0, invert=False, fmt=".2f", suffix=""): if val is None: return "" diff = val - neutral if invert: diff = -diff col = C["up"] if diff > 0 else (C["down"] if diff < 0 else C["stable"]) sign = "+" if diff > 0 else "" return f'{sign}{diff:{fmt}}{suffix}' vix = macro.get("vix") or 0 vix_regime = "HIGH FEAR" if vix > 30 else ("CAUTION" if vix > 20 else "CALM") vix_col = C["down"] if vix > 30 else (C["gold"] if vix > 20 else C["up"]) dxy = macro.get("dxy") or 0 t10y = macro.get("t10y") or 0 ff = macro.get("fedfunds") or 0 yield_inv = t10y < ff spread = t10y - ff st.markdown(f"""
USD Index (DXY)
{dxy:.1f}
Broad USD Strength
VIX Volatility
{vix:.1f}
{vix_regime}
10Y Treasury
{t10y:.2f}%
US Yield
Fed Funds
{ff:.2f}%
Policy Rate
Yield Spread
{spread:+.2f}%
{'⚠ INVERTED' if yield_inv else 'Normal'}
Copper 3M Trend
{(macro.get('copper_basis') or 0):+.1f}%
Industrial Demand
""", unsafe_allow_html=True) # ── commodity grid ───────────────────────────────────────────────────────────── def _render_commodity_grid(forecasts: dict, horizon_key: str, active_sym: str) -> str | None: fk = "forecast_7d" if horizon_key == "7d" else "forecast_30d" st.markdown(f"""
Market Overview — {horizon_key.upper()} Forecast
""", unsafe_allow_html=True) clicked = None rows = [ALL_SYMBOLS[i:i+5] for i in range(0, len(ALL_SYMBOLS), 5)] for row_syms in rows: cols = st.columns(len(row_syms)) for col, sym in zip(cols, row_syms): fc = forecasts.get(sym, {}) f = fc.get(fk, {}) if fc and "error" not in fc else {} dir_ = f.get("direction", "STABLE") conf = f.get("confidence", "LOW") prob = f.get("probability", 0.5) price = fc.get("current_price", 0) if fc else 0 name = SYMBOL_NAMES.get(sym, sym) icon = DIR_ICON.get(dir_, "◆") dcol = DIR_COLOR.get(dir_, C["stable"]) ddim = DIR_DICT = DIR_DIM.get(dir_, C["stable_dim"]) ccol = CONF_COLOR.get(conf, C["conf_low"]) is_active = sym == active_sym warn = fc.get("forecast_7d", {}).get("model_warning") if fc else None with col: st.markdown(f"""
{sym}
{name}
{conf}
${price:,.2f}
{icon}
{dir_}
{prob:.0%} probability
{'
⚠ Use 30d model
' if warn and horizon_key == "7d" else ''}
""", unsafe_allow_html=True) if st.button("Analyze →", key=f"btn_{sym}", use_container_width=True): clicked = sym return clicked # ── deep dive ────────────────────────────────────────────────────────────────── def _price_chart(symbol: str, days: int, fc: dict, horizon_key: str): df = _load_price_history(symbol, days) if df.empty: st.info("No price history — run the price collector.") return fk = "forecast_7d" if horizon_key == "7d" else "forecast_30d" fcast = fc.get(fk, {}) dir_ = fcast.get("direction", "STABLE") dcol = DIR_COLOR.get(dir_, C["stable"]) low = fcast.get("price_range_low") high = fcast.get("price_range_high") fig = go.Figure() fig.add_trace(go.Candlestick( x=df["date"], open=df["open"], high=df["high"], low=df["low"], close=df["close"], name="Price", increasing=dict(line=dict(color=C["up"], width=1), fillcolor=C["up_dim"]), decreasing=dict(line=dict(color=C["down"], width=1), fillcolor=C["down_dim"]), )) # 20-day SMA df["sma20"] = df["close"].rolling(20, min_periods=1).mean() fig.add_trace(go.Scatter( x=df["date"], y=df["sma20"], mode="lines", line=dict(color=C["accent"], width=1.2, dash="dot"), name="SMA 20", opacity=0.6, )) # Forecast zone if low and high and not df.empty: last_date = pd.to_datetime(df["date"].max()) fwd = last_date + timedelta(days=7 if horizon_key == "7d" else 30) fig.add_shape(type="rect", x0=str(last_date.date()), x1=str(fwd.date()), y0=low, y1=high, fillcolor=dcol, opacity=0.10, line=dict(color=dcol, width=1, dash="dot"), ) fig.add_annotation( x=str(fwd.date()), y=(low + high) / 2, text=f" {DIR_ICON.get(dir_,'')} {dir_} {fcast.get('probability',0):.0%}", showarrow=False, font=dict(color=dcol, size=11, family="JetBrains Mono"), bgcolor=C["surface2"], bordercolor=dcol, ) fig.update_layout( template="plotly_dark", paper_bgcolor=C["bg"], plot_bgcolor=C["bg"], xaxis_rangeslider_visible=False, height=360, margin=dict(l=0, r=0, t=8, b=0), legend=dict(orientation="h", x=0, y=1.06, font=dict(size=10, color=C["text2"])), xaxis=dict(gridcolor=C["surface2"], showgrid=True), yaxis=dict(gridcolor=C["surface2"], showgrid=True), font=dict(family="Inter", color=C["text2"]), ) st.plotly_chart(fig, use_container_width=True) def _shap_chart(fc: dict): signals = fc.get("top_signals", []) if not signals: st.caption("No SHAP signals — retrain models to enable.") return labels = [s.get("label", s.get("feature", ""))[:32] for s in signals] weights = [s["weight"] if s["impact"] == "BULLISH" else -s["weight"] for s in signals] colors = [C["up"] if w > 0 else C["down"] for w in weights] fig = go.Figure(go.Bar( x=weights, y=labels, orientation="h", marker=dict(color=colors, opacity=0.85), text=[f"{'▲' if w>0 else '▼'} {abs(w):.3f}" for w in weights], textposition="outside", textfont=dict(size=10, family="JetBrains Mono", color=C["text2"]), )) fig.update_layout( template="plotly_dark", paper_bgcolor=C["bg"], plot_bgcolor=C["bg"], title=dict(text="Top Signal Drivers (SHAP)", font=dict(size=11, color=C["text2"])), xaxis=dict(gridcolor=C["surface2"], zeroline=True, zerolinecolor=C["border_hi"], showticklabels=False), yaxis=dict(gridcolor="transparent"), height=260, margin=dict(l=0, r=40, t=32, b=0), showlegend=False, ) st.plotly_chart(fig, use_container_width=True) def _cot_chart(symbol: str): df = _load_cot_history(symbol) if df.empty: st.caption("No COT data for this symbol.") return fig = go.Figure() fig.add_trace(go.Scatter( x=df["date"], y=df["commercial_net_pct"] * 100, mode="lines", fill="tozeroy", line=dict(color=C["up"], width=1.5), fillcolor="rgba(0,217,126,0.08)", name="Commercial (Smart $)", )) fig.add_trace(go.Scatter( x=df["date"], y=df["mm_net_pct"] * 100, mode="lines", fill="tozeroy", line=dict(color=C["accent"], width=1.5), fillcolor="rgba(61,127,255,0.08)", name="Managed Money", )) fig.add_hline(y=0, line_dash="dot", line_color=C["border_hi"], line_width=1) fig.update_layout( template="plotly_dark", paper_bgcolor=C["bg"], plot_bgcolor=C["bg"], title=dict(text="COT Positioning — % of Open Interest", font=dict(size=11, color=C["text2"])), xaxis=dict(gridcolor=C["surface2"]), yaxis=dict(gridcolor=C["surface2"], ticksuffix="%"), height=220, margin=dict(l=0, r=0, t=32, b=0), legend=dict(orientation="h", x=0, y=1.12, font=dict(size=10)), font=dict(family="Inter", color=C["text2"]), ) st.plotly_chart(fig, use_container_width=True) def _sentiment_chart(symbol: str): df = _load_sentiment_history(symbol) if df.empty: st.caption("No sentiment data — run the NLP processor.") return colors = [C["up"] if float(s) > 0.1 else (C["down"] if float(s) < -0.1 else C["stable"]) for s in df["sentiment_score"].fillna(0)] fig = go.Figure() fig.add_hrect(y0=0.1, y1=1, fillcolor=C["up_dim"], opacity=1, line_width=0) fig.add_hrect(y0=-1, y1=-0.1, fillcolor=C["down_dim"], opacity=1, line_width=0) fig.add_trace(go.Scatter( x=df["date"], y=df["sentiment_score"], mode="lines+markers", line=dict(color=C["text2"], width=1.5), marker=dict(color=colors, size=5), fill="tozeroy", fillcolor="rgba(139,148,158,0.06)", name="Sentiment", )) fig.add_hline(y=0, line_dash="solid", line_color=C["border_hi"], line_width=1) fig.update_layout( template="plotly_dark", paper_bgcolor=C["bg"], plot_bgcolor=C["bg"], title=dict(text="News Sentiment (60-day)", font=dict(size=11, color=C["text2"])), yaxis=dict(range=[-1, 1], gridcolor=C["surface2"], tickformat=".1f"), xaxis=dict(gridcolor=C["surface2"]), height=200, margin=dict(l=0, r=0, t=32, b=0), showlegend=False, ) st.plotly_chart(fig, use_container_width=True) def _eia_chart(symbol: str): series = {"CL=F": "crude_stocks", "NG=F": "natgas_storage"}.get(symbol) if not series: return df = _load_eia_history(series) if df.empty: return label = "Crude Oil Stocks (Mbbls)" if symbol == "CL=F" else "Natural Gas Storage (Bcf)" div = 1000 if symbol == "CL=F" else 1 fig = go.Figure() fig.add_trace(go.Bar( x=df["date"], y=df["value"] / div, name=label, marker=dict( color=[C["down_dim"] if (v or 0) > 0 else C["up_dim"] for v in df.get("chg_1w", [])], line=dict(width=0), ), opacity=0.8, )) fig.add_trace(go.Scatter( x=df["date"], y=(df["value"] / div).rolling(4).mean(), mode="lines", line=dict(color=C["accent"], width=1.5, dash="dot"), name="4-wk avg", )) fig.update_layout( template="plotly_dark", paper_bgcolor=C["bg"], plot_bgcolor=C["bg"], title=dict(text=label, font=dict(size=11, color=C["text2"])), height=200, margin=dict(l=0, r=0, t=32, b=0), legend=dict(orientation="h", x=0, y=1.15, font=dict(size=10)), xaxis=dict(gridcolor=C["surface2"]), yaxis=dict(gridcolor=C["surface2"]), ) st.plotly_chart(fig, use_container_width=True) def _render_deep_dive(symbol: str, days: int, horizon_key: str): fc = _load_forecast(symbol) name = SYMBOL_NAMES.get(symbol, symbol) if "error" in fc: st.warning(f"No forecast for {name} — run `python model/trainer.py --symbol {symbol}`") return fk = "forecast_7d" if horizon_key == "7d" else "forecast_30d" fcast = fc.get(fk, {}) dir_ = fcast.get("direction", "STABLE") prob = fcast.get("probability", 0.5) conf = fcast.get("confidence", "LOW") price = fc.get("current_price", 0) dcol = DIR_COLOR.get(dir_, C["stable"]) ddim = DIR_DIM.get(dir_, C["stable_dim"]) icon = DIR_ICON.get(dir_, "◆") ccol = CONF_COLOR.get(conf, C["conf_low"]) warn = fcast.get("model_warning") # Breadcrumb + headline st.markdown(f"""
ANALYSIS
{name}
{symbol}
{icon}
${price:,.2f}
{dir_} {conf} CONF {prob:.1%} probability · {horizon_key.upper()}
{f'
⚠ {warn}
' if warn else ''}
{f'''
Price Target Range
${fcast.get("price_range_low",0):,.0f} – ${fcast.get("price_range_high",0):,.0f}
''' if fcast.get("price_range_low") else ''}
""", unsafe_allow_html=True) # Main layout: chart | signals chart_col, signal_col = st.columns([3, 2]) with chart_col: st.markdown(f'
Price Chart
', unsafe_allow_html=True) _price_chart(symbol, days, fc, horizon_key) with signal_col: st.markdown(f'
Signal Drivers
', unsafe_allow_html=True) _shap_chart(fc) # Both 7d and 30d forecast side by side f7 = fc.get("forecast_7d", {}) f30 = fc.get("forecast_30d", {}) st.markdown(f"""
7-Day
{DIR_ICON.get(f7.get('direction','STABLE'),'◆')} {f7.get('direction','—')}
{f7.get('probability',0):.0%}
30-Day
{DIR_ICON.get(f30.get('direction','STABLE'),'◆')} {f30.get('direction','—')}
{f30.get('probability',0):.0%}
""", unsafe_allow_html=True) # Tabbed data panels tab_labels = ["COT Positioning", "Sentiment", "EIA Inventory", "Weather", "AI Report"] tabs = st.tabs(tab_labels) with tabs[0]: _cot_chart(symbol) with tabs[1]: _sentiment_chart(symbol) with tabs[2]: _eia_chart(symbol) if symbol not in ("CL=F", "NG=F"): st.caption("EIA inventory data is available for Crude Oil (CL=F) and Natural Gas (NG=F) only.") with tabs[3]: weather = _load_weather(symbol) if weather and weather.get("drought_index", 0) > 0: w1, w2, w3 = st.columns(3) w1.metric("Drought Index", f"{weather['drought_index']:.2f}", help="0=normal, 1=extreme drought") w2.metric("Heat Stress Days", weather["heat_stress_days"]) w3.metric("Precip Anomaly", f"{weather['precip_anomaly_pct']:+.1f}%") else: st.caption("No weather data available. Weather signals apply to agricultural commodities.") with tabs[4]: reports = load_latest_reports() report = reports.get(symbol) if not report: with st.spinner("Generating AI analysis..."): report = generate_report(fc) if report and isinstance(report, dict): # Header st.markdown( f'
' f'🤖' f'AI Analyst Report' f'' f'Powered by Llama 3.3 70B · Groq
', unsafe_allow_html=True, ) # One st.markdown per section — avoids nested HTML escaping for key, title, color, icon in [ ("outlook", "Market Outlook", C["accent"], "◈"), ("key_drivers", "Key Drivers", C["up"], "▲"), ("risk", "Primary Risk", C["down"], "⚠"), ("trade_idea", "Trade Idea", C["gold"], "◎"), ]: text = report.get(key, "") if not text: continue st.markdown( f'
' f'
' f'{icon}  {title}
' f'
' f'{text}
', unsafe_allow_html=True, ) else: st.caption("AI report unavailable — set GROQ_API_KEY in your environment.") # ── news feed ────────────────────────────────────────────────────────────────── def _render_news(symbol: str): name = SYMBOL_NAMES.get(symbol, symbol) df = _load_recent_news(symbol) source_label = df["source"].iloc[0] if not df.empty and "source" in df.columns else "GDELT" live_badge = ( f'LIVE' if source_label == "Yahoo Finance" else "" ) st.markdown( f'
' f'
' f'
News — {name}{live_badge}
' f'
' f'{source_label}
', unsafe_allow_html=True, ) if df.empty: st.caption("No news available for this commodity.") return for _, row in df.iterrows(): score = float(row.get("sentiment_score") or 0) has_score = abs(score) > 0.01 scol = C["up"] if score > 0.1 else (C["down"] if score < -0.1 else C["stable"]) sign = "+" if score > 0 else "" title = str(row.get("title", ""))[:130] url = str(row.get("url", "#")) pub = str(row.get("published_date", ""))[:16] score_html = ( f'{sign}{score:.2f}' if has_score else f'' ) st.markdown( f'
' f'
{pub}
' f'
{score_html}
' f'
' f'{title}' f'
', unsafe_allow_html=True, ) # ── sidebar controls ─────────────────────────────────────────────────────────── def _render_sidebar() -> tuple[str, int]: with st.sidebar: st.markdown(f"""
◈ CommodiSense
COMMODITY INTELLIGENCE
""", unsafe_allow_html=True) horizon = st.radio("Forecast Horizon", ["7d", "30d"], index=0, format_func=lambda x: "7-Day" if x == "7d" else "30-Day") days = st.slider("Chart History", 30, 365, 90, step=15, format="%d days") st.markdown("---") if st.button("↺ Refresh Data", use_container_width=True): st.cache_data.clear() st.rerun() st.markdown(f"""
Data Sources
""", unsafe_allow_html=True) sources = [ ("Prices", "yfinance", "12,613 rows"), ("COT", "CFTC", "8,826 rows"), ("Macro", "FRED", "7,193 rows"), ("EIA", "DOE", "3,134 rows"), ("USDA", "NASS", "1,104 rows"), ("News", "GDELT", "392 articles"), ("Weather", "Open-Meteo", "210 rows"), ] for name, src, count in sources: st.markdown(f"""
{name}
{count}
""", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) st.markdown(f"""
Pipeline
GitHub Actions · Mon–Fri 06:00 UTC
XGBoost + LightGBM ensemble
SHAP explainability · FinBERT NLP
""", unsafe_allow_html=True) return horizon, days # ── main ─────────────────────────────────────────────────────────────────────── def main(): _ensure_schema() _ensure_prices() _retag_news() _inject_css() _render_header() horizon, days = _render_sidebar() # Load all forecasts at once forecasts = _load_all_forecasts(tuple(ALL_SYMBOLS)) # Ticker strip _render_ticker(forecasts, horizon) # Macro environment _render_macro_bar() # Commodity grid — track active symbol in session state clicked = _render_commodity_grid(forecasts, horizon, st.session_state.get("active_sym", ALL_SYMBOLS[0])) if clicked: st.session_state["active_sym"] = clicked active = st.session_state.get("active_sym") if not active: active = ALL_SYMBOLS[0] st.session_state["active_sym"] = active # Divider st.markdown(f'
', unsafe_allow_html=True) # Deep dive _render_deep_dive(active, days, horizon) # News st.markdown(f'
', unsafe_allow_html=True) _render_news(active) if __name__ == "__main__": main()