diff --git a/.env.example b/.env.example index 952da298..a4c87457 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,8 @@ OPENROUTER_API_KEY= # ── Data Provider API Keys ─────────────────────────────────────────── ALPHA_VANTAGE_API_KEY= +# Free at https://finnhub.io — required for earnings/economic calendars and insider transactions +FINNHUB_API_KEY= TRADINGAGENTS_RESULTS_DIR=./my_results TRADINGAGENTS_MAX_DEBATE_ROUNDS=2 @@ -61,9 +63,10 @@ TRADINGAGENTS_MAX_DEBATE_ROUNDS=2 # TRADINGAGENTS_MAX_RECUR_LIMIT=100 # LangGraph recursion limit # ── Data vendor routing ────────────────────────────────────────────── -# Category-level vendor selection (yfinance | alpha_vantage) +# Category-level vendor selection (yfinance | alpha_vantage | finnhub) # TRADINGAGENTS_VENDOR_CORE_STOCK_APIS=yfinance # TRADINGAGENTS_VENDOR_TECHNICAL_INDICATORS=yfinance # TRADINGAGENTS_VENDOR_FUNDAMENTAL_DATA=yfinance # TRADINGAGENTS_VENDOR_NEWS_DATA=yfinance # TRADINGAGENTS_VENDOR_SCANNER_DATA=yfinance +# TRADINGAGENTS_VENDOR_CALENDAR_DATA=finnhub diff --git a/docs/agent/CURRENT_STATE.md b/docs/agent/CURRENT_STATE.md index f5c56e57..591e84e1 100644 --- a/docs/agent/CURRENT_STATE.md +++ b/docs/agent/CURRENT_STATE.md @@ -10,6 +10,8 @@ Scanner pipeline is feature-complete and quality-improved. Focus shifts to Macro - Thread-safe rate limiter for Alpha Vantage implemented - Vendor fallback (AV -> yfinance) broadened to catch `AlphaVantageError`, `ConnectionError`, `TimeoutError` - **PR #13 merged**: Industry Deep Dive quality fixed — enriched industry data (price returns), explicit sector routing via `_extract_top_sectors()`, tool-call nudge in `run_tool_loop` +- Finnhub integrated as third vendor: insider transactions (primary), earnings calendar (new), economic calendar (new) +- ADR 010 written documenting Finnhub vendor decision and paid-tier constraints # Active Blockers diff --git a/docs/agent/decisions/010-finnhub-vendor-integration.md b/docs/agent/decisions/010-finnhub-vendor-integration.md new file mode 100644 index 00000000..05897a82 --- /dev/null +++ b/docs/agent/decisions/010-finnhub-vendor-integration.md @@ -0,0 +1,41 @@ +--- +type: decision +status: active +date: 2026-03-18 +agent_author: "claude" +tags: [data, finnhub, vendor, calendar, insider] +related_files: [tradingagents/dataflows/interface.py, tradingagents/dataflows/finnhub_scanner.py, tradingagents/agents/utils/scanner_tools.py] +--- + +## Context + +Live integration testing of the Finnhub API (2026-03-18) confirmed free-tier availability +of 6 endpoints. Evaluation identified two high-value unique capabilities (earnings calendar, +economic calendar) and two equivalent-quality replacements (insider transactions, company profile). + +## The Decision + +- Add Finnhub as a third vendor alongside yfinance and Alpha Vantage. +- `get_insider_transactions` → Finnhub primary (free, same data + MSPR aggregate bonus signal) +- `get_earnings_calendar` → Finnhub only (new capability, not in AV at any tier) +- `get_economic_calendar` → Finnhub only (new capability, FOMC/CPI/NFP dates) +- AV remains primary for news (per-article sentiment scores irreplaceable), market movers (TOP_GAINERS_LOSERS full-market coverage), and financial statements (Finnhub requires paid) + +## Paid-Tier Endpoints (do NOT use on free key) + +- `/stock/candle` → HTTP 403 on free tier (use yfinance for OHLCV) +- `/financials-reported` → HTTP 403 on free tier (use AV for statements) +- `/indicator` → HTTP 403 on free tier (yfinance/stockstats already primary) + +## Constraints + +- `FINNHUB_API_KEY` env var required — `APIKeyInvalidError` raised if missing +- Free tier rate limit: 60 calls/min — enforced by `_rate_limited_request` in `finnhub_common.py` +- Calendar endpoints return empty list (not error) when no events exist in range — return formatted "no events" message, do NOT raise + +## Actionable Rules + +- Finnhub functions in `route_to_vendor` must raise `FinnhubError` (not return error strings) on total failure +- `route_to_vendor` fallback catch must include `FinnhubError` alongside `AlphaVantageError` +- Calendar functions return graceful empty-state strings (not raise) when API returns empty list — this is normal behaviour, not an error +- Never add Finnhub paid-tier endpoints (`/stock/candle`, `/financials-reported`, `/indicator`) to free-tier routing diff --git a/tradingagents/agents/utils/scanner_tools.py b/tradingagents/agents/utils/scanner_tools.py index b1869a4b..c3d9f9ac 100644 --- a/tradingagents/agents/utils/scanner_tools.py +++ b/tradingagents/agents/utils/scanner_tools.py @@ -76,12 +76,38 @@ def get_topic_news( """ Search news by arbitrary topic for market-wide analysis. Uses the configured scanner_data vendor. - + Args: topic (str): Search query/topic for news limit (int): Maximum number of articles to return (default 10) - + Returns: str: Formatted list of news articles for the topic with title, summary, source, and link """ return route_to_vendor("get_topic_news", topic, limit) + + +@tool +def get_earnings_calendar( + from_date: Annotated[str, "Start date in YYYY-MM-DD format"], + to_date: Annotated[str, "End date in YYYY-MM-DD format"], +) -> str: + """ + Retrieve upcoming earnings release calendar. + Shows companies reporting earnings, EPS estimates, and prior-year actuals. + Unique Finnhub capability not available in Alpha Vantage. + """ + return route_to_vendor("get_earnings_calendar", from_date, to_date) + + +@tool +def get_economic_calendar( + from_date: Annotated[str, "Start date in YYYY-MM-DD format"], + to_date: Annotated[str, "End date in YYYY-MM-DD format"], +) -> str: + """ + Retrieve macro economic event calendar (FOMC, CPI, NFP, GDP, PPI). + Shows market-moving macro events with estimates and prior readings. + Unique Finnhub capability not available in Alpha Vantage. + """ + return route_to_vendor("get_economic_calendar", from_date, to_date) diff --git a/tradingagents/dataflows/finnhub.py b/tradingagents/dataflows/finnhub.py index 57f5fcc7..4c98ed32 100644 --- a/tradingagents/dataflows/finnhub.py +++ b/tradingagents/dataflows/finnhub.py @@ -36,6 +36,8 @@ from .finnhub_scanner import ( get_market_indices_finnhub, get_sector_performance_finnhub, get_topic_news_finnhub, + get_earnings_calendar_finnhub, + get_economic_calendar_finnhub, ) # Technical indicators @@ -68,6 +70,8 @@ __all__ = [ "get_market_indices_finnhub", "get_sector_performance_finnhub", "get_topic_news_finnhub", + "get_earnings_calendar_finnhub", + "get_economic_calendar_finnhub", # Indicators "get_indicator_finnhub", # Exceptions diff --git a/tradingagents/dataflows/finnhub_scanner.py b/tradingagents/dataflows/finnhub_scanner.py index d6f6c612..89255010 100644 --- a/tradingagents/dataflows/finnhub_scanner.py +++ b/tradingagents/dataflows/finnhub_scanner.py @@ -368,3 +368,95 @@ def get_topic_news_finnhub( result += "\n" return result + + +def get_earnings_calendar_finnhub(from_date: str, to_date: str) -> str: + """Fetch upcoming earnings releases via Finnhub /calendar/earnings. + + Returns a formatted markdown table of companies reporting earnings between + from_date and to_date, including EPS estimates and prior-year actuals. + Unique capability not available in Alpha Vantage at any tier. + + Args: + from_date: Start date in YYYY-MM-DD format. + to_date: End date in YYYY-MM-DD format. + + Returns: + Markdown-formatted table with Symbol, Date, EPS Estimate, EPS Prior. + + Raises: + FinnhubError: On API-level errors or empty response. + """ + data = _rate_limited_request("calendar/earnings", {"from": from_date, "to": to_date}) + earnings_list = data.get("earningsCalendar", []) + header = ( + f"# Earnings Calendar: {from_date} to {to_date} — Finnhub\n" + f"# Data retrieved on: {_now_str()}\n\n" + ) + if not earnings_list: + return header + "_No earnings events found in this date range._\n" + + lines = [ + header, + "| Symbol | Company | Date | EPS Estimate | EPS Prior | Revenue Estimate |", + "|--------|---------|------|--------------|-----------|-----------------|", + ] + for item in sorted(earnings_list, key=lambda x: x.get("date", "")): + symbol = item.get("symbol", "N/A") + company = item.get("company", "N/A")[:30] + date = item.get("date", "N/A") + eps_est = item.get("epsEstimate", None) + eps_prior = item.get("epsPrior", None) + rev_est = item.get("revenueEstimate", None) + eps_est_s = f"${eps_est:.2f}" if eps_est is not None else "N/A" + eps_prior_s = f"${eps_prior:.2f}" if eps_prior is not None else "N/A" + rev_est_s = f"${float(rev_est)/1e9:.2f}B" if rev_est is not None else "N/A" + lines.append(f"| {symbol} | {company} | {date} | {eps_est_s} | {eps_prior_s} | {rev_est_s} |") + return "\n".join(lines) + + +def get_economic_calendar_finnhub(from_date: str, to_date: str) -> str: + """Fetch macro economic events via Finnhub /calendar/economic. + + Returns FOMC meetings, CPI releases, NFP (Non-Farm Payroll), PPI, + GDP announcements, and other market-moving macro events. Unique + capability not available in Alpha Vantage at any tier. + + Args: + from_date: Start date in YYYY-MM-DD format. + to_date: End date in YYYY-MM-DD format. + + Returns: + Markdown-formatted table with Date, Event, Country, Impact, Estimate, Prior. + + Raises: + FinnhubError: On API-level errors or empty response. + """ + data = _rate_limited_request("calendar/economic", {"from": from_date, "to": to_date}) + events = data.get("economicCalendar", []) + header = ( + f"# Economic Calendar: {from_date} to {to_date} — Finnhub\n" + f"# Data retrieved on: {_now_str()}\n\n" + ) + if not events: + return header + "_No economic events found in this date range._\n" + + lines = [ + header, + "| Date | Time | Event | Country | Impact | Estimate | Prior |", + "|------|------|-------|---------|--------|----------|-------|", + ] + for ev in sorted(events, key=lambda x: (x.get("time", ""), x.get("event", ""))): + date = ev.get("time", "N/A")[:10] if ev.get("time") else "N/A" + time_str = ( + ev.get("time", "N/A")[11:16] + if ev.get("time") and len(ev.get("time", "")) > 10 + else "N/A" + ) + event = ev.get("event", "N/A")[:40] + country = ev.get("country", "N/A") + impact = ev.get("impact", "N/A") + estimate = str(ev.get("estimate", "N/A")) + prior = str(ev.get("prev", "N/A")) + lines.append(f"| {date} | {time_str} | {event} | {country} | {impact} | {estimate} | {prior} |") + return "\n".join(lines) diff --git a/tradingagents/dataflows/interface.py b/tradingagents/dataflows/interface.py index 7ffde51f..fd279430 100644 --- a/tradingagents/dataflows/interface.py +++ b/tradingagents/dataflows/interface.py @@ -37,6 +37,15 @@ from .alpha_vantage_scanner import ( get_topic_news_alpha_vantage, ) from .alpha_vantage_common import AlphaVantageError, AlphaVantageRateLimitError, RateLimitError +from .finnhub_common import FinnhubError +from .finnhub_news import get_insider_transactions as get_finnhub_insider_transactions +from .finnhub_scanner import ( + get_market_indices_finnhub, + get_sector_performance_finnhub, + get_topic_news_finnhub, + get_earnings_calendar_finnhub, + get_economic_calendar_finnhub, +) # Configuration and routing logic from .config import get_config @@ -82,12 +91,20 @@ TOOLS_CATEGORIES = { "get_industry_performance", "get_topic_news", ] - } + }, + "calendar_data": { + "description": "Earnings and economic event calendars", + "tools": [ + "get_earnings_calendar", + "get_economic_calendar", + ] + }, } VENDOR_LIST = [ "yfinance", "alpha_vantage", + "finnhub", ] # Mapping of methods to their vendor-specific implementations @@ -129,6 +146,7 @@ VENDOR_METHODS = { "alpha_vantage": get_alpha_vantage_global_news, }, "get_insider_transactions": { + "finnhub": get_finnhub_insider_transactions, "alpha_vantage": get_alpha_vantage_insider_transactions, "yfinance": get_yfinance_insider_transactions, }, @@ -138,10 +156,12 @@ VENDOR_METHODS = { "alpha_vantage": get_market_movers_alpha_vantage, }, "get_market_indices": { + "finnhub": get_market_indices_finnhub, "alpha_vantage": get_market_indices_alpha_vantage, "yfinance": get_market_indices_yfinance, }, "get_sector_performance": { + "finnhub": get_sector_performance_finnhub, "alpha_vantage": get_sector_performance_alpha_vantage, "yfinance": get_sector_performance_yfinance, }, @@ -150,9 +170,17 @@ VENDOR_METHODS = { "yfinance": get_industry_performance_yfinance, }, "get_topic_news": { + "finnhub": get_topic_news_finnhub, "alpha_vantage": get_topic_news_alpha_vantage, "yfinance": get_topic_news_yfinance, }, + # calendar_data — Finnhub only (unique capabilities) + "get_earnings_calendar": { + "finnhub": get_earnings_calendar_finnhub, + }, + "get_economic_calendar": { + "finnhub": get_economic_calendar_finnhub, + }, } def get_category_for_method(method: str) -> str: @@ -202,7 +230,7 @@ def route_to_vendor(method: str, *args, **kwargs): try: return impl_func(*args, **kwargs) - except (AlphaVantageError, ConnectionError, TimeoutError): - continue # Any AV error or connection/timeout triggers fallback to next vendor + except (AlphaVantageError, FinnhubError, ConnectionError, TimeoutError): + continue # Any vendor error or connection/timeout triggers fallback to next vendor raise RuntimeError(f"No available vendor for '{method}'") \ No newline at end of file diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index e42787b1..d5cb1c3a 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -78,9 +78,11 @@ DEFAULT_CONFIG = { "fundamental_data": _env("VENDOR_FUNDAMENTAL_DATA", "yfinance"), "news_data": _env("VENDOR_NEWS_DATA", "yfinance"), "scanner_data": _env("VENDOR_SCANNER_DATA", "yfinance"), + "calendar_data": _env("VENDOR_CALENDAR_DATA", "finnhub"), }, # Tool-level configuration (takes precedence over category-level) "tool_vendors": { - # Example: "get_stock_data": "alpha_vantage", # Override category default + # Finnhub free tier provides same data + MSPR aggregate bonus signal + "get_insider_transactions": "finnhub", }, }