From 7c8fe2fe9f0f29548c838f75c9cba47f9302390c Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sat, 13 Jun 2026 20:39:52 +0000 Subject: [PATCH] fix(data): normalize symbols on the identity and reflection paths resolve_instrument_identity and the reflection return lookup queried Yahoo with the raw ticker, so broker/forex/commodity symbols (XAUUSD, BTCUSD, EURUSD) failed identity or could mismatch the priced instrument even though the price path already normalized them. Route both through normalize_symbol (#983, #984). --- tests/test_symbol_normalization_paths.py | 54 +++++++++++++++++++++++ tradingagents/agents/utils/agent_utils.py | 7 ++- tradingagents/graph/trading_graph.py | 7 ++- 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/test_symbol_normalization_paths.py diff --git a/tests/test_symbol_normalization_paths.py b/tests/test_symbol_normalization_paths.py new file mode 100644 index 000000000..a023d24a6 --- /dev/null +++ b/tests/test_symbol_normalization_paths.py @@ -0,0 +1,54 @@ +"""Symbol normalization must apply on every yfinance path, not just price fetch. + +Regression tests for #983 (instrument identity) and #984 (reflection returns): +a broker symbol like XAUUSD must resolve to the same Yahoo symbol (GC=F) that +the price path uses, so identity and realized-return lookups hit the right +instrument instead of failing/mismatching. +""" +import pandas as pd + +import tradingagents.agents.utils.agent_utils as au +import tradingagents.graph.trading_graph as tg +from tradingagents.graph.trading_graph import TradingAgentsGraph + + +def test_identity_lookup_normalizes_symbol(monkeypatch): + seen = {} + + class FakeTicker: + def __init__(self, symbol): + seen["symbol"] = symbol + + @property + def info(self): + return {"longName": "Gold Futures", "quoteType": "FUTURE"} + + monkeypatch.setattr(au.yf, "Ticker", FakeTicker) + au.resolve_instrument_identity.cache_clear() + + identity = au.resolve_instrument_identity("XAUUSD") + + assert seen["symbol"] == "GC=F" # normalized, not the raw broker symbol + assert identity.get("company_name") == "Gold Futures" + + +def test_fetch_returns_normalizes_symbol(monkeypatch): + queried = [] + + class FakeTicker: + def __init__(self, symbol): + queried.append(symbol) + + def history(self, *args, **kwargs): + return pd.DataFrame({"Close": [100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0]}) + + monkeypatch.setattr(tg.yf, "Ticker", FakeTicker) + + # _fetch_returns does not use ``self``; call unbound to avoid building the graph. + raw, alpha, days = TradingAgentsGraph._fetch_returns( + None, "XAUUSD", "2025-01-02", holding_days=5, benchmark="SPY" + ) + + assert queried[0] == "GC=F" # stock symbol normalized (#984) + assert queried[1] == "SPY" # benchmark left as the canonical symbol + assert raw is not None and days is not None diff --git a/tradingagents/agents/utils/agent_utils.py b/tradingagents/agents/utils/agent_utils.py index 7476654c0..92113e5e5 100644 --- a/tradingagents/agents/utils/agent_utils.py +++ b/tradingagents/agents/utils/agent_utils.py @@ -70,9 +70,14 @@ def resolve_instrument_identity(ticker: str) -> dict: 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. + + The symbol is normalized first (e.g. ``XAUUSD`` -> ``GC=F``) so identity + resolves for the same instrument the price path actually fetches (#983). """ + from tradingagents.dataflows.symbol_utils import normalize_symbol + try: - info = yf.Ticker(ticker.upper()).info or {} + info = yf.Ticker(normalize_symbol(ticker)).info or {} 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 {} diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 2a851b344..a3de660f7 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -232,12 +232,17 @@ class TradingAgentsGraph: actual_holding_days)`` or ``(None, None, None)`` if price data is unavailable (too recent, delisted, or network error). """ + from tradingagents.dataflows.symbol_utils import normalize_symbol + try: start = datetime.strptime(trade_date, "%Y-%m-%d") end = start + timedelta(days=holding_days + 7) # buffer for weekends/holidays end_str = end.strftime("%Y-%m-%d") - stock = yf.Ticker(ticker).history(start=trade_date, end=end_str) + # 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) if len(stock) < 2 or len(bench) < 2: