diff --git a/tests/test_graph_end_to_end.py b/tests/test_graph_end_to_end.py index 7259108a8..b8092327a 100644 --- a/tests/test_graph_end_to_end.py +++ b/tests/test_graph_end_to_end.py @@ -115,7 +115,7 @@ def offline(monkeypatch, tmp_path): 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(yahoo_market.yf, "Ticker", lambda s: type("T", (), {"info": {"longName": "NVIDIA"}})()) - context.resolve_instrument_identity.cache_clear() + context._identity.cache_clear() return called diff --git a/tests/test_instrument_identity.py b/tests/test_instrument_identity.py index 11c79e67c..c4216f110 100644 --- a/tests/test_instrument_identity.py +++ b/tests/test_instrument_identity.py @@ -7,6 +7,7 @@ from unittest.mock import patch import pytest from tradingagents.agents.context import ( + _identity, build_instrument_context, get_instrument_context_from_state, resolve_instrument_identity, @@ -16,7 +17,7 @@ from tradingagents.agents.context import ( @pytest.mark.unit class ResolveInstrumentIdentityTests(unittest.TestCase): def setUp(self): - resolve_instrument_identity.cache_clear() + _identity.cache_clear() def test_resolves_company_metadata_from_yfinance(self): with patch("tradingagents.dataflows.vendors.yahoo.market.yf.Ticker") as mock: @@ -54,6 +55,15 @@ class ResolveInstrumentIdentityTests(unittest.TestCase): ): self.assertEqual(resolve_instrument_identity("TOTDY"), {}) + def test_a_failed_lookup_is_asked_again(self): + # A long process (a backtest) must not run every later cell without an + # identity because one request failed. + with patch("tradingagents.dataflows.vendors.yahoo.market.yf.Ticker") as mock: + type(mock.return_value).info = property(lambda self: (_ for _ in ()).throw(TimeoutError())) + self.assertEqual(resolve_instrument_identity("TOTDY"), {}) + type(mock.return_value).info = {"longName": "TOTO LTD."} + self.assertEqual(resolve_instrument_identity("TOTDY")["company_name"], "TOTO LTD.") + def test_result_is_cached(self): with patch("tradingagents.dataflows.vendors.yahoo.market.yf.Ticker") as mock: mock.return_value.info = {"longName": "TOTO LTD."} diff --git a/tests/test_symbol_normalization_paths.py b/tests/test_symbol_normalization_paths.py index f3ed1b21d..eb8a4dc35 100644 --- a/tests/test_symbol_normalization_paths.py +++ b/tests/test_symbol_normalization_paths.py @@ -25,7 +25,7 @@ def test_identity_lookup_normalizes_symbol(monkeypatch): return {"longName": "Gold Futures", "quoteType": "FUTURE"} monkeypatch.setattr(yahoo_market.yf, "Ticker", FakeTicker) - au.resolve_instrument_identity.cache_clear() + au._identity.cache_clear() identity = au.resolve_instrument_identity("XAUUSD") diff --git a/tradingagents/agents/context.py b/tradingagents/agents/context.py index e2a754369..fd6a77169 100644 --- a/tradingagents/agents/context.py +++ b/tradingagents/agents/context.py @@ -52,7 +52,6 @@ def _clean_identity_value(value: Any) -> str | None: return cleaned -@functools.lru_cache(maxsize=256) def resolve_instrument_identity(ticker: str) -> dict: """Resolve deterministic identity metadata (company name, sector, …) for a ticker. @@ -64,18 +63,23 @@ def resolve_instrument_identity(ticker: str) -> dict: Best-effort by design: if yfinance is unavailable, rate-limited, or doesn't recognise the ticker, we return ``{}`` and the caller falls back to - ticker-only context rather than failing before analysis starts. Cached so - the lookup happens at most once per ticker per process. + ticker-only context rather than failing before analysis starts. An answer + is cached for the process; a failed lookup is asked again next time. Identity resolves for the same instrument the price path fetches (``XAUUSD`` -> ``GC=F``, #983). """ try: - info = get_company_profile(ticker) + return _identity(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 {} + +@functools.lru_cache(maxsize=256) +def _identity(ticker: str) -> dict: + """The vendor's identity fields for ``ticker``; raises if the lookup fails.""" + info = get_company_profile(ticker) identity: dict[str, str] = {} company_name = _clean_identity_value(info.get("longName")) or _clean_identity_value( info.get("shortName")