polysimulator 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {polysimulator-0.4.1 → polysimulator-0.4.2}/PKG-INFO +10 -4
- {polysimulator-0.4.1 → polysimulator-0.4.2}/README.md +8 -3
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_clob_client/__init__.py +2 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_clob_client/client.py +41 -3
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/__init__.py +2 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/clients/_account.py +54 -4
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/clients/_common.py +17 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/clients/_trade.py +70 -21
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/clients/async_public.py +4 -7
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/clients/async_secure.py +26 -11
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/clients/public.py +4 -5
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/clients/secure.py +32 -13
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/models.py +2 -2
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/rfq.py +17 -1
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/_adapt.py +89 -35
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/_handle.py +17 -1
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/_validators.py +12 -7
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/__init__.py +2 -2
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/_http.py +3 -2
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/_shared.py +11 -2
- polysimulator-0.4.2/polysim_sdk/_version.py +28 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/aio.py +3 -2
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/client.py +3 -2
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/sse.py +3 -2
- {polysimulator-0.4.1 → polysimulator-0.4.2}/pyproject.toml +5 -1
- {polysimulator-0.4.1 → polysimulator-0.4.2}/.gitignore +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/LICENSE +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/NOTICE +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_clob_client/clob_types.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_clob_client/constants.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_clob_client/exceptions.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_clob_client/order_builder/__init__.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_clob_client/order_builder/constants.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/clients/__init__.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/clients/_onchain.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/environments.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/errors.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/pagination.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/__init__.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/_crypto_events.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/_market_events.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/_runner.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/_specs.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/_stream_open.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_polymarket/streams/_user_events.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/constants.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/exceptions.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/pagination.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/retry.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/updown.py +0 -0
- {polysimulator-0.4.1 → polysimulator-0.4.2}/polysim_sdk/ws.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polysimulator
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Python SDK for the PolySimulator paper-trading API, with py-clob-client v1 drop-in parity (not py-clob-client-v2) and a polysim_polymarket paper-mode mirror of Polymarket's unified py-sdk (polymarket-client 0.1.0b8).
|
|
5
5
|
Project-URL: Homepage, https://polysimulator.com
|
|
6
6
|
Project-URL: Documentation, https://docs.polysimulator.com
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Office/Business :: Financial
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Requires-Dist: httpx<1,>=0.27
|
|
23
23
|
Requires-Dist: pydantic<3,>=2
|
|
24
|
+
Requires-Dist: typing-extensions>=4
|
|
24
25
|
Requires-Dist: websockets<17,>=12
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: build>=1.0; extra == 'dev'
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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 ────────────────────────────────────────────────
|
|
@@ -112,6 +112,7 @@ from polysim_polymarket.rfq import (
|
|
|
112
112
|
RfqSession,
|
|
113
113
|
RfqSide,
|
|
114
114
|
)
|
|
115
|
+
from polysim_sdk import __version__
|
|
115
116
|
|
|
116
117
|
# py-sdk exports ``SyncTransactionHandle`` (a ``TypeAlias`` for the
|
|
117
118
|
# gasless|EOA sync-handle union) off its root; a ported bot type-hints
|
|
@@ -151,6 +152,7 @@ from polysim_polymarket.rfq import (
|
|
|
151
152
|
# Each is verified present on py-sdk's root by the re-export subset test.
|
|
152
153
|
__all__ = [
|
|
153
154
|
"PRODUCTION",
|
|
155
|
+
"__version__",
|
|
154
156
|
"AcceptedOrder",
|
|
155
157
|
"ApiKeyCreds",
|
|
156
158
|
"AssetType",
|
|
@@ -17,6 +17,7 @@ authenticated-surface logic the public client has no analog for.
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
from collections.abc import Sequence
|
|
20
|
+
from decimal import Decimal, InvalidOperation
|
|
20
21
|
from typing import Any
|
|
21
22
|
|
|
22
23
|
from polysim_polymarket.errors import UnexpectedResponseError, UserInputError
|
|
@@ -69,8 +70,13 @@ def validate_asset_type(asset_type: object) -> None:
|
|
|
69
70
|
Mirrors py-sdk's ``account._validate_asset_type`` (case-SENSITIVE — there is
|
|
70
71
|
no ``.upper()``): anything else raises ``UserInputError`` with py-sdk's exact
|
|
71
72
|
message, before any read.
|
|
73
|
+
|
|
74
|
+
The ``isinstance(asset_type, str)`` guard runs FIRST so an unhashable arg (a
|
|
75
|
+
``list`` / ``dict`` a bot might pass by mistake) raises ``UserInputError``
|
|
76
|
+
rather than letting a ``TypeError`` escape the ``in`` membership test against
|
|
77
|
+
the frozenset.
|
|
72
78
|
"""
|
|
73
|
-
if asset_type not in VALID_ASSET_TYPES:
|
|
79
|
+
if not isinstance(asset_type, str) or asset_type not in VALID_ASSET_TYPES:
|
|
74
80
|
raise UserInputError(
|
|
75
81
|
f"asset_type must be 'COLLATERAL' or 'CONDITIONAL', got {asset_type!r}."
|
|
76
82
|
)
|
|
@@ -118,6 +124,11 @@ def adapt_balance_allowance(payload: Any) -> BalanceAllowance:
|
|
|
118
124
|
nearest base unit. Paper trading has no on-chain allowance, so ``allowances``
|
|
119
125
|
is empty.
|
|
120
126
|
|
|
127
|
+
The scaling goes through :class:`~decimal.Decimal` (``Decimal(str(raw))``),
|
|
128
|
+
not ``float``, so a monetary value with binary-float drift (e.g. a value
|
|
129
|
+
whose ``float * 1e6`` would round to the wrong base-unit count) converts
|
|
130
|
+
exactly — money never rides a binary float through this adapter.
|
|
131
|
+
|
|
121
132
|
A malformed payload (not a dict, or no numeric balance field) raises
|
|
122
133
|
``UnexpectedResponseError`` rather than silently reporting zero — a porting
|
|
123
134
|
author should see the shape mismatch, not a wrong $0 balance.
|
|
@@ -132,12 +143,51 @@ def adapt_balance_allowance(payload: Any) -> BalanceAllowance:
|
|
|
132
143
|
if raw is None:
|
|
133
144
|
raise UnexpectedResponseError("balance response did not match expected shape")
|
|
134
145
|
try:
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
# ``Decimal(str(raw))`` keeps full decimal precision (str() avoids the
|
|
147
|
+
# binary-float artefacts of Decimal(<float>)); round half-even to the
|
|
148
|
+
# nearest integer base unit.
|
|
149
|
+
usd = Decimal(str(raw))
|
|
150
|
+
except (TypeError, ValueError, InvalidOperation) as error:
|
|
137
151
|
raise UnexpectedResponseError(
|
|
138
152
|
f"balance response 'balance' was not numeric: {raw!r}"
|
|
139
153
|
) from error
|
|
140
|
-
base_units = round(usd * USDC_BASE_UNITS_PER_USD)
|
|
154
|
+
base_units = int(round(usd * USDC_BASE_UNITS_PER_USD))
|
|
155
|
+
return BalanceAllowance(balance=base_units, allowances={})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def adapt_conditional_balance(
|
|
159
|
+
positions: Any, market_id: str, outcome: str
|
|
160
|
+
) -> BalanceAllowance:
|
|
161
|
+
"""Adapt the open-positions list onto a CONDITIONAL ``BalanceAllowance``.
|
|
162
|
+
|
|
163
|
+
Resolution of the CodeRabbit/Codex disagreement: the real py-sdk's
|
|
164
|
+
``get_balance_allowance`` for ``asset_type='CONDITIONAL'`` returns the
|
|
165
|
+
**conditional TOKEN** balance the server holds for that ``token_id`` (the
|
|
166
|
+
position's share count), NOT collateral cash. PolySimulator is paper trading
|
|
167
|
+
with no on-chain CTF balance, so the faithful analog is the open position's
|
|
168
|
+
share count for the resolved ``(market_id, outcome)`` — the conditional token
|
|
169
|
+
Polymarket would settle. Both the conditional token and USDC carry 6 decimals,
|
|
170
|
+
so the share count scales by :data:`USDC_BASE_UNITS_PER_USD`; a flat / absent
|
|
171
|
+
position is a genuine 0 conditional balance (not an error). Outcome matching
|
|
172
|
+
is case-insensitive (positions store ``"Yes"`` / ``"No"``; the token resolver
|
|
173
|
+
returns ``"YES"`` / ``"NO"`` / ``"UP"`` / ``"DOWN"``). Paper trading has no
|
|
174
|
+
on-chain allowance, so ``allowances`` is empty.
|
|
175
|
+
"""
|
|
176
|
+
rows = positions if isinstance(positions, list) else []
|
|
177
|
+
target_outcome = outcome.upper()
|
|
178
|
+
shares = Decimal("0")
|
|
179
|
+
for row in rows:
|
|
180
|
+
if not isinstance(row, dict):
|
|
181
|
+
continue
|
|
182
|
+
if str(row.get("market_id")) != str(market_id):
|
|
183
|
+
continue
|
|
184
|
+
if str(row.get("outcome", "")).upper() != target_outcome:
|
|
185
|
+
continue
|
|
186
|
+
try:
|
|
187
|
+
shares += Decimal(str(row.get("quantity", "0")))
|
|
188
|
+
except (TypeError, ValueError, InvalidOperation):
|
|
189
|
+
continue
|
|
190
|
+
base_units = int(round(shares * USDC_BASE_UNITS_PER_USD))
|
|
141
191
|
return BalanceAllowance(balance=base_units, allowances={})
|
|
142
192
|
|
|
143
193
|
|
|
@@ -571,3 +571,20 @@ def list_markets_forward(
|
|
|
571
571
|
if ascending is not None:
|
|
572
572
|
forward["ascending"] = ascending
|
|
573
573
|
return forward
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def resolve_page_size(page_size: int) -> int:
|
|
577
|
+
"""Validate + clamp a ``list_markets`` page size to the effective request limit.
|
|
578
|
+
|
|
579
|
+
Mirrors py-sdk's ``paginate_offset`` guard — a ``page_size < 1`` raises
|
|
580
|
+
``UserInputError("page_size must be a positive integer.")`` (a non-int /
|
|
581
|
+
bool is also rejected). PolySimulator's ``/v1/markets`` hard-caps a page at
|
|
582
|
+
:data:`PAGE_LIMIT` (100) rows, so a larger ``page_size`` is clamped down to
|
|
583
|
+
that ceiling — the returned value is the effective ``limit`` the fetch +
|
|
584
|
+
cursor boundary use, so ``page_size`` is honoured rather than ignored.
|
|
585
|
+
"""
|
|
586
|
+
if not isinstance(page_size, int) or isinstance(page_size, bool):
|
|
587
|
+
raise UserInputError("page_size must be a positive integer.")
|
|
588
|
+
if page_size < 1:
|
|
589
|
+
raise UserInputError("page_size must be a positive integer.")
|
|
590
|
+
return min(page_size, PAGE_LIMIT)
|
|
@@ -138,6 +138,27 @@ _ACCEPTED_STATUS_MAP: dict[str, str] = {
|
|
|
138
138
|
"queued": "delayed",
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
# Coarse backend rejection statuses with no finer py-sdk error code. ``/v1/orders``
|
|
142
|
+
# (and the per-row ``/v1/orders/batch`` entries) can return HTTP 200 with a row
|
|
143
|
+
# whose status is ``REJECTED`` / ``ERROR`` / ``FAILED`` / ``CANCELLED``. These map
|
|
144
|
+
# to a RejectedOrder with the "unknown" catch-all code (the more specific statuses
|
|
145
|
+
# in ``_STATUS_TO_ERROR_CODE`` keep their codes). Kept for naming clarity even
|
|
146
|
+
# though acceptance is now decided by the allowlist below, not this denylist —
|
|
147
|
+
# any status outside the accepted set is rejected regardless.
|
|
148
|
+
_REJECTED_STATUSES: frozenset[str] = frozenset(
|
|
149
|
+
{"rejected", "error", "failed", "cancelled", "canceled"}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Acceptance ALLOWLIST — mirrors py-sdk's ``order_response._is_accepted``. The real
|
|
153
|
+
# py-sdk accepts a row ONLY when ``status in {live, matched, delayed}`` AND
|
|
154
|
+
# ``success`` is truthy AND ``order_id != ''`` AND ``error_msg == ''``. We project
|
|
155
|
+
# the paper backend's wider status vocabulary onto those three post-statuses via
|
|
156
|
+
# ``_ACCEPTED_STATUS_MAP``, so the mirror's accepted set is exactly the keys of
|
|
157
|
+
# that map. An UNKNOWN / unexpected status is therefore NOT accepted (it falls
|
|
158
|
+
# through to RejectedOrder), matching py-sdk's strict allowlist rather than the
|
|
159
|
+
# previous denylist (which mistook an unrecognised status for ``matched``).
|
|
160
|
+
_ACCEPTED_STATUSES: frozenset[str] = frozenset(_ACCEPTED_STATUS_MAP)
|
|
161
|
+
|
|
141
162
|
|
|
142
163
|
# ── numeric / side validation ────────────────────────────────────────────────
|
|
143
164
|
# ``coerce_positive_decimal`` + ``validate_side`` are NOT re-implemented here:
|
|
@@ -149,7 +170,14 @@ _ACCEPTED_STATUS_MAP: dict[str, str] = {
|
|
|
149
170
|
# not import ``_trade``.
|
|
150
171
|
|
|
151
172
|
|
|
152
|
-
def
|
|
173
|
+
def validate_token_id(token_id: object) -> str:
|
|
174
|
+
"""Reject a non-string / empty ``token_id`` like py-sdk's token guard.
|
|
175
|
+
|
|
176
|
+
A non-``str`` raises ``"token_id must be a string, got <type>."`` and an
|
|
177
|
+
empty string raises ``"token_id is required"``. Public so the secure client
|
|
178
|
+
can validate a token UP FRONT — before any reverse-resolution network call —
|
|
179
|
+
rather than only inside ``build_*_order`` after resolution.
|
|
180
|
+
"""
|
|
153
181
|
if not isinstance(token_id, str):
|
|
154
182
|
raise UserInputError(f"token_id must be a string, got {type(token_id).__name__}.")
|
|
155
183
|
if not token_id:
|
|
@@ -210,7 +238,7 @@ def build_limit_order(
|
|
|
210
238
|
Returns a ``SignedOrder`` carrying the trading-semantic fields + the unsigned
|
|
211
239
|
paper body; signing fields stay empty placeholders.
|
|
212
240
|
"""
|
|
213
|
-
|
|
241
|
+
validate_token_id(token_id)
|
|
214
242
|
validated_price = coerce_positive_decimal("price", price)
|
|
215
243
|
validated_size = coerce_positive_decimal("size", size)
|
|
216
244
|
validate_side(side)
|
|
@@ -232,14 +260,18 @@ def build_limit_order(
|
|
|
232
260
|
|
|
233
261
|
gtd_expiration = expiration if expiration is not None and expiration > 0 else 0
|
|
234
262
|
order_type = "GTD" if gtd_expiration > 0 else "GTC"
|
|
263
|
+
# Serialize the monetary/size fields as decimal STRINGS, not floats: the
|
|
264
|
+
# backend's order body is a string-decimal contract, and a float would let
|
|
265
|
+
# binary-float drift (e.g. 0.1) ride onto the wire. ``str(Decimal(...))``
|
|
266
|
+
# carries the exact decimal the caller gave.
|
|
235
267
|
body: dict[str, Any] = {
|
|
236
268
|
"market_id": market_id,
|
|
237
269
|
"outcome": outcome,
|
|
238
270
|
"side": side,
|
|
239
|
-
"price":
|
|
271
|
+
"price": str(validated_price),
|
|
240
272
|
"order_type": "limit",
|
|
241
273
|
"time_in_force": order_type,
|
|
242
|
-
"quantity":
|
|
274
|
+
"quantity": str(validated_size),
|
|
243
275
|
}
|
|
244
276
|
if post_only:
|
|
245
277
|
body["post_only"] = True
|
|
@@ -288,7 +320,7 @@ def build_market_order(
|
|
|
288
320
|
when given, else the :data:`DEFAULT_BUY_WORST_PRICE` /
|
|
289
321
|
:data:`DEFAULT_SELL_WORST_PRICE` default — so the FOK/FAK is never uncapped.
|
|
290
322
|
"""
|
|
291
|
-
|
|
323
|
+
validate_token_id(token_id)
|
|
292
324
|
validate_side(side)
|
|
293
325
|
if order_type not in _VALID_MARKET_ORDER_TYPES:
|
|
294
326
|
raise UserInputError(f"order_type must be 'FAK' or 'FOK', got {order_type!r}.")
|
|
@@ -319,9 +351,10 @@ def build_market_order(
|
|
|
319
351
|
if max_price is not None
|
|
320
352
|
else DEFAULT_BUY_WORST_PRICE
|
|
321
353
|
)
|
|
322
|
-
# USD notional for a market BUY -> sent as ``amount`` (server derives
|
|
323
|
-
|
|
324
|
-
body["
|
|
354
|
+
# USD notional for a market BUY -> sent as ``amount`` (server derives
|
|
355
|
+
# shares). Serialized as a decimal string (string-decimal contract).
|
|
356
|
+
body["amount"] = str(validated_amount)
|
|
357
|
+
body["price"] = str(cap)
|
|
325
358
|
else: # SELL
|
|
326
359
|
if shares is None:
|
|
327
360
|
raise UserInputError("shares is required for SELL market orders.")
|
|
@@ -337,9 +370,9 @@ def build_market_order(
|
|
|
337
370
|
if min_price is not None
|
|
338
371
|
else DEFAULT_SELL_WORST_PRICE
|
|
339
372
|
)
|
|
340
|
-
# Share count for a market SELL -> sent as ``quantity
|
|
341
|
-
body["quantity"] =
|
|
342
|
-
body["price"] =
|
|
373
|
+
# Share count for a market SELL -> sent as ``quantity`` (decimal string).
|
|
374
|
+
body["quantity"] = str(validated_shares)
|
|
375
|
+
body["price"] = str(cap)
|
|
343
376
|
|
|
344
377
|
return _make_signed_order(
|
|
345
378
|
token_id=token_id,
|
|
@@ -414,11 +447,16 @@ def adapt_order_response(raw: Any) -> OrderResponse:
|
|
|
414
447
|
"""Adapt a PolySim ``POST /v1/orders`` reply onto py-sdk's ``OrderResponse``.
|
|
415
448
|
|
|
416
449
|
A malformed reply (not a dict) raises ``UnexpectedResponseError``. Otherwise
|
|
417
|
-
we
|
|
418
|
-
``
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
450
|
+
we decide accepted vs rejected with py-sdk's strict ALLOWLIST
|
|
451
|
+
(``order_response._is_accepted``): a row is an :class:`AcceptedOrder` ONLY when
|
|
452
|
+
its status is a recognised accepted post-status AND ``success`` is not
|
|
453
|
+
explicitly false AND it carries a non-empty ``order_id`` AND no error message —
|
|
454
|
+
exactly py-sdk's ``status in {live, matched, delayed} AND success AND
|
|
455
|
+
order_id != '' AND error_msg == ''``. ANYTHING else (including an UNKNOWN /
|
|
456
|
+
unexpected status the backend never documented) is a :class:`RejectedOrder`
|
|
457
|
+
with a mapped :data:`OrderResponseErrorCode` (specific codes for the known
|
|
458
|
+
rejection statuses, ``"unknown"`` otherwise). Field names track py-sdk so a
|
|
459
|
+
ported bot's ``resp.ok`` / ``resp.order_id`` / ``resp.status`` read identically.
|
|
422
460
|
"""
|
|
423
461
|
if not isinstance(raw, dict):
|
|
424
462
|
raise UnexpectedResponseError("order response did not match expected shape")
|
|
@@ -427,13 +465,23 @@ def adapt_order_response(raw: Any) -> OrderResponse:
|
|
|
427
465
|
status_key = status_raw.lower()
|
|
428
466
|
success = raw.get("success")
|
|
429
467
|
order_id = str(raw.get("order_id", "") or raw.get("id", "") or "")
|
|
468
|
+
error_message = str(
|
|
469
|
+
raw.get("error") or raw.get("message") or raw.get("detail") or ""
|
|
470
|
+
)
|
|
430
471
|
|
|
431
|
-
|
|
432
|
-
|
|
472
|
+
# py-sdk allowlist: accepted requires ALL four — a recognised accepted status,
|
|
473
|
+
# success not explicitly false, a non-empty order id, and no error message.
|
|
474
|
+
# (The paper backend omits ``success`` on accepted rows, so absence is truthy;
|
|
475
|
+
# only an explicit ``success: false`` rejects on that axis.)
|
|
476
|
+
accepted = (
|
|
477
|
+
status_key in _ACCEPTED_STATUSES
|
|
478
|
+
and success is not False
|
|
479
|
+
and order_id != ""
|
|
480
|
+
and error_message == ""
|
|
481
|
+
)
|
|
482
|
+
if not accepted:
|
|
433
483
|
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
|
-
)
|
|
484
|
+
message = error_message or status_raw or "rejected"
|
|
437
485
|
return RejectedOrder(code=code, message=message)
|
|
438
486
|
|
|
439
487
|
return AcceptedOrder(
|
|
@@ -517,4 +565,5 @@ __all__ = [
|
|
|
517
565
|
"paper_order_kwargs",
|
|
518
566
|
"split_token_local",
|
|
519
567
|
"validate_cancel_order_ids",
|
|
568
|
+
"validate_token_id",
|
|
520
569
|
]
|
|
@@ -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)
|
|
@@ -57,7 +57,7 @@ import logging
|
|
|
57
57
|
from collections.abc import Sequence
|
|
58
58
|
from decimal import Decimal
|
|
59
59
|
from types import TracebackType
|
|
60
|
-
from typing import
|
|
60
|
+
from typing import Any, Literal, overload
|
|
61
61
|
|
|
62
62
|
from polysim_polymarket.clients import _account, _onchain, _trade
|
|
63
63
|
from polysim_polymarket.clients.async_public import AsyncPublicClient
|
|
@@ -106,13 +106,14 @@ from polysim_polymarket.streams import (
|
|
|
106
106
|
)
|
|
107
107
|
from polysim_polymarket.streams._stream_open import open_secure_stream as _open_secure_stream
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
# ``TransactionHandle`` is the py-sdk name for the ASYNC on-chain return type (an
|
|
110
|
+
# alias of the async paper handle, defined in ``_onchain``). The on-chain methods
|
|
111
|
+
# annotate ``-> TransactionHandle`` with the bare py-sdk name (annotation parity);
|
|
112
|
+
# ``_onchain`` is already imported above, so bind the name into this module's
|
|
113
|
+
# globals at RUNTIME (not only under TYPE_CHECKING) — otherwise
|
|
114
|
+
# ``typing.get_type_hints(AsyncSecureClient.<onchain method>)`` raises
|
|
115
|
+
# ``NameError``. The returned object is the same ``_onchain`` async paper handle.
|
|
116
|
+
TransactionHandle = _onchain.TransactionHandle
|
|
116
117
|
|
|
117
118
|
|
|
118
119
|
class AsyncSecureClient:
|
|
@@ -489,11 +490,20 @@ class AsyncSecureClient:
|
|
|
489
490
|
|
|
490
491
|
Async twin of ``SecureClient.get_balance_allowance``: validates
|
|
491
492
|
``asset_type`` (case-SENSITIVE) via the shared
|
|
492
|
-
:func:`_account.validate_asset_type
|
|
493
|
-
|
|
494
|
-
|
|
493
|
+
:func:`_account.validate_asset_type`. ``COLLATERAL`` awaits
|
|
494
|
+
``GET /v1/account/balance`` and adapts the paper-cash figure;
|
|
495
|
+
``CONDITIONAL`` (token_id required) resolves the token, awaits
|
|
496
|
+
``GET /v1/account/positions`` and reports the open position's share count
|
|
497
|
+
as the conditional-token balance (matching real py-sdk's CONDITIONAL
|
|
498
|
+
semantics — the held conditional token, not collateral; flat = 0).
|
|
495
499
|
"""
|
|
496
500
|
_account.validate_asset_type(asset_type)
|
|
501
|
+
if asset_type == "CONDITIONAL":
|
|
502
|
+
if not token_id:
|
|
503
|
+
raise UserInputError("token_id is required for a CONDITIONAL balance.")
|
|
504
|
+
market_id, outcome = await self._resolve_coordinates(token_id)
|
|
505
|
+
positions = await self._client.positions()
|
|
506
|
+
return _account.adapt_conditional_balance(positions, market_id, outcome)
|
|
497
507
|
payload = await self._client.balance()
|
|
498
508
|
return _account.adapt_balance_allowance(payload)
|
|
499
509
|
|
|
@@ -598,7 +608,12 @@ class AsyncSecureClient:
|
|
|
598
608
|
reverse-resolved by awaiting ``GET /v1/markets-by-token/{id}`` (cached per
|
|
599
609
|
token). All routing logic is the transport-free ``_trade`` seam; only the
|
|
600
610
|
awaited fetch lives here.
|
|
611
|
+
|
|
612
|
+
``token_id`` is validated (non-empty string) UP FRONT — before the
|
|
613
|
+
reverse-resolution network call — so a bad token raises ``UserInputError``
|
|
614
|
+
rather than hitting the network.
|
|
601
615
|
"""
|
|
616
|
+
_trade.validate_token_id(token_id)
|
|
602
617
|
if not _trade.needs_token_reverse_resolution(token_id):
|
|
603
618
|
return _trade.split_token_local(token_id)
|
|
604
619
|
cached = self._token_coordinates.get(token_id)
|
|
@@ -428,16 +428,15 @@ class PublicClient:
|
|
|
428
428
|
shared cursor<->offset helpers.
|
|
429
429
|
"""
|
|
430
430
|
forward = _common.list_markets_forward(closed=closed, order=order, ascending=ascending)
|
|
431
|
+
limit = _common.resolve_page_size(page_size)
|
|
431
432
|
|
|
432
433
|
def fetch(cursor: str | None) -> Page[Market]:
|
|
433
434
|
offset = _decode_cursor(cursor)
|
|
434
435
|
if offset < 0:
|
|
435
436
|
return Page(items=(), has_more=False)
|
|
436
|
-
rows = self._client.list_markets(limit=
|
|
437
|
+
rows = self._client.list_markets(limit=limit, offset=offset, **forward)
|
|
437
438
|
items = tuple(_common.adapt_market(row) for row in rows)
|
|
438
|
-
next_cur = _next_cursor(offset, len(rows),
|
|
439
|
-
return Page(
|
|
440
|
-
items=items, has_more=len(rows) >= _common.PAGE_LIMIT, next_cursor=next_cur
|
|
441
|
-
)
|
|
439
|
+
next_cur = _next_cursor(offset, len(rows), limit)
|
|
440
|
+
return Page(items=items, has_more=len(rows) >= limit, next_cursor=next_cur)
|
|
442
441
|
|
|
443
442
|
return Paginator(fetch=fetch)
|
|
@@ -67,7 +67,7 @@ import logging
|
|
|
67
67
|
from collections.abc import Sequence
|
|
68
68
|
from decimal import Decimal
|
|
69
69
|
from types import TracebackType
|
|
70
|
-
from typing import
|
|
70
|
+
from typing import Any, Literal
|
|
71
71
|
|
|
72
72
|
from polysim_polymarket.clients import _account, _onchain, _trade
|
|
73
73
|
from polysim_polymarket.clients.public import PublicClient
|
|
@@ -106,13 +106,15 @@ from polysim_polymarket.models import (
|
|
|
106
106
|
)
|
|
107
107
|
from polysim_polymarket.pagination import Page, Paginator, _EmptyPaginator
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
# ``SyncTransactionHandle`` is the py-sdk name for the on-chain return type (an
|
|
110
|
+
# alias of the paper handle, defined in ``_onchain``). The on-chain methods
|
|
111
|
+
# annotate ``-> SyncTransactionHandle`` with the bare py-sdk name (annotation
|
|
112
|
+
# parity); ``_onchain`` is already imported above, so bind the name into this
|
|
113
|
+
# module's globals at RUNTIME (not only under TYPE_CHECKING) — otherwise
|
|
114
|
+
# ``typing.get_type_hints(SecureClient.<onchain method>)`` raises ``NameError``,
|
|
115
|
+
# breaking any tool / doc generator that introspects the client. The returned
|
|
116
|
+
# object is the same ``_onchain`` paper handle either way.
|
|
117
|
+
SyncTransactionHandle = _onchain.SyncTransactionHandle
|
|
116
118
|
|
|
117
119
|
|
|
118
120
|
class SecureClient:
|
|
@@ -450,13 +452,25 @@ class SecureClient:
|
|
|
450
452
|
case-SENSITIVE (``"COLLATERAL"`` / ``"CONDITIONAL"`` only — py-sdk's
|
|
451
453
|
contract); anything else raises ``UserInputError`` before any read.
|
|
452
454
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
455
|
+
* ``COLLATERAL`` reads ``GET /v1/account/balance`` and adapts the (USD)
|
|
456
|
+
paper-cash figure onto py-sdk's base-unit shape (USDC has 6 decimals,
|
|
457
|
+
so 1 USD = 1_000_000 base units).
|
|
458
|
+
* ``CONDITIONAL`` reports the conditional TOKEN balance — matching the
|
|
459
|
+
real py-sdk, where a CONDITIONAL read returns the held conditional-token
|
|
460
|
+
balance for ``token_id`` (NOT collateral). ``token_id`` is required; it
|
|
461
|
+
is resolved to ``(market_id, outcome)`` and the open position's share
|
|
462
|
+
count is reported in base units (a flat position is a genuine 0).
|
|
463
|
+
|
|
464
|
+
Paper trading has no on-chain allowance, so ``allowances`` is empty —
|
|
465
|
+
matching the v1 mirror's "no allowance on paper" semantics.
|
|
458
466
|
"""
|
|
459
467
|
_account.validate_asset_type(asset_type)
|
|
468
|
+
if asset_type == "CONDITIONAL":
|
|
469
|
+
if not token_id:
|
|
470
|
+
raise UserInputError("token_id is required for a CONDITIONAL balance.")
|
|
471
|
+
market_id, outcome = self._resolve_coordinates(token_id)
|
|
472
|
+
positions = self._client.positions()
|
|
473
|
+
return _account.adapt_conditional_balance(positions, market_id, outcome)
|
|
460
474
|
payload = self._client.balance()
|
|
461
475
|
return _account.adapt_balance_allowance(payload)
|
|
462
476
|
|
|
@@ -572,7 +586,12 @@ class SecureClient:
|
|
|
572
586
|
market + outcome. All the routing logic is the transport-free
|
|
573
587
|
:mod:`~polysim_polymarket.clients._trade` seam; only the network fetch
|
|
574
588
|
lives here.
|
|
589
|
+
|
|
590
|
+
``token_id`` is validated (non-empty string) UP FRONT — before the
|
|
591
|
+
reverse-resolution ``GET /v1/markets-by-token`` network call — so a bad
|
|
592
|
+
token raises ``UserInputError`` rather than hitting the network.
|
|
575
593
|
"""
|
|
594
|
+
_trade.validate_token_id(token_id)
|
|
576
595
|
if not _trade.needs_token_reverse_resolution(token_id):
|
|
577
596
|
return _trade.split_token_local(token_id)
|
|
578
597
|
cached = self._token_coordinates.get(token_id)
|
|
@@ -21,7 +21,7 @@ exactly so the scalar types match. Timestamps mirror py-sdk's epoch-ms parsing.
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
from dataclasses import dataclass
|
|
24
|
-
from datetime import
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
25
|
from decimal import Decimal
|
|
26
26
|
from typing import Annotated, Literal, NamedTuple, TypeAlias
|
|
27
27
|
|
|
@@ -80,7 +80,7 @@ def _parse_epoch_ms_timestamp(value: object) -> object:
|
|
|
80
80
|
raise ValueError(f"invalid epoch-ms timestamp: {value!r}")
|
|
81
81
|
ms = int(value)
|
|
82
82
|
try:
|
|
83
|
-
return datetime.fromtimestamp(ms / 1000, tz=
|
|
83
|
+
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
|
84
84
|
except (OverflowError, OSError, ValueError) as error:
|
|
85
85
|
raise ValueError(f"invalid epoch-ms timestamp: {value!r}") from error
|
|
86
86
|
|
|
@@ -24,13 +24,29 @@ from __future__ import annotations
|
|
|
24
24
|
from collections.abc import AsyncIterator, Generator
|
|
25
25
|
from dataclasses import dataclass, field
|
|
26
26
|
from decimal import Decimal
|
|
27
|
-
from enum import
|
|
27
|
+
from enum import Enum
|
|
28
28
|
from types import TracebackType
|
|
29
29
|
from typing import Any, Literal, Protocol, TypeAlias, runtime_checkable
|
|
30
30
|
|
|
31
31
|
from polysim_polymarket.clients._onchain import RFQ_NOT_SIMULATED
|
|
32
32
|
from polysim_polymarket.errors import PolyException
|
|
33
33
|
|
|
34
|
+
|
|
35
|
+
class StrEnum(str, Enum):
|
|
36
|
+
"""3.10-safe stand-in for ``enum.StrEnum`` (3.11+).
|
|
37
|
+
|
|
38
|
+
``enum.StrEnum`` only exists from 3.11; the package floor is 3.10, so we
|
|
39
|
+
mirror its observable behaviour with the classic ``(str, Enum)`` mixin —
|
|
40
|
+
members compare/hash equal to their string value and ``str(member)`` is the
|
|
41
|
+
value. The one cosmetic difference (``StrEnum``'s ``__str__`` returns the
|
|
42
|
+
value directly) is replicated below so a ported bot's ``str(RfqSide.YES)``
|
|
43
|
+
yields ``"YES"`` on both Python versions.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return str(self.value)
|
|
48
|
+
|
|
49
|
+
|
|
34
50
|
# Id aliases — bare ``str`` NewType-equivalents (py-sdk uses ``TypeAlias = str``).
|
|
35
51
|
RfqId: TypeAlias = str
|
|
36
52
|
RfqQuoteId: TypeAlias = str
|
|
@@ -67,6 +67,7 @@ SEAMS — documented, not fabricated:
|
|
|
67
67
|
|
|
68
68
|
from __future__ import annotations
|
|
69
69
|
|
|
70
|
+
from datetime import datetime, timezone
|
|
70
71
|
from typing import Any
|
|
71
72
|
|
|
72
73
|
from polysim_polymarket.streams._crypto_events import (
|
|
@@ -151,6 +152,34 @@ def _split_condition_id(token_id: str) -> str:
|
|
|
151
152
|
return tid
|
|
152
153
|
|
|
153
154
|
|
|
155
|
+
def _parse_fill_timestamp(value: Any) -> datetime | None:
|
|
156
|
+
"""Parse a fill frame's ``filled_at`` into an aware UTC datetime, or ``None``.
|
|
157
|
+
|
|
158
|
+
The backend stamps ``filled_at`` as an ISO-8601 string (``Order.filled_at``'s
|
|
159
|
+
``.isoformat()``), but a frame may also carry an epoch-seconds / epoch-ms
|
|
160
|
+
value. The user-event payloads' ``timestamp`` is an
|
|
161
|
+
``EpochSecondsOrMsTimestamp`` whose validator accepts a ``datetime`` as-is but
|
|
162
|
+
NOT an ISO string, so we resolve ISO here (normalising a trailing ``Z`` for
|
|
163
|
+
3.10's ``fromisoformat``) and let an already-numeric value flow through the
|
|
164
|
+
validator. A bad/None value yields ``None`` (the timestamp stays unset)
|
|
165
|
+
rather than sinking an otherwise-valid event.
|
|
166
|
+
"""
|
|
167
|
+
if value is None or value == "":
|
|
168
|
+
return None
|
|
169
|
+
if isinstance(value, datetime):
|
|
170
|
+
return value
|
|
171
|
+
if isinstance(value, str) and not value.isdecimal():
|
|
172
|
+
text = value
|
|
173
|
+
if text.endswith(("Z", "z")):
|
|
174
|
+
text = text[:-1] + "+00:00"
|
|
175
|
+
try:
|
|
176
|
+
dt = datetime.fromisoformat(text)
|
|
177
|
+
except ValueError:
|
|
178
|
+
return None
|
|
179
|
+
return dt.replace(tzinfo=timezone.utc) if dt.tzinfo is None else dt
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
154
183
|
def _coerce_epoch_ms_str(value: Any) -> str | None:
|
|
155
184
|
"""Render an epoch-ms timestamp as the digit-string the strict market
|
|
156
185
|
``EpochMsTimestamp`` validator wants. Non-integer / non-digit inputs become
|
|
@@ -306,41 +335,66 @@ def adapt_execution_frame(frame: dict[str, Any], spec: UserSpec) -> list[UserEve
|
|
|
306
335
|
if token_id is None:
|
|
307
336
|
return []
|
|
308
337
|
|
|
309
|
-
trade
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
338
|
+
# The fill's ``filled_at`` is the trade/order timestamp. Carry it onto both
|
|
339
|
+
# payloads' ``timestamp`` (an ``EpochSecondsOrMsTimestamp``). That validator
|
|
340
|
+
# accepts an epoch number / digit-string and a ``datetime`` as-is, but NOT an
|
|
341
|
+
# ISO string — and the backend stamps ``filled_at`` as ISO — so resolve an
|
|
342
|
+
# ISO value to a ``datetime`` here and pass an epoch value through unchanged.
|
|
343
|
+
# A ``None`` / unparseable value leaves ``timestamp`` at its default ``None``.
|
|
344
|
+
raw_filled_at = frame.get("filled_at")
|
|
345
|
+
filled_at: Any
|
|
346
|
+
if isinstance(raw_filled_at, bool):
|
|
347
|
+
# ``bool`` is an ``int`` subclass — never a valid epoch; don't let
|
|
348
|
+
# ``True`` slip through as the epoch ``1`` (1970) timestamp.
|
|
349
|
+
filled_at = None
|
|
350
|
+
elif isinstance(raw_filled_at, (int, float, datetime)):
|
|
351
|
+
# epoch seconds/ms or an already-resolved datetime — the validator
|
|
352
|
+
# accepts these as-is.
|
|
353
|
+
filled_at = raw_filled_at
|
|
354
|
+
elif isinstance(raw_filled_at, str):
|
|
355
|
+
# a digit-string epoch passes straight through; an ISO (or other)
|
|
356
|
+
# string is resolved to a datetime here (or ``None`` if unparseable).
|
|
357
|
+
filled_at = (
|
|
358
|
+
raw_filled_at
|
|
359
|
+
if raw_filled_at.isdecimal()
|
|
360
|
+
else _parse_fill_timestamp(raw_filled_at)
|
|
361
|
+
)
|
|
362
|
+
else:
|
|
363
|
+
# dict / list / unexpected type — never forward it into ``timestamp``,
|
|
364
|
+
# which would make ``model_validate`` raise and sink the whole fill.
|
|
365
|
+
filled_at = None
|
|
366
|
+
|
|
367
|
+
trade_payload: dict[str, Any] = {
|
|
368
|
+
"id": order_id,
|
|
369
|
+
"taker_order_id": order_id,
|
|
370
|
+
"market": market,
|
|
371
|
+
"asset_id": token_id,
|
|
372
|
+
"side": side,
|
|
373
|
+
"size": quantity,
|
|
374
|
+
"price": price,
|
|
375
|
+
"status": "MATCHED",
|
|
376
|
+
"owner": "",
|
|
377
|
+
"outcome": outcome,
|
|
378
|
+
}
|
|
379
|
+
order_payload: dict[str, Any] = {
|
|
380
|
+
"id": order_id,
|
|
381
|
+
"owner": "",
|
|
382
|
+
"market": market,
|
|
383
|
+
"asset_id": token_id,
|
|
384
|
+
"side": side,
|
|
385
|
+
"original_size": quantity,
|
|
386
|
+
"size_matched": quantity,
|
|
387
|
+
"price": price,
|
|
388
|
+
"type": "UPDATE",
|
|
389
|
+
"status": "MATCHED",
|
|
390
|
+
"outcome": outcome,
|
|
391
|
+
}
|
|
392
|
+
if filled_at is not None and filled_at != "":
|
|
393
|
+
trade_payload["timestamp"] = filled_at
|
|
394
|
+
order_payload["timestamp"] = filled_at
|
|
395
|
+
|
|
396
|
+
trade = UserTradeEvent.model_validate({"type": "trade", "payload": trade_payload})
|
|
397
|
+
order = UserOrderEvent.model_validate({"type": "order", "payload": order_payload})
|
|
344
398
|
return [trade, order]
|
|
345
399
|
|
|
346
400
|
|
|
@@ -19,7 +19,11 @@ import asyncio
|
|
|
19
19
|
import contextlib
|
|
20
20
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
21
21
|
from types import TracebackType
|
|
22
|
-
from typing import Generic, Protocol,
|
|
22
|
+
from typing import Generic, Protocol, TypeVar, runtime_checkable
|
|
23
|
+
|
|
24
|
+
# ``typing.Self`` is 3.11-only; the package floor is 3.10, so source it from
|
|
25
|
+
# ``typing_extensions`` (a declared dependency) for a single 3.10-safe symbol.
|
|
26
|
+
from typing_extensions import Self
|
|
23
27
|
|
|
24
28
|
T = TypeVar("T")
|
|
25
29
|
T_co = TypeVar("T_co", covariant=True)
|
|
@@ -62,6 +66,10 @@ class AsyncSubscriptionHandle(Generic[T]):
|
|
|
62
66
|
self._ended = False
|
|
63
67
|
self._dropped = 0
|
|
64
68
|
self._end_error: BaseException | None = None
|
|
69
|
+
# Set once the end sentinel has been consumed by ``__anext__``. After
|
|
70
|
+
# that the handle is permanently terminal: a further ``__anext__`` must
|
|
71
|
+
# re-raise the terminal outcome WITHOUT blocking on the (now-empty) queue.
|
|
72
|
+
self._terminated = False
|
|
65
73
|
self._closing: asyncio.Task[None] | None = None
|
|
66
74
|
self._on_close: Callable[[AsyncSubscriptionHandle[T]], Awaitable[None]] | None = None
|
|
67
75
|
|
|
@@ -116,8 +124,16 @@ class AsyncSubscriptionHandle(Generic[T]):
|
|
|
116
124
|
return self
|
|
117
125
|
|
|
118
126
|
async def __anext__(self) -> T:
|
|
127
|
+
# Once the end sentinel has been consumed the handle is terminal: re-raise
|
|
128
|
+
# the terminal outcome immediately instead of blocking on the empty queue
|
|
129
|
+
# (the sentinel is single-use — a second ``get()`` would hang forever).
|
|
130
|
+
if self._terminated:
|
|
131
|
+
if self._end_error is not None:
|
|
132
|
+
raise self._end_error
|
|
133
|
+
raise StopAsyncIteration
|
|
119
134
|
item = await self._queue.get()
|
|
120
135
|
if isinstance(item, _EndSentinel):
|
|
136
|
+
self._terminated = True
|
|
121
137
|
if self._end_error is not None:
|
|
122
138
|
raise self._end_error
|
|
123
139
|
raise StopAsyncIteration
|
|
@@ -10,7 +10,7 @@ permissive parsers field-for-field so the parsed scalar types match.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
from datetime import
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
14
|
from decimal import Decimal
|
|
15
15
|
from typing import TYPE_CHECKING, Annotated
|
|
16
16
|
|
|
@@ -44,7 +44,7 @@ def _parse_epoch_ms_timestamp(value: object) -> object:
|
|
|
44
44
|
raise ValueError(f"invalid epoch-ms timestamp: {value!r}")
|
|
45
45
|
ms = int(value)
|
|
46
46
|
try:
|
|
47
|
-
return datetime.fromtimestamp(ms / 1000, tz=
|
|
47
|
+
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
|
48
48
|
except (OverflowError, OSError, ValueError) as error:
|
|
49
49
|
raise ValueError(f"invalid epoch-ms timestamp: {value!r}") from error
|
|
50
50
|
|
|
@@ -58,18 +58,23 @@ def _parse_epoch_ms_or_iso_timestamp(value: object) -> object:
|
|
|
58
58
|
raise ValueError(f"expected timestamp, got bool {value!r}")
|
|
59
59
|
if isinstance(value, int):
|
|
60
60
|
try:
|
|
61
|
-
return datetime.fromtimestamp(value / 1000, tz=
|
|
61
|
+
return datetime.fromtimestamp(value / 1000, tz=timezone.utc)
|
|
62
62
|
except (OverflowError, OSError, ValueError) as error:
|
|
63
63
|
raise ValueError(f"invalid epoch-ms timestamp: {value!r}") from error
|
|
64
64
|
if isinstance(value, str):
|
|
65
65
|
if value.isdecimal():
|
|
66
66
|
ms = int(value)
|
|
67
67
|
try:
|
|
68
|
-
return datetime.fromtimestamp(ms / 1000, tz=
|
|
68
|
+
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
|
69
69
|
except (OverflowError, OSError, ValueError) as error:
|
|
70
70
|
raise ValueError(f"invalid epoch-ms timestamp: {value!r}") from error
|
|
71
|
+
# 3.10's ``datetime.fromisoformat`` does NOT accept a trailing ``Z``
|
|
72
|
+
# (Zulu/UTC) — that only landed in 3.11. The package floor is 3.10, so
|
|
73
|
+
# normalise a trailing ``Z`` to ``+00:00`` before parsing, matching how
|
|
74
|
+
# the rest of the SDK (``polysim_sdk.updown._parse_iso``) handles it.
|
|
75
|
+
iso = value[:-1] + "+00:00" if value.endswith(("Z", "z")) else value
|
|
71
76
|
try:
|
|
72
|
-
return datetime.fromisoformat(
|
|
77
|
+
return datetime.fromisoformat(iso)
|
|
73
78
|
except ValueError as error:
|
|
74
79
|
raise ValueError(f"invalid timestamp: {value!r}") from error
|
|
75
80
|
raise ValueError(f"expected epoch-ms or ISO timestamp, got {type(value).__name__}")
|
|
@@ -92,7 +97,7 @@ def _parse_epoch_seconds_or_ms_timestamp(value: object) -> object:
|
|
|
92
97
|
raise ValueError(f"expected epoch timestamp, got {type(value).__name__}")
|
|
93
98
|
seconds = magnitude / 1000 if magnitude >= _EPOCH_MS_THRESHOLD else magnitude
|
|
94
99
|
try:
|
|
95
|
-
return datetime.fromtimestamp(seconds, tz=
|
|
100
|
+
return datetime.fromtimestamp(seconds, tz=timezone.utc)
|
|
96
101
|
except (OverflowError, OSError, ValueError) as error:
|
|
97
102
|
raise ValueError(f"invalid epoch timestamp: {value!r}") from error
|
|
98
103
|
|
|
@@ -113,7 +118,7 @@ def _parse_epoch_seconds_timestamp(value: object) -> object:
|
|
|
113
118
|
else:
|
|
114
119
|
raise ValueError(f"expected epoch-seconds timestamp, got {type(value).__name__}")
|
|
115
120
|
try:
|
|
116
|
-
return datetime.fromtimestamp(seconds, tz=
|
|
121
|
+
return datetime.fromtimestamp(seconds, tz=timezone.utc)
|
|
117
122
|
except (OverflowError, OSError, ValueError) as error:
|
|
118
123
|
raise ValueError(f"invalid epoch-seconds timestamp: {value!r}") from error
|
|
119
124
|
|
|
@@ -26,6 +26,7 @@ Example:
|
|
|
26
26
|
print(fill["fee"], fill["slippage_bps"])
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
+
from polysim_sdk._version import __version__
|
|
29
30
|
from polysim_sdk.aio import AsyncPolySimClient
|
|
30
31
|
from polysim_sdk.client import PolySimClient
|
|
31
32
|
from polysim_sdk.constants import OrderType, Side
|
|
@@ -49,6 +50,5 @@ __all__ = [
|
|
|
49
50
|
"RateLimitError",
|
|
50
51
|
"ValidationError",
|
|
51
52
|
"retry_on_market_warmup",
|
|
53
|
+
"__version__",
|
|
52
54
|
]
|
|
53
|
-
|
|
54
|
-
__version__ = "0.2.2"
|
|
@@ -30,6 +30,7 @@ from urllib.parse import urljoin
|
|
|
30
30
|
|
|
31
31
|
import httpx
|
|
32
32
|
|
|
33
|
+
from polysim_sdk._version import DEFAULT_USER_AGENT
|
|
33
34
|
from polysim_sdk.exceptions import (
|
|
34
35
|
ApiError,
|
|
35
36
|
EdgeBlockedError,
|
|
@@ -206,7 +207,7 @@ class SyncTransport:
|
|
|
206
207
|
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
207
208
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
208
209
|
floor_interval: float = DEFAULT_FLOOR_INTERVAL_SECONDS,
|
|
209
|
-
user_agent: str =
|
|
210
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
210
211
|
) -> None:
|
|
211
212
|
self.base_url = base_url.rstrip("/")
|
|
212
213
|
self._max_retries = max_retries
|
|
@@ -317,7 +318,7 @@ class AsyncTransport:
|
|
|
317
318
|
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
318
319
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
319
320
|
floor_interval: float = DEFAULT_FLOOR_INTERVAL_SECONDS,
|
|
320
|
-
user_agent: str =
|
|
321
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
321
322
|
) -> None:
|
|
322
323
|
self.base_url = base_url.rstrip("/")
|
|
323
324
|
self._max_retries = max_retries
|
|
@@ -31,14 +31,23 @@ END_CURSOR = "LTE="
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def _decode_cursor(cursor: str | None) -> int:
|
|
34
|
-
"""base64 cursor -> integer offset. START/empty -> 0, END -> -1.
|
|
34
|
+
"""base64 cursor -> integer offset. START/empty -> 0, END -> -1.
|
|
35
|
+
|
|
36
|
+
ANY decode failure (bad base64, non-ASCII / non-numeric payload, a wrong
|
|
37
|
+
input type) falls back to ``0`` rather than propagating an unexpected error
|
|
38
|
+
type — a malformed cursor must never crash a paginator. The catch is broad
|
|
39
|
+
(``Exception``) on purpose: ``base64.b64decode`` / ``bytes.decode`` /
|
|
40
|
+
``int()`` can raise ``binascii.Error`` / ``UnicodeDecodeError`` / ``ValueError``
|
|
41
|
+
/ ``TypeError`` (and a future codec could surface another), all of which mean
|
|
42
|
+
the same thing here — "unparseable cursor, start from 0".
|
|
43
|
+
"""
|
|
35
44
|
if not cursor or cursor == START_CURSOR:
|
|
36
45
|
return 0
|
|
37
46
|
if cursor == END_CURSOR:
|
|
38
47
|
return -1
|
|
39
48
|
try:
|
|
40
49
|
return int(base64.b64decode(cursor).decode())
|
|
41
|
-
except
|
|
50
|
+
except Exception:
|
|
42
51
|
return 0
|
|
43
52
|
|
|
44
53
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Single source of truth for the SDK version + default User-Agent.
|
|
2
|
+
|
|
3
|
+
The version is read once from the installed package metadata
|
|
4
|
+
(``importlib.metadata.version("polysimulator")``) so ``polysim_sdk.__version__``
|
|
5
|
+
and the outbound ``User-Agent`` can never drift from ``[project].version`` in
|
|
6
|
+
``pyproject.toml``. A literal fallback covers an editable / not-yet-installed
|
|
7
|
+
checkout where the distribution metadata is absent.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
13
|
+
|
|
14
|
+
# Fallback for a checkout with no installed distribution metadata (e.g. running
|
|
15
|
+
# straight from source without ``pip install -e``). Kept in sync with
|
|
16
|
+
# ``[project].version`` as a last resort only — the metadata read is canonical.
|
|
17
|
+
_FALLBACK_VERSION = "0.4.2"
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
__version__ = version("polysimulator")
|
|
21
|
+
except PackageNotFoundError: # pragma: no cover - exercised only in bare checkouts
|
|
22
|
+
__version__ = _FALLBACK_VERSION
|
|
23
|
+
|
|
24
|
+
# The default outbound User-Agent. Built once from the resolved version so every
|
|
25
|
+
# transport (sync/async HTTP + SSE + WS) sends a UA that matches the package.
|
|
26
|
+
DEFAULT_USER_AGENT = f"polysim-sdk/{__version__}"
|
|
27
|
+
|
|
28
|
+
__all__ = ["DEFAULT_USER_AGENT", "__version__"]
|
|
@@ -32,6 +32,7 @@ from polysim_sdk._http import (
|
|
|
32
32
|
AsyncTransport,
|
|
33
33
|
unwrap_list,
|
|
34
34
|
)
|
|
35
|
+
from polysim_sdk._version import DEFAULT_USER_AGENT
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class AsyncPolySimClient:
|
|
@@ -49,7 +50,7 @@ class AsyncPolySimClient:
|
|
|
49
50
|
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
50
51
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
51
52
|
floor_interval: float = DEFAULT_FLOOR_INTERVAL_SECONDS,
|
|
52
|
-
user_agent: str =
|
|
53
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
53
54
|
) -> None:
|
|
54
55
|
resolved_key = api_key or os.environ.get("POLYSIM_API_KEY")
|
|
55
56
|
if not resolved_key:
|
|
@@ -607,7 +608,7 @@ class AsyncPolySimClient:
|
|
|
607
608
|
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
608
609
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
609
610
|
floor_interval: float = DEFAULT_FLOOR_INTERVAL_SECONDS,
|
|
610
|
-
user_agent: str =
|
|
611
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
611
612
|
) -> dict[str, Any]:
|
|
612
613
|
"""``POST /v1/keys/bootstrap`` — JWT-authed first-key mint, no key needed.
|
|
613
614
|
|
|
@@ -25,6 +25,7 @@ from polysim_sdk._http import (
|
|
|
25
25
|
SyncTransport,
|
|
26
26
|
unwrap_list,
|
|
27
27
|
)
|
|
28
|
+
from polysim_sdk._version import DEFAULT_USER_AGENT
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class PolySimClient:
|
|
@@ -43,7 +44,7 @@ class PolySimClient:
|
|
|
43
44
|
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
44
45
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
45
46
|
floor_interval: float = DEFAULT_FLOOR_INTERVAL_SECONDS,
|
|
46
|
-
user_agent: str =
|
|
47
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
47
48
|
) -> None:
|
|
48
49
|
resolved_key = api_key or os.environ.get("POLYSIM_API_KEY")
|
|
49
50
|
if not resolved_key:
|
|
@@ -713,7 +714,7 @@ class PolySimClient:
|
|
|
713
714
|
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
714
715
|
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
715
716
|
floor_interval: float = DEFAULT_FLOOR_INTERVAL_SECONDS,
|
|
716
|
-
user_agent: str =
|
|
717
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
717
718
|
) -> dict[str, Any]:
|
|
718
719
|
"""``POST /v1/keys/bootstrap`` — mint the FIRST key from a Supabase JWT.
|
|
719
720
|
|
|
@@ -32,6 +32,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
32
32
|
import httpx
|
|
33
33
|
|
|
34
34
|
from polysim_sdk._http import raise_for_status, resolve_url
|
|
35
|
+
from polysim_sdk._version import DEFAULT_USER_AGENT
|
|
35
36
|
|
|
36
37
|
if TYPE_CHECKING:
|
|
37
38
|
from polysim_sdk.aio import AsyncPolySimClient
|
|
@@ -170,7 +171,7 @@ def spot_stream(
|
|
|
170
171
|
backoff = reconnect_min_interval
|
|
171
172
|
attempts = 0
|
|
172
173
|
url = resolve_url(client.base_url, "/prices/stream")
|
|
173
|
-
headers = _sse_headers(client._api_key,
|
|
174
|
+
headers = _sse_headers(client._api_key, DEFAULT_USER_AGENT)
|
|
174
175
|
with httpx.Client(timeout=_stream_timeout()) as http:
|
|
175
176
|
while True:
|
|
176
177
|
try:
|
|
@@ -228,7 +229,7 @@ async def aspot_stream(
|
|
|
228
229
|
backoff = reconnect_min_interval
|
|
229
230
|
attempts = 0
|
|
230
231
|
url = resolve_url(client.base_url, "/prices/stream")
|
|
231
|
-
headers = _sse_headers(client._api_key,
|
|
232
|
+
headers = _sse_headers(client._api_key, DEFAULT_USER_AGENT)
|
|
232
233
|
async with httpx.AsyncClient(timeout=_stream_timeout()) as http:
|
|
233
234
|
while True:
|
|
234
235
|
try:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "polysimulator"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Python SDK for the PolySimulator paper-trading API, with py-clob-client v1 drop-in parity (not py-clob-client-v2) and a polysim_polymarket paper-mode mirror of Polymarket's unified py-sdk (polymarket-client 0.1.0b8)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -27,6 +27,10 @@ dependencies = [
|
|
|
27
27
|
# polysim_polymarket (py-sdk mirror) parses read models with pydantic v2,
|
|
28
28
|
# matching the real polymarket-client's modelling layer.
|
|
29
29
|
"pydantic>=2,<3",
|
|
30
|
+
# ``typing_extensions.Self`` backfills ``typing.Self`` (3.11+) for the 3.10
|
|
31
|
+
# floor the package declares. (pydantic already pulls it in transitively;
|
|
32
|
+
# declared explicitly so the import is a first-class dependency.)
|
|
33
|
+
"typing_extensions>=4",
|
|
30
34
|
]
|
|
31
35
|
|
|
32
36
|
[project.optional-dependencies]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|