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.
- {polysimulator-0.4.1 → polysimulator-0.4.3}/PKG-INFO +16 -10
- {polysimulator-0.4.1 → polysimulator-0.4.3}/README.md +12 -7
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/__init__.py +2 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/client.py +41 -3
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/__init__.py +5 -1
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/_account.py +54 -4
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/_common.py +17 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/_trade.py +77 -27
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/async_public.py +4 -7
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/async_secure.py +26 -11
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/public.py +4 -5
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/secure.py +32 -13
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/models.py +3 -3
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/rfq.py +59 -3
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_adapt.py +89 -35
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_handle.py +17 -1
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_validators.py +12 -7
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/__init__.py +2 -2
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/_http.py +3 -2
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/_shared.py +11 -2
- polysimulator-0.4.3/polysim_sdk/_version.py +28 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/aio.py +3 -2
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/client.py +3 -2
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/sse.py +3 -2
- {polysimulator-0.4.1 → polysimulator-0.4.3}/pyproject.toml +7 -3
- {polysimulator-0.4.1 → polysimulator-0.4.3}/.gitignore +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/LICENSE +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/NOTICE +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/clob_types.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/constants.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/exceptions.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/order_builder/__init__.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_clob_client/order_builder/constants.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/__init__.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/clients/_onchain.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/environments.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/errors.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/pagination.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/__init__.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_crypto_events.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_market_events.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_runner.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_specs.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_stream_open.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_polymarket/streams/_user_events.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/constants.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/exceptions.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/pagination.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/retry.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.3}/polysim_sdk/updown.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
[](https://pypi.org/project/polysimulator/)
|
|
40
41
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
41
42
|
[](https://github.com/Polymarket/py-clob-client)
|
|
42
|
-
[](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.
|
|
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.
|
|
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…")
|
|
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
|
|
401
|
-
|
|
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.
|
|
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
|
[](https://pypi.org/project/polysimulator/)
|
|
5
5
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
6
6
|
[](https://github.com/Polymarket/py-clob-client)
|
|
7
|
-
[](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.
|
|
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.
|
|
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…")
|
|
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
|
|
366
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
136
|
-
|
|
146
|
+
# ``Decimal(str(raw))`` keeps full decimal precision (str() avoids the
|
|
147
|
+
# binary-float artefacts of Decimal(<float>)); round half-even to the
|
|
148
|
+
# nearest integer base unit.
|
|
149
|
+
usd = Decimal(str(raw))
|
|
150
|
+
except (TypeError, ValueError, InvalidOperation) as error:
|
|
137
151
|
raise UnexpectedResponseError(
|
|
138
152
|
f"balance response 'balance' was not numeric: {raw!r}"
|
|
139
153
|
) from error
|
|
140
|
-
base_units = round(usd * USDC_BASE_UNITS_PER_USD)
|
|
154
|
+
base_units = int(round(usd * USDC_BASE_UNITS_PER_USD))
|
|
155
|
+
return BalanceAllowance(balance=base_units, allowances={})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def adapt_conditional_balance(
|
|
159
|
+
positions: Any, market_id: str, outcome: str
|
|
160
|
+
) -> BalanceAllowance:
|
|
161
|
+
"""Adapt the open-positions list onto a CONDITIONAL ``BalanceAllowance``.
|
|
162
|
+
|
|
163
|
+
Resolution of the CodeRabbit/Codex disagreement: the real py-sdk's
|
|
164
|
+
``get_balance_allowance`` for ``asset_type='CONDITIONAL'`` returns the
|
|
165
|
+
**conditional TOKEN** balance the server holds for that ``token_id`` (the
|
|
166
|
+
position's share count), NOT collateral cash. PolySimulator is paper trading
|
|
167
|
+
with no on-chain CTF balance, so the faithful analog is the open position's
|
|
168
|
+
share count for the resolved ``(market_id, outcome)`` — the conditional token
|
|
169
|
+
Polymarket would settle. Both the conditional token and USDC carry 6 decimals,
|
|
170
|
+
so the share count scales by :data:`USDC_BASE_UNITS_PER_USD`; a flat / absent
|
|
171
|
+
position is a genuine 0 conditional balance (not an error). Outcome matching
|
|
172
|
+
is case-insensitive (positions store ``"Yes"`` / ``"No"``; the token resolver
|
|
173
|
+
returns ``"YES"`` / ``"NO"`` / ``"UP"`` / ``"DOWN"``). Paper trading has no
|
|
174
|
+
on-chain allowance, so ``allowances`` is empty.
|
|
175
|
+
"""
|
|
176
|
+
rows = positions if isinstance(positions, list) else []
|
|
177
|
+
target_outcome = outcome.upper()
|
|
178
|
+
shares = Decimal("0")
|
|
179
|
+
for row in rows:
|
|
180
|
+
if not isinstance(row, dict):
|
|
181
|
+
continue
|
|
182
|
+
if str(row.get("market_id")) != str(market_id):
|
|
183
|
+
continue
|
|
184
|
+
if str(row.get("outcome", "")).upper() != target_outcome:
|
|
185
|
+
continue
|
|
186
|
+
try:
|
|
187
|
+
shares += Decimal(str(row.get("quantity", "0")))
|
|
188
|
+
except (TypeError, ValueError, InvalidOperation):
|
|
189
|
+
continue
|
|
190
|
+
base_units = int(round(shares * USDC_BASE_UNITS_PER_USD))
|
|
141
191
|
return BalanceAllowance(balance=base_units, allowances={})
|
|
142
192
|
|
|
143
193
|
|
|
@@ -571,3 +571,20 @@ def list_markets_forward(
|
|
|
571
571
|
if ascending is not None:
|
|
572
572
|
forward["ascending"] = ascending
|
|
573
573
|
return forward
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def resolve_page_size(page_size: int) -> int:
|
|
577
|
+
"""Validate + clamp a ``list_markets`` page size to the effective request limit.
|
|
578
|
+
|
|
579
|
+
Mirrors py-sdk's ``paginate_offset`` guard — a ``page_size < 1`` raises
|
|
580
|
+
``UserInputError("page_size must be a positive integer.")`` (a non-int /
|
|
581
|
+
bool is also rejected). PolySimulator's ``/v1/markets`` hard-caps a page at
|
|
582
|
+
:data:`PAGE_LIMIT` (100) rows, so a larger ``page_size`` is clamped down to
|
|
583
|
+
that ceiling — the returned value is the effective ``limit`` the fetch +
|
|
584
|
+
cursor boundary use, so ``page_size`` is honoured rather than ignored.
|
|
585
|
+
"""
|
|
586
|
+
if not isinstance(page_size, int) or isinstance(page_size, bool):
|
|
587
|
+
raise UserInputError("page_size must be a positive integer.")
|
|
588
|
+
if page_size < 1:
|
|
589
|
+
raise UserInputError("page_size must be a positive integer.")
|
|
590
|
+
return min(page_size, PAGE_LIMIT)
|
|
@@ -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-
|
|
62
|
-
# or past timestamp is rejected before any request.
|
|
63
|
-
#
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
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 +
|
|
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":
|
|
272
|
+
"price": str(validated_price),
|
|
240
273
|
"order_type": "limit",
|
|
241
274
|
"time_in_force": order_type,
|
|
242
|
-
"quantity":
|
|
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
|
-
|
|
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
|
|
323
|
-
|
|
324
|
-
body["
|
|
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"] =
|
|
342
|
-
body["price"] =
|
|
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
|
|
418
|
-
``
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
432
|
-
|
|
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 =
|
|
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),
|
|
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)
|