refactor(dataflows): group the vendors under dataflows/vendors

- vendors/yahoo: ohlcv (loader and cache), market (prices, indicators), fundamentals (profile, statements, insider), news, snapshot
- vendors/alpha_vantage is a package; sec_edgar, fred, polymarket, reddit and stocktwits sit beside it
- the one-method StockstatsUtils class is a function; the duplicate Yahoo host constant is gone
- tests are named after the modules they cover: test_ohlcv_date_column, test_yahoo_snapshot, and the ohlcv and snapshot aliases
This commit is contained in:
Yijia-Xiao
2026-09-24 04:37:40 +00:00
parent c42a2f2c61
commit 6097b582d9
45 changed files with 417 additions and 389 deletions
+7 -7
View File
@@ -10,10 +10,10 @@ import json
import pytest
import tradingagents.dataflows.alpha_vantage_common as av
import tradingagents.dataflows.alpha_vantage_fundamentals as avf
import tradingagents.dataflows.alpha_vantage_stock as avs
import tradingagents.dataflows.net as net
import tradingagents.dataflows.vendors.alpha_vantage.common as av
import tradingagents.dataflows.vendors.alpha_vantage.fundamentals as avf
import tradingagents.dataflows.vendors.alpha_vantage.stock as avs
class _FakeResponse:
@@ -156,7 +156,7 @@ def test_request_error_message_carries_no_key(monkeypatch):
@pytest.mark.unit
def test_global_news_omitted_optionals_use_the_configured_defaults(monkeypatch):
"""The tool passes None for an omitted look_back_days or limit (#1326)."""
from tradingagents.dataflows import alpha_vantage_news
from tradingagents.dataflows.vendors.alpha_vantage import news as alpha_vantage_news
monkeypatch.setattr(alpha_vantage_news, "get_config",
lambda: {"global_news_lookback_days": 3, "global_news_article_limit": 9})
@@ -173,7 +173,7 @@ def test_the_news_window_includes_the_analysis_day(monkeypatch):
"""time_to was midnight at the start of the end date, so everything
published during the analysis day, the most decision-relevant day, was
excluded. The yfinance path includes it."""
from tradingagents.dataflows import alpha_vantage_news
from tradingagents.dataflows.vendors.alpha_vantage import news as alpha_vantage_news
seen = {}
monkeypatch.setattr(alpha_vantage_news, "_make_api_request",
@@ -190,8 +190,8 @@ def test_the_news_window_includes_the_analysis_day(monkeypatch):
def test_an_indicator_this_vendor_lacks_lets_the_next_one_serve_it(indicator):
"""Returning prose counts as success to the router, so the chain stops at a
vendor that cannot compute the indicator while the next one can."""
from tradingagents.dataflows import alpha_vantage_indicator
from tradingagents.dataflows.errors import VendorError
from tradingagents.dataflows.vendors.alpha_vantage import indicator as alpha_vantage_indicator
with pytest.raises(VendorError):
alpha_vantage_indicator.get_indicator("AAPL", indicator, "2026-05-08", 30)
@@ -201,7 +201,7 @@ def test_an_indicator_this_vendor_lacks_lets_the_next_one_serve_it(indicator):
def test_ticker_news_asks_for_only_as_many_articles_as_configured(monkeypatch):
"""The endpoint returns 50 articles with per-article sentiment arrays by
default, and the whole payload went into the prompt."""
from tradingagents.dataflows import alpha_vantage_news
from tradingagents.dataflows.vendors.alpha_vantage import news as alpha_vantage_news
monkeypatch.setattr(alpha_vantage_news, "get_config", lambda: {"news_article_limit": 8})
seen = {}
+4 -4
View File
@@ -7,9 +7,9 @@ row omitted).
import pandas as pd
import pytest
import tradingagents.dataflows.stockstats_utils as su
import tradingagents.dataflows.y_finance as yfin
import tradingagents.dataflows.vendors.yahoo.market as yfin
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.vendors.yahoo import ohlcv
@pytest.mark.unit
@@ -53,9 +53,9 @@ def test_load_ohlcv_requests_inclusive_end(monkeypatch, tmp_path):
index=idx,
)
monkeypatch.setattr(su.yf, "download", fake_download)
monkeypatch.setattr(ohlcv.yf, "download", fake_download)
today = pd.Timestamp.today().strftime("%Y-%m-%d")
su.load_ohlcv("AAPL", today)
ohlcv.load_ohlcv("AAPL", today)
expected_end = (pd.Timestamp.today() + pd.Timedelta(days=1)).strftime("%Y-%m-%d")
assert captured["end"] == expected_end # tomorrow -> today's row included (#986)
+2 -1
View File
@@ -12,8 +12,9 @@ import requests
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import fred, router
from tradingagents.dataflows import router
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.vendors import fred
# A small, stable set of observations to format against.
_META = {
+11 -6
View File
@@ -19,7 +19,12 @@ from unittest import mock
import pytest
from tradingagents.dataflows import alpha_vantage_fundamentals as av, date_window, y_finance
from tradingagents.dataflows import date_window
from tradingagents.dataflows.vendors.alpha_vantage import fundamentals as av
from tradingagents.dataflows.vendors.yahoo import (
fundamentals as yahoo_fundamentals,
market as yahoo_market,
)
_TODAY = "2026-09-07"
_PAST = "2024-05-10"
@@ -42,9 +47,9 @@ _LEAKY = ("3500000000000", "34.2", "260.1", "391000000000",
def _yf(curr_date, info=_INFO, today=_TODAY):
with mock.patch.object(date_window, "get_current_date", return_value=today), \
mock.patch.object(y_finance, "yf_retry", lambda fn: info), \
mock.patch.object(y_finance.yf, "Ticker"):
return y_finance.get_fundamentals("AAPL", curr_date)
mock.patch.object(yahoo_fundamentals, "yf_retry", lambda fn: info), \
mock.patch.object(yahoo_market.yf, "Ticker"):
return yahoo_fundamentals.get_fundamentals("AAPL", curr_date)
def _av(curr_date, today=_TODAY):
@@ -78,8 +83,8 @@ class TestYFinanceHistoricalRun:
# The response would only be discarded; skipping it also avoids burning
# vendor quota on a call whose result cannot be used.
with mock.patch.object(date_window, "get_current_date", return_value=_TODAY), \
mock.patch.object(y_finance.yf, "Ticker") as tk:
y_finance.get_fundamentals("AAPL", _PAST)
mock.patch.object(yahoo_market.yf, "Ticker") as tk:
yahoo_fundamentals.get_fundamentals("AAPL", _PAST)
tk.assert_not_called()
+4 -3
View File
@@ -20,7 +20,8 @@ from pydantic import Field
from tradingagents.agents import schemas
from tradingagents.agents.analysts import sentiment_analyst
from tradingagents.agents.utils import agent_utils
from tradingagents.dataflows import market_data_validator, router, y_finance
from tradingagents.dataflows import router
from tradingagents.dataflows.vendors.yahoo import market as yahoo_market, snapshot
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.graph import trading_graph
@@ -102,11 +103,11 @@ def offline(monkeypatch, tmp_path):
"Date": pd.bdate_range(end=TRADE_DATE, periods=60),
"Open": 100.0, "High": 101.0, "Low": 99.0, "Close": 100.5, "Volume": 1_000_000,
})
monkeypatch.setattr(market_data_validator, "load_ohlcv",
monkeypatch.setattr(snapshot, "load_ohlcv",
lambda *a, **k: called.add("ohlcv") or prices.copy())
monkeypatch.setattr(sentiment_analyst, "fetch_stocktwits_messages", lambda *a, **k: "no posts")
monkeypatch.setattr(sentiment_analyst, "fetch_reddit_posts", lambda *a, **k: "no posts")
monkeypatch.setattr(y_finance.yf, "Ticker", lambda s: type("T", (), {"info": {"longName": "NVIDIA"}})())
monkeypatch.setattr(yahoo_market.yf, "Ticker", lambda s: type("T", (), {"info": {"longName": "NVIDIA"}})())
agent_utils.resolve_instrument_identity.cache_clear()
return called
+6 -6
View File
@@ -21,7 +21,7 @@ class ResolveInstrumentIdentityTests(unittest.TestCase):
resolve_instrument_identity.cache_clear()
def test_resolves_company_metadata_from_yfinance(self):
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
with patch("tradingagents.dataflows.vendors.yahoo.market.yf.Ticker") as mock:
mock.return_value.info = {
"longName": "TOTO LTD.",
"shortName": "TOTO",
@@ -38,26 +38,26 @@ class ResolveInstrumentIdentityTests(unittest.TestCase):
self.assertEqual(identity["exchange"], "PNK")
def test_falls_back_to_short_name(self):
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
with patch("tradingagents.dataflows.vendors.yahoo.market.yf.Ticker") as mock:
mock.return_value.info = {"shortName": "TOTO", "sector": "Industrials"}
identity = resolve_instrument_identity("TOTDY")
self.assertEqual(identity["company_name"], "TOTO")
def test_skips_placeholder_values(self):
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
with patch("tradingagents.dataflows.vendors.yahoo.market.yf.Ticker") as mock:
mock.return_value.info = {"longName": " ", "sector": "None", "industry": "n/a"}
identity = resolve_instrument_identity("TOTDY")
self.assertEqual(identity, {})
def test_fails_open_on_exception(self):
with patch(
"tradingagents.dataflows.y_finance.yf.Ticker",
"tradingagents.dataflows.vendors.yahoo.market.yf.Ticker",
side_effect=RuntimeError("rate limited"),
):
self.assertEqual(resolve_instrument_identity("TOTDY"), {})
def test_result_is_cached(self):
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
with patch("tradingagents.dataflows.vendors.yahoo.market.yf.Ticker") as mock:
mock.return_value.info = {"longName": "TOTO LTD."}
first = resolve_instrument_identity("TOTDY")
second = resolve_instrument_identity("TOTDY")
@@ -104,7 +104,7 @@ class GetInstrumentContextFromStateTests(unittest.TestCase):
def test_fallback_is_network_free_ticker_only(self):
# No instrument_context and no yfinance call — must not hit the network.
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
with patch("tradingagents.dataflows.vendors.yahoo.market.yf.Ticker") as mock:
context = get_instrument_context_from_state(
{"company_of_interest": "NVDA", "asset_type": "stock"}
)
+1 -1
View File
@@ -1042,7 +1042,7 @@ def test_a_longer_window_asks_for_enough_price_history(monkeypatch):
days = pd.bdate_range(start, end)
return pd.DataFrame({"Close": range(len(days))}, index=days)
monkeypatch.setattr("tradingagents.dataflows.y_finance.yf.Ticker", _Ticker)
monkeypatch.setattr("tradingagents.dataflows.vendors.yahoo.market.yf.Ticker", _Ticker)
raw, alpha, days, resolved = graph._fetch_returns("NVDA", "2026-06-01", 21, benchmark="SPY")
+1 -1
View File
@@ -10,7 +10,7 @@ from datetime import datetime, timezone
import pytest
import tradingagents.dataflows.yfinance_news as ynews
import tradingagents.dataflows.vendors.yahoo.news as ynews
from tradingagents.dataflows.date_window import in_window
+6 -5
View File
@@ -14,9 +14,10 @@ from unittest import mock
import pandas as pd
import pytest
from tradingagents.dataflows import router, stockstats_utils
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
@@ -33,16 +34,16 @@ class TestLoadOhlcvNoPoison(unittest.TestCase):
def test_empty_download_raises_and_does_not_cache(self):
empty = pd.DataFrame()
with mock.patch.object(stockstats_utils.yf, "download", return_value=empty), \
with mock.patch.object(ohlcv.yf, "download", return_value=empty), \
self.assertRaises(NoMarketDataError):
stockstats_utils.load_ohlcv("FAKE", "2026-01-01")
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(stockstats_utils.yf, "download", return_value=empty) as dl2:
with mock.patch.object(ohlcv.yf, "download", return_value=empty) as dl2:
with self.assertRaises(NoMarketDataError):
stockstats_utils.load_ohlcv("FAKE", "2026-01-01")
ohlcv.load_ohlcv("FAKE", "2026-01-01")
self.assertTrue(dl2.called)
+15 -15
View File
@@ -12,10 +12,10 @@ import os
import pandas as pd
import pytest
import tradingagents.dataflows.stockstats_utils as su
from tradingagents.dataflows.vendors.yahoo import ohlcv
NOW = pd.Timestamp("2026-07-18 12:00")
STALE = su.OHLCV_CACHE_TTL_SECONDS + 60
STALE = ohlcv.OHLCV_CACHE_TTL_SECONDS + 60
def _stamp(path, ts):
@@ -33,10 +33,10 @@ def _write(tmp_path, name="AAPL-YFin-data.csv", age_seconds=0.0, last_date="2026
def _load(tmp_path, monkeypatch, curr_date, download):
monkeypatch.setattr(su, "get_config", lambda: {"data_cache_dir": str(tmp_path)})
monkeypatch.setattr(su.pd.Timestamp, "today", staticmethod(lambda: NOW))
monkeypatch.setattr(su.yf, "download", download)
return su.load_ohlcv("AAPL", curr_date)
monkeypatch.setattr(ohlcv, "get_config", lambda: {"data_cache_dir": str(tmp_path)})
monkeypatch.setattr(ohlcv.pd.Timestamp, "today", staticmethod(lambda: NOW))
monkeypatch.setattr(ohlcv.yf, "download", download)
return ohlcv.load_ohlcv("AAPL", curr_date)
def _fail_download(*a, **k):
@@ -46,27 +46,27 @@ def _fail_download(*a, **k):
@pytest.mark.unit
def test_current_day_cache_past_ttl_is_not_fresh(tmp_path):
# Today's bar missing or still in progress: row inspection can't tell, so the TTL governs.
assert su._cache_is_fresh(_write(tmp_path, age_seconds=STALE), NOW.normalize(), NOW) is False
assert ohlcv._cache_is_fresh(_write(tmp_path, age_seconds=STALE), NOW.normalize(), NOW) is False
f = _write(tmp_path, age_seconds=STALE, last_date="2026-07-18")
assert su._cache_is_fresh(f, NOW.normalize(), NOW) is False
assert ohlcv._cache_is_fresh(f, NOW.normalize(), NOW) is False
@pytest.mark.unit
def test_recent_cache_is_fresh(tmp_path):
# Written moments ago: don't hammer the vendor (weekend/holiday guard).
assert su._cache_is_fresh(_write(tmp_path), NOW.normalize(), NOW) is True
assert ohlcv._cache_is_fresh(_write(tmp_path), NOW.normalize(), NOW) is True
@pytest.mark.unit
def test_historical_request_uses_todays_cache_past_the_ttl(tmp_path):
f = _write(tmp_path, age_seconds=STALE, last_date="2026-04-30")
assert su._cache_is_fresh(f, pd.Timestamp("2026-05-01"), NOW) is True
assert ohlcv._cache_is_fresh(f, pd.Timestamp("2026-05-01"), NOW) is True
@pytest.mark.unit
def test_a_download_from_an_earlier_day_is_not_fresh(tmp_path):
f = _write(tmp_path, age_seconds=13 * 3600) # yesterday 23:00
assert su._cache_is_fresh(f, pd.Timestamp("2026-05-01"), NOW) is False
assert ohlcv._cache_is_fresh(f, pd.Timestamp("2026-05-01"), NOW) is False
@pytest.mark.unit
@@ -95,15 +95,15 @@ def test_load_ohlcv_reuses_fresh_same_day_cache(tmp_path, monkeypatch):
@pytest.mark.unit
def test_one_cache_file_per_symbol_across_days(tmp_path, monkeypatch):
"""A later day's download replaces the symbol's file instead of adding one (#1330)."""
monkeypatch.setattr(su, "get_config", lambda: {"data_cache_dir": str(tmp_path)})
monkeypatch.setattr(ohlcv, "get_config", lambda: {"data_cache_dir": str(tmp_path)})
frame = pd.DataFrame({"Date": pd.to_datetime(["2026-07-16", "2026-07-17"]), "Close": [1.0, 2.0]})
downloads = []
monkeypatch.setattr(su.yf, "download", lambda *a, **k: downloads.append(1) or frame.set_index("Date"))
monkeypatch.setattr(ohlcv.yf, "download", lambda *a, **k: downloads.append(1) or frame.set_index("Date"))
for day in ("2026-07-18 10:00", "2026-07-19 10:00", "2026-07-20 10:00"):
now = pd.Timestamp(day)
monkeypatch.setattr(su.pd.Timestamp, "today", staticmethod(lambda now=now: now))
su.load_ohlcv("AAPL", "2026-07-17")
monkeypatch.setattr(ohlcv.pd.Timestamp, "today", staticmethod(lambda now=now: now))
ohlcv.load_ohlcv("AAPL", "2026-07-17")
written = list(tmp_path.glob("AAPL-*.csv"))
_stamp(written[0], now)
@@ -1,4 +1,4 @@
"""Tests for tolerating a non-`Date` index column in stockstats_utils (#890).
"""Tests for tolerating a non-`Date` index column in the Yahoo OHLCV loader (#890).
Guards against a download frame whose date column is `index` or `Datetime`
instead of `Date`, which would otherwise silently drop every indicator.
@@ -9,7 +9,7 @@ from __future__ import annotations
import pandas as pd
import pytest
from tradingagents.dataflows import stockstats_utils as su
from tradingagents.dataflows.vendors.yahoo import ohlcv
def _ohlcv(date_col: str) -> pd.DataFrame:
@@ -28,20 +28,20 @@ def _ohlcv(date_col: str) -> pd.DataFrame:
@pytest.mark.unit
class TestEnsureDateColumn:
def test_renames_index_column(self):
out = su._ensure_date_column(_ohlcv("index"))
out = ohlcv._ensure_date_column(_ohlcv("index"))
assert "Date" in out.columns and "index" not in out.columns
def test_renames_datetime_and_date_variants(self):
assert "Date" in su._ensure_date_column(_ohlcv("Datetime")).columns
assert "Date" in su._ensure_date_column(_ohlcv("date")).columns
assert "Date" in ohlcv._ensure_date_column(_ohlcv("Datetime")).columns
assert "Date" in ohlcv._ensure_date_column(_ohlcv("date")).columns
def test_leaves_existing_date_untouched(self):
df = _ohlcv("Date")
assert su._ensure_date_column(df) is df # no-op short-circuit
assert ohlcv._ensure_date_column(df) is df # no-op short-circuit
def test_no_datelike_column_is_left_alone(self):
df = pd.DataFrame({"Close": [1, 2, 3]})
out = su._ensure_date_column(df)
out = ohlcv._ensure_date_column(df)
assert "Date" not in out.columns # nothing to rename; caller handles
@@ -50,20 +50,20 @@ class TestCleanDataframeAcrossVersions:
def test_clean_handles_index_column(self):
"""A frame with `index` instead of `Date` must still clean to a
usable, date-parsed frame (was KeyError: 'Date')."""
cleaned = su._clean_dataframe(_ohlcv("index"))
cleaned = ohlcv._clean_dataframe(_ohlcv("index"))
assert "Date" in cleaned.columns
assert pd.api.types.is_datetime64_any_dtype(cleaned["Date"])
assert len(cleaned) == 10
def test_clean_handles_legacy_date_column(self):
cleaned = su._clean_dataframe(_ohlcv("Date"))
cleaned = ohlcv._clean_dataframe(_ohlcv("Date"))
assert len(cleaned) == 10
def test_indicators_compute_after_index_rename(self):
"""stockstats must compute indicators on a frame whose date column
arrived as `index`, instead of erroring per indicator."""
from stockstats import wrap
cleaned = su._clean_dataframe(_ohlcv("index"))
cleaned = ohlcv._clean_dataframe(_ohlcv("index"))
df = wrap(cleaned)
df["close_5_sma"] # triggers calculation
assert "close_5_sma" in df.columns
+16 -16
View File
@@ -18,8 +18,8 @@ import os
import pandas as pd
import pytest
from tradingagents.dataflows import stockstats_utils as su
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.vendors.yahoo import ohlcv
def _stamp(path, ts):
@@ -35,7 +35,7 @@ def test_normalize_dates_strips_tz_and_normalizes_to_midnight():
aware = pd.Series(pd.to_datetime(
["2026-05-08 09:30:00-04:00", "2026-05-09 16:00:00-04:00"]
))
out = su._normalize_dates(aware)
out = ohlcv._normalize_dates(aware)
assert out.dt.tz is None
assert list(out) == [pd.Timestamp("2026-05-08"), pd.Timestamp("2026-05-09")]
@@ -43,7 +43,7 @@ def test_normalize_dates_strips_tz_and_normalizes_to_midnight():
@pytest.mark.unit
def test_normalize_dates_leaves_naive_dates_at_midnight():
naive = pd.Series(pd.to_datetime(["2026-05-08 14:30:00", "2026-05-09 00:00:00"]))
out = su._normalize_dates(naive)
out = ohlcv._normalize_dates(naive)
assert out.dt.tz is None
assert list(out) == [pd.Timestamp("2026-05-08"), pd.Timestamp("2026-05-09")]
@@ -57,7 +57,7 @@ def test_normalize_dates_handles_mixed_dst_offsets():
"2026-06-08 00:00:00-04:00", # EDT
"not-a-date", # -> NaT
])
out = su._normalize_dates(mixed)
out = ohlcv._normalize_dates(mixed)
assert out.iloc[0] == pd.Timestamp("2026-01-08")
assert out.iloc[1] == pd.Timestamp("2026-06-08")
assert pd.isna(out.iloc[2])
@@ -68,7 +68,7 @@ def test_normalize_dates_keeps_positive_offset_local_date():
# A Tokyo bar at local midnight (+09:00) must stay on its own calendar day,
# not shift to the previous UTC day (which utc=True parsing would cause).
jst = pd.Series(["2026-05-08 00:00:00+09:00"])
assert su._normalize_dates(jst).iloc[0] == pd.Timestamp("2026-05-08")
assert ohlcv._normalize_dates(jst).iloc[0] == pd.Timestamp("2026-05-08")
# --- fill vs guard responsibilities ----------------------------------------
@@ -77,7 +77,7 @@ def test_normalize_dates_keeps_positive_offset_local_date():
def test_clean_dataframe_keeps_nan_close_for_the_caller_to_inspect():
# _clean_dataframe normalizes but no longer drops the NaN close itself.
df = pd.DataFrame({"Date": ["2026-05-08", "2026-05-09"], "Close": [100.0, float("nan")]})
cleaned = su._clean_dataframe(df)
cleaned = ohlcv._clean_dataframe(df)
assert len(cleaned) == 2
assert pd.isna(cleaned["Close"].iloc[-1])
@@ -86,7 +86,7 @@ def test_clean_dataframe_keeps_nan_close_for_the_caller_to_inspect():
def test_fill_price_gaps_drops_nan_close_rows():
df = pd.DataFrame({"Date": pd.to_datetime(["2026-05-07", "2026-05-08"]),
"Close": [float("nan"), 100.0]})
filled = su._fill_price_gaps(df)
filled = ohlcv._fill_price_gaps(df)
assert len(filled) == 1
assert filled["Close"].iloc[0] == 100.0
@@ -95,17 +95,17 @@ def test_fill_price_gaps_drops_nan_close_rows():
def _run_load(monkeypatch, tmp_path, frame, curr_date):
"""Drive load_ohlcv against a pre-seeded cache frame (no network)."""
monkeypatch.setattr(su, "get_config", lambda: {"data_cache_dir": str(tmp_path)})
monkeypatch.setattr(ohlcv, "get_config", lambda: {"data_cache_dir": str(tmp_path)})
today = pd.Timestamp(curr_date)
monkeypatch.setattr(su.pd.Timestamp, "today", staticmethod(lambda: today))
monkeypatch.setattr(ohlcv.pd.Timestamp, "today", staticmethod(lambda: today))
cache_file = tmp_path / "AAPL-YFin-data.csv"
cache_file.write_text(frame.to_csv(index=False))
_stamp(cache_file, today)
def _fail_download(*a, **k):
raise AssertionError("should use the seeded cache, not download")
monkeypatch.setattr(su.yf, "download", _fail_download)
return su.load_ohlcv("AAPL", curr_date)
monkeypatch.setattr(ohlcv.yf, "download", _fail_download)
return ohlcv.load_ohlcv("AAPL", curr_date)
@pytest.mark.unit
@@ -181,7 +181,7 @@ def test_the_snapshot_does_not_present_a_filled_price_as_reported(monkeypatch, t
"""Gap filling exists so indicators compute on a continuous series. The
verification snapshot is the one place a number must be what the vendor
reported, or the module built to stop invented prices supplies them."""
from tradingagents.dataflows import market_data_validator as mdv, stockstats_utils as su
from tradingagents.dataflows.vendors.yahoo import ohlcv, snapshot
frame = pd.DataFrame({
"Date": ["2026-05-06", "2026-05-07", "2026-05-08"],
@@ -192,15 +192,15 @@ def test_the_snapshot_does_not_present_a_filled_price_as_reported(monkeypatch, t
"Volume": [1000000, 1000000, ""],
})
today = pd.Timestamp("2026-05-08 12:00")
monkeypatch.setattr(su, "get_config", lambda: {"data_cache_dir": str(tmp_path)})
monkeypatch.setattr(su.pd.Timestamp, "today", staticmethod(lambda: today))
monkeypatch.setattr(ohlcv, "get_config", lambda: {"data_cache_dir": str(tmp_path)})
monkeypatch.setattr(ohlcv.pd.Timestamp, "today", staticmethod(lambda: today))
cache = tmp_path / "AAPL-YFin-data.csv"
cache.write_text(frame.to_csv(index=False))
_stamp(cache, today)
monkeypatch.setattr(su.yf, "download", lambda *a, **k: (_ for _ in ()).throw(
monkeypatch.setattr(ohlcv.yf, "download", lambda *a, **k: (_ for _ in ()).throw(
AssertionError("should read the seeded cache")))
out = mdv.build_verified_market_snapshot("AAPL", "2026-05-08", 3)
out = snapshot.build_verified_market_snapshot("AAPL", "2026-05-08", 3)
row = out.split("Latest verified OHLCV row")[1].split("###")[0]
assert "104.50" not in row and "105.50" not in row # the previous session's numbers
+2 -1
View File
@@ -12,8 +12,9 @@ import requests
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import polymarket, router
from tradingagents.dataflows import router
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.vendors import polymarket
def _market(question, prob, *, volume, end_date, closed=False, wk=None):
+1 -1
View File
@@ -9,7 +9,7 @@ from urllib.error import HTTPError
import pytest
from tradingagents.dataflows import reddit
from tradingagents.dataflows.vendors import reddit
_SAMPLE_ATOM = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
+1 -1
View File
@@ -13,8 +13,8 @@ from unittest import mock
import pytest
from tradingagents.dataflows import sec_edgar
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.vendors import sec_edgar
_REAL_FETCH = sec_edgar._fetch_json
+1 -1
View File
@@ -12,8 +12,8 @@ from datetime import datetime, timezone
import pytest
from tradingagents.dataflows import reddit, stocktwits
from tradingagents.dataflows.date_window import in_window
from tradingagents.dataflows.vendors import reddit, stocktwits
class _JsonResp:
+1 -1
View File
@@ -13,7 +13,7 @@ from urllib.error import HTTPError
import pytest
from tradingagents.dataflows import stocktwits
from tradingagents.dataflows.vendors import stocktwits
def _raise(exc):
+4 -4
View File
@@ -8,8 +8,8 @@ hit the right instrument instead of failing/mismatching.
import pandas as pd
import tradingagents.agents.utils.agent_utils as au
import tradingagents.dataflows.y_finance as y_finance
import tradingagents.dataflows.yfinance_news as ynews
import tradingagents.dataflows.vendors.yahoo.market as yahoo_market
import tradingagents.dataflows.vendors.yahoo.news as ynews
from tradingagents.graph.trading_graph import TradingAgentsGraph
@@ -24,7 +24,7 @@ def test_identity_lookup_normalizes_symbol(monkeypatch):
def info(self):
return {"longName": "Gold Futures", "quoteType": "FUTURE"}
monkeypatch.setattr(y_finance.yf, "Ticker", FakeTicker)
monkeypatch.setattr(yahoo_market.yf, "Ticker", FakeTicker)
au.resolve_instrument_identity.cache_clear()
identity = au.resolve_instrument_identity("XAUUSD")
@@ -45,7 +45,7 @@ def test_fetch_returns_normalizes_symbol(monkeypatch):
idx = pd.date_range(start="2025-01-02", periods=len(prices), freq="D")
return pd.DataFrame({"Close": prices}, index=idx)
monkeypatch.setattr(y_finance.yf, "Ticker", FakeTicker)
monkeypatch.setattr(yahoo_market.yf, "Ticker", FakeTicker)
# _fetch_returns does not use ``self``; call unbound to avoid building the graph.
raw, alpha, days, resolved = TradingAgentsGraph._fetch_returns(
+38 -35
View File
@@ -14,7 +14,12 @@ import pandas as pd
import pytest
from tradingagents.agents.utils import news_data_tools, prediction_markets_tools
from tradingagents.dataflows import alpha_vantage_news, polymarket, y_finance
from tradingagents.dataflows.vendors import polymarket
from tradingagents.dataflows.vendors.alpha_vantage import news as alpha_vantage_news
from tradingagents.dataflows.vendors.yahoo import (
fundamentals as yahoo_fundamentals,
market as yahoo_market,
)
def _insider_frame(*dates):
@@ -27,8 +32,8 @@ def _insider_frame(*dates):
def _yf_insider(frame, curr_date):
ticker = mock.Mock(insider_transactions=frame)
with mock.patch.object(y_finance.yf, "Ticker", return_value=ticker):
return y_finance.get_insider_transactions("AAPL", curr_date)
with mock.patch.object(yahoo_market.yf, "Ticker", return_value=ticker):
return yahoo_fundamentals.get_insider_transactions("AAPL", curr_date)
@pytest.mark.unit
@@ -121,16 +126,14 @@ def test_insider_rows_are_dated_by_the_trade_not_the_filing():
run must not be told these rows were public on their transaction date."""
import pandas as pd
from tradingagents.dataflows import y_finance
frame = pd.DataFrame({
"Shares": [100, 200],
"Text": ["Sale at price 10.00 per share.", "Sale at price 11.00 per share."],
"Start Date": pd.to_datetime(["2026-05-01", "2026-05-20"]),
})
ticker = mock.Mock(insider_transactions=frame)
with mock.patch.object(y_finance.yf, "Ticker", return_value=ticker):
out = y_finance.get_insider_transactions("AAPL", "2026-05-10")
with mock.patch.object(yahoo_market.yf, "Ticker", return_value=ticker):
out = yahoo_fundamentals.get_insider_transactions("AAPL", "2026-05-10")
assert "2026-05-01" in out and "2026-05-20" not in out # still bounded by the date
assert "transaction date" in out.lower() # and says what the date means
@@ -142,13 +145,12 @@ def test_an_indicator_that_could_not_be_read_is_not_shown_as_a_blank_value():
"""The per-day fallback returned an empty string for a failed read, so the
table rendered a row per day with nothing after the colon: an analyst reads
that as "no value on that day" rather than "could not be obtained"."""
from tradingagents.dataflows import y_finance
from tradingagents.dataflows.errors import VendorError
with mock.patch.object(y_finance.StockstatsUtils, "get_stock_stats",
with mock.patch.object(yahoo_market, "get_stock_stats",
side_effect=RuntimeError("cache parse failed")), \
pytest.raises(VendorError):
y_finance.get_stockstats_indicator("AAPL", "rsi", "2026-05-08")
yahoo_market.get_stockstats_indicator("AAPL", "rsi", "2026-05-08")
@pytest.mark.unit
@@ -165,12 +167,11 @@ def test_a_yfinance_failure_is_a_vendor_error_not_a_report(func, args):
"""Returning the failure as text makes the router count it as an answer, so
the chain stops and the analyst reads the error message as if it were data.
yfinance serves the default path, so this is the one that matters most."""
from tradingagents.dataflows import y_finance
from tradingagents.dataflows.errors import VendorError
with mock.patch.object(y_finance.yf, "Ticker", side_effect=RuntimeError("yahoo hiccup")), \
with mock.patch.object(yahoo_market.yf, "Ticker", side_effect=RuntimeError("yahoo hiccup")), \
pytest.raises(VendorError):
getattr(y_finance, func)(*args)
getattr(yahoo_fundamentals, func)(*args)
@pytest.mark.unit
@@ -179,13 +180,13 @@ def test_a_yfinance_failure_is_a_vendor_error_not_a_report(func, args):
("get_global_news_yfinance", ("2026-09-01", 7, 5)),
])
def test_a_yfinance_news_failure_is_a_vendor_error_not_a_report(func, args):
from tradingagents.dataflows import yfinance_news
from tradingagents.dataflows.errors import VendorError
from tradingagents.dataflows.vendors.yahoo import news as yahoo_news
target = "Ticker" if "global" not in func else "Search"
with mock.patch.object(yfinance_news.yf, target, side_effect=RuntimeError("yahoo hiccup")), \
with mock.patch.object(yahoo_news.yf, target, side_effect=RuntimeError("yahoo hiccup")), \
pytest.raises(VendorError):
getattr(yfinance_news, func)(*args)
getattr(yahoo_news, func)(*args)
@pytest.mark.unit
@@ -195,19 +196,19 @@ 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 stockstats_utils, y_finance
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
from tradingagents.dataflows.vendors.yahoo import ohlcv
empty = mock.Mock(quarterly_balance_sheet=pd.DataFrame(), balance_sheet=pd.DataFrame())
monkeypatch.setattr(y_finance.yf, "Ticker", lambda s: empty)
monkeypatch.setattr(yahoo_market.yf, "Ticker", lambda s: empty)
monkeypatch.setattr(stockstats_utils, "vendor_reachable", lambda url: False)
monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: False)
with pytest.raises(VendorRateLimitError, match="unreachable"):
y_finance.get_balance_sheet("AAPL", "annual", "2026-09-01")
yahoo_fundamentals.get_balance_sheet("AAPL", "annual", "2026-09-01")
monkeypatch.setattr(stockstats_utils, "vendor_reachable", lambda url: True)
monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: True)
with pytest.raises(NoMarketDataError):
y_finance.get_balance_sheet("AAPL", "annual", "2026-09-01")
yahoo_fundamentals.get_balance_sheet("AAPL", "annual", "2026-09-01")
@pytest.mark.unit
@@ -234,27 +235,29 @@ def test_the_price_path_also_tells_an_outage_from_an_unknown_symbol(monkeypatch)
delisted symbol either."""
import pandas as pd
from tradingagents.dataflows import stockstats_utils, y_finance
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
from tradingagents.dataflows.vendors.yahoo import ohlcv
monkeypatch.setattr(y_finance.yf, "Ticker", lambda s: mock.Mock(history=lambda **k: pd.DataFrame()))
monkeypatch.setattr(yahoo_market.yf, "Ticker", lambda s: mock.Mock(history=lambda **k: pd.DataFrame()))
monkeypatch.setattr(stockstats_utils, "vendor_reachable", lambda url: False)
monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: False)
with pytest.raises(VendorRateLimitError, match="unreachable"):
y_finance.get_YFin_data_online("AAPL", "2026-09-01", "2026-09-10")
yahoo_market.get_YFin_data_online("AAPL", "2026-09-01", "2026-09-10")
monkeypatch.setattr(stockstats_utils, "vendor_reachable", lambda url: True)
monkeypatch.setattr(ohlcv, "vendor_reachable", lambda url: True)
with pytest.raises(NoMarketDataError):
y_finance.get_YFin_data_online("AAPL", "2026-09-01", "2026-09-10")
yahoo_market.get_YFin_data_online("AAPL", "2026-09-01", "2026-09-10")
@pytest.mark.unit
@pytest.mark.parametrize("func, args", [
("get_YFin_data_online", ("AAPL", "2025-06-02", "2025-06-06")),
("get_balance_sheet", ("AAPL", "quarterly", "2025-06-06")),
("get_cashflow", ("AAPL", "quarterly", "2025-06-06")),
("get_income_statement", ("AAPL", "quarterly", "2025-06-06")),
("get_insider_transactions", ("AAPL", "2025-06-06")),
pytest.param(f, a, id=f.__name__) for f, a in (
(yahoo_market.get_YFin_data_online, ("AAPL", "2025-06-02", "2025-06-06")),
(yahoo_fundamentals.get_balance_sheet, ("AAPL", "quarterly", "2025-06-06")),
(yahoo_fundamentals.get_cashflow, ("AAPL", "quarterly", "2025-06-06")),
(yahoo_fundamentals.get_income_statement, ("AAPL", "quarterly", "2025-06-06")),
(yahoo_fundamentals.get_insider_transactions, ("AAPL", "2025-06-06")),
)
])
def test_a_historical_run_is_not_told_todays_date(func, args):
"""A header stamped with the wall clock tells a backtest when it is really running."""
@@ -267,8 +270,8 @@ def test_a_historical_run_is_not_told_todays_date(func, args):
quarterly_income_stmt=statement,
insider_transactions=_insider_frame("2025-05-30"),
history=lambda **k: prices)
with mock.patch.object(y_finance.yf, "Ticker", return_value=ticker):
out = getattr(y_finance, func)(*args)
with mock.patch.object(yahoo_market.yf, "Ticker", return_value=ticker):
out = func(*args)
assert date.today().isoformat() not in out
+5 -5
View File
@@ -11,10 +11,6 @@ import pytest
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import router
from tradingagents.dataflows.alpha_vantage_common import (
AlphaVantageNotConfiguredError,
AlphaVantageRateLimitError,
)
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.errors import (
NoMarketDataError,
@@ -22,7 +18,11 @@ from tradingagents.dataflows.errors import (
VendorNotConfiguredError,
VendorRateLimitError,
)
from tradingagents.dataflows.fred import FredNotConfiguredError
from tradingagents.dataflows.vendors.alpha_vantage.common import (
AlphaVantageNotConfiguredError,
AlphaVantageRateLimitError,
)
from tradingagents.dataflows.vendors.fred import FredNotConfiguredError
@pytest.mark.unit
@@ -5,7 +5,7 @@ from __future__ import annotations
import pandas as pd
import pytest
import tradingagents.dataflows.market_data_validator as validator
import tradingagents.dataflows.vendors.yahoo.snapshot as validator
def _sample_ohlcv() -> pd.DataFrame:
+4 -4
View File
@@ -13,12 +13,12 @@ import pandas as pd
import pytest
import tradingagents.dataflows.config as config_module
import tradingagents.dataflows.y_finance as y_finance
import tradingagents.dataflows.vendors.yahoo.market as yahoo_market
import tradingagents.default_config as default_config
from tradingagents.dataflows import router
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.stockstats_utils import _assert_ohlcv_not_stale
from tradingagents.dataflows.vendors.yahoo.ohlcv import _assert_ohlcv_not_stale
def _frame(date):
@@ -76,9 +76,9 @@ class StaleGuardPropagationTests(unittest.TestCase):
def history(self, start, end):
return stale
with mock.patch.object(y_finance.yf, "Ticker", DummyTicker), \
with mock.patch.object(yahoo_market.yf, "Ticker", DummyTicker), \
self.assertRaises(NoMarketDataError):
y_finance.get_YFin_data_online("CB", "2026-06-01", "2026-06-11")
yahoo_market.get_YFin_data_online("CB", "2026-06-01", "2026-06-11")
@pytest.mark.unit