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.
Files changed (51) hide show
  1. {polysimulator-0.4.0 → polysimulator-0.4.2}/PKG-INFO +110 -7
  2. {polysimulator-0.4.0 → polysimulator-0.4.2}/README.md +108 -6
  3. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/__init__.py +2 -0
  4. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/client.py +53 -11
  5. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/__init__.py +2 -0
  6. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/_account.py +54 -4
  7. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/_common.py +17 -0
  8. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/_trade.py +70 -21
  9. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/async_public.py +9 -10
  10. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/async_secure.py +26 -11
  11. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/public.py +9 -9
  12. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/secure.py +32 -13
  13. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/models.py +2 -2
  14. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/rfq.py +17 -1
  15. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_adapt.py +89 -35
  16. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_handle.py +17 -1
  17. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_validators.py +12 -7
  18. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/__init__.py +4 -2
  19. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/_http.py +13 -2
  20. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/_shared.py +22 -6
  21. polysimulator-0.4.2/polysim_sdk/_version.py +28 -0
  22. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/aio.py +3 -2
  23. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/client.py +3 -2
  24. polysimulator-0.4.2/polysim_sdk/retry.py +89 -0
  25. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/sse.py +3 -2
  26. {polysimulator-0.4.0 → polysimulator-0.4.2}/pyproject.toml +5 -1
  27. {polysimulator-0.4.0 → polysimulator-0.4.2}/.gitignore +0 -0
  28. {polysimulator-0.4.0 → polysimulator-0.4.2}/LICENSE +0 -0
  29. {polysimulator-0.4.0 → polysimulator-0.4.2}/NOTICE +0 -0
  30. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/clob_types.py +0 -0
  31. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/constants.py +0 -0
  32. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/exceptions.py +0 -0
  33. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/order_builder/__init__.py +0 -0
  34. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_clob_client/order_builder/constants.py +0 -0
  35. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/__init__.py +0 -0
  36. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/clients/_onchain.py +0 -0
  37. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/environments.py +0 -0
  38. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/errors.py +0 -0
  39. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/pagination.py +0 -0
  40. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/__init__.py +0 -0
  41. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_crypto_events.py +0 -0
  42. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_market_events.py +0 -0
  43. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_runner.py +0 -0
  44. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_specs.py +0 -0
  45. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_stream_open.py +0 -0
  46. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_polymarket/streams/_user_events.py +0 -0
  47. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/constants.py +0 -0
  48. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/exceptions.py +0 -0
  49. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/pagination.py +0 -0
  50. {polysimulator-0.4.0 → polysimulator-0.4.2}/polysim_sdk/updown.py +0 -0
  51. {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.0
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…") # real Polymarket wallet + EIP-712 signing
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 account's collateral; there
368
- is no real USDC, no allowance/approval transaction, no funder wallet.
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 exists for callers who
833
- prefer to address PolySimulator markets by condition id directly.
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…") # real Polymarket wallet + EIP-712 signing
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 account's collateral; there
333
- is no real USDC, no allowance/approval transaction, no funder wallet.
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 exists for callers who
798
- prefer to address PolySimulator markets by condition id directly.
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 and short
165
- / non-numeric ids resolve **locally with no network call** via
166
- :meth:`_split_token` (preserving the parity seam).
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** is our own convenience extension; it keeps condition-id
195
- routing and threads the outcome through as a query param so ``:NO``
196
- actually reads the NO book.
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
- tif = order.get("time_in_force", "GTC")
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._split_token(asset_id)
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
- usd = float(raw)
136
- except (TypeError, ValueError) as error:
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)