hyperquant 1.47__tar.gz → 1.48__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.
- {hyperquant-1.47 → hyperquant-1.48}/PKG-INFO +1 -1
- {hyperquant-1.47 → hyperquant-1.48}/pyproject.toml +2 -2
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/polymarket.py +142 -15
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/polymarket.py +121 -28
- {hyperquant-1.47 → hyperquant-1.48}/uv.lock +2 -593
- {hyperquant-1.47 → hyperquant-1.48}/.gitignore +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/README.md +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/requirements-dev.lock +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/requirements.lock +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/auth.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/lib/polymarket/ctfAbi.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/lib/polymarket/safeAbi.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/core.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/db.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.47 → hyperquant-1.48}/src/hyperquant/notikit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperquant
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.48
|
|
4
4
|
Summary: A minimal yet hyper-efficient backtesting framework for quantitative trading
|
|
5
5
|
Project-URL: Homepage, https://github.com/yourusername/hyperquant
|
|
6
6
|
Project-URL: Issues, https://github.com/yourusername/hyperquant/issues
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hyperquant"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.48"
|
|
4
4
|
description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "MissinA", email = "1421329142@qq.com" }
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
"eth-account>=0.10.0",
|
|
18
18
|
"web3>=7.14.0",
|
|
19
19
|
"python-dotenv>=1.2.1",
|
|
20
|
-
"coincurve>=21.0.0"
|
|
20
|
+
"coincurve>=21.0.0"
|
|
21
21
|
]
|
|
22
22
|
readme = "README.md"
|
|
23
23
|
requires-python = ">=3.13"
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from heapq import heappop, heappush
|
|
6
|
+
import time
|
|
6
7
|
from typing import TYPE_CHECKING, Any, Iterable
|
|
7
8
|
|
|
8
9
|
from pybotters.store import DataStore, DataStoreCollection
|
|
@@ -15,11 +16,37 @@ if TYPE_CHECKING:
|
|
|
15
16
|
class Position(DataStore):
|
|
16
17
|
"""Position DataStore keyed by Polymarket token id."""
|
|
17
18
|
|
|
18
|
-
_KEYS = ["asset"
|
|
19
|
+
_KEYS = ["asset"]
|
|
19
20
|
|
|
20
|
-
def
|
|
21
|
+
def _init(self) -> None:
|
|
22
|
+
# 缓存LIVE订单已计入的size_matched: {order_id: size_matched}
|
|
23
|
+
self._live_cache: dict[str, float] = {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sorted(
|
|
27
|
+
self, query: Item | None = None, limit: int | None = None
|
|
28
|
+
) -> dict[str, list[Item]]:
|
|
29
|
+
"""按ts降序排列,按outcome分组"""
|
|
30
|
+
if query is None:
|
|
31
|
+
query = {}
|
|
32
|
+
result: dict[str, list[Item]] = {}
|
|
33
|
+
for item in self:
|
|
34
|
+
if all(k in item and query[k] == item[k] for k in query):
|
|
35
|
+
outcome = item.get("outcome") or "unknown"
|
|
36
|
+
if outcome not in result:
|
|
37
|
+
result[outcome] = []
|
|
38
|
+
result[outcome].append(item)
|
|
39
|
+
for outcome in result:
|
|
40
|
+
result[outcome].sort(key=lambda x: (x.get("eventSlug") or '0'), reverse=True)
|
|
41
|
+
if limit:
|
|
42
|
+
result[outcome] = result[outcome][:limit]
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
def _on_response(self, msg: list[Item]) -> None:
|
|
21
46
|
if msg:
|
|
22
47
|
self._clear()
|
|
48
|
+
for rec in msg:
|
|
49
|
+
rec["ts"] = 0
|
|
23
50
|
self._update(msg)
|
|
24
51
|
|
|
25
52
|
def on_trade(self, trade: Item) -> None:
|
|
@@ -33,6 +60,7 @@ class Position(DataStore):
|
|
|
33
60
|
size_raw = trade.get("size")
|
|
34
61
|
price_raw = trade.get("price")
|
|
35
62
|
|
|
63
|
+
|
|
36
64
|
if not asset_id or not outcome or side not in {"BUY", "SELL"}:
|
|
37
65
|
return
|
|
38
66
|
|
|
@@ -45,6 +73,8 @@ class Position(DataStore):
|
|
|
45
73
|
except (TypeError, ValueError):
|
|
46
74
|
price = None
|
|
47
75
|
|
|
76
|
+
|
|
77
|
+
|
|
48
78
|
key = {"asset": asset_id, "outcome": outcome}
|
|
49
79
|
existing = self.get(key) or {}
|
|
50
80
|
|
|
@@ -86,6 +116,85 @@ class Position(DataStore):
|
|
|
86
116
|
else:
|
|
87
117
|
self._insert([rec])
|
|
88
118
|
|
|
119
|
+
def _on_order(self, order: dict[str, Any]) -> None:
|
|
120
|
+
"""通过order更新持仓,处理LIVE时部分成交的增量统计"""
|
|
121
|
+
# print(order)
|
|
122
|
+
# order写入本地尝试后续分析
|
|
123
|
+
with open("polymarket_orders.log", "a") as f:
|
|
124
|
+
f.write(json.dumps(order) + "\n")
|
|
125
|
+
order_id = order.get("id")
|
|
126
|
+
asset_id = order.get("asset_id")
|
|
127
|
+
outcome = order.get("outcome")
|
|
128
|
+
side = str(order.get("side") or "").upper()
|
|
129
|
+
size_matched = float(order.get("size_matched") or 0)
|
|
130
|
+
price = float(order.get("price") or 0)
|
|
131
|
+
status = str(order.get("status") or "").upper()
|
|
132
|
+
|
|
133
|
+
if not order_id or not asset_id or not outcome or side not in {"BUY", "SELL"}:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
cached = self._live_cache.get(order_id, 0.0)
|
|
137
|
+
|
|
138
|
+
if status == "LIVE":
|
|
139
|
+
# LIVE时计算增量
|
|
140
|
+
delta = size_matched - cached
|
|
141
|
+
if delta > 0:
|
|
142
|
+
self._live_cache[order_id] = size_matched
|
|
143
|
+
self._apply_trade(asset_id, outcome, side, delta, price)
|
|
144
|
+
elif status in {"CANCELED", "MATCHED"}:
|
|
145
|
+
# 订单完结:计算最终增量 = 最终size_matched - 已计入的cached
|
|
146
|
+
delta = size_matched - cached
|
|
147
|
+
if delta > 0:
|
|
148
|
+
self._apply_trade(asset_id, outcome, side, delta, price)
|
|
149
|
+
# 清理缓存
|
|
150
|
+
self._live_cache.pop(order_id, None)
|
|
151
|
+
|
|
152
|
+
def _apply_trade(self, asset_id: str, outcome: str, side: str, size: float, price: float) -> None:
|
|
153
|
+
"""应用成交到持仓"""
|
|
154
|
+
if size <= 0:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
key = {"asset": asset_id, "outcome": outcome}
|
|
158
|
+
existing = self.get(key) or {}
|
|
159
|
+
|
|
160
|
+
cur_size = float(existing.get("size") or 0.0)
|
|
161
|
+
cur_total_bought = float(existing.get("totalBought") or 0.0)
|
|
162
|
+
cur_avg_price = float(existing.get("avgPrice") or 0.0)
|
|
163
|
+
cur_cost = cur_size * cur_avg_price
|
|
164
|
+
|
|
165
|
+
if side == "BUY":
|
|
166
|
+
new_size = cur_size + size
|
|
167
|
+
total_bought = cur_total_bought + size
|
|
168
|
+
effective_price = price if price else cur_avg_price
|
|
169
|
+
new_cost = cur_cost + size * effective_price
|
|
170
|
+
else: # SELL
|
|
171
|
+
new_size = cur_size - size
|
|
172
|
+
total_bought = cur_total_bought
|
|
173
|
+
new_cost = cur_cost - min(size, cur_size) * cur_avg_price
|
|
174
|
+
|
|
175
|
+
if new_size <= 0:
|
|
176
|
+
new_size = 0.0
|
|
177
|
+
avg_price = 0.0
|
|
178
|
+
new_cost = 0.0
|
|
179
|
+
else:
|
|
180
|
+
avg_price = max(new_cost, 0.0) / new_size
|
|
181
|
+
|
|
182
|
+
rec: dict[str, Any] = {
|
|
183
|
+
"asset": asset_id,
|
|
184
|
+
"outcome": outcome,
|
|
185
|
+
"side": side,
|
|
186
|
+
"size": new_size,
|
|
187
|
+
"totalBought": total_bought,
|
|
188
|
+
"avgPrice": avg_price,
|
|
189
|
+
"ts": int(time.time() * 1000),
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if existing:
|
|
193
|
+
self._update([rec])
|
|
194
|
+
else:
|
|
195
|
+
self._insert([rec])
|
|
196
|
+
|
|
197
|
+
|
|
89
198
|
|
|
90
199
|
class Fill(DataStore):
|
|
91
200
|
"""Fill records keyed by maker order id."""
|
|
@@ -169,6 +278,7 @@ class Order(DataStore):
|
|
|
169
278
|
return normalized
|
|
170
279
|
|
|
171
280
|
def _on_response(self, items: list[dict[str, Any]] | dict[str, Any]) -> None:
|
|
281
|
+
"""增量同步:insert新增、update变更、delete消失的订单"""
|
|
172
282
|
rows: list[dict[str, Any]] = []
|
|
173
283
|
if isinstance(items, dict):
|
|
174
284
|
items = [items]
|
|
@@ -176,20 +286,34 @@ class Order(DataStore):
|
|
|
176
286
|
norm = self._normalize(it)
|
|
177
287
|
if norm:
|
|
178
288
|
rows.append(norm)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
289
|
+
|
|
290
|
+
# 构建新订单id集合
|
|
291
|
+
new_ids = {r["id"] for r in rows}
|
|
292
|
+
|
|
293
|
+
# 删除不再存在的订单(传入完整状态)
|
|
294
|
+
to_delete = [dict(item) for item in self if item["id"] not in new_ids]
|
|
295
|
+
if to_delete:
|
|
296
|
+
self._delete(to_delete)
|
|
297
|
+
|
|
298
|
+
# 插入或更新
|
|
299
|
+
for row in rows:
|
|
300
|
+
existing = self.get({"id": row["id"]})
|
|
301
|
+
if existing:
|
|
302
|
+
# 有变化才update
|
|
303
|
+
if any(existing.get(k) != row.get(k) for k in row):
|
|
304
|
+
self._update([row])
|
|
305
|
+
else:
|
|
306
|
+
self._insert([row])
|
|
182
307
|
|
|
183
308
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
self._update([norm])
|
|
190
|
-
else:
|
|
191
|
-
self._insert([norm])
|
|
309
|
+
status = str(msg.get("status") or "").upper()
|
|
310
|
+
# CANCELED MATCHED 删除
|
|
311
|
+
order = self.get({"id": msg.get("id")})
|
|
312
|
+
if not order:
|
|
313
|
+
self._insert([msg])
|
|
192
314
|
|
|
315
|
+
if status in {"CANCELED", "MATCHED"}:
|
|
316
|
+
self._delete([msg])
|
|
193
317
|
|
|
194
318
|
class MyTrade(DataStore):
|
|
195
319
|
"""User trades keyed by trade id."""
|
|
@@ -795,6 +919,7 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
795
919
|
"order_type": "GTC",
|
|
796
920
|
"created_at": 1762912331
|
|
797
921
|
}
|
|
922
|
+
|
|
798
923
|
"""
|
|
799
924
|
|
|
800
925
|
return self._get("order")
|
|
@@ -908,10 +1033,12 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
908
1033
|
self.book._on_message(m)
|
|
909
1034
|
elif msg_type == "order":
|
|
910
1035
|
self.orders._on_message(m)
|
|
1036
|
+
self.position._on_order(m)
|
|
1037
|
+
|
|
911
1038
|
elif msg_type == "trade":
|
|
912
1039
|
self.mytrade._on_message(m)
|
|
913
|
-
self.fill._on_trade(m)
|
|
914
|
-
self.position.on_trade(m)
|
|
1040
|
+
# self.fill._on_trade(m)
|
|
1041
|
+
# self.position.on_trade(m)
|
|
915
1042
|
elif msg_type == 'orders_matched':
|
|
916
1043
|
payload = m.get("payload") or {}
|
|
917
1044
|
if not payload:
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import logging
|
|
4
5
|
from contextlib import suppress
|
|
5
6
|
from datetime import UTC, datetime, timedelta
|
|
6
7
|
from functools import lru_cache
|
|
7
8
|
import os
|
|
9
|
+
import time
|
|
8
10
|
from typing import Any, Iterable, Iterator, Literal, Mapping, Sequence
|
|
9
11
|
|
|
10
12
|
import json
|
|
@@ -227,6 +229,16 @@ class Polymarket:
|
|
|
227
229
|
signature_type: int | None = None,
|
|
228
230
|
funder: str | None = None
|
|
229
231
|
) -> None:
|
|
232
|
+
# Logger (per-class, safe default)
|
|
233
|
+
self.logger = logging.getLogger(f"{API_NAME}.{self.__class__.__name__}")
|
|
234
|
+
if not self.logger.handlers:
|
|
235
|
+
handler = logging.StreamHandler()
|
|
236
|
+
formatter = logging.Formatter(
|
|
237
|
+
"[%(asctime)s][%(levelname)s][%(name)s] %(message)s"
|
|
238
|
+
)
|
|
239
|
+
handler.setFormatter(formatter)
|
|
240
|
+
self.logger.addHandler(handler)
|
|
241
|
+
self.logger.setLevel(logging.INFO)
|
|
230
242
|
self.client = client
|
|
231
243
|
self.rest_api = (rest_api or DEFAULT_REST_ENDPOINT).rstrip("/")
|
|
232
244
|
self.ws_public = ws_public or DEFAULT_WS_ENDPOINT
|
|
@@ -271,22 +283,29 @@ class Polymarket:
|
|
|
271
283
|
"book",
|
|
272
284
|
"books",
|
|
273
285
|
"position",
|
|
274
|
-
"history_position",
|
|
275
286
|
"orders",
|
|
276
|
-
] = "all",
|
|
287
|
+
] | Sequence[str] = "all",
|
|
277
288
|
*,
|
|
278
289
|
token_ids: Sequence[str] | str | None = None,
|
|
279
290
|
limit: int | None = None,
|
|
280
|
-
funder: str | None = None,
|
|
281
|
-
event_id: str | None = None
|
|
282
291
|
) -> None:
|
|
283
|
-
"""Refresh cached data using Polymarket REST endpoints.
|
|
292
|
+
"""Refresh cached data using Polymarket REST endpoints.
|
|
293
|
+
|
|
294
|
+
update_type 可以是单个字符串或列表,例如:
|
|
295
|
+
update_type='position'
|
|
296
|
+
update_type=['position', 'orders']
|
|
297
|
+
"""
|
|
298
|
+
# 统一转为 set
|
|
299
|
+
if isinstance(update_type, str):
|
|
300
|
+
types = {update_type}
|
|
301
|
+
else:
|
|
302
|
+
types = set(update_type)
|
|
284
303
|
|
|
285
|
-
include_detail =
|
|
286
|
-
include_books =
|
|
287
|
-
include_position =
|
|
288
|
-
include_history_position =
|
|
289
|
-
include_orders =
|
|
304
|
+
include_detail = "all" in types or "detail" in types or "markets" in types
|
|
305
|
+
include_books = "all" in types or "book" in types or "books" in types
|
|
306
|
+
include_position = "all" in types or "position" in types
|
|
307
|
+
include_history_position = "history_position" in types
|
|
308
|
+
include_orders = "all" in types or "orders" in types
|
|
290
309
|
|
|
291
310
|
if include_books and token_ids is None:
|
|
292
311
|
raise ValueError("token_ids are required when updating books")
|
|
@@ -315,21 +334,11 @@ class Polymarket:
|
|
|
315
334
|
)
|
|
316
335
|
|
|
317
336
|
if include_position or include_history_position:
|
|
318
|
-
funder = funder or self.funder
|
|
319
|
-
path = '/positions' if include_position else '/closed-positions'
|
|
320
|
-
params = {"user": funder, 'sizeThreshold': 0.1}
|
|
321
|
-
if event_id:
|
|
322
|
-
params.update({'eventId': event_id})
|
|
323
337
|
tasks.append(
|
|
324
338
|
(
|
|
325
339
|
"position",
|
|
326
340
|
asyncio.create_task(
|
|
327
|
-
self.
|
|
328
|
-
"GET",
|
|
329
|
-
path,
|
|
330
|
-
params=params,
|
|
331
|
-
host=DEFAULT_DATA_ENDPOINT
|
|
332
|
-
)
|
|
341
|
+
self.get_mergeable_positions()
|
|
333
342
|
),
|
|
334
343
|
)
|
|
335
344
|
)
|
|
@@ -342,8 +351,25 @@ class Polymarket:
|
|
|
342
351
|
raise ValueError(f"Unsupported update_type={update_type}")
|
|
343
352
|
|
|
344
353
|
results: dict[str, Any] = {}
|
|
345
|
-
|
|
346
|
-
|
|
354
|
+
|
|
355
|
+
keys = [k for k, _ in tasks]
|
|
356
|
+
futs = [f for _, f in tasks]
|
|
357
|
+
|
|
358
|
+
done = await asyncio.gather(*futs, return_exceptions=True)
|
|
359
|
+
|
|
360
|
+
for key, res in zip(keys, done):
|
|
361
|
+
if isinstance(res, Exception):
|
|
362
|
+
# REST 更新为 best-effort:记录错误但不中断整体流程
|
|
363
|
+
try:
|
|
364
|
+
logger = getattr(self, "logger", None)
|
|
365
|
+
if logger:
|
|
366
|
+
logger.warning(f"[update] {key} failed: {res}", exc_info=True)
|
|
367
|
+
else:
|
|
368
|
+
print(f"[update] {key} failed: {res}")
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
continue
|
|
372
|
+
results[key] = res
|
|
347
373
|
|
|
348
374
|
|
|
349
375
|
if "books" in results:
|
|
@@ -441,12 +467,12 @@ class Polymarket:
|
|
|
441
467
|
|
|
442
468
|
wsapp = self.client.ws_connect(
|
|
443
469
|
RTS_DATA_ENDPOINT,
|
|
470
|
+
send_json=payload,
|
|
444
471
|
hdlr_str=callback,
|
|
445
472
|
heartbeat=5,
|
|
446
473
|
)
|
|
447
474
|
|
|
448
475
|
await wsapp._event.wait()
|
|
449
|
-
await wsapp.current_ws.send_json(payload)
|
|
450
476
|
return wsapp
|
|
451
477
|
|
|
452
478
|
|
|
@@ -480,6 +506,9 @@ class Polymarket:
|
|
|
480
506
|
self,
|
|
481
507
|
callback: Any = None,
|
|
482
508
|
markets: Sequence[str] | None = None,
|
|
509
|
+
rest_sync: bool = True,
|
|
510
|
+
rest_order_sync_interval: int = 5,
|
|
511
|
+
rest_position_sync_interval: int = 8,
|
|
483
512
|
) -> pybotters.ws.WebSocketApp:
|
|
484
513
|
"""Subscribe to personal updates (requires authentication)."""
|
|
485
514
|
|
|
@@ -487,8 +516,15 @@ class Polymarket:
|
|
|
487
516
|
if not creds:
|
|
488
517
|
raise RuntimeError("Polymarket API credentials are required for personal subscriptions")
|
|
489
518
|
|
|
519
|
+
# 记录 position store 最后更新时间
|
|
520
|
+
last_position_update = time.time()
|
|
521
|
+
|
|
490
522
|
def _handler(message, ws=None):
|
|
523
|
+
nonlocal last_position_update
|
|
491
524
|
self.store.onmessage(message, ws)
|
|
525
|
+
# 检测是否是 position 相关消息
|
|
526
|
+
if isinstance(message, dict) and message.get('event_type') in ('order', 'trade'):
|
|
527
|
+
last_position_update = time.time()
|
|
492
528
|
if callback:
|
|
493
529
|
callback(message, ws)
|
|
494
530
|
|
|
@@ -503,14 +539,45 @@ class Polymarket:
|
|
|
503
539
|
auth = {"apiKey": api_key, "secret": api_secret, "passphrase": api_passphrase}
|
|
504
540
|
payload = {"markets": list(markets or []), "type": "user", "auth": auth}
|
|
505
541
|
|
|
542
|
+
# 在开始前用rest_api同步持仓
|
|
543
|
+
await self.update('position')
|
|
544
|
+
|
|
545
|
+
# 后台任务:3秒无更新则同步持仓
|
|
546
|
+
async def _rest_sync_watchdog():
|
|
547
|
+
nonlocal last_position_update
|
|
548
|
+
last_orders_update = time.time()
|
|
549
|
+
while True:
|
|
550
|
+
await asyncio.sleep(1)
|
|
551
|
+
now = time.time()
|
|
552
|
+
# position: 6秒无更新则同步
|
|
553
|
+
if now - last_position_update > rest_position_sync_interval:
|
|
554
|
+
try:
|
|
555
|
+
await self.update('position')
|
|
556
|
+
last_position_update = now
|
|
557
|
+
except Exception:
|
|
558
|
+
pass
|
|
559
|
+
# orders: 每3秒同步一次
|
|
560
|
+
if now - last_orders_update > rest_order_sync_interval:
|
|
561
|
+
try:
|
|
562
|
+
await self.update('orders')
|
|
563
|
+
last_orders_update = now
|
|
564
|
+
except Exception:
|
|
565
|
+
pass
|
|
566
|
+
|
|
567
|
+
if rest_sync:
|
|
568
|
+
asyncio.create_task(_rest_sync_watchdog())
|
|
569
|
+
|
|
570
|
+
# 使用 send_json 参数,这样重连后会自动重新订阅
|
|
506
571
|
self._ws_personal = self.client.ws_connect(
|
|
507
572
|
"wss://ws-subscriptions-clob.polymarket.com/ws/user",
|
|
573
|
+
send_json=payload,
|
|
508
574
|
hdlr_json=effective_cb,
|
|
509
575
|
heartbeat=30,
|
|
510
576
|
auth=None,
|
|
511
577
|
)
|
|
512
578
|
await self._ws_personal._event.wait()
|
|
513
|
-
|
|
579
|
+
|
|
580
|
+
|
|
514
581
|
return self._ws_personal
|
|
515
582
|
|
|
516
583
|
async def sub_trades(self, slug: str):
|
|
@@ -522,7 +589,6 @@ class Polymarket:
|
|
|
522
589
|
"topic": "activity",
|
|
523
590
|
"type": "orders_matched",
|
|
524
591
|
"filters": json.dumps({"event_slug": slug}, separators=(',', ':'))
|
|
525
|
-
# "filters": "{\"event_slug\":\"btc-updown-15m-1762951500\"}"
|
|
526
592
|
}
|
|
527
593
|
]
|
|
528
594
|
}
|
|
@@ -537,14 +603,14 @@ class Polymarket:
|
|
|
537
603
|
|
|
538
604
|
self.store.onmessage(data, ws)
|
|
539
605
|
|
|
540
|
-
|
|
606
|
+
# 使用 send_json 参数,重连后自动重新订阅
|
|
541
607
|
wsapp = self.client.ws_connect(
|
|
542
608
|
RTS_DATA_ENDPOINT,
|
|
609
|
+
send_json=payload,
|
|
543
610
|
hdlr_str=callback,
|
|
544
611
|
heartbeat=5
|
|
545
612
|
)
|
|
546
613
|
await wsapp._event.wait()
|
|
547
|
-
await wsapp.current_ws.send_json(payload)
|
|
548
614
|
return wsapp
|
|
549
615
|
|
|
550
616
|
|
|
@@ -1433,6 +1499,33 @@ class Polymarket:
|
|
|
1433
1499
|
position = position / 1e6
|
|
1434
1500
|
return position
|
|
1435
1501
|
|
|
1502
|
+
async def get_mergeable_positions(
|
|
1503
|
+
self,
|
|
1504
|
+
*,
|
|
1505
|
+
size_threshold: float = 0.1,
|
|
1506
|
+
limit: int = 100,
|
|
1507
|
+
sort_by: str = "TOKENS",
|
|
1508
|
+
sort_direction: str = "DESC",
|
|
1509
|
+
user: str | None = None,
|
|
1510
|
+
neg_risk: bool = False,
|
|
1511
|
+
mergeable: bool = True,
|
|
1512
|
+
) -> Any:
|
|
1513
|
+
params = {
|
|
1514
|
+
"sizeThreshold": str(size_threshold),
|
|
1515
|
+
"limit": str(limit),
|
|
1516
|
+
"sortBy": sort_by,
|
|
1517
|
+
"sortDirection": sort_direction,
|
|
1518
|
+
"mergeable": str(mergeable).lower(),
|
|
1519
|
+
}
|
|
1520
|
+
if user is not None:
|
|
1521
|
+
params["user"] = user
|
|
1522
|
+
else:
|
|
1523
|
+
params['user'] = self.funder
|
|
1524
|
+
|
|
1525
|
+
if neg_risk:
|
|
1526
|
+
params["negRisk"] = "true"
|
|
1527
|
+
return await self._rest("GET", "/positions", params=params, host=DEFAULT_DATA_ENDPOINT)
|
|
1528
|
+
|
|
1436
1529
|
|
|
1437
1530
|
async def merge_tokens_strict(
|
|
1438
1531
|
self,
|