polynode 0.14.1__tar.gz → 0.15.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. polynode-0.15.1/CHANGELOG.md +10 -0
  2. {polynode-0.14.1 → polynode-0.15.1}/PKG-INFO +111 -6
  3. {polynode-0.14.1 → polynode-0.15.1}/README.md +110 -5
  4. {polynode-0.14.1 → polynode-0.15.1}/core-contract-v1.json +55 -1
  5. polynode-0.15.1/polynode/__init__.py +161 -0
  6. polynode-0.15.1/polynode/_version.py +1 -0
  7. polynode-0.15.1/polynode/market_protocol.py +466 -0
  8. {polynode-0.14.1 → polynode-0.15.1}/polynode/redemption_watcher.py +66 -8
  9. {polynode-0.14.1 → polynode-0.15.1}/polynode/short_form.py +35 -2
  10. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/__init__.py +17 -1
  11. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/clob_api.py +12 -0
  12. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/constants.py +16 -0
  13. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/eip712.py +93 -7
  14. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/onboarding.py +192 -1
  15. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/position_management.py +93 -0
  16. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/sqlite_backend.py +37 -1
  17. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/trader.py +566 -63
  18. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/types.py +58 -1
  19. {polynode-0.14.1 → polynode-0.15.1}/polynode/types/__init__.py +1 -0
  20. {polynode-0.14.1 → polynode-0.15.1}/polynode/types/events.py +59 -0
  21. {polynode-0.14.1 → polynode-0.15.1}/polynode/types/perps.py +62 -1
  22. {polynode-0.14.1 → polynode-0.15.1}/polynode/types/short_form.py +3 -0
  23. polynode-0.15.1/polynode/types/v3.py +263 -0
  24. {polynode-0.14.1 → polynode-0.15.1}/polynode/v3.py +469 -3
  25. {polynode-0.14.1 → polynode-0.15.1}/polynode/v3_operations.py +9 -0
  26. {polynode-0.14.1 → polynode-0.15.1}/pyproject.toml +1 -1
  27. polynode-0.14.1/polynode/__init__.py +0 -83
  28. polynode-0.14.1/polynode/_version.py +0 -1
  29. {polynode-0.14.1 → polynode-0.15.1}/.gitignore +0 -0
  30. {polynode-0.14.1 → polynode-0.15.1}/core-fixtures-v1.json +0 -0
  31. {polynode-0.14.1 → polynode-0.15.1}/polynode/cache/__init__.py +0 -0
  32. {polynode-0.14.1 → polynode-0.15.1}/polynode/client.py +0 -0
  33. {polynode-0.14.1 → polynode-0.15.1}/polynode/engine.py +0 -0
  34. {polynode-0.14.1 → polynode-0.15.1}/polynode/errors.py +0 -0
  35. {polynode-0.14.1 → polynode-0.15.1}/polynode/orderbook.py +0 -0
  36. {polynode-0.14.1 → polynode-0.15.1}/polynode/orderbook_integrity.py +0 -0
  37. {polynode-0.14.1 → polynode-0.15.1}/polynode/orderbook_state.py +0 -0
  38. {polynode-0.14.1 → polynode-0.15.1}/polynode/perps.py +0 -0
  39. {polynode-0.14.1 → polynode-0.15.1}/polynode/subscription.py +0 -0
  40. {polynode-0.14.1 → polynode-0.15.1}/polynode/testing.py +0 -0
  41. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/cosigner.py +0 -0
  42. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/escrow.py +0 -0
  43. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/privy.py +0 -0
  44. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/relayer.py +0 -0
  45. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/signer.py +0 -0
  46. {polynode-0.14.1 → polynode-0.15.1}/polynode/trading/user_relayer.py +0 -0
  47. {polynode-0.14.1 → polynode-0.15.1}/polynode/types/enums.py +0 -0
  48. {polynode-0.14.1 → polynode-0.15.1}/polynode/types/orderbook.py +0 -0
  49. {polynode-0.14.1 → polynode-0.15.1}/polynode/types/rest.py +0 -0
  50. {polynode-0.14.1 → polynode-0.15.1}/polynode/types/ws.py +0 -0
  51. {polynode-0.14.1 → polynode-0.15.1}/polynode/ws.py +0 -0
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.15.1 (2026-10-11)
4
+
5
+ - Fixed `wrap_to_polyusd()` and `unwrap_from_polyusd()` for EOA wallets on Polygon. With web3 7 or 8 they failed before sending anything, with an `ExtraDataLengthError` about a POA chain. They now set gas and gas price themselves.
6
+ - `wrap_to_polyusd()` and `unwrap_from_polyusd()` now wait for the approval transaction to confirm before sending the wrap or unwrap, and return the transaction hash with a `0x` prefix.
7
+ - Fixed `set_approvals()` on Polygon in the same way.
8
+ - `ensure_ready()` results have a new `approval_tx_hashes` field with the hashes of any approval transactions that call sent. It is an empty list when nothing was sent. Existing fields are unchanged.
9
+
10
+ Earlier releases are described at the top of the README.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: polynode
3
- Version: 0.14.1
3
+ Version: 0.15.1
4
4
  Summary: Python SDK for the Polynode real-time prediction market data platform
5
5
  Project-URL: Homepage, https://polynode.dev
6
6
  Project-URL: Documentation, https://docs.polynode.dev
@@ -36,7 +36,11 @@ Description-Content-Type: text/markdown
36
36
 
37
37
  Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.
38
38
 
39
- **New in v0.14.1:** Every prepared user-owned order exposes its canonical V2 exchange `order_hash` before submission. This is the exact CLOB/open-order ID and fill `order_hash`; deposit-wallet orders hash the standard inner `Order`, not the POLY_1271 wallet-signing wrapper. Record it before the network attempt so timeouts can be reconciled without risking a duplicate.
39
+ **New in v0.15.1:** Fixes `wrap_to_polyusd()` and `unwrap_from_polyusd()` for EOA wallets on Polygon. With web3 7 or 8 they failed before sending anything, with an `ExtraDataLengthError` about a POA chain. They now set gas and gas price themselves, wait for the approval to confirm before sending the wrap or unwrap, and return the transaction hash with a `0x` prefix. `set_approvals()` gets the same gas price fix. `ensure_ready()` results now include `approval_tx_hashes`: the hashes of any approval transactions that call sent, or an empty list when none were needed.
40
+
41
+ **In v0.15.0:** Polymarket Protocol V2 markets (October 2026). Upgrade before 2026-11-02 if you trade: `order()` looks up each market's protocol and signs Protocol V2 orders for the right exchange automatically, and split, merge and the new redeem work on them too. Nothing changes for current markets or for streaming and REST users. This is not the April 2026 "V2" (CTF Exchange V2 and pUSD) described elsewhere in this README; see [Polymarket Protocol V2 markets](#polymarket-protocol-v2-markets).
42
+
43
+ **In v0.14.1:** Every prepared user-owned order exposes its canonical V2 exchange `order_hash` before submission. This is the exact CLOB/open-order ID and fill `order_hash`; deposit-wallet orders hash the standard inner `Order`, not the POLY_1271 wallet-signing wrapper. Record it before the network attempt so timeouts can be reconciled without risking a duplicate.
40
44
 
41
45
  **In v0.14.0:** Web platforms can take a user from wallet authorization through an exact browser-signed user-owned order without exposing backend credentials. The SDK imports the shared versioned browser bundle into memory, produces a credential-free signing request with a complete order preview, supports one-time multi-worker state, validates canonical signatures and wallet identity, checks BUY collateral before prompting, and submits with exact zero builder attribution.
42
46
 
@@ -72,7 +76,6 @@ with PolyNode(api_key="pn_live_...") as pn:
72
76
  status = pn.status()
73
77
  connections = pn.connections()
74
78
  markets = pn.markets(count=10)
75
- settlements = pn.recent_settlements(count=5)
76
79
  wallet_positions = pn.wallet_positions(
77
80
  address, redeemable=True, condition_id=condition_id
78
81
  )
@@ -127,7 +130,7 @@ asyncio.run(main())
127
130
 
128
131
  ### Complete V3 API
129
132
 
130
- V3 includes wallets, combos, rewards, credits, identities, markets, builders, profiles, perps, crypto, sports, backtesting, and other current product families. `execute()` gives you access to all 120 current V3 operations through one consistent Python interface.
133
+ V3 includes wallets, combos, rewards, credits, identities, markets, events, builders, profiles, perps, crypto, sports, backtesting, and other current product families. `execute()` gives you access to all 129 current V3 operations through one consistent Python interface.
131
134
 
132
135
  ```python
133
136
  import asyncio
@@ -135,7 +138,7 @@ from polynode import AsyncPolyNode
135
138
 
136
139
  async def read_v3(address: str):
137
140
  async with AsyncPolyNode(api_key="pn_live_...") as pn:
138
- print(len(pn.v3.operations)) # 120
141
+ print(len(pn.v3.operations)) # 128
139
142
 
140
143
  combo_activity = await pn.v3.execute(
141
144
  "GET /v3/combos/activity",
@@ -146,11 +149,76 @@ async def read_v3(address: str):
146
149
  path_params={"address": address},
147
150
  query={"limit": 100},
148
151
  )
149
- return combo_activity, wallet_rewards
152
+ scopes = await pn.v3.execute(
153
+ "GET /v3/leaderboard/scopes",
154
+ query={"period": "7d", "scope": "category"},
155
+ )
156
+ markets = await pn.v3.execute(
157
+ "GET /v3/markets",
158
+ query={"status": "open", "limit": 100},
159
+ )
160
+ return combo_activity, wallet_rewards, scopes, markets
150
161
 
151
162
  asyncio.run(read_v3("0xabc..."))
152
163
  ```
153
164
 
165
+ Use the typed Combo P&L helper for All Time realized, unrealized, and total
166
+ P&L, or realized P&L over a fixed or custom half-open window:
167
+
168
+ ```python
169
+ from polynode import PolyNodeV3
170
+
171
+ with PolyNodeV3("pn_live_...") as v3:
172
+ all_time = v3.combo_pnl_leaderboard(
173
+ period="all",
174
+ sort_by="total_pnl",
175
+ )
176
+ custom = v3.combo_pnl_leaderboard(
177
+ period="custom",
178
+ after=1_788_134_400,
179
+ before=1_788_220_800,
180
+ sort_by="realized_pnl",
181
+ )
182
+ ```
183
+
184
+ Wallets with incomplete arbitrary transfer basis or without a current mark are
185
+ omitted rather than returned with zero P&L. Inspect `coverage` on every response
186
+ before presenting the ranking.
187
+
188
+ Market and event catalogs use opaque cursor pagination. Reward-market list
189
+ rows include nullable title, slug, image, event, category, and tag fields.
190
+
191
+ Named perps helpers accept the returned opaque cursor:
192
+
193
+ ```python
194
+ from polynode import PolyNodeV3
195
+
196
+ with PolyNodeV3(api_key="pn_live_...") as v3:
197
+ first = v3.perps_trades(
198
+ "BTC-USD",
199
+ after=1_787_875_200,
200
+ before=1_787_961_600,
201
+ limit=100,
202
+ )
203
+ if first.get("next_cursor"):
204
+ second = v3.perps_trades(
205
+ "BTC-USD",
206
+ after=1_787_875_200,
207
+ before=1_787_961_600,
208
+ limit=100,
209
+ cursor=first["next_cursor"],
210
+ )
211
+
212
+ events = v3.perps_events(address=address, limit=100)
213
+ liquidations = v3.perps_liquidations(min_notional=10_000)
214
+ flows = v3.perps_wallet_flows(address, limit=100)
215
+ ```
216
+
217
+ Reuse the same route and filters on every cursor page. Wallet-flow totals and
218
+ counts remain fixed during one traversal. Cumulative volume delta remains a
219
+ bounded single-page series and has no cursor helper. Async clients expose the
220
+ same methods with `await`.
221
+
154
222
  The SDK encodes path parameters for you. Read requests that are safe to repeat retry transient failures and honor `Retry-After`; requests that change data are never retried automatically. JSON decimals decode as `Decimal`, and `ApiError` exposes the status, request ID, retry details, and a request URL with credentials removed.
155
223
 
156
224
  Current presets include `dome`, `fills`, `combos`, `redemptions`, and `deposits`. Current filters include `since()`, `combo_condition_ids()`, `leg_position_ids()`, `event_ids()`, `module_ids()`, `action()`, and `direction()`.
@@ -301,6 +369,43 @@ For the V2 order flow, required approvals, and common failure modes, see [docs.p
301
369
 
302
370
  V2 fees are determined at match time and are not signed into an order, so V2 payloads omit `feeRateBps`, `nonce`, and `taker`. Explicit legacy V1 mode still signs `feeRateBps`; for that path the SDK fetches `/fee-rate` and fails closed if fee, tick-size, or neg-risk metadata is unavailable or malformed.
303
371
 
372
+ #### Polymarket Protocol V2 markets
373
+
374
+ > **Two different "V2"s.** Polymarket Protocol V2 (October 2026) is Polymarket's new position system: positions live in a new ledger (PositionManager), every market type trades on one new exchange (ExchangeV3, order signing domain version `"3"`), and split, merge and redeem go through a Router. It is **not** the April 2026 upgrade (CTF Exchange V2 with pUSD collateral) that `ExchangeVersion.V2` and the rest of this README call "V2". Current markets keep using the April exchanges.
375
+
376
+ **When.** Polymarket's test markets for Protocol V2 trade from 2026-10-05 to 2026-10-30. New markets move to Protocol V2 from around 2026-11-02 (tentative, per Polymarket). Existing markets stay where they are.
377
+
378
+ **What you need to do.**
379
+
380
+ - Streaming and REST only: nothing. Same methods, same response shapes. Rows and events for Protocol V2 markets carry a few optional fields: `protocol_version: "v2"`, `module` (`"binary"` or `"neg_risk"`), `exchange_version: "v3"` on fills, and `payouts_ppm` / `derived` on oracle events. A row without them is a current market.
381
+ - Trading: upgrade to `polynode` 0.15.0 before 2026-11-02 and place orders exactly as before. Older versions keep working on current markets, but Polymarket rejects their orders on Protocol V2 markets (no funds move).
382
+ - Storing ids yourself: keep outcome ids as strings (up to 78 digits). Condition ids are returned in the 66-character form; the 64-character form is accepted as input.
383
+
384
+ The first order on a Protocol V2 market grants the two approvals it needs (pUSD and PositionManager, both for ExchangeV3): one gasless batch for Safe and deposit wallets, or two transactions with a little POL gas for an EOA. To do it ahead of time, or for browser-signed user-owned orders, call `ensure_ready` once with `protocol_v2=True`. Plain `ensure_ready()` behaves exactly as before.
385
+
386
+ ```python
387
+ from polynode.trading import MergeParams, RedeemParams, SplitParams
388
+
389
+ await trader.ensure_ready("0xYourPrivateKey...", protocol_v2=True) # optional
390
+ version = await trader.get_market_protocol(token_id) # "v1" (current) or "v2"
391
+
392
+ # Same call for every market; the SDK picks the exchange and signing domain.
393
+ await trader.order(OrderParams(token_id=token_id, side="BUY", price=0.5, size=10))
394
+
395
+ # Position operations (user-owned execution) route through the Router on V2 markets.
396
+ await trader.execute_split(SplitParams(condition_id=condition_id, amount=10))
397
+ await trader.execute_merge(MergeParams(condition_id=condition_id, amount=10))
398
+ await trader.execute_redeem(RedeemParams(condition_id=condition_id, outcome_index=0)) # 0 = YES, 1 = NO
399
+ payout = await trader.preview_payout(position_id, 10) # None until resolved
400
+
401
+ status = await trader.check_approvals(protocol_v2=True)
402
+ print(status.protocol_v2_ready, status.position_ops_v2_ready, status.auto_redeem_enabled)
403
+ ```
404
+
405
+ The SDK learns a market's protocol from Polymarket's market data and caches it for 24 hours in a new local table (existing tables are unchanged). It never guesses from the shape of an id: if the protocol cannot be learned, the order is refused with a retryable `MarketProtocolError` instead of being signed for the wrong exchange. Pass `protocol_version="v2"` (or call `trader.hint_market_protocol(...)`) to skip the lookup; a value that contradicts what the SDK already knows is refused. Short-form streams pick `positionIds` for Protocol V2 markets and expose `market.protocol_version`, so orders on discovered markets skip the lookup.
406
+
407
+ Results of Protocol V2 markets are shares of 1,000,000 (`[1000000, 0]` = YES won, `[500000, 500000]` = 50/50); `payouts` carries the same result in smallest whole numbers, like current markets. `RedemptionWatcher` handles both. Automatic redemption by Polymarket's AutoRedeemer is opt-in only (`enable_auto_redeem()` / `disable_auto_redeem()`); proceeds always return to your wallet. Converting neg-risk positions is not supported for Protocol V2 markets yet.
408
+
304
409
  #### Optional user-owned execution
305
410
 
306
411
  Set `execution_mode=ExecutionMode.USER_OWNED` when the signing wallet should trade with zero builder attribution and use its own gasless authorization. Builder mode remains the default and existing integrations are unchanged.
@@ -2,7 +2,11 @@
2
2
 
3
3
  Python SDK for the [Polynode](https://polynode.dev) real-time prediction market data platform.
4
4
 
5
- **New in v0.14.1:** Every prepared user-owned order exposes its canonical V2 exchange `order_hash` before submission. This is the exact CLOB/open-order ID and fill `order_hash`; deposit-wallet orders hash the standard inner `Order`, not the POLY_1271 wallet-signing wrapper. Record it before the network attempt so timeouts can be reconciled without risking a duplicate.
5
+ **New in v0.15.1:** Fixes `wrap_to_polyusd()` and `unwrap_from_polyusd()` for EOA wallets on Polygon. With web3 7 or 8 they failed before sending anything, with an `ExtraDataLengthError` about a POA chain. They now set gas and gas price themselves, wait for the approval to confirm before sending the wrap or unwrap, and return the transaction hash with a `0x` prefix. `set_approvals()` gets the same gas price fix. `ensure_ready()` results now include `approval_tx_hashes`: the hashes of any approval transactions that call sent, or an empty list when none were needed.
6
+
7
+ **In v0.15.0:** Polymarket Protocol V2 markets (October 2026). Upgrade before 2026-11-02 if you trade: `order()` looks up each market's protocol and signs Protocol V2 orders for the right exchange automatically, and split, merge and the new redeem work on them too. Nothing changes for current markets or for streaming and REST users. This is not the April 2026 "V2" (CTF Exchange V2 and pUSD) described elsewhere in this README; see [Polymarket Protocol V2 markets](#polymarket-protocol-v2-markets).
8
+
9
+ **In v0.14.1:** Every prepared user-owned order exposes its canonical V2 exchange `order_hash` before submission. This is the exact CLOB/open-order ID and fill `order_hash`; deposit-wallet orders hash the standard inner `Order`, not the POLY_1271 wallet-signing wrapper. Record it before the network attempt so timeouts can be reconciled without risking a duplicate.
6
10
 
7
11
  **In v0.14.0:** Web platforms can take a user from wallet authorization through an exact browser-signed user-owned order without exposing backend credentials. The SDK imports the shared versioned browser bundle into memory, produces a credential-free signing request with a complete order preview, supports one-time multi-worker state, validates canonical signatures and wallet identity, checks BUY collateral before prompting, and submits with exact zero builder attribution.
8
12
 
@@ -38,7 +42,6 @@ with PolyNode(api_key="pn_live_...") as pn:
38
42
  status = pn.status()
39
43
  connections = pn.connections()
40
44
  markets = pn.markets(count=10)
41
- settlements = pn.recent_settlements(count=5)
42
45
  wallet_positions = pn.wallet_positions(
43
46
  address, redeemable=True, condition_id=condition_id
44
47
  )
@@ -93,7 +96,7 @@ asyncio.run(main())
93
96
 
94
97
  ### Complete V3 API
95
98
 
96
- V3 includes wallets, combos, rewards, credits, identities, markets, builders, profiles, perps, crypto, sports, backtesting, and other current product families. `execute()` gives you access to all 120 current V3 operations through one consistent Python interface.
99
+ V3 includes wallets, combos, rewards, credits, identities, markets, events, builders, profiles, perps, crypto, sports, backtesting, and other current product families. `execute()` gives you access to all 129 current V3 operations through one consistent Python interface.
97
100
 
98
101
  ```python
99
102
  import asyncio
@@ -101,7 +104,7 @@ from polynode import AsyncPolyNode
101
104
 
102
105
  async def read_v3(address: str):
103
106
  async with AsyncPolyNode(api_key="pn_live_...") as pn:
104
- print(len(pn.v3.operations)) # 120
107
+ print(len(pn.v3.operations)) # 128
105
108
 
106
109
  combo_activity = await pn.v3.execute(
107
110
  "GET /v3/combos/activity",
@@ -112,11 +115,76 @@ async def read_v3(address: str):
112
115
  path_params={"address": address},
113
116
  query={"limit": 100},
114
117
  )
115
- return combo_activity, wallet_rewards
118
+ scopes = await pn.v3.execute(
119
+ "GET /v3/leaderboard/scopes",
120
+ query={"period": "7d", "scope": "category"},
121
+ )
122
+ markets = await pn.v3.execute(
123
+ "GET /v3/markets",
124
+ query={"status": "open", "limit": 100},
125
+ )
126
+ return combo_activity, wallet_rewards, scopes, markets
116
127
 
117
128
  asyncio.run(read_v3("0xabc..."))
118
129
  ```
119
130
 
131
+ Use the typed Combo P&L helper for All Time realized, unrealized, and total
132
+ P&L, or realized P&L over a fixed or custom half-open window:
133
+
134
+ ```python
135
+ from polynode import PolyNodeV3
136
+
137
+ with PolyNodeV3("pn_live_...") as v3:
138
+ all_time = v3.combo_pnl_leaderboard(
139
+ period="all",
140
+ sort_by="total_pnl",
141
+ )
142
+ custom = v3.combo_pnl_leaderboard(
143
+ period="custom",
144
+ after=1_788_134_400,
145
+ before=1_788_220_800,
146
+ sort_by="realized_pnl",
147
+ )
148
+ ```
149
+
150
+ Wallets with incomplete arbitrary transfer basis or without a current mark are
151
+ omitted rather than returned with zero P&L. Inspect `coverage` on every response
152
+ before presenting the ranking.
153
+
154
+ Market and event catalogs use opaque cursor pagination. Reward-market list
155
+ rows include nullable title, slug, image, event, category, and tag fields.
156
+
157
+ Named perps helpers accept the returned opaque cursor:
158
+
159
+ ```python
160
+ from polynode import PolyNodeV3
161
+
162
+ with PolyNodeV3(api_key="pn_live_...") as v3:
163
+ first = v3.perps_trades(
164
+ "BTC-USD",
165
+ after=1_787_875_200,
166
+ before=1_787_961_600,
167
+ limit=100,
168
+ )
169
+ if first.get("next_cursor"):
170
+ second = v3.perps_trades(
171
+ "BTC-USD",
172
+ after=1_787_875_200,
173
+ before=1_787_961_600,
174
+ limit=100,
175
+ cursor=first["next_cursor"],
176
+ )
177
+
178
+ events = v3.perps_events(address=address, limit=100)
179
+ liquidations = v3.perps_liquidations(min_notional=10_000)
180
+ flows = v3.perps_wallet_flows(address, limit=100)
181
+ ```
182
+
183
+ Reuse the same route and filters on every cursor page. Wallet-flow totals and
184
+ counts remain fixed during one traversal. Cumulative volume delta remains a
185
+ bounded single-page series and has no cursor helper. Async clients expose the
186
+ same methods with `await`.
187
+
120
188
  The SDK encodes path parameters for you. Read requests that are safe to repeat retry transient failures and honor `Retry-After`; requests that change data are never retried automatically. JSON decimals decode as `Decimal`, and `ApiError` exposes the status, request ID, retry details, and a request URL with credentials removed.
121
189
 
122
190
  Current presets include `dome`, `fills`, `combos`, `redemptions`, and `deposits`. Current filters include `since()`, `combo_condition_ids()`, `leg_position_ids()`, `event_ids()`, `module_ids()`, `action()`, and `direction()`.
@@ -267,6 +335,43 @@ For the V2 order flow, required approvals, and common failure modes, see [docs.p
267
335
 
268
336
  V2 fees are determined at match time and are not signed into an order, so V2 payloads omit `feeRateBps`, `nonce`, and `taker`. Explicit legacy V1 mode still signs `feeRateBps`; for that path the SDK fetches `/fee-rate` and fails closed if fee, tick-size, or neg-risk metadata is unavailable or malformed.
269
337
 
338
+ #### Polymarket Protocol V2 markets
339
+
340
+ > **Two different "V2"s.** Polymarket Protocol V2 (October 2026) is Polymarket's new position system: positions live in a new ledger (PositionManager), every market type trades on one new exchange (ExchangeV3, order signing domain version `"3"`), and split, merge and redeem go through a Router. It is **not** the April 2026 upgrade (CTF Exchange V2 with pUSD collateral) that `ExchangeVersion.V2` and the rest of this README call "V2". Current markets keep using the April exchanges.
341
+
342
+ **When.** Polymarket's test markets for Protocol V2 trade from 2026-10-05 to 2026-10-30. New markets move to Protocol V2 from around 2026-11-02 (tentative, per Polymarket). Existing markets stay where they are.
343
+
344
+ **What you need to do.**
345
+
346
+ - Streaming and REST only: nothing. Same methods, same response shapes. Rows and events for Protocol V2 markets carry a few optional fields: `protocol_version: "v2"`, `module` (`"binary"` or `"neg_risk"`), `exchange_version: "v3"` on fills, and `payouts_ppm` / `derived` on oracle events. A row without them is a current market.
347
+ - Trading: upgrade to `polynode` 0.15.0 before 2026-11-02 and place orders exactly as before. Older versions keep working on current markets, but Polymarket rejects their orders on Protocol V2 markets (no funds move).
348
+ - Storing ids yourself: keep outcome ids as strings (up to 78 digits). Condition ids are returned in the 66-character form; the 64-character form is accepted as input.
349
+
350
+ The first order on a Protocol V2 market grants the two approvals it needs (pUSD and PositionManager, both for ExchangeV3): one gasless batch for Safe and deposit wallets, or two transactions with a little POL gas for an EOA. To do it ahead of time, or for browser-signed user-owned orders, call `ensure_ready` once with `protocol_v2=True`. Plain `ensure_ready()` behaves exactly as before.
351
+
352
+ ```python
353
+ from polynode.trading import MergeParams, RedeemParams, SplitParams
354
+
355
+ await trader.ensure_ready("0xYourPrivateKey...", protocol_v2=True) # optional
356
+ version = await trader.get_market_protocol(token_id) # "v1" (current) or "v2"
357
+
358
+ # Same call for every market; the SDK picks the exchange and signing domain.
359
+ await trader.order(OrderParams(token_id=token_id, side="BUY", price=0.5, size=10))
360
+
361
+ # Position operations (user-owned execution) route through the Router on V2 markets.
362
+ await trader.execute_split(SplitParams(condition_id=condition_id, amount=10))
363
+ await trader.execute_merge(MergeParams(condition_id=condition_id, amount=10))
364
+ await trader.execute_redeem(RedeemParams(condition_id=condition_id, outcome_index=0)) # 0 = YES, 1 = NO
365
+ payout = await trader.preview_payout(position_id, 10) # None until resolved
366
+
367
+ status = await trader.check_approvals(protocol_v2=True)
368
+ print(status.protocol_v2_ready, status.position_ops_v2_ready, status.auto_redeem_enabled)
369
+ ```
370
+
371
+ The SDK learns a market's protocol from Polymarket's market data and caches it for 24 hours in a new local table (existing tables are unchanged). It never guesses from the shape of an id: if the protocol cannot be learned, the order is refused with a retryable `MarketProtocolError` instead of being signed for the wrong exchange. Pass `protocol_version="v2"` (or call `trader.hint_market_protocol(...)`) to skip the lookup; a value that contradicts what the SDK already knows is refused. Short-form streams pick `positionIds` for Protocol V2 markets and expose `market.protocol_version`, so orders on discovered markets skip the lookup.
372
+
373
+ Results of Protocol V2 markets are shares of 1,000,000 (`[1000000, 0]` = YES won, `[500000, 500000]` = 50/50); `payouts` carries the same result in smallest whole numbers, like current markets. `RedemptionWatcher` handles both. Automatic redemption by Polymarket's AutoRedeemer is opt-in only (`enable_auto_redeem()` / `disable_auto_redeem()`); proceeds always return to your wallet. Converting neg-risk positions is not supported for Protocol V2 markets yet.
374
+
270
375
  #### Optional user-owned execution
271
376
 
272
377
  Set `execution_mode=ExecutionMode.USER_OWNED` when the signing wallet should trade with zero builder attribution and use its own gasless authorization. Builder mode remains the default and existing integrations are unchanged.
@@ -237,7 +237,7 @@
237
237
  "never_drop_silently": true
238
238
  }
239
239
  },
240
- "operation_count": 120,
240
+ "operation_count": 129,
241
241
  "operations": [
242
242
  {
243
243
  "method": "POST",
@@ -371,12 +371,48 @@
371
371
  "domain": "combos",
372
372
  "retry": "safe_read"
373
373
  },
374
+ {
375
+ "method": "GET",
376
+ "path": "/v3/combos/activity/daily",
377
+ "domain": "combos",
378
+ "retry": "safe_read"
379
+ },
380
+ {
381
+ "method": "GET",
382
+ "path": "/v3/combos/leaderboard",
383
+ "domain": "combos",
384
+ "retry": "safe_read"
385
+ },
374
386
  {
375
387
  "method": "GET",
376
388
  "path": "/v3/combos/redemptions",
377
389
  "domain": "combos",
378
390
  "retry": "safe_read"
379
391
  },
392
+ {
393
+ "method": "GET",
394
+ "path": "/v3/markets",
395
+ "domain": "markets",
396
+ "retry": "safe_read"
397
+ },
398
+ {
399
+ "method": "GET",
400
+ "path": "/v3/markets/stats",
401
+ "domain": "markets",
402
+ "retry": "safe_read"
403
+ },
404
+ {
405
+ "method": "GET",
406
+ "path": "/v3/events",
407
+ "domain": "events",
408
+ "retry": "safe_read"
409
+ },
410
+ {
411
+ "method": "GET",
412
+ "path": "/v3/events/stats",
413
+ "domain": "events",
414
+ "retry": "safe_read"
415
+ },
380
416
  {
381
417
  "method": "GET",
382
418
  "path": "/v3/markets/search",
@@ -485,6 +521,12 @@
485
521
  "domain": "global",
486
522
  "retry": "safe_read"
487
523
  },
524
+ {
525
+ "method": "GET",
526
+ "path": "/v3/leaderboard/scopes",
527
+ "domain": "global",
528
+ "retry": "safe_read"
529
+ },
488
530
  {
489
531
  "method": "GET",
490
532
  "path": "/v3/builders",
@@ -497,6 +539,12 @@
497
539
  "domain": "builders",
498
540
  "retry": "safe_read"
499
541
  },
542
+ {
543
+ "method": "GET",
544
+ "path": "/v3/builders/{code}/leaderboard",
545
+ "domain": "builders",
546
+ "retry": "safe_read"
547
+ },
500
548
  {
501
549
  "method": "GET",
502
550
  "path": "/v3/builders/{code}/trades",
@@ -887,6 +935,12 @@
887
935
  "domain": "perps",
888
936
  "retry": "safe_read"
889
937
  },
938
+ {
939
+ "method": "GET",
940
+ "path": "/v3/perps/activity/daily",
941
+ "domain": "perps",
942
+ "retry": "safe_read"
943
+ },
890
944
  {
891
945
  "method": "GET",
892
946
  "path": "/v3/perps/funding-pnl/{instrument}",
@@ -0,0 +1,161 @@
1
+ """PolyNode Python SDK — real-time prediction market data and trading."""
2
+
3
+ from ._version import __version__
4
+ from .client import AsyncPolyNode, PolyNode
5
+ from .engine import EngineView, OrderbookEngine
6
+ from .errors import ApiError, PolyNodeError, WsError
7
+ from .market_protocol import (
8
+ MarketProtocolError,
9
+ MarketProtocolInfo,
10
+ MarketProtocolResolver,
11
+ ProtocolVersion,
12
+ get_market_protocol,
13
+ normalize_condition_id,
14
+ protocol_v2_position_ids,
15
+ )
16
+ from .orderbook import OrderbookWS
17
+ from .orderbook_state import LocalOrderbook
18
+ from .perps import PerpsChannels, PerpsWS, perps_channels, perps_websocket_url
19
+ from .types.perps import (
20
+ PerpsConnected,
21
+ PerpsDecodeError,
22
+ PerpsEvent,
23
+ PerpsLagWarning,
24
+ PerpsMessage,
25
+ PerpsPositionEvent,
26
+ PerpsPositionEventsResponse,
27
+ PerpsPositionEventType,
28
+ PerpsPong,
29
+ PerpsQueueOverflow,
30
+ PerpsReconnectNotice,
31
+ PerpsRejectedChannel,
32
+ PerpsServerError,
33
+ PerpsSubscribed,
34
+ PerpsRestTrade,
35
+ PerpsTradesResponse,
36
+ PerpsUnknownMessage,
37
+ PerpsUnsubscribed,
38
+ PerpsWalletFlowEvent,
39
+ PerpsWalletFlowsResponse,
40
+ )
41
+ from .types.v3 import (
42
+ BuilderTraderLeaderboardCoverage,
43
+ BuilderTraderLeaderboardPeriod,
44
+ BuilderTraderLeaderboardResponse,
45
+ BuilderTraderLeaderboardRow,
46
+ BuilderTraderLeaderboardSort,
47
+ BuilderTraderLeaderboardWindow,
48
+ ComboActivityCountSemantics,
49
+ ComboActivityDailyResponse,
50
+ ComboActivityDay,
51
+ ComboPnlLeaderboardCoverage,
52
+ ComboPnlLeaderboardPeriod,
53
+ ComboPnlLeaderboardResponse,
54
+ ComboPnlLeaderboardRow,
55
+ ComboPnlLeaderboardSort,
56
+ ComboPnlLeaderboardWindow,
57
+ ComboPnlScope,
58
+ DailyActivityPeriod,
59
+ PerpsActivityDailyResponse,
60
+ PerpsActivityDay,
61
+ PerpsActivityTotal,
62
+ PerpsInstrumentActivityDay,
63
+ PerpsInstrumentActivitySeries,
64
+ ProtocolV2RowFields,
65
+ )
66
+ from .redemption_watcher import RedeemableAlert, RedemptionWatcher, TrackedPosition
67
+ from .short_form import ShortFormStream, short_form_chainlink_feed, short_form_twap_window
68
+ from .subscription import Subscription, SubscriptionBuilder
69
+ from .testing import get_active_test_wallet, get_active_test_wallets
70
+ from .v3 import AsyncPolyNodeV3, PolyNodeV3
71
+ from .v3_operations import V3_OPERATIONS, V3_OPERATION_COUNT, V3Operation
72
+ from .ws import PolyNodeWS
73
+
74
+ __all__ = [
75
+ "MarketProtocolError",
76
+ "MarketProtocolInfo",
77
+ "MarketProtocolResolver",
78
+ "ProtocolVersion",
79
+ "ProtocolV2RowFields",
80
+ "get_market_protocol",
81
+ "normalize_condition_id",
82
+ "protocol_v2_position_ids",
83
+ "__version__",
84
+ # Clients
85
+ "PolyNode",
86
+ "AsyncPolyNode",
87
+ "PolyNodeV3",
88
+ "AsyncPolyNodeV3",
89
+ "PerpsWS",
90
+ "PerpsChannels",
91
+ "perps_channels",
92
+ "perps_websocket_url",
93
+ "PerpsConnected",
94
+ "PerpsRejectedChannel",
95
+ "PerpsSubscribed",
96
+ "PerpsUnsubscribed",
97
+ "PerpsPong",
98
+ "PerpsEvent",
99
+ "PerpsLagWarning",
100
+ "PerpsServerError",
101
+ "PerpsReconnectNotice",
102
+ "PerpsDecodeError",
103
+ "PerpsUnknownMessage",
104
+ "PerpsQueueOverflow",
105
+ "PerpsMessage",
106
+ "PerpsPositionEventType",
107
+ "PerpsRestTrade",
108
+ "PerpsTradesResponse",
109
+ "PerpsPositionEvent",
110
+ "PerpsPositionEventsResponse",
111
+ "PerpsWalletFlowEvent",
112
+ "PerpsWalletFlowsResponse",
113
+ "BuilderTraderLeaderboardPeriod",
114
+ "BuilderTraderLeaderboardSort",
115
+ "BuilderTraderLeaderboardRow",
116
+ "BuilderTraderLeaderboardWindow",
117
+ "BuilderTraderLeaderboardCoverage",
118
+ "BuilderTraderLeaderboardResponse",
119
+ "ComboPnlLeaderboardPeriod",
120
+ "ComboPnlLeaderboardSort",
121
+ "ComboPnlScope",
122
+ "ComboPnlLeaderboardWindow",
123
+ "ComboPnlLeaderboardCoverage",
124
+ "ComboPnlLeaderboardRow",
125
+ "ComboPnlLeaderboardResponse",
126
+ "DailyActivityPeriod",
127
+ "ComboActivityDay",
128
+ "ComboActivityCountSemantics",
129
+ "ComboActivityDailyResponse",
130
+ "PerpsActivityDay",
131
+ "PerpsInstrumentActivityDay",
132
+ "PerpsInstrumentActivitySeries",
133
+ "PerpsActivityTotal",
134
+ "PerpsActivityDailyResponse",
135
+ "V3Operation",
136
+ "V3_OPERATIONS",
137
+ "V3_OPERATION_COUNT",
138
+ # WebSocket
139
+ "PolyNodeWS",
140
+ "SubscriptionBuilder",
141
+ "Subscription",
142
+ # Orderbook
143
+ "OrderbookWS",
144
+ "LocalOrderbook",
145
+ "OrderbookEngine",
146
+ "EngineView",
147
+ # Streams
148
+ "ShortFormStream",
149
+ "short_form_chainlink_feed",
150
+ "short_form_twap_window",
151
+ "RedemptionWatcher",
152
+ "RedeemableAlert",
153
+ "TrackedPosition",
154
+ # Testing
155
+ "get_active_test_wallet",
156
+ "get_active_test_wallets",
157
+ # Errors
158
+ "PolyNodeError",
159
+ "ApiError",
160
+ "WsError",
161
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.15.1"