diff --git a/tests/test_undated_tools_as_of.py b/tests/test_undated_tools_as_of.py new file mode 100644 index 000000000..5650d8019 --- /dev/null +++ b/tests/test_undated_tools_as_of.py @@ -0,0 +1,85 @@ +"""Insider filings and prediction-market odds are bounded by the run's trade date. + +Neither tool takes a date from the model, so the run's trade_date is injected from +graph state. Insider filings carry dates and are filtered to it; Polymarket serves +only live odds, so a historical run withholds them. +""" + +from __future__ import annotations + +import json +from unittest import mock + +import pandas as pd +import pytest + +from tradingagents.agents.utils import news_data_tools, prediction_markets_tools +from tradingagents.dataflows import alpha_vantage_news, polymarket, y_finance + + +def _insider_frame(*dates): + return pd.DataFrame({ + "Shares": [100] * len(dates), + "Text": [f"Sale at price {100 + i} per share." for i in range(len(dates))], + "Start Date": pd.to_datetime(list(dates)), + }) + + +def _yf_insider(frame, curr_date): + ticker = mock.Mock(insider_transactions=frame) + with mock.patch.object(y_finance.yf, "Ticker", return_value=ticker): + return y_finance.get_insider_transactions("AAPL", curr_date) + + +@pytest.mark.unit +def test_yfinance_insider_filings_after_the_date_are_dropped(): + out = _yf_insider(_insider_frame("2026-09-08", "2025-06-02", "2025-05-30", "2025-01-10"), "2025-06-01") + assert "2026-09-08" not in out and "2025-06-02" not in out + assert "2025-05-30" in out and "2025-01-10" in out + + +@pytest.mark.unit +def test_yfinance_insider_date_before_coverage_is_unavailable_not_absent(): + out = _yf_insider(_insider_frame("2026-09-08", "2025-06-02"), "2024-01-01") + assert "unavailable" in out and "No insider transactions reported" not in out + assert "2025-06-02" in out # where coverage starts + + +@pytest.mark.unit +def test_yfinance_insider_without_a_date_is_unfiltered(): + out = _yf_insider(_insider_frame("2026-09-08", "2025-01-10"), None) + assert "2026-09-08" in out and "2025-01-10" in out + + +@pytest.mark.unit +def test_alpha_vantage_insider_filings_after_the_date_are_dropped(): + body = json.dumps({"data": [ + {"transaction_date": "2026-09-08", "executive": "A"}, + {"transaction_date": "2025-05-30", "executive": "B"}, + ]}) + with mock.patch.object(alpha_vantage_news, "_make_api_request", return_value=body): + out = json.loads(alpha_vantage_news.get_insider_transactions("AAPL", "2025-06-01")) + assert [t["executive"] for t in out["data"]] == ["B"] + + +@pytest.mark.unit +def test_polymarket_withholds_live_odds_from_a_historical_run(): + with mock.patch.object(polymarket, "_request", side_effect=AssertionError("must not fetch")): + out = polymarket.get_prediction_markets("Fed rate cut", curr_date="2025-06-01") + assert "withheld" in out + + +@pytest.mark.unit +def test_polymarket_serves_a_current_run(): + with mock.patch.object(polymarket, "_request", return_value={"events": []}) as req: + polymarket.get_prediction_markets("Fed rate cut", curr_date=polymarket.get_current_date()) + req.assert_called_once() + + +@pytest.mark.unit +@pytest.mark.parametrize("tool", [news_data_tools.get_insider_transactions, + prediction_markets_tools.get_prediction_markets], ids=lambda t: t.name) +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 diff --git a/tradingagents/agents/utils/news_data_tools.py b/tradingagents/agents/utils/news_data_tools.py index 5be1c310d..122240a5a 100644 --- a/tradingagents/agents/utils/news_data_tools.py +++ b/tradingagents/agents/utils/news_data_tools.py @@ -53,6 +53,7 @@ def get_global_news( @tool def get_insider_transactions( ticker: Annotated[str, "ticker symbol"], + trade_date: Annotated[str, InjectedState("trade_date")] = "", ) -> str: """ Retrieve insider transaction information about a company. @@ -62,4 +63,4 @@ def get_insider_transactions( Returns: str: A report of insider transaction data """ - return route_to_vendor("get_insider_transactions", ticker) + return route_to_vendor("get_insider_transactions", ticker, trade_date or None) diff --git a/tradingagents/agents/utils/prediction_markets_tools.py b/tradingagents/agents/utils/prediction_markets_tools.py index 843c9a49c..dfa50f9b9 100644 --- a/tradingagents/agents/utils/prediction_markets_tools.py +++ b/tradingagents/agents/utils/prediction_markets_tools.py @@ -1,6 +1,7 @@ from typing import Annotated from langchain_core.tools import tool +from langgraph.prebuilt import InjectedState from tradingagents.dataflows.interface import route_to_vendor @@ -13,6 +14,7 @@ def get_prediction_markets( "'US election', or a sector/company event.", ], limit: Annotated[int | None, "Max markets to return; omit for a default of 6"] = None, + trade_date: Annotated[str, InjectedState("trade_date")] = "", ) -> str: """ Retrieve live, market-implied probabilities for forward-looking events from @@ -28,4 +30,4 @@ def get_prediction_markets( Returns: str: A formatted markdown report of matching prediction markets """ - return route_to_vendor("get_prediction_markets", topic, limit) + return route_to_vendor("get_prediction_markets", topic, limit, trade_date or None) diff --git a/tradingagents/dataflows/alpha_vantage_news.py b/tradingagents/dataflows/alpha_vantage_news.py index f9c7cfc99..233739b1b 100644 --- a/tradingagents/dataflows/alpha_vantage_news.py +++ b/tradingagents/dataflows/alpha_vantage_news.py @@ -1,3 +1,5 @@ +import json + from .alpha_vantage_common import _make_api_request, format_datetime_for_api @@ -53,13 +55,14 @@ def get_global_news(curr_date, look_back_days: int = 7, limit: int = 50) -> dict return _make_api_request("NEWS_SENTIMENT", params) -def get_insider_transactions(symbol: str) -> dict[str, str] | str: +def get_insider_transactions(symbol: str, curr_date: str | None = None) -> dict[str, str] | str: """Returns latest and historical insider transactions by key stakeholders. Covers transactions by founders, executives, board members, etc. Args: symbol: Ticker symbol. Example: "IBM". + curr_date: When given, only transactions on or before it (yyyy-mm-dd). Returns: Dictionary containing insider transaction data or JSON string. @@ -69,4 +72,9 @@ def get_insider_transactions(symbol: str) -> dict[str, str] | str: "symbol": symbol, } - return _make_api_request("INSIDER_TRANSACTIONS", params) + response = _make_api_request("INSIDER_TRANSACTIONS", params) + if not curr_date: + return response + payload = json.loads(response) + payload["data"] = [t for t in payload["data"] if t["transaction_date"] <= curr_date] + return json.dumps(payload) diff --git a/tradingagents/dataflows/polymarket.py b/tradingagents/dataflows/polymarket.py index b76dfe7f0..3188ac034 100644 --- a/tradingagents/dataflows/polymarket.py +++ b/tradingagents/dataflows/polymarket.py @@ -15,6 +15,8 @@ from datetime import datetime, timezone import requests +from .utils import get_current_date + logger = logging.getLogger(__name__) GAMMA_BASE = "https://gamma-api.polymarket.com" @@ -65,7 +67,7 @@ def _is_forward_looking(market: dict, now: datetime) -> bool: ) -def get_prediction_markets(topic: str, limit: int | None = None) -> str: +def get_prediction_markets(topic: str, limit: int | None = None, curr_date: str | None = None) -> str: """Return live prediction-market probabilities for an event topic. Args: @@ -73,12 +75,20 @@ def get_prediction_markets(topic: str, limit: int | None = None) -> str: "US election", or a sector/company event. limit: Max markets to return (ranked by traded volume); ``None`` uses DEFAULT_LIMIT. + curr_date: The analysis date. Polymarket serves only live odds, so a + date before today withholds them. Returns: A markdown report of the most-traded open markets matching the topic, each with its implied probability, traded volume, resolution date, and recent (1-week) move. """ + if curr_date and curr_date < get_current_date(): + return ( + f"Prediction-market odds are withheld for {curr_date}. Polymarket serves " + f"only live odds on open markets, with no historical vintage, so serving " + f"them would put post-decision information into a {curr_date} analysis." + ) if limit is None: limit = DEFAULT_LIMIT diff --git a/tradingagents/dataflows/y_finance.py b/tradingagents/dataflows/y_finance.py index 9e67ac591..7bc9dd18a 100644 --- a/tradingagents/dataflows/y_finance.py +++ b/tradingagents/dataflows/y_finance.py @@ -455,7 +455,8 @@ def get_income_statement( def get_insider_transactions( - ticker: Annotated[str, "ticker symbol of the company"] + ticker: Annotated[str, "ticker symbol of the company"], + curr_date: Annotated[str | None, "only filings on or before this date, yyyy-mm-dd"] = None, ): """Get insider transactions data from yfinance.""" canonical = normalize_symbol(ticker) @@ -468,6 +469,16 @@ def get_insider_transactions( if data is None or data.empty: return f"No insider transactions reported for symbol '{canonical}'" + if curr_date: + filed = data["Start Date"] + kept = data[filed <= pd.Timestamp(curr_date)] + if kept.empty: + return ( + f"" + ) + data = kept + # Convert to CSV string for consistency with other functions csv_string = data.to_csv()