mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-25 05:52:35 +03:00
refactor(dataflows): read company identity and settlement prices through the data layer
- the Yahoo vendor owns get_company_profile and get_closes; agent_utils and the graph no longer import yfinance - a test keeps vendor libraries inside dataflows
This commit is contained in:
@@ -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
|
||||
from tradingagents.dataflows import interface, market_data_validator, y_finance
|
||||
from tradingagents.default_config import DEFAULT_CONFIG
|
||||
from tradingagents.graph import trading_graph
|
||||
|
||||
@@ -106,7 +106,7 @@ def offline(monkeypatch, tmp_path):
|
||||
lambda *a, **k: called.add("ohlcv") or prices.copy())
|
||||
monkeypatch.setattr(sentiment_analyst, "fetch_stocktwits_messages", lambda *a, **k: "no posts")
|
||||
monkeypatch.setattr(sentiment_analyst, "fetch_reddit_posts", lambda *a, **k: "no posts")
|
||||
monkeypatch.setattr(agent_utils.yf, "Ticker", lambda s: type("T", (), {"info": {"longName": "NVIDIA"}})())
|
||||
monkeypatch.setattr(y_finance.yf, "Ticker", lambda s: type("T", (), {"info": {"longName": "NVIDIA"}})())
|
||||
agent_utils.resolve_instrument_identity.cache_clear()
|
||||
return called
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class ResolveInstrumentIdentityTests(unittest.TestCase):
|
||||
resolve_instrument_identity.cache_clear()
|
||||
|
||||
def test_resolves_company_metadata_from_yfinance(self):
|
||||
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
|
||||
mock.return_value.info = {
|
||||
"longName": "TOTO LTD.",
|
||||
"shortName": "TOTO",
|
||||
@@ -38,26 +38,26 @@ class ResolveInstrumentIdentityTests(unittest.TestCase):
|
||||
self.assertEqual(identity["exchange"], "PNK")
|
||||
|
||||
def test_falls_back_to_short_name(self):
|
||||
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
|
||||
mock.return_value.info = {"shortName": "TOTO", "sector": "Industrials"}
|
||||
identity = resolve_instrument_identity("TOTDY")
|
||||
self.assertEqual(identity["company_name"], "TOTO")
|
||||
|
||||
def test_skips_placeholder_values(self):
|
||||
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
|
||||
mock.return_value.info = {"longName": " ", "sector": "None", "industry": "n/a"}
|
||||
identity = resolve_instrument_identity("TOTDY")
|
||||
self.assertEqual(identity, {})
|
||||
|
||||
def test_fails_open_on_exception(self):
|
||||
with patch(
|
||||
"tradingagents.agents.utils.agent_utils.yf.Ticker",
|
||||
"tradingagents.dataflows.y_finance.yf.Ticker",
|
||||
side_effect=RuntimeError("rate limited"),
|
||||
):
|
||||
self.assertEqual(resolve_instrument_identity("TOTDY"), {})
|
||||
|
||||
def test_result_is_cached(self):
|
||||
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
|
||||
mock.return_value.info = {"longName": "TOTO LTD."}
|
||||
first = resolve_instrument_identity("TOTDY")
|
||||
second = resolve_instrument_identity("TOTDY")
|
||||
@@ -104,7 +104,7 @@ class GetInstrumentContextFromStateTests(unittest.TestCase):
|
||||
|
||||
def test_fallback_is_network_free_ticker_only(self):
|
||||
# No instrument_context and no yfinance call — must not hit the network.
|
||||
with patch("tradingagents.agents.utils.agent_utils.yf.Ticker") as mock:
|
||||
with patch("tradingagents.dataflows.y_finance.yf.Ticker") as mock:
|
||||
context = get_instrument_context_from_state(
|
||||
{"company_of_interest": "NVDA", "asset_type": "stock"}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Only the data layer imports vendor libraries.
|
||||
|
||||
Vendor calls belong in dataflows, where failures are raised as VendorError
|
||||
subclasses; a call made elsewhere can report an outage as a fact about the market.
|
||||
"""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
VENDOR_LIBRARIES = {"yfinance"}
|
||||
|
||||
|
||||
def _imports(path: Path) -> set[str]:
|
||||
names = set()
|
||||
for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
|
||||
if isinstance(node, ast.Import):
|
||||
names |= {a.name.split(".")[0] for a in node.names}
|
||||
elif isinstance(node, ast.ImportFrom) and node.module and not node.level:
|
||||
names.add(node.module.split(".")[0])
|
||||
return names
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_vendor_libraries_are_imported_only_by_the_data_layer():
|
||||
data_layer = ROOT / "tradingagents" / "dataflows"
|
||||
offenders = sorted(
|
||||
str(path.relative_to(ROOT))
|
||||
for package in ("tradingagents", "cli")
|
||||
for path in (ROOT / package).rglob("*.py")
|
||||
if data_layer not in path.parents and _imports(path) & VENDOR_LIBRARIES
|
||||
)
|
||||
assert offenders == []
|
||||
@@ -1043,7 +1043,7 @@ def test_a_longer_window_asks_for_enough_price_history(monkeypatch):
|
||||
days = pd.bdate_range(start, end)
|
||||
return pd.DataFrame({"Close": range(len(days))}, index=days)
|
||||
|
||||
monkeypatch.setattr("tradingagents.graph.trading_graph.yf.Ticker", _Ticker)
|
||||
monkeypatch.setattr("tradingagents.dataflows.y_finance.yf.Ticker", _Ticker)
|
||||
|
||||
raw, alpha, days, resolved = graph._fetch_returns("NVDA", "2026-06-01", 21, benchmark="SPY")
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ hit the right instrument instead of failing/mismatching.
|
||||
import pandas as pd
|
||||
|
||||
import tradingagents.agents.utils.agent_utils as au
|
||||
import tradingagents.dataflows.y_finance as y_finance
|
||||
import tradingagents.dataflows.yfinance_news as ynews
|
||||
import tradingagents.graph.trading_graph as tg
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_identity_lookup_normalizes_symbol(monkeypatch):
|
||||
def info(self):
|
||||
return {"longName": "Gold Futures", "quoteType": "FUTURE"}
|
||||
|
||||
monkeypatch.setattr(au.yf, "Ticker", FakeTicker)
|
||||
monkeypatch.setattr(y_finance.yf, "Ticker", FakeTicker)
|
||||
au.resolve_instrument_identity.cache_clear()
|
||||
|
||||
identity = au.resolve_instrument_identity("XAUUSD")
|
||||
@@ -45,7 +45,7 @@ def test_fetch_returns_normalizes_symbol(monkeypatch):
|
||||
idx = pd.date_range(start="2025-01-02", periods=len(prices), freq="D")
|
||||
return pd.DataFrame({"Close": prices}, index=idx)
|
||||
|
||||
monkeypatch.setattr(tg.yf, "Ticker", FakeTicker)
|
||||
monkeypatch.setattr(y_finance.yf, "Ticker", FakeTicker)
|
||||
|
||||
# _fetch_returns does not use ``self``; call unbound to avoid building the graph.
|
||||
raw, alpha, days, resolved = TradingAgentsGraph._fetch_returns(
|
||||
|
||||
@@ -3,7 +3,6 @@ import logging
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
import yfinance as yf
|
||||
from langchain_core.messages import HumanMessage, RemoveMessage
|
||||
|
||||
# Import tools from separate utility files
|
||||
@@ -23,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.y_finance import get_company_profile
|
||||
|
||||
# Public surface: the data tools are imported here so agents and the graph
|
||||
# import them from one place, plus the instrument/language helpers defined below.
|
||||
@@ -106,13 +106,11 @@ def resolve_instrument_identity(ticker: str) -> dict:
|
||||
ticker-only context rather than failing before analysis starts. Cached so
|
||||
the lookup happens at most once per ticker per process.
|
||||
|
||||
The symbol is normalized first (e.g. ``XAUUSD`` -> ``GC=F``) so identity
|
||||
resolves for the same instrument the price path actually fetches (#983).
|
||||
Identity resolves for the same instrument the price path fetches
|
||||
(``XAUUSD`` -> ``GC=F``, #983).
|
||||
"""
|
||||
from tradingagents.dataflows.symbol_utils import normalize_symbol
|
||||
|
||||
try:
|
||||
info = yf.Ticker(normalize_symbol(ticker)).info or {}
|
||||
info = get_company_profile(ticker)
|
||||
except Exception as exc: # noqa: BLE001 — fail open, never block the run
|
||||
logger.debug("Could not resolve instrument identity for %s: %s", ticker, exc)
|
||||
return {}
|
||||
|
||||
@@ -519,3 +519,22 @@ def get_insider_transactions(
|
||||
|
||||
except Exception as e:
|
||||
raise NoMarketDataError(ticker, canonical, f"insider transactions unavailable: {e}") from e
|
||||
|
||||
|
||||
def get_company_profile(ticker: str) -> dict:
|
||||
"""Yahoo's current profile for ``ticker``: name, sector, industry and the like."""
|
||||
canonical = normalize_symbol(ticker)
|
||||
try:
|
||||
return yf_retry(lambda: yf.Ticker(canonical).info) or {}
|
||||
except Exception as e:
|
||||
raise NoMarketDataError(ticker, canonical, f"profile unavailable: {e}") from e
|
||||
|
||||
|
||||
def get_closes(symbol: str, start_date: str, end_date: str) -> pd.Series:
|
||||
"""Daily closes from ``start_date`` up to, not including, ``end_date``."""
|
||||
canonical = normalize_symbol(symbol)
|
||||
try:
|
||||
history = yf_retry(lambda: yf.Ticker(canonical).history(start=start_date, end=end_date))
|
||||
except Exception as e:
|
||||
raise NoMarketDataError(symbol, canonical, f"prices unavailable: {e}") from e
|
||||
return history["Close"] if "Close" in history else pd.Series(dtype=float)
|
||||
|
||||
@@ -8,7 +8,6 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yfinance as yf
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
# Import the abstract tool methods from agent_utils
|
||||
@@ -31,6 +30,7 @@ from tradingagents.agents.utils.agent_utils import (
|
||||
from tradingagents.agents.utils.memory import TradingMemoryLog
|
||||
from tradingagents.dataflows.config import run_config, set_config
|
||||
from tradingagents.dataflows.utils import get_current_date, 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
|
||||
from tradingagents.reporting import write_report_tree
|
||||
@@ -302,8 +302,6 @@ class TradingAgentsGraph:
|
||||
the full holding window has not traded (#1169), or the symbol is delisted
|
||||
or unreachable.
|
||||
"""
|
||||
from tradingagents.dataflows.symbol_utils import normalize_symbol
|
||||
|
||||
try:
|
||||
start = datetime.strptime(trade_date, "%Y-%m-%d")
|
||||
# holding_days counts trading days, so ask for the calendar span they
|
||||
@@ -311,11 +309,9 @@ class TradingAgentsGraph:
|
||||
end = start + timedelta(days=round(holding_days * 7 / 5) + 7)
|
||||
end_str = end.strftime("%Y-%m-%d")
|
||||
|
||||
# Normalize so the realized-return lookup hits the same instrument
|
||||
# the analysis priced (e.g. XAUUSD -> GC=F) (#984). The benchmark is
|
||||
# already a canonical Yahoo symbol from ``_resolve_benchmark``.
|
||||
stock = yf.Ticker(normalize_symbol(ticker)).history(start=trade_date, end=end_str)
|
||||
bench = yf.Ticker(benchmark).history(start=trade_date, end=end_str)
|
||||
# Closes for the instrument the analysis priced (XAUUSD -> GC=F, #984).
|
||||
stock = get_closes(ticker, trade_date, end_str)
|
||||
bench = get_closes(benchmark, trade_date, end_str)
|
||||
|
||||
# Require the full holding window in both series. A rerun before it
|
||||
# has traded leaves the entry pending to retry next run, rather than
|
||||
@@ -323,14 +319,8 @@ class TradingAgentsGraph:
|
||||
if len(stock) <= holding_days or len(bench) <= holding_days:
|
||||
return None, None, None, None
|
||||
|
||||
raw = float(
|
||||
(stock["Close"].iloc[holding_days] - stock["Close"].iloc[0])
|
||||
/ stock["Close"].iloc[0]
|
||||
)
|
||||
bench_ret = float(
|
||||
(bench["Close"].iloc[holding_days] - bench["Close"].iloc[0])
|
||||
/ bench["Close"].iloc[0]
|
||||
)
|
||||
raw = float((stock.iloc[holding_days] - stock.iloc[0]) / stock.iloc[0])
|
||||
bench_ret = float((bench.iloc[holding_days] - bench.iloc[0]) / bench.iloc[0])
|
||||
alpha = raw - bench_ret
|
||||
# The date of the last price bar used is when this outcome became
|
||||
# known — the point-in-time cutoff for injecting the lesson (#1251).
|
||||
|
||||
Reference in New Issue
Block a user