fix(agents): ask again for an instrument identity whose lookup failed

- only an answered lookup is cached for the process, so one failed request no longer leaves every later run for that ticker without an identity
This commit is contained in:
Yijia-Xiao
2026-09-29 05:57:18 +00:00
parent 466d33ab3b
commit b89909724a
4 changed files with 21 additions and 7 deletions
+1 -1
View File
@@ -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
+11 -1
View File
@@ -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."}
+1 -1
View File
@@ -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")
+8 -4
View File
@@ -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")