Files
tradingagents/tests/test_no_data_handling.py
T
Yijia-Xiao bbd4d0413a fix(yahoo): report an unreachable Yahoo on insider transactions as unavailable
- the outage raised for an empty insider table reaches the router as a vendor error, not as a symbol with no data
2026-09-24 05:00:36 +00:00

106 lines
4.2 KiB
Python

"""Tests that empty vendor results never become fabricated data.
Covers two systematic fixes:
- load_ohlcv must not cache an empty download (cache poisoning), and must
raise NoMarketDataError instead of returning an empty frame.
- route_to_vendor must convert NoMarketDataError into a single explicit
"NO_DATA_AVAILABLE" sentinel after all vendors are exhausted.
"""
import os
import unittest
from unittest import mock
import pandas as pd
import pytest
from tradingagents.dataflows import router
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.vendors.yahoo import ohlcv
@pytest.mark.unit
class TestLoadOhlcvNoPoison(unittest.TestCase):
def setUp(self):
self._tmp = os.path.join(os.path.dirname(__file__), "_tmp_cache")
os.makedirs(self._tmp, exist_ok=True)
set_config({"data_cache_dir": self._tmp})
def tearDown(self):
for f in os.listdir(self._tmp):
os.remove(os.path.join(self._tmp, f))
os.rmdir(self._tmp)
def test_empty_download_raises_and_does_not_cache(self):
empty = pd.DataFrame()
# Yahoo answers, so an empty download means the symbol has no data.
reachable = mock.patch.object(ohlcv, "vendor_reachable", return_value=True)
reachable.start()
self.addCleanup(reachable.stop)
with mock.patch.object(ohlcv.yf, "download", return_value=empty), \
self.assertRaises(NoMarketDataError):
ohlcv.load_ohlcv("FAKE", "2026-01-01")
# Nothing should have been written to the cache.
self.assertEqual(os.listdir(self._tmp), [])
# A second call must re-attempt the fetch (no poisoned cache served).
with mock.patch.object(ohlcv.yf, "download", return_value=empty) as dl2:
with self.assertRaises(NoMarketDataError):
ohlcv.load_ohlcv("FAKE", "2026-01-01")
self.assertTrue(dl2.called)
@pytest.mark.unit
class TestRouteToVendorSentinel(unittest.TestCase):
def test_no_data_from_all_vendors_returns_sentinel(self):
def raises_no_data(symbol, *a, **k):
raise NoMarketDataError(symbol, "GC=F", "no rows")
patched = {"yfinance": raises_no_data, "alpha_vantage": raises_no_data}
with mock.patch.dict(
router.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
):
result = router.route_to_vendor(
"get_stock_data", "XAUUSD+", "2026-01-01", "2026-01-10"
)
self.assertIn("NO_DATA_AVAILABLE", result)
self.assertIn("XAUUSD+", result)
self.assertIn("GC=F", result)
self.assertIn("Do not estimate", result)
def test_unconfigured_fallback_does_not_mask_no_data(self):
# When the primary vendor reports no data and the fallback is simply
# unavailable (e.g. missing API key -> raises), the no-data sentinel
# must win rather than the fallback's incidental error crashing out.
def raises_no_data(symbol, *a, **k):
raise NoMarketDataError(symbol, symbol, "no rows")
def raises_unavailable(symbol, *a, **k):
raise ValueError("ALPHA_VANTAGE_API_KEY environment variable is not set.")
patched = {"yfinance": raises_no_data, "alpha_vantage": raises_unavailable}
with mock.patch.dict(
router.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
):
result = router.route_to_vendor(
"get_stock_data", "FAKE", "2026-01-01", "2026-01-10"
)
self.assertIn("NO_DATA_AVAILABLE", result)
if __name__ == "__main__":
unittest.main()
@pytest.mark.unit
def test_an_unreachable_yahoo_is_not_reported_as_a_symbol_without_insider_data():
from tradingagents.dataflows.errors import VendorRateLimitError
from tradingagents.dataflows.vendors.yahoo import fundamentals
ticker = type("T", (), {"insider_transactions": pd.DataFrame()})()
with mock.patch.object(fundamentals.yf, "Ticker", return_value=ticker), \
mock.patch.object(fundamentals, "vendor_reachable", return_value=False), \
pytest.raises(VendorRateLimitError):
fundamentals.get_insider_transactions("AAPL", curr_date="2026-09-21")