mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-06-17 05:16:14 +03:00
feat(data): add Polymarket prediction markets as a keyless vendor
Surface live, market-implied probabilities for forward-looking events (Fed decisions, recession, elections, geopolitics, crypto) to the news analyst via a new get_prediction_markets tool and a prediction_markets vendor category. Backed by Polymarket's public Gamma API (no key). Results are filtered to open, forward-looking markets (closed and past-dated events excluded), ranked by traded volume, and rendered with implied probability, volume, resolution date, and the recent move. External errors degrade to a clear unavailable message rather than interrupting the analyst.
This commit is contained in:
129
tests/test_polymarket.py
Normal file
129
tests/test_polymarket.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"""Polymarket prediction-market vendor: forward-looking filtering, volume
|
||||||
|
ranking, formatting, graceful degradation, and router integration.
|
||||||
|
|
||||||
|
All API access is mocked, so these run without a network connection.
|
||||||
|
"""
|
||||||
|
import copy
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import tradingagents.dataflows.config as config_module
|
||||||
|
import tradingagents.default_config as default_config
|
||||||
|
from tradingagents.dataflows import interface, polymarket
|
||||||
|
from tradingagents.dataflows.config import set_config
|
||||||
|
|
||||||
|
|
||||||
|
def _market(question, prob, *, volume, end_date, closed=False, wk=None):
|
||||||
|
return {
|
||||||
|
"question": question,
|
||||||
|
"outcomes": '["Yes", "No"]',
|
||||||
|
"outcomePrices": f'["{prob}", "{round(1 - prob, 4)}"]',
|
||||||
|
"volumeNum": volume,
|
||||||
|
"endDate": end_date,
|
||||||
|
"closed": closed,
|
||||||
|
"oneWeekPriceChange": wk,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# One event with a mix: a high-volume open market, a closed one, a past-dated
|
||||||
|
# one, and a lower-volume open one. Far-future / far-past dates keep the test
|
||||||
|
# independent of the real clock.
|
||||||
|
_SEARCH = {
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"markets": [
|
||||||
|
_market("Open big?", 0.76, volume=5_000_000, end_date="2030-12-31T00:00:00Z", wk=-0.045),
|
||||||
|
_market("Resolved already?", 1.0, volume=9_000_000, end_date="2030-12-31T00:00:00Z", closed=True),
|
||||||
|
_market("Past event?", 0.5, volume=8_000_000, end_date="2020-01-01T00:00:00Z"),
|
||||||
|
_market("Open small?", 0.30, volume=1_000, end_date="2030-06-30T00:00:00Z"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class PolymarketFilterTests(unittest.TestCase):
|
||||||
|
def test_closed_and_past_markets_are_excluded(self):
|
||||||
|
with mock.patch.object(polymarket, "_request", return_value=_SEARCH):
|
||||||
|
out = polymarket.get_prediction_markets("anything", limit=10)
|
||||||
|
self.assertIn("Open big?", out)
|
||||||
|
self.assertIn("Open small?", out)
|
||||||
|
self.assertNotIn("Resolved already?", out) # closed
|
||||||
|
self.assertNotIn("Past event?", out) # endDate in the past
|
||||||
|
|
||||||
|
def test_ranked_by_volume(self):
|
||||||
|
with mock.patch.object(polymarket, "_request", return_value=_SEARCH):
|
||||||
|
out = polymarket.get_prediction_markets("anything", limit=10)
|
||||||
|
self.assertLess(out.index("Open big?"), out.index("Open small?"))
|
||||||
|
|
||||||
|
def test_limit_caps_results(self):
|
||||||
|
with mock.patch.object(polymarket, "_request", return_value=_SEARCH):
|
||||||
|
out = polymarket.get_prediction_markets("anything", limit=1)
|
||||||
|
self.assertIn("Open big?", out)
|
||||||
|
self.assertNotIn("Open small?", out)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class PolymarketFormatTests(unittest.TestCase):
|
||||||
|
def test_probability_volume_and_weekly_change_render(self):
|
||||||
|
with mock.patch.object(polymarket, "_request", return_value=_SEARCH):
|
||||||
|
out = polymarket.get_prediction_markets("anything", limit=10)
|
||||||
|
self.assertIn("Yes 76%", out)
|
||||||
|
self.assertIn("$5,000,000 volume", out)
|
||||||
|
self.assertIn("resolves 2030-12-31", out)
|
||||||
|
self.assertIn("1-week -4.5pp", out) # -0.045 -> -4.5pp
|
||||||
|
|
||||||
|
def test_weekly_change_omitted_when_absent(self):
|
||||||
|
# "Open small?" has wk=None -> no 1-week clause on its line.
|
||||||
|
with mock.patch.object(polymarket, "_request", return_value=_SEARCH):
|
||||||
|
out = polymarket.get_prediction_markets("anything", limit=10)
|
||||||
|
small_line = next(ln for ln in out.splitlines() if "Open small?" in ln)
|
||||||
|
self.assertNotIn("1-week", small_line)
|
||||||
|
|
||||||
|
def test_no_matches_reports_clearly(self):
|
||||||
|
with mock.patch.object(polymarket, "_request", return_value={"events": []}):
|
||||||
|
out = polymarket.get_prediction_markets("obscure ticker", limit=6)
|
||||||
|
self.assertIn("No open prediction markets", out)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class PolymarketResilienceTests(unittest.TestCase):
|
||||||
|
def test_network_error_degrades_gracefully(self):
|
||||||
|
# An external-service hiccup must not raise into the analyst.
|
||||||
|
with mock.patch.object(
|
||||||
|
polymarket, "_request", side_effect=requests.RequestException("boom")
|
||||||
|
):
|
||||||
|
out = polymarket.get_prediction_markets("Fed rate cut")
|
||||||
|
self.assertIn("unavailable", out.lower())
|
||||||
|
self.assertIn("Fed rate cut", out)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class PolymarketRoutingTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
config_module._config = copy.deepcopy(default_config.DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
config_module._config = copy.deepcopy(default_config.DEFAULT_CONFIG)
|
||||||
|
|
||||||
|
def test_category_routes_to_polymarket(self):
|
||||||
|
self.assertEqual(
|
||||||
|
interface.get_category_for_method("get_prediction_markets"),
|
||||||
|
"prediction_markets",
|
||||||
|
)
|
||||||
|
set_config({"data_vendors": {"prediction_markets": "polymarket"}})
|
||||||
|
with mock.patch.dict(
|
||||||
|
interface.VENDOR_METHODS,
|
||||||
|
{"get_prediction_markets": {"polymarket": lambda *a, **k: "POLY_OK"}},
|
||||||
|
clear=False,
|
||||||
|
):
|
||||||
|
out = interface.route_to_vendor("get_prediction_markets", "fed", 5)
|
||||||
|
self.assertEqual(out, "POLY_OK")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -5,6 +5,7 @@ from tradingagents.agents.utils.agent_utils import (
|
|||||||
get_language_instruction,
|
get_language_instruction,
|
||||||
get_macro_indicators,
|
get_macro_indicators,
|
||||||
get_news,
|
get_news,
|
||||||
|
get_prediction_markets,
|
||||||
)
|
)
|
||||||
from tradingagents.dataflows.config import get_config
|
from tradingagents.dataflows.config import get_config
|
||||||
|
|
||||||
@@ -20,10 +21,11 @@ def create_news_analyst(llm):
|
|||||||
get_news,
|
get_news,
|
||||||
get_global_news,
|
get_global_news,
|
||||||
get_macro_indicators,
|
get_macro_indicators,
|
||||||
|
get_prediction_markets,
|
||||||
]
|
]
|
||||||
|
|
||||||
system_message = (
|
system_message = (
|
||||||
f"You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for {asset_label}-specific or targeted news searches, get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news, and get_macro_indicators(indicator, curr_date, look_back_days) to ground macro commentary in actual data from FRED (e.g. 'cpi', 'core_pce', 'unemployment', 'fed_funds_rate', '10y_treasury', 'yield_curve'). Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
|
f"You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for {asset_label}-specific or targeted news searches, get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news, get_macro_indicators(indicator, curr_date, look_back_days) to ground macro commentary in actual data from FRED (e.g. 'cpi', 'core_pce', 'unemployment', 'fed_funds_rate', '10y_treasury', 'yield_curve'), and get_prediction_markets(topic, limit) for live market-implied probabilities of forward-looking events (e.g. 'Fed rate cut', 'recession 2026', geopolitical or sector events). Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
|
||||||
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
|
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
|
||||||
+ get_language_instruction()
|
+ get_language_instruction()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ from tradingagents.agents.utils.news_data_tools import (
|
|||||||
from tradingagents.agents.utils.macro_data_tools import (
|
from tradingagents.agents.utils.macro_data_tools import (
|
||||||
get_macro_indicators
|
get_macro_indicators
|
||||||
)
|
)
|
||||||
|
from tradingagents.agents.utils.prediction_markets_tools import (
|
||||||
|
get_prediction_markets
|
||||||
|
)
|
||||||
from tradingagents.agents.utils.market_data_validation_tools import (
|
from tradingagents.agents.utils.market_data_validation_tools import (
|
||||||
get_verified_market_snapshot
|
get_verified_market_snapshot
|
||||||
)
|
)
|
||||||
|
|||||||
31
tradingagents/agents/utils/prediction_markets_tools.py
Normal file
31
tradingagents/agents/utils/prediction_markets_tools.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
|
||||||
|
from tradingagents.dataflows.interface import route_to_vendor
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_prediction_markets(
|
||||||
|
topic: Annotated[
|
||||||
|
str,
|
||||||
|
"Event topic/keyword, e.g. 'Fed rate cut', 'recession 2026', "
|
||||||
|
"'US election', or a sector/company event.",
|
||||||
|
],
|
||||||
|
limit: Annotated[int | None, "Max markets to return; omit for a default of 6"] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Retrieve live, market-implied probabilities for forward-looking events from
|
||||||
|
prediction markets (Polymarket): Fed decisions, recession, elections,
|
||||||
|
geopolitics, crypto. Returns the most-traded open markets matching the
|
||||||
|
topic, each with its implied probability, traded volume, resolution date,
|
||||||
|
and recent move. Uses the configured prediction_markets vendor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic (str): Event keyword(s) to search
|
||||||
|
limit (int): Max markets to return; omit for a default of 6
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A formatted markdown report of matching prediction markets
|
||||||
|
"""
|
||||||
|
return route_to_vendor("get_prediction_markets", topic, limit)
|
||||||
@@ -25,6 +25,7 @@ from .alpha_vantage import (
|
|||||||
)
|
)
|
||||||
from .alpha_vantage_common import AlphaVantageRateLimitError
|
from .alpha_vantage_common import AlphaVantageRateLimitError
|
||||||
from .fred import get_macro_data as get_fred_macro_data
|
from .fred import get_macro_data as get_fred_macro_data
|
||||||
|
from .polymarket import get_prediction_markets as get_polymarket_prediction_markets
|
||||||
from .symbol_utils import NoMarketDataError
|
from .symbol_utils import NoMarketDataError
|
||||||
|
|
||||||
# Configuration and routing logic
|
# Configuration and routing logic
|
||||||
@@ -68,12 +69,19 @@ TOOLS_CATEGORIES = {
|
|||||||
"tools": [
|
"tools": [
|
||||||
"get_macro_indicators",
|
"get_macro_indicators",
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"prediction_markets": {
|
||||||
|
"description": "Market-implied probabilities for forward-looking events",
|
||||||
|
"tools": [
|
||||||
|
"get_prediction_markets",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VENDOR_LIST = [
|
VENDOR_LIST = [
|
||||||
"yfinance",
|
"yfinance",
|
||||||
"fred",
|
"fred",
|
||||||
|
"polymarket",
|
||||||
"alpha_vantage",
|
"alpha_vantage",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -123,6 +131,10 @@ VENDOR_METHODS = {
|
|||||||
"get_macro_indicators": {
|
"get_macro_indicators": {
|
||||||
"fred": get_fred_macro_data,
|
"fred": get_fred_macro_data,
|
||||||
},
|
},
|
||||||
|
# prediction_markets
|
||||||
|
"get_prediction_markets": {
|
||||||
|
"polymarket": get_polymarket_prediction_markets,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_category_for_method(method: str) -> str:
|
def get_category_for_method(method: str) -> str:
|
||||||
|
|||||||
139
tradingagents/dataflows/polymarket.py
Normal file
139
tradingagents/dataflows/polymarket.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
"""Polymarket prediction-market vendor.
|
||||||
|
|
||||||
|
Surfaces live, market-implied probabilities for forward-looking events (Fed
|
||||||
|
decisions, recession, elections, geopolitics, crypto) to the news analyst, as a
|
||||||
|
complement to news (what happened) and FRED macro data (where things stand):
|
||||||
|
what the crowd actually prices to happen next.
|
||||||
|
|
||||||
|
Uses Polymarket's public Gamma API (https://gamma-api.polymarket.com) — no key,
|
||||||
|
no auth. Each market's ``outcomePrices`` are the implied probabilities of its
|
||||||
|
outcomes (a "Yes" at 0.76 means the market prices a 76% chance).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
GAMMA_BASE = "https://gamma-api.polymarket.com"
|
||||||
|
|
||||||
|
# Network timeout (seconds), consistent with the other vendors.
|
||||||
|
REQUEST_TIMEOUT = 30
|
||||||
|
|
||||||
|
# Default number of markets to return, ranked by traded volume.
|
||||||
|
DEFAULT_LIMIT = 6
|
||||||
|
|
||||||
|
|
||||||
|
def _request(path: str, params: dict) -> dict:
|
||||||
|
response = requests.get(
|
||||||
|
f"{GAMMA_BASE}/{path}", params=params, timeout=REQUEST_TIMEOUT
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_json_list(value) -> list:
|
||||||
|
"""Gamma encodes ``outcomes``/``outcomePrices`` as JSON-string arrays."""
|
||||||
|
if isinstance(value, list):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return json.loads(value)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _is_forward_looking(market: dict, now: datetime) -> bool:
|
||||||
|
"""Keep only open markets that resolve in the future.
|
||||||
|
|
||||||
|
``closed`` is the reliable resolved flag (``active`` stays True even for
|
||||||
|
settled markets), and a past ``endDate`` means the event already resolved —
|
||||||
|
either way it is not a forward-looking signal.
|
||||||
|
"""
|
||||||
|
if market.get("closed"):
|
||||||
|
return False
|
||||||
|
end_date = market.get("endDate")
|
||||||
|
if end_date:
|
||||||
|
try:
|
||||||
|
if datetime.fromisoformat(end_date.replace("Z", "+00:00")) < now:
|
||||||
|
return False
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return bool(_parse_json_list(market.get("outcomePrices"))) and bool(
|
||||||
|
_parse_json_list(market.get("outcomes"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_prediction_markets(topic: str, limit: int | None = None) -> str:
|
||||||
|
"""Return live prediction-market probabilities for an event topic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic: Event keyword(s), e.g. "Fed rate cut", "recession 2026",
|
||||||
|
"US election", or a sector/company event.
|
||||||
|
limit: Max markets to return (ranked by traded volume); ``None`` uses
|
||||||
|
DEFAULT_LIMIT.
|
||||||
|
|
||||||
|
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 limit is None:
|
||||||
|
limit = DEFAULT_LIMIT
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = _request("public-search", {"q": topic, "limit_per_type": 20})
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.warning("Polymarket search failed for %r: %s", topic, e)
|
||||||
|
return (
|
||||||
|
f"Polymarket data is currently unavailable (network error: {e}). "
|
||||||
|
f"Proceed without prediction-market signal for '{topic}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
candidates = [
|
||||||
|
m
|
||||||
|
for event in data.get("events", [])
|
||||||
|
for m in event.get("markets", [])
|
||||||
|
if _is_forward_looking(m, now)
|
||||||
|
]
|
||||||
|
candidates.sort(key=lambda m: m.get("volumeNum") or 0, reverse=True)
|
||||||
|
|
||||||
|
header = (
|
||||||
|
f'## Polymarket prediction markets: "{topic}"\n'
|
||||||
|
f"Live, market-implied probabilities (higher traded volume = deeper, "
|
||||||
|
f"more reliable). A probability is the crowd's priced odds of the event, "
|
||||||
|
f"not a forecast you should take as certain.\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return header + (
|
||||||
|
f"No open prediction markets matched '{topic}'. Polymarket coverage "
|
||||||
|
f"is concentrated in macro, political, geopolitical, and crypto "
|
||||||
|
f"events; a specific equity may have none."
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for m in candidates[:limit]:
|
||||||
|
prices = _parse_json_list(m.get("outcomePrices"))
|
||||||
|
outcomes = _parse_json_list(m.get("outcomes"))
|
||||||
|
try:
|
||||||
|
prob = float(prices[0])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
continue
|
||||||
|
label = outcomes[0] if outcomes else "Yes"
|
||||||
|
volume = m.get("volumeNum") or 0
|
||||||
|
end_date = (m.get("endDate") or "")[:10]
|
||||||
|
wk = m.get("oneWeekPriceChange")
|
||||||
|
wk_str = (
|
||||||
|
f", 1-week {wk * 100:+.1f}pp"
|
||||||
|
if isinstance(wk, (int, float)) and wk
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f"- **{m.get('question')}** — {label} {prob:.0%} "
|
||||||
|
f"(${volume:,.0f} volume, resolves {end_date}{wk_str})"
|
||||||
|
)
|
||||||
|
|
||||||
|
return header + "\n".join(lines) + "\n"
|
||||||
@@ -107,6 +107,7 @@ DEFAULT_CONFIG = _apply_env_overrides({
|
|||||||
"fundamental_data": "yfinance", # Options: alpha_vantage, yfinance
|
"fundamental_data": "yfinance", # Options: alpha_vantage, yfinance
|
||||||
"news_data": "yfinance", # Options: alpha_vantage, yfinance
|
"news_data": "yfinance", # Options: alpha_vantage, yfinance
|
||||||
"macro_data": "fred", # Options: fred (needs FRED_API_KEY)
|
"macro_data": "fred", # Options: fred (needs FRED_API_KEY)
|
||||||
|
"prediction_markets": "polymarket", # Options: polymarket (keyless)
|
||||||
},
|
},
|
||||||
# Tool-level configuration (takes precedence over category-level)
|
# Tool-level configuration (takes precedence over category-level)
|
||||||
"tool_vendors": {
|
"tool_vendors": {
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ from tradingagents.agents.utils.agent_utils import (
|
|||||||
get_news,
|
get_news,
|
||||||
get_insider_transactions,
|
get_insider_transactions,
|
||||||
get_global_news,
|
get_global_news,
|
||||||
get_macro_indicators
|
get_macro_indicators,
|
||||||
|
get_prediction_markets
|
||||||
)
|
)
|
||||||
|
|
||||||
from .checkpointer import checkpoint_step, clear_checkpoint, get_checkpointer, thread_id
|
from .checkpointer import checkpoint_step, clear_checkpoint, get_checkpointer, thread_id
|
||||||
@@ -194,6 +195,7 @@ class TradingAgentsGraph:
|
|||||||
get_global_news,
|
get_global_news,
|
||||||
get_insider_transactions,
|
get_insider_transactions,
|
||||||
get_macro_indicators,
|
get_macro_indicators,
|
||||||
|
get_prediction_markets,
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
"fundamentals": ToolNode(
|
"fundamentals": ToolNode(
|
||||||
|
|||||||
Reference in New Issue
Block a user