polysimulator 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- {polysimulator-0.4.0 → polysimulator-0.4.2}/PKG-INFO +110 -7
- {polysimulator-0.4.0 → polysimulator-0.4.2}/README.md +108 -6
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/__init__.py +2 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/client.py +53 -11
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/__init__.py +2 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/_account.py +54 -4
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/_common.py +17 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/_trade.py +70 -21
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/async_public.py +9 -10
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/async_secure.py +26 -11
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/public.py +9 -9
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/secure.py +32 -13
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/models.py +2 -2
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/rfq.py +17 -1
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_adapt.py +89 -35
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_handle.py +17 -1
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_validators.py +12 -7
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/__init__.py +4 -2
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/_http.py +13 -2
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/_shared.py +22 -6
- polysimulator-0.4.2/polysim_sdk/_version.py +28 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/aio.py +3 -2
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/client.py +3 -2
- polysimulator-0.4.2/polysim_sdk/retry.py +89 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/sse.py +3 -2
- {polysimulator-0.4.0 → polysimulator-0.4.2}/pyproject.toml +5 -1
- {polysimulator-0.4.0 → polysimulator-0.4.2}/.gitignore +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/LICENSE +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/NOTICE +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/clob_types.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/constants.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/exceptions.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/order_builder/__init__.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/order_builder/constants.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/__init__.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/_onchain.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/environments.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/errors.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/pagination.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/__init__.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_crypto_events.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_market_events.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_runner.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_specs.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_stream_open.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_user_events.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/constants.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/exceptions.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/pagination.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/updown.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/ws.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polysimulator
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Python SDK for the PolySimulator paper-trading API, with py-clob-client v1 drop-in parity (not py-clob-client-v2) and a polysim_polymarket paper-mode mirror of Polymarket's unified py-sdk (polymarket-client 0.1.0b8).
|
|
5
5
|
Project-URL: Homepage, https://polysimulator.com
|
|
6
6
|
Project-URL: Documentation, https://docs.polysimulator.com
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Office/Business :: Financial
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Requires-Dist: httpx<1,>=0.27
|
|
23
23
|
Requires-Dist: pydantic<3,>=2
|
|
24
|
+
Requires-Dist: typing-extensions>=4
|
|
24
25
|
Requires-Dist: websockets<17,>=12
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: build>=1.0; extra == 'dev'
|
|
@@ -236,6 +237,39 @@ network, tolerant of missing fields): `seconds_to_expiry`, `is_window_open`,
|
|
|
236
237
|
`INTERVALS` vocab tuples. For a **streaming** tap on the underlying, see
|
|
237
238
|
[SSE: live underlying spot](#sse-live-underlying-spot) below.
|
|
238
239
|
|
|
240
|
+
### New-market warm-up: retry order placement on a 404
|
|
241
|
+
|
|
242
|
+
A **brand-new** market — especially the 5m Up/Down windows that roll every few
|
|
243
|
+
minutes — has a short window after creation before it enters the
|
|
244
|
+
order-validation catalog. An order placed in that window transiently fails with
|
|
245
|
+
a `404` (`code="MARKET_NOT_FOUND"`). The window is short — on the order of ~30s,
|
|
246
|
+
the Up/Down roll cadence — but it is a cadence, **not** a guarantee.
|
|
247
|
+
|
|
248
|
+
The SDK does **not** auto-retry 404s (by design — a 404 is normally permanent),
|
|
249
|
+
so a bot trading freshly-rolled markets should catch and retry that one signal
|
|
250
|
+
with backoff. The native client ships a small opt-in helper that retries **only**
|
|
251
|
+
the warm-up 404 and re-raises everything else (and the 404 itself once the budget
|
|
252
|
+
is spent):
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from polysim_sdk import PolySimClient, retry_on_market_warmup
|
|
256
|
+
|
|
257
|
+
with PolySimClient() as client:
|
|
258
|
+
fill = retry_on_market_warmup(
|
|
259
|
+
lambda: client.place_order(
|
|
260
|
+
market_id=cid, side="BUY", outcome="Up", amount="10", price="0.99",
|
|
261
|
+
),
|
|
262
|
+
attempts=6, base_delay=2.0, # ~2,4,8,16,30s capped backoff between tries
|
|
263
|
+
)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
It wraps any zero-argument callable, so the same helper works with a
|
|
267
|
+
`polysim_clob_client` drop-in call (e.g.
|
|
268
|
+
`retry_on_market_warmup(lambda: clob.create_and_post_order(args))`); the drop-in
|
|
269
|
+
surfaces the same condition as a `PolyApiException` with `status_code == 404`.
|
|
270
|
+
See [Errors](#errors) for the standalone retry-on-404 pattern if you'd rather
|
|
271
|
+
roll your own.
|
|
272
|
+
|
|
239
273
|
---
|
|
240
274
|
|
|
241
275
|
## Polymarket compatibility
|
|
@@ -301,7 +335,7 @@ The port is the same mechanical swap as v1 — change the import **prefix**, the
|
|
|
301
335
|
- from polymarket import SecureClient
|
|
302
336
|
+ from polysim_polymarket import SecureClient
|
|
303
337
|
|
|
304
|
-
- client = SecureClient(private_key="0x…")
|
|
338
|
+
- client = SecureClient.create(private_key="0x…") # real Polymarket wallet + EIP-712 signing
|
|
305
339
|
+ client = SecureClient(host="https://api.polysimulator.com", api_key="ps_live_…")
|
|
306
340
|
|
|
307
341
|
# unchanged from here on — same method names, same keyword-only call shapes,
|
|
@@ -364,8 +398,13 @@ they're getting before they rely on a surface area:
|
|
|
364
398
|
happens entirely in PolySimulator's paper matching engine.
|
|
365
399
|
- **Collateral / balances are PAPER.** Your balance is PolySimulator's paper
|
|
366
400
|
balance (the `balance` from `me()` on the native client), not an on-chain USDC
|
|
367
|
-
balance. `get_balance_allowance` reports the paper
|
|
368
|
-
|
|
401
|
+
balance. `get_balance_allowance(asset_type="COLLATERAL")` reports the paper
|
|
402
|
+
account's cash collateral; `get_balance_allowance(asset_type="CONDITIONAL",
|
|
403
|
+
token_id=…)` reports the **conditional-token** balance — the open position's
|
|
404
|
+
share count for that token, in base units (matching real py-sdk, where a
|
|
405
|
+
CONDITIONAL read returns the held conditional token, not collateral; a flat
|
|
406
|
+
position is `0`). There is no real USDC, no allowance/approval transaction, no
|
|
407
|
+
funder wallet — so `allowances` is always empty.
|
|
369
408
|
- **A settled trade carries the per-fill fee RATE, not a fee amount.**
|
|
370
409
|
PolySimulator's matching engine charges and records a real (paper) fee on each
|
|
371
410
|
fill. What it surfaces over the typed read surface is the fee *rate*: a settled
|
|
@@ -802,11 +841,14 @@ Every method maps by one of three strategies:
|
|
|
802
841
|
`py-clob-client` addresses a single outcome token by `token_id`; PolySimulator
|
|
803
842
|
addresses a **market plus an outcome**. The seam: a bare `token_id` is treated as
|
|
804
843
|
the market id with outcome `YES`. Append `":NO"` or `":YES"` to target the other
|
|
805
|
-
side explicitly.
|
|
844
|
+
side explicitly. **Up/Down markets** carry `Up`/`Down` outcomes, so the same
|
|
845
|
+
colon form also accepts `":UP"` / `":DOWN"` (case-insensitive); the backend
|
|
846
|
+
matches the order outcome case-insensitively, so either case is accepted.
|
|
806
847
|
|
|
807
848
|
```python
|
|
808
849
|
client.create_order(OrderArgs(token_id="0xMARKET", ...)) # → market 0xMARKET, YES
|
|
809
850
|
client.create_order(OrderArgs(token_id="0xMARKET:NO", ...)) # → market 0xMARKET, NO
|
|
851
|
+
client.create_order(OrderArgs(token_id="0xUPDOWN:UP", ...)) # → Up/Down market, UP
|
|
810
852
|
```
|
|
811
853
|
|
|
812
854
|
**Reads are true-token-parity; writes use the market+outcome model.** This is an
|
|
@@ -829,8 +871,9 @@ intentional asymmetry:
|
|
|
829
871
|
order loudly — it never silently places a different one.
|
|
830
872
|
|
|
831
873
|
If you only ever pass real outcome-token ids (the `py-clob-client` norm), reads
|
|
832
|
-
"just work" with full parity. The `:YES`/`:NO` suffix
|
|
833
|
-
prefer to address PolySimulator markets
|
|
874
|
+
"just work" with full parity. The `:YES`/`:NO` suffix (and `:UP`/`:DOWN` for
|
|
875
|
+
Up/Down markets) exists for callers who prefer to address PolySimulator markets
|
|
876
|
+
by condition id directly.
|
|
834
877
|
|
|
835
878
|
### Orders are never signed
|
|
836
879
|
|
|
@@ -947,6 +990,66 @@ The client paces itself (a 50 ms floor between requests) and backs off on
|
|
|
947
990
|
`429`/`425`/`5xx` using `Retry-After`. Opt out with `floor_interval=0.0` and
|
|
948
991
|
`max_retries=0` to handle pacing yourself.
|
|
949
992
|
|
|
993
|
+
### New-market warm-up `404` (retry-on-404)
|
|
994
|
+
|
|
995
|
+
The SDK auto-retries `429`/`5xx` but **never** a `404` — a 404 is normally
|
|
996
|
+
permanent. The one benign exception is a **freshly-created market** (most often
|
|
997
|
+
a 5m Up/Down window): for a short window after creation — on the order of ~30s,
|
|
998
|
+
the Up/Down roll cadence, though not a guarantee — it is not yet in the
|
|
999
|
+
order-validation catalog and order placement transiently fails with
|
|
1000
|
+
`ApiError(status_code=404, code="MARKET_NOT_FOUND")`. Retry **only** that signal,
|
|
1001
|
+
with backoff; re-raise any other error (including a 404 with a different `code`,
|
|
1002
|
+
which is a real "not found").
|
|
1003
|
+
|
|
1004
|
+
Use the shipped helper (recommended) …
|
|
1005
|
+
|
|
1006
|
+
```python
|
|
1007
|
+
from polysim_sdk import PolySimClient, retry_on_market_warmup
|
|
1008
|
+
|
|
1009
|
+
with PolySimClient() as client:
|
|
1010
|
+
fill = retry_on_market_warmup(
|
|
1011
|
+
lambda: client.place_order(
|
|
1012
|
+
market_id=cid, side="BUY", outcome="Up", amount="10", price="0.99",
|
|
1013
|
+
),
|
|
1014
|
+
attempts=6, base_delay=2.0,
|
|
1015
|
+
)
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
… or roll your own on the native `ApiError`:
|
|
1019
|
+
|
|
1020
|
+
```python
|
|
1021
|
+
import time
|
|
1022
|
+
from polysim_sdk import ApiError
|
|
1023
|
+
|
|
1024
|
+
for i in range(6):
|
|
1025
|
+
try:
|
|
1026
|
+
fill = client.place_order(market_id=cid, side="BUY", outcome="Up",
|
|
1027
|
+
amount="10", price="0.99")
|
|
1028
|
+
break
|
|
1029
|
+
except ApiError as exc:
|
|
1030
|
+
if exc.status_code == 404 and exc.code == "MARKET_NOT_FOUND" and i < 5:
|
|
1031
|
+
time.sleep(min(2.0 * 2**i, 30.0)) # capped backoff, then retry
|
|
1032
|
+
continue
|
|
1033
|
+
raise # any other error — and the final 404 — propagates
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
With the **`polysim_clob_client` drop-in**, the same condition surfaces as a
|
|
1037
|
+
`PolyApiException` with `status_code == 404`; catch it and retry the same way (or
|
|
1038
|
+
pass your drop-in call to `retry_on_market_warmup(lambda: clob.create_and_post_order(...))`,
|
|
1039
|
+
which works with any callable):
|
|
1040
|
+
|
|
1041
|
+
```python
|
|
1042
|
+
from polysim_clob_client.exceptions import PolyApiException
|
|
1043
|
+
|
|
1044
|
+
try:
|
|
1045
|
+
resp = clob.create_and_post_order(order_args)
|
|
1046
|
+
except PolyApiException as exc:
|
|
1047
|
+
if exc.status_code == 404 and getattr(exc, "code", None) == "MARKET_NOT_FOUND":
|
|
1048
|
+
... # warm-up — retry with backoff
|
|
1049
|
+
else:
|
|
1050
|
+
raise # any other error — including a 404 with a different code — propagates
|
|
1051
|
+
```
|
|
1052
|
+
|
|
950
1053
|
### Troubleshooting: `error code: 1010` / blocked User-Agent
|
|
951
1054
|
|
|
952
1055
|
If a raw HTTP call returns **HTTP 403** with the body `error code: 1010`, the
|
|
@@ -201,6 +201,39 @@ network, tolerant of missing fields): `seconds_to_expiry`, `is_window_open`,
|
|
|
201
201
|
`INTERVALS` vocab tuples. For a **streaming** tap on the underlying, see
|
|
202
202
|
[SSE: live underlying spot](#sse-live-underlying-spot) below.
|
|
203
203
|
|
|
204
|
+
### New-market warm-up: retry order placement on a 404
|
|
205
|
+
|
|
206
|
+
A **brand-new** market — especially the 5m Up/Down windows that roll every few
|
|
207
|
+
minutes — has a short window after creation before it enters the
|
|
208
|
+
order-validation catalog. An order placed in that window transiently fails with
|
|
209
|
+
a `404` (`code="MARKET_NOT_FOUND"`). The window is short — on the order of ~30s,
|
|
210
|
+
the Up/Down roll cadence — but it is a cadence, **not** a guarantee.
|
|
211
|
+
|
|
212
|
+
The SDK does **not** auto-retry 404s (by design — a 404 is normally permanent),
|
|
213
|
+
so a bot trading freshly-rolled markets should catch and retry that one signal
|
|
214
|
+
with backoff. The native client ships a small opt-in helper that retries **only**
|
|
215
|
+
the warm-up 404 and re-raises everything else (and the 404 itself once the budget
|
|
216
|
+
is spent):
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from polysim_sdk import PolySimClient, retry_on_market_warmup
|
|
220
|
+
|
|
221
|
+
with PolySimClient() as client:
|
|
222
|
+
fill = retry_on_market_warmup(
|
|
223
|
+
lambda: client.place_order(
|
|
224
|
+
market_id=cid, side="BUY", outcome="Up", amount="10", price="0.99",
|
|
225
|
+
),
|
|
226
|
+
attempts=6, base_delay=2.0, # ~2,4,8,16,30s capped backoff between tries
|
|
227
|
+
)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
It wraps any zero-argument callable, so the same helper works with a
|
|
231
|
+
`polysim_clob_client` drop-in call (e.g.
|
|
232
|
+
`retry_on_market_warmup(lambda: clob.create_and_post_order(args))`); the drop-in
|
|
233
|
+
surfaces the same condition as a `PolyApiException` with `status_code == 404`.
|
|
234
|
+
See [Errors](#errors) for the standalone retry-on-404 pattern if you'd rather
|
|
235
|
+
roll your own.
|
|
236
|
+
|
|
204
237
|
---
|
|
205
238
|
|
|
206
239
|
## Polymarket compatibility
|
|
@@ -266,7 +299,7 @@ The port is the same mechanical swap as v1 — change the import **prefix**, the
|
|
|
266
299
|
- from polymarket import SecureClient
|
|
267
300
|
+ from polysim_polymarket import SecureClient
|
|
268
301
|
|
|
269
|
-
- client = SecureClient(private_key="0x…")
|
|
302
|
+
- client = SecureClient.create(private_key="0x…") # real Polymarket wallet + EIP-712 signing
|
|
270
303
|
+ client = SecureClient(host="https://api.polysimulator.com", api_key="ps_live_…")
|
|
271
304
|
|
|
272
305
|
# unchanged from here on — same method names, same keyword-only call shapes,
|
|
@@ -329,8 +362,13 @@ they're getting before they rely on a surface area:
|
|
|
329
362
|
happens entirely in PolySimulator's paper matching engine.
|
|
330
363
|
- **Collateral / balances are PAPER.** Your balance is PolySimulator's paper
|
|
331
364
|
balance (the `balance` from `me()` on the native client), not an on-chain USDC
|
|
332
|
-
balance. `get_balance_allowance` reports the paper
|
|
333
|
-
|
|
365
|
+
balance. `get_balance_allowance(asset_type="COLLATERAL")` reports the paper
|
|
366
|
+
account's cash collateral; `get_balance_allowance(asset_type="CONDITIONAL",
|
|
367
|
+
token_id=…)` reports the **conditional-token** balance — the open position's
|
|
368
|
+
share count for that token, in base units (matching real py-sdk, where a
|
|
369
|
+
CONDITIONAL read returns the held conditional token, not collateral; a flat
|
|
370
|
+
position is `0`). There is no real USDC, no allowance/approval transaction, no
|
|
371
|
+
funder wallet — so `allowances` is always empty.
|
|
334
372
|
- **A settled trade carries the per-fill fee RATE, not a fee amount.**
|
|
335
373
|
PolySimulator's matching engine charges and records a real (paper) fee on each
|
|
336
374
|
fill. What it surfaces over the typed read surface is the fee *rate*: a settled
|
|
@@ -767,11 +805,14 @@ Every method maps by one of three strategies:
|
|
|
767
805
|
`py-clob-client` addresses a single outcome token by `token_id`; PolySimulator
|
|
768
806
|
addresses a **market plus an outcome**. The seam: a bare `token_id` is treated as
|
|
769
807
|
the market id with outcome `YES`. Append `":NO"` or `":YES"` to target the other
|
|
770
|
-
side explicitly.
|
|
808
|
+
side explicitly. **Up/Down markets** carry `Up`/`Down` outcomes, so the same
|
|
809
|
+
colon form also accepts `":UP"` / `":DOWN"` (case-insensitive); the backend
|
|
810
|
+
matches the order outcome case-insensitively, so either case is accepted.
|
|
771
811
|
|
|
772
812
|
```python
|
|
773
813
|
client.create_order(OrderArgs(token_id="0xMARKET", ...)) # → market 0xMARKET, YES
|
|
774
814
|
client.create_order(OrderArgs(token_id="0xMARKET:NO", ...)) # → market 0xMARKET, NO
|
|
815
|
+
client.create_order(OrderArgs(token_id="0xUPDOWN:UP", ...)) # → Up/Down market, UP
|
|
775
816
|
```
|
|
776
817
|
|
|
777
818
|
**Reads are true-token-parity; writes use the market+outcome model.** This is an
|
|
@@ -794,8 +835,9 @@ intentional asymmetry:
|
|
|
794
835
|
order loudly — it never silently places a different one.
|
|
795
836
|
|
|
796
837
|
If you only ever pass real outcome-token ids (the `py-clob-client` norm), reads
|
|
797
|
-
"just work" with full parity. The `:YES`/`:NO` suffix
|
|
798
|
-
prefer to address PolySimulator markets
|
|
838
|
+
"just work" with full parity. The `:YES`/`:NO` suffix (and `:UP`/`:DOWN` for
|
|
839
|
+
Up/Down markets) exists for callers who prefer to address PolySimulator markets
|
|
840
|
+
by condition id directly.
|
|
799
841
|
|
|
800
842
|
### Orders are never signed
|
|
801
843
|
|
|
@@ -912,6 +954,66 @@ The client paces itself (a 50 ms floor between requests) and backs off on
|
|
|
912
954
|
`429`/`425`/`5xx` using `Retry-After`. Opt out with `floor_interval=0.0` and
|
|
913
955
|
`max_retries=0` to handle pacing yourself.
|
|
914
956
|
|
|
957
|
+
### New-market warm-up `404` (retry-on-404)
|
|
958
|
+
|
|
959
|
+
The SDK auto-retries `429`/`5xx` but **never** a `404` — a 404 is normally
|
|
960
|
+
permanent. The one benign exception is a **freshly-created market** (most often
|
|
961
|
+
a 5m Up/Down window): for a short window after creation — on the order of ~30s,
|
|
962
|
+
the Up/Down roll cadence, though not a guarantee — it is not yet in the
|
|
963
|
+
order-validation catalog and order placement transiently fails with
|
|
964
|
+
`ApiError(status_code=404, code="MARKET_NOT_FOUND")`. Retry **only** that signal,
|
|
965
|
+
with backoff; re-raise any other error (including a 404 with a different `code`,
|
|
966
|
+
which is a real "not found").
|
|
967
|
+
|
|
968
|
+
Use the shipped helper (recommended) …
|
|
969
|
+
|
|
970
|
+
```python
|
|
971
|
+
from polysim_sdk import PolySimClient, retry_on_market_warmup
|
|
972
|
+
|
|
973
|
+
with PolySimClient() as client:
|
|
974
|
+
fill = retry_on_market_warmup(
|
|
975
|
+
lambda: client.place_order(
|
|
976
|
+
market_id=cid, side="BUY", outcome="Up", amount="10", price="0.99",
|
|
977
|
+
),
|
|
978
|
+
attempts=6, base_delay=2.0,
|
|
979
|
+
)
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
… or roll your own on the native `ApiError`:
|
|
983
|
+
|
|
984
|
+
```python
|
|
985
|
+
import time
|
|
986
|
+
from polysim_sdk import ApiError
|
|
987
|
+
|
|
988
|
+
for i in range(6):
|
|
989
|
+
try:
|
|
990
|
+
fill = client.place_order(market_id=cid, side="BUY", outcome="Up",
|
|
991
|
+
amount="10", price="0.99")
|
|
992
|
+
break
|
|
993
|
+
except ApiError as exc:
|
|
994
|
+
if exc.status_code == 404 and exc.code == "MARKET_NOT_FOUND" and i < 5:
|
|
995
|
+
time.sleep(min(2.0 * 2**i, 30.0)) # capped backoff, then retry
|
|
996
|
+
continue
|
|
997
|
+
raise # any other error — and the final 404 — propagates
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
With the **`polysim_clob_client` drop-in**, the same condition surfaces as a
|
|
1001
|
+
`PolyApiException` with `status_code == 404`; catch it and retry the same way (or
|
|
1002
|
+
pass your drop-in call to `retry_on_market_warmup(lambda: clob.create_and_post_order(...))`,
|
|
1003
|
+
which works with any callable):
|
|
1004
|
+
|
|
1005
|
+
```python
|
|
1006
|
+
from polysim_clob_client.exceptions import PolyApiException
|
|
1007
|
+
|
|
1008
|
+
try:
|
|
1009
|
+
resp = clob.create_and_post_order(order_args)
|
|
1010
|
+
except PolyApiException as exc:
|
|
1011
|
+
if exc.status_code == 404 and getattr(exc, "code", None) == "MARKET_NOT_FOUND":
|
|
1012
|
+
... # warm-up — retry with backoff
|
|
1013
|
+
else:
|
|
1014
|
+
raise # any other error — including a 404 with a different code — propagates
|
|
1015
|
+
```
|
|
1016
|
+
|
|
915
1017
|
### Troubleshooting: `error code: 1010` / blocked User-Agent
|
|
916
1018
|
|
|
917
1019
|
If a raw HTTP call returns **HTTP 403** with the body `error code: 1010`, the
|
|
@@ -30,9 +30,11 @@ from polysim_clob_client.constants import (
|
|
|
30
30
|
ZERO_ADDRESS,
|
|
31
31
|
)
|
|
32
32
|
from polysim_clob_client.exceptions import PolyApiException, PolyException
|
|
33
|
+
from polysim_sdk import __version__
|
|
33
34
|
|
|
34
35
|
__all__ = [
|
|
35
36
|
"ClobClient",
|
|
37
|
+
"__version__",
|
|
36
38
|
"ApiCreds",
|
|
37
39
|
"AssetType",
|
|
38
40
|
"BalanceAllowanceParams",
|
|
@@ -149,7 +149,9 @@ class ClobClient:
|
|
|
149
149
|
|
|
150
150
|
Thin wrapper over :func:`polysim_sdk._shared._split_token` (the shared
|
|
151
151
|
parity seam): a bare ``token_id`` is the market id with outcome
|
|
152
|
-
``YES``; ``":NO"`` / ``":YES"`` targets the other outcome explicitly
|
|
152
|
+
``YES``; ``":NO"`` / ``":YES"`` targets the other outcome explicitly,
|
|
153
|
+
and ``":UP"`` / ``":DOWN"`` targets an UpDown market's outcomes (the
|
|
154
|
+
backend matches the outcome case-insensitively).
|
|
153
155
|
"""
|
|
154
156
|
return _split_token_shared(token_id)
|
|
155
157
|
|
|
@@ -161,9 +163,10 @@ class ClobClient:
|
|
|
161
163
|
condition ids. For those it reverse-resolves via
|
|
162
164
|
``GET /v1/markets-by-token/{token_id}`` (result cached per token), so an
|
|
163
165
|
order placed with a genuine Polymarket token id lands on the right
|
|
164
|
-
market + outcome. The ``condition_id:YES``/``:NO`` colon form
|
|
165
|
-
|
|
166
|
-
:meth:`_split_token`
|
|
166
|
+
market + outcome. The ``condition_id:YES``/``:NO`` colon form (plus
|
|
167
|
+
``:UP``/``:DOWN`` for UpDown markets) and short / non-numeric ids
|
|
168
|
+
resolve **locally with no network call** via :meth:`_split_token`
|
|
169
|
+
(preserving the parity seam).
|
|
167
170
|
"""
|
|
168
171
|
tid = str(token_id)
|
|
169
172
|
# Colon form is our own convenience extension — always resolve locally.
|
|
@@ -191,15 +194,16 @@ class ClobClient:
|
|
|
191
194
|
``GET /v1/book?token_id=...`` endpoint — this is what gives genuine
|
|
192
195
|
parity with Polymarket's CLOB book reads, where py-clob-client always
|
|
193
196
|
passes a real outcome-token id. The ``condition_id:YES`` / ``:NO``
|
|
194
|
-
**colon form**
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
**colon form** (plus ``:UP`` / ``:DOWN`` for UpDown markets) is our own
|
|
198
|
+
convenience extension; it keeps condition-id routing and threads the
|
|
199
|
+
outcome through as a query param so e.g. ``:NO`` reads the NO book and
|
|
200
|
+
``:UP`` reads the UP book.
|
|
197
201
|
"""
|
|
198
202
|
tid = str(token_id)
|
|
199
203
|
if ":" in tid:
|
|
200
204
|
market_id, _, outcome = tid.rpartition(":")
|
|
201
205
|
outcome = outcome.upper()
|
|
202
|
-
if outcome in ("YES", "NO") and market_id:
|
|
206
|
+
if outcome in ("YES", "NO", "UP", "DOWN") and market_id:
|
|
203
207
|
return self._client.get_book(market_id, outcome=outcome)
|
|
204
208
|
return self._client.get_book_by_token(tid)
|
|
205
209
|
|
|
@@ -660,7 +664,8 @@ class ClobClient:
|
|
|
660
664
|
order_type: OrderType | str | None,
|
|
661
665
|
post_only: bool = False,
|
|
662
666
|
) -> dict[str, Any]:
|
|
663
|
-
|
|
667
|
+
embedded_tif = str(order.get("time_in_force", "GTC"))
|
|
668
|
+
tif = embedded_tif
|
|
664
669
|
if order_type is not None:
|
|
665
670
|
tif = order_type.value if hasattr(order_type, "value") else str(order_type)
|
|
666
671
|
# An embedded expiration means GTD was intended (e.g. a `create_order`
|
|
@@ -670,6 +675,17 @@ class ClobClient:
|
|
|
670
675
|
# Never downgrade an expiring order to GTC.
|
|
671
676
|
if order.get("expiration") and tif.upper() == "GTC":
|
|
672
677
|
tif = "GTD"
|
|
678
|
+
# A MARKET order is a marketable-limit FOK/FAK; `create_market_order`
|
|
679
|
+
# embeds that tif. `post_order` defaults `orderType` to GTC, which would
|
|
680
|
+
# otherwise ship the contradictory `order_type=market, time_in_force=GTC`
|
|
681
|
+
# shape. When the caller left `orderType` at the GTC default, preserve the
|
|
682
|
+
# embedded FOK/FAK; an EXPLICIT FOK/FAK/GTD (anything but GTC) still wins.
|
|
683
|
+
if (
|
|
684
|
+
order.get("order_type") == "market"
|
|
685
|
+
and tif.upper() == "GTC"
|
|
686
|
+
and embedded_tif.upper() in ("FOK", "FAK")
|
|
687
|
+
):
|
|
688
|
+
tif = embedded_tif
|
|
673
689
|
return self._client.place_order(
|
|
674
690
|
market_id=order["market_id"],
|
|
675
691
|
side=order["side"],
|
|
@@ -709,7 +725,24 @@ class ClobClient:
|
|
|
709
725
|
bodies: list[dict[str, Any]] = []
|
|
710
726
|
for a in args:
|
|
711
727
|
order = dict(a.order)
|
|
728
|
+
embedded_tif = str(order.get("time_in_force", "GTC"))
|
|
712
729
|
tif = a.orderType.value if hasattr(a.orderType, "value") else str(a.orderType)
|
|
730
|
+
# Mirror _submit's GTD guard: an embedded expiration means GTD was
|
|
731
|
+
# intended (a `create_order(expiration=...)` order). `PostOrdersArgs`
|
|
732
|
+
# defaults `orderType` to GTC, which would otherwise downgrade the
|
|
733
|
+
# order to a contradictory GTC+expiration shape. Never downgrade an
|
|
734
|
+
# expiring order to GTC (an explicit non-GTC orderType still wins —
|
|
735
|
+
# exactly as `_submit` orders the two guards).
|
|
736
|
+
if order.get("expiration") and tif.upper() == "GTC":
|
|
737
|
+
tif = "GTD"
|
|
738
|
+
# Mirror _submit: a market order's embedded FOK/FAK must survive the
|
|
739
|
+
# GTC default of PostOrdersArgs.orderType (an explicit non-GTC wins).
|
|
740
|
+
if (
|
|
741
|
+
order.get("order_type") == "market"
|
|
742
|
+
and tif.upper() == "GTC"
|
|
743
|
+
and embedded_tif.upper() in ("FOK", "FAK")
|
|
744
|
+
):
|
|
745
|
+
tif = embedded_tif
|
|
713
746
|
order["time_in_force"] = tif
|
|
714
747
|
if getattr(a, "postOnly", False):
|
|
715
748
|
order["post_only"] = True
|
|
@@ -747,12 +780,21 @@ class ClobClient:
|
|
|
747
780
|
return self._client.cancel_all()
|
|
748
781
|
|
|
749
782
|
def cancel_market_orders(self, market: str = "", asset_id: str = "") -> dict[str, Any]:
|
|
750
|
-
"""adapt — cancel all orders in a market (``DELETE /v1/cancel-market-orders``).
|
|
783
|
+
"""adapt — cancel all orders in a market (``DELETE /v1/cancel-market-orders``).
|
|
784
|
+
|
|
785
|
+
When only ``asset_id`` is given it is resolved to its market id via
|
|
786
|
+
:meth:`_resolve_token` (NOT the local-only ``_split_token``): a real
|
|
787
|
+
Polymarket CLOB outcome-token id (a long all-digit string) is
|
|
788
|
+
reverse-resolved through ``GET /v1/markets-by-token`` so the cancel scopes
|
|
789
|
+
the right condition id — a local split would treat the whole digit string
|
|
790
|
+
as the market and cancel the wrong (or no) market. The colon form and
|
|
791
|
+
short/non-numeric ids still resolve locally with no network call.
|
|
792
|
+
"""
|
|
751
793
|
market_id = market or asset_id
|
|
752
794
|
if not market_id:
|
|
753
795
|
raise PolyApiException(400, "cancel_market_orders requires market or asset_id.")
|
|
754
796
|
if asset_id and not market:
|
|
755
|
-
market_id, _ = self.
|
|
797
|
+
market_id, _ = self._resolve_token(asset_id)
|
|
756
798
|
return self._client.cancel_market_orders(market_id)
|
|
757
799
|
|
|
758
800
|
# ── order / trade reads ────────────────────────────────────────────────
|
|
@@ -112,6 +112,7 @@ from polysim_polymarket.rfq import (
|
|
|
112
112
|
RfqSession,
|
|
113
113
|
RfqSide,
|
|
114
114
|
)
|
|
115
|
+
from polysim_sdk import __version__
|
|
115
116
|
|
|
116
117
|
# py-sdk exports ``SyncTransactionHandle`` (a ``TypeAlias`` for the
|
|
117
118
|
# gasless|EOA sync-handle union) off its root; a ported bot type-hints
|
|
@@ -151,6 +152,7 @@ from polysim_polymarket.rfq import (
|
|
|
151
152
|
# Each is verified present on py-sdk's root by the re-export subset test.
|
|
152
153
|
__all__ = [
|
|
153
154
|
"PRODUCTION",
|
|
155
|
+
"__version__",
|
|
154
156
|
"AcceptedOrder",
|
|
155
157
|
"ApiKeyCreds",
|
|
156
158
|
"AssetType",
|
|
@@ -17,6 +17,7 @@ authenticated-surface logic the public client has no analog for.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
from collections.abc import Sequence
|
|
20
|
+
from decimal import Decimal, InvalidOperation
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
from polysim_polymarket.errors import UnexpectedResponseError, UserInputError
|
|
@@ -69,8 +70,13 @@ def validate_asset_type(asset_type: object) -> None:
|
|
|
69
70
|
Mirrors py-sdk's ``account._validate_asset_type`` (case-SENSITIVE — there is
|
|
70
71
|
no ``.upper()``): anything else raises ``UserInputError`` with py-sdk's exact
|
|
71
72
|
message, before any read.
|
|
73
|
+
|
|
74
|
+
The ``isinstance(asset_type, str)`` guard runs FIRST so an unhashable arg (a
|
|
75
|
+
``list`` / ``dict`` a bot might pass by mistake) raises ``UserInputError``
|
|
76
|
+
rather than letting a ``TypeError`` escape the ``in`` membership test against
|
|
77
|
+
the frozenset.
|
|
72
78
|
"""
|
|
73
|
-
if asset_type not in VALID_ASSET_TYPES:
|
|
79
|
+
if not isinstance(asset_type, str) or asset_type not in VALID_ASSET_TYPES:
|
|
74
80
|
raise UserInputError(
|
|
75
81
|
f"asset_type must be 'COLLATERAL' or 'CONDITIONAL', got {asset_type!r}."
|
|
76
82
|
)
|
|
@@ -118,6 +124,11 @@ def adapt_balance_allowance(payload: Any) -> BalanceAllowance:
|
|
|
118
124
|
nearest base unit. Paper trading has no on-chain allowance, so ``allowances``
|
|
119
125
|
is empty.
|
|
120
126
|
|
|
127
|
+
The scaling goes through :class:`~decimal.Decimal` (``Decimal(str(raw))``),
|
|
128
|
+
not ``float``, so a monetary value with binary-float drift (e.g. a value
|
|
129
|
+
whose ``float * 1e6`` would round to the wrong base-unit count) converts
|
|
130
|
+
exactly — money never rides a binary float through this adapter.
|
|
131
|
+
|
|
121
132
|
A malformed payload (not a dict, or no numeric balance field) raises
|
|
122
133
|
``UnexpectedResponseError`` rather than silently reporting zero — a porting
|
|
123
134
|
author should see the shape mismatch, not a wrong $0 balance.
|
|
@@ -132,12 +143,51 @@ def adapt_balance_allowance(payload: Any) -> BalanceAllowance:
|
|
|
132
143
|
if raw is None:
|
|
133
144
|
raise UnexpectedResponseError("balance response did not match expected shape")
|
|
134
145
|
try:
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
# ``Decimal(str(raw))`` keeps full decimal precision (str() avoids the
|
|
147
|
+
# binary-float artefacts of Decimal(<float>)); round half-even to the
|
|
148
|
+
# nearest integer base unit.
|
|
149
|
+
usd = Decimal(str(raw))
|
|
150
|
+
except (TypeError, ValueError, InvalidOperation) as error:
|
|
137
151
|
raise UnexpectedResponseError(
|
|
138
152
|
f"balance response 'balance' was not numeric: {raw!r}"
|
|
139
153
|
) from error
|
|
140
|
-
base_units = round(usd * USDC_BASE_UNITS_PER_USD)
|
|
154
|
+
base_units = int(round(usd * USDC_BASE_UNITS_PER_USD))
|
|
155
|
+
return BalanceAllowance(balance=base_units, allowances={})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def adapt_conditional_balance(
|
|
159
|
+
positions: Any, market_id: str, outcome: str
|
|
160
|
+
) -> BalanceAllowance:
|
|
161
|
+
"""Adapt the open-positions list onto a CONDITIONAL ``BalanceAllowance``.
|
|
162
|
+
|
|
163
|
+
Resolution of the CodeRabbit/Codex disagreement: the real py-sdk's
|
|
164
|
+
``get_balance_allowance`` for ``asset_type='CONDITIONAL'`` returns the
|
|
165
|
+
**conditional TOKEN** balance the server holds for that ``token_id`` (the
|
|
166
|
+
position's share count), NOT collateral cash. PolySimulator is paper trading
|
|
167
|
+
with no on-chain CTF balance, so the faithful analog is the open position's
|
|
168
|
+
share count for the resolved ``(market_id, outcome)`` — the conditional token
|
|
169
|
+
Polymarket would settle. Both the conditional token and USDC carry 6 decimals,
|
|
170
|
+
so the share count scales by :data:`USDC_BASE_UNITS_PER_USD`; a flat / absent
|
|
171
|
+
position is a genuine 0 conditional balance (not an error). Outcome matching
|
|
172
|
+
is case-insensitive (positions store ``"Yes"`` / ``"No"``; the token resolver
|
|
173
|
+
returns ``"YES"`` / ``"NO"`` / ``"UP"`` / ``"DOWN"``). Paper trading has no
|
|
174
|
+
on-chain allowance, so ``allowances`` is empty.
|
|
175
|
+
"""
|
|
176
|
+
rows = positions if isinstance(positions, list) else []
|
|
177
|
+
target_outcome = outcome.upper()
|
|
178
|
+
shares = Decimal("0")
|
|
179
|
+
for row in rows:
|
|
180
|
+
if not isinstance(row, dict):
|
|
181
|
+
continue
|
|
182
|
+
if str(row.get("market_id")) != str(market_id):
|
|
183
|
+
continue
|
|
184
|
+
if str(row.get("outcome", "")).upper() != target_outcome:
|
|
185
|
+
continue
|
|
186
|
+
try:
|
|
187
|
+
shares += Decimal(str(row.get("quantity", "0")))
|
|
188
|
+
except (TypeError, ValueError, InvalidOperation):
|
|
189
|
+
continue
|
|
190
|
+
base_units = int(round(shares * USDC_BASE_UNITS_PER_USD))
|
|
141
191
|
return BalanceAllowance(balance=base_units, allowances={})
|
|
142
192
|
|
|
143
193
|
|
|
@@ -571,3 +571,20 @@ def list_markets_forward(
|
|
|
571
571
|
if ascending is not None:
|
|
572
572
|
forward["ascending"] = ascending
|
|
573
573
|
return forward
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def resolve_page_size(page_size: int) -> int:
|
|
577
|
+
"""Validate + clamp a ``list_markets`` page size to the effective request limit.
|
|
578
|
+
|
|
579
|
+
Mirrors py-sdk's ``paginate_offset`` guard — a ``page_size < 1`` raises
|
|
580
|
+
``UserInputError("page_size must be a positive integer.")`` (a non-int /
|
|
581
|
+
bool is also rejected). PolySimulator's ``/v1/markets`` hard-caps a page at
|
|
582
|
+
:data:`PAGE_LIMIT` (100) rows, so a larger ``page_size`` is clamped down to
|
|
583
|
+
that ceiling — the returned value is the effective ``limit`` the fetch +
|
|
584
|
+
cursor boundary use, so ``page_size`` is honoured rather than ignored.
|
|
585
|
+
"""
|
|
586
|
+
if not isinstance(page_size, int) or isinstance(page_size, bool):
|
|
587
|
+
raise UserInputError("page_size must be a positive integer.")
|
|
588
|
+
if page_size < 1:
|
|
589
|
+
raise UserInputError("page_size must be a positive integer.")
|
|
590
|
+
return min(page_size, PAGE_LIMIT)
|