mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 11:15:24 +03:00
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:
85
tests/test_undated_tools_as_of.py
Normal file
85
tests/test_undated_tools_as_of.py
Normal 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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user