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
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 {}

View File

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