aboutsummaryrefslogtreecommitdiff
path: root/components/news.py
diff options
context:
space:
mode:
authorTyler <tyler@tylerhoang.xyz>2026-03-28 23:01:14 -0700
committerTyler <tyler@tylerhoang.xyz>2026-03-28 23:01:14 -0700
commit23675b39b8055a8568cdcf71f66482b9d0cf90a9 (patch)
tree14e42cf710b47072e904b1c21d7322352ae1823c /components/news.py
Initial commit โ€” Prism financial analysis dashboard
Streamlit app with market bar, price chart, financial statements, DCF valuation engine, comparable companies, and news feed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'components/news.py')
-rw-r--r--components/news.py93
1 files changed, 93 insertions, 0 deletions
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()