390 lines
14 KiB
Markdown
390 lines
14 KiB
Markdown
# TradingAgents → Polymarket Prediction Agent Design
|
|
|
|
## Overview
|
|
|
|
Fork the TradingAgents multi-agent financial analysis framework to analyze Polymarket prediction markets. The system accepts any event category (politics, crypto, sports, economics, etc.), produces a structured YES/NO/SKIP recommendation with confidence and edge calculations, and outputs analysis reports.
|
|
|
|
**Phase 1**: Analysis and reporting only (human executes bets manually).
|
|
**Phase 2** (future): Automated betting via Relayer API.
|
|
|
|
## Approach
|
|
|
|
Incremental replacement (Approach A). Modify the existing codebase layer by layer:
|
|
|
|
1. Data layer (tool functions → Polymarket API)
|
|
2. Agent prompts (stock → prediction market context)
|
|
3. Output format (BUY/SELL/HOLD → YES/NO/SKIP + structured JSON)
|
|
4. CLI (ticker input → event URL/scan)
|
|
|
|
---
|
|
|
|
## 1. Data Layer
|
|
|
|
Replace `tradingagents/agents/utils/agent_utils.py` tool functions with Polymarket API calls.
|
|
|
|
### Tool Mapping
|
|
|
|
| Old Tool | New Tool | API Source | Description |
|
|
|---|---|---|---|
|
|
| `get_stock_data` | `get_market_data` | Gamma + CLOB | Event/market metadata, current price, spread, volume |
|
|
| `get_indicators` | `get_price_history` | CLOB `prices-history` | Price timeseries with self-computed trend indicators |
|
|
| `get_news` | `get_event_news` | Tavily API | Event keyword-based web search |
|
|
| `get_global_news` | `get_global_news` | Tavily API | Macro news (similar to existing) |
|
|
| `get_insider_transactions` | `get_whale_activity` | Data API `holders` + `trades` | Whale/smart money position changes |
|
|
| `get_fundamentals` | `get_event_details` | Gamma `events/{id}` | Resolution criteria, deadline, description, resolutionSource |
|
|
| `get_balance_sheet` | `get_orderbook` | CLOB `book` | Orderbook depth (bid/ask distribution) |
|
|
| `get_cashflow` | `get_market_stats` | Data API `openInterest` + Gamma | OI, volume trends, liquidity |
|
|
| `get_income_statement` | `get_leaderboard_signals` | Data API `leaderboard` | Top trader performance + market positions |
|
|
|
|
### New Tools
|
|
|
|
| Tool | API | Description |
|
|
|---|---|---|
|
|
| `get_social_sentiment` | X API / Reddit API | Social sentiment for event keywords |
|
|
| `search_markets` | Gamma `events` + filters | Auto-scan: discover events by volume, deadline, category |
|
|
|
|
### Data Flow
|
|
|
|
```
|
|
User input (event URL/ID or auto-scan)
|
|
↓
|
|
search_markets or URL parsing → event_id, token_ids extraction
|
|
↓
|
|
Each Analyst receives tool access for that event
|
|
```
|
|
|
|
### Multi-Market Events
|
|
|
|
Polymarket events can contain multiple markets (e.g., "Who will win?" has YES/NO tokens per candidate). Handling:
|
|
- Single-market events: Analyze directly.
|
|
- Multi-market events: CLI displays the list of markets within the event, user selects one specific market to analyze. Each market is a binary YES/NO outcome.
|
|
|
|
### Error Handling
|
|
|
|
Polymarket APIs (Gamma, CLOB, Data) can fail due to rate limits, downtime, or unexpected data.
|
|
- Retry with exponential backoff (max 3 retries) for transient failures.
|
|
- Timeout: 30s per API call.
|
|
- If a tool fails after retries, the analyst produces a partial report noting which data was unavailable.
|
|
|
|
---
|
|
|
|
## 2. Agent Structure
|
|
|
|
### Phase 1: 4 Analysts (Parallel)
|
|
|
|
| Agent | Tools | Output Field | Prompt Focus |
|
|
|---|---|---|---|
|
|
| **Odds Analyst** | `get_market_data`, `get_price_history`, `get_orderbook` | `odds_report` | Price trends, orderbook asymmetry, volume patterns, smart money direction |
|
|
| **News Analyst** | `get_event_news`, `get_global_news` | `news_report` | Latest event-related news, macro impact, timeline analysis |
|
|
| **Social Analyst** | `get_social_sentiment`, `get_whale_activity` | `sentiment_report` | Social opinion + whale/top trader position direction |
|
|
| **Event Analyst** | `get_event_details`, `get_market_stats`, `get_leaderboard_signals` | `event_report` | Resolution criteria interpretation, deadline, base probability estimation, similar event comparison |
|
|
|
|
### Phase 2: 3-Way Debate (Sequential)
|
|
|
|
Expanded from Bull/Bear 2-way to YES/NO/Timing 3-way debate.
|
|
|
|
| Agent | Role | Memory |
|
|
|---|---|---|
|
|
| **YES Advocate** | Argues for event occurrence. Based on existing Bull. | Yes |
|
|
| **NO Advocate** | Argues against event occurrence. Based on existing Bear. | Yes |
|
|
| **Timing Advocate** | "Might be right, but not now" — market timing / probability distortion perspective | Yes |
|
|
| **Research Manager** | Judges 3-way debate → `investment_plan` (YES/NO/SKIP + confidence) | Yes |
|
|
|
|
Debate flow:
|
|
```
|
|
YES → NO → Timing → YES → NO → Timing → ... → Judge
|
|
```
|
|
|
|
### Phase 3: Trader
|
|
|
|
Input: `investment_plan` + 4 analyst reports.
|
|
Output: `trader_plan` with YES/NO/SKIP + confidence + edge + position_size.
|
|
|
|
### Phase 4: Risk Debate (Existing structure retained)
|
|
|
|
Aggressive/Conservative/Neutral 3-way debate → Risk Manager judgment.
|
|
Prompts modified from stock to prediction market context only.
|
|
|
|
Final output: `final_decision` in JSON format.
|
|
|
|
---
|
|
|
|
## 3. AgentState Changes
|
|
|
|
```python
|
|
class InvestDebateState(TypedDict):
|
|
yes_history: str # was bull_history
|
|
no_history: str # was bear_history
|
|
timing_history: str # new
|
|
history: str
|
|
current_yes_response: str # per-advocate responses (mirrors RiskDebateState)
|
|
current_no_response: str
|
|
current_timing_response: str
|
|
latest_speaker: str # new — mirrors RiskDebateState pattern
|
|
judge_decision: str
|
|
count: int
|
|
|
|
class RiskDebateState(TypedDict):
|
|
# unchanged — Aggressive/Conservative/Neutral retained
|
|
aggressive_history: str
|
|
conservative_history: str
|
|
neutral_history: str
|
|
history: str
|
|
latest_speaker: str
|
|
current_aggressive_response: str
|
|
current_conservative_response: str
|
|
current_neutral_response: str
|
|
judge_decision: str
|
|
count: int
|
|
|
|
class AgentState(MessagesState):
|
|
event_id: str # was company_of_interest
|
|
event_question: str # new — "Will X happen?"
|
|
trade_date: str
|
|
sender: str
|
|
odds_report: str # was market_report
|
|
sentiment_report: str # retained
|
|
news_report: str # retained
|
|
event_report: str # was fundamentals_report
|
|
investment_debate_state: InvestDebateState
|
|
investment_plan: str
|
|
trader_plan: str # was trader_investment_plan
|
|
risk_debate_state: RiskDebateState
|
|
final_decision: str # was final_trade_decision
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Graph Workflow Changes
|
|
|
|
### setup.py — Node composition
|
|
|
|
```
|
|
Old: Market → Social → News → Fundamentals (parallel)
|
|
→ Bull ↔ Bear (2-way cycle) → Research Manager
|
|
|
|
New: Odds → Social → News → Event (parallel)
|
|
→ YES ↔ NO ↔ Timing (3-way cycle) → Research Manager
|
|
```
|
|
|
|
### conditional_logic.py — Debate routing
|
|
|
|
Mirrors the existing risk debate pattern using `latest_speaker` inspection (not count-modulo).
|
|
Rename analyst methods: `should_continue_market` → `should_continue_odds`, `should_continue_fundamentals` → `should_continue_event`.
|
|
|
|
```python
|
|
def should_continue_debate(self, state): # method on ConditionalLogic class
|
|
count = state["investment_debate_state"]["count"]
|
|
if count >= 3 * self.max_debate_rounds: # 3 speakers per round
|
|
return "Research Manager"
|
|
latest = state["investment_debate_state"].get("latest_speaker", "")
|
|
if latest.startswith("YES"):
|
|
return "NO Advocate"
|
|
elif latest.startswith("NO"):
|
|
return "Timing Advocate"
|
|
else: # Initial entry or after Timing → start with YES
|
|
return "YES Advocate"
|
|
```
|
|
|
|
### setup.py — 3-Way debate edge definitions
|
|
|
|
Each debater node gets conditional edges to the next speaker or Research Manager:
|
|
|
|
```python
|
|
# Last analyst's Msg Clear → YES Advocate (was Bull Researcher)
|
|
graph.add_edge(f"{last_analyst}_clear", "YES Advocate")
|
|
|
|
# YES Advocate → {NO Advocate, Research Manager}
|
|
graph.add_conditional_edges("YES Advocate", should_continue_debate,
|
|
{"NO Advocate": "NO Advocate", "Research Manager": "Research Manager"})
|
|
|
|
# NO Advocate → {Timing Advocate, Research Manager}
|
|
graph.add_conditional_edges("NO Advocate", should_continue_debate,
|
|
{"Timing Advocate": "Timing Advocate", "Research Manager": "Research Manager"})
|
|
|
|
# Timing Advocate → {YES Advocate, Research Manager}
|
|
graph.add_conditional_edges("Timing Advocate", should_continue_debate,
|
|
{"YES Advocate": "YES Advocate", "Research Manager": "Research Manager"})
|
|
```
|
|
|
|
### 3-Way advocate state preservation
|
|
|
|
Each advocate node reads all three histories but only writes its own. Pseudocode:
|
|
|
|
```python
|
|
# In YES Advocate node:
|
|
def yes_node(state):
|
|
# Read: no_history, timing_history, yes_history (for context)
|
|
# Write: yes_history (append), current_response, latest_speaker = "YES Advocate"
|
|
# Preserve: no_history, timing_history unchanged
|
|
|
|
# Same pattern for NO and Timing advocates
|
|
```
|
|
|
|
### propagation.py — Initial state
|
|
|
|
- `company_name` parameter → `event_id` + `event_question`
|
|
- Report field names updated: `market_report` → `odds_report`, `fundamentals_report` → `event_report`
|
|
- `InvestDebateState` gains `timing_history: ""`, `latest_speaker: ""`
|
|
- `propagate()` signature: `propagate(self, event_id, event_question, trade_date)`
|
|
|
|
### signal_processing.py — Output parsing
|
|
|
|
Old: Extract `BUY/SELL/HOLD` from text.
|
|
New: Extract structured JSON using Pydantic validation:
|
|
|
|
```python
|
|
from pydantic import BaseModel
|
|
|
|
class PredictionDecision(BaseModel):
|
|
action: Literal["YES", "NO", "SKIP"]
|
|
confidence: float # 0.0 ~ 1.0
|
|
edge: float # estimated probability - market price
|
|
position_size: float # recommended bet size
|
|
reasoning: str
|
|
time_horizon: str
|
|
```
|
|
|
|
Use `llm.with_structured_output(PredictionDecision)` where supported, fallback to JSON extraction + Pydantic parse.
|
|
|
|
### trading_graph.py — Memory/tool initialization
|
|
|
|
- `bull_memory` → `yes_memory`, `bear_memory` → `no_memory`, new `timing_memory`
|
|
- `_create_tool_nodes()` maps to new Polymarket tools
|
|
- `GraphSetup.__init__` signature updated to accept `timing_memory`
|
|
- `propagate()` updated: signature, field name references (`final_decision` not `final_trade_decision`)
|
|
- `_log_state()` updated: all field names changed (`odds_report`, `event_report`, `yes_history`, `no_history`, `timing_history`, `trader_plan`, `final_decision`)
|
|
- `reflect_and_remember()` updated: calls `reflect_yes_advocate`, `reflect_no_advocate`, `reflect_timing_advocate`
|
|
|
|
### reflection.py
|
|
|
|
- Rename methods: `reflect_bull_researcher` → `reflect_yes_advocate`, `reflect_bear_researcher` → `reflect_no_advocate`
|
|
- Add: `reflect_timing_advocate` (same pattern, new `timing_memory`)
|
|
- Update `_extract_current_situation`: `market_report` → `odds_report`, `fundamentals_report` → `event_report`
|
|
|
|
### agents/__init__.py
|
|
|
|
Update exports: remove `create_bull_researcher`, `create_bear_researcher`. Add `create_yes_advocate`, `create_no_advocate`, `create_timing_advocate`. Rename `create_market_analyst` → `create_odds_analyst`, `create_fundamentals_analyst` → `create_event_analyst`.
|
|
|
|
---
|
|
|
|
## 5. CLI Changes
|
|
|
|
### Input Flow
|
|
|
|
```
|
|
Old: Ticker → Date → Analysts → Depth → Provider → Model
|
|
New: Mode → Event → Analysts → Depth → Provider → Model
|
|
```
|
|
|
|
**Step 1: Mode Selection**
|
|
- `Manual` — Enter event URL or ID directly
|
|
- `Scan` — Auto-discover by filters (category, min volume, deadline, etc.)
|
|
|
|
**Step 2: Event Specification**
|
|
- Manual: Parse URL (`polymarket.com/event/xxx` → event_id) or direct ID input
|
|
- Scan: Call `search_markets` → select from result list
|
|
|
|
**Steps 3-6**: Same flow, but analyst keys change: `market` → `odds`, `fundamentals` → `event`. CLI constants updated:
|
|
- `ANALYST_MAPPING`: `{"odds": "Odds Analyst", "social": "Social Analyst", "news": "News Analyst", "event": "Event Analyst"}`
|
|
- `REPORT_SECTIONS`: `odds_report`, `event_report` etc.
|
|
- `ANALYST_ORDER`: `["odds", "social", "news", "event"]`
|
|
|
|
### Output Display
|
|
|
|
- `MessageBuffer` agent names and report sections updated to new names
|
|
- `FIXED_AGENTS`: Research Team adds `"Timing Advocate"` → `["YES Advocate", "NO Advocate", "Timing Advocate", "Research Manager"]`
|
|
- `REPORT_SECTIONS` field names: `odds_report`, `event_report`, `trader_plan`, `final_decision`
|
|
- Final result: Structured JSON decision + reports
|
|
|
|
### Report Saving
|
|
|
|
```
|
|
reports/{event_id}_{timestamp}/
|
|
├── 1_analysts/
|
|
│ ├── odds.md
|
|
│ ├── sentiment.md
|
|
│ ├── news.md
|
|
│ └── event.md
|
|
├── 2_research/
|
|
│ ├── yes_advocate.md
|
|
│ ├── no_advocate.md
|
|
│ ├── timing_advocate.md
|
|
│ └── manager.md
|
|
├── 3_trading/
|
|
│ └── trader.md
|
|
├── 4_risk/
|
|
│ ├── aggressive.md
|
|
│ ├── conservative.md
|
|
│ └── neutral.md
|
|
├── 5_risk_manager/
|
|
│ └── decision.json
|
|
└── complete_report.md
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Configuration & Dependencies
|
|
|
|
### default_config.py
|
|
|
|
```python
|
|
DEFAULT_CONFIG = {
|
|
# Retained
|
|
"llm_provider": "openrouter",
|
|
"deep_think_llm": "z-ai/glm-4.5-air:free",
|
|
"quick_think_llm": "nvidia/nemotron-3-nano-30b-a3b:free",
|
|
"max_debate_rounds": 1, # 1 round = 3 turns in 3-way debate
|
|
"max_risk_discuss_rounds": 1,
|
|
|
|
# New
|
|
"tavily_api_key": None, # loaded from TAVILY_API_KEY env
|
|
"twitter_bearer_token": None, # loaded from env
|
|
"reddit_client_id": None, # loaded from env
|
|
"polymarket_relayer_key": None, # Phase 2 auto-betting (unused in Phase 1)
|
|
|
|
# Auto-scan defaults
|
|
"scan_defaults": {
|
|
"min_volume_24h": 10000,
|
|
"min_liquidity": 5000,
|
|
"max_days_to_end": 30,
|
|
"categories": [], # empty = all
|
|
},
|
|
}
|
|
```
|
|
|
|
### .env Additions
|
|
|
|
```
|
|
TAVILY_API_KEY=...
|
|
TWITTER_BEARER_TOKEN=... # optional
|
|
REDDIT_CLIENT_ID=... # optional
|
|
REDDIT_CLIENT_SECRET=... # optional
|
|
```
|
|
|
|
### New Dependencies (requirements.txt)
|
|
|
|
```
|
|
py-clob-client # Polymarket CLOB API client
|
|
tavily-python # Web search
|
|
tweepy # X API (optional)
|
|
praw # Reddit API (optional)
|
|
```
|
|
|
|
Social APIs (tweepy, praw) gracefully skip when keys are absent — the corresponding analyst produces a "no social data available" report instead.
|
|
|
|
---
|
|
|
|
## 7. Reflection & Memory (Phase 1)
|
|
|
|
In Phase 1 (analysis only, no bet execution), there are no realized returns to learn from. `reflect_and_remember` is **disabled** in Phase 1. Memory will be seeded when Phase 2 (auto-betting) provides actual outcomes to reflect on. Alternatively, a post-resolution accuracy check can be added as a Phase 1.5 enhancement: after an event resolves, compare the agent's prediction against the outcome and store reflections.
|
|
|
|
---
|
|
|
|
## Non-Goals (Phase 1)
|
|
|
|
- Automated bet execution (Phase 2)
|
|
- Backtesting framework
|
|
- Multi-event portfolio optimization
|
|
- Real-time WebSocket streaming (batch analysis only)
|