fix(agents): say when the resolved identity is today's, not the run date's

- the vendor profile has no historical vintage, and every agent is told to anchor to it
This commit is contained in:
Yijia-Xiao
2026-09-17 23:37:43 +00:00
parent de7e43fc4a
commit f0a1cf6290
5 changed files with 87 additions and 5 deletions

View File

@@ -31,7 +31,7 @@ def test_create_run_state_settles_pending_and_carries_context(tmp_path, monkeypa
graph.propagator = Propagator()
settled = []
monkeypatch.setattr(graph, "_resolve_pending_entries", settled.append, raising=False)
monkeypatch.setattr(graph, "resolve_instrument_context", lambda t, a="stock": f"id:{t}", raising=False)
monkeypatch.setattr(graph, "resolve_instrument_context", lambda t, a="stock", d=None: f"id:{t}", raising=False)
monkeypatch.setattr(graph, "_memory_as_of", lambda d: d, raising=False)
graph.memory_log.store_decision("NVDA", "2026-01-05", "Rating: Buy\nold call")
graph.memory_log.update_with_outcome("NVDA", "2026-01-05", 0.01, 0.005, 5, "great trade", "2026-01-12")

View File

@@ -87,7 +87,7 @@ def _bare_graph(tmp_path):
graph.propagator = Propagator()
graph.selected_analysts = ["market"]
graph._resolve_pending_entries = lambda t: None
graph.resolve_instrument_context = lambda t, a="stock": ""
graph.resolve_instrument_context = lambda t, a="stock", d=None: ""
graph._memory_as_of = lambda d: None
return graph

View File

@@ -83,3 +83,69 @@ def test_trade_date_is_injected_not_model_visible(tool):
assert "trade_date" in tool.func.__code__.co_varnames
props = tool.tool_call_schema.model_json_schema()["properties"]
assert "trade_date" not in props and "curr_date" not in props
# --- the instrument's identity -------------------------------------------------
@pytest.mark.unit
def test_a_historical_run_is_told_the_identity_is_current(monkeypatch):
"""The company name, sector and industry come from today's vendor profile.
They are usually right for a past date, but a company that renamed or was
reclassified since would read wrong, and every agent is told to anchor to
this identity, so the run has to know which date it describes."""
from tradingagents.agents.utils.agent_utils import build_instrument_context
identity = {"company_name": "Example Corp", "sector": "Technology",
"industry": "Software", "exchange": "NMS"}
historical = build_instrument_context("EXMP", "stock", identity, curr_date="2024-03-14")
assert "Example Corp" in historical
assert "2024-03-14" in historical and "today" in historical.lower()
@pytest.mark.unit
def test_a_current_run_is_not_cluttered_with_a_vintage_note(monkeypatch):
from tradingagents.agents.utils.agent_utils import build_instrument_context
from tradingagents.dataflows.utils import get_current_date
today = build_instrument_context("EXMP", "stock", {"company_name": "Example Corp"},
curr_date=get_current_date())
assert "Example Corp" in today
assert "resolved today" not in today.lower()
@pytest.mark.unit
def test_insider_rows_are_dated_by_the_trade_not_the_filing():
"""yfinance reports the transaction date and carries no filing date. A trade
becomes public when the Form 4 is filed, up to two business days later, so a
run must not be told these rows were public on their transaction date."""
import pandas as pd
from tradingagents.dataflows import y_finance
frame = pd.DataFrame({
"Shares": [100, 200],
"Text": ["Sale at price 10.00 per share.", "Sale at price 11.00 per share."],
"Start Date": pd.to_datetime(["2026-05-01", "2026-05-20"]),
})
ticker = mock.Mock(insider_transactions=frame)
with mock.patch.object(y_finance.yf, "Ticker", return_value=ticker):
out = y_finance.get_insider_transactions("AAPL", "2026-05-10")
assert "2026-05-01" in out and "2026-05-20" not in out # still bounded by the date
assert "transaction date" in out.lower() # and says what the date means
assert "filed" in out.lower() # and that filing comes later
@pytest.mark.unit
def test_an_indicator_that_could_not_be_read_is_not_shown_as_a_blank_value():
"""The per-day fallback returned an empty string for a failed read, so the
table rendered a row per day with nothing after the colon: an analyst reads
that as "no value on that day" rather than "could not be obtained"."""
from tradingagents.dataflows import y_finance
from tradingagents.dataflows.errors import VendorError
with mock.patch.object(y_finance.StockstatsUtils, "get_stock_stats",
side_effect=RuntimeError("cache parse failed")), \
pytest.raises(VendorError):
y_finance.get_stockstats_indicator("AAPL", "rsi", "2026-05-08")

View File

@@ -48,6 +48,8 @@ __all__ = [
logger = logging.getLogger(__name__)
from tradingagents.dataflows.utils import get_current_date # noqa: E402
def get_language_instruction() -> str:
"""Return a prompt instruction for the configured output language.
@@ -137,6 +139,7 @@ def build_instrument_context(
ticker: str,
asset_type: str = "stock",
identity: Mapping[str, str] | None = None,
curr_date: str | None = None,
) -> str:
"""Describe the exact instrument so agents preserve identity and ticker.
@@ -144,6 +147,11 @@ def build_instrument_context(
:func:`resolve_instrument_identity`), the company name and business
classification are injected so agents anchor to the real company rather
than pattern-matching the price chart to a wrong one (#814).
That profile carries no historical vintage: it describes the company today.
For a run dated earlier, the context says so, since a company that has since
renamed or been reclassified would otherwise anchor the whole graph to an
identity it did not have on the analysis date.
"""
is_crypto = asset_type == "crypto"
instrument_label = "asset" if is_crypto else "instrument"
@@ -174,6 +182,13 @@ def build_instrument_context(
"Do not substitute a different company or ticker unless a tool "
"result explicitly disproves this resolved identity."
)
today = get_current_date()
if curr_date and str(curr_date) < today:
context += (
f" This identity is how the vendor describes the instrument today "
f"({today}), not necessarily on {curr_date}: a name or "
f"classification changed since then would read as the current one."
)
if is_crypto:
context += (

View File

@@ -393,7 +393,8 @@ class TradingAgentsGraph:
if updates:
self.memory_log.batch_update_with_outcomes(updates)
def resolve_instrument_context(self, ticker: str, asset_type: str = "stock") -> str:
def resolve_instrument_context(self, ticker: str, asset_type: str = "stock",
curr_date: str | None = None) -> str:
"""Resolve ticker identity once and return the full instrument context.
Deterministic yfinance lookup (cached, fail-open) injected into a
@@ -403,7 +404,7 @@ class TradingAgentsGraph:
graph regardless of entry point.
"""
identity = resolve_instrument_identity(ticker)
return build_instrument_context(ticker, asset_type, identity)
return build_instrument_context(ticker, asset_type, identity, curr_date)
def _memory_as_of(self, trade_date) -> str | None:
"""Point-in-time cutoff for past-context lessons (#1251).
@@ -551,7 +552,7 @@ class TradingAgentsGraph:
past_context=self.memory_log.get_past_context(
company_name, as_of=self._memory_as_of(trade_date)
),
instrument_context=self.resolve_instrument_context(company_name, asset_type),
instrument_context=self.resolve_instrument_context(company_name, asset_type, trade_date),
portfolio_context=portfolio.render(company_name) if portfolio is not None else "",
)