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:
parent
5b6d3a0c3f
commit
04b7efdb68
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}'")
|
||||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue