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
This commit is contained in:
Yijia-Xiao
2026-09-29 05:57:18 +00:00
parent ec2ecc79c3
commit 466d33ab3b
5 changed files with 51 additions and 20 deletions
+23
View File
@@ -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
+3 -2
View File
@@ -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):
+4 -2
View File
@@ -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
+16 -13
View File
@@ -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
+5 -3
View File
@@ -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