1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
"""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()
|