polysimulator 0.2.2__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polysimulator-0.4.0/PKG-INFO +1040 -0
- polysimulator-0.4.0/README.md +1005 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_clob_client/client.py +23 -81
- polysimulator-0.4.0/polysim_polymarket/__init__.py +235 -0
- polysimulator-0.4.0/polysim_polymarket/clients/__init__.py +0 -0
- polysimulator-0.4.0/polysim_polymarket/clients/_account.py +213 -0
- polysimulator-0.4.0/polysim_polymarket/clients/_common.py +573 -0
- polysimulator-0.4.0/polysim_polymarket/clients/_onchain.py +293 -0
- polysimulator-0.4.0/polysim_polymarket/clients/_trade.py +520 -0
- polysimulator-0.4.0/polysim_polymarket/clients/async_public.py +448 -0
- polysimulator-0.4.0/polysim_polymarket/clients/async_secure.py +1153 -0
- polysimulator-0.4.0/polysim_polymarket/clients/public.py +442 -0
- polysimulator-0.4.0/polysim_polymarket/clients/secure.py +1200 -0
- polysimulator-0.4.0/polysim_polymarket/environments.py +62 -0
- polysimulator-0.4.0/polysim_polymarket/errors.py +59 -0
- polysimulator-0.4.0/polysim_polymarket/models.py +784 -0
- polysimulator-0.4.0/polysim_polymarket/pagination.py +228 -0
- polysimulator-0.4.0/polysim_polymarket/rfq.py +312 -0
- polysimulator-0.4.0/polysim_polymarket/streams/__init__.py +112 -0
- polysimulator-0.4.0/polysim_polymarket/streams/_adapt.py +432 -0
- polysimulator-0.4.0/polysim_polymarket/streams/_crypto_events.py +54 -0
- polysimulator-0.4.0/polysim_polymarket/streams/_handle.py +152 -0
- polysimulator-0.4.0/polysim_polymarket/streams/_market_events.py +221 -0
- polysimulator-0.4.0/polysim_polymarket/streams/_runner.py +80 -0
- polysimulator-0.4.0/polysim_polymarket/streams/_specs.py +144 -0
- polysimulator-0.4.0/polysim_polymarket/streams/_stream_open.py +212 -0
- polysimulator-0.4.0/polysim_polymarket/streams/_user_events.py +136 -0
- polysimulator-0.4.0/polysim_polymarket/streams/_validators.py +149 -0
- polysimulator-0.4.0/polysim_sdk/_shared.py +128 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/pyproject.toml +12 -3
- polysimulator-0.2.2/PKG-INFO +0 -508
- polysimulator-0.2.2/README.md +0 -475
- {polysimulator-0.2.2 → polysimulator-0.4.0}/.gitignore +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/LICENSE +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/NOTICE +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_clob_client/__init__.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_clob_client/clob_types.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_clob_client/constants.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_clob_client/exceptions.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_clob_client/order_builder/__init__.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_clob_client/order_builder/constants.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/__init__.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/_http.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/aio.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/client.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/constants.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/exceptions.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/pagination.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/sse.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/updown.py +0 -0
- {polysimulator-0.2.2 → polysimulator-0.4.0}/polysim_sdk/ws.py +0 -0
|
@@ -0,0 +1,1040 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polysimulator
|
|
3
|
+
Version: 0.4.0
|
|
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
|
+
Project-URL: Homepage, https://polysimulator.com
|
|
6
|
+
Project-URL: Documentation, https://docs.polysimulator.com
|
|
7
|
+
Project-URL: API Reference, https://api.polysimulator.com/docs
|
|
8
|
+
Project-URL: Repository, https://github.com/Bavariance/polysimulator-sdk
|
|
9
|
+
Author: Bavariance
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: NOTICE
|
|
13
|
+
Keywords: api-sdk,paper-trading,polymarket,polysimulator,trading
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx<1,>=0.27
|
|
23
|
+
Requires-Dist: pydantic<3,>=2
|
|
24
|
+
Requires-Dist: websockets<17,>=12
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: polymarket-client==0.1.0b8; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
33
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# PolySimulator Python SDK
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/polysimulator/)
|
|
39
|
+
[](https://pypi.org/project/polysimulator/)
|
|
40
|
+
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
41
|
+
[](https://github.com/Polymarket/py-clob-client)
|
|
42
|
+
[](https://pypi.org/project/polymarket-client/)
|
|
43
|
+
|
|
44
|
+
> **Mirrors Polymarket's unified py-sdk** — `polysim_polymarket` is a paper-mode mirror of [`polymarket-client`](https://pypi.org/project/polymarket-client/) **`0.1.0b8`**: sync + async PublicClient/SecureClient, trading, and core streams. Swap the import prefix + host + auth and your py-sdk bot runs on paper. [Compat matrix ↓](#compat-matrix)
|
|
45
|
+
|
|
46
|
+
The official Python client for the [PolySimulator](https://polysimulator.com)
|
|
47
|
+
paper-trading API. **One package, three import surfaces:**
|
|
48
|
+
|
|
49
|
+
| Import surface | Use it when |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `polysim_sdk` | You're starting fresh. A clean, modern client — sync **and** async, WebSocket streaming, pagination iterators, typed exceptions. |
|
|
52
|
+
| `polysim_clob_client` | You already have a bot written against Polymarket's [`py-clob-client`](https://github.com/Polymarket/py-clob-client). This is a **drop-in mirror** — port by changing the import path, the host, and the auth call. |
|
|
53
|
+
| `polysim_polymarket` | You target Polymarket's newer unified **[py-sdk](https://pypi.org/project/polymarket-client/)** (`pip install polymarket-client`, import `polymarket`). A **paper-mode mirror** of its unified surface — sync **and** async `PublicClient` / `SecureClient` (reads, account/auth, trading) plus core realtime streams. Port by swapping the import prefix + host + auth. See [Polymarket py-sdk (v2) mirror](#polymarket-py-sdk-v2-mirror) — read the [sim→real seam contract](#the-simreal-seam-contract-what-paper-mode-does-and-doesnt-do) and [compat matrix](#compat-matrix) before you rely on a surface area. |
|
|
54
|
+
|
|
55
|
+
PolySimulator is paper trading, so there is **no on-chain anything**: no private
|
|
56
|
+
key, no `chain_id`, no `funder`, no `signature_type`, no EIP-712 signing, no USDC
|
|
57
|
+
allowance/approval transactions, no web3/Polygon RPC. The SDK depends only on
|
|
58
|
+
`httpx` and `websockets`. Authentication is a single `ps_live_*` API key sent as
|
|
59
|
+
the `X-API-Key` header.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
Targets Python 3.10+.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install polysimulator
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> The install name is **`polysimulator`**; the import name stays **`polysim_sdk`**
|
|
72
|
+
> (like `pip install pillow` → `import PIL`). The older `polysim-sdk` name is now a
|
|
73
|
+
> thin alias that installs `polysimulator` for you, so existing installs keep working.
|
|
74
|
+
|
|
75
|
+
From a checkout of this directory (for development):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m venv .venv && source .venv/bin/activate
|
|
79
|
+
pip install -e ".[dev]" # editable install + pytest/ruff/mypy/build/twine
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Provide your key via the `POLYSIM_API_KEY` environment variable (recommended) or
|
|
83
|
+
pass it to the constructor. To target staging instead of production:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export POLYSIM_API_KEY="ps_live_…"
|
|
87
|
+
export POLYSIM_BASE_URL="https://staging-api.polysimulator.com"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Quickstart — native `polysim_sdk`
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from polysim_sdk import PolySimClient
|
|
96
|
+
|
|
97
|
+
with PolySimClient() as client: # POLYSIM_API_KEY from env
|
|
98
|
+
me = client.me()
|
|
99
|
+
print(me["balance"]) # your trading balance (a JSON string)
|
|
100
|
+
|
|
101
|
+
market = client.list_markets(limit=1, hot_only=True)[0]
|
|
102
|
+
cid = market["condition_id"]
|
|
103
|
+
|
|
104
|
+
book = client.get_book(cid) # by condition id (+ outcome=/depth=)
|
|
105
|
+
asks = book.get("asks") or [] # levels: [{"price","size"}, ...]
|
|
106
|
+
best_ask = min((float(a["price"]) for a in asks), default=None)
|
|
107
|
+
print("best ask:", best_ask) # lowest ask = best price to buy at
|
|
108
|
+
|
|
109
|
+
# …or by outcome-token id, the Polymarket-native way:
|
|
110
|
+
# token_book = client.get_book_by_token("<outcome-token-id>")
|
|
111
|
+
|
|
112
|
+
fill = client.place_order(
|
|
113
|
+
market_id=cid,
|
|
114
|
+
side="BUY",
|
|
115
|
+
outcome="YES",
|
|
116
|
+
quantity=10,
|
|
117
|
+
order_type="market",
|
|
118
|
+
price="0.99", # worst-acceptable fill (YES < $1 → "any fill")
|
|
119
|
+
)
|
|
120
|
+
print(f"filled {fill['status']} @ {fill.get('price')}")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Async is the same API with `await`:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import asyncio
|
|
127
|
+
from polysim_sdk import AsyncPolySimClient
|
|
128
|
+
|
|
129
|
+
async def main():
|
|
130
|
+
async with AsyncPolySimClient() as client:
|
|
131
|
+
me = await client.me()
|
|
132
|
+
print(me["balance"])
|
|
133
|
+
|
|
134
|
+
asyncio.run(main())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Finding BTC Up/Down markets
|
|
140
|
+
|
|
141
|
+
Short-horizon **crypto Up/Down** markets — "is BTC/ETH/SOL/… up or down over
|
|
142
|
+
this window" resolving on a 5-minute … daily horizon — are the platform's
|
|
143
|
+
most-traded product. They have a dedicated endpoint and do **not** show up
|
|
144
|
+
through `list_markets(q="btc")` (free-text search is the `q` filter). Use
|
|
145
|
+
`list_updown` / `get_updown`:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
with PolySimClient() as client:
|
|
149
|
+
# Currently-tradeable BTC 5-minute windows, filtered server-side:
|
|
150
|
+
live = client.list_updown(asset="BTC", interval="5M", live=True)
|
|
151
|
+
window = live[0]
|
|
152
|
+
print(window["slug"]) # btc-updown-5m-1781400900
|
|
153
|
+
print(window["time_range"]) # "9:35PM-9:40PM ET"
|
|
154
|
+
print(window["group_item_threshold"]) # resolution strike (None until the window opens)
|
|
155
|
+
|
|
156
|
+
# Outcome tokens live in the nested markets[] array (JSON-string fields):
|
|
157
|
+
nested = window["markets"][0]
|
|
158
|
+
print(nested["outcomes"]) # '["Up", "Down"]'
|
|
159
|
+
print(nested["outcome_prices"]) # '["0.595", "0.405"]'
|
|
160
|
+
cid = window["condition_id"]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`interval` is one of `5M` / `15M` / `1H` / `4H` / `daily` (**uppercase**).
|
|
164
|
+
`live=True` keeps only `active and not closed and not resolved` windows.
|
|
165
|
+
|
|
166
|
+
The **live underlying spot price** plus the available assets/intervals come from
|
|
167
|
+
the full payload via `get_updown`:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
payload = client.get_updown(asset="BTC")
|
|
171
|
+
btc = (payload.get("crypto_prices") or {}).get("BTC") or {}
|
|
172
|
+
print(btc.get("price")) # 64588.0 — live BTC/USD (coingecko)
|
|
173
|
+
print(payload["available_intervals"]) # ['5M', '15M', '1H', '4H', 'daily']
|
|
174
|
+
print(payload["interval_counts"]) # {'5M': 169, '15M': 402, ...}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
> The spot price can be momentarily absent right at a window boundary (the
|
|
178
|
+
> payload is cached and the feed can lag a second or two), so read it
|
|
179
|
+
> defensively with `.get(...)` rather than chained `[...]` indexing.
|
|
180
|
+
|
|
181
|
+
### Pricing an Up/Down market
|
|
182
|
+
|
|
183
|
+
The raw order book (`get_book`) on an Up/Down market is a **synthetic
|
|
184
|
+
placeholder** — its `asks[0]` sits near ~0.99 and is not a real resting quote.
|
|
185
|
+
For price, use the market's own fields instead:
|
|
186
|
+
|
|
187
|
+
- `window["live_price"]` → `{"buy": 0.595, "sell": 0.405, ...}` (current marketable prices)
|
|
188
|
+
- `nested["outcome_prices"]` → the latest Up/Down implied probabilities
|
|
189
|
+
- the realised `price` returned by `place_order` — the SDK fills against the real
|
|
190
|
+
internal book and reports the true fill price, `fee`, and `book_walk_levels`
|
|
191
|
+
|
|
192
|
+
A market order with a worst-price cap still fills correctly regardless; just
|
|
193
|
+
don't read `get_book(...)["asks"][0]` as a tradeable quote on these markets.
|
|
194
|
+
|
|
195
|
+
> **Drop-in `polysim_clob_client` users:** `get_midpoint`, `get_price`,
|
|
196
|
+
> `get_spread` and `calculate_market_price` read the outcome token's **synthetic
|
|
197
|
+
> ~0.99 ladder** on Up/Down markets — they are *not* the underlying asset price.
|
|
198
|
+
> For the asset, use the native reads below.
|
|
199
|
+
|
|
200
|
+
### Up/Down for HFT: strike, spot, and a push feed
|
|
201
|
+
|
|
202
|
+
The two numbers an Up/Down strategy actually trades on — the **strike**
|
|
203
|
+
("price to beat") and the **live underlying spot** — have dedicated reads on the
|
|
204
|
+
native client, plus a push stream and a few pure helpers:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from polysim_sdk import PolySimClient
|
|
208
|
+
from polysim_sdk import updown
|
|
209
|
+
|
|
210
|
+
with PolySimClient() as client:
|
|
211
|
+
live = client.list_updown(asset="BTC", interval="5M", live=True)
|
|
212
|
+
window = updown.next_to_expire(live) or live[0] # the contract closing soonest
|
|
213
|
+
cid = window["condition_id"]
|
|
214
|
+
|
|
215
|
+
strike = client.get_price_to_beat(cid)["price"] # GET /prices/ptb/{cid}; 404 = not set yet
|
|
216
|
+
spot = client.get_spot("BTC")["price"] # GET /prices/live/BTC — live underlying
|
|
217
|
+
# all spots in one call: client.get_spots()["prices"]
|
|
218
|
+
|
|
219
|
+
print(updown.ptb_distance(spot, strike)) # signed price units; >0 ⇒ "Up" in the money
|
|
220
|
+
print(updown.ptb_distance_bps(spot, strike)) # same, in bps of the strike
|
|
221
|
+
print(updown.seconds_to_expiry(window)) # time left on the window
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`get_price_to_beat` raises `ApiError` with `status_code == 404` while a window's
|
|
225
|
+
strike is still pending — that is "not set yet", not "market absent". It accepts
|
|
226
|
+
a `condition_id` **or** an outcome-token id. Its `source` field tells you the
|
|
227
|
+
provenance: `polymarket_open_price` / `polymarket_scrape` / `gamma_event_metadata`
|
|
228
|
+
(Polymarket's own reported strike) and `chainlink_onchain` / `chainlink_timeline`
|
|
229
|
+
(the resolution oracle) are the settlement strike; `gamma_api` / `cryptocompare`
|
|
230
|
+
are best-effort fallbacks — treat them as indicative.
|
|
231
|
+
|
|
232
|
+
The `polysim_sdk.updown` helpers are pure functions over the row dicts (no
|
|
233
|
+
network, tolerant of missing fields): `seconds_to_expiry`, `is_window_open`,
|
|
234
|
+
`price_to_beat` (reads the strike off a row you already have), `ptb_distance` /
|
|
235
|
+
`ptb_distance_bps`, `open_windows`, `next_to_expire`, plus the `ASSETS` /
|
|
236
|
+
`INTERVALS` vocab tuples. For a **streaming** tap on the underlying, see
|
|
237
|
+
[SSE: live underlying spot](#sse-live-underlying-spot) below.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Polymarket compatibility
|
|
242
|
+
|
|
243
|
+
`polysim_clob_client` is a drop-in mirror of Polymarket's
|
|
244
|
+
[`py-clob-client`](https://github.com/Polymarket/py-clob-client) **v1** — the
|
|
245
|
+
`0.x` line, surface-parity with v1 `0.34.x`. Method names, argument shapes, and
|
|
246
|
+
return shapes match v1, so a v1 bot ports by changing three things (import path,
|
|
247
|
+
host, auth) and deleting the on-chain prelude — see
|
|
248
|
+
[Migrating from `py-clob-client`](#migrating-from-py-clob-client) below.
|
|
249
|
+
|
|
250
|
+
Polymarket's newer [`py-clob-client-v2`](https://github.com/Polymarket/py-clob-client-v2)
|
|
251
|
+
is a **separate PyPI package** (`pip install py-clob-client-v2`, import
|
|
252
|
+
`py_clob_client_v2`; current `1.0.1`) and a hard breaking release — it renames the
|
|
253
|
+
package, changes the constructor and the order-signing payload, and moves to pUSD
|
|
254
|
+
collateral. A bot written against v2 will **not** run against this mirror
|
|
255
|
+
unchanged (the `py_clob_client_v2` import and the v2 `create_or_derive_api_key()`
|
|
256
|
+
bootstrap don't exist here). If you're on v2, either pin `py-clob-client<2` for
|
|
257
|
+
the drop-in path, or use the native
|
|
258
|
+
[`polysim_sdk`](#quickstart--native-polysim_sdk) client, which is
|
|
259
|
+
paper-mode-native and not tied to any `py-clob-client` generation.
|
|
260
|
+
|
|
261
|
+
| Polymarket client | Compatibility |
|
|
262
|
+
|---|---|
|
|
263
|
+
| `py-clob-client` v1 (`< 2.0`; latest `0.34.6`) | ✅ Drop-in via `polysim_clob_client` |
|
|
264
|
+
| `py-clob-client-v2` (`1.0.x`) | ❌ Not supported — use `polysim_sdk`, or pin `py-clob-client<2` |
|
|
265
|
+
| unified **py-sdk** (`polymarket-client`, import `polymarket`; `0.1.0b8`) | 🔶 **Paper-mode mirror** via `polysim_polymarket` — sync + async `PublicClient` / `SecureClient` (reads, account/auth, trading) + core streams; on-chain are paper no-ops and Gamma/Data reads are deferred. See [below](#polymarket-py-sdk-v2-mirror) and the [compat matrix](#compat-matrix) |
|
|
266
|
+
|
|
267
|
+
The v2-only concepts — EIP-712 v2 signing, pUSD collateral, protocol-side fees,
|
|
268
|
+
builder codes — have no analog in paper trading, so there is nothing to port:
|
|
269
|
+
you drop them entirely.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Polymarket py-sdk (v2) mirror
|
|
274
|
+
|
|
275
|
+
Polymarket also ships a newer **unified Python SDK**, published to PyPI as
|
|
276
|
+
[`polymarket-client`](https://pypi.org/project/polymarket-client/) and imported
|
|
277
|
+
as `polymarket`. It folds the CLOB, gamma (markets/events), data, RFQ, and
|
|
278
|
+
realtime-stream surfaces into one client and returns **typed pydantic models**
|
|
279
|
+
(`OrderBook`, `Market`, `LastTradePrice`, …) instead of raw dicts.
|
|
280
|
+
`polysim_polymarket` is our **paper-mode mirror** of that SDK — the package we
|
|
281
|
+
call the **v2 mirror** in this repo (the `polysim_clob_client` mirror above is
|
|
282
|
+
v1).
|
|
283
|
+
|
|
284
|
+
It mirrors the unified client family in paper mode: **sync and async**
|
|
285
|
+
`PublicClient` and `SecureClient`, the authenticated **trading** write surface,
|
|
286
|
+
the **account/auth/order** reads, the **on-chain** methods (as paper no-ops), the
|
|
287
|
+
**rewards/builder/RFQ** stubs, and the **core realtime streams** (`market` /
|
|
288
|
+
`user` / `crypto_prices`). What it does *not* mirror are the **Gamma/Data reads**
|
|
289
|
+
(events, series, tags, comments, sports, portfolio/positions, leaderboards) and
|
|
290
|
+
the **rewards-engine** reads — those are documented-deferred. The exact
|
|
291
|
+
support level for every surface area is in the [compat matrix](#compat-matrix)
|
|
292
|
+
below; the precise places paper diverges from real are in the
|
|
293
|
+
[sim→real seam contract](#the-simreal-seam-contract-what-paper-mode-does-and-doesnt-do).
|
|
294
|
+
Read both before you rely on a surface area — this is a **paper-trading** mirror,
|
|
295
|
+
not a drop-in for *everything* py-sdk does.
|
|
296
|
+
|
|
297
|
+
The port is the same mechanical swap as v1 — change the import **prefix**, the
|
|
298
|
+
**host**, and the **auth**:
|
|
299
|
+
|
|
300
|
+
```diff
|
|
301
|
+
- from polymarket import SecureClient
|
|
302
|
+
+ from polysim_polymarket import SecureClient
|
|
303
|
+
|
|
304
|
+
- client = SecureClient(private_key="0x…") # real Polymarket wallet + EIP-712 signing
|
|
305
|
+
+ client = SecureClient(host="https://api.polysimulator.com", api_key="ps_live_…")
|
|
306
|
+
|
|
307
|
+
# unchanged from here on — same method names, same keyword-only call shapes,
|
|
308
|
+
# same model field names/types. A read:
|
|
309
|
+
book = client.get_order_book(token_id="711811…")
|
|
310
|
+
# py-sdk OrderBook ordering: bids ascending (best = bids[-1]), asks
|
|
311
|
+
# descending (best = asks[-1]).
|
|
312
|
+
print(book.bids[-1].price, book.asks[-1].price, book.token_id)
|
|
313
|
+
|
|
314
|
+
# …and a trade — flat keyword args, py-sdk's SignedOrder / OrderResponse:
|
|
315
|
+
signed = client.create_market_order(token_id="711811…", side="BUY", amount=10)
|
|
316
|
+
resp = client.post_order(signed) # paper-accepted, not on-chain-settled
|
|
317
|
+
print(resp.order_id, resp.status)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
(The `PublicClient` swap is identical — drop the auth entirely:
|
|
321
|
+
`PublicClient(host="https://api.polysimulator.com", api_key="ps_live_…")` against
|
|
322
|
+
`polymarket`'s `PublicClient()`.)
|
|
323
|
+
|
|
324
|
+
The model field names and types (`OrderBook`, `OrderBookLevel`,
|
|
325
|
+
`LastTradePrice`, `PriceHistoryPoint`, `Market` — with its nested `.state`
|
|
326
|
+
`MarketState` — `Page`, `Paginator`, the trading types `OrderSide` / `OrderType`
|
|
327
|
+
/ `SignedOrder` / `OrderResponse` / `CancelOrdersResponse`, and the RFQ types) all
|
|
328
|
+
track py-sdk so the swap stays mechanical, and every public name a bot imports is
|
|
329
|
+
re-exported off the package root, exactly as on real Polymarket — including the
|
|
330
|
+
named errors the surface raises (`UserInputError`,
|
|
331
|
+
`InsufficientLiquidityError`, `UnexpectedResponseError`), so
|
|
332
|
+
`from polymarket import UserInputError` survives the prefix swap unchanged. (Like
|
|
333
|
+
py-sdk, `MarketState` itself is *not* promoted to the package root — it's reached
|
|
334
|
+
via `market.state` or `polysim_polymarket.models.MarketState`, never `from
|
|
335
|
+
polysim_polymarket import MarketState`.) The **one** deliberate divergence is the
|
|
336
|
+
error-tree *base* name: those three named errors subclass the
|
|
337
|
+
py-clob-client-lineage `PolyException` / `PolyApiException` base (shared by
|
|
338
|
+
identity with the v1 mirror) rather than py-sdk's `PolymarketError`, so a bot
|
|
339
|
+
that catches the broad base by name needs `except PolyException` here vs `except
|
|
340
|
+
PolymarketError` on real Polymarket; an `except UserInputError` block is
|
|
341
|
+
identical on both:
|
|
342
|
+
|
|
343
|
+
```python
|
|
344
|
+
from polysim_polymarket import PublicClient, OrderBook, Environment, PolyException, Paginator
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### The sim→real seam contract: what paper mode does and doesn't do
|
|
348
|
+
|
|
349
|
+
This is a **paper-trading** mirror. The call shapes and model fields match py-sdk
|
|
350
|
+
so the port is mechanical, but paper mode is not Polymarket — it has no chain, no
|
|
351
|
+
wallet, and its own market universe. Every place paper deliberately diverges from
|
|
352
|
+
real Polymarket is enumerated here, up front, so a bot author knows exactly what
|
|
353
|
+
they're getting before they rely on a surface area:
|
|
354
|
+
|
|
355
|
+
- **Signing is inert (accepted, never settled on-chain).** The authenticated
|
|
356
|
+
trading write surface IS implemented — `create_limit_order` /
|
|
357
|
+
`create_market_order` / `place_limit_order` / `place_market_order` /
|
|
358
|
+
`post_order` / `post_orders` / `cancel_order` / `cancel_orders` / `cancel_all` /
|
|
359
|
+
`cancel_market_orders`, with py-sdk's flat keyword args and `SignedOrder` /
|
|
360
|
+
`OrderResponse` / `CancelOrdersResponse` return models. But paper trading needs
|
|
361
|
+
no signer, so signing is **accepted-and-inert**: the order is produced with an
|
|
362
|
+
**empty signature** and the *unsigned* paper body is submitted. There is **no
|
|
363
|
+
EIP-712 signing, no on-chain settlement, and no Polygon transaction** — the fill
|
|
364
|
+
happens entirely in PolySimulator's paper matching engine.
|
|
365
|
+
- **Collateral / balances are PAPER.** Your balance is PolySimulator's paper
|
|
366
|
+
balance (the `balance` from `me()` on the native client), not an on-chain USDC
|
|
367
|
+
balance. `get_balance_allowance` reports the paper account's collateral; there
|
|
368
|
+
is no real USDC, no allowance/approval transaction, no funder wallet.
|
|
369
|
+
- **A settled trade carries the per-fill fee RATE, not a fee amount.**
|
|
370
|
+
PolySimulator's matching engine charges and records a real (paper) fee on each
|
|
371
|
+
fill. What it surfaces over the typed read surface is the fee *rate*: a settled
|
|
372
|
+
trade read via `list_account_trades` returns a `ClobTrade` model carrying the
|
|
373
|
+
per-fill fee rate as `fee_rate_bps` (basis points — the category taker rate
|
|
374
|
+
applied to the fill, `0` for fee-free fills), exactly matching py-sdk's
|
|
375
|
+
`ClobTrade`. **What IS surfaced:** the per-fill `fee_rate_bps`. **What is NOT
|
|
376
|
+
surfaced:** a realized per-fill fee *amount* — there is no `fee` / `fee_usdc`
|
|
377
|
+
field on `ClobTrade` (the only model carrying those is `BuilderTrade`, whose
|
|
378
|
+
reads are stubbed — see below), so the sim's debited fee amount is not exposed
|
|
379
|
+
as a typed field on the implemented read surface. **What is NOT mirrored at
|
|
380
|
+
all:** Polymarket **builder** fees (the builder-attribution program — see the
|
|
381
|
+
builder stub row), and any protocol-side / pUSD v2 fee economics; the
|
|
382
|
+
`builder_code=` kwarg on the order builders is accepted for parity but stays
|
|
383
|
+
**inert** (no builder fee taken, no builder-revenue ledger). On maker/taker: the
|
|
384
|
+
sim classifies a marketable-at-placement GTC (one that crosses the book on
|
|
385
|
+
submit) as a **taker** and charges the taker fee, matching Polymarket; only a
|
|
386
|
+
genuinely-resting GTC that is filled later — when the market moves into it — is
|
|
387
|
+
booked as a $0-fee maker. This is a fidelity *match*, not a gap.
|
|
388
|
+
- **The market universe is PolySimulator's, NOT Polymarket's live universe.**
|
|
389
|
+
Reads resolve against the markets PolySimulator carries, which is a curated
|
|
390
|
+
subset of Polymarket plus the platform's own crypto **Up/Down** products. A
|
|
391
|
+
`token_id` / `condition_id` / slug that exists on Polymarket but not in
|
|
392
|
+
PolySimulator's universe will not resolve here. Do not assume `list_markets` /
|
|
393
|
+
`get_market` returns the same set, count, or ordering as real Polymarket.
|
|
394
|
+
- **On-chain methods are instant PAPER no-ops.** `approve_erc20` /
|
|
395
|
+
`approve_erc1155_for_all` / `transfer_erc20` / `split_position` /
|
|
396
|
+
`merge_positions` / `redeem_positions` / `setup_trading_approvals` /
|
|
397
|
+
`setup_gasless_wallet` carry py-sdk's exact signatures, but paper mode accepts
|
|
398
|
+
the call (replicating py-sdk's pre-chain input guards so a bot hits the SAME
|
|
399
|
+
`UserInputError`) and **settles nothing**: no chain write, no ledger mutation,
|
|
400
|
+
no balance change. Each returns a paper transaction handle whose `.wait()`
|
|
401
|
+
returns **instantly** with a valid-format placeholder hash
|
|
402
|
+
(`0x` + 64 hex, `transaction_id=None`). Unlike real py-sdk,
|
|
403
|
+
`redeem_positions` / `merge_positions` do **not** balance-check the position on
|
|
404
|
+
paper. (Address checks are a 40-hex FORMAT check, not an EIP-55 checksum, and
|
|
405
|
+
the returned address is not checksum-normalized — moot on paper, where it's
|
|
406
|
+
never used on a chain.)
|
|
407
|
+
- **Rewards / builder / RFQ are honest stubs.** The rewards/scoring reads return
|
|
408
|
+
honest **empties** (`get_order_scoring` → `False`, the `list_*` rewards reads →
|
|
409
|
+
empty paginators, `get_reward_percentages` → `{}`, …) — the paper rewards engine
|
|
410
|
+
is a separate roadmap item, so a reward-accounting loop runs and truthfully finds
|
|
411
|
+
nothing. The builder-attribution reads (`get_builder_volumes` /
|
|
412
|
+
`list_builder_trades` / `get_builder_fee_rates` / `list_builder_leaderboard`)
|
|
413
|
+
mirror py-sdk's signatures but **raise `NotImplementedError`** — there is no
|
|
414
|
+
builder economy on paper. RFQ has no synchronous `SecureClient` method on py-sdk
|
|
415
|
+
(so the mirror invents none), the RFQ **types** re-export for type-hint parity,
|
|
416
|
+
and any RFQ *action* (incl. the async `open_rfq_session`) **raises
|
|
417
|
+
`NotImplementedError`** — RFQ quoting is not simulated.
|
|
418
|
+
- **Streams are core-topics-only, with documented seams.** `subscribe()` is
|
|
419
|
+
**async-only** (sync clients have no `subscribe`, matching py-sdk) and covers
|
|
420
|
+
py-sdk's **CORE** topics: `market` (`MarketSpec`), `crypto_prices`
|
|
421
|
+
(`CryptoPricesSpec`), and the authenticated `user` feed (`UserSpec`, secure
|
|
422
|
+
client only). The **sports / comments / equity** topics are **deferred** (their
|
|
423
|
+
specs/events aren't shipped). The documented stream seams: the market feed is
|
|
424
|
+
**top-of-book, not L2** (it emits `price_change` / `last_trade_price`, never a
|
|
425
|
+
`book` event — call `get_order_book` for the ladder); `MarketSpec.token_ids` are
|
|
426
|
+
the SDK's **`condition_id:LABEL`** tokens (not raw Polymarket numeric token ids);
|
|
427
|
+
stream id fields are plain `str` (not py-sdk's hex-validated newtypes); and the
|
|
428
|
+
lifecycle market events (`NewMarketEvent`, `MarketResolvedEvent`, …) are defined
|
|
429
|
+
for `match`-parity but never emitted on paper. Full detail in
|
|
430
|
+
[Realtime streams](#realtime-streams-v2-mirror).
|
|
431
|
+
- **Read-model fidelity gaps.** A handful of implemented reads are present but
|
|
432
|
+
*narrower* than py-sdk's (e.g. `Market` is a focused identity+state subset, not
|
|
433
|
+
the full gamma model; `list_markets` accepts py-sdk's full gamma keyword set but
|
|
434
|
+
only forwards the few PolySimulator honours; `get_last_trade_price` reports a
|
|
435
|
+
constant `side="BUY"` from a book snapshot with no trade side; `Decimal`
|
|
436
|
+
string-form may carry extra trailing zeros; `0` is the no-quote sentinel). None
|
|
437
|
+
break the call shape — see [Fidelity gaps](#fidelity-gaps-within-the-implemented-surface).
|
|
438
|
+
|
|
439
|
+
Because the real py-sdk is a **superset**, swapping back to real Polymarket only
|
|
440
|
+
ever *adds* capability over this seam — it never breaks a bot that stuck to the
|
|
441
|
+
mirrored surface and respected the seams above.
|
|
442
|
+
|
|
443
|
+
### Compat matrix
|
|
444
|
+
|
|
445
|
+
> Pinned to **`polymarket-client==0.1.0b8`** — parity is re-locked against this exact pin by the full-surface parity gate; when the pin bumps, this matrix + the badge bump with it.
|
|
446
|
+
|
|
447
|
+
Support level for every py-sdk surface area, cross-checked against what's
|
|
448
|
+
actually implemented in `polysim_polymarket`:
|
|
449
|
+
|
|
450
|
+
| Surface area | Support | Notes |
|
|
451
|
+
|---|---|---|
|
|
452
|
+
| **Public reads** (CLOB market data) | ✅ **Full parity** | `get_order_book(s)` / `get_midpoint(s)` / `get_price(s)` / `get_spread(s)` / `get_last_trade_price(s)` / `get_price_history` / `estimate_market_price` / `get_market` / `list_markets` — sync + async, identical call shapes & model fields (subset/sentinel gaps documented; see [Fidelity gaps](#fidelity-gaps-within-the-implemented-surface) for the `Market` subset + the ignored `list_markets` filters). |
|
|
453
|
+
| **Secure reads** (CLOB market data) | ✅ **Full parity** | The secure client shares the public reads, identical signatures. |
|
|
454
|
+
| **Account / auth** | 🟡 **Paper seam** | `fetch_api_keys` / `delete_api_key` / `get_balance_allowance` / `is_gasless_ready` / `get_closed_only_mode` / `get_notifications` / `get_order` / `list_open_orders` / `list_account_trades` implemented over paper auth (single `ps_live_*` key). Note: `is_gasless_ready` / `get_closed_only_mode` / `get_notifications` are **constant honest stubs** (return `True` / `False` / `()` with **no network call**), distinct from `get_balance_allowance` / `get_order` / `list_open_orders` / `list_account_trades`, which are genuinely backed by the paper API. py-sdk's wallet-auth `end_authentication` and the `drop_notifications` write are **deferred**. |
|
|
455
|
+
| **Trading** (orders) | 🟡 **Paper seam** | Full write surface (`create_*` / `place_*` / `post_order(s)` / `cancel_*`) with py-sdk's flat kwargs + `SignedOrder` / `OrderResponse` / `CancelOrdersResponse`. **Signing inert, settled in paper engine, not on-chain.** |
|
|
456
|
+
| **On-chain** (ERC-20/1155, split/merge/redeem, approvals) | 🟡 **Paper seam (no-op)** | All methods implemented with exact signatures; each is an **instant no-op** returning a placeholder tx handle — no chain write, no balance/ledger mutation, no balance-check. |
|
|
457
|
+
| **Rewards / scoring** | ⚪ **Stub (honest empties)** | `get_order(s)_scoring` / `list_*` rewards / `get_total_earnings_*` / `get_reward_percentages` implemented but return honest empties — no paper rewards engine yet. |
|
|
458
|
+
| **Builder attribution** | ⚪ **Stub (NotImplementedError)** | `get_builder_volumes` / `list_builder_trades` / `get_builder_fee_rates` / `list_builder_leaderboard` mirror signatures, raise `NotImplementedError` — no builder economy on paper. |
|
|
459
|
+
| **RFQ** | ⚪ **Stub (types + NotImplementedError)** | RFQ **types** re-export for type-hint parity; py-sdk has no sync `SecureClient` RFQ method (mirror invents none); the async `open_rfq_session` and any RFQ action raise `NotImplementedError`. |
|
|
460
|
+
| **Market stream** | 🟡 **Paper seam** | `subscribe(MarketSpec)` → `price_change` / `last_trade_price` over `/v1/ws/prices`. **TOB, not L2** (no `book` event); `condition_id:LABEL` token keying; lifecycle events defined-but-never-emitted. |
|
|
461
|
+
| **User stream** | 🟡 **Paper seam** | `subscribe(UserSpec)` (secure only) → order/trade fills over the authenticated executions channel; `UserSpec.markets` is a client-side filter; lean frames with honest derivations. |
|
|
462
|
+
| **Crypto stream** | 🟡 **Paper seam** | `subscribe(CryptoPricesSpec)` → Binance / Chainlink ticks, routed by exact source label, over the spot SSE feed. |
|
|
463
|
+
| **Sports / comments / equity streams** | 🔴 **Deferred** | `SportsSpec` / `CommentsSpec` / `EquityPricesSpec` (+ the `RtdsSpec` base alias) and their events are **not shipped**; pass a single CORE spec per `subscribe()`. |
|
|
464
|
+
| **Gamma / Data reads** (events, series, tags, comments, sports, portfolio, positions, leaderboards, combo, search, accounting export) | 🔴 **Deferred** | Not implemented on `PublicClient` / `SecureClient` (calling one raises `AttributeError`). Real py-sdk is a superset, so the swap to real Polymarket only *adds* them. |
|
|
465
|
+
|
|
466
|
+
Legend — **✅ Full parity**: same behaviour & shape as py-sdk (documented
|
|
467
|
+
narrowness aside). **🟡 Paper seam**: implemented with paper-mode semantics
|
|
468
|
+
(see the seam contract). **⚪ Stub**: importable & callable, returns an honest
|
|
469
|
+
empty or raises `NotImplementedError` — no fabricated data. **🔴 Deferred**: not
|
|
470
|
+
implemented yet; absent from the surface.
|
|
471
|
+
|
|
472
|
+
### Implemented surface — method reference
|
|
473
|
+
|
|
474
|
+
The **CLOB market-data READ** core (the foundation, shared by every client and
|
|
475
|
+
backed by the same proven read path as the v1 mirror):
|
|
476
|
+
|
|
477
|
+
| Method | Returns |
|
|
478
|
+
|---|---|
|
|
479
|
+
| `get_order_book(*, token_id)` / `get_order_books(*, token_ids)` | `OrderBook` / `tuple[OrderBook, …]` |
|
|
480
|
+
| `get_midpoint(*, token_id)` / `get_midpoints(*, token_ids)` | `Decimal` / `dict[str, Decimal]` |
|
|
481
|
+
| `get_price(*, token_id, side)` / `get_prices(*, requests)` | `Decimal` / `dict[str, dict[side, Decimal]]` |
|
|
482
|
+
| `get_spread(*, token_id)` / `get_spreads(*, token_ids)` | `Decimal` / `dict[str, Decimal]` |
|
|
483
|
+
| `get_last_trade_price(*, token_id)` / `get_last_trade_prices(*, token_ids)` | `LastTradePrice` / `tuple[LastTradePriceForToken, …]` |
|
|
484
|
+
| `get_price_history(*, token_id, …)` | `tuple[PriceHistoryPoint, …]` (a bare tuple, like py-sdk) |
|
|
485
|
+
| `estimate_market_price(*, token_id, side, …)` | `Decimal` (marginal/limit price — the worst book level touched to fill, like py-sdk; `FOK` underfill raises `InsufficientLiquidityError`, `FAK` falls back to the deepest level) |
|
|
486
|
+
| `get_market(*, id=…/slug=…)` | `Market` (with a nested `.state`) |
|
|
487
|
+
| `list_markets(*, …)` | `Paginator[Market]` — drive with `.first_page()` / `.iter_items()` |
|
|
488
|
+
|
|
489
|
+
The **authenticated `SecureClient`** adds, on top of those reads (see the
|
|
490
|
+
[compat matrix](#compat-matrix) for support level and the
|
|
491
|
+
[seam contract](#the-simreal-seam-contract-what-paper-mode-does-and-doesnt-do)
|
|
492
|
+
for paper semantics):
|
|
493
|
+
|
|
494
|
+
| Group | Methods |
|
|
495
|
+
|---|---|
|
|
496
|
+
| Account / auth | `fetch_api_keys` / `delete_api_key` / `get_balance_allowance` / `is_gasless_ready` / `get_closed_only_mode` / `get_notifications` / `get_order` / `list_open_orders` / `list_account_trades` |
|
|
497
|
+
| Trading | `create_limit_order` / `create_market_order` / `place_limit_order` / `place_market_order` / `post_order` / `post_orders` / `cancel_order` / `cancel_orders` / `cancel_all` / `cancel_market_orders` |
|
|
498
|
+
| On-chain (paper no-ops) | `approve_erc20` / `approve_erc1155_for_all` / `transfer_erc20` / `split_position` / `merge_positions` / `redeem_positions` / `setup_trading_approvals` / `setup_gasless_wallet` |
|
|
499
|
+
| Rewards / scoring (honest stubs) | `get_order_scoring` / `get_orders_scoring` / `list_current_rewards` / `list_market_rewards` / `list_user_earnings_for_day` / `get_total_earnings_for_user_for_day` / `list_user_earnings_and_markets_config` / `get_reward_percentages` |
|
|
500
|
+
| Builder (NotImplementedError) | `get_builder_volumes` / `list_builder_trades` / `get_builder_fee_rates` / `list_builder_leaderboard` |
|
|
501
|
+
|
|
502
|
+
Develop and paper-test a bot here against PolySimulator (no wallet, no key, no
|
|
503
|
+
gas), then run the **same code** on real Polymarket by swapping the prefix back to
|
|
504
|
+
`polymarket`, pointing at the real host, and supplying real credentials.
|
|
505
|
+
|
|
506
|
+
**Async twins.** Both clients have async counterparts — `AsyncPublicClient` (the
|
|
507
|
+
async twin of `PublicClient`) and `AsyncSecureClient` (the async twin of
|
|
508
|
+
`SecureClient`) — exposing the identical surface with every per-request method an
|
|
509
|
+
`async def` coroutine (the `list_*` reads stay synchronous, returning an
|
|
510
|
+
`AsyncPaginator` you then `await`). Construct with `AsyncSecureClient(api_key=…)`
|
|
511
|
+
or `await AsyncSecureClient.create(private_key=…)`, drive with `await`, and use
|
|
512
|
+
`async with AsyncSecureClient(…) as client:` for lifecycle — exactly as py-sdk's
|
|
513
|
+
async client does. The async twins share the same transport-free logic as the sync
|
|
514
|
+
clients (no second copy), so their behaviour is identical by construction.
|
|
515
|
+
|
|
516
|
+
### Seam details — implementation reference
|
|
517
|
+
|
|
518
|
+
The [seam contract](#the-simreal-seam-contract-what-paper-mode-does-and-doesnt-do)
|
|
519
|
+
above is the high-level "what diverges"; this section is the precise
|
|
520
|
+
implementation reference for each seam — the exact input guards, return values,
|
|
521
|
+
and edge behaviours. (A genuinely *deferred* method — the Gamma/Data reads — is
|
|
522
|
+
absent and raises `AttributeError`, which is honest: the surface simply isn't
|
|
523
|
+
there yet, rather than a stub that pretends and fails later. The stubs below
|
|
524
|
+
instead exist, are callable, and return honest empties / raise
|
|
525
|
+
`NotImplementedError`.)
|
|
526
|
+
|
|
527
|
+
- **EIP-712 signing & on-chain machinery** — the real-Polymarket signer /
|
|
528
|
+
builder-auth path. Paper trading needs no signer, so signing is
|
|
529
|
+
**accepted-and-inert**: the authenticated `SecureClient`'s **trading write
|
|
530
|
+
surface IS implemented** (`create_limit_order` / `create_market_order` /
|
|
531
|
+
`place_limit_order` / `place_market_order` / `post_order` / `post_orders` /
|
|
532
|
+
`cancel_order` / `cancel_orders` / `cancel_all` / `cancel_market_orders`, with
|
|
533
|
+
py-sdk's flat keyword args and `SignedOrder` / `OrderResponse` /
|
|
534
|
+
`CancelOrdersResponse` return models) — it just produces an inert-signed order
|
|
535
|
+
(empty signature) and submits the unsigned paper body.
|
|
536
|
+
- **On-chain methods are PAPER no-ops.** `approve_erc20` /
|
|
537
|
+
`approve_erc1155_for_all` / `transfer_erc20` / `split_position` /
|
|
538
|
+
`merge_positions` / `redeem_positions` / `setup_trading_approvals` /
|
|
539
|
+
`setup_gasless_wallet` are **implemented** with py-sdk's exact signatures, but
|
|
540
|
+
paper mode accepts the call (validating inputs) and **does NOT settle on-chain
|
|
541
|
+
or mutate any paper state** (no ledger write, no balance change): each returns a
|
|
542
|
+
paper transaction handle whose `.wait()` returns **instantly** with a
|
|
543
|
+
valid-format placeholder `TransactionOutcome` (`0x` + 64 hex hash,
|
|
544
|
+
`transaction_id=None`). In particular — unlike real py-sdk, which consults
|
|
545
|
+
on-chain ERC-1155 balances — `redeem_positions` / `merge_positions` do NOT
|
|
546
|
+
balance-check the position on paper, so they always succeed (the paper position
|
|
547
|
+
ledger isn't wired into the on-chain methods in this gate). py-sdk's pre-chain
|
|
548
|
+
input guards are replicated so a bot hits the SAME `UserInputError` in paper as
|
|
549
|
+
in prod (exactly-one of
|
|
550
|
+
`condition_id`/`legs`; exactly-one of `condition_id`/`market_id`/`position_id`;
|
|
551
|
+
positive amount on a combo split). **Address checks are a 40-hex FORMAT check,
|
|
552
|
+
NOT an EIP-55 checksum.** The accepted shape matches py-sdk's
|
|
553
|
+
`to_checksum_address` acceptance set — a 40-hex address with an **optional**
|
|
554
|
+
`0x`/`0X` prefix, case-insensitive — so the guard accepts every input py-sdk
|
|
555
|
+
accepts and rejects the same malformed ones (wrong length, non-hex). Paper
|
|
556
|
+
trading never touches a chain, so to keep the dependency surface thin the mirror
|
|
557
|
+
does not pull in `eth-utils`/`web3`/`eth_account`; that means — unlike py-sdk's
|
|
558
|
+
`to_checksum_address`, which normalizes any accepted address to its
|
|
559
|
+
EIP-55-checksummed form for on-chain use — the mirror does NOT compute the
|
|
560
|
+
checksum normalization and returns the address unchanged (moot on paper, where
|
|
561
|
+
the address is never used on a chain).
|
|
562
|
+
- **Rewards / scoring reads are honest empty stubs.** `get_order_scoring` →
|
|
563
|
+
`False`, `get_orders_scoring` → all-`False` dict, the `list_*` rewards reads →
|
|
564
|
+
empty paginators, `get_total_earnings_for_user_for_day` → `()`,
|
|
565
|
+
`get_reward_percentages` → `{}`. The paper **rewards engine** is a separate
|
|
566
|
+
backend roadmap item, so these return honest empties with no fabricated data —
|
|
567
|
+
a bot's reward-accounting loop runs and truthfully finds nothing (rewards
|
|
568
|
+
aren't earned on paper).
|
|
569
|
+
- **Builder attribution raises `NotImplementedError`.** `get_builder_volumes` /
|
|
570
|
+
`list_builder_trades` / `get_builder_fee_rates` / `list_builder_leaderboard`
|
|
571
|
+
mirror py-sdk's signatures but raise `NotImplementedError("Builder attribution
|
|
572
|
+
is not simulated in PolySimulator paper mode.")` — there is no builder fee, no
|
|
573
|
+
builder revenue ledger, no builder leaderboard on paper. The `builder_code=`
|
|
574
|
+
kwarg on the order builders stays inert (no builder fee).
|
|
575
|
+
- **Collateral / balances / positions** — the account balance/order reads ARE
|
|
576
|
+
implemented (`get_balance_allowance` / `get_order` / `list_open_orders` /
|
|
577
|
+
`list_account_trades`); the wider portfolio/positions reads are deferred (see
|
|
578
|
+
the Gamma/Data row of the [compat matrix](#compat-matrix)).
|
|
579
|
+
- **Realtime streams (CORE topics ARE implemented).** The async clients'
|
|
580
|
+
`subscribe()` covers py-sdk's **CORE** stream topics — `market`
|
|
581
|
+
(`MarketSpec` → book / price_change / last_trade), `crypto_prices`
|
|
582
|
+
(`CryptoPricesSpec` → Binance / Chainlink ticks), and the authenticated `user`
|
|
583
|
+
feed (`UserSpec` → order / trade fills, secure client only) — backed by our
|
|
584
|
+
`polysim_sdk` WS + SSE transport. See the **Realtime streams (v2 mirror)**
|
|
585
|
+
section below for the topic→transport map, the deferred topics, and the seams.
|
|
586
|
+
Streaming is **async-only** (the sync `PublicClient` / `SecureClient` have no
|
|
587
|
+
`subscribe`, mirroring py-sdk). The **sports / comments / equity** topics are
|
|
588
|
+
DEFERRED (their specs/events aren't shipped).
|
|
589
|
+
- **RFQ** — the RFQ session flow is not simulated. The **RFQ types**
|
|
590
|
+
(`RfqQuoteRequestEvent`, `RfqSession`, the enums, the rejection errors, …) DO
|
|
591
|
+
re-export from the package root so a maker bot's type hints survive the prefix
|
|
592
|
+
swap; py-sdk has no synchronous `SecureClient` RFQ method, so the mirror
|
|
593
|
+
invents none, and any RFQ *action* raises `NotImplementedError("RFQ quoting is
|
|
594
|
+
not simulated in PolySimulator paper mode.")`.
|
|
595
|
+
- **`Paginator` dataframe exports** — `.to_pandas` / `.to_polars` / `.to_arrow`
|
|
596
|
+
(they need pandas/polars/pyarrow) are not shipped; the paginator's
|
|
597
|
+
`.first_page()` / `.iter_items()` / iteration surface is here.
|
|
598
|
+
|
|
599
|
+
Because the real py-sdk is a **superset**, the swap to real Polymarket only ever
|
|
600
|
+
*adds* these — it never breaks a bot that stuck to the implemented surface.
|
|
601
|
+
|
|
602
|
+
### Fidelity gaps within the implemented surface
|
|
603
|
+
|
|
604
|
+
A handful of implemented methods are present but **narrower** than py-sdk's. None
|
|
605
|
+
break the prefix swap (the field names and call shapes match), but a bot that
|
|
606
|
+
relies on the listed behaviour will see *more* on real Polymarket than the
|
|
607
|
+
mirror provides:
|
|
608
|
+
|
|
609
|
+
- **`Market` is a focused subset.** The mirror's `Market` carries only the
|
|
610
|
+
identity fields (`id`, `condition_id`, `question`, `slug`) plus a focused
|
|
611
|
+
nested `.state` (`active` / `closed` / `neg_risk`). py-sdk's `Market` is a
|
|
612
|
+
deeply-nested gamma model — the **`outcomes`, per-outcome `prices`, and the
|
|
613
|
+
`metrics` / `volume` / `liquidity` blocks are not present here**, and
|
|
614
|
+
`market.state` omits py-sdk's wider fields (`archived`, `accepting_orders`,
|
|
615
|
+
`enable_order_book`, `start_date` / `end_date` / `closed_time`). The mirror
|
|
616
|
+
also **does not hex-validate `condition_id`** and **does not reject
|
|
617
|
+
non-binary markets** the way py-sdk's `Market` model does — a payload py-sdk
|
|
618
|
+
would raise `UnexpectedResponseError` on may bind here. Read only the focused
|
|
619
|
+
identity+state subset for a clean swap; the rest lands when `Market` widens.
|
|
620
|
+
- **`list_markets` accepts py-sdk's full gamma keyword set but only forwards a
|
|
621
|
+
few.** Only the filters PolySimulator's `GET /v1/markets` honours (`closed`,
|
|
622
|
+
`order`→`sort`, `ascending`) reach the server; **every other gamma keyword
|
|
623
|
+
(`liquidity_num_min/max`, `volume_num_min/max`, `tag_id`, `clob_token_ids`,
|
|
624
|
+
`question_ids`, `start_date_*` / `end_date_*`, …) is accepted for signature
|
|
625
|
+
parity and silently ignored**. The call won't error, but the result set is
|
|
626
|
+
*not* filtered by an ignored knob. (On real Polymarket every one of these
|
|
627
|
+
filters applies server-side.)
|
|
628
|
+
- **`get_last_trade_price` always reports side `BUY`.** PolySim's book snapshot
|
|
629
|
+
carries `last_trade_price` but **no trade side**, so the mirror fills py-sdk's
|
|
630
|
+
required `side` field with a constant `"BUY"`. Don't branch on the last-trade
|
|
631
|
+
side against the mirror — it isn't real provenance; on Polymarket it reflects
|
|
632
|
+
the actual maker/taker side.
|
|
633
|
+
- **`Decimal` string-form (trailing zeros) may differ.** The mirror computes
|
|
634
|
+
prices from PolySim's `/v1/book` and quantises to the 4-decimal grid, so a
|
|
635
|
+
midpoint/price/spread can render as `Decimal("0.5000")` where py-sdk's
|
|
636
|
+
server-computed value renders `Decimal("0.5")`. The **numeric value is equal**
|
|
637
|
+
(`==` and arithmetic match — the behavioural-parity suite asserts this), but
|
|
638
|
+
`str(value)` and `value.as_tuple()` can differ in trailing zeros. Compare by
|
|
639
|
+
numeric value, never by string form, for a clean swap.
|
|
640
|
+
- **`0` is the no-quote sentinel.** When a token's book has no two-sided quote,
|
|
641
|
+
`get_midpoint` / `get_price` / `get_spread` return `Decimal("0")` (py-sdk's
|
|
642
|
+
"no value available" sentinel) rather than `None` or an error. A genuine `0`
|
|
643
|
+
and a missing quote are indistinguishable — treat a `0` result as "no quote",
|
|
644
|
+
not as a real price of zero.
|
|
645
|
+
|
|
646
|
+
### Realtime streams (v2 mirror)
|
|
647
|
+
|
|
648
|
+
The async clients mirror py-sdk's `subscribe()` for the **CORE** stream topics,
|
|
649
|
+
backed by our `polysim_sdk` WS + SSE transport. A consumer iterates a
|
|
650
|
+
`SubscriptionHandle` and closes it (or uses it as an async context manager):
|
|
651
|
+
|
|
652
|
+
```python
|
|
653
|
+
from polysim_polymarket import AsyncSecureClient
|
|
654
|
+
from polysim_polymarket.streams import MarketSpec, CryptoPricesSpec, UserSpec
|
|
655
|
+
|
|
656
|
+
async with await AsyncSecureClient.create(api_key="ps_live_…") as client:
|
|
657
|
+
async with await client.subscribe(MarketSpec(token_ids=["…"])) as h:
|
|
658
|
+
async for ev in h: # MarketPriceChangeEvent / MarketLastTradePriceEvent
|
|
659
|
+
print(ev.payload.market, ev.type)
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
The stream surface lives in `polysim_polymarket.streams` (the specs, the event
|
|
663
|
+
types, `SubscriptionHandle`) — **exactly as py-sdk keeps it in
|
|
664
|
+
`polymarket.streams`. Neither package promotes any stream name to its package
|
|
665
|
+
root**, so nothing stream-related is re-exported from `polysim_polymarket` (the
|
|
666
|
+
re-export subset is empty by design).
|
|
667
|
+
|
|
668
|
+
**CORE topics → transport.** Each topic maps to one `polysim_sdk` stream:
|
|
669
|
+
|
|
670
|
+
| Spec | Topic | Events emitted | Transport |
|
|
671
|
+
|---|---|---|---|
|
|
672
|
+
| `MarketSpec` | `market` | `MarketPriceChangeEvent`, `MarketLastTradePriceEvent` | `ws.aprices_stream` (`/v1/ws/prices`) |
|
|
673
|
+
| `CryptoPricesSpec` | `prices.crypto.{binance,chainlink}` | `CryptoPricesBinanceEvent` / `CryptoPricesChainlinkEvent` | `sse.aspot_stream` (`/prices/stream`) |
|
|
674
|
+
| `UserSpec` *(secure only)* | `user` | `UserTradeEvent`, `UserOrderEvent` | `ws.aexecutions_stream` (`/v1/ws/executions`) |
|
|
675
|
+
|
|
676
|
+
`subscribe()` is **async-only** (the sync clients have no `subscribe`, mirroring
|
|
677
|
+
py-sdk). The handle is a bounded queue with **drop-oldest backpressure**: under a
|
|
678
|
+
slow consumer it drops the oldest events and counts the losses in
|
|
679
|
+
`handle.dropped`. Pass `queue_size=` (a mirror-only keyword) to size it.
|
|
680
|
+
|
|
681
|
+
**Seams — documented, not fabricated.** Because PolySimulator's push surface is
|
|
682
|
+
leaner than Polymarket's, the mirror is explicit where a py-sdk field has no
|
|
683
|
+
real provenance:
|
|
684
|
+
|
|
685
|
+
- **Auth handshake (user stream).** The `user` feed mints a short-lived WS JWT
|
|
686
|
+
from the secure client's API key (`ws_token()`) and connects to the
|
|
687
|
+
authenticated `/v1/ws/executions` channel — so it's scoped to that one account.
|
|
688
|
+
There is no per-subscription credential; `UserSpec.markets`, when set, is
|
|
689
|
+
applied as a **client-side filter** in the adapter (the channel delivers all of
|
|
690
|
+
the account's fills).
|
|
691
|
+
- **`MarketSpec.token_ids` are the SDK's `condition_id:LABEL` tokens** (the
|
|
692
|
+
same form `get_*` and trading use) — **not** raw Polymarket numeric token ids.
|
|
693
|
+
The prices WS subscribes by `condition_id`, so the opener strips each token's
|
|
694
|
+
`:LABEL` suffix (a **generic last-colon split**, so non-binary `UP`/`DOWN`
|
|
695
|
+
labels strip correctly — not the binary-only `_split_token`), de-duplicates,
|
|
696
|
+
and subscribes those condition ids. Each delivered frame carries per-outcome
|
|
697
|
+
entries with a `label` (`Yes`/`No`/`Up`/`Down`) and the Polymarket
|
|
698
|
+
**CLOB-numeric** `token_id`; that raw digit is a *different namespace* from the
|
|
699
|
+
SDK token, so the adapter does **not** filter on it (it would match nothing).
|
|
700
|
+
Instead the adapter **derives** each outcome's SDK token
|
|
701
|
+
`f"{condition_id}:{LABEL}"` from the frame's `market_id` + outcome `label` and
|
|
702
|
+
both filters on and emits that — so a `MarketSpec` built from the SDK's own
|
|
703
|
+
`condition_id:LABEL` tokens (e.g. `["0xCID:YES", "0xCID:NO"]`) subscribes
|
|
704
|
+
`["0xCID"]` AND receives correctly-filtered events for those outcomes, end to
|
|
705
|
+
end. The emitted event's `token_id` is the `condition_id:LABEL` form a bot
|
|
706
|
+
uses for reads/trading, never the backend CLOB digit. (A raw pm numeric token
|
|
707
|
+
id is not a valid `MarketSpec.token_id` here.)
|
|
708
|
+
- **No `book` event from the market stream.** `/v1/ws/prices` is a top-of-book
|
|
709
|
+
cache, not an L2 ladder feed — so the market stream emits `price_change` and
|
|
710
|
+
`last_trade_price` but never a full `book` event. (Call `get_order_book` for
|
|
711
|
+
the ladder.)
|
|
712
|
+
- **Stream id fields are plain `str`.** Token / condition / order id fields on
|
|
713
|
+
the stream-event models (`token_id`, `market`, `id`, `taker_order_id`, …) are
|
|
714
|
+
plain `str`, consistent with the mirror-wide id-typing decision — they are
|
|
715
|
+
**not** py-sdk's hex-validated `TokenId` / `CtfConditionId` newtypes, so no
|
|
716
|
+
hex-shape validation is applied to ids arriving on the wire.
|
|
717
|
+
- **Custom-feature / lifecycle market events are defined for parity but never
|
|
718
|
+
emitted.** `MarketBestBidAskEvent`, `NewMarketEvent`, `MarketResolvedEvent`,
|
|
719
|
+
and `MarketTickSizeChangeEvent` are part of the `MarketEvent` union (so a ported
|
|
720
|
+
bot's imports and `match`-on-`type` resolve) but the paper stream produces none
|
|
721
|
+
of them — `MarketSpec(custom_feature_enabled=True)` is accepted for signature
|
|
722
|
+
parity and is otherwise inert.
|
|
723
|
+
- **Lean user frames, honest derivation.** The backend fill frame carries
|
|
724
|
+
`market_id` + `outcome` but **no CLOB token/asset id**, so the event's
|
|
725
|
+
`token_id` is *derived* — `f"{market_id}:{OUTCOME}"`, the SDK's canonical token
|
|
726
|
+
convention (the inverse of `_split_token`, the same form `get_*`/trading use),
|
|
727
|
+
not faked from the order id. `id`/`taker_order_id` keep the fill's real
|
|
728
|
+
`order_id` (those are order ids). Remaining py-sdk fields the frame lacks get
|
|
729
|
+
honest defaults: the order's `original_size` == `size_matched` == the filled
|
|
730
|
+
`quantity` with `order_event_type="UPDATE"`, `status="MATCHED"`. A fill missing
|
|
731
|
+
its `outcome` yields no event (no honest token could be built).
|
|
732
|
+
- **`last_trade_price` side + size are read from the frame top level.** The
|
|
733
|
+
prices producer stamps the most-recent trade's `last_trade_side` /
|
|
734
|
+
`last_trade_size` at the **frame top level** (not on the per-outcome entry,
|
|
735
|
+
which carries only the trade *price*). The adapter reads them there and emits
|
|
736
|
+
the real side — a SELL trade yields `side="SELL"`, never a fabricated `BUY`. If
|
|
737
|
+
a frame ever lacks the top-level side, the `last_trade_price` event is dropped
|
|
738
|
+
rather than mislabeled.
|
|
739
|
+
- **Crypto topic routing is by exact source label.** The Binance feed's wire
|
|
740
|
+
`source` is `polymarket_rtds` (with `relay_binance` as a fallback-relay label);
|
|
741
|
+
Chainlink's is `chainlink_rtds`. The adapter routes by exact membership in
|
|
742
|
+
those label sets — `prices.crypto.binance` receives the live Binance ticks and
|
|
743
|
+
Binance ticks never leak into a `prices.crypto.chainlink` subscription.
|
|
744
|
+
|
|
745
|
+
**DEFERRED topics.** py-sdk's `polymarket.streams` also ships `sports`
|
|
746
|
+
(`SportsSpec` / `SportsEvent`), `comments` (`CommentsSpec` / `CommentsEvent` /
|
|
747
|
+
reactions), and `prices.equity.pyth` (`EquityPricesSpec` / `EquityPricesEvent`),
|
|
748
|
+
plus the `RtdsSpec` / `StreamEvent` aliases and the merged multi-spec
|
|
749
|
+
`subscribe(Sequence[...])`. **None of those are shipped here** — pass a single
|
|
750
|
+
CORE spec per `subscribe()` call. Because real py-sdk is a superset, the swap to
|
|
751
|
+
real Polymarket only *adds* them.
|
|
752
|
+
|
|
753
|
+
---
|
|
754
|
+
|
|
755
|
+
## Migrating from `py-clob-client`
|
|
756
|
+
|
|
757
|
+
> The import below is the **v1** path (`py_clob_client`). `py-clob-client-v2`
|
|
758
|
+
> (`py_clob_client_v2`) is a different package and is out of scope — see
|
|
759
|
+
> [Polymarket compatibility](#polymarket-compatibility) above.
|
|
760
|
+
|
|
761
|
+
If you have a Polymarket bot, you change **three things** and delete the on-chain
|
|
762
|
+
prelude. Everything else — method names, argument shapes, return shapes — stays.
|
|
763
|
+
|
|
764
|
+
```diff
|
|
765
|
+
- from py_clob_client.client import ClobClient
|
|
766
|
+
- from py_clob_client.clob_types import OrderArgs, OrderType
|
|
767
|
+
+ from polysim_clob_client.client import ClobClient
|
|
768
|
+
+ from polysim_clob_client.clob_types import OrderArgs, OrderType
|
|
769
|
+
|
|
770
|
+
- client = ClobClient(
|
|
771
|
+
- host="https://clob.polymarket.com",
|
|
772
|
+
- key=PRIVATE_KEY, # your wallet's private key
|
|
773
|
+
- chain_id=POLYGON,
|
|
774
|
+
- signature_type=1,
|
|
775
|
+
- funder=PROXY_WALLET_ADDRESS,
|
|
776
|
+
- )
|
|
777
|
+
- client.set_api_creds(client.create_or_derive_api_creds())
|
|
778
|
+
- # ...plus USDC allowance/approval txns, web3 setup, etc.
|
|
779
|
+
+ client = ClobClient(
|
|
780
|
+
+ host="https://api.polysimulator.com",
|
|
781
|
+
+ key="ps_live_…", # your PolySimulator API key
|
|
782
|
+
+ )
|
|
783
|
+
|
|
784
|
+
# unchanged from here on:
|
|
785
|
+
order = client.create_order(OrderArgs(token_id=tid, price=0.55, size=10, side="BUY"))
|
|
786
|
+
resp = client.post_order(order)
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
### What maps how
|
|
790
|
+
|
|
791
|
+
Every method maps by one of three strategies:
|
|
792
|
+
|
|
793
|
+
- **mirror** — identical behaviour, delegated to the HTTP core.
|
|
794
|
+
- **adapt** — translated onto the PolySim REST surface (e.g. `get_midpoint`,
|
|
795
|
+
`get_price`, `get_spread` are computed from `GET /v1/markets/{id}/book`).
|
|
796
|
+
- **stub-noop** — on-chain machinery with no analog (allowances, signing,
|
|
797
|
+
scoring, builder auth). These return a benign canned value and **make no
|
|
798
|
+
network call**; each says so in its docstring so nothing breaks silently.
|
|
799
|
+
|
|
800
|
+
### `token_id` ↔ `(market_id, outcome)`
|
|
801
|
+
|
|
802
|
+
`py-clob-client` addresses a single outcome token by `token_id`; PolySimulator
|
|
803
|
+
addresses a **market plus an outcome**. The seam: a bare `token_id` is treated as
|
|
804
|
+
the market id with outcome `YES`. Append `":NO"` or `":YES"` to target the other
|
|
805
|
+
side explicitly.
|
|
806
|
+
|
|
807
|
+
```python
|
|
808
|
+
client.create_order(OrderArgs(token_id="0xMARKET", ...)) # → market 0xMARKET, YES
|
|
809
|
+
client.create_order(OrderArgs(token_id="0xMARKET:NO", ...)) # → market 0xMARKET, NO
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
**Reads are true-token-parity; writes use the market+outcome model.** This is an
|
|
813
|
+
intentional asymmetry:
|
|
814
|
+
|
|
815
|
+
- **Book/quote reads** — `get_order_book`, `get_midpoint`, `get_price`,
|
|
816
|
+
`get_spread`, `get_tick_size`, `get_neg_risk`, `get_last_trade_price`,
|
|
817
|
+
`calculate_market_price` — send a **bare** `token_id` to the token-native
|
|
818
|
+
endpoint `GET /v1/book?token_id=...`. This matches Polymarket's CLOB book
|
|
819
|
+
reads exactly: pass the real outcome-token id you already use with
|
|
820
|
+
`py-clob-client` and the quote comes back for *that* token, no `:YES`/`:NO`
|
|
821
|
+
needed. The `condition_id:OUTCOME` colon form still works and routes to the
|
|
822
|
+
condition-id book endpoint (threading the outcome through), but it is a
|
|
823
|
+
PolySimulator convenience extension, not the parity path.
|
|
824
|
+
- **Order writes** — `create_order` / `create_market_order` / `post_order` —
|
|
825
|
+
resolve the `token_id` through the market+outcome seam above and submit to the
|
|
826
|
+
rich `POST /v1/orders` endpoint (with slippage/impact/position telemetry in the
|
|
827
|
+
response). A *bare* token id on the write path is read as a `market_id` with
|
|
828
|
+
the `YES` outcome; if that id isn't a valid market, the server rejects the
|
|
829
|
+
order loudly — it never silently places a different one.
|
|
830
|
+
|
|
831
|
+
If you only ever pass real outcome-token ids (the `py-clob-client` norm), reads
|
|
832
|
+
"just work" with full parity. The `:YES`/`:NO` suffix exists for callers who
|
|
833
|
+
prefer to address PolySimulator markets by condition id directly.
|
|
834
|
+
|
|
835
|
+
### Orders are never signed
|
|
836
|
+
|
|
837
|
+
`create_order` / `create_market_order` return a plain `dict` with **no
|
|
838
|
+
`signature` field** — there is nothing to sign. `post_order` serialises it
|
|
839
|
+
straight to `POST /v1/orders`. The recommended one-call path is
|
|
840
|
+
`create_and_post_order(...)`.
|
|
841
|
+
|
|
842
|
+
### Auth assertions
|
|
843
|
+
|
|
844
|
+
The three `py-clob-client` auth levels collapse into one. `assert_level_1_auth()`
|
|
845
|
+
and `assert_builder_auth()` are no-ops; `assert_level_2_auth()` raises
|
|
846
|
+
`PolyApiException` only if no API key is configured.
|
|
847
|
+
|
|
848
|
+
---
|
|
849
|
+
|
|
850
|
+
## Pagination
|
|
851
|
+
|
|
852
|
+
The native client exposes Python iterators that page transparently:
|
|
853
|
+
|
|
854
|
+
```python
|
|
855
|
+
from polysim_sdk import PolySimClient
|
|
856
|
+
from polysim_sdk.pagination import iter_markets, iter_orders
|
|
857
|
+
|
|
858
|
+
with PolySimClient() as client:
|
|
859
|
+
for market in iter_markets(client, hot_only=True):
|
|
860
|
+
...
|
|
861
|
+
for order in iter_orders(client, status="OPEN"):
|
|
862
|
+
...
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
The drop-in surface preserves `py-clob-client`'s base64 cursor protocol, so the
|
|
866
|
+
classic loop terminates correctly:
|
|
867
|
+
|
|
868
|
+
```python
|
|
869
|
+
from polysim_clob_client.constants import END_CURSOR
|
|
870
|
+
|
|
871
|
+
cursor = ""
|
|
872
|
+
while cursor != END_CURSOR:
|
|
873
|
+
page = client.get_markets(cursor)
|
|
874
|
+
handle(page["data"])
|
|
875
|
+
cursor = page["next_cursor"]
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
---
|
|
879
|
+
|
|
880
|
+
## WebSocket streaming
|
|
881
|
+
|
|
882
|
+
```python
|
|
883
|
+
from polysim_sdk import PolySimClient
|
|
884
|
+
from polysim_sdk.ws import prices_stream, executions_stream
|
|
885
|
+
|
|
886
|
+
with PolySimClient() as client:
|
|
887
|
+
for event in prices_stream(client, ["0xMARKET_A", "0xMARKET_B"]):
|
|
888
|
+
print(event) # blocks; reconnects with a fresh token + backoff
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
Async generators (`aprices_stream`, `aexecutions_stream`) are available for
|
|
892
|
+
`async for`. The SDK mints a short-lived WS JWT per connection automatically.
|
|
893
|
+
|
|
894
|
+
### SSE: live underlying spot
|
|
895
|
+
|
|
896
|
+
The WS channel above carries **market** prices. The live **underlying spot**
|
|
897
|
+
(BTC/ETH/SOL/… ticks — the number an Up/Down bet resolves against) comes from the
|
|
898
|
+
public, unauthenticated `/prices/stream` Server-Sent-Events firehose instead:
|
|
899
|
+
|
|
900
|
+
```python
|
|
901
|
+
from polysim_sdk import PolySimClient
|
|
902
|
+
from polysim_sdk.sse import spot_stream
|
|
903
|
+
|
|
904
|
+
with PolySimClient() as client:
|
|
905
|
+
for event in spot_stream(client, ["BTC", "ETH"], crypto_source="chainlink"):
|
|
906
|
+
if event["event"] == "crypto_price":
|
|
907
|
+
tick = event["data"]
|
|
908
|
+
print(tick["symbol"], tick["price"], tick["source"])
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
Each event is `{"event": <type>, "data": <payload>}`. The spot-bearing types are
|
|
912
|
+
`crypto_price` and `crypto_price_batch`; `keepalive` arrives every ~5 s. Pass
|
|
913
|
+
`crypto_source="chainlink"` (authoritative for 5m/15m settlement) or `"binance"`
|
|
914
|
+
(denser ~10 Hz ticks); omit for both. With `condition_ids=[...]` the same stream
|
|
915
|
+
also delivers `snapshot` / `market_price` / `orderbook` events. The async twin
|
|
916
|
+
`aspot_stream` is available for `async for`. Both reconnect with exponential
|
|
917
|
+
backoff and a per-connection cache-buster (so a CDN can't coalesce subscribers).
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
921
|
+
## Errors
|
|
922
|
+
|
|
923
|
+
Catch `PolySimError` to handle every SDK-raised error; `PolyApiException` (the
|
|
924
|
+
drop-in alias) is the same class.
|
|
925
|
+
|
|
926
|
+
```python
|
|
927
|
+
from polysim_sdk.exceptions import (
|
|
928
|
+
EdgeBlockedError,
|
|
929
|
+
PolySimError,
|
|
930
|
+
RateLimitError,
|
|
931
|
+
ValidationError,
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
try:
|
|
935
|
+
fill = client.place_order(...)
|
|
936
|
+
except RateLimitError as exc:
|
|
937
|
+
print(f"backing off for {exc.retry_after}s")
|
|
938
|
+
except ValidationError as exc:
|
|
939
|
+
print(f"bad request: {exc.code} → {exc}")
|
|
940
|
+
except EdgeBlockedError as exc:
|
|
941
|
+
print(f"blocked at the CDN edge — check your User-Agent: {exc}")
|
|
942
|
+
except PolySimError as exc:
|
|
943
|
+
print(f"other API error ({exc.status_code}): {exc}")
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
The client paces itself (a 50 ms floor between requests) and backs off on
|
|
947
|
+
`429`/`425`/`5xx` using `Retry-After`. Opt out with `floor_interval=0.0` and
|
|
948
|
+
`max_retries=0` to handle pacing yourself.
|
|
949
|
+
|
|
950
|
+
### Troubleshooting: `error code: 1010` / blocked User-Agent
|
|
951
|
+
|
|
952
|
+
If a raw HTTP call returns **HTTP 403** with the body `error code: 1010`, the
|
|
953
|
+
request was blocked at the CDN edge — **not** by the API. The edge rejects
|
|
954
|
+
Python's stdlib `urllib` default User-Agent (`Python-urllib/x.y`). `requests`,
|
|
955
|
+
`httpx`, `aiohttp`, and **this SDK** all send User-Agents that pass, so you only
|
|
956
|
+
hit this with raw `urllib` or a custom client that forwards the stdlib UA.
|
|
957
|
+
|
|
958
|
+
This SDK sends a branded `User-Agent: polysim-sdk/<version>` and turns any edge
|
|
959
|
+
block into a clear `EdgeBlockedError` (an `ApiError`, `code="EDGE_BLOCKED"`). If
|
|
960
|
+
you must call the API without the SDK, set your own header:
|
|
961
|
+
|
|
962
|
+
```python
|
|
963
|
+
# raw urllib — set a User-Agent so the edge lets you through
|
|
964
|
+
import urllib.request, json
|
|
965
|
+
req = urllib.request.Request(
|
|
966
|
+
"https://api.polysimulator.com/v1/markets?limit=1",
|
|
967
|
+
headers={"User-Agent": "my-app/1.0", "X-API-Key": "ps_live_..."},
|
|
968
|
+
)
|
|
969
|
+
data = json.load(urllib.request.urlopen(req))
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
---
|
|
973
|
+
|
|
974
|
+
## Rate limits
|
|
975
|
+
|
|
976
|
+
Authoritative values are returned live by `GET /v1/keys/tiers` (`client.tiers()`).
|
|
977
|
+
At the time of writing:
|
|
978
|
+
|
|
979
|
+
| Tier | rps | rpm | WS conns | Batch | API balance |
|
|
980
|
+
|---|---|---|---|---|---|
|
|
981
|
+
| Free | 2 | 120 | 1 | 1 | none — read only |
|
|
982
|
+
| Pro | 10 | 600 | 3 | 5 | $10K |
|
|
983
|
+
| Pro+ | 30 | 1800 | 10 | 10 | $25K |
|
|
984
|
+
| Enterprise | 100 | 6000 | 50 | 25 | custom |
|
|
985
|
+
|
|
986
|
+
`free` allows 2 requests/second up to 120/minute and is read-only (no API
|
|
987
|
+
balance). Use `pro` or above for any continuously-running bot. The table is
|
|
988
|
+
illustrative — always trust `client.tiers()` for the live values.
|
|
989
|
+
|
|
990
|
+
---
|
|
991
|
+
|
|
992
|
+
## Beta caveats
|
|
993
|
+
|
|
994
|
+
During the closed-cohort phase, a few sim behaviours differ from Polymarket.
|
|
995
|
+
These are known limitations, not bugs:
|
|
996
|
+
|
|
997
|
+
1. **Shared API balance across keys.** Every key a user owns draws on the same
|
|
998
|
+
balance pool today (the `balance` field returned by `me()`). Treat it as
|
|
999
|
+
user-scoped, not key-scoped.
|
|
1000
|
+
2. **Maker/taker classification.** A marketable-at-placement GTC (one that
|
|
1001
|
+
crosses the book on submit) is classified as a **taker** and charged the taker
|
|
1002
|
+
fee, matching Polymarket; only a genuinely-resting GTC that is filled later —
|
|
1003
|
+
when the market moves into it — is booked as a $0-fee maker. The realized
|
|
1004
|
+
per-fill fee *rate* is surfaced as `ClobTrade.fee_rate_bps` (the realized fee
|
|
1005
|
+
*amount* is not a typed field).
|
|
1006
|
+
3. **Orderbook freshness.** The book cache TTL is 300 s. For markets Polymarket
|
|
1007
|
+
updates less often than that, a fill may revert to displayed midpoint; a ±15%
|
|
1008
|
+
sanity guard bounds the drift. High-volume markets always fill at real ask/bid.
|
|
1009
|
+
|
|
1010
|
+
---
|
|
1011
|
+
|
|
1012
|
+
## What's included
|
|
1013
|
+
|
|
1014
|
+
| Path | Purpose |
|
|
1015
|
+
|---|---|
|
|
1016
|
+
| `polysim_sdk/` | Native client: `PolySimClient` (sync) + `AsyncPolySimClient` (async), `ws`, `sse` (underlying-spot stream), `updown` (Up/Down row helpers), `pagination`, `exceptions`, `constants`. |
|
|
1017
|
+
| `polysim_clob_client/` | `py-clob-client` drop-in: `ClobClient` + matching `clob_types`, `constants`, `order_builder`, `exceptions`. |
|
|
1018
|
+
| `polysim_polymarket/` | Unified **py-sdk** (`polymarket`) paper-mode mirror: sync + async `PublicClient` / `SecureClient` (CLOB reads, account/auth, trading), on-chain paper no-ops, rewards/builder/RFQ stubs, and core realtime streams — returning typed pydantic models. See [Polymarket py-sdk (v2) mirror](#polymarket-py-sdk-v2-mirror). |
|
|
1019
|
+
| `scripts/01_balance_and_market.py` | Smallest end-to-end native demo. |
|
|
1020
|
+
| `scripts/02_clob_dropin_demo.py` | The `py-clob-client` drop-in surface end-to-end. |
|
|
1021
|
+
| `scripts/03_async_concurrent.py` | Concurrent reads via the async client. |
|
|
1022
|
+
| `scripts/04_btc_updown.py` | Discover the live BTC Up/Down window: spot, strike, prices, tokens. |
|
|
1023
|
+
| `tests/` | respx-mocked unit tests for both surfaces (no creds, no network). |
|
|
1024
|
+
|
|
1025
|
+
Run the test suite with `pytest`. Lint/type with `ruff check . && mypy polysim_sdk polysim_clob_client polysim_polymarket`.
|
|
1026
|
+
|
|
1027
|
+
---
|
|
1028
|
+
|
|
1029
|
+
## License
|
|
1030
|
+
|
|
1031
|
+
[Apache License 2.0](./LICENSE). You may use, modify, and redistribute this
|
|
1032
|
+
SDK — including in commercial and closed-source projects — provided you keep
|
|
1033
|
+
the license and attribution notices (see [`NOTICE`](./NOTICE)). Apache-2.0 also
|
|
1034
|
+
grants an explicit patent license, which is why many companies prefer it over
|
|
1035
|
+
MIT for dependencies.
|
|
1036
|
+
|
|
1037
|
+
The `polysim_clob_client` package mirrors the *public surface* of
|
|
1038
|
+
[`py-clob-client`](https://github.com/Polymarket/py-clob-client) (MIT) for
|
|
1039
|
+
drop-in compatibility; no upstream source is bundled. This project is not
|
|
1040
|
+
affiliated with or endorsed by Polymarket.
|