-
Notifications
You must be signed in to change notification settings - Fork 94
Expand file tree
/
Copy pathbot.py
More file actions
499 lines (410 loc) · 14.3 KB
/
Copy pathbot.py
File metadata and controls
499 lines (410 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
#!/usr/bin/env python3
"""
Gabagool Bot - Core Trading Bot
Purpose:
High-level trading bot that wraps ClobClient and OrderSigner
to provide a simple interface for order execution, position queries,
and wallet management.
Author: AI-Generated (adapted from discountry/polymarket-trading-bot)
Created: 2026-01-26
Modified: 2026-01-26
Source:
Adapted from: samples/discountry-base/src/bot.py
Dependencies:
- src.client (ClobClient, GammaClient)
- src.signer (OrderSigner)
- src.config (Config)
Usage:
from src.bot import TradingBot
from src.config import Config
config = Config.from_env()
bot = TradingBot(config)
bot.connect()
# Place order
result = bot.place_order(token_id, price=0.45, size=10, side='BUY')
# Get positions
positions = bot.get_open_orders()
Notes:
- This is the primary interface for trading
- Wraps lower-level client and signer modules
- Supports dry_run mode for testing
"""
import logging
from dataclasses import dataclass
from typing import Optional, Dict, Any, List
from pathlib import Path
from .client import ClobClient, GammaClient, ApiCredentials, ApiError
from .signer import OrderSigner, Order
@dataclass
class BotConfig:
"""
Configuration for the trading bot.
Attributes:
private_key: EOA private key for signing
safe_address: Polymarket Safe/Proxy wallet address
clob_api_url: CLOB API endpoint
gamma_api_url: Gamma API endpoint
chain_id: Polygon chain ID (137 for mainnet)
dry_run: If True, simulate trades without executing
creds_path: Path to store/load API credentials
"""
private_key: str
safe_address: str
clob_api_url: str = "https://clob.polymarket.com"
gamma_api_url: str = "https://gamma-api.polymarket.com"
chain_id: int = 137
dry_run: bool = False
creds_path: str = "data/api_creds.json"
class TradingBot:
"""
High-level trading bot for Polymarket.
Provides a simple interface for:
- Order placement and cancellation
- Position and order queries
- Price lookups
- Market discovery
Example:
config = BotConfig(
private_key="0x...",
safe_address="0x...",
)
bot = TradingBot(config)
bot.connect()
# Place a buy order
result = bot.place_order(
token_id="abc123",
price=0.45,
size=10,
side="BUY"
)
"""
def __init__(self, config: BotConfig):
"""
Initialize the trading bot.
Args:
config: BotConfig with wallet and API settings
"""
self.config = config
self.logger = logging.getLogger("TradingBot")
# Initialize components
self.signer = OrderSigner(config.private_key)
self.clob = ClobClient(
host=config.clob_api_url,
chain_id=config.chain_id,
funder=config.safe_address,
)
self.gamma = GammaClient(host=config.gamma_api_url)
self._connected = False
self.logger.info(
"TradingBot initialized (dry_run=%s, address=%s...)",
config.dry_run,
config.safe_address[:10] if config.safe_address else "none"
)
def connect(self) -> bool:
"""
Connect to Polymarket and authenticate.
Attempts to load cached API credentials, or derives new ones.
Returns:
True if connection successful
"""
try:
# Try to load cached credentials
creds_path = Path(self.config.creds_path)
if creds_path.exists():
self.logger.info("Loading cached API credentials...")
creds = ApiCredentials.load(str(creds_path))
if creds.is_valid():
self.clob.set_api_creds(creds)
self._connected = True
self.logger.info("Connected using cached credentials")
return True
# Derive new credentials
self.logger.info("Deriving new API credentials...")
creds = self.clob.create_or_derive_api_key(self.signer)
if creds.is_valid():
self.clob.set_api_creds(creds)
# Cache credentials
creds_path.parent.mkdir(parents=True, exist_ok=True)
creds.save(str(creds_path))
self.logger.info("Credentials cached to %s", creds_path)
self._connected = True
self.logger.info("Connected successfully")
return True
self.logger.error("Failed to derive valid credentials")
return False
except Exception as e:
self.logger.error("Connection failed: %s", e)
return False
@property
def is_connected(self) -> bool:
"""Check if bot is connected."""
return self._connected
# =========================================================================
# Order Operations
# =========================================================================
def place_order(
self,
token_id: str,
price: float,
size: float,
side: str = "BUY",
order_type: str = "GTC"
) -> Optional[Dict[str, Any]]:
"""
Place an order on Polymarket.
Args:
token_id: The token ID (YES or NO token)
price: Order price (0-1)
size: Number of shares
side: 'BUY' or 'SELL'
order_type: 'GTC' (Good Till Cancelled), 'FOK' (Fill Or Kill)
Returns:
Order response dict with order_id and status, or None if failed
"""
side = side.upper()
if self.config.dry_run:
self.logger.info(
"DRY RUN: Would place %s order: %s @ $%.3f x %.2f",
side, token_id[:16], price, size
)
return {
"orderID": "dry_run_" + token_id[:8],
"status": "SIMULATED",
"price": price,
"size": size,
"side": side,
}
try:
# Sign the order
signed = self.signer.sign_order_dict(
token_id=token_id,
price=price,
size=size,
side=side,
maker=self.config.safe_address,
)
# Submit to CLOB
result = self.clob.post_order(signed, order_type=order_type)
self.logger.info(
"Order placed: %s %s @ $%.3f x %.2f -> %s",
side, token_id[:16], price, size,
result.get("orderID", "unknown")
)
return result
except ApiError as e:
self.logger.error("Order failed: %s", e)
raise # Re-raise so callers get full error details (status_code, response_body)
except Exception as e:
self.logger.error("Unexpected error placing order: %s", e)
raise
def cancel_order(self, order_id: str) -> bool:
"""
Cancel an existing order.
Args:
order_id: The order ID to cancel
Returns:
True if cancelled successfully
"""
if self.config.dry_run:
self.logger.info("DRY RUN: Would cancel order %s", order_id)
return True
try:
result = self.clob.cancel_order(order_id)
self.logger.info("Order cancelled: %s", order_id)
return True
except ApiError as e:
self.logger.error("Cancel failed for %s: %s", order_id, e)
return False
def cancel_all_orders(self) -> Dict[str, Any]:
"""
Cancel all open orders.
Returns:
Dict with 'canceled' and 'not_canceled' lists
"""
if self.config.dry_run:
self.logger.info("DRY RUN: Would cancel all orders")
return {"canceled": [], "not_canceled": []}
try:
result = self.clob.cancel_all_orders()
canceled = result.get("canceled", [])
self.logger.info("Cancelled %d orders", len(canceled))
return result
except ApiError as e:
self.logger.error("Cancel all failed: %s", e)
return {"canceled": [], "not_canceled": [], "error": str(e)}
# =========================================================================
# Query Operations
# =========================================================================
def get_open_orders(self, market: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Get all open orders.
Args:
market: Filter by market/condition ID (optional)
Returns:
List of open order dicts
"""
try:
return self.clob.get_open_orders(market=market)
except ApiError as e:
self.logger.error("Failed to get orders: %s", e)
return []
def get_trades(
self,
token_id: Optional[str] = None,
limit: int = 100
) -> List[Dict[str, Any]]:
"""
Get recent trade history.
Args:
token_id: Filter by token (optional)
limit: Maximum number of trades
Returns:
List of trade dicts
"""
try:
return self.clob.get_trades(token_id=token_id, limit=limit)
except ApiError as e:
self.logger.error("Failed to get trades: %s", e)
return []
def get_order_book(self, token_id: str) -> Dict[str, Any]:
"""
Get order book for a token.
Args:
token_id: Market token ID
Returns:
Order book with bids and asks
"""
try:
return self.clob.get_order_book(token_id)
except ApiError as e:
self.logger.error("Failed to get orderbook: %s", e)
return {"bids": [], "asks": []}
def get_price(self, token_id: str, side: str = "BUY") -> float:
"""
Get current price for a token.
Args:
token_id: The token ID
side: 'BUY' or 'SELL'
Returns:
Current price (0-1), or 0 on error
"""
try:
return self.clob.get_price(token_id, side)
except ApiError as e:
self.logger.error("Failed to get price: %s", e)
return 0.0
def get_spread(self, token_id: str) -> Dict[str, float]:
"""
Get bid-ask spread for a token.
Args:
token_id: Market token ID
Returns:
Dict with bid, ask, spread
"""
try:
return self.clob.get_spread(token_id)
except ApiError as e:
self.logger.error("Failed to get spread: %s", e)
return {"bid": 0.0, "ask": 0.0, "spread": 0.0}
# =========================================================================
# Market Discovery (via Gamma API)
# =========================================================================
def get_markets(
self,
active: bool = True,
limit: int = 100
) -> List[Dict[str, Any]]:
"""
Get list of markets from Gamma API.
Args:
active: Include only active markets
limit: Maximum number of markets
Returns:
List of market dicts
"""
try:
return self.gamma.get_markets(active=active, limit=limit)
except ApiError as e:
self.logger.error("Failed to get markets: %s", e)
return []
def get_market(self, condition_id: str) -> Optional[Dict[str, Any]]:
"""
Get market details by condition ID.
Args:
condition_id: Market condition ID
Returns:
Market details or None
"""
try:
return self.gamma.get_market(condition_id)
except ApiError as e:
self.logger.error("Failed to get market %s: %s", condition_id, e)
return None
def search_markets(self, query: str, limit: int = 20) -> List[Dict[str, Any]]:
"""
Search markets by text query.
Args:
query: Search query (e.g., "BTC", "15 minute")
limit: Maximum results
Returns:
List of matching markets
"""
try:
return self.gamma.search_markets(query, limit=limit)
except ApiError as e:
self.logger.error("Market search failed: %s", e)
return []
# =========================================================================
# Utility Methods
# =========================================================================
def check_prices(
self,
yes_token_id: str,
no_token_id: str
) -> Dict[str, float]:
"""
Get prices for both YES and NO tokens.
Args:
yes_token_id: YES token ID
no_token_id: NO token ID
Returns:
Dict with yes_price, no_price, combined_cost, profit_margin
"""
yes_price = self.get_price(yes_token_id, "BUY")
no_price = self.get_price(no_token_id, "BUY")
combined = yes_price + no_price
profit = 1.0 - combined if combined < 1.0 else 0.0
return {
"yes_price": yes_price,
"no_price": no_price,
"combined_cost": combined,
"profit_margin": profit,
"is_arbitrage": combined < 1.0,
}
def __repr__(self) -> str:
"""String representation."""
status = "connected" if self._connected else "disconnected"
mode = "DRY RUN" if self.config.dry_run else "LIVE"
return f"TradingBot({status}, {mode})"
# Convenience function to create bot from Config
def create_bot_from_config(config: "Config") -> TradingBot:
"""
Create TradingBot from Config object.
Args:
config: Config instance
Returns:
Configured TradingBot
"""
from .config import Config
private_key = config.get_private_key()
if not private_key:
raise ValueError("POLY_PRIVATE_KEY not set in environment")
bot_config = BotConfig(
private_key=private_key,
safe_address=config.safe_address,
clob_api_url=config.clob.host,
chain_id=config.clob.chain_id,
dry_run=config.dry_run,
creds_path=str(Path(config.data_dir) / "api_creds.json"),
)
return TradingBot(bot_config)