mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-25 22:12:27 +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 import schemas
|
||||||
from tradingagents.agents.analysts import sentiment_analyst
|
from tradingagents.agents.analysts import sentiment_analyst
|
||||||
from tradingagents.agents.utils import agent_utils
|
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.default_config import DEFAULT_CONFIG
|
||||||
from tradingagents.graph import trading_graph
|
from tradingagents.graph import trading_graph
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ def offline(monkeypatch, tmp_path):
|
|||||||
lambda *a, **k: called.add("ohlcv") or prices.copy())
|
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_stocktwits_messages", lambda *a, **k: "no posts")
|
||||||
monkeypatch.setattr(sentiment_analyst, "fetch_reddit_posts", 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()
|
agent_utils.resolve_instrument_identity.cache_clear()
|
||||||
return called
|
return called
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ResolveInstrumentIdentityTests(unittest.TestCase):
|
|||||||
resolve_instrument_identity.cache_clear()
|
resolve_instrument_identity.cache_clear()
|
||||||
|
|
||||||
def test_resolves_company_metadata_from_yfinance(self):
|
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 = {
|
mock.return_value.info = {
|
||||||
"longName": "TOTO LTD.",
|
"longName": "TOTO LTD.",
|
||||||
"shortName": "TOTO",
|
"shortName": "TOTO",
|
||||||
@@ -38,26 +38,26 @@ class ResolveInstrumentIdentityTests(unittest.TestCase):
|
|||||||
self.assertEqual(identity["exchange"], "PNK")
|
self.assertEqual(identity["exchange"], "PNK")
|
||||||
|
|
||||||
def test_falls_back_to_short_name(self):
|
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"}
|
mock.return_value.info = {"shortName": "TOTO", "sector": "Industrials"}
|
||||||
identity = resolve_instrument_identity("TOTDY")
|
identity = resolve_instrument_identity("TOTDY")
|
||||||
self.assertEqual(identity["company_name"], "TOTO")
|
self.assertEqual(identity["company_name"], "TOTO")
|
||||||
|
|
||||||
def test_skips_placeholder_values(self):
|
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"}
|
mock.return_value.info = {"longName": " ", "sector": "None", "industry": "n/a"}
|
||||||
identity = resolve_instrument_identity("TOTDY")
|
identity = resolve_instrument_identity("TOTDY")
|
||||||
self.assertEqual(identity, {})
|
self.assertEqual(identity, {})
|
||||||
|
|
||||||
def test_fails_open_on_exception(self):
|
def test_fails_open_on_exception(self):
|
||||||
with patch(
|
with patch(
|
||||||
"tradingagents.agents.utils.agent_utils.yf.Ticker",
|
"tradingagents.dataflows.y_finance.yf.Ticker",
|
||||||
side_effect=RuntimeError("rate limited"),
|
side_effect=RuntimeError("rate limited"),
|
||||||
):
|
):
|
||||||
self.assertEqual(resolve_instrument_identity("TOTDY"), {})
|
self.assertEqual(resolve_instrument_identity("TOTDY"), {})
|
||||||
|
|
||||||
def test_result_is_cached(self):
|
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."}
|
mock.return_value.info = {"longName": "TOTO LTD."}
|
||||||
first = resolve_instrument_identity("TOTDY")
|
first = resolve_instrument_identity("TOTDY")
|
||||||
second = 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):
|
def test_fallback_is_network_free_ticker_only(self):
|
||||||
# No instrument_context and no yfinance call — must not hit the network.
|
# 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(
|
context = get_instrument_context_from_state(
|
||||||
{"company_of_interest": "NVDA", "asset_type": "stock"}
|
{"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)
|
days = pd.bdate_range(start, end)
|
||||||
return pd.DataFrame({"Close": range(len(days))}, index=days)
|
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")
|
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 pandas as pd
|
||||||
|
|
||||||
import tradingagents.agents.utils.agent_utils as au
|
import tradingagents.agents.utils.agent_utils as au
|
||||||
|
import tradingagents.dataflows.y_finance as y_finance
|
||||||
import tradingagents.dataflows.yfinance_news as ynews
|
import tradingagents.dataflows.yfinance_news as ynews
|
||||||
import tradingagents.graph.trading_graph as tg
|
|
||||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ def test_identity_lookup_normalizes_symbol(monkeypatch):
|
|||||||
def info(self):
|
def info(self):
|
||||||
return {"longName": "Gold Futures", "quoteType": "FUTURE"}
|
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()
|
au.resolve_instrument_identity.cache_clear()
|
||||||
|
|
||||||
identity = au.resolve_instrument_identity("XAUUSD")
|
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")
|
idx = pd.date_range(start="2025-01-02", periods=len(prices), freq="D")
|
||||||
return pd.DataFrame({"Close": prices}, index=idx)
|
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.
|
# _fetch_returns does not use ``self``; call unbound to avoid building the graph.
|
||||||
raw, alpha, days, resolved = TradingAgentsGraph._fetch_returns(
|
raw, alpha, days, resolved = TradingAgentsGraph._fetch_returns(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import logging
|
|||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import yfinance as yf
|
|
||||||
from langchain_core.messages import HumanMessage, RemoveMessage
|
from langchain_core.messages import HumanMessage, RemoveMessage
|
||||||
|
|
||||||
# Import tools from separate utility files
|
# 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.prediction_markets_tools import get_prediction_markets
|
||||||
from tradingagents.agents.utils.technical_indicators_tools import get_indicators
|
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
|
# 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.
|
# 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
|
ticker-only context rather than failing before analysis starts. Cached so
|
||||||
the lookup happens at most once per ticker per process.
|
the lookup happens at most once per ticker per process.
|
||||||
|
|
||||||
The symbol is normalized first (e.g. ``XAUUSD`` -> ``GC=F``) so identity
|
Identity resolves for the same instrument the price path fetches
|
||||||
resolves for the same instrument the price path actually fetches (#983).
|
(``XAUUSD`` -> ``GC=F``, #983).
|
||||||
"""
|
"""
|
||||||
from tradingagents.dataflows.symbol_utils import normalize_symbol
|
|
||||||
|
|
||||||
try:
|
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
|
except Exception as exc: # noqa: BLE001 — fail open, never block the run
|
||||||
logger.debug("Could not resolve instrument identity for %s: %s", ticker, exc)
|
logger.debug("Could not resolve instrument identity for %s: %s", ticker, exc)
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -519,3 +519,22 @@ def get_insider_transactions(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise NoMarketDataError(ticker, canonical, f"insider transactions unavailable: {e}") from 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 pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import yfinance as yf
|
|
||||||
from langgraph.prebuilt import ToolNode
|
from langgraph.prebuilt import ToolNode
|
||||||
|
|
||||||
# Import the abstract tool methods from agent_utils
|
# 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.agents.utils.memory import TradingMemoryLog
|
||||||
from tradingagents.dataflows.config import run_config, set_config
|
from tradingagents.dataflows.config import run_config, set_config
|
||||||
from tradingagents.dataflows.utils import get_current_date, safe_ticker_component
|
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.default_config import DEFAULT_CONFIG
|
||||||
from tradingagents.llm_clients import create_llm_client
|
from tradingagents.llm_clients import create_llm_client
|
||||||
from tradingagents.reporting import write_report_tree
|
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
|
the full holding window has not traded (#1169), or the symbol is delisted
|
||||||
or unreachable.
|
or unreachable.
|
||||||
"""
|
"""
|
||||||
from tradingagents.dataflows.symbol_utils import normalize_symbol
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start = datetime.strptime(trade_date, "%Y-%m-%d")
|
start = datetime.strptime(trade_date, "%Y-%m-%d")
|
||||||
# holding_days counts trading days, so ask for the calendar span they
|
# 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 = start + timedelta(days=round(holding_days * 7 / 5) + 7)
|
||||||
end_str = end.strftime("%Y-%m-%d")
|
end_str = end.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
# Normalize so the realized-return lookup hits the same instrument
|
# Closes for the instrument the analysis priced (XAUUSD -> GC=F, #984).
|
||||||
# the analysis priced (e.g. XAUUSD -> GC=F) (#984). The benchmark is
|
stock = get_closes(ticker, trade_date, end_str)
|
||||||
# already a canonical Yahoo symbol from ``_resolve_benchmark``.
|
bench = get_closes(benchmark, trade_date, end_str)
|
||||||
stock = yf.Ticker(normalize_symbol(ticker)).history(start=trade_date, end=end_str)
|
|
||||||
bench = yf.Ticker(benchmark).history(start=trade_date, end=end_str)
|
|
||||||
|
|
||||||
# Require the full holding window in both series. A rerun before it
|
# Require the full holding window in both series. A rerun before it
|
||||||
# has traded leaves the entry pending to retry next run, rather than
|
# 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:
|
if len(stock) <= holding_days or len(bench) <= holding_days:
|
||||||
return None, None, None, None
|
return None, None, None, None
|
||||||
|
|
||||||
raw = float(
|
raw = float((stock.iloc[holding_days] - stock.iloc[0]) / stock.iloc[0])
|
||||||
(stock["Close"].iloc[holding_days] - stock["Close"].iloc[0])
|
bench_ret = float((bench.iloc[holding_days] - bench.iloc[0]) / bench.iloc[0])
|
||||||
/ stock["Close"].iloc[0]
|
|
||||||
)
|
|
||||||
bench_ret = float(
|
|
||||||
(bench["Close"].iloc[holding_days] - bench["Close"].iloc[0])
|
|
||||||
/ bench["Close"].iloc[0]
|
|
||||||
)
|
|
||||||
alpha = raw - bench_ret
|
alpha = raw - bench_ret
|
||||||
# The date of the last price bar used is when this outcome became
|
# The date of the last price bar used is when this outcome became
|
||||||
# known — the point-in-time cutoff for injecting the lesson (#1251).
|
# known — the point-in-time cutoff for injecting the lesson (#1251).
|
||||||
|
|||||||
Reference in New Issue
Block a user