polytrader 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
polytrader/__init__.py ADDED
@@ -0,0 +1,85 @@
1
+ from polytrader.client import PolyTrader
2
+ from polytrader.models import (
3
+ Balance,
4
+ BestBidAsk,
5
+ Book,
6
+ Coin,
7
+ EventMessage,
8
+ LastTradePrice,
9
+ MakerOrder,
10
+ MarketEventType,
11
+ MarketResolved,
12
+ NewMarket,
13
+ OrderBookLevel,
14
+ OrderResult,
15
+ OrderResultStatus,
16
+ OrderSide,
17
+ OrderStatus,
18
+ OrderType,
19
+ Outcome,
20
+ PolymarketAuth,
21
+ PolymarketOrder,
22
+ PolymarketOrderType,
23
+ PolymarketPosition,
24
+ PolymarketTrade,
25
+ PriceChange,
26
+ PriceChangeItem,
27
+ TickSizeChange,
28
+ Timeframe,
29
+ TokenIdPair,
30
+ TraderSide,
31
+ TradeStatus,
32
+ UpDownMarket,
33
+ UpDownMarketToken,
34
+ UserEventType,
35
+ UserOrder,
36
+ UserTrade,
37
+ crypto_fee,
38
+ )
39
+ from polytrader.websocket import (
40
+ BasePolymarketWebSocket,
41
+ PolymarketMarketWebSocket,
42
+ PolymarketUserWebSocket,
43
+ )
44
+
45
+ __all__ = [
46
+ "Balance",
47
+ "BasePolymarketWebSocket",
48
+ "BestBidAsk",
49
+ "Book",
50
+ "Coin",
51
+ "EventMessage",
52
+ "LastTradePrice",
53
+ "MakerOrder",
54
+ "MarketEventType",
55
+ "MarketResolved",
56
+ "NewMarket",
57
+ "OrderBookLevel",
58
+ "OrderResult",
59
+ "OrderResultStatus",
60
+ "OrderSide",
61
+ "OrderStatus",
62
+ "OrderType",
63
+ "Outcome",
64
+ "PolymarketAuth",
65
+ "PolymarketOrder",
66
+ "PolymarketOrderType",
67
+ "PolymarketPosition",
68
+ "PolymarketTrade",
69
+ "PolyTrader",
70
+ "PolymarketMarketWebSocket",
71
+ "PolymarketUserWebSocket",
72
+ "PriceChange",
73
+ "PriceChangeItem",
74
+ "TickSizeChange",
75
+ "Timeframe",
76
+ "TokenIdPair",
77
+ "TradeStatus",
78
+ "TraderSide",
79
+ "UpDownMarket",
80
+ "UpDownMarketToken",
81
+ "UserEventType",
82
+ "UserOrder",
83
+ "UserTrade",
84
+ "crypto_fee",
85
+ ]
polytrader/client.py ADDED
@@ -0,0 +1,554 @@
1
+ import json
2
+ import logging
3
+ from datetime import UTC, datetime
4
+ from decimal import Decimal
5
+ from typing import Any, cast
6
+
7
+ import httpx
8
+ from eth_account import Account
9
+ from py_clob_client.client import ApiCreds, ClobClient, OrderBookSummary
10
+ from py_clob_client.clob_types import (
11
+ AssetType,
12
+ BalanceAllowanceParams,
13
+ MarketOrderArgs,
14
+ OpenOrderParams,
15
+ OrderArgs,
16
+ PartialCreateOrderOptions,
17
+ TradeParams,
18
+ )
19
+
20
+ from polytrader.constants import (
21
+ CHAIN_ID,
22
+ CLOB_HOST,
23
+ DATA_API_HOST,
24
+ GAMMA_API_HOST,
25
+ TOKEN_DECIMALS,
26
+ )
27
+ from polytrader.models import (
28
+ ZERO,
29
+ Balance,
30
+ Coin,
31
+ OrderResult,
32
+ OrderSide,
33
+ PolymarketAuth,
34
+ PolymarketOrder,
35
+ PolymarketOrderType,
36
+ PolymarketPosition,
37
+ PolymarketTrade,
38
+ Timeframe,
39
+ TokenIdPair,
40
+ UpDownMarket,
41
+ )
42
+ from polytrader.rpc import (
43
+ BuilderCreds as _BuilderCreds,
44
+ )
45
+ from polytrader.rpc import (
46
+ approve_all as _approve_all,
47
+ )
48
+ from polytrader.rpc import (
49
+ approve_collateral as _approve_collateral,
50
+ )
51
+ from polytrader.rpc import (
52
+ approve_token as _approve_token,
53
+ )
54
+ from polytrader.websocket import PolymarketMarketWebSocket, PolymarketUserWebSocket
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+
59
+ class PolyTrader:
60
+ """Polymarket client for API credential management and WebSocket connections"""
61
+
62
+ def __init__(
63
+ self,
64
+ private_key: str,
65
+ funder: str,
66
+ signature_type: int,
67
+ builder_key: str | None = None,
68
+ builder_secret: str | None = None,
69
+ builder_passphrase: str | None = None,
70
+ ) -> None:
71
+ self._private_key = private_key
72
+ self.funder: str = funder
73
+ self._signature_type: int = signature_type
74
+ self._builder_creds: _BuilderCreds | None = None
75
+ if builder_key and builder_secret and builder_passphrase:
76
+ self._builder_creds = _BuilderCreds(
77
+ key=builder_key, secret=builder_secret, passphrase=builder_passphrase
78
+ )
79
+ self._clob_client: ClobClient | None = None
80
+ self._auth: PolymarketAuth | None = None
81
+ self._market_ws: PolymarketMarketWebSocket | None = None
82
+ self._user_ws: PolymarketUserWebSocket | None = None
83
+ self._http = httpx.AsyncClient(timeout=10.0)
84
+
85
+ @property
86
+ def private_key(self) -> str:
87
+ """Get private key without 0x prefix"""
88
+ pk = self._private_key
89
+ return pk[2:] if pk.startswith("0x") else pk
90
+
91
+ @property
92
+ def wallet_address(self) -> str:
93
+ """Get wallet address (MetaMask) from private key"""
94
+ address = Account.from_key(self._private_key).address
95
+ if not isinstance(address, str):
96
+ raise ValueError("Invalid private key")
97
+ return address
98
+
99
+ def _get_clob_client(self) -> ClobClient:
100
+ """Get or create CLOB client"""
101
+ if self._clob_client is None:
102
+ self._clob_client = ClobClient(
103
+ CLOB_HOST,
104
+ key=self.private_key,
105
+ chain_id=CHAIN_ID,
106
+ signature_type=self._signature_type,
107
+ funder=self.funder,
108
+ )
109
+ return self._clob_client
110
+
111
+ def _get_authenticated_client(self) -> ClobClient:
112
+ """Get CLOB client with API credentials set (cached)"""
113
+ client = self._get_clob_client()
114
+ if client.creds is None:
115
+ auth = self.get_auth()
116
+ client.set_api_creds(
117
+ ApiCreds(
118
+ api_key=auth.api_key,
119
+ api_secret=auth.secret,
120
+ api_passphrase=auth.passphrase,
121
+ )
122
+ )
123
+ return client
124
+
125
+ def _derive_credentials(self) -> None:
126
+ """Derive API credentials and funder from private key"""
127
+ client = self._get_clob_client()
128
+ resp: ApiCreds = client.derive_api_key()
129
+
130
+ self._auth = PolymarketAuth(
131
+ api_key=resp.api_key,
132
+ secret=resp.api_secret,
133
+ passphrase=resp.api_passphrase,
134
+ )
135
+
136
+ logger.info(f"[POLYMARKET] Credentials derived, funder: {self.funder}")
137
+
138
+ def get_auth(self) -> PolymarketAuth:
139
+ """Get API credentials (cached)"""
140
+ if self._auth is None:
141
+ self._derive_credentials()
142
+ if self._auth is None:
143
+ raise RuntimeError("Failed to derive credentials")
144
+ return self._auth
145
+
146
+ # ========================================================================
147
+ # Market Data
148
+ # ========================================================================
149
+
150
+ @staticmethod
151
+ def _parse_token_ids(market_data: dict[str, Any]) -> TokenIdPair:
152
+ """Extract up/down token IDs from market data"""
153
+ token_ids_raw = market_data.get("clobTokenIds", "[]")
154
+ outcomes_raw = market_data.get("outcomes", "[]")
155
+
156
+ token_ids = (
157
+ json.loads(token_ids_raw)
158
+ if isinstance(token_ids_raw, str)
159
+ else token_ids_raw
160
+ )
161
+ outcomes = (
162
+ json.loads(outcomes_raw) if isinstance(outcomes_raw, str) else outcomes_raw
163
+ )
164
+
165
+ outcome_map: dict[str, str] = {}
166
+ for i, outcome in enumerate(outcomes):
167
+ if i < len(token_ids):
168
+ outcome_map[outcome.lower()] = token_ids[i]
169
+
170
+ return TokenIdPair(
171
+ up=outcome_map.get("up", ""), down=outcome_map.get("down", "")
172
+ )
173
+
174
+ async def get_updown_market(
175
+ self, coin: Coin, timeframe: Timeframe, timestamp: int
176
+ ) -> UpDownMarket:
177
+ """
178
+ Get Up/Down market by coin, timeframe, and Unix timestamp.
179
+
180
+ Args:
181
+ coin: Coin enum (BTC, ETH, SOL, XRP)
182
+ timeframe: Timeframe enum (M5, M15)
183
+ timestamp: Unix timestamp for the market period
184
+
185
+ Returns:
186
+ UpDownMarket with token IDs for Up and Down outcomes
187
+
188
+ Raises:
189
+ ValueError: If no market found for the given parameters
190
+ httpx.HTTPError: If the API request fails
191
+ """
192
+ slug = f"{coin}-updown-{timeframe}-{timestamp}"
193
+ url = f"{GAMMA_API_HOST}/markets?slug={slug}"
194
+ resp = await self._http.get(url)
195
+ resp.raise_for_status()
196
+ data = resp.json()
197
+
198
+ if not data:
199
+ raise ValueError(f"No market found for slug: {slug}")
200
+
201
+ market_data = data[0] if isinstance(data, list) else data
202
+ tokens = self._parse_token_ids(market_data)
203
+
204
+ return UpDownMarket(
205
+ coin=coin,
206
+ timeframe=timeframe,
207
+ condition_id=market_data.get("conditionId", ""),
208
+ question_id=market_data.get("questionID", ""),
209
+ slug=market_data.get("slug", slug),
210
+ title=market_data.get("question", ""),
211
+ up_token_id=tokens.up,
212
+ down_token_id=tokens.down,
213
+ end_date=datetime.fromisoformat(market_data["endDate"]),
214
+ active=market_data.get("active", False),
215
+ closed=market_data.get("closed", False),
216
+ order_price_min_tick_size=Decimal(
217
+ str(market_data.get("orderPriceMinTickSize", 0))
218
+ ),
219
+ order_min_size=Decimal(str(market_data.get("orderMinSize", 0))),
220
+ neg_risk=market_data.get("negRisk", False),
221
+ accepting_orders=market_data.get("acceptingOrders", False),
222
+ best_bid=Decimal(str(market_data.get("bestBid", 0))),
223
+ best_ask=Decimal(str(market_data.get("bestAsk", 0))),
224
+ last_trade_price=Decimal(str(market_data.get("lastTradePrice", 0))),
225
+ spread=Decimal(str(market_data.get("spread", 0))),
226
+ maker_base_fee=int(market_data.get("makerBaseFee", 0)),
227
+ taker_base_fee=int(market_data.get("takerBaseFee", 0)),
228
+ )
229
+
230
+ async def get_current_updown_market(
231
+ self, coin: Coin, timeframe: Timeframe
232
+ ) -> UpDownMarket:
233
+ """Get current Up/Down market for a coin and timeframe."""
234
+ now = datetime.now(UTC)
235
+ interval = 300 if timeframe == Timeframe.M5 else 900
236
+ rounded_ts = (int(now.timestamp()) // interval) * interval
237
+ return await self.get_updown_market(coin, timeframe, rounded_ts)
238
+
239
+ # ========================================================================
240
+ # Order Management
241
+ # ========================================================================
242
+
243
+ def create_order(
244
+ self,
245
+ token_id: str,
246
+ side: OrderSide,
247
+ price: Decimal,
248
+ size: Decimal,
249
+ tick_size: str = "0.01",
250
+ neg_risk: bool = False,
251
+ order_type: PolymarketOrderType = PolymarketOrderType.GTC,
252
+ expiration: int = 0,
253
+ post_only: bool = False,
254
+ ) -> OrderResult:
255
+ """
256
+ Create and post an order.
257
+
258
+ For limit orders (GTC, GTD): specify price and size.
259
+ For market orders (FOK, FAK): size is the dollar amount for BUY,
260
+ or number of shares for SELL. Price acts as worst-price limit.
261
+ For MARKET pseudo-type: converted to FOK with aggressive price.
262
+
263
+ Args:
264
+ token_id: The token ID (asset ID) to trade
265
+ side: BUY or SELL
266
+ price: Limit price (slippage protection for FOK/FAK)
267
+ size: Shares for limit/SELL, dollar amount for FOK/FAK BUY
268
+ tick_size: Market tick size ("0.1", "0.01", "0.001", "0.0001")
269
+ neg_risk: Whether this is a negative risk market (3+ outcomes)
270
+ order_type: GTC, GTD, FOK, FAK, or MARKET
271
+ expiration: Unix timestamp for GTD orders (add 60s security buffer)
272
+ post_only: Reject if order would match immediately (GTC/GTD only)
273
+ """
274
+ client = self._get_authenticated_client()
275
+ options = PartialCreateOrderOptions(tick_size=tick_size, neg_risk=neg_risk)
276
+
277
+ actual_order_type = order_type
278
+ if order_type == PolymarketOrderType.MARKET:
279
+ actual_order_type = PolymarketOrderType.FOK
280
+
281
+ if actual_order_type in (PolymarketOrderType.FOK, PolymarketOrderType.FAK):
282
+ # Market orders: use create_market_order
283
+ market_price = price
284
+ if order_type == PolymarketOrderType.MARKET:
285
+ market_price = (
286
+ Decimal("0.99") if side == OrderSide.BUY else Decimal("0.01")
287
+ )
288
+
289
+ market_args = MarketOrderArgs(
290
+ token_id=token_id,
291
+ amount=float(size),
292
+ side=side.value,
293
+ price=float(market_price),
294
+ )
295
+ signed_order = client.create_market_order(market_args, options)
296
+ else:
297
+ # Limit orders: GTC or GTD
298
+ order_args = OrderArgs(
299
+ token_id=token_id,
300
+ price=float(price),
301
+ size=float(size),
302
+ side=side.value,
303
+ expiration=expiration,
304
+ )
305
+ signed_order = client.create_order(order_args, options)
306
+
307
+ resp = client.post_order(
308
+ signed_order,
309
+ orderType=actual_order_type.to_clob_order_type(),
310
+ post_only=post_only
311
+ and actual_order_type
312
+ in (
313
+ PolymarketOrderType.GTC,
314
+ PolymarketOrderType.GTD,
315
+ ),
316
+ )
317
+
318
+ result = OrderResult.from_dict(resp)
319
+
320
+ logger.info(
321
+ "[POLYMARKET] Order %s: %s %s@%s type=%s id=%s",
322
+ result.status.value,
323
+ side.value,
324
+ size,
325
+ price,
326
+ order_type.value,
327
+ result.order_id,
328
+ )
329
+
330
+ return result
331
+
332
+ @staticmethod
333
+ def _extract_cancelled(resp: dict[str, Any]) -> list[str]:
334
+ """Extract cancelled order IDs from API response (handles both spellings)"""
335
+ result = resp.get("canceled", resp.get("cancelled", []))
336
+ return result if isinstance(result, list) else []
337
+
338
+ def cancel_order(self, order_id: str) -> bool:
339
+ """Cancel a specific order."""
340
+ client = self._get_authenticated_client()
341
+ resp = client.cancel(order_id)
342
+ cancelled = bool(
343
+ self._extract_cancelled(resp)
344
+ or resp.get("canceled", resp.get("cancelled", False))
345
+ )
346
+ if cancelled:
347
+ logger.info(f"[POLYMARKET] Order cancelled: {order_id}")
348
+ return cancelled
349
+
350
+ def cancel_all_orders(self) -> int:
351
+ """Cancel all open orders."""
352
+ client = self._get_authenticated_client()
353
+ resp = client.cancel_all()
354
+ cancelled_ids = self._extract_cancelled(resp)
355
+ logger.info(f"[POLYMARKET] Cancelled {len(cancelled_ids)} orders")
356
+ return len(cancelled_ids)
357
+
358
+ def cancel_orders_for_market(self, market_id: str) -> int:
359
+ """Cancel all orders for a specific market."""
360
+ client = self._get_authenticated_client()
361
+ resp = client.cancel_market_orders(market_id)
362
+ cancelled_ids = self._extract_cancelled(resp)
363
+ logger.info(
364
+ f"[POLYMARKET] Cancelled {len(cancelled_ids)} orders for market {market_id}"
365
+ )
366
+ return len(cancelled_ids)
367
+
368
+ # ========================================================================
369
+ # Order/Position Queries
370
+ # ========================================================================
371
+
372
+ def get_order(self, order_id: str) -> PolymarketOrder:
373
+ """Get a single order by ID."""
374
+ client = self._get_authenticated_client()
375
+ resp = client.get_order(order_id)
376
+ return PolymarketOrder(**resp)
377
+
378
+ def get_orders(
379
+ self,
380
+ market_id: str | None = None,
381
+ asset_id: str | None = None,
382
+ ) -> list[PolymarketOrder]:
383
+ """Get open orders, optionally filtered by market or asset."""
384
+ client = self._get_authenticated_client()
385
+ params = OpenOrderParams(market=market_id, asset_id=asset_id)
386
+ resp = client.get_orders(params)
387
+ return [PolymarketOrder(**d) for d in resp]
388
+
389
+ def get_trades(
390
+ self,
391
+ market_id: str | None = None,
392
+ asset_id: str | None = None,
393
+ ) -> list[PolymarketTrade]:
394
+ """Get trade history."""
395
+ client = self._get_authenticated_client()
396
+ params = TradeParams(market=market_id, asset_id=asset_id)
397
+ resp = client.get_trades(params)
398
+ return [PolymarketTrade(**d) for d in resp]
399
+
400
+ async def get_positions(self) -> list[PolymarketPosition]:
401
+ """Get current positions from the data API."""
402
+ url = f"{DATA_API_HOST}/positions?user={self.funder}"
403
+ resp = await self._http.get(url)
404
+ resp.raise_for_status()
405
+ data = resp.json()
406
+ return [PolymarketPosition.from_dict(pos_data) for pos_data in data]
407
+
408
+ def get_balance(self) -> Balance:
409
+ """Get USDC balance and allowance."""
410
+ client = self._get_authenticated_client()
411
+ params = BalanceAllowanceParams(
412
+ asset_type=cast(AssetType, AssetType.COLLATERAL)
413
+ )
414
+ resp = cast(dict[str, Any], client.get_balance_allowance(params))
415
+ return Balance.from_dict(resp)
416
+
417
+ def get_token_balance(self, token_id: str) -> Balance:
418
+ """Get conditional token balance and allowance."""
419
+ client = self._get_authenticated_client()
420
+ params = BalanceAllowanceParams(
421
+ asset_type=cast(AssetType, AssetType.CONDITIONAL),
422
+ token_id=token_id,
423
+ )
424
+ resp = cast(dict[str, Any], client.get_balance_allowance(params))
425
+ return Balance.from_dict(resp)
426
+
427
+ def get_orderbook(self, token_id: str) -> OrderBookSummary:
428
+ """Get orderbook for a token."""
429
+ client = self._get_clob_client()
430
+ return client.get_order_book(token_id)
431
+
432
+ # ========================================================================
433
+ # Balance & Allowance
434
+ # ========================================================================
435
+
436
+ def ensure_can_sell(
437
+ self, token_id: str, size: Decimal, neg_risk: bool = False
438
+ ) -> bool:
439
+ """
440
+ Check if a sell order is possible, auto-approving if needed.
441
+
442
+ Verifies token balance, on-chain allowance, and available (unlocked)
443
+ shares. If allowance is zero, sends an on-chain ``setApprovalForAll``
444
+ transaction and refreshes the server cache.
445
+
446
+ Args:
447
+ token_id: Conditional token to sell.
448
+ size: Number of shares to sell (human-readable, e.g. ``5.0``).
449
+ neg_risk: Whether this is a neg-risk market.
450
+
451
+ Returns:
452
+ True if sell of given size is possible.
453
+ """
454
+ token_bal = self.get_token_balance(token_id)
455
+
456
+ # Balance & allowance from the CLOB API are in raw 6-decimal units;
457
+ # *size* is human-readable (create_order multiplies by 1e6 internally).
458
+ raw_size = size * TOKEN_DECIMALS
459
+
460
+ if token_bal.balance < raw_size:
461
+ return False
462
+
463
+ if token_bal.allowance < raw_size:
464
+ self.approve_token(neg_risk)
465
+ self.refresh_token_allowance(token_id)
466
+ token_bal = self.get_token_balance(token_id)
467
+ if token_bal.allowance < raw_size:
468
+ return False
469
+
470
+ # Account for tokens locked in open sell orders (human-readable sizes)
471
+ open_orders = self.get_orders(asset_id=token_id)
472
+ locked_raw = sum(
473
+ (
474
+ o.size_remaining * TOKEN_DECIMALS
475
+ for o in open_orders
476
+ if o.side == OrderSide.SELL
477
+ ),
478
+ ZERO,
479
+ )
480
+ available = token_bal.balance - locked_raw
481
+ return available >= raw_size
482
+
483
+ def refresh_token_allowance(self, token_id: str) -> None:
484
+ """Refresh server's cached balance/allowance for a conditional token.
485
+
486
+ This does NOT perform on-chain approval. It tells the CLOB server to
487
+ re-read on-chain state. For first-time token approval, use the
488
+ Polymarket UI or send a setApprovalForAll transaction on-chain.
489
+ """
490
+ client = self._get_authenticated_client()
491
+ params = BalanceAllowanceParams(
492
+ asset_type=cast(AssetType, AssetType.CONDITIONAL),
493
+ token_id=token_id,
494
+ )
495
+ client.update_balance_allowance(params)
496
+
497
+ def refresh_collateral_allowance(self) -> None:
498
+ """Refresh server's cached balance/allowance for USDC collateral."""
499
+ client = self._get_authenticated_client()
500
+ params = BalanceAllowanceParams(
501
+ asset_type=cast(AssetType, AssetType.COLLATERAL),
502
+ )
503
+ client.update_balance_allowance(params)
504
+
505
+ def approve_token(self, neg_risk: bool = False) -> str:
506
+ """Approve the CTF conditional tokens for the exchange on-chain.
507
+
508
+ Sends a setApprovalForAll transaction on the CTF ERC1155 contract.
509
+ One-time setup per exchange (neg_risk vs non-neg_risk).
510
+
511
+ Returns:
512
+ Transaction hash.
513
+ """
514
+ return _approve_token(
515
+ self._private_key, neg_risk, self.funder, self._builder_creds
516
+ )
517
+
518
+ def approve_collateral(self, neg_risk: bool = False) -> str:
519
+ """Approve USDC for the exchange contract on-chain.
520
+
521
+ Sends an ERC20 approve transaction for max uint256.
522
+
523
+ Returns:
524
+ Transaction hash.
525
+ """
526
+ return _approve_collateral(
527
+ self._private_key, neg_risk, self.funder, self._builder_creds
528
+ )
529
+
530
+ def approve_all(self) -> list[str]:
531
+ """Approve both exchanges (neg_risk + non-neg_risk) for tokens and USDC.
532
+
533
+ Returns:
534
+ List of transaction hashes.
535
+ """
536
+ return _approve_all(self._private_key, self.funder, self._builder_creds)
537
+
538
+ # ========================================================================
539
+ # WebSocket
540
+ # ========================================================================
541
+
542
+ @property
543
+ def market_ws(self) -> PolymarketMarketWebSocket:
544
+ """Lazy-initialized market WebSocket (public, subscribes by asset_id)."""
545
+ if self._market_ws is None:
546
+ self._market_ws = PolymarketMarketWebSocket()
547
+ return self._market_ws
548
+
549
+ @property
550
+ def user_ws(self) -> PolymarketUserWebSocket:
551
+ """Lazy-initialized user WebSocket (authenticated, subscribes by market_id)."""
552
+ if self._user_ws is None:
553
+ self._user_ws = PolymarketUserWebSocket(auth=self.get_auth())
554
+ return self._user_ws
@@ -0,0 +1,16 @@
1
+ from decimal import Decimal
2
+
3
+ CLOB_HOST = "https://clob.polymarket.com"
4
+ GAMMA_API_HOST = "https://gamma-api.polymarket.com"
5
+ DATA_API_HOST = "https://data-api.polymarket.com"
6
+ CHAIN_ID = 137 # Polygon mainnet
7
+ POLYGON_RPC = "https://polygon-bor-rpc.publicnode.com"
8
+ RELAYER_HOST = "https://relayer-v2.polymarket.com"
9
+
10
+ # Conditional tokens and USDC on Polygon both use 6 decimals.
11
+ # The CLOB balance API returns raw values; create_order expects human-readable.
12
+ TOKEN_DECIMALS = Decimal("1000000")
13
+
14
+ # Crypto-market fee formula: fee = size * price * CRYPTO_FEE_RATE * (price * (1 - price))^CRYPTO_FEE_EXPONENT
15
+ CRYPTO_FEE_RATE = Decimal("0.25")
16
+ CRYPTO_FEE_EXPONENT = 2