fix(dataflows): bound insider filings and prediction markets by the trade date

- insider transactions are filtered to filings on or before the run date
- prediction-market odds are withheld from a historical run
This commit is contained in:
Yijia-Xiao
2026-09-15 00:00:26 +00:00
parent 29e331a9af
commit fadc698e20
6 changed files with 123 additions and 6 deletions

View File

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

View File

@@ -53,6 +53,7 @@ def get_global_news(
@tool @tool
def get_insider_transactions( def get_insider_transactions(
ticker: Annotated[str, "ticker symbol"], ticker: Annotated[str, "ticker symbol"],
trade_date: Annotated[str, InjectedState("trade_date")] = "",
) -> str: ) -> str:
""" """
Retrieve insider transaction information about a company. Retrieve insider transaction information about a company.
@@ -62,4 +63,4 @@ def get_insider_transactions(
Returns: Returns:
str: A report of insider transaction data 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)

View File

@@ -1,6 +1,7 @@
from typing import Annotated from typing import Annotated
from langchain_core.tools import tool from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
from tradingagents.dataflows.interface import route_to_vendor from tradingagents.dataflows.interface import route_to_vendor
@@ -13,6 +14,7 @@ def get_prediction_markets(
"'US election', or a sector/company event.", "'US election', or a sector/company event.",
], ],
limit: Annotated[int | None, "Max markets to return; omit for a default of 6"] = None, limit: Annotated[int | None, "Max markets to return; omit for a default of 6"] = None,
trade_date: Annotated[str, InjectedState("trade_date")] = "",
) -> str: ) -> str:
""" """
Retrieve live, market-implied probabilities for forward-looking events from Retrieve live, market-implied probabilities for forward-looking events from
@@ -28,4 +30,4 @@ def get_prediction_markets(
Returns: Returns:
str: A formatted markdown report of matching prediction markets 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)

View File

@@ -1,3 +1,5 @@
import json
from .alpha_vantage_common import _make_api_request, format_datetime_for_api 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) 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. """Returns latest and historical insider transactions by key stakeholders.
Covers transactions by founders, executives, board members, etc. Covers transactions by founders, executives, board members, etc.
Args: Args:
symbol: Ticker symbol. Example: "IBM". symbol: Ticker symbol. Example: "IBM".
curr_date: When given, only transactions on or before it (yyyy-mm-dd).
Returns: Returns:
Dictionary containing insider transaction data or JSON string. Dictionary containing insider transaction data or JSON string.
@@ -69,4 +72,9 @@ def get_insider_transactions(symbol: str) -> dict[str, str] | str:
"symbol": symbol, "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)

View File

@@ -15,6 +15,8 @@ from datetime import datetime, timezone
import requests import requests
from .utils import get_current_date
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
GAMMA_BASE = "https://gamma-api.polymarket.com" 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. """Return live prediction-market probabilities for an event topic.
Args: Args:
@@ -73,12 +75,20 @@ def get_prediction_markets(topic: str, limit: int | None = None) -> str:
"US election", or a sector/company event. "US election", or a sector/company event.
limit: Max markets to return (ranked by traded volume); ``None`` uses limit: Max markets to return (ranked by traded volume); ``None`` uses
DEFAULT_LIMIT. DEFAULT_LIMIT.
curr_date: The analysis date. Polymarket serves only live odds, so a
date before today withholds them.
Returns: Returns:
A markdown report of the most-traded open markets matching the topic, A markdown report of the most-traded open markets matching the topic,
each with its implied probability, traded volume, resolution date, and each with its implied probability, traded volume, resolution date, and
recent (1-week) move. 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: if limit is None:
limit = DEFAULT_LIMIT limit = DEFAULT_LIMIT

View File

@@ -455,7 +455,8 @@ def get_income_statement(
def get_insider_transactions( 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.""" """Get insider transactions data from yfinance."""
canonical = normalize_symbol(ticker) canonical = normalize_symbol(ticker)
@@ -468,6 +469,16 @@ def get_insider_transactions(
if data is None or data.empty: if data is None or data.empty:
return f"No insider transactions reported for symbol '{canonical}'" 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"<insider transactions unavailable for {canonical} as of {curr_date}: "
f"Yahoo serves recent filings only (coverage starts {filed.min():%Y-%m-%d})>"
)
data = kept
# Convert to CSV string for consistency with other functions # Convert to CSV string for consistency with other functions
csv_string = data.to_csv() csv_string = data.to_csv()