Files
robinhood/backend/app/services/news_service.py
T

321 lines
11 KiB
Python

import httpx
from typing import List, Dict
from datetime import datetime, timedelta
from textblob import TextBlob
import hashlib
from app.config import settings
from app.schemas.schemas import (
NewsArticle,
NewsFeedResponse,
Sentiment,
EconomicEvent,
EconomicCalendarResponse,
)
class NewsService:
def __init__(self):
self.alpha_vantage_key = settings.ALPHA_VANTAGE_API_KEY
self.finnhub_key = settings.FINNHUB_API_KEY
self.news_api_key = settings.NEWS_API_KEY
# Gold-related keywords for relevance scoring
self.gold_keywords = {
"high_relevance": [
"gold", "xau", "precious metals", "bullion", "gold price",
"gold market", "gold trading", "gold miners", "gold etf"
],
"medium_relevance": [
"federal reserve", "fed", "inflation", "interest rates",
"dollar", "usd", "monetary policy", "central bank",
"jerome powell", "treasury", "bonds"
],
"context_relevance": [
"geopolitics", "war", "sanctions", "recession",
"crisis", "safe haven", "risk off", "uncertainty"
]
}
# Impact categories
self.impact_categories = {
"MONETARY_POLICY": ["federal reserve", "fed", "interest rate", "monetary policy", "central bank"],
"GEOPOLITICS": ["war", "conflict", "sanctions", "tension", "geopolitical"],
"ECONOMIC_DATA": ["inflation", "cpi", "gdp", "employment", "jobs", "unemployment"],
"MARKET_SENTIMENT": ["risk", "sentiment", "volatility", "safe haven"],
"COMMODITY": ["gold", "precious metals", "bullion", "commodities"],
}
def _calculate_relevance_score(self, text: str) -> float:
"""Calculate how relevant a news article is to gold trading"""
text_lower = text.lower()
score = 0.0
# High relevance keywords
for keyword in self.gold_keywords["high_relevance"]:
if keyword in text_lower:
score += 0.4
# Medium relevance keywords
for keyword in self.gold_keywords["medium_relevance"]:
if keyword in text_lower:
score += 0.2
# Context relevance keywords
for keyword in self.gold_keywords["context_relevance"]:
if keyword in text_lower:
score += 0.1
return min(score, 1.0)
def _categorize_news(self, text: str) -> str:
"""Categorize news based on content"""
text_lower = text.lower()
for category, keywords in self.impact_categories.items():
for keyword in keywords:
if keyword in text_lower:
return category
return "OTHER"
def _analyze_sentiment(self, text: str) -> tuple[Sentiment, float]:
"""Analyze sentiment using TextBlob"""
try:
analysis = TextBlob(text)
polarity = analysis.sentiment.polarity
if polarity > 0.1:
sentiment = Sentiment.POSITIVE
elif polarity < -0.1:
sentiment = Sentiment.NEGATIVE
else:
sentiment = Sentiment.NEUTRAL
return sentiment, polarity
except Exception:
return Sentiment.NEUTRAL, 0.0
def _assess_gold_impact(self, sentiment: Sentiment, category: str, relevance: float) -> str:
"""Assess impact level on gold prices"""
# High impact categories
high_impact_cats = ["MONETARY_POLICY", "ECONOMIC_DATA"]
if relevance > 0.7:
if category in high_impact_cats:
return "HIGH"
return "MEDIUM"
elif relevance > 0.4:
return "MEDIUM"
else:
return "LOW"
async def fetch_alpha_vantage_news(self, topics: str = "economy_monetary,finance") -> List[NewsArticle]:
"""Fetch news from Alpha Vantage News Sentiment API"""
try:
params = {
"function": "NEWS_SENTIMENT",
"topics": topics,
"limit": 50,
"apikey": self.alpha_vantage_key,
}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(
settings.ALPHA_VANTAGE_BASE_URL,
params=params
)
response.raise_for_status()
data = response.json()
if "feed" not in data:
return []
articles = []
for item in data["feed"]:
title = item.get("title", "")
summary = item.get("summary", "")
full_text = f"{title} {summary}"
relevance = self._calculate_relevance_score(full_text)
# Filter only gold-relevant news
if relevance < 0.3:
continue
sentiment, score = self._analyze_sentiment(full_text)
category = self._categorize_news(full_text)
impact = self._assess_gold_impact(sentiment, category, relevance)
# Parse published date
published_str = item.get("time_published", "")
try:
published_at = datetime.strptime(published_str, "%Y%m%dT%H%M%S")
except:
published_at = datetime.now()
article_id = hashlib.md5(f"{title}{published_str}".encode()).hexdigest()
articles.append(
NewsArticle(
id=article_id,
source=item.get("source", "Alpha Vantage"),
title=title,
description=summary,
url=item.get("url", ""),
published_at=published_at,
sentiment=sentiment,
sentiment_score=score,
impact_on_gold=impact,
relevance_score=relevance,
category=category,
)
)
return articles
except Exception as e:
print(f"Error fetching Alpha Vantage news: {e}")
return []
async def fetch_finnhub_news(self) -> List[NewsArticle]:
"""Fetch gold-related news from Finnhub"""
if not self.finnhub_key:
return []
try:
# Get general market news
params = {
"category": "forex",
"token": self.finnhub_key,
}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(
f"{settings.FINNHUB_BASE_URL}/news",
params=params
)
response.raise_for_status()
data = response.json()
articles = []
for item in data[:50]: # Limit to 50 articles
title = item.get("headline", "")
summary = item.get("summary", "")
full_text = f"{title} {summary}"
relevance = self._calculate_relevance_score(full_text)
# Filter only gold-relevant news
if relevance < 0.3:
continue
sentiment, score = self._analyze_sentiment(full_text)
category = self._categorize_news(full_text)
impact = self._assess_gold_impact(sentiment, category, relevance)
published_at = datetime.fromtimestamp(item.get("datetime", 0))
article_id = hashlib.md5(f"{title}{item.get('id', '')}".encode()).hexdigest()
articles.append(
NewsArticle(
id=article_id,
source=item.get("source", "Finnhub"),
title=title,
description=summary,
url=item.get("url", ""),
published_at=published_at,
sentiment=sentiment,
sentiment_score=score,
impact_on_gold=impact,
relevance_score=relevance,
category=category,
)
)
return articles
except Exception as e:
print(f"Error fetching Finnhub news: {e}")
return []
async def get_aggregated_news_feed(self) -> NewsFeedResponse:
"""Get aggregated news from all sources"""
# Fetch from multiple sources
alpha_news = await self.fetch_alpha_vantage_news()
finnhub_news = await self.fetch_finnhub_news() if self.finnhub_key else []
# Combine and deduplicate
all_articles = alpha_news + finnhub_news
# Remove duplicates based on similar titles
unique_articles = []
seen_titles = set()
for article in all_articles:
title_key = article.title.lower()[:50] # First 50 chars
if title_key not in seen_titles:
seen_titles.add(title_key)
unique_articles.append(article)
# Sort by published date (newest first)
unique_articles.sort(key=lambda x: x.published_at, reverse=True)
# Limit to most recent 50
unique_articles = unique_articles[:50]
# Calculate statistics
bullish_count = sum(1 for a in unique_articles if a.sentiment == Sentiment.POSITIVE)
bearish_count = sum(1 for a in unique_articles if a.sentiment == Sentiment.NEGATIVE)
neutral_count = sum(1 for a in unique_articles if a.sentiment == Sentiment.NEUTRAL)
avg_sentiment = (
sum(a.sentiment_score for a in unique_articles) / len(unique_articles)
if unique_articles else 0.0
)
# Determine overall sentiment
if avg_sentiment > 0.1:
overall_sentiment = Sentiment.POSITIVE
elif avg_sentiment < -0.1:
overall_sentiment = Sentiment.NEGATIVE
else:
overall_sentiment = Sentiment.NEUTRAL
return NewsFeedResponse(
articles=unique_articles,
total_count=len(unique_articles),
bullish_count=bullish_count,
bearish_count=bearish_count,
neutral_count=neutral_count,
overall_sentiment=overall_sentiment,
avg_sentiment_score=avg_sentiment,
)
async def get_economic_calendar(self) -> EconomicCalendarResponse:
"""Get upcoming economic events that impact gold"""
# This would integrate with economic calendar APIs
# For MVP, return curated list of upcoming events
# In production, integrate with:
# - Forex Factory API
# - Investing.com Economic Calendar
# - Alpha Vantage Economic Indicators
# For now, return empty with structure
events = []
# Count high-impact upcoming events
now = datetime.now()
upcoming_high_impact = sum(
1 for e in events
if e.importance == "HIGH" and e.event_date > now
)
return EconomicCalendarResponse(
events=events,
upcoming_high_impact=upcoming_high_impact,
)
news_service = NewsService()