razvan commited on
Commit
792b2a2
Β·
verified Β·
1 Parent(s): 280f851

Upload builderbrain/polymarket_client.py

Browse files
Files changed (1) hide show
  1. builderbrain/polymarket_client.py +350 -0
builderbrain/polymarket_client.py ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Polymarket API Client + Builder Code Router
3
+ ===========================================
4
+
5
+ Handles:
6
+ - Live market data ingestion (prices, liquidity, orderbook)
7
+ - Builder code registration and routing
8
+ - Order construction and execution
9
+ - Paper trading mode for hackathon demo
10
+
11
+ Builder codes earn fees on every trade routed through them.
12
+ This is the monetization layer for the intelligence agent.
13
+ """
14
+
15
+ import requests
16
+ import json
17
+ from dataclasses import dataclass, asdict
18
+ from typing import List, Dict, Optional, Any
19
+ from datetime import datetime
20
+ import hashlib
21
+
22
+
23
+ @dataclass
24
+ class PolymarketMarket:
25
+ """Simplified Polymarket market representation."""
26
+ market_id: str
27
+ slug: str
28
+ question: str
29
+ description: str
30
+ active: bool
31
+ closed: bool
32
+ best_yes_price: float # 0-1
33
+ best_no_price: float
34
+ volume_24h: float
35
+ liquidity: float
36
+ resolution_source: str
37
+ end_date: str
38
+ category: str
39
+
40
+ @property
41
+ def implied_yes_prob(self) -> float:
42
+ return self.best_yes_price
43
+
44
+ @property
45
+ def spread(self) -> float:
46
+ return abs(self.best_yes_price + self.best_no_price - 1.0)
47
+
48
+
49
+ @dataclass
50
+ class OrderIntent:
51
+ """An order ready for execution."""
52
+ market_id: str
53
+ side: str # 'BUY' or 'SELL'
54
+ outcome: str # 'YES' or 'NO'
55
+ size: float # USD notional
56
+ price: float # limit price 0-1
57
+ builder_code: Optional[str] = None
58
+ client_order_id: Optional[str] = None
59
+
60
+
61
+ @dataclass
62
+ class BuilderCode:
63
+ """A registered builder code for fee sharing."""
64
+ code: str
65
+ name: str
66
+ description: str
67
+ fee_share_bps: int # basis points earned on routed volume
68
+ total_volume_usd: float = 0.0
69
+ total_fees_earned_usd: float = 0.0
70
+ markets: List[str] = None
71
+
72
+ def __post_init__(self):
73
+ if self.markets is None:
74
+ self.markets = []
75
+
76
+
77
+ class PolymarketClient:
78
+ """
79
+ Polymarket API client with builder code support.
80
+
81
+ Uses Gamma API for market data.
82
+ Supports paper trading for hackathon demos.
83
+ """
84
+
85
+ GAMMA_API = "https://gamma-api.polymarket.com"
86
+ CLOB_API = "https://clob.polymarket.com"
87
+
88
+ def __init__(self, api_key: Optional[str] = None, paper_trade: bool = True):
89
+ self.api_key = api_key
90
+ self.paper_trade = paper_trade
91
+ self.session = requests.Session()
92
+ self.builder_codes: Dict[str, BuilderCode] = {}
93
+ self.paper_portfolio: Dict[str, Any] = {}
94
+ self.trade_history: List[Dict] = []
95
+
96
+ # ────────────────────────────── Market Data ──────────────────────────────
97
+
98
+ def fetch_markets(
99
+ self,
100
+ active: bool = True,
101
+ limit: int = 100,
102
+ category: Optional[str] = None,
103
+ ) -> List[PolymarketMarket]:
104
+ """Fetch live markets from Polymarket Gamma API."""
105
+ url = f"{self.GAMMA_API}/markets"
106
+ params = {
107
+ "active": str(active).lower(),
108
+ "limit": limit,
109
+ "sort": "volume24hr",
110
+ "order": "desc",
111
+ }
112
+ if category:
113
+ params["category"] = category
114
+
115
+ try:
116
+ resp = self.session.get(url, params=params, timeout=15)
117
+ resp.raise_for_status()
118
+ data = resp.json()
119
+
120
+ markets = []
121
+ for m in data.get("markets", data if isinstance(data, list) else []):
122
+ # Handle different API response shapes
123
+ if isinstance(m, dict):
124
+ markets.append(self._parse_market(m))
125
+
126
+ return markets
127
+ except Exception as e:
128
+ print(f"[PolymarketClient] fetch error: {e}")
129
+ return []
130
+
131
+ def _parse_market(self, raw: Dict) -> PolymarketMarket:
132
+ """Parse raw API response into PolymarketMarket."""
133
+ # Handle nested structure
134
+ market_data = raw.get("market", raw)
135
+
136
+ # Prices can be in different fields depending on API version
137
+ yes_price = market_data.get("bestYesPrice", market_data.get("yesPrice", 0.5))
138
+ no_price = market_data.get("bestNoPrice", market_data.get("noPrice", 0.5))
139
+
140
+ # Normalize prices to 0-1
141
+ if yes_price > 1:
142
+ yes_price /= 100
143
+ if no_price > 1:
144
+ no_price /= 100
145
+
146
+ return PolymarketMarket(
147
+ market_id=market_data.get("id", market_data.get("slug", "unknown")),
148
+ slug=market_data.get("slug", ""),
149
+ question=market_data.get("question", ""),
150
+ description=market_data.get("description", "")[:200],
151
+ active=market_data.get("active", True),
152
+ closed=market_data.get("closed", False),
153
+ best_yes_price=float(yes_price),
154
+ best_no_price=float(no_price),
155
+ volume_24h=float(market_data.get("volume24hr", market_data.get("volume24h", 0))),
156
+ liquidity=float(market_data.get("liquidity", market_data.get("volume", 0))),
157
+ resolution_source=market_data.get("resolutionSource", ""),
158
+ end_date=market_data.get("endDate", ""),
159
+ category=market_data.get("category", "general"),
160
+ )
161
+
162
+ def fetch_orderbook(self, market_id: str) -> Optional[Dict]:
163
+ """Fetch L2 orderbook for a market."""
164
+ url = f"{self.CLOB_API}/book/{market_id}"
165
+ try:
166
+ resp = self.session.get(url, timeout=10)
167
+ resp.raise_for_status()
168
+ return resp.json()
169
+ except Exception as e:
170
+ print(f"[PolymarketClient] orderbook error: {e}")
171
+ return None
172
+
173
+ # ────────────────────────────── Builder Codes ──────────────────────────────
174
+
175
+ def register_builder_code(
176
+ self,
177
+ code: str,
178
+ name: str,
179
+ description: str,
180
+ fee_share_bps: int = 10,
181
+ ) -> BuilderCode:
182
+ """Register a builder code for fee sharing."""
183
+ bc = BuilderCode(
184
+ code=code,
185
+ name=name,
186
+ description=description,
187
+ fee_share_bps=fee_share_bps,
188
+ )
189
+ self.builder_codes[code] = bc
190
+ return bc
191
+
192
+ def route_order(
193
+ self,
194
+ order: OrderIntent,
195
+ builder_code: Optional[str] = None,
196
+ ) -> Dict:
197
+ """
198
+ Route an order with builder code attribution.
199
+
200
+ In paper trade mode, simulates execution.
201
+ In live mode, would submit to Polymarket CLOB.
202
+ """
203
+ # Attach builder code if provided
204
+ if builder_code and builder_code in self.builder_codes:
205
+ bc = self.builder_codes[builder_code]
206
+ order.builder_code = builder_code
207
+ bc.total_volume_usd += order.size
208
+ bc.total_fees_earned_usd += order.size * bc.fee_share_bps / 10000
209
+ if order.market_id not in bc.markets:
210
+ bc.markets.append(order.market_id)
211
+
212
+ if self.paper_trade:
213
+ return self._paper_execute(order)
214
+ else:
215
+ return self._live_execute(order)
216
+
217
+ def _paper_execute(self, order: OrderIntent) -> Dict:
218
+ """Simulate order execution for hackathon demo."""
219
+ execution = {
220
+ "status": "FILLED",
221
+ "order_id": f"paper_{hashlib.sha256(json.dumps(asdict(order)).encode()).hexdigest()[:16]}",
222
+ "market_id": order.market_id,
223
+ "side": order.side,
224
+ "outcome": order.outcome,
225
+ "size_usd": order.size,
226
+ "price": order.price,
227
+ "filled_at": datetime.utcnow().isoformat(),
228
+ "builder_code": order.builder_code,
229
+ "paper_trade": True,
230
+ "fees_paid_usd": order.size * 0.002, # 20 bps
231
+ }
232
+
233
+ # Update paper portfolio
234
+ key = f"{order.market_id}_{order.outcome}"
235
+ if key not in self.paper_portfolio:
236
+ self.paper_portfolio[key] = {"size": 0, "avg_price": 0}
237
+
238
+ pos = self.paper_portfolio[key]
239
+ new_size = pos["size"] + order.size
240
+ pos["avg_price"] = (pos["size"] * pos["avg_price"] + order.size * order.price) / new_size
241
+ pos["size"] = new_size
242
+
243
+ self.trade_history.append(execution)
244
+ return execution
245
+
246
+ def _live_execute(self, order: OrderIntent) -> Dict:
247
+ """Placeholder for live execution via CLOB API."""
248
+ # TODO: Implement Polymarket CLOB signing + submission
249
+ # Requires API key + wallet integration
250
+ return {
251
+ "status": "PENDING",
252
+ "order": asdict(order),
253
+ "note": "Live execution requires CLOB API key + wallet",
254
+ }
255
+
256
+ # ────────────────────────────── Portfolio ──────────────────────────────
257
+
258
+ def get_paper_portfolio(self) -> Dict:
259
+ """Get current paper trading portfolio."""
260
+ return {
261
+ "positions": self.paper_portfolio,
262
+ "trade_count": len(self.trade_history),
263
+ "total_volume": sum(t["size_usd"] for t in self.trade_history),
264
+ "total_fees": sum(t.get("fees_paid_usd", 0) for t in self.trade_history),
265
+ "builder_codes": {
266
+ code: {
267
+ "volume": bc.total_volume_usd,
268
+ "fees_earned": bc.total_fees_earned_usd,
269
+ "markets": bc.markets,
270
+ }
271
+ for code, bc in self.builder_codes.items()
272
+ },
273
+ }
274
+
275
+ def get_trade_history(self, limit: int = 50) -> List[Dict]:
276
+ """Get recent trade history."""
277
+ return self.trade_history[-limit:]
278
+
279
+
280
+ class BuilderCodeRouter:
281
+ """
282
+ Intelligent builder code selection and routing.
283
+
284
+ Routes orders to the most appropriate builder code based on:
285
+ - Market category alignment
286
+ - Fee share optimization
287
+ - Volume tracking for leaderboard
288
+ """
289
+
290
+ def __init__(self, client: PolymarketClient):
291
+ self.client = client
292
+ self.category_map: Dict[str, str] = {}
293
+
294
+ def register_category_mapping(self, category: str, builder_code: str):
295
+ """Map a market category to a builder code."""
296
+ self.category_map[category] = builder_code
297
+
298
+ def select_builder_code(
299
+ self,
300
+ market: PolymarketMarket,
301
+ strategy: str = "max_fee_share",
302
+ ) -> Optional[str]:
303
+ """Select best builder code for a market."""
304
+ if strategy == "category_match":
305
+ return self.category_map.get(market.category)
306
+
307
+ elif strategy == "max_fee_share":
308
+ # Route to builder code with highest fee share
309
+ # (incentivizes registering competitive codes)
310
+ best = None
311
+ best_bps = 0
312
+ for code, bc in self.client.builder_codes.items():
313
+ if bc.fee_share_bps > best_bps:
314
+ best = code
315
+ best_bps = bc.fee_share_bps
316
+ return best
317
+
318
+ elif strategy == "volume_leader":
319
+ # Route to builder with most volume (social proof)
320
+ best = None
321
+ best_vol = 0
322
+ for code, bc in self.client.builder_codes.items():
323
+ if bc.total_volume_usd > best_vol:
324
+ best = code
325
+ best_vol = bc.total_volume_usd
326
+ return best
327
+
328
+ return None
329
+
330
+ def route_with_intent(
331
+ self,
332
+ market: PolymarketMarket,
333
+ side: str,
334
+ outcome: str,
335
+ size_usd: float,
336
+ price: float,
337
+ ) -> Dict:
338
+ """Full routing pipeline: select builder code + execute."""
339
+ builder_code = self.select_builder_code(market)
340
+
341
+ order = OrderIntent(
342
+ market_id=market.market_id,
343
+ side=side,
344
+ outcome=outcome,
345
+ size=size_usd,
346
+ price=price,
347
+ builder_code=builder_code,
348
+ )
349
+
350
+ return self.client.route_order(order, builder_code)