polymarket-apis 0.2.2__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.
Potentially problematic release.
This version of polymarket-apis might be problematic. Click here for more details.
- polymarket_apis/__init__.py +2 -0
- polymarket_apis/clients/__init__.py +0 -0
- polymarket_apis/clients/clob_client.py +730 -0
- polymarket_apis/clients/data_client.py +234 -0
- polymarket_apis/clients/gamma_client.py +311 -0
- polymarket_apis/clients/web3_client.py +261 -0
- polymarket_apis/clients/websockets_client.py +131 -0
- polymarket_apis/types/__init__.py +0 -0
- polymarket_apis/types/clob_types.py +494 -0
- polymarket_apis/types/common.py +49 -0
- polymarket_apis/types/data_types.py +161 -0
- polymarket_apis/types/gamma_types.py +313 -0
- polymarket_apis/types/websockets_types.py +191 -0
- polymarket_apis/utilities/__init__.py +0 -0
- polymarket_apis/utilities/config.py +36 -0
- polymarket_apis/utilities/constants.py +26 -0
- polymarket_apis/utilities/endpoints.py +37 -0
- polymarket_apis/utilities/exceptions.py +11 -0
- polymarket_apis/utilities/headers.py +54 -0
- polymarket_apis/utilities/order_builder/__init__.py +0 -0
- polymarket_apis/utilities/order_builder/builder.py +240 -0
- polymarket_apis/utilities/order_builder/helpers.py +61 -0
- polymarket_apis/utilities/signing/__init__.py +0 -0
- polymarket_apis/utilities/signing/eip712.py +28 -0
- polymarket_apis/utilities/signing/hmac.py +20 -0
- polymarket_apis/utilities/signing/model.py +8 -0
- polymarket_apis/utilities/signing/signer.py +25 -0
- polymarket_apis/utilities/web3/__init__.py +0 -0
- polymarket_apis/utilities/web3/abis/CTFExchange.json +1851 -0
- polymarket_apis/utilities/web3/abis/ConditionalTokens.json +705 -0
- polymarket_apis/utilities/web3/abis/NegRiskAdapter.json +999 -0
- polymarket_apis/utilities/web3/abis/NegRiskCtfExchange.json +1856 -0
- polymarket_apis/utilities/web3/abis/ProxyWalletFactory.json +319 -0
- polymarket_apis/utilities/web3/abis/UChildERC20Proxy.json +1438 -0
- polymarket_apis/utilities/web3/abis/__init__.py +0 -0
- polymarket_apis/utilities/web3/abis/custom_contract_errors.py +31 -0
- polymarket_apis/utilities/web3/helpers.py +8 -0
- polymarket_apis-0.2.2.dist-info/METADATA +18 -0
- polymarket_apis-0.2.2.dist-info/RECORD +40 -0
- polymarket_apis-0.2.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from typing import Literal, Optional
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from httpx import HTTPStatusError
|
|
9
|
+
from py_order_utils.model import SignedOrder
|
|
10
|
+
|
|
11
|
+
from ..types.clob_types import (
|
|
12
|
+
ApiCreds,
|
|
13
|
+
BidAsk,
|
|
14
|
+
BookParams,
|
|
15
|
+
ClobMarket,
|
|
16
|
+
CreateOrderOptions,
|
|
17
|
+
DailyEarnedReward,
|
|
18
|
+
MarketOrderArgs,
|
|
19
|
+
Midpoint,
|
|
20
|
+
OpenOrder,
|
|
21
|
+
OrderArgs,
|
|
22
|
+
OrderBookSummary,
|
|
23
|
+
OrderCancelResponse,
|
|
24
|
+
OrderPostResponse,
|
|
25
|
+
OrderType,
|
|
26
|
+
PaginatedResponse,
|
|
27
|
+
PartialCreateOrderOptions,
|
|
28
|
+
PolygonTrade,
|
|
29
|
+
PolymarketRewardItem,
|
|
30
|
+
PostOrdersArgs,
|
|
31
|
+
Price,
|
|
32
|
+
PriceHistory,
|
|
33
|
+
RequestArgs,
|
|
34
|
+
RewardsMarket,
|
|
35
|
+
Spread,
|
|
36
|
+
TickSize,
|
|
37
|
+
TokenBidAskDict,
|
|
38
|
+
TokenValueDict,
|
|
39
|
+
)
|
|
40
|
+
from ..types.common import EthAddress, Keccak256
|
|
41
|
+
from ..utilities.constants import END_CURSOR, POLYGON
|
|
42
|
+
from ..utilities.endpoints import (
|
|
43
|
+
ARE_ORDERS_SCORING,
|
|
44
|
+
CANCEL,
|
|
45
|
+
CANCEL_ALL,
|
|
46
|
+
CANCEL_ORDERS,
|
|
47
|
+
CREATE_API_KEY,
|
|
48
|
+
DELETE_API_KEY,
|
|
49
|
+
DERIVE_API_KEY,
|
|
50
|
+
GET_API_KEYS,
|
|
51
|
+
GET_LAST_TRADE_PRICE,
|
|
52
|
+
GET_LAST_TRADES_PRICES,
|
|
53
|
+
GET_MARKET,
|
|
54
|
+
GET_MARKETS,
|
|
55
|
+
GET_NEG_RISK,
|
|
56
|
+
GET_ORDER_BOOK,
|
|
57
|
+
GET_ORDER_BOOKS,
|
|
58
|
+
GET_PRICES,
|
|
59
|
+
GET_SPREAD,
|
|
60
|
+
GET_SPREADS,
|
|
61
|
+
GET_TICK_SIZE,
|
|
62
|
+
IS_ORDER_SCORING,
|
|
63
|
+
MID_POINT,
|
|
64
|
+
MID_POINTS,
|
|
65
|
+
ORDERS,
|
|
66
|
+
POST_ORDER,
|
|
67
|
+
POST_ORDERS,
|
|
68
|
+
PRICE,
|
|
69
|
+
TIME,
|
|
70
|
+
TRADES,
|
|
71
|
+
)
|
|
72
|
+
from ..utilities.exceptions import (
|
|
73
|
+
InvalidPriceError,
|
|
74
|
+
InvalidTickSizeError,
|
|
75
|
+
LiquidityError,
|
|
76
|
+
MissingOrderbookError,
|
|
77
|
+
)
|
|
78
|
+
from ..utilities.headers import create_level_1_headers, create_level_2_headers
|
|
79
|
+
from ..utilities.order_builder.builder import OrderBuilder
|
|
80
|
+
from ..utilities.order_builder.helpers import (
|
|
81
|
+
is_tick_size_smaller,
|
|
82
|
+
order_to_json,
|
|
83
|
+
price_valid,
|
|
84
|
+
)
|
|
85
|
+
from ..utilities.signing.signer import Signer
|
|
86
|
+
|
|
87
|
+
logger = logging.getLogger(__name__)
|
|
88
|
+
|
|
89
|
+
class PolymarketClobClient:
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
private_key: str,
|
|
93
|
+
proxy_address: EthAddress,
|
|
94
|
+
creds: Optional[ApiCreds] = None,
|
|
95
|
+
chain_id: Literal[137, 80002] = POLYGON,
|
|
96
|
+
signature_type: Literal[0, 1, 2] = 1,
|
|
97
|
+
# 0 - EOA wallet, 1 - Proxy wallet, 2 - Gnosis Safe wallet
|
|
98
|
+
):
|
|
99
|
+
self.proxy_address = proxy_address
|
|
100
|
+
self.client = httpx.Client(http2=True, timeout=30.0)
|
|
101
|
+
self.async_client = httpx.AsyncClient(http2=True, timeout=30.0)
|
|
102
|
+
self.base_url: str = "https://clob.polymarket.com"
|
|
103
|
+
self.signer = Signer(private_key=private_key, chain_id=chain_id)
|
|
104
|
+
self.builder = OrderBuilder(
|
|
105
|
+
signer=self.signer,
|
|
106
|
+
sig_type=signature_type,
|
|
107
|
+
funder=proxy_address,
|
|
108
|
+
)
|
|
109
|
+
self.creds = creds if creds else self.create_or_derive_api_creds()
|
|
110
|
+
|
|
111
|
+
# local cache
|
|
112
|
+
self.__tick_sizes = {}
|
|
113
|
+
self.__neg_risk = {}
|
|
114
|
+
|
|
115
|
+
def _build_url(self, endpoint: str) -> str:
|
|
116
|
+
return urljoin(self.base_url, endpoint)
|
|
117
|
+
|
|
118
|
+
def get_ok(self) -> str:
|
|
119
|
+
response = self.client.get(self.base_url)
|
|
120
|
+
response.raise_for_status()
|
|
121
|
+
return response.json()
|
|
122
|
+
|
|
123
|
+
def create_api_creds(self, nonce: Optional[int] = None) -> ApiCreds:
|
|
124
|
+
headers = create_level_1_headers(self.signer, nonce)
|
|
125
|
+
response = self.client.post(self._build_url(CREATE_API_KEY), headers=headers)
|
|
126
|
+
response.raise_for_status()
|
|
127
|
+
return ApiCreds(**response.json())
|
|
128
|
+
|
|
129
|
+
def derive_api_key(self, nonce: Optional[int] = None) -> ApiCreds:
|
|
130
|
+
headers = create_level_1_headers(self.signer, nonce)
|
|
131
|
+
response = self.client.get(self._build_url(DERIVE_API_KEY), headers=headers)
|
|
132
|
+
response.raise_for_status()
|
|
133
|
+
return ApiCreds(**response.json())
|
|
134
|
+
|
|
135
|
+
def create_or_derive_api_creds(self, nonce: Optional[int] = None) -> ApiCreds:
|
|
136
|
+
try:
|
|
137
|
+
return self.create_api_creds(nonce)
|
|
138
|
+
except HTTPStatusError:
|
|
139
|
+
return self.derive_api_key(nonce)
|
|
140
|
+
|
|
141
|
+
def set_api_creds(self, creds: ApiCreds) -> None:
|
|
142
|
+
self.creds = creds
|
|
143
|
+
|
|
144
|
+
def get_api_keys(self) -> dict:
|
|
145
|
+
request_args = RequestArgs(method="GET", request_path=GET_API_KEYS)
|
|
146
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
147
|
+
response = self.client.get(self._build_url(GET_API_KEYS), headers=headers)
|
|
148
|
+
response.raise_for_status()
|
|
149
|
+
return response.json()
|
|
150
|
+
|
|
151
|
+
def delete_api_keys(self) -> Literal["OK"]:
|
|
152
|
+
request_args = RequestArgs(method="DELETE", request_path=DELETE_API_KEY)
|
|
153
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
154
|
+
response = self.client.delete(self._build_url(DELETE_API_KEY), headers=headers)
|
|
155
|
+
response.raise_for_status()
|
|
156
|
+
return response.json()
|
|
157
|
+
|
|
158
|
+
def get_utc_time(self) -> datetime:
|
|
159
|
+
# parse server timestamp into utc datetime
|
|
160
|
+
response = self.client.get(self._build_url(TIME))
|
|
161
|
+
response.raise_for_status()
|
|
162
|
+
return datetime.fromtimestamp(response.json(), tz=UTC)
|
|
163
|
+
|
|
164
|
+
def get_tick_size(self, token_id: str) -> TickSize:
|
|
165
|
+
if token_id in self.__tick_sizes:
|
|
166
|
+
return self.__tick_sizes[token_id]
|
|
167
|
+
|
|
168
|
+
params = {"token_id": token_id}
|
|
169
|
+
response = self.client.get(self._build_url(GET_TICK_SIZE), params=params)
|
|
170
|
+
response.raise_for_status()
|
|
171
|
+
self.__tick_sizes[token_id] = str(response.json()["minimum_tick_size"])
|
|
172
|
+
|
|
173
|
+
return self.__tick_sizes[token_id]
|
|
174
|
+
|
|
175
|
+
def get_neg_risk(self, token_id: str) -> bool:
|
|
176
|
+
if token_id in self.__neg_risk:
|
|
177
|
+
return self.__neg_risk[token_id]
|
|
178
|
+
|
|
179
|
+
params = {"token_id": token_id}
|
|
180
|
+
response = self.client.get(self._build_url(GET_NEG_RISK), params=params)
|
|
181
|
+
response.raise_for_status()
|
|
182
|
+
self.__neg_risk[token_id] = response.json()["neg_risk"]
|
|
183
|
+
|
|
184
|
+
return self.__neg_risk[token_id]
|
|
185
|
+
|
|
186
|
+
def __resolve_tick_size(
|
|
187
|
+
self, token_id: str, tick_size: TickSize = None,
|
|
188
|
+
) -> TickSize:
|
|
189
|
+
min_tick_size = self.get_tick_size(token_id)
|
|
190
|
+
if tick_size is not None:
|
|
191
|
+
if is_tick_size_smaller(tick_size, min_tick_size):
|
|
192
|
+
msg = f"invalid tick size ({tick_size!s}), minimum for the market is {min_tick_size!s}"
|
|
193
|
+
raise InvalidTickSizeError(msg)
|
|
194
|
+
else:
|
|
195
|
+
tick_size = min_tick_size
|
|
196
|
+
return tick_size
|
|
197
|
+
|
|
198
|
+
def get_midpoint(self, token_id: str) -> Midpoint:
|
|
199
|
+
"""Get the mid-market price for the given token."""
|
|
200
|
+
params = {"token_id": token_id}
|
|
201
|
+
response = self.client.get(self._build_url(MID_POINT), params=params)
|
|
202
|
+
response.raise_for_status()
|
|
203
|
+
return Midpoint(token_id=token_id, value=float(response.json()["mid"]))
|
|
204
|
+
|
|
205
|
+
def get_midpoints(self, token_ids: list[str]) -> dict:
|
|
206
|
+
"""Get the mid-market prices for a set of tokens."""
|
|
207
|
+
data = [{"token_id": token_id} for token_id in token_ids]
|
|
208
|
+
response = self.client.post(self._build_url(MID_POINTS), json=data)
|
|
209
|
+
response.raise_for_status()
|
|
210
|
+
return TokenValueDict(**response.json()).root
|
|
211
|
+
|
|
212
|
+
def get_spread(self, token_id: str) -> Spread:
|
|
213
|
+
"""Get the spread for the given token."""
|
|
214
|
+
params = {"token_id": token_id}
|
|
215
|
+
response = self.client.get(self._build_url(GET_SPREAD), params=params)
|
|
216
|
+
response.raise_for_status()
|
|
217
|
+
return Spread(token_id=token_id, value=float(response.json()["mid"]))
|
|
218
|
+
|
|
219
|
+
def get_spreads(self, token_ids: list[str]) -> dict:
|
|
220
|
+
"""Get the spreads for a set of tokens."""
|
|
221
|
+
data = [{"token_id": token_id} for token_id in token_ids]
|
|
222
|
+
response = self.client.post(self._build_url(GET_SPREADS), json=data)
|
|
223
|
+
response.raise_for_status()
|
|
224
|
+
return TokenValueDict(**response.json()).root
|
|
225
|
+
|
|
226
|
+
def get_price(self, token_id: str, side: Literal["BUY", "SELL"]) -> Price:
|
|
227
|
+
"""Get the market price for the given token and side."""
|
|
228
|
+
params = {"token_id": token_id, "side": side}
|
|
229
|
+
response = self.client.get(self._build_url(PRICE), params=params)
|
|
230
|
+
response.raise_for_status()
|
|
231
|
+
return Price(**response.json(), token_id=token_id, side=side)
|
|
232
|
+
|
|
233
|
+
def get_prices(self, params: list[BookParams]) -> dict[str, BidAsk]:
|
|
234
|
+
"""Get the market prices for a set of tokens and sides."""
|
|
235
|
+
data = [{"token_id": param.token_id, "side": param.side} for param in params]
|
|
236
|
+
response = self.client.post(self._build_url(GET_PRICES), json=data)
|
|
237
|
+
response.raise_for_status()
|
|
238
|
+
return TokenBidAskDict(**response.json()).root
|
|
239
|
+
|
|
240
|
+
def get_last_trade_price(self, token_id) -> Price:
|
|
241
|
+
"""Fetches the last trade price for a token_id."""
|
|
242
|
+
params = {"token_id": token_id}
|
|
243
|
+
response = self.client.get(self._build_url(GET_LAST_TRADE_PRICE), params=params)
|
|
244
|
+
response.raise_for_status()
|
|
245
|
+
return Price(**response.json(), token_id=token_id)
|
|
246
|
+
|
|
247
|
+
def get_last_trades_prices(self, token_ids: list[str]) -> list[Price]:
|
|
248
|
+
"""Fetches the last trades prices for a set of token ids."""
|
|
249
|
+
body = [{"token_id": token_id} for token_id in token_ids]
|
|
250
|
+
response = self.client.post(self._build_url(GET_LAST_TRADES_PRICES), json=body)
|
|
251
|
+
response.raise_for_status()
|
|
252
|
+
return [Price(**price) for price in response.json()]
|
|
253
|
+
|
|
254
|
+
def get_order_book(self, token_id) -> OrderBookSummary:
|
|
255
|
+
"""Get the orderbook for the given token."""
|
|
256
|
+
params = {"token_id": token_id}
|
|
257
|
+
response = self.client.get(self._build_url(GET_ORDER_BOOK), params=params)
|
|
258
|
+
response.raise_for_status()
|
|
259
|
+
return OrderBookSummary(**response.json())
|
|
260
|
+
|
|
261
|
+
def get_order_books(self, token_ids: list[str]) -> list[OrderBookSummary]:
|
|
262
|
+
"""Get the orderbook for a set of tokens."""
|
|
263
|
+
body = [{"token_id": token_id} for token_id in token_ids]
|
|
264
|
+
response = self.client.post(self._build_url(GET_ORDER_BOOKS), json=body)
|
|
265
|
+
response.raise_for_status()
|
|
266
|
+
return [OrderBookSummary(**obs) for obs in response.json()]
|
|
267
|
+
|
|
268
|
+
async def get_order_books_async(self, token_ids: list[str]) -> list[OrderBookSummary]:
|
|
269
|
+
"""Get the orderbook for a set of tokens asynchronously."""
|
|
270
|
+
body = [{"token_id": token_id} for token_id in token_ids]
|
|
271
|
+
response = await self.async_client.post(self._build_url(GET_ORDER_BOOKS), json=body)
|
|
272
|
+
response.raise_for_status()
|
|
273
|
+
return [OrderBookSummary(**obs) for obs in response.json()]
|
|
274
|
+
|
|
275
|
+
def get_market(self, condition_id) -> ClobMarket:
|
|
276
|
+
"""Get a ClobMarket by condition_id."""
|
|
277
|
+
response = self.client.get(self._build_url(GET_MARKET + condition_id))
|
|
278
|
+
response.raise_for_status()
|
|
279
|
+
return ClobMarket(**response.json())
|
|
280
|
+
|
|
281
|
+
def get_markets(self, next_cursor="MA==") -> PaginatedResponse[ClobMarket]:
|
|
282
|
+
"""Get paginated ClobMarkets."""
|
|
283
|
+
params = {"next_cursor": next_cursor}
|
|
284
|
+
response = self.client.get(self._build_url(GET_MARKETS), params=params)
|
|
285
|
+
response.raise_for_status()
|
|
286
|
+
return PaginatedResponse[ClobMarket](**response.json())
|
|
287
|
+
|
|
288
|
+
def get_all_markets(self, next_cursor="MA==") -> list[ClobMarket]:
|
|
289
|
+
"""Recursively fetch all ClobMarkets using pagination."""
|
|
290
|
+
# Base case: Stop recursion if next_cursor indicates the last page
|
|
291
|
+
if next_cursor == "LTE=":
|
|
292
|
+
print("Reached the last page of markets.")
|
|
293
|
+
return []
|
|
294
|
+
|
|
295
|
+
# Fetch current page of markets
|
|
296
|
+
paginated_response = self.get_markets(next_cursor=next_cursor)
|
|
297
|
+
|
|
298
|
+
# Collect current page data
|
|
299
|
+
current_markets = paginated_response.data
|
|
300
|
+
|
|
301
|
+
# Recursively fetch remaining pages
|
|
302
|
+
next_page_markets = self.get_all_markets(
|
|
303
|
+
next_cursor=paginated_response.next_cursor,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Combine current page data with data from subsequent pages
|
|
307
|
+
return current_markets + next_page_markets
|
|
308
|
+
|
|
309
|
+
def get_recent_history(
|
|
310
|
+
self,
|
|
311
|
+
token_id: str,
|
|
312
|
+
interval: Optional[Literal["1d", "6h", "1h"]] = "1d",
|
|
313
|
+
fidelity: int = 1, # resolution in minutes
|
|
314
|
+
) -> PriceHistory:
|
|
315
|
+
"""Get the recent price history of a token (up to now) - 1h, 6h, 1d."""
|
|
316
|
+
if fidelity < 1:
|
|
317
|
+
msg = f"invalid filters: minimum 'fidelity' for '{interval}' range is 1"
|
|
318
|
+
raise ValueError(msg)
|
|
319
|
+
|
|
320
|
+
params = {
|
|
321
|
+
"market": token_id,
|
|
322
|
+
"interval": interval,
|
|
323
|
+
"fidelity": fidelity,
|
|
324
|
+
}
|
|
325
|
+
response = self.client.get(self._build_url("/prices-history"), params=params)
|
|
326
|
+
response.raise_for_status()
|
|
327
|
+
return PriceHistory(**response.json(), token_id=token_id)
|
|
328
|
+
|
|
329
|
+
def get_history(
|
|
330
|
+
self,
|
|
331
|
+
token_id: str,
|
|
332
|
+
start_time: Optional[datetime] = None,
|
|
333
|
+
end_time: Optional[datetime] = None,
|
|
334
|
+
interval: Optional[Literal["max", "1m", "1w"]] = "max",
|
|
335
|
+
fidelity: Optional[int] = 2, # resolution in minutes
|
|
336
|
+
) -> PriceHistory:
|
|
337
|
+
"""Get the price history of a token between selected dates - 1m, 1w, max."""
|
|
338
|
+
min_fidelities = {"1m": 10, "1w": 5, "max": 2}
|
|
339
|
+
|
|
340
|
+
if fidelity < min_fidelities[interval]:
|
|
341
|
+
msg = f"invalid filters: minimum 'fidelity' for '{interval}' range is {min_fidelities[interval]}"
|
|
342
|
+
raise ValueError(msg)
|
|
343
|
+
|
|
344
|
+
if start_time is None and end_time is None:
|
|
345
|
+
msg = "At least one of 'start_time' or 'end_time' must be provided."
|
|
346
|
+
raise ValueError(msg)
|
|
347
|
+
|
|
348
|
+
# Default values for timestamps if one is not provided
|
|
349
|
+
|
|
350
|
+
if start_time is None:
|
|
351
|
+
start_time = datetime(2020, 1, 1, tzinfo=UTC) # Default start time
|
|
352
|
+
if end_time is None:
|
|
353
|
+
end_time = datetime.now(UTC) # Default end time
|
|
354
|
+
|
|
355
|
+
params = {
|
|
356
|
+
"market": token_id,
|
|
357
|
+
"startTs": int(start_time.timestamp()),
|
|
358
|
+
"endTs": int(end_time.timestamp()),
|
|
359
|
+
"interval": interval,
|
|
360
|
+
"fidelity": fidelity,
|
|
361
|
+
}
|
|
362
|
+
response = self.client.get(self._build_url("/prices-history"), params=params)
|
|
363
|
+
response.raise_for_status()
|
|
364
|
+
return PriceHistory(**response.json(), token_id=token_id)
|
|
365
|
+
|
|
366
|
+
def get_orders(self, order_id: Optional[str] = None, condition_id: Optional[Keccak256] = None, token_id: Optional[str] = None, next_cursor: str ="MA==") -> list[OpenOrder]:
|
|
367
|
+
"""Gets your active orders, filtered by order_id, condition_id, token_id."""
|
|
368
|
+
params = {}
|
|
369
|
+
if order_id:
|
|
370
|
+
params["id"] = order_id
|
|
371
|
+
if condition_id:
|
|
372
|
+
params["market"] = condition_id
|
|
373
|
+
if token_id:
|
|
374
|
+
params["asset_id"] = token_id
|
|
375
|
+
|
|
376
|
+
request_args = RequestArgs(method="GET", request_path=ORDERS)
|
|
377
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
378
|
+
|
|
379
|
+
results = []
|
|
380
|
+
next_cursor = next_cursor if next_cursor is not None else "MA=="
|
|
381
|
+
while next_cursor != END_CURSOR:
|
|
382
|
+
params["next_cursor"] = next_cursor
|
|
383
|
+
response = self.client.get(self._build_url(ORDERS), headers=headers, params=params)
|
|
384
|
+
response.raise_for_status()
|
|
385
|
+
next_cursor = response.json()["next_cursor"]
|
|
386
|
+
results += [OpenOrder(**order) for order in response.json()["data"]]
|
|
387
|
+
|
|
388
|
+
return results
|
|
389
|
+
|
|
390
|
+
def create_order(self, order_args: OrderArgs, options: Optional[PartialCreateOrderOptions] = None) -> SignedOrder:
|
|
391
|
+
"""Creates and signs an order."""
|
|
392
|
+
# add resolve_order_options, or similar
|
|
393
|
+
tick_size = self.__resolve_tick_size(
|
|
394
|
+
order_args.token_id,
|
|
395
|
+
options.tick_size if options else None,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
if not price_valid(order_args.price, tick_size):
|
|
399
|
+
msg = f"price ({order_args.price}), min: {tick_size} - max: {1 - float(tick_size)}"
|
|
400
|
+
raise InvalidPriceError(msg)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
neg_risk = (
|
|
404
|
+
options.neg_risk
|
|
405
|
+
if options and options.neg_risk
|
|
406
|
+
else self.get_neg_risk(order_args.token_id)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
return self.builder.create_order(
|
|
410
|
+
order_args,
|
|
411
|
+
CreateOrderOptions(
|
|
412
|
+
tick_size=tick_size,
|
|
413
|
+
neg_risk=neg_risk,
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def post_order(self, order: SignedOrder, order_type: OrderType = OrderType.GTC) -> Optional[OrderPostResponse]:
|
|
418
|
+
"""Posts a SignedOrder."""
|
|
419
|
+
body = order_to_json(order, self.creds.api_key, order_type)
|
|
420
|
+
headers = create_level_2_headers(
|
|
421
|
+
self.signer,
|
|
422
|
+
self.creds,
|
|
423
|
+
RequestArgs(method="POST", request_path=POST_ORDER, body=body),
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
try:
|
|
427
|
+
response = self.client.post(
|
|
428
|
+
self._build_url("/order"),
|
|
429
|
+
headers=headers,
|
|
430
|
+
content=json.dumps(body).encode("utf-8"),
|
|
431
|
+
)
|
|
432
|
+
response.raise_for_status()
|
|
433
|
+
return OrderPostResponse(**response.json())
|
|
434
|
+
except httpx.HTTPStatusError as exc:
|
|
435
|
+
msg = f"Client Error '{exc.response.status_code} {exc.response.reason_phrase}' while posting order"
|
|
436
|
+
logger.warning(msg)
|
|
437
|
+
error_json = exc.response.json()
|
|
438
|
+
print("Details:", error_json["error"])
|
|
439
|
+
|
|
440
|
+
def create_and_post_order(self, order_args: OrderArgs, options: Optional[PartialCreateOrderOptions] = None, order_type: OrderType = OrderType.GTC) -> OrderPostResponse:
|
|
441
|
+
"""Utility function to create and publish an order."""
|
|
442
|
+
order = self.create_order(order_args, options)
|
|
443
|
+
return self.post_order(order=order, order_type=order_type)
|
|
444
|
+
|
|
445
|
+
def post_orders(self, args: list[PostOrdersArgs]):
|
|
446
|
+
"""Posts multiple SignedOrders at once."""
|
|
447
|
+
body = [order_to_json(arg.order, self.creds.api_key, arg.order_type) for arg in args]
|
|
448
|
+
headers = create_level_2_headers(
|
|
449
|
+
self.signer,
|
|
450
|
+
self.creds,
|
|
451
|
+
RequestArgs(method="POST", request_path=POST_ORDERS, body=body),
|
|
452
|
+
)
|
|
453
|
+
try:
|
|
454
|
+
response = self.client.post(
|
|
455
|
+
self._build_url("/orders"),
|
|
456
|
+
headers=headers,
|
|
457
|
+
content=json.dumps(body).encode("utf-8"),
|
|
458
|
+
)
|
|
459
|
+
response.raise_for_status()
|
|
460
|
+
order_responses = []
|
|
461
|
+
for index, item in enumerate(response.json()):
|
|
462
|
+
resp = OrderPostResponse(**item)
|
|
463
|
+
order_responses.append(resp)
|
|
464
|
+
if resp.error_msg:
|
|
465
|
+
msg = (f"Error posting order in position {index} \n"
|
|
466
|
+
f"Details: {resp.error_msg}")
|
|
467
|
+
logger.warning(msg)
|
|
468
|
+
except httpx.HTTPStatusError as exc:
|
|
469
|
+
msg = f"Client Error '{exc.response.status_code} {exc.response.reason_phrase}' while posting order"
|
|
470
|
+
logger.warning(msg)
|
|
471
|
+
error_json = exc.response.json()
|
|
472
|
+
print("Details:", error_json["error"])
|
|
473
|
+
else:
|
|
474
|
+
return order_responses
|
|
475
|
+
|
|
476
|
+
def create_and_post_orders(self, args: list[OrderArgs], order_types: list[OrderType]) -> list[OrderPostResponse]:
|
|
477
|
+
"""Utility function to create and publish multiple orders at once."""
|
|
478
|
+
return self.post_orders(
|
|
479
|
+
[PostOrdersArgs(order=self.create_order(order_args),
|
|
480
|
+
order_type=order_type)
|
|
481
|
+
for order_args, order_type in zip(args, order_types, strict=True)],
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def calculate_market_price(self, token_id: str, side: str, amount: float, order_type: OrderType) -> float:
|
|
485
|
+
"""Calculates the matching price considering an amount and the current orderbook."""
|
|
486
|
+
book = self.get_order_book(token_id)
|
|
487
|
+
if book is None:
|
|
488
|
+
msg = "Order book is None"
|
|
489
|
+
raise MissingOrderbookError(msg)
|
|
490
|
+
if side == "BUY":
|
|
491
|
+
if book.asks is None:
|
|
492
|
+
msg = "No ask orders available"
|
|
493
|
+
raise LiquidityError(msg)
|
|
494
|
+
return self.builder.calculate_buy_market_price(
|
|
495
|
+
book.asks, amount, order_type,
|
|
496
|
+
)
|
|
497
|
+
if book.bids is None:
|
|
498
|
+
msg = "No bid orders available"
|
|
499
|
+
raise LiquidityError(msg)
|
|
500
|
+
return self.builder.calculate_sell_market_price(
|
|
501
|
+
book.bids, amount, order_type,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def create_market_order(self, order_args: MarketOrderArgs, options: Optional[PartialCreateOrderOptions] = None):
|
|
505
|
+
"""Creates and signs a market order."""
|
|
506
|
+
tick_size = self.__resolve_tick_size(
|
|
507
|
+
order_args.token_id,
|
|
508
|
+
options.tick_size if options else None,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
if order_args.price is None or order_args.price <= 0:
|
|
512
|
+
order_args.price = self.calculate_market_price(
|
|
513
|
+
order_args.token_id,
|
|
514
|
+
order_args.side,
|
|
515
|
+
order_args.amount,
|
|
516
|
+
order_args.order_type,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
if not price_valid(order_args.price, tick_size):
|
|
520
|
+
msg = f"price ({order_args.price}), min: {tick_size} - max: {1 - float(tick_size)}"
|
|
521
|
+
raise InvalidPriceError(msg)
|
|
522
|
+
|
|
523
|
+
neg_risk = (
|
|
524
|
+
options.neg_risk
|
|
525
|
+
if options and options.neg_risk
|
|
526
|
+
else self.get_neg_risk(order_args.token_id)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
return self.builder.create_market_order(
|
|
530
|
+
order_args,
|
|
531
|
+
CreateOrderOptions(
|
|
532
|
+
tick_size=tick_size,
|
|
533
|
+
neg_risk=neg_risk,
|
|
534
|
+
),
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
def create_and_post_market_order(
|
|
538
|
+
self,
|
|
539
|
+
order_args: MarketOrderArgs,
|
|
540
|
+
options: Optional[PartialCreateOrderOptions] = None,
|
|
541
|
+
order_type: OrderType = OrderType.FOK,
|
|
542
|
+
) -> OrderPostResponse:
|
|
543
|
+
"""Utility function to create and publish a market order."""
|
|
544
|
+
order = self.create_market_order(order_args, options)
|
|
545
|
+
return self.post_order(order=order, order_type=order_type)
|
|
546
|
+
|
|
547
|
+
def cancel_order(self, order_id: Keccak256) -> OrderCancelResponse:
|
|
548
|
+
"""Cancels an order."""
|
|
549
|
+
body = {"orderID": order_id}
|
|
550
|
+
|
|
551
|
+
request_args = RequestArgs(method="DELETE", request_path=CANCEL, body=body)
|
|
552
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
553
|
+
|
|
554
|
+
response = self.client.request("DELETE", self._build_url(CANCEL), headers=headers, data=json.dumps(body).encode("utf-8"))
|
|
555
|
+
response.raise_for_status()
|
|
556
|
+
return OrderCancelResponse(**response.json())
|
|
557
|
+
|
|
558
|
+
def cancel_orders(self, order_ids: list[Keccak256]) -> OrderCancelResponse:
|
|
559
|
+
"""Cancels orders."""
|
|
560
|
+
body = order_ids
|
|
561
|
+
|
|
562
|
+
request_args = RequestArgs(
|
|
563
|
+
method="DELETE", request_path=CANCEL_ORDERS, body=body,
|
|
564
|
+
)
|
|
565
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
566
|
+
|
|
567
|
+
response = self.client.request("DELETE", self._build_url(CANCEL_ORDERS), headers=headers, data=json.dumps(body).encode("utf-8"))
|
|
568
|
+
response.raise_for_status()
|
|
569
|
+
return OrderCancelResponse(**response.json())
|
|
570
|
+
|
|
571
|
+
def cancel_all(self) -> OrderCancelResponse:
|
|
572
|
+
"""Cancels all available orders for the user."""
|
|
573
|
+
request_args = RequestArgs(method="DELETE", request_path=CANCEL_ALL)
|
|
574
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
575
|
+
|
|
576
|
+
response = self.client.delete(self._build_url(CANCEL_ALL), headers=headers)
|
|
577
|
+
response.raise_for_status()
|
|
578
|
+
return OrderCancelResponse(**response.json())
|
|
579
|
+
|
|
580
|
+
def is_order_scoring(self, order_id: Keccak256) -> bool:
|
|
581
|
+
"""Check if the order is currently scoring."""
|
|
582
|
+
request_args = RequestArgs(method="GET", request_path=IS_ORDER_SCORING)
|
|
583
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
584
|
+
|
|
585
|
+
response = self.client.get(self._build_url(IS_ORDER_SCORING), headers=headers, params={"order_id": order_id})
|
|
586
|
+
response.raise_for_status()
|
|
587
|
+
return response.json()["scoring"]
|
|
588
|
+
|
|
589
|
+
def are_orders_scoring(self, order_ids: list[Keccak256]) -> dict[Keccak256, bool]:
|
|
590
|
+
"""Check if the orders are currently scoring."""
|
|
591
|
+
body = order_ids
|
|
592
|
+
request_args = RequestArgs(
|
|
593
|
+
method="POST", request_path=ARE_ORDERS_SCORING, body=body,
|
|
594
|
+
)
|
|
595
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
596
|
+
headers["Content-Type"] = "application/json"
|
|
597
|
+
|
|
598
|
+
response = self.client.post(self._build_url(ARE_ORDERS_SCORING), headers=headers, json=body)
|
|
599
|
+
response.raise_for_status()
|
|
600
|
+
return response.json()
|
|
601
|
+
|
|
602
|
+
def get_rewards_market(self, condition_id: Keccak256) -> RewardsMarket:
|
|
603
|
+
"""
|
|
604
|
+
Get the RewardsMarket for a given market (condition_id).
|
|
605
|
+
|
|
606
|
+
- metadata, tokens, max_spread, min_size, rewards_config, market_competitiveness.
|
|
607
|
+
"""
|
|
608
|
+
request_args = RequestArgs(method="GET", request_path="/rewards/markets/")
|
|
609
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
610
|
+
|
|
611
|
+
response = self.client.get(self._build_url("/rewards/markets/" + condition_id), headers=headers)
|
|
612
|
+
response.raise_for_status()
|
|
613
|
+
return next(RewardsMarket(**market) for market in response.json()["data"])
|
|
614
|
+
|
|
615
|
+
def get_trades(
|
|
616
|
+
self,
|
|
617
|
+
condition_id: Keccak256 | None = None,
|
|
618
|
+
token_id: Optional[str] = None,
|
|
619
|
+
trade_id: Optional[str] = None,
|
|
620
|
+
before: Optional[datetime] = None,
|
|
621
|
+
after: Optional[datetime] = None,
|
|
622
|
+
maker_address: Optional[int] = None,
|
|
623
|
+
next_cursor="MA==") -> list[PolygonTrade]:
|
|
624
|
+
"""Fetches the trade history for a user."""
|
|
625
|
+
params = {}
|
|
626
|
+
if condition_id:
|
|
627
|
+
params["market"] = condition_id
|
|
628
|
+
if token_id:
|
|
629
|
+
params["asset_id"] = token_id
|
|
630
|
+
if trade_id:
|
|
631
|
+
params["id"] = trade_id
|
|
632
|
+
if before:
|
|
633
|
+
params["before"] = int(before.replace(microsecond=0).timestamp())
|
|
634
|
+
if after:
|
|
635
|
+
params["after"] = int(after.replace(microsecond=0).timestamp())
|
|
636
|
+
if maker_address:
|
|
637
|
+
params["maker_address"] = maker_address
|
|
638
|
+
|
|
639
|
+
request_args = RequestArgs(method="GET", request_path=TRADES)
|
|
640
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
641
|
+
|
|
642
|
+
results = []
|
|
643
|
+
next_cursor = next_cursor if next_cursor is not None else "MA=="
|
|
644
|
+
while next_cursor != END_CURSOR:
|
|
645
|
+
params["next_cursor"] = next_cursor
|
|
646
|
+
response = self.client.get(self._build_url(TRADES), headers=headers, params=params)
|
|
647
|
+
response.raise_for_status()
|
|
648
|
+
next_cursor = response.json()["next_cursor"]
|
|
649
|
+
results += [PolygonTrade(**trade) for trade in response.json()["data"]]
|
|
650
|
+
|
|
651
|
+
return results
|
|
652
|
+
|
|
653
|
+
def get_total_rewards(self, date: Optional[datetime] = None) -> DailyEarnedReward:
|
|
654
|
+
"""Get the total rewards earned on a given date (seems to only hold the 6 most recent data points)."""
|
|
655
|
+
if date is None:
|
|
656
|
+
date = datetime.now(UTC)
|
|
657
|
+
params = {
|
|
658
|
+
"authenticationType": "magic",
|
|
659
|
+
"date": f"{date.strftime("%Y-%m-%d")}",
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
request_args = RequestArgs(method="GET", request_path="/rewards/user/total")
|
|
663
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
664
|
+
params["l2Headers"] = json.dumps(headers)
|
|
665
|
+
|
|
666
|
+
response = self.client.get("https://polymarket.com/api/rewards/totalEarnings", params=params)
|
|
667
|
+
response.raise_for_status()
|
|
668
|
+
if response.json():
|
|
669
|
+
return DailyEarnedReward(**response.json()[0])
|
|
670
|
+
return DailyEarnedReward(
|
|
671
|
+
date=date,
|
|
672
|
+
asset_address="0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
|
673
|
+
maker_address=self.proxy_address,
|
|
674
|
+
earnings=0.0,
|
|
675
|
+
asset_rate=0.0,
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
def get_reward_markets(
|
|
679
|
+
self,
|
|
680
|
+
sort_by: Optional[Literal["market", "max_spread", "min_size", "rate_per_day", "spread", "price", "earnings", "earning_percentage"]] = "market",
|
|
681
|
+
sort_direction: Optional[Literal["ASC", "DESC"]] = None,
|
|
682
|
+
query: Optional[str] = None,
|
|
683
|
+
show_favorites: bool = False,
|
|
684
|
+
) -> list[PolymarketRewardItem]:
|
|
685
|
+
"""
|
|
686
|
+
Get all polymarket.com/rewards items, sorted by different criteria.
|
|
687
|
+
|
|
688
|
+
- market start date ("market") - TODO confirm this
|
|
689
|
+
- max spread for rewards in usdc
|
|
690
|
+
- min size for rewards in shares
|
|
691
|
+
- reward rate per day in usdc
|
|
692
|
+
- current spread of a market
|
|
693
|
+
- current price of a market
|
|
694
|
+
- your daily earnings on a market - only need auth for these last two
|
|
695
|
+
- your current earning percentage on a market.
|
|
696
|
+
"""
|
|
697
|
+
results = []
|
|
698
|
+
desc = {"ASC": False, "DESC": True}
|
|
699
|
+
params = {
|
|
700
|
+
"authenticationType": "magic",
|
|
701
|
+
"showFavorites": show_favorites,
|
|
702
|
+
}
|
|
703
|
+
if sort_by:
|
|
704
|
+
params["orderBy"] = sort_by
|
|
705
|
+
if query:
|
|
706
|
+
params["query"] = query
|
|
707
|
+
params["desc"] = False
|
|
708
|
+
if sort_direction:
|
|
709
|
+
params["desc"] = desc[sort_direction]
|
|
710
|
+
|
|
711
|
+
request_args = RequestArgs(method="GET", request_path="/rewards/user/markets")
|
|
712
|
+
headers = create_level_2_headers(self.signer, self.creds, request_args)
|
|
713
|
+
params["l2Headers"] = json.dumps(headers)
|
|
714
|
+
|
|
715
|
+
next_cursor = "MA=="
|
|
716
|
+
while next_cursor != END_CURSOR:
|
|
717
|
+
params["nextCursor"] = next_cursor
|
|
718
|
+
response = self.client.get("https://polymarket.com/api/rewards/markets", params=params)
|
|
719
|
+
# can probably use clob/rewards/user/markets here but haven't figure out auth
|
|
720
|
+
response.raise_for_status()
|
|
721
|
+
next_cursor = response.json()["next_cursor"]
|
|
722
|
+
results += [PolymarketRewardItem(**reward) for reward in response.json()["data"]]
|
|
723
|
+
|
|
724
|
+
return results
|
|
725
|
+
|
|
726
|
+
def __enter__(self):
|
|
727
|
+
return self
|
|
728
|
+
|
|
729
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
730
|
+
self.client.close()
|