""" Earnings Calendar Service Primary: Alpha Vantage EARNINGS_CALENDAR (free, CSV, uses ALPHA_VANTAGE_KEY or demo key) Fallback: StockAnalysis.com JSON scraping """ import csv import io import json import logging import re from datetime import datetime, timedelta from typing import List, Dict, Optional import requests from bs4 import BeautifulSoup logger = logging.getLogger(__name__) # Top companies to highlight (filter from the full calendar) TOP_TICKERS = { "AAPL", "MSFT", "NVDA", "AMZN", "GOOGL", "GOOG", "META", "TSLA", "AVGO", "JPM", "LLY", "V", "MA", "UNH", "XOM", "COST", "HD", "WMT", "PG", "NFLX", "BAC", "ORCL", "CRM", "AMD", "INTC", "QCOM", "GS", "MS", "CVX", "ABBV", "MRK", "BRK-B", "BRKB", "KO", "PEP", "DIS", "PYPL", "ADBE", "CSCO", "TXN", "HON", "RTX", "CAT", "IBM", } class EarningsCalendarService: """Fetches upcoming earnings report dates without mock data.""" ALPHA_VANTAGE_URL = ( "https://www.alphavantage.co/query" "?function=EARNINGS_CALENDAR&horizon=3month&apikey={api_key}" ) STOCKANALYSIS_URL = "https://stockanalysis.com/stocks/earnings-calendar/" def __init__(self, api_key: Optional[str] = None): self.api_key = api_key or "demo" self.session = requests.Session() self.session.headers.update({ "User-Agent": ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ), "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", }) def get_upcoming_earnings(self, days_ahead: int = 30) -> List[Dict]: """ Returns upcoming earnings entries sorted by date. Each entry: {ticker, company, date, days_until, report_time, eps_estimate} """ entries = self._fetch_alpha_vantage(days_ahead) if not entries: logger.warning("Alpha Vantage returned no data, trying StockAnalysis fallback") entries = self._fetch_stockanalysis(days_ahead) return entries # ------------------------------------------------------------------ # Primary: Alpha Vantage # ------------------------------------------------------------------ def _fetch_alpha_vantage(self, days_ahead: int) -> List[Dict]: try: url = self.ALPHA_VANTAGE_URL.format(api_key=self.api_key) resp = self.session.get(url, timeout=15) resp.raise_for_status() # Alpha Vantage returns CSV for this endpoint content = resp.text if not content or "symbol" not in content[:200].lower(): logger.warning("Alpha Vantage response doesn't look like CSV") return [] reader = csv.DictReader(io.StringIO(content)) now = datetime.now() cutoff = now + timedelta(days=days_ahead) entries = [] for row in reader: try: ticker = (row.get("symbol") or "").strip().upper() if not ticker: continue # Filter to top companies only if ticker not in TOP_TICKERS: continue date_str = (row.get("reportDate") or "").strip() if not date_str: continue date = datetime.strptime(date_str, "%Y-%m-%d") if date < now - timedelta(days=1) or date > cutoff: continue days_until = (date.date() - now.date()).days eps_str = (row.get("estimate") or "").strip() eps_estimate = float(eps_str) if eps_str and eps_str != "-" else None fiscal_end = (row.get("fiscalDateEnding") or "").strip() currency = (row.get("currency") or "USD").strip() entries.append({ "ticker": ticker, "company": row.get("name", ticker).strip(), "date": date, "days_until": days_until, "report_time": None, # AV doesn't provide BMO/AMC "eps_estimate": eps_estimate, "fiscal_end": fiscal_end, "currency": currency, "source": "Alpha Vantage", }) except (ValueError, KeyError) as e: logger.debug(f"Skipping AV row: {e}") continue entries.sort(key=lambda x: x["date"]) logger.info(f"Alpha Vantage: {len(entries)} top-company earnings fetched") return entries except Exception as e: logger.error(f"Alpha Vantage earnings fetch failed: {e}") return [] # ------------------------------------------------------------------ # Fallback: StockAnalysis.com # ------------------------------------------------------------------ def _fetch_stockanalysis(self, days_ahead: int) -> List[Dict]: try: resp = self.session.get(self.STOCKANALYSIS_URL, timeout=15) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") # StockAnalysis embeds earnings data as JSON in a