feat: wire Finnhub into routing layer — insider txns, calendars, fallback

Changes:
- interface.py: Finnhub added as third vendor (alongside yfinance + AV)
  - get_insider_transactions: Finnhub primary (free, + MSPR bonus signal)
  - get_market_indices/sector_performance/topic_news: Finnhub added as option
  - Fallback catch extended: (AlphaVantageError, FinnhubError, ConnectionError, TimeoutError)
  - New calendar_data category with get_earnings_calendar + get_economic_calendar
- finnhub_scanner.py: added get_earnings_calendar_finnhub, get_economic_calendar_finnhub
  (FOMC/CPI/NFP/GDP events + earnings beats — unique, not in AV at any tier)
- finnhub.py: re-exports new calendar functions
- scanner_tools.py: @tool wrappers for get_earnings_calendar, get_economic_calendar
- default_config.py: tool_vendors["get_insider_transactions"]="finnhub",
  calendar_data vendor category defaulting to "finnhub"
- .env.example: FINNHUB_API_KEY documented
- docs/agent/decisions/010-finnhub-vendor-integration.md: ADR for this decision

All 173 offline tests pass. ADR 002 constraints respected throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ahmet Guzererler 2026-03-18 09:41:41 +01:00
parent 5b6d3a0c3f
commit 04b7efdb68
8 changed files with 205 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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}'")

View File

@ -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",
},
}