aboutsummaryrefslogtreecommitdiff
path: root/components/news.py
blob: cea678ed4ee5e2860c70ea3f663e8ac8c9dd23cc (plain)
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
"""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()