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:
Yijia-Xiao
2026-06-14 06:30:43 +00:00
parent ddfb840ecf
commit db059034a2
8 changed files with 321 additions and 2 deletions

129
tests/test_polymarket.py Normal file
View 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()

View File

@@ -5,6 +5,7 @@ from tradingagents.agents.utils.agent_utils import (
get_language_instruction,
get_macro_indicators,
get_news,
get_prediction_markets,
)
from tradingagents.dataflows.config import get_config
@@ -20,10 +21,11 @@ def create_news_analyst(llm):
get_news,
get_global_news,
get_macro_indicators,
get_prediction_markets,
]
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."""
+ get_language_instruction()
)

View File

@@ -26,6 +26,9 @@ from tradingagents.agents.utils.news_data_tools import (
from tradingagents.agents.utils.macro_data_tools import (
get_macro_indicators
)
from tradingagents.agents.utils.prediction_markets_tools import (
get_prediction_markets
)
from tradingagents.agents.utils.market_data_validation_tools import (
get_verified_market_snapshot
)

View 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)

View File

@@ -25,6 +25,7 @@ from .alpha_vantage import (
)
from .alpha_vantage_common import AlphaVantageRateLimitError
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
# Configuration and routing logic
@@ -68,12 +69,19 @@ TOOLS_CATEGORIES = {
"tools": [
"get_macro_indicators",
]
},
"prediction_markets": {
"description": "Market-implied probabilities for forward-looking events",
"tools": [
"get_prediction_markets",
]
}
}
VENDOR_LIST = [
"yfinance",
"fred",
"polymarket",
"alpha_vantage",
]
@@ -123,6 +131,10 @@ VENDOR_METHODS = {
"get_macro_indicators": {
"fred": get_fred_macro_data,
},
# prediction_markets
"get_prediction_markets": {
"polymarket": get_polymarket_prediction_markets,
},
}
def get_category_for_method(method: str) -> str:

View 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"

View File

@@ -107,6 +107,7 @@ DEFAULT_CONFIG = _apply_env_overrides({
"fundamental_data": "yfinance", # Options: alpha_vantage, yfinance
"news_data": "yfinance", # Options: alpha_vantage, yfinance
"macro_data": "fred", # Options: fred (needs FRED_API_KEY)
"prediction_markets": "polymarket", # Options: polymarket (keyless)
},
# Tool-level configuration (takes precedence over category-level)
"tool_vendors": {

View File

@@ -40,7 +40,8 @@ from tradingagents.agents.utils.agent_utils import (
get_news,
get_insider_transactions,
get_global_news,
get_macro_indicators
get_macro_indicators,
get_prediction_markets
)
from .checkpointer import checkpoint_step, clear_checkpoint, get_checkpointer, thread_id
@@ -194,6 +195,7 @@ class TradingAgentsGraph:
get_global_news,
get_insider_transactions,
get_macro_indicators,
get_prediction_markets,
]
),
"fundamentals": ToolNode(