mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-25 14:02:38 +03:00
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:
+1
-1
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user