polysimulator 0.4.0__tar.gz → 0.4.1__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.1}/PKG-INFO +101 -4
- {polysimulator-0.4.0 → polysimulator-0.4.1}/README.md +100 -3
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_clob_client/client.py +12 -8
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/clients/async_public.py +5 -3
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/clients/public.py +5 -4
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/__init__.py +2 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/_http.py +10 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/_shared.py +11 -4
- polysimulator-0.4.1/polysim_sdk/retry.py +89 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/pyproject.toml +1 -1
- {polysimulator-0.4.0 → polysimulator-0.4.1}/.gitignore +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/LICENSE +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/NOTICE +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_clob_client/__init__.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_clob_client/clob_types.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_clob_client/constants.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_clob_client/exceptions.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_clob_client/order_builder/__init__.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_clob_client/order_builder/constants.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/__init__.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/clients/__init__.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/clients/_account.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/clients/_common.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/clients/_onchain.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/clients/_trade.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/clients/async_secure.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/clients/secure.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/environments.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/errors.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/models.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/pagination.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/rfq.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/__init__.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/_adapt.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/_crypto_events.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/_handle.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/_market_events.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/_runner.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/_specs.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/_stream_open.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/_user_events.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_polymarket/streams/_validators.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/aio.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/client.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/constants.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/exceptions.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/pagination.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/sse.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/polysim_sdk/updown.py +0 -0
- {polysimulator-0.4.0 → polysimulator-0.4.1}/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.1
|
|
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
|
|
@@ -236,6 +236,39 @@ network, tolerant of missing fields): `seconds_to_expiry`, `is_window_open`,
|
|
|
236
236
|
`INTERVALS` vocab tuples. For a **streaming** tap on the underlying, see
|
|
237
237
|
[SSE: live underlying spot](#sse-live-underlying-spot) below.
|
|
238
238
|
|
|
239
|
+
### New-market warm-up: retry order placement on a 404
|
|
240
|
+
|
|
241
|
+
A **brand-new** market — especially the 5m Up/Down windows that roll every few
|
|
242
|
+
minutes — has a short window after creation before it enters the
|
|
243
|
+
order-validation catalog. An order placed in that window transiently fails with
|
|
244
|
+
a `404` (`code="MARKET_NOT_FOUND"`). The window is short — on the order of ~30s,
|
|
245
|
+
the Up/Down roll cadence — but it is a cadence, **not** a guarantee.
|
|
246
|
+
|
|
247
|
+
The SDK does **not** auto-retry 404s (by design — a 404 is normally permanent),
|
|
248
|
+
so a bot trading freshly-rolled markets should catch and retry that one signal
|
|
249
|
+
with backoff. The native client ships a small opt-in helper that retries **only**
|
|
250
|
+
the warm-up 404 and re-raises everything else (and the 404 itself once the budget
|
|
251
|
+
is spent):
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from polysim_sdk import PolySimClient, retry_on_market_warmup
|
|
255
|
+
|
|
256
|
+
with PolySimClient() as client:
|
|
257
|
+
fill = retry_on_market_warmup(
|
|
258
|
+
lambda: client.place_order(
|
|
259
|
+
market_id=cid, side="BUY", outcome="Up", amount="10", price="0.99",
|
|
260
|
+
),
|
|
261
|
+
attempts=6, base_delay=2.0, # ~2,4,8,16,30s capped backoff between tries
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
It wraps any zero-argument callable, so the same helper works with a
|
|
266
|
+
`polysim_clob_client` drop-in call (e.g.
|
|
267
|
+
`retry_on_market_warmup(lambda: clob.create_and_post_order(args))`); the drop-in
|
|
268
|
+
surfaces the same condition as a `PolyApiException` with `status_code == 404`.
|
|
269
|
+
See [Errors](#errors) for the standalone retry-on-404 pattern if you'd rather
|
|
270
|
+
roll your own.
|
|
271
|
+
|
|
239
272
|
---
|
|
240
273
|
|
|
241
274
|
## Polymarket compatibility
|
|
@@ -802,11 +835,14 @@ Every method maps by one of three strategies:
|
|
|
802
835
|
`py-clob-client` addresses a single outcome token by `token_id`; PolySimulator
|
|
803
836
|
addresses a **market plus an outcome**. The seam: a bare `token_id` is treated as
|
|
804
837
|
the market id with outcome `YES`. Append `":NO"` or `":YES"` to target the other
|
|
805
|
-
side explicitly.
|
|
838
|
+
side explicitly. **Up/Down markets** carry `Up`/`Down` outcomes, so the same
|
|
839
|
+
colon form also accepts `":UP"` / `":DOWN"` (case-insensitive); the backend
|
|
840
|
+
matches the order outcome case-insensitively, so either case is accepted.
|
|
806
841
|
|
|
807
842
|
```python
|
|
808
843
|
client.create_order(OrderArgs(token_id="0xMARKET", ...)) # → market 0xMARKET, YES
|
|
809
844
|
client.create_order(OrderArgs(token_id="0xMARKET:NO", ...)) # → market 0xMARKET, NO
|
|
845
|
+
client.create_order(OrderArgs(token_id="0xUPDOWN:UP", ...)) # → Up/Down market, UP
|
|
810
846
|
```
|
|
811
847
|
|
|
812
848
|
**Reads are true-token-parity; writes use the market+outcome model.** This is an
|
|
@@ -829,8 +865,9 @@ intentional asymmetry:
|
|
|
829
865
|
order loudly — it never silently places a different one.
|
|
830
866
|
|
|
831
867
|
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
|
|
868
|
+
"just work" with full parity. The `:YES`/`:NO` suffix (and `:UP`/`:DOWN` for
|
|
869
|
+
Up/Down markets) exists for callers who prefer to address PolySimulator markets
|
|
870
|
+
by condition id directly.
|
|
834
871
|
|
|
835
872
|
### Orders are never signed
|
|
836
873
|
|
|
@@ -947,6 +984,66 @@ The client paces itself (a 50 ms floor between requests) and backs off on
|
|
|
947
984
|
`429`/`425`/`5xx` using `Retry-After`. Opt out with `floor_interval=0.0` and
|
|
948
985
|
`max_retries=0` to handle pacing yourself.
|
|
949
986
|
|
|
987
|
+
### New-market warm-up `404` (retry-on-404)
|
|
988
|
+
|
|
989
|
+
The SDK auto-retries `429`/`5xx` but **never** a `404` — a 404 is normally
|
|
990
|
+
permanent. The one benign exception is a **freshly-created market** (most often
|
|
991
|
+
a 5m Up/Down window): for a short window after creation — on the order of ~30s,
|
|
992
|
+
the Up/Down roll cadence, though not a guarantee — it is not yet in the
|
|
993
|
+
order-validation catalog and order placement transiently fails with
|
|
994
|
+
`ApiError(status_code=404, code="MARKET_NOT_FOUND")`. Retry **only** that signal,
|
|
995
|
+
with backoff; re-raise any other error (including a 404 with a different `code`,
|
|
996
|
+
which is a real "not found").
|
|
997
|
+
|
|
998
|
+
Use the shipped helper (recommended) …
|
|
999
|
+
|
|
1000
|
+
```python
|
|
1001
|
+
from polysim_sdk import PolySimClient, retry_on_market_warmup
|
|
1002
|
+
|
|
1003
|
+
with PolySimClient() as client:
|
|
1004
|
+
fill = retry_on_market_warmup(
|
|
1005
|
+
lambda: client.place_order(
|
|
1006
|
+
market_id=cid, side="BUY", outcome="Up", amount="10", price="0.99",
|
|
1007
|
+
),
|
|
1008
|
+
attempts=6, base_delay=2.0,
|
|
1009
|
+
)
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
… or roll your own on the native `ApiError`:
|
|
1013
|
+
|
|
1014
|
+
```python
|
|
1015
|
+
import time
|
|
1016
|
+
from polysim_sdk import ApiError
|
|
1017
|
+
|
|
1018
|
+
for i in range(6):
|
|
1019
|
+
try:
|
|
1020
|
+
fill = client.place_order(market_id=cid, side="BUY", outcome="Up",
|
|
1021
|
+
amount="10", price="0.99")
|
|
1022
|
+
break
|
|
1023
|
+
except ApiError as exc:
|
|
1024
|
+
if exc.status_code == 404 and exc.code == "MARKET_NOT_FOUND" and i < 5:
|
|
1025
|
+
time.sleep(min(2.0 * 2**i, 30.0)) # capped backoff, then retry
|
|
1026
|
+
continue
|
|
1027
|
+
raise # any other error — and the final 404 — propagates
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
With the **`polysim_clob_client` drop-in**, the same condition surfaces as a
|
|
1031
|
+
`PolyApiException` with `status_code == 404`; catch it and retry the same way (or
|
|
1032
|
+
pass your drop-in call to `retry_on_market_warmup(lambda: clob.create_and_post_order(...))`,
|
|
1033
|
+
which works with any callable):
|
|
1034
|
+
|
|
1035
|
+
```python
|
|
1036
|
+
from polysim_clob_client.exceptions import PolyApiException
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
resp = clob.create_and_post_order(order_args)
|
|
1040
|
+
except PolyApiException as exc:
|
|
1041
|
+
if exc.status_code == 404 and getattr(exc, "code", None) == "MARKET_NOT_FOUND":
|
|
1042
|
+
... # warm-up — retry with backoff
|
|
1043
|
+
else:
|
|
1044
|
+
raise # any other error — including a 404 with a different code — propagates
|
|
1045
|
+
```
|
|
1046
|
+
|
|
950
1047
|
### Troubleshooting: `error code: 1010` / blocked User-Agent
|
|
951
1048
|
|
|
952
1049
|
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
|
|
@@ -767,11 +800,14 @@ Every method maps by one of three strategies:
|
|
|
767
800
|
`py-clob-client` addresses a single outcome token by `token_id`; PolySimulator
|
|
768
801
|
addresses a **market plus an outcome**. The seam: a bare `token_id` is treated as
|
|
769
802
|
the market id with outcome `YES`. Append `":NO"` or `":YES"` to target the other
|
|
770
|
-
side explicitly.
|
|
803
|
+
side explicitly. **Up/Down markets** carry `Up`/`Down` outcomes, so the same
|
|
804
|
+
colon form also accepts `":UP"` / `":DOWN"` (case-insensitive); the backend
|
|
805
|
+
matches the order outcome case-insensitively, so either case is accepted.
|
|
771
806
|
|
|
772
807
|
```python
|
|
773
808
|
client.create_order(OrderArgs(token_id="0xMARKET", ...)) # → market 0xMARKET, YES
|
|
774
809
|
client.create_order(OrderArgs(token_id="0xMARKET:NO", ...)) # → market 0xMARKET, NO
|
|
810
|
+
client.create_order(OrderArgs(token_id="0xUPDOWN:UP", ...)) # → Up/Down market, UP
|
|
775
811
|
```
|
|
776
812
|
|
|
777
813
|
**Reads are true-token-parity; writes use the market+outcome model.** This is an
|
|
@@ -794,8 +830,9 @@ intentional asymmetry:
|
|
|
794
830
|
order loudly — it never silently places a different one.
|
|
795
831
|
|
|
796
832
|
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
|
|
833
|
+
"just work" with full parity. The `:YES`/`:NO` suffix (and `:UP`/`:DOWN` for
|
|
834
|
+
Up/Down markets) exists for callers who prefer to address PolySimulator markets
|
|
835
|
+
by condition id directly.
|
|
799
836
|
|
|
800
837
|
### Orders are never signed
|
|
801
838
|
|
|
@@ -912,6 +949,66 @@ The client paces itself (a 50 ms floor between requests) and backs off on
|
|
|
912
949
|
`429`/`425`/`5xx` using `Retry-After`. Opt out with `floor_interval=0.0` and
|
|
913
950
|
`max_retries=0` to handle pacing yourself.
|
|
914
951
|
|
|
952
|
+
### New-market warm-up `404` (retry-on-404)
|
|
953
|
+
|
|
954
|
+
The SDK auto-retries `429`/`5xx` but **never** a `404` — a 404 is normally
|
|
955
|
+
permanent. The one benign exception is a **freshly-created market** (most often
|
|
956
|
+
a 5m Up/Down window): for a short window after creation — on the order of ~30s,
|
|
957
|
+
the Up/Down roll cadence, though not a guarantee — it is not yet in the
|
|
958
|
+
order-validation catalog and order placement transiently fails with
|
|
959
|
+
`ApiError(status_code=404, code="MARKET_NOT_FOUND")`. Retry **only** that signal,
|
|
960
|
+
with backoff; re-raise any other error (including a 404 with a different `code`,
|
|
961
|
+
which is a real "not found").
|
|
962
|
+
|
|
963
|
+
Use the shipped helper (recommended) …
|
|
964
|
+
|
|
965
|
+
```python
|
|
966
|
+
from polysim_sdk import PolySimClient, retry_on_market_warmup
|
|
967
|
+
|
|
968
|
+
with PolySimClient() as client:
|
|
969
|
+
fill = retry_on_market_warmup(
|
|
970
|
+
lambda: client.place_order(
|
|
971
|
+
market_id=cid, side="BUY", outcome="Up", amount="10", price="0.99",
|
|
972
|
+
),
|
|
973
|
+
attempts=6, base_delay=2.0,
|
|
974
|
+
)
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
… or roll your own on the native `ApiError`:
|
|
978
|
+
|
|
979
|
+
```python
|
|
980
|
+
import time
|
|
981
|
+
from polysim_sdk import ApiError
|
|
982
|
+
|
|
983
|
+
for i in range(6):
|
|
984
|
+
try:
|
|
985
|
+
fill = client.place_order(market_id=cid, side="BUY", outcome="Up",
|
|
986
|
+
amount="10", price="0.99")
|
|
987
|
+
break
|
|
988
|
+
except ApiError as exc:
|
|
989
|
+
if exc.status_code == 404 and exc.code == "MARKET_NOT_FOUND" and i < 5:
|
|
990
|
+
time.sleep(min(2.0 * 2**i, 30.0)) # capped backoff, then retry
|
|
991
|
+
continue
|
|
992
|
+
raise # any other error — and the final 404 — propagates
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
With the **`polysim_clob_client` drop-in**, the same condition surfaces as a
|
|
996
|
+
`PolyApiException` with `status_code == 404`; catch it and retry the same way (or
|
|
997
|
+
pass your drop-in call to `retry_on_market_warmup(lambda: clob.create_and_post_order(...))`,
|
|
998
|
+
which works with any callable):
|
|
999
|
+
|
|
1000
|
+
```python
|
|
1001
|
+
from polysim_clob_client.exceptions import PolyApiException
|
|
1002
|
+
|
|
1003
|
+
try:
|
|
1004
|
+
resp = clob.create_and_post_order(order_args)
|
|
1005
|
+
except PolyApiException as exc:
|
|
1006
|
+
if exc.status_code == 404 and getattr(exc, "code", None) == "MARKET_NOT_FOUND":
|
|
1007
|
+
... # warm-up — retry with backoff
|
|
1008
|
+
else:
|
|
1009
|
+
raise # any other error — including a 404 with a different code — propagates
|
|
1010
|
+
```
|
|
1011
|
+
|
|
915
1012
|
### Troubleshooting: `error code: 1010` / blocked User-Agent
|
|
916
1013
|
|
|
917
1014
|
If a raw HTTP call returns **HTTP 403** with the body `error code: 1010`, the
|
|
@@ -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
|
|
|
@@ -165,14 +165,16 @@ class AsyncPublicClient:
|
|
|
165
165
|
|
|
166
166
|
Same routing as the sync client's ``_book_for_token``: a bare token id
|
|
167
167
|
routes to the token-native ``GET /v1/book?token_id=`` endpoint; the
|
|
168
|
-
``condition_id:YES`` / ``:NO`` colon form
|
|
169
|
-
|
|
168
|
+
``condition_id:YES`` / ``:NO`` colon form (plus ``:UP`` / ``:DOWN`` for
|
|
169
|
+
UpDown markets) keeps condition-id routing and threads the outcome
|
|
170
|
+
through as a query param so ``:NO`` reads the NO book and ``:UP`` reads
|
|
171
|
+
the UP book.
|
|
170
172
|
"""
|
|
171
173
|
tid = str(token_id)
|
|
172
174
|
if ":" in tid:
|
|
173
175
|
market_id, _, outcome = tid.rpartition(":")
|
|
174
176
|
outcome = outcome.upper()
|
|
175
|
-
if outcome in ("YES", "NO") and market_id:
|
|
177
|
+
if outcome in ("YES", "NO", "UP", "DOWN") and market_id:
|
|
176
178
|
return await self._client.get_book(market_id, outcome=outcome)
|
|
177
179
|
return await self._client.get_book_by_token(tid)
|
|
178
180
|
|
|
@@ -113,15 +113,16 @@ class PublicClient:
|
|
|
113
113
|
|
|
114
114
|
A **bare** token id routes to the token-native ``GET /v1/book?token_id=``
|
|
115
115
|
endpoint — the parity path, since py-sdk always passes a real
|
|
116
|
-
outcome-token id. The ``condition_id:YES`` / ``:NO`` **colon form**
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
outcome-token id. The ``condition_id:YES`` / ``:NO`` **colon form** (plus
|
|
117
|
+
``:UP`` / ``:DOWN`` for UpDown markets) is our convenience extension: it
|
|
118
|
+
keeps condition-id routing and threads the outcome through as a query
|
|
119
|
+
param so ``:NO`` reads the NO book and ``:UP`` reads the UP book.
|
|
119
120
|
"""
|
|
120
121
|
tid = str(token_id)
|
|
121
122
|
if ":" in tid:
|
|
122
123
|
market_id, _, outcome = tid.rpartition(":")
|
|
123
124
|
outcome = outcome.upper()
|
|
124
|
-
if outcome in ("YES", "NO") and market_id:
|
|
125
|
+
if outcome in ("YES", "NO", "UP", "DOWN") and market_id:
|
|
125
126
|
return self._client.get_book(market_id, outcome=outcome)
|
|
126
127
|
return self._client.get_book_by_token(tid)
|
|
127
128
|
|
|
@@ -36,6 +36,7 @@ from polysim_sdk.exceptions import (
|
|
|
36
36
|
RateLimitError,
|
|
37
37
|
ValidationError,
|
|
38
38
|
)
|
|
39
|
+
from polysim_sdk.retry import retry_on_market_warmup
|
|
39
40
|
|
|
40
41
|
__all__ = [
|
|
41
42
|
"PolySimClient",
|
|
@@ -47,6 +48,7 @@ __all__ = [
|
|
|
47
48
|
"PolySimError",
|
|
48
49
|
"RateLimitError",
|
|
49
50
|
"ValidationError",
|
|
51
|
+
"retry_on_market_warmup",
|
|
50
52
|
]
|
|
51
53
|
|
|
52
54
|
__version__ = "0.2.2"
|
|
@@ -159,6 +159,16 @@ def raise_for_status(resp: httpx.Response) -> None:
|
|
|
159
159
|
code = payload.get("code") or payload.get("error")
|
|
160
160
|
if isinstance(code, dict):
|
|
161
161
|
code = code.get("code") or code.get("error")
|
|
162
|
+
# The authoritative, docs-promised machine code rides in the
|
|
163
|
+
# ``X-Polysim-Code`` response header (always exposed). The body's ``error``
|
|
164
|
+
# field is the PM single-field shape — a human sentence, not a stable code
|
|
165
|
+
# — so the header wins when present. We only fall back to the body-derived
|
|
166
|
+
# value (the back-compat ``{error: <CODE>, message: <text>}`` inline shape)
|
|
167
|
+
# when the header is absent. Header lookup is case-insensitive (httpx
|
|
168
|
+
# ``Headers``); strip stray whitespace and ignore an empty value.
|
|
169
|
+
header_code = (resp.headers.get("X-Polysim-Code") or "").strip()
|
|
170
|
+
if header_code:
|
|
171
|
+
code = header_code
|
|
162
172
|
request_id = resp.headers.get("x-request-id")
|
|
163
173
|
|
|
164
174
|
if resp.status_code in (400, 422):
|
|
@@ -103,15 +103,22 @@ def _split_token(token_id: str) -> tuple[str, str]:
|
|
|
103
103
|
"""Map a py-clob ``token_id`` onto PolySim ``(market_id, outcome)``.
|
|
104
104
|
|
|
105
105
|
py-clob-client addresses a single outcome token; PolySim addresses a
|
|
106
|
-
market plus an outcome
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
market plus an outcome. The parity seam: a bare ``token_id`` is treated
|
|
107
|
+
as the market id with outcome ``YES``; append ``":NO"`` / ``":YES"`` to
|
|
108
|
+
target the other outcome explicitly. UpDown markets carry ``Up`` / ``Down``
|
|
109
|
+
outcomes, so the same colon form also accepts ``":UP"`` / ``":DOWN"``
|
|
110
|
+
(case-insensitive, returned uppercase). The backend matches the outcome
|
|
111
|
+
case-insensitively, so ``"UP"`` / ``"DOWN"`` are accepted as written.
|
|
112
|
+
|
|
113
|
+
The colon form is the drop-in's own convenience extension (it is **not**
|
|
114
|
+
py-clob-client parity, where a real outcome-token id is always passed), so
|
|
115
|
+
accepting the extra UpDown outcomes leaves the v1 parity contract intact.
|
|
109
116
|
"""
|
|
110
117
|
tid = str(token_id)
|
|
111
118
|
if ":" in tid:
|
|
112
119
|
market_id, _, outcome = tid.rpartition(":")
|
|
113
120
|
outcome = outcome.upper()
|
|
114
|
-
if outcome in ("YES", "NO") and market_id:
|
|
121
|
+
if outcome in ("YES", "NO", "UP", "DOWN") and market_id:
|
|
115
122
|
return market_id, outcome
|
|
116
123
|
return tid, "YES"
|
|
117
124
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Opt-in retry helpers for transient, well-understood failure windows.
|
|
2
|
+
|
|
3
|
+
The SDK deliberately does **not** auto-retry ``404`` responses — a 404 is
|
|
4
|
+
normally permanent (unknown market / order id), and silently retrying it would
|
|
5
|
+
hide real bugs behind a long backoff. There is, however, one benign 404: a
|
|
6
|
+
brand-new market (especially the BTC/ETH UpDown 5m windows that roll every few
|
|
7
|
+
minutes) has a short warm-up window after creation before it enters the
|
|
8
|
+
order-validation catalog, during which order placement transiently 404s with
|
|
9
|
+
``code="MARKET_NOT_FOUND"``.
|
|
10
|
+
|
|
11
|
+
:func:`retry_on_market_warmup` is a small, explicit opt-in wrapper for exactly
|
|
12
|
+
that case: it retries a callable **only** on a ``MARKET_NOT_FOUND`` 404 with
|
|
13
|
+
capped exponential backoff, re-raising any other error immediately and the 404
|
|
14
|
+
itself once the attempt budget is spent. It wraps any zero-argument callable, so
|
|
15
|
+
it works with both the native ``polysim_sdk`` client and the ``polysim_clob_client``
|
|
16
|
+
drop-in.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import time
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from typing import TypeVar
|
|
24
|
+
|
|
25
|
+
from polysim_sdk.exceptions import ApiError
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
|
|
29
|
+
# The server's ``error.code`` for an order placed against a market that is not
|
|
30
|
+
# (yet) in the validation catalog. The drop-in's PolyApiException carries the
|
|
31
|
+
# same status code; callers using it can wrap on status_code == 404 themselves.
|
|
32
|
+
MARKET_WARMUP_CODE = "MARKET_NOT_FOUND"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_market_warmup_404(exc: BaseException) -> bool:
|
|
36
|
+
"""True iff ``exc`` is the transient "market not in catalog yet" 404."""
|
|
37
|
+
return (
|
|
38
|
+
isinstance(exc, ApiError)
|
|
39
|
+
and exc.status_code == 404
|
|
40
|
+
and exc.code == MARKET_WARMUP_CODE
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def retry_on_market_warmup(
|
|
45
|
+
fn: Callable[[], T],
|
|
46
|
+
*,
|
|
47
|
+
attempts: int = 6,
|
|
48
|
+
base_delay: float = 2.0,
|
|
49
|
+
max_delay: float = 30.0,
|
|
50
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
51
|
+
) -> T:
|
|
52
|
+
"""Call ``fn`` and retry it through a market's warm-up 404.
|
|
53
|
+
|
|
54
|
+
Retries **only** when ``fn`` raises an :class:`~polysim_sdk.exceptions.ApiError`
|
|
55
|
+
with ``status_code == 404`` and ``code == "MARKET_NOT_FOUND"`` — the transient
|
|
56
|
+
signal that a freshly-created market has not yet entered the order-validation
|
|
57
|
+
catalog (typically clears within ~30s, the UpDown roll cadence; this is a
|
|
58
|
+
cadence, not a guarantee). Any other exception — including a 404 with a
|
|
59
|
+
different ``code`` — is re-raised immediately, and the warm-up 404 itself is
|
|
60
|
+
re-raised once ``attempts`` is exhausted.
|
|
61
|
+
|
|
62
|
+
Backoff is exponential (``base_delay * 2**i``) capped at ``max_delay``; the
|
|
63
|
+
sleep happens **between** attempts only (never after the final one). ``fn``
|
|
64
|
+
is a zero-argument callable, so wrap your call site in a ``lambda`` /
|
|
65
|
+
``functools.partial`` — e.g.
|
|
66
|
+
``retry_on_market_warmup(lambda: client.place_order(...))``. It works with
|
|
67
|
+
both the native client and the ``polysim_clob_client`` drop-in.
|
|
68
|
+
|
|
69
|
+
:param attempts: total number of calls to ``fn`` (must be >= 1).
|
|
70
|
+
:param base_delay: first backoff, in seconds.
|
|
71
|
+
:param max_delay: ceiling for the per-attempt backoff, in seconds.
|
|
72
|
+
:param sleep: injection seam for the backoff sleep (defaults to
|
|
73
|
+
:func:`time.sleep`); override in tests to avoid real waits.
|
|
74
|
+
"""
|
|
75
|
+
if attempts < 1:
|
|
76
|
+
raise ValueError("attempts must be >= 1")
|
|
77
|
+
for i in range(attempts):
|
|
78
|
+
try:
|
|
79
|
+
return fn()
|
|
80
|
+
except ApiError as exc:
|
|
81
|
+
if not _is_market_warmup_404(exc) or i == attempts - 1:
|
|
82
|
+
raise
|
|
83
|
+
sleep(min(base_delay * (2**i), max_delay))
|
|
84
|
+
# Unreachable: the final iteration either returns or re-raises above. Present
|
|
85
|
+
# only so static checkers see every path returns a value or raises.
|
|
86
|
+
raise AssertionError("unreachable") # pragma: no cover
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
__all__ = ["retry_on_market_warmup", "MARKET_WARMUP_CODE"]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "polysimulator"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "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)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|