mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 11:15:24 +03:00
fix(dataflows): tell an outage from an unknown symbol on every Yahoo path
- prices, indicators and insider filings now use the same check as the statements
This commit is contained in:
@@ -195,17 +195,17 @@ def test_an_unreachable_vendor_is_not_reported_as_a_missing_symbol(monkeypatch):
|
||||
company has no balance sheet, when the truth is we could not ask."""
|
||||
import pandas as pd
|
||||
|
||||
from tradingagents.dataflows import y_finance
|
||||
from tradingagents.dataflows import stockstats_utils, y_finance
|
||||
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
|
||||
|
||||
empty = mock.Mock(quarterly_balance_sheet=pd.DataFrame(), balance_sheet=pd.DataFrame())
|
||||
monkeypatch.setattr(y_finance.yf, "Ticker", lambda s: empty)
|
||||
|
||||
monkeypatch.setattr(y_finance, "vendor_reachable", lambda url: False)
|
||||
monkeypatch.setattr(stockstats_utils, "vendor_reachable", lambda url: False)
|
||||
with pytest.raises(VendorRateLimitError, match="unreachable"):
|
||||
y_finance.get_balance_sheet("AAPL", "annual", "2026-09-01")
|
||||
|
||||
monkeypatch.setattr(y_finance, "vendor_reachable", lambda url: True)
|
||||
monkeypatch.setattr(stockstats_utils, "vendor_reachable", lambda url: True)
|
||||
with pytest.raises(NoMarketDataError):
|
||||
y_finance.get_balance_sheet("AAPL", "annual", "2026-09-01")
|
||||
|
||||
@@ -226,3 +226,23 @@ def test_every_vendor_unavailable_says_so_rather_than_crashing(monkeypatch):
|
||||
|
||||
assert "unavailable" in out.lower() and "unreachable" in out.lower()
|
||||
assert "delisted" not in out.lower() # not a claim about the symbol
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_price_path_also_tells_an_outage_from_an_unknown_symbol(monkeypatch):
|
||||
"""Prices are the most-used path, so an outage there must not read as a
|
||||
delisted symbol either."""
|
||||
import pandas as pd
|
||||
|
||||
from tradingagents.dataflows import stockstats_utils, y_finance
|
||||
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
|
||||
|
||||
monkeypatch.setattr(y_finance.yf, "Ticker", lambda s: mock.Mock(history=lambda **k: pd.DataFrame()))
|
||||
|
||||
monkeypatch.setattr(stockstats_utils, "vendor_reachable", lambda url: False)
|
||||
with pytest.raises(VendorRateLimitError, match="unreachable"):
|
||||
y_finance.get_YFin_data_online("AAPL", "2026-09-01", "2026-09-10")
|
||||
|
||||
monkeypatch.setattr(stockstats_utils, "vendor_reachable", lambda url: True)
|
||||
with pytest.raises(NoMarketDataError):
|
||||
y_finance.get_YFin_data_online("AAPL", "2026-09-01", "2026-09-10")
|
||||
|
||||
@@ -9,11 +9,14 @@ from stockstats import wrap
|
||||
from yfinance.exceptions import YFRateLimitError
|
||||
|
||||
from .config import get_config
|
||||
from .errors import VendorRateLimitError
|
||||
from .symbol_utils import NoMarketDataError, normalize_symbol
|
||||
from .utils import safe_ticker_component
|
||||
from .utils import safe_ticker_component, vendor_reachable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_YAHOO_HOST = "https://query2.finance.yahoo.com"
|
||||
|
||||
# A vendor's latest OHLCV row this many calendar days before the requested date
|
||||
# is treated as stale. Generous enough to span long holiday weekends, tight
|
||||
# enough to catch the year-old frames yfinance occasionally returns (#1021).
|
||||
@@ -26,6 +29,17 @@ MAX_OHLCV_STALE_DAYS = 10
|
||||
OHLCV_CACHE_TTL_SECONDS = 900
|
||||
|
||||
|
||||
def raise_for_empty(symbol: str, canonical: str, what: str) -> None:
|
||||
"""Report an empty Yahoo result as an absence, or as an outage if it is one.
|
||||
|
||||
yfinance returns an empty frame for a failed request rather than raising, so
|
||||
without this a Yahoo outage reads as "this symbol has no {what}".
|
||||
"""
|
||||
if not vendor_reachable(_YAHOO_HOST):
|
||||
raise VendorRateLimitError(f"Yahoo Finance is unreachable; no {what} was retrieved")
|
||||
raise NoMarketDataError(symbol, canonical, f"no {what}")
|
||||
|
||||
|
||||
def yf_retry(func, max_retries=3, base_delay=2.0):
|
||||
"""Execute a yfinance call with exponential backoff on rate limits.
|
||||
|
||||
@@ -239,9 +253,7 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
|
||||
downloaded = _ensure_date_column(downloaded.reset_index())
|
||||
# Only cache real data — never persist an empty frame.
|
||||
if downloaded.empty or "Close" not in downloaded.columns:
|
||||
raise NoMarketDataError(
|
||||
symbol, canonical, "Yahoo Finance returned no rows"
|
||||
)
|
||||
raise_for_empty(symbol, canonical, "price rows")
|
||||
downloaded.to_csv(data_file, index=False, encoding="utf-8")
|
||||
data = downloaded
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from .stockstats_utils import (
|
||||
_assert_ohlcv_not_stale,
|
||||
filter_financials_by_date,
|
||||
load_ohlcv,
|
||||
raise_for_empty,
|
||||
yf_retry,
|
||||
)
|
||||
from .symbol_utils import NoMarketDataError, normalize_symbol
|
||||
@@ -46,9 +47,7 @@ def get_YFin_data_online(
|
||||
# instead of returning prose: the routing layer turns it into a single
|
||||
# unambiguous "no data" signal so the agent never fabricates a price.
|
||||
if data.empty:
|
||||
raise NoMarketDataError(
|
||||
symbol, canonical, f"no rows between {start_date} and {end_date}"
|
||||
)
|
||||
raise_for_empty(symbol, canonical, f"rows between {start_date} and {end_date}")
|
||||
|
||||
# Remove timezone info from index for cleaner output
|
||||
if data.index.tz is not None:
|
||||
@@ -304,7 +303,7 @@ def get_fundamentals(
|
||||
info = yf_retry(lambda: ticker_obj.info)
|
||||
|
||||
if not info:
|
||||
_raise_for_empty(ticker, canonical, "fundamentals")
|
||||
raise_for_empty(ticker, canonical, "fundamentals")
|
||||
|
||||
fields = [
|
||||
("Name", info.get("longName")),
|
||||
@@ -375,7 +374,7 @@ def get_balance_sheet(
|
||||
data = filter_financials_by_date(data, curr_date)
|
||||
|
||||
if data.empty:
|
||||
_raise_for_empty(ticker, canonical, "balance sheet data")
|
||||
raise_for_empty(ticker, canonical, "balance sheet data")
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
@@ -411,7 +410,7 @@ def get_cashflow(
|
||||
data = filter_financials_by_date(data, curr_date)
|
||||
|
||||
if data.empty:
|
||||
_raise_for_empty(ticker, canonical, "cash flow data")
|
||||
raise_for_empty(ticker, canonical, "cash flow data")
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
@@ -447,7 +446,7 @@ def get_income_statement(
|
||||
data = filter_financials_by_date(data, curr_date)
|
||||
|
||||
if data.empty:
|
||||
_raise_for_empty(ticker, canonical, "income statement data")
|
||||
raise_for_empty(ticker, canonical, "income statement data")
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
@@ -487,17 +486,6 @@ _PERIOD_END_VINTAGE = (
|
||||
)
|
||||
|
||||
|
||||
def _raise_for_empty(ticker: str, canonical: str, what: str) -> None:
|
||||
"""Report an empty result as an absence, or as an outage if Yahoo is down.
|
||||
|
||||
yfinance returns an empty frame for a failed request rather than raising, so
|
||||
without this an outage reads as "this company reports no {what}".
|
||||
"""
|
||||
if not vendor_reachable(_YAHOO_HOST):
|
||||
raise VendorRateLimitError(f"Yahoo Finance is unreachable; no {what} was retrieved")
|
||||
raise NoMarketDataError(ticker, canonical, f"no {what}")
|
||||
|
||||
|
||||
def get_insider_transactions(
|
||||
ticker: Annotated[str, "ticker symbol of the company"],
|
||||
curr_date: Annotated[str | None, "only transactions on or before this date, yyyy-mm-dd"] = None,
|
||||
@@ -511,6 +499,8 @@ def get_insider_transactions(
|
||||
# Empty is normal here (many valid symbols have no insider filings),
|
||||
# so report it plainly rather than treating the symbol as invalid.
|
||||
if data is None or data.empty:
|
||||
if not vendor_reachable(_YAHOO_HOST):
|
||||
raise VendorRateLimitError("Yahoo Finance is unreachable; insider filings were not retrieved")
|
||||
return f"No insider transactions reported for symbol '{canonical}'"
|
||||
|
||||
if curr_date:
|
||||
|
||||
Reference in New Issue
Block a user