mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-07-07 23:24:22 +03:00
fix(data): normalize ticker on the news path
The yfinance news fetch queried the raw ticker while every other path uses the canonical symbol, so broker/forex/crypto aliases silently returned no news. Normalize it (XAUUSD -> GC=F) and keep the user's ticker in the report header.
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
"""Symbol normalization must apply on every yfinance path, not just price fetch.
|
"""Symbol normalization must apply on every yfinance path, not just price fetch.
|
||||||
|
|
||||||
Regression tests for #983 (instrument identity) and #984 (reflection returns):
|
Regression tests for #983 (instrument identity), #984 (reflection returns), and
|
||||||
a broker symbol like XAUUSD must resolve to the same Yahoo symbol (GC=F) that
|
the news path: a broker symbol like XAUUSD must resolve to the same Yahoo symbol
|
||||||
the price path uses, so identity and realized-return lookups hit the right
|
(GC=F) that the price path uses, so identity, realized-return, and news lookups
|
||||||
instrument instead of failing/mismatching.
|
hit the right instrument instead of failing/mismatching.
|
||||||
"""
|
"""
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
import tradingagents.agents.utils.agent_utils as au
|
import tradingagents.agents.utils.agent_utils as au
|
||||||
|
import tradingagents.dataflows.yfinance_news as ynews
|
||||||
import tradingagents.graph.trading_graph as tg
|
import tradingagents.graph.trading_graph as tg
|
||||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||||
|
|
||||||
@@ -52,3 +53,23 @@ def test_fetch_returns_normalizes_symbol(monkeypatch):
|
|||||||
assert queried[0] == "GC=F" # stock symbol normalized (#984)
|
assert queried[0] == "GC=F" # stock symbol normalized (#984)
|
||||||
assert queried[1] == "SPY" # benchmark left as the canonical symbol
|
assert queried[1] == "SPY" # benchmark left as the canonical symbol
|
||||||
assert raw is not None and days is not None
|
assert raw is not None and days is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_news_lookup_normalizes_symbol(monkeypatch):
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
class FakeTicker:
|
||||||
|
def __init__(self, symbol):
|
||||||
|
seen["symbol"] = symbol
|
||||||
|
|
||||||
|
def get_news(self, count):
|
||||||
|
return []
|
||||||
|
|
||||||
|
monkeypatch.setattr(ynews.yf, "Ticker", FakeTicker)
|
||||||
|
monkeypatch.setattr(ynews, "yf_retry", lambda fn: fn())
|
||||||
|
|
||||||
|
out = ynews.get_news_yfinance("XAUUSD", "2025-01-01", "2025-01-10")
|
||||||
|
|
||||||
|
assert seen["symbol"] == "GC=F" # news queried with the canonical symbol
|
||||||
|
assert "XAUUSD" in out # the user's ticker stays in the report
|
||||||
|
assert "GC=F" in out # provenance noted
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from dateutil.relativedelta import relativedelta
|
|||||||
|
|
||||||
from .config import get_config
|
from .config import get_config
|
||||||
from .stockstats_utils import yf_retry
|
from .stockstats_utils import yf_retry
|
||||||
|
from .symbol_utils import normalize_symbol
|
||||||
|
|
||||||
|
|
||||||
def _extract_article_data(article: dict) -> dict:
|
def _extract_article_data(article: dict) -> dict:
|
||||||
@@ -87,12 +88,17 @@ def get_news_yfinance(
|
|||||||
Formatted string containing news articles
|
Formatted string containing news articles
|
||||||
"""
|
"""
|
||||||
article_limit = get_config()["news_article_limit"]
|
article_limit = get_config()["news_article_limit"]
|
||||||
|
# Query Yahoo with the canonical symbol, like every other yfinance path —
|
||||||
|
# a raw broker/forex/crypto alias (XAUUSD, BTCUSD) otherwise silently
|
||||||
|
# returns no news. Keep the user's ticker in the report header.
|
||||||
|
canonical = normalize_symbol(ticker)
|
||||||
|
resolved = "" if canonical == ticker else f" (resolved to {canonical})"
|
||||||
try:
|
try:
|
||||||
stock = yf.Ticker(ticker)
|
stock = yf.Ticker(canonical)
|
||||||
news = yf_retry(lambda: stock.get_news(count=article_limit))
|
news = yf_retry(lambda: stock.get_news(count=article_limit))
|
||||||
|
|
||||||
if not news:
|
if not news:
|
||||||
return f"No news found for {ticker}"
|
return f"No news found for {ticker}{resolved}"
|
||||||
|
|
||||||
# Parse date range for filtering
|
# Parse date range for filtering
|
||||||
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
||||||
@@ -117,9 +123,9 @@ def get_news_yfinance(
|
|||||||
filtered_count += 1
|
filtered_count += 1
|
||||||
|
|
||||||
if filtered_count == 0:
|
if filtered_count == 0:
|
||||||
return f"No news found for {ticker} between {start_date} and {end_date}"
|
return f"No news found for {ticker}{resolved} between {start_date} and {end_date}"
|
||||||
|
|
||||||
return f"## {ticker} News, from {start_date} to {end_date}:\n\n{news_str}"
|
return f"## {ticker}{resolved} News, from {start_date} to {end_date}:\n\n{news_str}"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error fetching news for {ticker}: {str(e)}"
|
return f"Error fetching news for {ticker}: {str(e)}"
|
||||||
|
|||||||
Reference in New Issue
Block a user