Resolve merge conflicts after PR #16 merge into main (#17)

* Initial plan

* feat: Finnhub integration layer with review fixes — 100 offline tests, vendor routing, evaluation report

Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com>

* review: extract _MAX_ERROR_LEN constant for error message truncation

Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com>

* re-review: fix version downgrade (0.2.0→0.2.1), extract _safe_fmt helper for earnings formatting

Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: aguzererler <6199053+aguzererler@users.noreply.github.com>
This commit is contained in:
Copilot 2026-03-18 11:38:44 +01:00 committed by GitHub
parent 26cd4c8b78
commit d5fb0fdd94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 39 additions and 29 deletions

View File

@ -11,6 +11,7 @@ requires-python = ">=3.10"
dependencies = [
"langchain-core>=0.3.81",
"backtrader>=1.9.78.123",
"chainlit>=2.5.5",
"langchain-anthropic>=0.3.15",
"langchain-experimental>=0.3.4",
"langchain-google-genai>=2.1.5",

View File

@ -911,7 +911,7 @@ def _make_quote_side_effect(symbols_quotes: dict) -> callable:
return side_effect
class TestGetMarketMoversFinnnhub:
class TestGetMarketMoversFinnhub:
"""get_market_movers_finnhub returns a sorted markdown table."""
_RATE_PATCH = "tradingagents.dataflows.finnhub_scanner._rate_limited_request"
@ -985,7 +985,7 @@ class TestGetMarketMoversFinnnhub:
# ---------------------------------------------------------------------------
class TestGetMarketIndicesFinnnhub:
class TestGetMarketIndicesFinnhub:
"""get_market_indices_finnhub builds a table of index levels."""
_RATE_PATCH = "tradingagents.dataflows.finnhub_scanner._rate_limited_request"
@ -1040,7 +1040,7 @@ class TestGetMarketIndicesFinnnhub:
# ---------------------------------------------------------------------------
class TestGetSectorPerformanceFinnnhub:
class TestGetSectorPerformanceFinnhub:
"""get_sector_performance_finnhub returns sector ETF data."""
_RATE_PATCH = "tradingagents.dataflows.finnhub_scanner._rate_limited_request"
@ -1080,7 +1080,7 @@ class TestGetSectorPerformanceFinnnhub:
# ---------------------------------------------------------------------------
class TestGetTopicNewsFinnnhub:
class TestGetTopicNewsFinnhub:
"""get_topic_news_finnhub maps topic strings to Finnhub categories."""
_RATE_PATCH = "tradingagents.dataflows.finnhub_scanner._rate_limited_request"

View File

@ -5,7 +5,6 @@ the Finnhub free tier), and the core HTTP request helper used by all other
finnhub_* modules.
"""
import json
import os
import threading
import time as _time
@ -76,7 +75,7 @@ _call_timestamps: list[float] = []
_RATE_LIMIT = 60 # calls per minute
def _rate_limited_request(endpoint: str, params: dict, timeout: int = 30) -> dict:
def _rate_limited_request(endpoint: str, params: dict, timeout: int = 30) -> dict | list:
"""Make a rate-limited Finnhub API request.
Enforces the 60-calls-per-minute limit for the free tier using a sliding
@ -89,7 +88,7 @@ def _rate_limited_request(endpoint: str, params: dict, timeout: int = 30) -> dic
timeout: HTTP request timeout in seconds.
Returns:
Parsed JSON response as a dict.
Parsed JSON response as a dict or list.
Raises:
FinnhubError subclass on any API or network error.
@ -126,20 +125,23 @@ def _rate_limited_request(endpoint: str, params: dict, timeout: int = 30) -> dic
# ---------------------------------------------------------------------------
def _make_api_request(endpoint: str, params: dict, timeout: int = 30) -> dict:
def _make_api_request(endpoint: str, params: dict, timeout: int = 30) -> dict | list:
"""Make a Finnhub API request with proper error handling.
Calls ``https://finnhub.io/api/v1/{endpoint}`` and returns the parsed JSON
body. The ``token`` parameter is injected automatically from the
``FINNHUB_API_KEY`` environment variable.
Most endpoints return a JSON object (dict), but some (e.g. ``/company-news``,
``/news``) return a JSON array (list).
Args:
endpoint: Finnhub endpoint path without leading slash (e.g. "quote").
params: Query parameters dict (do NOT include ``token`` here).
timeout: HTTP request timeout in seconds.
Returns:
Parsed JSON response as a dict.
Parsed JSON response as a dict or list.
Raises:
APIKeyInvalidError: Invalid or missing API key (HTTP 401 or env missing).

View File

@ -5,7 +5,6 @@ using the Finnhub REST API. Output formats mirror the Alpha Vantage
equivalents where possible for consistent agent-facing data.
"""
import json
from typing import Literal
from .finnhub_common import (

View File

@ -18,10 +18,6 @@ from typing import Annotated
from .finnhub_common import (
FinnhubError,
RateLimitError,
ThirdPartyError,
ThirdPartyParseError,
ThirdPartyTimeoutError,
_make_api_request,
_now_str,
_rate_limited_request,
@ -31,6 +27,20 @@ from .finnhub_common import (
# Constants
# ---------------------------------------------------------------------------
# Maximum length for error messages embedded in table cells / log lines
_MAX_ERROR_LEN = 60
def _safe_fmt(value, fmt: str = "${:.2f}", fallback: str = "N/A") -> str:
"""Safely format a numeric value, returning *fallback* on None or bad types."""
if value is None:
return fallback
try:
return fmt.format(float(value))
except (ValueError, TypeError):
return str(value)
# Representative S&P 500 large-caps used as the movers basket.
# Sorted roughly by market-cap weight — first 50 cover the bulk of the index.
_SP500_SAMPLE: list[str] = [
@ -170,9 +180,8 @@ def get_market_movers_finnhub(
if quote["current_price"] == 0 and quote["prev_close"] == 0:
continue
rows.append(quote)
except (FinnhubError, RateLimitError, ThirdPartyError,
ThirdPartyTimeoutError, ThirdPartyParseError) as exc:
errors.append(f"{symbol}: {exc!s:.60}")
except FinnhubError as exc:
errors.append(f"{symbol}: {str(exc)[:_MAX_ERROR_LEN]}")
if not rows:
raise FinnhubError(
@ -250,9 +259,8 @@ def get_market_indices_finnhub() -> str:
result += f"| {display_name} | {price_str} | {change_str} | {change_pct_str} |\n"
success_count += 1
except (FinnhubError, RateLimitError, ThirdPartyError,
ThirdPartyTimeoutError, ThirdPartyParseError) as exc:
result += f"| {display_name} | Error | - | {exc!s:.40} |\n"
except FinnhubError as exc:
result += f"| {display_name} | Error | - | {str(exc)[:_MAX_ERROR_LEN]} |\n"
if success_count == 0:
raise FinnhubError("All market index fetches failed.")
@ -291,10 +299,9 @@ def get_sector_performance_finnhub() -> str:
result += f"| {sector_name} | {etf} | {price_str} | {change_pct_str} |\n"
success_count += 1
except (FinnhubError, RateLimitError, ThirdPartyError,
ThirdPartyTimeoutError, ThirdPartyParseError) as exc:
except FinnhubError as exc:
last_error = exc
result += f"| {sector_name} | {etf} | Error | {exc!s:.30} |\n"
result += f"| {sector_name} | {etf} | Error | {str(exc)[:_MAX_ERROR_LEN]} |\n"
# If ALL sectors failed, raise so route_to_vendor can fall back
if success_count == 0 and last_error is not None:
@ -405,12 +412,13 @@ def get_earnings_calendar_finnhub(from_date: str, to_date: str) -> str:
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"
eps_est_s = _safe_fmt(item.get("epsEstimate"))
eps_prior_s = _safe_fmt(item.get("epsPrior"))
rev_raw = item.get("revenueEstimate")
rev_est_s = _safe_fmt(
float(rev_raw) / 1e9 if rev_raw is not None else None,
fmt="${:.2f}B",
)
lines.append(f"| {symbol} | {company} | {date} | {eps_est_s} | {eps_prior_s} | {rev_est_s} |")
return "\n".join(lines)