mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-06 17:37:11 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user