"""News feed with sentiment badges.""" import streamlit as st from datetime import datetime from services.news_service import get_company_news, get_news_sentiment from services.fmp_service import get_company_news as get_fmp_news def _sentiment_badge(sentiment: str) -> str: badges = { "bullish": "๐ŸŸข Bullish (heuristic)", "bearish": "๐Ÿ”ด Bearish (heuristic)", "neutral": "โšช Neutral (heuristic)", } return badges.get(sentiment.lower(), "โšช Neutral (heuristic)") def _classify_sentiment(article: dict) -> str: """Classify sentiment from Finnhub article data.""" # Finnhub doesn't return per-article sentiment, use headline heuristics positive = ["beats", "surges", "rises", "gains", "profit", "record", "upgrade", "buy", "outperform", "growth", "strong", "higher", "rally"] negative = ["misses", "falls", "drops", "loss", "cut", "downgrade", "sell", "underperform", "weak", "lower", "decline", "warning", "layoff"] headline = (article.get("headline") or article.get("title") or "").lower() summary = (article.get("summary") or "").lower() text = headline + " " + summary pos = sum(1 for w in positive if w in text) neg = sum(1 for w in negative if w in text) if pos > neg: return "bullish" if neg > pos: return "bearish" return "neutral" def _fmt_time(timestamp) -> str: try: if isinstance(timestamp, (int, float)): return datetime.utcfromtimestamp(timestamp).strftime("%b %d, %Y") return str(timestamp)[:10] except Exception: return "" def render_news(ticker: str): # Overall sentiment summary from Finnhub sentiment_data = get_news_sentiment(ticker) if sentiment_data: buzz = sentiment_data.get("buzz", {}) score = sentiment_data.get("sentiment", {}) col1, col2, col3 = st.columns(3) col1.metric("Articles (7d)", buzz.get("articlesInLastWeek", "โ€”")) bull_pct = score.get("bullishPercent") bear_pct = score.get("bearishPercent") col2.metric("Bullish %", f"{bull_pct * 100:.1f}%" if bull_pct is not None else "โ€”") col3.metric("Bearish %", f"{bear_pct * 100:.1f}%" if bear_pct is not None else "โ€”") st.caption("Sentiment tags below are rule-based headline heuristics, not model-scored article sentiment.") st.divider() # Fetch articles โ€” Finnhub first, FMP as fallback articles = get_company_news(ticker) if not articles: articles = get_fmp_news(ticker) if not articles: st.info("No recent news found.") return for article in articles: headline = article.get("headline") or article.get("title", "No title") source = article.get("source") or article.get("site", "") url = article.get("url") or article.get("newsURL") or article.get("url", "") timestamp = article.get("datetime") or article.get("publishedDate", "") summary = article.get("summary") or article.get("text") or "" sentiment = _classify_sentiment(article) badge = _sentiment_badge(sentiment) time_str = _fmt_time(timestamp) with st.container(): col1, col2 = st.columns([5, 1]) with col1: if url: st.markdown(f"**[{headline}]({url})**") else: st.markdown(f"**{headline}**") meta = " ยท ".join(filter(None, [source, time_str])) if meta: st.caption(meta) if summary: st.caption(summary[:200] + ("โ€ฆ" if len(summary) > 200 else "")) with col2: st.markdown(badge) st.divider()