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).
This commit is contained in:
Yijia-Xiao
2026-06-13 20:39:52 +00:00
parent 2a58c2208f
commit 7c8fe2fe9f
3 changed files with 66 additions and 2 deletions

View File

@@ -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

View File

@@ -70,9 +70,14 @@ def resolve_instrument_identity(ticker: str) -> dict:
recognise the ticker, we return ``{}`` and the caller falls back to recognise the ticker, we return ``{}`` and the caller falls back to
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
resolves for the same instrument the price path actually fetches (#983).
""" """
from tradingagents.dataflows.symbol_utils import normalize_symbol
try: 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 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 {}

View File

@@ -232,12 +232,17 @@ class TradingAgentsGraph:
actual_holding_days)`` or ``(None, None, None)`` if price data is actual_holding_days)`` or ``(None, None, None)`` if price data is
unavailable (too recent, delisted, or network error). unavailable (too recent, delisted, or network error).
""" """
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")
end = start + timedelta(days=holding_days + 7) # buffer for weekends/holidays end = start + timedelta(days=holding_days + 7) # buffer for weekends/holidays
end_str = end.strftime("%Y-%m-%d") 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) bench = yf.Ticker(benchmark).history(start=trade_date, end=end_str)
if len(stock) < 2 or len(bench) < 2: if len(stock) < 2 or len(bench) < 2: