From d5fb0fdd9486b63b62baacb585fd6f534fa21ded Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 11:38:44 +0100 Subject: [PATCH] Resolve merge conflicts after PR #16 merge into main (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- pyproject.toml | 1 + tests/test_finnhub_integration.py | 8 ++-- tradingagents/dataflows/finnhub_common.py | 12 +++-- .../dataflows/finnhub_fundamentals.py | 1 - tradingagents/dataflows/finnhub_scanner.py | 46 +++++++++++-------- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bad98f0c..b7dc3ce7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/tests/test_finnhub_integration.py b/tests/test_finnhub_integration.py index 57badfe4..cde1743b 100644 --- a/tests/test_finnhub_integration.py +++ b/tests/test_finnhub_integration.py @@ -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" diff --git a/tradingagents/dataflows/finnhub_common.py b/tradingagents/dataflows/finnhub_common.py index 5e9d0dc2..4c69e528 100644 --- a/tradingagents/dataflows/finnhub_common.py +++ b/tradingagents/dataflows/finnhub_common.py @@ -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). diff --git a/tradingagents/dataflows/finnhub_fundamentals.py b/tradingagents/dataflows/finnhub_fundamentals.py index bfa108f5..a91e53b8 100644 --- a/tradingagents/dataflows/finnhub_fundamentals.py +++ b/tradingagents/dataflows/finnhub_fundamentals.py @@ -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 ( diff --git a/tradingagents/dataflows/finnhub_scanner.py b/tradingagents/dataflows/finnhub_scanner.py index 89255010..3fe5c4f9 100644 --- a/tradingagents/dataflows/finnhub_scanner.py +++ b/tradingagents/dataflows/finnhub_scanner.py @@ -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)