You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
148 lines
5.1 KiB
Python
148 lines
5.1 KiB
Python
"""
|
|
Yahoo Finance RSS feed adapter - collect headline events and match to tickers.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Dict, List, Optional
|
|
|
|
import aiohttp
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
|
|
from app.models.overlay_raw_event import OverlayHeadlineEvent
|
|
from app.services.overlay.entity_resolver import EntityResolver
|
|
from app.core.overlay_config import YAHOO_RSS_FEEDS, TOP_50_SYMBOLS
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class YahooRSSAdapter:
|
|
"""Collect Yahoo Finance RSS headlines and match to tickers."""
|
|
|
|
def __init__(self):
|
|
self.resolver = EntityResolver()
|
|
|
|
async def fetch_feed(self, url: str) -> List[Dict]:
|
|
"""Fetch and parse a single RSS feed URL."""
|
|
try:
|
|
import feedparser
|
|
except ImportError:
|
|
logger.error("feedparser not installed; Yahoo RSS adapter inactive")
|
|
return []
|
|
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
headers = {
|
|
"User-Agent": "Mozilla/5.0 StockOracle/1.0",
|
|
"Accept": "application/rss+xml, application/xml, text/xml",
|
|
}
|
|
async with session.get(
|
|
url, headers=headers, timeout=aiohttp.ClientTimeout(total=30)
|
|
) as resp:
|
|
if resp.status != 200:
|
|
logger.warning(f"Yahoo RSS: non-200 from {url}: {resp.status}")
|
|
return []
|
|
text = await resp.text()
|
|
|
|
feed = feedparser.parse(text)
|
|
entries = []
|
|
for entry in feed.entries:
|
|
guid = getattr(entry, "id", None) or getattr(entry, "link", None)
|
|
title = getattr(entry, "title", "")
|
|
publisher = getattr(entry, "publisher", None)
|
|
if not publisher:
|
|
src = getattr(entry, "source", {})
|
|
publisher = src.get("title") if isinstance(src, dict) else None
|
|
|
|
published = getattr(entry, "published_parsed", None)
|
|
if published:
|
|
try:
|
|
pub_dt = datetime(*published[:6], tzinfo=timezone.utc)
|
|
except Exception:
|
|
pub_dt = datetime.now(timezone.utc)
|
|
else:
|
|
pub_dt = datetime.now(timezone.utc)
|
|
|
|
entries.append({
|
|
"guid": guid or title,
|
|
"title": title,
|
|
"publisher": publisher,
|
|
"published_at": pub_dt,
|
|
})
|
|
return entries
|
|
|
|
except Exception as e:
|
|
logger.error(f"Yahoo RSS fetch error for {url}: {e}")
|
|
return []
|
|
|
|
async def collect(self, db: AsyncSession, symbols: Optional[List[str]] = None) -> int:
|
|
"""
|
|
Collect RSS headlines from general feeds and per-symbol feeds.
|
|
|
|
Persists new events (deduped by article_guid), skips duplicates.
|
|
Returns number of new records inserted.
|
|
"""
|
|
await self.resolver.load_aliases(db)
|
|
|
|
all_entries: List[Dict] = []
|
|
|
|
# General feeds
|
|
tasks = [self.fetch_feed(url) for url in YAHOO_RSS_FEEDS]
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
for r in results:
|
|
if isinstance(r, list):
|
|
all_entries.extend(r)
|
|
|
|
# Per-symbol feeds for watch-list
|
|
watch_symbols = symbols or TOP_50_SYMBOLS
|
|
sym_tasks = [
|
|
self.fetch_feed(f"https://finance.yahoo.com/rss/headline?s={sym}")
|
|
for sym in watch_symbols
|
|
]
|
|
sym_results = await asyncio.gather(*sym_tasks, return_exceptions=True)
|
|
for r in sym_results:
|
|
if isinstance(r, list):
|
|
all_entries.extend(r)
|
|
|
|
# Deduplicate by guid within this batch
|
|
seen_guids: set = set()
|
|
unique_entries = []
|
|
for entry in all_entries:
|
|
g = entry.get("guid")
|
|
if g and g not in seen_guids:
|
|
seen_guids.add(g)
|
|
unique_entries.append(entry)
|
|
|
|
inserted = 0
|
|
for entry in unique_entries:
|
|
if not entry.get("guid") or not entry.get("title"):
|
|
continue
|
|
|
|
# Check DB duplicate
|
|
existing = await db.execute(
|
|
select(OverlayHeadlineEvent.id).where(
|
|
OverlayHeadlineEvent.article_guid == entry["guid"]
|
|
)
|
|
)
|
|
if existing.first():
|
|
continue
|
|
|
|
matched = self.resolver.resolve_from_title(entry["title"])
|
|
event = OverlayHeadlineEvent(
|
|
article_guid=entry["guid"],
|
|
title=entry["title"],
|
|
publisher=entry.get("publisher"),
|
|
published_at=entry["published_at"],
|
|
matched_symbols=matched,
|
|
)
|
|
db.add(event)
|
|
inserted += 1
|
|
|
|
if inserted:
|
|
await db.commit()
|
|
logger.info(f"Yahoo RSS: inserted {inserted} new headline events")
|
|
|
|
return inserted
|