diff --git a/tests/test_cli_decision_log.py b/tests/test_cli_decision_log.py index 63dfee7ac..719fcba29 100644 --- a/tests/test_cli_decision_log.py +++ b/tests/test_cli_decision_log.py @@ -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") diff --git a/tests/test_portfolio_context.py b/tests/test_portfolio_context.py index 63cdc41f9..12305a8ec 100644 --- a/tests/test_portfolio_context.py +++ b/tests/test_portfolio_context.py @@ -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 diff --git a/tests/test_undated_tools_as_of.py b/tests/test_undated_tools_as_of.py index 5650d8019..d8be2abd4 100644 --- a/tests/test_undated_tools_as_of.py +++ b/tests/test_undated_tools_as_of.py @@ -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") diff --git a/tradingagents/agents/utils/agent_utils.py b/tradingagents/agents/utils/agent_utils.py index 68d609534..3004d576f 100644 --- a/tradingagents/agents/utils/agent_utils.py +++ b/tradingagents/agents/utils/agent_utils.py @@ -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 += ( diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index e08dd5750..a3510ddc2 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -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 "", )