polysimulator 0.4.1__tar.gz → 0.4.3__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.1 → polysimulator-0.4.3}/PKG-INFO +16 -10
  2. {polysimulator-0.4.1 → polysimulator-0.4.3}/README.md +12 -7
  3. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/__init__.py +2 -0
  4. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/client.py +41 -3
  5. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/__init__.py +5 -1
  6. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/_account.py +54 -4
  7. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/_common.py +17 -0
  8. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/_trade.py +77 -27
  9. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/async_public.py +4 -7
  10. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/async_secure.py +26 -11
  11. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/public.py +4 -5
  12. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/secure.py +32 -13
  13. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/models.py +3 -3
  14. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/rfq.py +59 -3
  15. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_adapt.py +89 -35
  16. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_handle.py +17 -1
  17. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_validators.py +12 -7
  18. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/__init__.py +2 -2
  19. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/_http.py +3 -2
  20. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/_shared.py +11 -2
  21. polysimulator-0.4.3/polysim_sdk/_version.py +28 -0
  22. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/aio.py +3 -2
  23. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/client.py +3 -2
  24. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/sse.py +3 -2
  25. {polysimulator-0.4.1 → polysimulator-0.4.3}/pyproject.toml +7 -3
  26. {polysimulator-0.4.1 → polysimulator-0.4.3}/.gitignore +0 -0
  27. {polysimulator-0.4.1 → polysimulator-0.4.3}/LICENSE +0 -0
  28. {polysimulator-0.4.1 → polysimulator-0.4.3}/NOTICE +0 -0
  29. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/clob_types.py +0 -0
  30. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/constants.py +0 -0
  31. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/exceptions.py +0 -0
  32. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/order_builder/__init__.py +0 -0
  33. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/order_builder/constants.py +0 -0
  34. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/__init__.py +0 -0
  35. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/_onchain.py +0 -0
  36. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/environments.py +0 -0
  37. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/errors.py +0 -0
  38. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/pagination.py +0 -0
  39. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/__init__.py +0 -0
  40. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_crypto_events.py +0 -0
  41. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_market_events.py +0 -0
  42. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_runner.py +0 -0
  43. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_specs.py +0 -0
  44. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_stream_open.py +0 -0
  45. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_user_events.py +0 -0
  46. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/constants.py +0 -0
  47. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/exceptions.py +0 -0
  48. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/pagination.py +0 -0
  49. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/retry.py +0 -0
  50. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/updown.py +0 -0
  51. {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/ws.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polysimulator
3
- Version: 0.4.1
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).
3
+ Version: 0.4.3
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.0b13).
5
5
  Project-URL: Homepage, https://polysimulator.com
6
6
  Project-URL: Documentation, https://docs.polysimulator.com
7
7
  Project-URL: API Reference, https://api.polysimulator.com/docs
@@ -21,11 +21,12 @@ 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'
27
28
  Requires-Dist: mypy>=1.10; extra == 'dev'
28
- Requires-Dist: polymarket-client==0.1.0b8; extra == 'dev'
29
+ Requires-Dist: polymarket-client==0.1.0b13; extra == 'dev'
29
30
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
31
  Requires-Dist: pytest>=8; extra == 'dev'
31
32
  Requires-Dist: respx>=0.21; extra == 'dev'
@@ -39,9 +40,9 @@ Description-Content-Type: text/markdown
39
40
  [![Python versions](https://img.shields.io/pypi/pyversions/polysimulator.svg)](https://pypi.org/project/polysimulator/)
40
41
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
41
42
  [![py-clob-client compat](https://img.shields.io/badge/py--clob--client-v1%20drop--in-blue.svg)](https://github.com/Polymarket/py-clob-client)
42
- [![py-sdk mirror](https://img.shields.io/badge/polymarket--client-0.1.0b8%20mirror-blueviolet.svg)](https://pypi.org/project/polymarket-client/)
43
+ [![py-sdk mirror](https://img.shields.io/badge/polymarket--client-0.1.0b13%20mirror-blueviolet.svg)](https://pypi.org/project/polymarket-client/)
43
44
 
44
- > **Mirrors Polymarket's unified py-sdk** — `polysim_polymarket` is a paper-mode mirror of [`polymarket-client`](https://pypi.org/project/polymarket-client/) **`0.1.0b8`**: sync + async PublicClient/SecureClient, trading, and core streams. Swap the import prefix + host + auth and your py-sdk bot runs on paper. [Compat matrix ↓](#compat-matrix)
45
+ > **Mirrors Polymarket's unified py-sdk** — `polysim_polymarket` is a paper-mode mirror of [`polymarket-client`](https://pypi.org/project/polymarket-client/) **`0.1.0b13`**: sync + async PublicClient/SecureClient, trading, and core streams. Swap the import prefix + host + auth and your py-sdk bot runs on paper. [Compat matrix ↓](#compat-matrix)
45
46
 
46
47
  The official Python client for the [PolySimulator](https://polysimulator.com)
47
48
  paper-trading API. **One package, three import surfaces:**
@@ -295,7 +296,7 @@ paper-mode-native and not tied to any `py-clob-client` generation.
295
296
  |---|---|
296
297
  | `py-clob-client` v1 (`< 2.0`; latest `0.34.6`) | ✅ Drop-in via `polysim_clob_client` |
297
298
  | `py-clob-client-v2` (`1.0.x`) | ❌ Not supported — use `polysim_sdk`, or pin `py-clob-client<2` |
298
- | unified **py-sdk** (`polymarket-client`, import `polymarket`; `0.1.0b8`) | 🔶 **Paper-mode mirror** via `polysim_polymarket` — sync + async `PublicClient` / `SecureClient` (reads, account/auth, trading) + core streams; on-chain are paper no-ops and Gamma/Data reads are deferred. See [below](#polymarket-py-sdk-v2-mirror) and the [compat matrix](#compat-matrix) |
299
+ | unified **py-sdk** (`polymarket-client`, import `polymarket`; `0.1.0b13`) | 🔶 **Paper-mode mirror** via `polysim_polymarket` — sync + async `PublicClient` / `SecureClient` (reads, account/auth, trading) + core streams; on-chain are paper no-ops and Gamma/Data reads are deferred. See [below](#polymarket-py-sdk-v2-mirror) and the [compat matrix](#compat-matrix) |
299
300
 
300
301
  The v2-only concepts — EIP-712 v2 signing, pUSD collateral, protocol-side fees,
301
302
  builder codes — have no analog in paper trading, so there is nothing to port:
@@ -334,7 +335,7 @@ The port is the same mechanical swap as v1 — change the import **prefix**, the
334
335
  - from polymarket import SecureClient
335
336
  + from polysim_polymarket import SecureClient
336
337
 
337
- - client = SecureClient(private_key="0x…") # real Polymarket wallet + EIP-712 signing
338
+ - client = SecureClient.create(private_key="0x…") # real Polymarket wallet + EIP-712 signing
338
339
  + client = SecureClient(host="https://api.polysimulator.com", api_key="ps_live_…")
339
340
 
340
341
  # unchanged from here on — same method names, same keyword-only call shapes,
@@ -397,8 +398,13 @@ they're getting before they rely on a surface area:
397
398
  happens entirely in PolySimulator's paper matching engine.
398
399
  - **Collateral / balances are PAPER.** Your balance is PolySimulator's paper
399
400
  balance (the `balance` from `me()` on the native client), not an on-chain USDC
400
- balance. `get_balance_allowance` reports the paper account's collateral; there
401
- 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.
402
408
  - **A settled trade carries the per-fill fee RATE, not a fee amount.**
403
409
  PolySimulator's matching engine charges and records a real (paper) fee on each
404
410
  fill. What it surfaces over the typed read surface is the fee *rate*: a settled
@@ -475,7 +481,7 @@ mirrored surface and respected the seams above.
475
481
 
476
482
  ### Compat matrix
477
483
 
478
- > Pinned to **`polymarket-client==0.1.0b8`** — parity is re-locked against this exact pin by the full-surface parity gate; when the pin bumps, this matrix + the badge bump with it.
484
+ > Pinned to **`polymarket-client==0.1.0b13`** — parity is re-locked against this exact pin by the full-surface parity gate; when the pin bumps, this matrix + the badge bump with it.
479
485
 
480
486
  Support level for every py-sdk surface area, cross-checked against what's
481
487
  actually implemented in `polysim_polymarket`:
@@ -4,9 +4,9 @@
4
4
  [![Python versions](https://img.shields.io/pypi/pyversions/polysimulator.svg)](https://pypi.org/project/polysimulator/)
5
5
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
6
6
  [![py-clob-client compat](https://img.shields.io/badge/py--clob--client-v1%20drop--in-blue.svg)](https://github.com/Polymarket/py-clob-client)
7
- [![py-sdk mirror](https://img.shields.io/badge/polymarket--client-0.1.0b8%20mirror-blueviolet.svg)](https://pypi.org/project/polymarket-client/)
7
+ [![py-sdk mirror](https://img.shields.io/badge/polymarket--client-0.1.0b13%20mirror-blueviolet.svg)](https://pypi.org/project/polymarket-client/)
8
8
 
9
- > **Mirrors Polymarket's unified py-sdk** — `polysim_polymarket` is a paper-mode mirror of [`polymarket-client`](https://pypi.org/project/polymarket-client/) **`0.1.0b8`**: sync + async PublicClient/SecureClient, trading, and core streams. Swap the import prefix + host + auth and your py-sdk bot runs on paper. [Compat matrix ↓](#compat-matrix)
9
+ > **Mirrors Polymarket's unified py-sdk** — `polysim_polymarket` is a paper-mode mirror of [`polymarket-client`](https://pypi.org/project/polymarket-client/) **`0.1.0b13`**: sync + async PublicClient/SecureClient, trading, and core streams. Swap the import prefix + host + auth and your py-sdk bot runs on paper. [Compat matrix ↓](#compat-matrix)
10
10
 
11
11
  The official Python client for the [PolySimulator](https://polysimulator.com)
12
12
  paper-trading API. **One package, three import surfaces:**
@@ -260,7 +260,7 @@ paper-mode-native and not tied to any `py-clob-client` generation.
260
260
  |---|---|
261
261
  | `py-clob-client` v1 (`< 2.0`; latest `0.34.6`) | ✅ Drop-in via `polysim_clob_client` |
262
262
  | `py-clob-client-v2` (`1.0.x`) | ❌ Not supported — use `polysim_sdk`, or pin `py-clob-client<2` |
263
- | unified **py-sdk** (`polymarket-client`, import `polymarket`; `0.1.0b8`) | 🔶 **Paper-mode mirror** via `polysim_polymarket` — sync + async `PublicClient` / `SecureClient` (reads, account/auth, trading) + core streams; on-chain are paper no-ops and Gamma/Data reads are deferred. See [below](#polymarket-py-sdk-v2-mirror) and the [compat matrix](#compat-matrix) |
263
+ | unified **py-sdk** (`polymarket-client`, import `polymarket`; `0.1.0b13`) | 🔶 **Paper-mode mirror** via `polysim_polymarket` — sync + async `PublicClient` / `SecureClient` (reads, account/auth, trading) + core streams; on-chain are paper no-ops and Gamma/Data reads are deferred. See [below](#polymarket-py-sdk-v2-mirror) and the [compat matrix](#compat-matrix) |
264
264
 
265
265
  The v2-only concepts — EIP-712 v2 signing, pUSD collateral, protocol-side fees,
266
266
  builder codes — have no analog in paper trading, so there is nothing to port:
@@ -299,7 +299,7 @@ The port is the same mechanical swap as v1 — change the import **prefix**, the
299
299
  - from polymarket import SecureClient
300
300
  + from polysim_polymarket import SecureClient
301
301
 
302
- - client = SecureClient(private_key="0x…") # real Polymarket wallet + EIP-712 signing
302
+ - client = SecureClient.create(private_key="0x…") # real Polymarket wallet + EIP-712 signing
303
303
  + client = SecureClient(host="https://api.polysimulator.com", api_key="ps_live_…")
304
304
 
305
305
  # unchanged from here on — same method names, same keyword-only call shapes,
@@ -362,8 +362,13 @@ they're getting before they rely on a surface area:
362
362
  happens entirely in PolySimulator's paper matching engine.
363
363
  - **Collateral / balances are PAPER.** Your balance is PolySimulator's paper
364
364
  balance (the `balance` from `me()` on the native client), not an on-chain USDC
365
- balance. `get_balance_allowance` reports the paper account's collateral; there
366
- 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.
367
372
  - **A settled trade carries the per-fill fee RATE, not a fee amount.**
368
373
  PolySimulator's matching engine charges and records a real (paper) fee on each
369
374
  fill. What it surfaces over the typed read surface is the fee *rate*: a settled
@@ -440,7 +445,7 @@ mirrored surface and respected the seams above.
440
445
 
441
446
  ### Compat matrix
442
447
 
443
- > Pinned to **`polymarket-client==0.1.0b8`** — parity is re-locked against this exact pin by the full-surface parity gate; when the pin bumps, this matrix + the badge bump with it.
448
+ > Pinned to **`polymarket-client==0.1.0b13`** — parity is re-locked against this exact pin by the full-surface parity gate; when the pin bumps, this matrix + the badge bump with it.
444
449
 
445
450
  Support level for every py-sdk surface area, cross-checked against what's
446
451
  actually implemented in `polysim_polymarket`:
@@ -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",
@@ -664,7 +664,8 @@ class ClobClient:
664
664
  order_type: OrderType | str | None,
665
665
  post_only: bool = False,
666
666
  ) -> dict[str, Any]:
667
- tif = order.get("time_in_force", "GTC")
667
+ embedded_tif = str(order.get("time_in_force", "GTC"))
668
+ tif = embedded_tif
668
669
  if order_type is not None:
669
670
  tif = order_type.value if hasattr(order_type, "value") else str(order_type)
670
671
  # An embedded expiration means GTD was intended (e.g. a `create_order`
@@ -674,6 +675,17 @@ class ClobClient:
674
675
  # Never downgrade an expiring order to GTC.
675
676
  if order.get("expiration") and tif.upper() == "GTC":
676
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
677
689
  return self._client.place_order(
678
690
  market_id=order["market_id"],
679
691
  side=order["side"],
@@ -713,7 +725,24 @@ class ClobClient:
713
725
  bodies: list[dict[str, Any]] = []
714
726
  for a in args:
715
727
  order = dict(a.order)
728
+ embedded_tif = str(order.get("time_in_force", "GTC"))
716
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
717
746
  order["time_in_force"] = tif
718
747
  if getattr(a, "postOnly", False):
719
748
  order["post_only"] = True
@@ -751,12 +780,21 @@ class ClobClient:
751
780
  return self._client.cancel_all()
752
781
 
753
782
  def cancel_market_orders(self, market: str = "", asset_id: str = "") -> dict[str, Any]:
754
- """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
+ """
755
793
  market_id = market or asset_id
756
794
  if not market_id:
757
795
  raise PolyApiException(400, "cancel_market_orders requires market or asset_id.")
758
796
  if asset_id and not market:
759
- market_id, _ = self._split_token(asset_id)
797
+ market_id, _ = self._resolve_token(asset_id)
760
798
  return self._client.cancel_market_orders(market_id)
761
799
 
762
800
  # ── order / trade reads ────────────────────────────────────────────────
@@ -23,7 +23,7 @@ This ships the full mirrored surface: sync + async ``PublicClient`` /
23
23
  ``SecureClient`` covering CLOB market-data reads, account/auth, and the trading
24
24
  write surface, plus on-chain paper no-ops, rewards/builder/RFQ stubs, and the
25
25
  core realtime streams (``market`` / ``user`` / ``crypto_prices``) — a paper-mode
26
- mirror of ``polymarket-client`` ``0.1.0b8``, returning typed pydantic models.
26
+ mirror of ``polymarket-client`` ``0.1.0b13``, returning typed pydantic models.
27
27
  """
28
28
 
29
29
  from __future__ import annotations
@@ -111,7 +111,9 @@ from polysim_polymarket.rfq import (
111
111
  RfqRequestorPublicId,
112
112
  RfqSession,
113
113
  RfqSide,
114
+ RfqTradeEvent,
114
115
  )
116
+ from polysim_sdk import __version__
115
117
 
116
118
  # py-sdk exports ``SyncTransactionHandle`` (a ``TypeAlias`` for the
117
119
  # gasless|EOA sync-handle union) off its root; a ported bot type-hints
@@ -151,6 +153,7 @@ from polysim_polymarket.rfq import (
151
153
  # Each is verified present on py-sdk's root by the re-export subset test.
152
154
  __all__ = [
153
155
  "PRODUCTION",
156
+ "__version__",
154
157
  "AcceptedOrder",
155
158
  "ApiKeyCreds",
156
159
  "AssetType",
@@ -219,6 +222,7 @@ __all__ = [
219
222
  "RfqRequestorPublicId",
220
223
  "RfqSession",
221
224
  "RfqSide",
225
+ "RfqTradeEvent",
222
226
  "SecureClient",
223
227
  "SignedOrder",
224
228
  "SyncTransactionHandle",
@@ -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)
@@ -58,10 +58,11 @@ DEFAULT_SELL_WORST_PRICE = Decimal("0.01")
58
58
  _VALID_MARKET_ORDER_TYPES = frozenset({"FAK", "FOK"})
59
59
 
60
60
  # py-sdk requires a GTD ``expiration`` to be an ABSOLUTE Unix timestamp at least
61
- # this many seconds in the future (``limit._MIN_EXPIRATION_BUFFER_S``); a sub-60s
62
- # or past timestamp is rejected before any request. We mirror the constant + rule
63
- # + message so a ported bot's bad-expiration call raises identically.
64
- _MIN_EXPIRATION_BUFFER_S = 60
61
+ # this many seconds in the future (``limit._MIN_EXPIRATION_BUFFER_S``); a sub-180s
62
+ # or past timestamp is rejected before any request. py-sdk b13 raised this floor
63
+ # from 60s to 180s (3-minute minimum GTD). We mirror the constant + rule + message
64
+ # so a ported bot's bad-expiration call raises identically.
65
+ _MIN_EXPIRATION_BUFFER_S = 180
65
66
 
66
67
  # A long all-digit token id is a real Polymarket CLOB outcome-token id (not a
67
68
  # PolySim condition id); it needs reverse-resolution via the network. Matches the
@@ -138,6 +139,27 @@ _ACCEPTED_STATUS_MAP: dict[str, str] = {
138
139
  "queued": "delayed",
139
140
  }
140
141
 
142
+ # Coarse backend rejection statuses with no finer py-sdk error code. ``/v1/orders``
143
+ # (and the per-row ``/v1/orders/batch`` entries) can return HTTP 200 with a row
144
+ # whose status is ``REJECTED`` / ``ERROR`` / ``FAILED`` / ``CANCELLED``. These map
145
+ # to a RejectedOrder with the "unknown" catch-all code (the more specific statuses
146
+ # in ``_STATUS_TO_ERROR_CODE`` keep their codes). Kept for naming clarity even
147
+ # though acceptance is now decided by the allowlist below, not this denylist —
148
+ # any status outside the accepted set is rejected regardless.
149
+ _REJECTED_STATUSES: frozenset[str] = frozenset(
150
+ {"rejected", "error", "failed", "cancelled", "canceled"}
151
+ )
152
+
153
+ # Acceptance ALLOWLIST — mirrors py-sdk's ``order_response._is_accepted``. The real
154
+ # py-sdk accepts a row ONLY when ``status in {live, matched, delayed}`` AND
155
+ # ``success`` is truthy AND ``order_id != ''`` AND ``error_msg == ''``. We project
156
+ # the paper backend's wider status vocabulary onto those three post-statuses via
157
+ # ``_ACCEPTED_STATUS_MAP``, so the mirror's accepted set is exactly the keys of
158
+ # that map. An UNKNOWN / unexpected status is therefore NOT accepted (it falls
159
+ # through to RejectedOrder), matching py-sdk's strict allowlist rather than the
160
+ # previous denylist (which mistook an unrecognised status for ``matched``).
161
+ _ACCEPTED_STATUSES: frozenset[str] = frozenset(_ACCEPTED_STATUS_MAP)
162
+
141
163
 
142
164
  # ── numeric / side validation ────────────────────────────────────────────────
143
165
  # ``coerce_positive_decimal`` + ``validate_side`` are NOT re-implemented here:
@@ -149,7 +171,14 @@ _ACCEPTED_STATUS_MAP: dict[str, str] = {
149
171
  # not import ``_trade``.
150
172
 
151
173
 
152
- def _validate_token_id(token_id: object) -> str:
174
+ def validate_token_id(token_id: object) -> str:
175
+ """Reject a non-string / empty ``token_id`` like py-sdk's token guard.
176
+
177
+ A non-``str`` raises ``"token_id must be a string, got <type>."`` and an
178
+ empty string raises ``"token_id is required"``. Public so the secure client
179
+ can validate a token UP FRONT — before any reverse-resolution network call —
180
+ rather than only inside ``build_*_order`` after resolution.
181
+ """
153
182
  if not isinstance(token_id, str):
154
183
  raise UserInputError(f"token_id must be a string, got {type(token_id).__name__}.")
155
184
  if not token_id:
@@ -210,7 +239,7 @@ def build_limit_order(
210
239
  Returns a ``SignedOrder`` carrying the trading-semantic fields + the unsigned
211
240
  paper body; signing fields stay empty placeholders.
212
241
  """
213
- _validate_token_id(token_id)
242
+ validate_token_id(token_id)
214
243
  validated_price = coerce_positive_decimal("price", price)
215
244
  validated_size = coerce_positive_decimal("size", size)
216
245
  validate_side(side)
@@ -221,8 +250,8 @@ def build_limit_order(
221
250
  raise UserInputError("expiration must be a non-negative integer.")
222
251
  if expiration < 0:
223
252
  raise UserInputError("expiration must be a non-negative integer.")
224
- # py-sdk: an ABSOLUTE Unix ts must be ≥ now + 60s. Layered AFTER the
225
- # non-negative check, exactly as py-sdk orders the two guards.
253
+ # py-sdk: an ABSOLUTE Unix ts must be ≥ now + 180s (b13's 3-min floor).
254
+ # Layered AFTER the non-negative check, exactly as py-sdk orders the two guards.
226
255
  minimum = int(time.time()) + _MIN_EXPIRATION_BUFFER_S
227
256
  if expiration < minimum:
228
257
  raise UserInputError(
@@ -232,14 +261,18 @@ def build_limit_order(
232
261
 
233
262
  gtd_expiration = expiration if expiration is not None and expiration > 0 else 0
234
263
  order_type = "GTD" if gtd_expiration > 0 else "GTC"
264
+ # Serialize the monetary/size fields as decimal STRINGS, not floats: the
265
+ # backend's order body is a string-decimal contract, and a float would let
266
+ # binary-float drift (e.g. 0.1) ride onto the wire. ``str(Decimal(...))``
267
+ # carries the exact decimal the caller gave.
235
268
  body: dict[str, Any] = {
236
269
  "market_id": market_id,
237
270
  "outcome": outcome,
238
271
  "side": side,
239
- "price": float(validated_price),
272
+ "price": str(validated_price),
240
273
  "order_type": "limit",
241
274
  "time_in_force": order_type,
242
- "quantity": float(validated_size),
275
+ "quantity": str(validated_size),
243
276
  }
244
277
  if post_only:
245
278
  body["post_only"] = True
@@ -288,7 +321,7 @@ def build_market_order(
288
321
  when given, else the :data:`DEFAULT_BUY_WORST_PRICE` /
289
322
  :data:`DEFAULT_SELL_WORST_PRICE` default — so the FOK/FAK is never uncapped.
290
323
  """
291
- _validate_token_id(token_id)
324
+ validate_token_id(token_id)
292
325
  validate_side(side)
293
326
  if order_type not in _VALID_MARKET_ORDER_TYPES:
294
327
  raise UserInputError(f"order_type must be 'FAK' or 'FOK', got {order_type!r}.")
@@ -319,9 +352,10 @@ def build_market_order(
319
352
  if max_price is not None
320
353
  else DEFAULT_BUY_WORST_PRICE
321
354
  )
322
- # USD notional for a market BUY -> sent as ``amount`` (server derives shares).
323
- body["amount"] = float(validated_amount)
324
- body["price"] = float(cap)
355
+ # USD notional for a market BUY -> sent as ``amount`` (server derives
356
+ # shares). Serialized as a decimal string (string-decimal contract).
357
+ body["amount"] = str(validated_amount)
358
+ body["price"] = str(cap)
325
359
  else: # SELL
326
360
  if shares is None:
327
361
  raise UserInputError("shares is required for SELL market orders.")
@@ -337,9 +371,9 @@ def build_market_order(
337
371
  if min_price is not None
338
372
  else DEFAULT_SELL_WORST_PRICE
339
373
  )
340
- # Share count for a market SELL -> sent as ``quantity``.
341
- body["quantity"] = float(validated_shares)
342
- body["price"] = float(cap)
374
+ # Share count for a market SELL -> sent as ``quantity`` (decimal string).
375
+ body["quantity"] = str(validated_shares)
376
+ body["price"] = str(cap)
343
377
 
344
378
  return _make_signed_order(
345
379
  token_id=token_id,
@@ -414,11 +448,16 @@ def adapt_order_response(raw: Any) -> OrderResponse:
414
448
  """Adapt a PolySim ``POST /v1/orders`` reply onto py-sdk's ``OrderResponse``.
415
449
 
416
450
  A malformed reply (not a dict) raises ``UnexpectedResponseError``. Otherwise
417
- we read the order id + status and decide accepted vs rejected: an explicit
418
- ``success: false`` (or a recognised error status) yields a
419
- :class:`RejectedOrder` with a mapped :data:`OrderResponseErrorCode`; anything
420
- else is an :class:`AcceptedOrder`. Field names track py-sdk so a ported bot's
421
- ``resp.ok`` / ``resp.order_id`` / ``resp.status`` read identically.
451
+ we decide accepted vs rejected with py-sdk's strict ALLOWLIST
452
+ (``order_response._is_accepted``): a row is an :class:`AcceptedOrder` ONLY when
453
+ its status is a recognised accepted post-status AND ``success`` is not
454
+ explicitly false AND it carries a non-empty ``order_id`` AND no error message —
455
+ exactly py-sdk's ``status in {live, matched, delayed} AND success AND
456
+ order_id != '' AND error_msg == ''``. ANYTHING else (including an UNKNOWN /
457
+ unexpected status the backend never documented) is a :class:`RejectedOrder`
458
+ with a mapped :data:`OrderResponseErrorCode` (specific codes for the known
459
+ rejection statuses, ``"unknown"`` otherwise). Field names track py-sdk so a
460
+ ported bot's ``resp.ok`` / ``resp.order_id`` / ``resp.status`` read identically.
422
461
  """
423
462
  if not isinstance(raw, dict):
424
463
  raise UnexpectedResponseError("order response did not match expected shape")
@@ -427,13 +466,23 @@ def adapt_order_response(raw: Any) -> OrderResponse:
427
466
  status_key = status_raw.lower()
428
467
  success = raw.get("success")
429
468
  order_id = str(raw.get("order_id", "") or raw.get("id", "") or "")
469
+ error_message = str(
470
+ raw.get("error") or raw.get("message") or raw.get("detail") or ""
471
+ )
430
472
 
431
- rejected = success is False or status_key in _STATUS_TO_ERROR_CODE
432
- if rejected:
473
+ # py-sdk allowlist: accepted requires ALL four a recognised accepted status,
474
+ # success not explicitly false, a non-empty order id, and no error message.
475
+ # (The paper backend omits ``success`` on accepted rows, so absence is truthy;
476
+ # only an explicit ``success: false`` rejects on that axis.)
477
+ accepted = (
478
+ status_key in _ACCEPTED_STATUSES
479
+ and success is not False
480
+ and order_id != ""
481
+ and error_message == ""
482
+ )
483
+ if not accepted:
433
484
  code = _STATUS_TO_ERROR_CODE.get(status_key, "unknown")
434
- message = str(
435
- raw.get("error") or raw.get("message") or raw.get("detail") or status_raw or "rejected"
436
- )
485
+ message = error_message or status_raw or "rejected"
437
486
  return RejectedOrder(code=code, message=message)
438
487
 
439
488
  return AcceptedOrder(
@@ -517,4 +566,5 @@ __all__ = [
517
566
  "paper_order_kwargs",
518
567
  "split_token_local",
519
568
  "validate_cancel_order_ids",
569
+ "validate_token_id",
520
570
  ]
@@ -433,18 +433,15 @@ class AsyncPublicClient:
433
433
  parity and ignored.
434
434
  """
435
435
  forward = _common.list_markets_forward(closed=closed, order=order, ascending=ascending)
436
+ limit = _common.resolve_page_size(page_size)
436
437
 
437
438
  async def fetch(cursor: str | None) -> Page[Market]:
438
439
  offset = _decode_cursor(cursor)
439
440
  if offset < 0:
440
441
  return Page(items=(), has_more=False)
441
- rows = await self._client.list_markets(
442
- limit=_common.PAGE_LIMIT, offset=offset, **forward
443
- )
442
+ rows = await self._client.list_markets(limit=limit, offset=offset, **forward)
444
443
  items = tuple(_common.adapt_market(row) for row in rows)
445
- next_cur = _next_cursor(offset, len(rows), _common.PAGE_LIMIT)
446
- return Page(
447
- items=items, has_more=len(rows) >= _common.PAGE_LIMIT, next_cursor=next_cur
448
- )
444
+ next_cur = _next_cursor(offset, len(rows), limit)
445
+ return Page(items=items, has_more=len(rows) >= limit, next_cursor=next_cur)
449
446
 
450
447
  return AsyncPaginator(fetch=fetch)