docs: add Polymarket prediction agent design spec

Design spec for converting TradingAgents stock analysis framework
to Polymarket prediction market analysis. Covers data layer, 3-way
YES/NO/Timing debate structure, structured JSON output, and CLI changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
test 2026-03-21 20:44:31 +09:00
parent f362a160c3
commit d9b752f4bf
1 changed files with 389 additions and 0 deletions

View File

@ -0,0 +1,389 @@
# 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)