refactor(dataflows): name the shared modules by what they hold

- interface -> router; symbol_utils -> symbols, which also takes safe_ticker_component
- utils is split: get_current_date to date_window, the HTTP helpers to net
- dataflows imports are absolute; the NoMarketDataError re-export from symbols is gone
This commit is contained in:
Yijia-Xiao
2026-09-24 04:31:05 +00:00
parent a58aa613fc
commit c42a2f2c61
47 changed files with 217 additions and 219 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ from cli.utils import (
)
from tradingagents.agents.utils.rating import is_review
from tradingagents.backtest import iter_grid, run_backtest, summarize
from tradingagents.dataflows.utils import safe_ticker_component
from tradingagents.dataflows.symbols import safe_ticker_component
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.graph.analyst_execution import (
AnalystWallTimeTracker,
+1 -1
View File
@@ -71,7 +71,7 @@ def normalize_ticker_symbol(ticker: str) -> str:
plain upper-case if the data layer is unavailable.
"""
try:
from tradingagents.dataflows.symbol_utils import normalize_symbol
from tradingagents.dataflows.symbols import normalize_symbol
return normalize_symbol(ticker)
except Exception:
+1 -1
View File
@@ -78,5 +78,5 @@ ignore = ["E501"]
[tool.ruff.lint.isort]
# Keep multiple aliased names from one module in a single combined import block
# (e.g. the vendor re-exports in interface.py) instead of one statement per name.
# (e.g. the vendor imports in router.py) instead of one statement per name.
combine-as-imports = true
+6 -6
View File
@@ -13,7 +13,7 @@ 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.utils as utils
import tradingagents.dataflows.net as net
class _FakeResponse:
@@ -37,7 +37,7 @@ def _patched_get(body, capture=None):
@pytest.mark.unit
def test_request_passes_timeout(monkeypatch):
captured = {}
monkeypatch.setattr(utils.requests, "get", _patched_get("Date,Close\n2025-01-02,1.0", captured))
monkeypatch.setattr(net.requests, "get", _patched_get("Date,Close\n2025-01-02,1.0", captured))
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
assert captured.get("timeout") == av.REQUEST_TIMEOUT # #990
@@ -45,7 +45,7 @@ def test_request_passes_timeout(monkeypatch):
@pytest.mark.unit
def test_rate_limit_detected(monkeypatch):
body = '{"Information": "Our standard API rate limit is 25 requests per day. ... your API key ..."}'
monkeypatch.setattr(utils.requests, "get", _patched_get(body))
monkeypatch.setattr(net.requests, "get", _patched_get(body))
with pytest.raises(av.AlphaVantageRateLimitError):
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
@@ -56,11 +56,11 @@ def test_invalid_key_not_mislabeled_as_rate_limit(monkeypatch):
# (transient) rate limit, but surface as a real configuration error (#991).
body = ('{"Information": "the parameter apikey is invalid or missing. '
'Please claim your free API key on (https://www.alphavantage.co/support/#api-key)."}')
monkeypatch.setattr(utils.requests, "get", _patched_get(body))
monkeypatch.setattr(net.requests, "get", _patched_get(body))
with pytest.raises(av.AlphaVantageNotConfiguredError):
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
with pytest.raises(av.AlphaVantageRateLimitError): # sanity: rate-limit path still distinct
monkeypatch.setattr(utils.requests, "get", _patched_get('{"Note": "API call frequency is 5 calls per minute."}'))
monkeypatch.setattr(net.requests, "get", _patched_get('{"Note": "API call frequency is 5 calls per minute."}'))
av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"})
@@ -147,7 +147,7 @@ def test_request_error_message_carries_no_key(monkeypatch):
def boom(*a, **k):
raise requests.Timeout(f"Read timed out. url: https://www.alphavantage.co/query?apikey={key}")
monkeypatch.setattr(utils.requests, "get", boom)
monkeypatch.setattr(net.requests, "get", boom)
with pytest.raises(requests.Timeout) as caught:
av._make_api_request("OVERVIEW", {"symbol": "IBM"})
assert key not in str(caught.value)
+1 -1
View File
@@ -7,7 +7,7 @@ import pytest
from cli.models import AssetType
from cli.utils import detect_asset_type, is_valid_ticker_input, normalize_ticker_symbol
from tradingagents.dataflows.symbol_utils import normalize_symbol
from tradingagents.dataflows.symbols import normalize_symbol
# --- #982: stablecoin-quoted crypto normalizes to Yahoo's -USD pair ---
+4 -4
View File
@@ -73,7 +73,7 @@ def _graph(config):
def _vendors_seen_by_a_run(graph, ticker="AAPL"):
from tradingagents.dataflows.interface import get_vendor
from tradingagents.dataflows.router import get_vendor
seen = []
@@ -121,7 +121,7 @@ def test_concurrent_runs_each_read_their_own_config():
config = copy.deepcopy(default_config.DEFAULT_CONFIG)
config["tool_vendors"] = {"get_balance_sheet": vendor}
graph = _graph(config)
from tradingagents.dataflows.interface import get_vendor
from tradingagents.dataflows.router import get_vendor
def _run(*a, **k):
barrier.wait(timeout=5) # both runs are in flight
@@ -141,7 +141,7 @@ def test_concurrent_runs_each_read_their_own_config():
@pytest.mark.unit
def test_settling_reads_the_graphs_own_config():
from tradingagents.dataflows.interface import get_vendor
from tradingagents.dataflows.router import get_vendor
config = copy.deepcopy(default_config.DEFAULT_CONFIG)
config["tool_vendors"] = {"get_stock_data": "alpha_vantage"}
@@ -164,7 +164,7 @@ def test_tools_inside_a_langgraph_run_see_the_run_config():
from langgraph.prebuilt import ToolNode
from tradingagents.dataflows.config import run_config
from tradingagents.dataflows.interface import get_vendor
from tradingagents.dataflows.router import get_vendor
@tool
def probe() -> str:
+9 -9
View File
@@ -12,7 +12,7 @@ import requests
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import fred, interface
from tradingagents.dataflows import fred, router
from tradingagents.dataflows.config import set_config
# A small, stable set of observations to format against.
@@ -203,15 +203,15 @@ class FredRoutingTests(unittest.TestCase):
def test_macro_category_routes_to_fred(self):
self.assertEqual(
interface.get_category_for_method("get_macro_indicators"), "macro_data"
router.get_category_for_method("get_macro_indicators"), "macro_data"
)
set_config({"data_vendors": {"macro_data": "fred"}})
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_macro_indicators": {"fred": lambda *a, **k: "MACRO_OK"}},
clear=False,
):
out = interface.route_to_vendor("get_macro_indicators", "cpi", "2026-06-01", 365)
out = router.route_to_vendor("get_macro_indicators", "cpi", "2026-06-01", 365)
self.assertEqual(out, "MACRO_OK")
def test_not_configured_degrades_gracefully(self):
@@ -224,11 +224,11 @@ class FredRoutingTests(unittest.TestCase):
raise fred.FredNotConfiguredError("FRED_API_KEY not set")
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_macro_indicators": {"fred": _unconfigured}},
clear=False,
):
out = interface.route_to_vendor("get_macro_indicators", "cpi", "2026-06-01", 365)
out = router.route_to_vendor("get_macro_indicators", "cpi", "2026-06-01", 365)
self.assertIn("DATA_UNAVAILABLE", out)
@@ -246,7 +246,7 @@ class TestKeyKeptOutOfErrors:
def _raises(self, side_effect):
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
mock.patch("tradingagents.dataflows.utils.requests.get", side_effect=side_effect), \
mock.patch("tradingagents.dataflows.net.requests.get", side_effect=side_effect), \
pytest.raises(requests.RequestException) as caught:
fred._request("series", {"series_id": "DGS10"})
return caught.value
@@ -258,7 +258,7 @@ class TestKeyKeptOutOfErrors:
response=response,
)
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
mock.patch("tradingagents.dataflows.utils.requests.get", return_value=response), \
mock.patch("tradingagents.dataflows.net.requests.get", return_value=response), \
pytest.raises(requests.HTTPError) as caught:
fred._request("series", {"series_id": "DGS10"})
exc = caught.value
@@ -280,7 +280,7 @@ def test_error_without_the_key_in_its_message_still_drops_the_request():
import requests as rq
req = rq.Request("GET", f"https://api.stlouisfed.org/fred/series?api_key={_KEY}").prepare()
with mock.patch.dict("os.environ", {"FRED_API_KEY": _KEY}), \
mock.patch("tradingagents.dataflows.utils.requests.get", side_effect=rq.Timeout("Read timed out.", request=req)), \
mock.patch("tradingagents.dataflows.net.requests.get", side_effect=rq.Timeout("Read timed out.", request=req)), \
pytest.raises(rq.Timeout) as caught:
fred._request("series", {"series_id": "DGS10"})
assert caught.value.request is None
+1 -1
View File
@@ -123,7 +123,7 @@ class TestNoUsableFieldsStillRaises:
def test_stub_payload_raises_no_market_data(self):
# yfinance returns {"trailingPegRatio": None} for unknown symbols; on a
# live run that must stay a hard "no data", not a bare header.
from tradingagents.dataflows.symbol_utils import NoMarketDataError
from tradingagents.dataflows.errors import NoMarketDataError
with pytest.raises(NoMarketDataError):
_yf(_TODAY, info={"trailingPegRatio": None})
+2 -2
View File
@@ -20,7 +20,7 @@ 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 interface, market_data_validator, y_finance
from tradingagents.dataflows import market_data_validator, router, y_finance
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.graph import trading_graph
@@ -94,7 +94,7 @@ class _Client:
def offline(monkeypatch, tmp_path):
"""Every vendor answers offline; returns the set of router methods called."""
called: set[str] = set()
for method, vendors in interface.VENDOR_METHODS.items():
for method, vendors in router.VENDOR_METHODS.items():
for vendor in vendors:
monkeypatch.setitem(vendors, vendor,
lambda *a, _m=method, **k: called.add(_m) or f"{_m} data")
+6 -6
View File
@@ -14,9 +14,9 @@ from unittest import mock
import pandas as pd
import pytest
from tradingagents.dataflows import interface, stockstats_utils
from tradingagents.dataflows import router, stockstats_utils
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.symbol_utils import NoMarketDataError
from tradingagents.dataflows.errors import NoMarketDataError
@pytest.mark.unit
@@ -54,9 +54,9 @@ class TestRouteToVendorSentinel(unittest.TestCase):
patched = {"yfinance": raises_no_data, "alpha_vantage": raises_no_data}
with mock.patch.dict(
interface.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
router.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
):
result = interface.route_to_vendor(
result = router.route_to_vendor(
"get_stock_data", "XAUUSD+", "2026-01-01", "2026-01-10"
)
self.assertIn("NO_DATA_AVAILABLE", result)
@@ -76,9 +76,9 @@ class TestRouteToVendorSentinel(unittest.TestCase):
patched = {"yfinance": raises_no_data, "alpha_vantage": raises_unavailable}
with mock.patch.dict(
interface.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
router.VENDOR_METHODS, {"get_stock_data": patched}, clear=False
):
result = interface.route_to_vendor(
result = router.route_to_vendor(
"get_stock_data", "FAKE", "2026-01-01", "2026-01-10"
)
self.assertIn("NO_DATA_AVAILABLE", result)
+1 -1
View File
@@ -19,7 +19,7 @@ import pandas as pd
import pytest
from tradingagents.dataflows import stockstats_utils as su
from tradingagents.dataflows.symbol_utils import NoMarketDataError
from tradingagents.dataflows.errors import NoMarketDataError
def _stamp(path, ts):
+4 -4
View File
@@ -12,7 +12,7 @@ import requests
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import interface, polymarket
from tradingagents.dataflows import polymarket, router
from tradingagents.dataflows.config import set_config
@@ -112,16 +112,16 @@ class PolymarketRoutingTests(unittest.TestCase):
def test_category_routes_to_polymarket(self):
self.assertEqual(
interface.get_category_for_method("get_prediction_markets"),
router.get_category_for_method("get_prediction_markets"),
"prediction_markets",
)
set_config({"data_vendors": {"prediction_markets": "polymarket"}})
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_prediction_markets": {"polymarket": lambda *a, **k: "POLY_OK"}},
clear=False,
):
out = interface.route_to_vendor("get_prediction_markets", "fed", 5)
out = router.route_to_vendor("get_prediction_markets", "fed", 5)
self.assertEqual(out, "POLY_OK")
+1 -1
View File
@@ -5,7 +5,7 @@ import unittest
import pytest
from tradingagents.dataflows.utils import safe_ticker_component
from tradingagents.dataflows.symbols import safe_ticker_component
@pytest.mark.unit
@@ -4,11 +4,8 @@ import unittest
import pytest
from tradingagents.dataflows.symbol_utils import (
NoMarketDataError,
crypto_base,
normalize_symbol,
)
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.symbols import crypto_base, normalize_symbol
@pytest.mark.unit
+9 -6
View File
@@ -106,7 +106,7 @@ def test_a_historical_run_is_told_the_identity_is_current(monkeypatch):
@pytest.mark.unit
def test_a_current_run_is_not_cluttered_with_a_vintage_note(monkeypatch):
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.dataflows.utils import get_current_date
from tradingagents.dataflows.date_window import get_current_date
today = build_instrument_context("EXMP", "stock", {"company_name": "Example Corp"},
curr_date=get_current_date())
@@ -214,15 +214,15 @@ def test_an_unreachable_vendor_is_not_reported_as_a_missing_symbol(monkeypatch):
def test_every_vendor_unavailable_says_so_rather_than_crashing(monkeypatch):
"""A throttled or unreachable chain used to raise RuntimeError('No available
vendor'), which ends the run, and never said the vendor was the problem."""
from tradingagents.dataflows import interface
from tradingagents.dataflows import router
from tradingagents.dataflows.errors import VendorRateLimitError
def _down(*a, **k):
raise VendorRateLimitError("Yahoo Finance is unreachable")
monkeypatch.setitem(interface.VENDOR_METHODS["get_balance_sheet"], "yfinance", _down)
monkeypatch.setitem(router.VENDOR_METHODS["get_balance_sheet"], "yfinance", _down)
out = interface.route_to_vendor("get_balance_sheet", "AAPL", "annual", "2026-09-01")
out = router.route_to_vendor("get_balance_sheet", "AAPL", "annual", "2026-09-01")
assert "unavailable" in out.lower() and "unreachable" in out.lower()
assert "delisted" not in out.lower() # not a claim about the symbol
@@ -283,8 +283,11 @@ def test_an_unavailable_notice_names_no_date_after_the_run():
"""A notice explaining why data is missing named where the vendor's coverage
starts or today's date, both after a historical run's date."""
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.dataflows.date_window import coverage_gap, withhold_live_profile
from tradingagents.dataflows.utils import get_current_date
from tradingagents.dataflows.date_window import (
coverage_gap,
get_current_date,
withhold_live_profile,
)
today = get_current_date()
notices = [
+7 -13
View File
@@ -10,7 +10,7 @@ import pytest
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import interface
from tradingagents.dataflows import router
from tradingagents.dataflows.alpha_vantage_common import (
AlphaVantageNotConfiguredError,
AlphaVantageRateLimitError,
@@ -42,12 +42,6 @@ class HierarchyTests(unittest.TestCase):
# ... and therefore still ValueErrors
self.assertTrue(issubclass(FredNotConfiguredError, ValueError))
def test_symbol_utils_reexports_no_market_data_error(self):
from tradingagents.dataflows.symbol_utils import (
NoMarketDataError as ReExported,
)
self.assertIs(ReExported, NoMarketDataError)
@pytest.mark.unit
class RouterHandlesBaseTypesTests(unittest.TestCase):
@@ -65,11 +59,11 @@ class RouterHandlesBaseTypesTests(unittest.TestCase):
raise AlphaVantageRateLimitError("slow down")
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": {"alpha_vantage": _throttled, "yfinance": lambda *a, **k: "YF"}},
clear=False,
):
out = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
out = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertEqual(out, "YF")
def test_not_configured_falls_through_to_next_vendor(self):
@@ -79,11 +73,11 @@ class RouterHandlesBaseTypesTests(unittest.TestCase):
raise AlphaVantageNotConfiguredError("no key")
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": {"alpha_vantage": _unconfigured, "yfinance": lambda *a, **k: "YF"}},
clear=False,
):
out = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
out = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertEqual(out, "YF")
def test_sole_unconfigured_vendor_surfaces_the_error(self):
@@ -94,11 +88,11 @@ class RouterHandlesBaseTypesTests(unittest.TestCase):
raise AlphaVantageNotConfiguredError("no key")
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": {"alpha_vantage": _unconfigured}},
clear=False,
), self.assertRaises(AlphaVantageNotConfiguredError):
interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
if __name__ == "__main__":
+12 -12
View File
@@ -13,9 +13,9 @@ import pytest
import tradingagents.dataflows.config as config_module
import tradingagents.default_config as default_config
from tradingagents.dataflows import interface
from tradingagents.dataflows import router
from tradingagents.dataflows.config import set_config
from tradingagents.dataflows.symbol_utils import NoMarketDataError
from tradingagents.dataflows.errors import NoMarketDataError
def _reset_config():
@@ -50,7 +50,7 @@ class VendorRoutingTests(unittest.TestCase):
def _route(self, vendors_for_get_stock_data):
return mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": vendors_for_get_stock_data},
clear=False,
)
@@ -60,7 +60,7 @@ class VendorRoutingTests(unittest.TestCase):
set_config({"data_vendors": {"core_stock_apis": "yfinance"}})
av = mock.Mock(side_effect=_returns("AV_DATA"))
with self._route({"yfinance": _no_data, "alpha_vantage": av}):
result = interface.route_to_vendor("get_stock_data", "FAKE", "2026-01-01", "2026-01-10")
result = router.route_to_vendor("get_stock_data", "FAKE", "2026-01-01", "2026-01-10")
self.assertIn("NO_DATA_AVAILABLE", result)
av.assert_not_called() # the unchosen vendor was never tried
@@ -68,7 +68,7 @@ class VendorRoutingTests(unittest.TestCase):
# Listing both vendors opts in to ordered fallback.
set_config({"data_vendors": {"core_stock_apis": "yfinance,alpha_vantage"}})
with self._route({"yfinance": _no_data, "alpha_vantage": _returns("AV_DATA")}):
result = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
result = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertEqual(result, "AV_DATA")
def test_primary_error_is_logged_not_masked(self):
@@ -76,8 +76,8 @@ class VendorRoutingTests(unittest.TestCase):
# must be visible in logs (broken primary not hidden).
set_config({"data_vendors": {"core_stock_apis": "yfinance,alpha_vantage"}})
with self._route({"yfinance": _raises(ValueError("boom")), "alpha_vantage": _no_data}), \
self.assertLogs("tradingagents.dataflows.interface", level="WARNING") as cm:
result = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertLogs("tradingagents.dataflows.router", level="WARNING") as cm:
result = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertIn("NO_DATA_AVAILABLE", result)
joined = "\n".join(cm.output)
self.assertIn("boom", joined) # the real error surfaced in logs
@@ -86,18 +86,18 @@ class VendorRoutingTests(unittest.TestCase):
def test_unknown_configured_vendor_raises(self):
set_config({"data_vendors": {"core_stock_apis": "bogus_vendor"}})
with self.assertRaises(ValueError) as ctx:
interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertIn("bogus_vendor", str(ctx.exception))
def test_default_sentinel_uses_all_vendors(self):
# No explicit choice ("default") keeps the resilient full-chain behavior.
set_config({"data_vendors": {"core_stock_apis": "default"}})
with self._route({"yfinance": _no_data, "alpha_vantage": _returns("AV_DATA")}):
result = interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
result = router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
self.assertEqual(result, "AV_DATA")
def _route_method(self, method, vendors):
return mock.patch.dict(interface.VENDOR_METHODS, {method: vendors}, clear=False)
return mock.patch.dict(router.VENDOR_METHODS, {method: vendors}, clear=False)
def test_optional_category_degrades_instead_of_raising(self):
# An optional enrichment vendor (FRED macro) that raises must NOT abort
@@ -106,7 +106,7 @@ class VendorRoutingTests(unittest.TestCase):
with self._route_method(
"get_macro_indicators", {"fred": _raises(ValueError("FRED 400: bad series"))}
):
result = interface.route_to_vendor("get_macro_indicators", "cpi", "2026-01-01")
result = router.route_to_vendor("get_macro_indicators", "cpi", "2026-01-01")
self.assertIn("DATA_UNAVAILABLE", result)
self.assertIn("macro_data", result)
@@ -116,7 +116,7 @@ class VendorRoutingTests(unittest.TestCase):
set_config({"data_vendors": {"core_stock_apis": "yfinance"}})
with self._route({"yfinance": _raises(ValueError("boom"))}), \
self.assertRaises(ValueError):
interface.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
router.route_to_vendor("get_stock_data", "AAPL", "2026-01-01", "2026-01-10")
if __name__ == "__main__":
+4 -4
View File
@@ -15,10 +15,10 @@ import pytest
import tradingagents.dataflows.config as config_module
import tradingagents.dataflows.y_finance as y_finance
import tradingagents.default_config as default_config
from tradingagents.dataflows import interface
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.symbol_utils import NoMarketDataError
def _frame(date):
@@ -98,11 +98,11 @@ class StaleGuardRoutingTests(unittest.TestCase):
)
with mock.patch.dict(
interface.VENDOR_METHODS,
router.VENDOR_METHODS,
{"get_stock_data": {"yfinance": _stale}},
clear=False,
):
out = interface.route_to_vendor(
out = router.route_to_vendor(
"get_stock_data", "CB", "2026-06-01", "2026-06-11"
)
self.assertIn("NO_DATA_AVAILABLE", out)
+1 -2
View File
@@ -22,6 +22,7 @@ from tradingagents.agents.utils.news_data_tools import (
)
from tradingagents.agents.utils.prediction_markets_tools import get_prediction_markets
from tradingagents.agents.utils.technical_indicators_tools import get_indicators
from tradingagents.dataflows.date_window import get_current_date
from tradingagents.dataflows.y_finance import get_company_profile
# Public surface: the data tools are imported here so agents and the graph
@@ -48,8 +49,6 @@ __all__ = [
logger = logging.getLogger(__name__)
from tradingagents.dataflows.utils import get_current_date # noqa: E402
def get_language_instruction() -> str:
"""Return a prompt instruction for the configured output language.
@@ -4,7 +4,7 @@ from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
from tradingagents.dataflows.date_window import as_of_window
from tradingagents.dataflows.interface import route_to_vendor
from tradingagents.dataflows.router import route_to_vendor
@tool
@@ -4,7 +4,7 @@ from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
from tradingagents.dataflows.date_window import as_of
from tradingagents.dataflows.interface import route_to_vendor
from tradingagents.dataflows.router import route_to_vendor
@tool
@@ -4,7 +4,7 @@ from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
from tradingagents.dataflows.date_window import as_of
from tradingagents.dataflows.interface import route_to_vendor
from tradingagents.dataflows.router import route_to_vendor
@tool
@@ -4,7 +4,7 @@ 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.interface import route_to_vendor
from tradingagents.dataflows.router import route_to_vendor
@tool
@@ -3,7 +3,7 @@ from typing import Annotated
from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
from tradingagents.dataflows.interface import route_to_vendor
from tradingagents.dataflows.router import route_to_vendor
@tool
@@ -4,7 +4,7 @@ from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
from tradingagents.dataflows.date_window import as_of
from tradingagents.dataflows.interface import route_to_vendor
from tradingagents.dataflows.router import route_to_vendor
@tool
+2 -1
View File
@@ -23,7 +23,8 @@ from pathlib import Path
from tradingagents.agents.utils.memory import TradingMemoryLog
from tradingagents.agents.utils.rating import RATING_REVIEW
from tradingagents.dataflows.utils import get_current_date, safe_ticker_component
from tradingagents.dataflows.date_window import get_current_date
from tradingagents.dataflows.symbols import safe_ticker_component
from tradingagents.graph.trading_graph import TradingAgentsGraph
logger = logging.getLogger(__name__)
+8 -4
View File
@@ -1,14 +1,18 @@
# Aggregates the per-category Alpha Vantage implementations into one module the
# vendor router imports from; the imports below are the public surface.
from .alpha_vantage_fundamentals import (
from tradingagents.dataflows.alpha_vantage_fundamentals import (
get_balance_sheet,
get_cashflow,
get_fundamentals,
get_income_statement,
)
from .alpha_vantage_indicator import get_indicator
from .alpha_vantage_news import get_global_news, get_insider_transactions, get_news
from .alpha_vantage_stock import get_stock
from tradingagents.dataflows.alpha_vantage_indicator import get_indicator
from tradingagents.dataflows.alpha_vantage_news import (
get_global_news,
get_insider_transactions,
get_news,
)
from tradingagents.dataflows.alpha_vantage_stock import get_stock
__all__ = [
"get_balance_sheet",
@@ -5,8 +5,8 @@ from io import StringIO
import pandas as pd
from .errors import VendorNotConfiguredError, VendorRateLimitError
from .utils import get_scrubbed
from tradingagents.dataflows.errors import VendorNotConfiguredError, VendorRateLimitError
from tradingagents.dataflows.net import get_scrubbed
API_BASE_URL = "https://www.alphavantage.co/query"
@@ -1,7 +1,7 @@
import json
from .alpha_vantage_common import _make_api_request
from .date_window import withhold_live_profile
from tradingagents.dataflows.alpha_vantage_common import _make_api_request
from tradingagents.dataflows.date_window import withhold_live_profile
def _filter_reports_by_date(result, curr_date: str):
@@ -1,7 +1,7 @@
import logging
from .alpha_vantage_common import _make_api_request
from .errors import NoMarketDataError, VendorError
from tradingagents.dataflows.alpha_vantage_common import _make_api_request
from tradingagents.dataflows.errors import NoMarketDataError, VendorError
logger = logging.getLogger(__name__)
@@ -1,7 +1,7 @@
import json
from .alpha_vantage_common import _make_api_request, format_datetime_for_api
from .config import get_config
from tradingagents.dataflows.alpha_vantage_common import _make_api_request, format_datetime_for_api
from tradingagents.dataflows.config import get_config
def get_news(ticker, start_date, end_date) -> dict[str, str] | str:
@@ -1,6 +1,9 @@
from datetime import datetime
from .alpha_vantage_common import _filter_csv_by_date_range, _make_api_request
from tradingagents.dataflows.alpha_vantage_common import (
_filter_csv_by_date_range,
_make_api_request,
)
def get_stock(
+6 -3
View File
@@ -11,9 +11,7 @@ in a backtest we can't prove it isn't future.
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from .utils import get_current_date
from datetime import date, datetime, timedelta, timezone
def to_utc(dt: datetime) -> datetime:
@@ -32,6 +30,11 @@ def in_window(pub_dt: datetime | None, start_dt: datetime, end_dt: datetime) ->
return end >= datetime.now(timezone.utc) - timedelta(days=1)
def get_current_date() -> str:
"""Today's date, YYYY-MM-DD."""
return date.today().strftime("%Y-%m-%d")
def coverage_gap(
dates, start_date: str, end_date: str, source: str, subject: str
) -> str | None:
+2 -2
View File
@@ -14,8 +14,8 @@ from datetime import datetime, timedelta
import pytz
from .errors import VendorNotConfiguredError
from .utils import get_scrubbed
from tradingagents.dataflows.errors import VendorNotConfiguredError
from tradingagents.dataflows.net import get_scrubbed
logger = logging.getLogger(__name__)
+37
View File
@@ -0,0 +1,37 @@
"""HTTP helpers shared by the vendors."""
import requests
def get_scrubbed(url: str, *, params: dict, timeout: float, secret: str, passthrough=()):
"""``requests.get`` plus ``raise_for_status``, with ``secret`` kept out of errors.
Vendors that authenticate with a query parameter put the key in the URL, and
requests quotes the full URL in HTTP, connection and timeout errors, so any
log or traceback that records one would carry the key (#1324). A requests
error is re-raised as the same class with the key replaced and nothing
attached: no request or response (both hold the URL) and no exception chain,
which is why this raises after the ``except`` block rather than inside it.
Statuses in ``passthrough`` are returned for the caller to handle.
"""
try:
response = requests.get(url, params=params, timeout=timeout)
if response.status_code not in passthrough:
response.raise_for_status()
return response
except requests.RequestException as exc:
error = type(exc)(str(exc).replace(secret, "***")) if secret else exc
raise error
def vendor_reachable(url: str, timeout: float = 5.0) -> bool:
"""Whether the vendor answers at all, for telling silence from an outage.
A client that returns an empty result instead of raising leaves those two
cases indistinguishable. Called only when a result is empty.
"""
try:
requests.head(url, timeout=timeout, allow_redirects=True)
return True
except requests.RequestException:
return False
+1 -1
View File
@@ -15,7 +15,7 @@ from datetime import datetime, timezone
import requests
from .utils import get_current_date
from tradingagents.dataflows.date_window import get_current_date
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -29,8 +29,8 @@ from urllib.error import HTTPError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from .date_window import coverage_gap, in_window
from .symbol_utils import crypto_base
from tradingagents.dataflows.date_window import coverage_gap, in_window
from tradingagents.dataflows.symbols import crypto_base
logger = logging.getLogger(__name__)
@@ -1,6 +1,6 @@
import logging
from .alpha_vantage import (
from tradingagents.dataflows.alpha_vantage import (
get_balance_sheet as get_alpha_vantage_balance_sheet,
get_cashflow as get_alpha_vantage_cashflow,
get_fundamentals as get_alpha_vantage_fundamentals,
@@ -11,20 +11,22 @@ from .alpha_vantage import (
get_news as get_alpha_vantage_news,
get_stock as get_alpha_vantage_stock,
)
from .config import get_config
from .errors import (
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.errors import (
NoMarketDataError,
VendorNotConfiguredError,
VendorRateLimitError,
)
from .fred import get_macro_data as get_fred_macro_data
from .polymarket import get_prediction_markets as get_polymarket_prediction_markets
from .sec_edgar import (
from tradingagents.dataflows.fred import get_macro_data as get_fred_macro_data
from tradingagents.dataflows.polymarket import (
get_prediction_markets as get_polymarket_prediction_markets,
)
from tradingagents.dataflows.sec_edgar import (
get_balance_sheet as get_sec_edgar_balance_sheet,
get_cashflow as get_sec_edgar_cashflow,
get_income_statement as get_sec_edgar_income_statement,
)
from .y_finance import (
from tradingagents.dataflows.y_finance import (
get_balance_sheet as get_yfinance_balance_sheet,
get_cashflow as get_yfinance_cashflow,
get_fundamentals as get_yfinance_fundamentals,
@@ -33,7 +35,7 @@ from .y_finance import (
get_stock_stats_indicators_window,
get_YFin_data_online,
)
from .yfinance_news import get_global_news_yfinance, get_news_yfinance
from tradingagents.dataflows.yfinance_news import get_global_news_yfinance, get_news_yfinance
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -27,8 +27,8 @@ from pathlib import Path
import requests
from .config import get_config
from .errors import NoMarketDataError, VendorRateLimitError
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
logger = logging.getLogger(__name__)
+4 -4
View File
@@ -8,10 +8,10 @@ import yfinance as yf
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, vendor_reachable
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.errors import NoMarketDataError, VendorRateLimitError
from tradingagents.dataflows.net import vendor_reachable
from tradingagents.dataflows.symbols import normalize_symbol, safe_ticker_component
logger = logging.getLogger(__name__)
+2 -2
View File
@@ -21,8 +21,8 @@ import logging
from datetime import datetime
from urllib.request import Request, urlopen
from .date_window import coverage_gap, in_window
from .symbol_utils import crypto_base
from tradingagents.dataflows.date_window import coverage_gap, in_window
from tradingagents.dataflows.symbols import crypto_base
logger = logging.getLogger(__name__)
@@ -1,4 +1,4 @@
"""Symbol normalization and market-data error types for vendor calls.
"""Symbol normalization for vendor calls, and ticker values safe to use in a path.
Yahoo Finance (the default vendor) uses specific ticker conventions that
differ from the broker / TradingView / MT5 style symbols users often type:
@@ -25,10 +25,6 @@ from __future__ import annotations
import logging
import re
# NoMarketDataError lives in the vendor-error taxonomy (errors.py); re-exported
# here for the many call sites that import it alongside normalize_symbol.
from .errors import NoMarketDataError as NoMarketDataError
logger = logging.getLogger(__name__)
@@ -147,3 +143,38 @@ def normalize_symbol(raw: str) -> str:
logger.info("Resolved symbol %r to Yahoo symbol %r", raw, canonical)
return canonical
# Tickers can contain letters, digits, dot, dash, underscore, caret
# (index symbols like ^GSPC), equals (futures like GC=F), and plus
# (forex/CFD symbols like XAUUSD+). None of these enable directory
# traversal, so the value never escapes a containing directory when
# interpolated into a path. Anything else is rejected.
_TICKER_PATH_RE = re.compile(r"^[A-Za-z0-9._\-\^=+]+$")
def safe_ticker_component(value: str, *, max_len: int = 32) -> str:
"""Validate ``value`` is safe to interpolate into a filesystem path.
Tickers come from user CLI input or from LLM tool calls, both of which
can be influenced by attacker-controlled content (e.g. prompt injection
embedded in fetched news). Without validation, a value like
``"../../../etc/foo"`` flows into ``os.path.join`` / ``Path /`` and
escapes the configured cache, checkpoint, or results directory.
Returns ``value`` unchanged when it matches the allowed pattern; raises
``ValueError`` otherwise.
"""
if not isinstance(value, str) or not value:
raise ValueError(f"ticker must be a non-empty string, got {value!r}")
if len(value) > max_len:
raise ValueError(f"ticker exceeds {max_len} chars: {value!r}")
if not _TICKER_PATH_RE.fullmatch(value):
raise ValueError(
f"ticker contains characters not allowed in a filesystem path: {value!r}"
)
# The regex above allows '.', so values like '.', '..', '...' would pass,
# and as a path component they traverse the parent directory. Reject any
# value that's only dots.
if set(value) == {"."}:
raise ValueError(f"ticker cannot consist solely of dots: {value!r}")
return value
-77
View File
@@ -1,77 +0,0 @@
import re
from datetime import date
import requests
# Tickers can contain letters, digits, dot, dash, underscore, caret
# (index symbols like ^GSPC), equals (futures like GC=F), and plus
# (forex/CFD symbols like XAUUSD+). None of these enable directory
# traversal, so the value never escapes a containing directory when
# interpolated into a path. Anything else is rejected.
_TICKER_PATH_RE = re.compile(r"^[A-Za-z0-9._\-\^=+]+$")
def safe_ticker_component(value: str, *, max_len: int = 32) -> str:
"""Validate ``value`` is safe to interpolate into a filesystem path.
Tickers come from user CLI input or from LLM tool calls, both of which
can be influenced by attacker-controlled content (e.g. prompt injection
embedded in fetched news). Without validation, a value like
``"../../../etc/foo"`` flows into ``os.path.join`` / ``Path /`` and
escapes the configured cache, checkpoint, or results directory.
Returns ``value`` unchanged when it matches the allowed pattern; raises
``ValueError`` otherwise.
"""
if not isinstance(value, str) or not value:
raise ValueError(f"ticker must be a non-empty string, got {value!r}")
if len(value) > max_len:
raise ValueError(f"ticker exceeds {max_len} chars: {value!r}")
if not _TICKER_PATH_RE.fullmatch(value):
raise ValueError(
f"ticker contains characters not allowed in a filesystem path: {value!r}"
)
# The regex above allows '.', so values like '.', '..', '...' would pass,
# and as a path component they traverse the parent directory. Reject any
# value that's only dots.
if set(value) == {"."}:
raise ValueError(f"ticker cannot consist solely of dots: {value!r}")
return value
def get_current_date():
return date.today().strftime("%Y-%m-%d")
def get_scrubbed(url: str, *, params: dict, timeout: float, secret: str, passthrough=()):
"""``requests.get`` plus ``raise_for_status``, with ``secret`` kept out of errors.
Vendors that authenticate with a query parameter put the key in the URL, and
requests quotes the full URL in HTTP, connection and timeout errors, so any
log or traceback that records one would carry the key (#1324). A requests
error is re-raised as the same class with the key replaced and nothing
attached: no request or response (both hold the URL) and no exception chain,
which is why this raises after the ``except`` block rather than inside it.
Statuses in ``passthrough`` are returned for the caller to handle.
"""
try:
response = requests.get(url, params=params, timeout=timeout)
if response.status_code not in passthrough:
response.raise_for_status()
return response
except requests.RequestException as exc:
error = type(exc)(str(exc).replace(secret, "***")) if secret else exc
raise error
def vendor_reachable(url: str, timeout: float = 5.0) -> bool:
"""Whether the vendor answers at all, for telling silence from an outage.
A client that returns an empty result instead of raising leaves those two
cases indistinguishable. Called only when a result is empty.
"""
try:
requests.head(url, timeout=timeout, allow_redirects=True)
return True
except requests.RequestException:
return False
+5 -5
View File
@@ -6,9 +6,10 @@ import pandas as pd
import yfinance as yf
from dateutil.relativedelta import relativedelta
from .date_window import withhold_live_profile
from .errors import VendorError, VendorRateLimitError
from .stockstats_utils import (
from tradingagents.dataflows.date_window import withhold_live_profile
from tradingagents.dataflows.errors import NoMarketDataError, VendorError, VendorRateLimitError
from tradingagents.dataflows.net import vendor_reachable
from tradingagents.dataflows.stockstats_utils import (
StockstatsUtils,
_assert_ohlcv_not_stale,
filter_financials_by_date,
@@ -16,8 +17,7 @@ from .stockstats_utils import (
raise_for_empty,
yf_retry,
)
from .symbol_utils import NoMarketDataError, normalize_symbol
from .utils import vendor_reachable
from tradingagents.dataflows.symbols import normalize_symbol
_YAHOO_HOST = "https://query2.finance.yahoo.com"
+5 -5
View File
@@ -6,11 +6,11 @@ from datetime import datetime, timezone
import yfinance as yf
from dateutil.relativedelta import relativedelta
from .config import get_config
from .date_window import coverage_gap, in_window
from .errors import NoMarketDataError
from .stockstats_utils import yf_retry
from .symbol_utils import normalize_symbol
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.date_window import coverage_gap, in_window
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.stockstats_utils import yf_retry
from tradingagents.dataflows.symbols import normalize_symbol
def _extract_article_data(article: dict) -> dict:
+1 -1
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from langgraph.checkpoint.sqlite import SqliteSaver
from tradingagents.dataflows.utils import safe_ticker_component
from tradingagents.dataflows.symbols import safe_ticker_component
def _db_path(data_dir: str | Path, ticker: str) -> Path:
+3 -2
View File
@@ -15,7 +15,8 @@ from tradingagents.agents.utils.agent_utils import (
from tradingagents.agents.utils.memory import TradingMemoryLog
from tradingagents.agents.utils.rating import parse_rating
from tradingagents.dataflows.config import run_config, set_config
from tradingagents.dataflows.utils import get_current_date, safe_ticker_component
from tradingagents.dataflows.date_window import get_current_date
from tradingagents.dataflows.symbols import safe_ticker_component
from tradingagents.dataflows.y_finance import get_closes
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.llm_clients import create_llm_client
@@ -207,7 +208,7 @@ class TradingAgentsGraph:
entry, which is the right default because the alpha calculation works
in USD.
"""
from tradingagents.dataflows.symbol_utils import normalize_symbol
from tradingagents.dataflows.symbols import normalize_symbol
explicit = self.config.get("benchmark_ticker")
if explicit: