From 23675b39b8055a8568cdcf71f66482b9d0cf90a9 Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 28 Mar 2026 23:01:14 -0700 Subject: Initial commit โ€” Prism financial analysis dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streamlit app with market bar, price chart, financial statements, DCF valuation engine, comparable companies, and news feed. Co-Authored-By: Claude Sonnet 4.6 --- components/news.py | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 components/news.py (limited to 'components/news.py') diff --git a/components/news.py b/components/news.py new file mode 100644 index 0000000..cea678e --- /dev/null +++ b/components/news.py @@ -0,0 +1,93 @@ +"""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", + "bearish": "๐Ÿ”ด Bearish", + "neutral": "โšช Neutral", + } + return badges.get(sentiment.lower(), "โšช Neutral") + + +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 else "โ€”") + col3.metric("Bearish %", f"{bear_pct * 100:.1f}%" if bear_pct else "โ€”") + 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() -- cgit v1.3-2-g0d8e