From 466d33ab3bbc313401e4eeaec2565b5d92b79977 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Mon, 28 Sep 2026 19:59:50 +0000 Subject: [PATCH] fix(agents): answer a snapshot with no price rows as no data instead of ending the run - build_verified_market_snapshot raises NoMarketDataError for an unknown symbol or no row by the date - get_verified_market_snapshot returns the router's no-data answer, shared as router.no_data_available --- tests/test_yahoo_rate_limit.py | 23 +++++++++++++++ tests/test_yahoo_snapshot.py | 5 ++-- tradingagents/agents/tools.py | 6 ++-- tradingagents/dataflows/router.py | 29 ++++++++++--------- .../dataflows/vendors/yahoo/snapshot.py | 8 +++-- 5 files changed, 51 insertions(+), 20 deletions(-) diff --git a/tests/test_yahoo_rate_limit.py b/tests/test_yahoo_rate_limit.py index a24f1d179..ffbab5ec4 100644 --- a/tests/test_yahoo_rate_limit.py +++ b/tests/test_yahoo_rate_limit.py @@ -176,3 +176,26 @@ def test_a_yahoo_request_that_fails_is_unavailable_not_absent(yahoo, call): with pytest.raises(VendorUnavailableError): call() + + +@pytest.mark.unit +@pytest.mark.parametrize("frame", [ + pytest.param(pd.DataFrame(), id="unknown_symbol"), + pytest.param(pd.DataFrame({"Date": pd.to_datetime(["2026-09-25"]), "Open": [1.0], "High": [1.0], + "Low": [1.0], "Close": [1.0], "Volume": [1]}), id="no_row_by_the_date"), +]) +def test_a_snapshot_with_no_rows_is_reported_not_raised(yahoo, frame): + """An exception out of a tool ends the run; no rows is an answer about the symbol.""" + from tradingagents.dataflows.errors import NoMarketDataError + from tradingagents.dataflows.vendors.yahoo import snapshot + + def load(symbol, as_of_date, **kwargs): + if frame.empty: + raise NoMarketDataError(symbol, symbol, "no price rows") + return frame + + yahoo.setattr(snapshot, "load_ohlcv", load) + + out = get_verified_market_snapshot.func("ZZZZ", DAY, trade_date=DAY) + + assert out.startswith("NO_DATA_AVAILABLE"), out diff --git a/tests/test_yahoo_snapshot.py b/tests/test_yahoo_snapshot.py index 51c852c08..07dbc3d7e 100644 --- a/tests/test_yahoo_snapshot.py +++ b/tests/test_yahoo_snapshot.py @@ -6,6 +6,7 @@ import pandas as pd import pytest import tradingagents.dataflows.vendors.yahoo.snapshot as validator +from tradingagents.dataflows.errors import NoMarketDataError def _sample_ohlcv() -> pd.DataFrame: @@ -47,12 +48,12 @@ class TestVerifiedSnapshot: def test_raises_when_no_rows_on_or_before_date(self, monkeypatch): monkeypatch.setattr(validator, "load_ohlcv", lambda s, d, fill_gaps=True: _sample_ohlcv()) - with pytest.raises(ValueError): + with pytest.raises(NoMarketDataError): validator.build_verified_market_snapshot("COF", "2020-01-01") def test_raises_on_empty_data(self, monkeypatch): monkeypatch.setattr(validator, "load_ohlcv", lambda s, d, fill_gaps=True: pd.DataFrame()) - with pytest.raises(ValueError): + with pytest.raises(NoMarketDataError): validator.build_verified_market_snapshot("COF", "2026-05-13") def test_look_back_window_capped_at_30(self, monkeypatch): diff --git a/tradingagents/agents/tools.py b/tradingagents/agents/tools.py index ef4949164..b8b323cba 100644 --- a/tradingagents/agents/tools.py +++ b/tradingagents/agents/tools.py @@ -10,8 +10,8 @@ from langchain_core.tools import tool from langgraph.prebuilt import InjectedState from tradingagents.dataflows.date_window import as_of, as_of_window -from tradingagents.dataflows.errors import VendorUnavailableError -from tradingagents.dataflows.router import route_to_vendor, vendor_unavailable +from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError +from tradingagents.dataflows.router import no_data_available, route_to_vendor, vendor_unavailable from tradingagents.dataflows.vendors.yahoo.snapshot import build_verified_market_snapshot @@ -87,6 +87,8 @@ def get_verified_market_snapshot( return build_verified_market_snapshot(symbol, as_of(curr_date, trade_date), look_back_days) except VendorUnavailableError as exc: return vendor_unavailable("get_verified_market_snapshot", exc) + except NoMarketDataError as exc: + return no_data_available(exc) @tool diff --git a/tradingagents/dataflows/router.py b/tradingagents/dataflows/router.py index b5f0d423f..b047e7699 100644 --- a/tradingagents/dataflows/router.py +++ b/tradingagents/dataflows/router.py @@ -182,6 +182,21 @@ def vendor_unavailable(method: str, error: Exception) -> str: ) +def no_data_available(error: NoMarketDataError) -> str: + """What a call returns when every vendor that answered had no usable data.""" + resolved = "" if error.canonical == error.symbol else f" (resolved to '{error.canonical}')" + # Surface the typed error's detail (e.g. "latest row is 2025-06-11 ... + # stale") so the agent sees the specific reason — invalid symbol, no + # coverage, or stale data — not just a generic "unavailable". + reason = f" ({error.detail})" if error.detail else "" + return ( + f"NO_DATA_AVAILABLE: No usable market data for '{error.symbol}'{resolved} from " + f"any configured vendor{reason}. The symbol may be invalid, delisted, " + f"not covered, or the vendor returned stale data. Do not estimate or " + f"fabricate values — report that data is unavailable for this symbol." + ) + + def route_to_vendor(method: str, *args, **kwargs): """Route method calls to appropriate vendor implementation with fallback support.""" category = get_category_for_method(method) @@ -264,19 +279,7 @@ def route_to_vendor(method: str, *args, **kwargs): "Returning NO_DATA for %s, but a vendor errored earlier: %s", method, first_error, ) - sym = last_no_data.symbol - canonical = last_no_data.canonical - resolved = "" if canonical == sym else f" (resolved to '{canonical}')" - # Surface the typed error's detail (e.g. "latest row is 2025-06-11 ... - # stale") so the agent sees the specific reason — invalid symbol, no - # coverage, or stale data — not just a generic "unavailable". - reason = f" ({last_no_data.detail})" if last_no_data.detail else "" - return ( - f"NO_DATA_AVAILABLE: No usable market data for '{sym}'{resolved} from " - f"any configured vendor{reason}. The symbol may be invalid, delisted, " - f"not covered, or the vendor returned stale data. Do not estimate or " - f"fabricate values — report that data is unavailable for this symbol." - ) + return no_data_available(last_no_data) # No vendor returned data and none reported clean "no data" — surface the # first real error (e.g. the primary vendor's network failure). Optional diff --git a/tradingagents/dataflows/vendors/yahoo/snapshot.py b/tradingagents/dataflows/vendors/yahoo/snapshot.py index 06c16bd3c..3bec5ebc5 100644 --- a/tradingagents/dataflows/vendors/yahoo/snapshot.py +++ b/tradingagents/dataflows/vendors/yahoo/snapshot.py @@ -15,6 +15,8 @@ from collections.abc import Iterable import pandas as pd from stockstats import wrap +from tradingagents.dataflows.errors import NoMarketDataError +from tradingagents.dataflows.symbols import normalize_symbol from tradingagents.dataflows.vendors.yahoo.ohlcv import load_ohlcv # A fixed, common indicator set so the snapshot is the same shape every run. @@ -26,7 +28,7 @@ DEFAULT_SNAPSHOT_INDICATORS: tuple[str, ...] = ( def _verified_rows(symbol: str, as_of_date: str) -> pd.DataFrame: - """OHLCV on or before as_of_date, date-sorted. Raises if nothing usable. + """OHLCV on or before as_of_date, date-sorted. Raises NoMarketDataError if nothing usable. ``load_ohlcv`` already normalizes the Date column and filters out look-ahead rows, but we re-apply the cutoff defensively — this is a @@ -36,14 +38,14 @@ def _verified_rows(symbol: str, as_of_date: str) -> pd.DataFrame: # gap-filled cell would put the previous session's number under this date. data = load_ohlcv(symbol, as_of_date, fill_gaps=False) if data is None or data.empty: - raise ValueError(f"No OHLCV data available for {symbol}.") + raise NoMarketDataError(symbol, normalize_symbol(symbol), "no price rows") df = data.copy() df["Date"] = pd.to_datetime(df["Date"], errors="coerce") df = df.dropna(subset=["Date"]) df = df[df["Date"] <= pd.to_datetime(as_of_date)].sort_values("Date") if df.empty: - raise ValueError(f"No OHLCV rows on or before {as_of_date} for {symbol}.") + raise NoMarketDataError(symbol, normalize_symbol(symbol), f"no price rows on or before {as_of_date}") return df