hyperquant 1.48__py3-none-any.whl
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.
Potentially problematic release.
This version of hyperquant might be problematic. Click here for more details.
- hyperquant/__init__.py +8 -0
- hyperquant/broker/auth.py +972 -0
- hyperquant/broker/bitget.py +311 -0
- hyperquant/broker/bitmart.py +720 -0
- hyperquant/broker/coinw.py +487 -0
- hyperquant/broker/deepcoin.py +651 -0
- hyperquant/broker/edgex.py +500 -0
- hyperquant/broker/hyperliquid.py +570 -0
- hyperquant/broker/lbank.py +661 -0
- hyperquant/broker/lib/edgex_sign.py +455 -0
- hyperquant/broker/lib/hpstore.py +252 -0
- hyperquant/broker/lib/hyper_types.py +48 -0
- hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
- hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
- hyperquant/broker/lib/util.py +22 -0
- hyperquant/broker/lighter.py +679 -0
- hyperquant/broker/models/apexpro.py +150 -0
- hyperquant/broker/models/bitget.py +359 -0
- hyperquant/broker/models/bitmart.py +635 -0
- hyperquant/broker/models/coinw.py +724 -0
- hyperquant/broker/models/deepcoin.py +809 -0
- hyperquant/broker/models/edgex.py +1053 -0
- hyperquant/broker/models/hyperliquid.py +284 -0
- hyperquant/broker/models/lbank.py +557 -0
- hyperquant/broker/models/lighter.py +868 -0
- hyperquant/broker/models/ourbit.py +1155 -0
- hyperquant/broker/models/polymarket.py +1071 -0
- hyperquant/broker/ourbit.py +550 -0
- hyperquant/broker/polymarket.py +2399 -0
- hyperquant/broker/ws.py +132 -0
- hyperquant/core.py +513 -0
- hyperquant/datavison/_util.py +18 -0
- hyperquant/datavison/binance.py +111 -0
- hyperquant/datavison/coinglass.py +237 -0
- hyperquant/datavison/okx.py +177 -0
- hyperquant/db.py +191 -0
- hyperquant/draw.py +1200 -0
- hyperquant/logkit.py +205 -0
- hyperquant/notikit.py +124 -0
- hyperquant-1.48.dist-info/METADATA +32 -0
- hyperquant-1.48.dist-info/RECORD +42 -0
- hyperquant-1.48.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1071 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from heapq import heappop, heappush
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Iterable
|
|
8
|
+
|
|
9
|
+
from pybotters.store import DataStore, DataStoreCollection
|
|
10
|
+
from pybotters.ws import ClientWebSocketResponse
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pybotters.typedefs import Item
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Position(DataStore):
|
|
17
|
+
"""Position DataStore keyed by Polymarket token id."""
|
|
18
|
+
|
|
19
|
+
_KEYS = ["asset"]
|
|
20
|
+
|
|
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:
|
|
46
|
+
if msg:
|
|
47
|
+
self._clear()
|
|
48
|
+
for rec in msg:
|
|
49
|
+
rec["ts"] = 0
|
|
50
|
+
self._update(msg)
|
|
51
|
+
|
|
52
|
+
def on_trade(self, trade: Item) -> None:
|
|
53
|
+
status = str(trade.get("status") or "").upper()
|
|
54
|
+
if status not in {"MATCHED"}:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
asset_id = trade.get("asset_id")
|
|
58
|
+
outcome = trade.get("outcome")
|
|
59
|
+
side = str(trade.get("side") or "").upper()
|
|
60
|
+
size_raw = trade.get("size")
|
|
61
|
+
price_raw = trade.get("price")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if not asset_id or not outcome or side not in {"BUY", "SELL"}:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
size = float(size_raw)
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
return
|
|
71
|
+
try:
|
|
72
|
+
price = float(price_raw)
|
|
73
|
+
except (TypeError, ValueError):
|
|
74
|
+
price = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
key = {"asset": asset_id, "outcome": outcome}
|
|
79
|
+
existing = self.get(key) or {}
|
|
80
|
+
|
|
81
|
+
cur_size = float(existing.get("size") or 0.0)
|
|
82
|
+
cur_total_bought = float(existing.get("totalBought") or 0.0)
|
|
83
|
+
cur_avg_price = float(existing.get("avgPrice") or 0.0)
|
|
84
|
+
cur_cost = cur_size * cur_avg_price
|
|
85
|
+
|
|
86
|
+
if side == "BUY":
|
|
87
|
+
new_size = cur_size + size
|
|
88
|
+
total_bought = cur_total_bought + size
|
|
89
|
+
# 未拿到成交价时使用当前均价兜底,避免均价被拉低
|
|
90
|
+
effective_price = price if price is not None else cur_avg_price
|
|
91
|
+
new_cost = cur_cost + size * effective_price
|
|
92
|
+
else: # SELL
|
|
93
|
+
new_size = cur_size - size
|
|
94
|
+
total_bought = cur_total_bought
|
|
95
|
+
# 卖出按照当前均价释放成本
|
|
96
|
+
new_cost = cur_cost - min(size, cur_size) * cur_avg_price
|
|
97
|
+
|
|
98
|
+
if new_size <= 0:
|
|
99
|
+
new_size = 0.0
|
|
100
|
+
avg_price = 0.0
|
|
101
|
+
new_cost = 0.0
|
|
102
|
+
else:
|
|
103
|
+
avg_price = max(new_cost, 0.0) / new_size
|
|
104
|
+
|
|
105
|
+
rec: dict[str, Any] = {
|
|
106
|
+
"asset": asset_id,
|
|
107
|
+
"outcome": outcome,
|
|
108
|
+
"side": side,
|
|
109
|
+
"size": new_size,
|
|
110
|
+
"totalBought": total_bought,
|
|
111
|
+
"avgPrice": avg_price,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if existing:
|
|
115
|
+
self._update([rec])
|
|
116
|
+
else:
|
|
117
|
+
self._insert([rec])
|
|
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
|
+
|
|
198
|
+
|
|
199
|
+
class Fill(DataStore):
|
|
200
|
+
"""Fill records keyed by maker order id."""
|
|
201
|
+
|
|
202
|
+
_KEYS = ["order_id"]
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def _from_trade(trade: dict[str, Any], maker: dict[str, Any]) -> dict[str, Any] | None:
|
|
206
|
+
order_id = maker.get("order_id")
|
|
207
|
+
if not order_id:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
record = {
|
|
211
|
+
"order_id": order_id,
|
|
212
|
+
"trade_id": trade.get("id"),
|
|
213
|
+
"asset_id": maker.get("asset_id") or trade.get("asset_id"),
|
|
214
|
+
"market": trade.get("market"),
|
|
215
|
+
"outcome": maker.get("outcome") or trade.get("outcome"),
|
|
216
|
+
"matched_amount": maker.get("matched_amount") or trade.get("size"),
|
|
217
|
+
"price": maker.get("price") or trade.get("price"),
|
|
218
|
+
"status": trade.get("status"),
|
|
219
|
+
"match_time": trade.get("match_time") or trade.get("timestamp"),
|
|
220
|
+
"maker_owner": maker.get("owner"),
|
|
221
|
+
"taker_order_id": trade.get("taker_order_id"),
|
|
222
|
+
"side": maker.get("side") or trade.get("side"),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for key in ("matched_amount", "price"):
|
|
226
|
+
value = record.get(key)
|
|
227
|
+
if value is None:
|
|
228
|
+
continue
|
|
229
|
+
try:
|
|
230
|
+
record[key] = float(value)
|
|
231
|
+
except (TypeError, ValueError):
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
return record
|
|
235
|
+
|
|
236
|
+
def _on_trade(self, trade: dict[str, Any]) -> None:
|
|
237
|
+
status = str(trade.get("status") or "").upper()
|
|
238
|
+
if status != "MATCHED":
|
|
239
|
+
return
|
|
240
|
+
maker_orders = trade.get("maker_orders") or []
|
|
241
|
+
upserts: list[dict[str, Any]] = []
|
|
242
|
+
for maker in maker_orders:
|
|
243
|
+
record = self._from_trade(trade, maker)
|
|
244
|
+
if not record:
|
|
245
|
+
continue
|
|
246
|
+
upserts.append(record)
|
|
247
|
+
|
|
248
|
+
if not upserts:
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
for record in upserts:
|
|
252
|
+
key = {"order_id": record["order_id"]}
|
|
253
|
+
if self.get(key):
|
|
254
|
+
self._update([record])
|
|
255
|
+
else:
|
|
256
|
+
self._insert([record])
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class Order(DataStore):
|
|
260
|
+
"""User orders keyed by order id (REST + WS)."""
|
|
261
|
+
|
|
262
|
+
_KEYS = ["id"]
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
266
|
+
oid = entry.get("id")
|
|
267
|
+
if not oid:
|
|
268
|
+
return None
|
|
269
|
+
normalized = dict(entry)
|
|
270
|
+
# numeric fields
|
|
271
|
+
for field in ("price", "original_size", "size_matched"):
|
|
272
|
+
val = normalized.get(field)
|
|
273
|
+
try:
|
|
274
|
+
if val is not None:
|
|
275
|
+
normalized[field] = float(val)
|
|
276
|
+
except (TypeError, ValueError):
|
|
277
|
+
pass
|
|
278
|
+
return normalized
|
|
279
|
+
|
|
280
|
+
def _on_response(self, items: list[dict[str, Any]] | dict[str, Any]) -> None:
|
|
281
|
+
"""增量同步:insert新增、update变更、delete消失的订单"""
|
|
282
|
+
rows: list[dict[str, Any]] = []
|
|
283
|
+
if isinstance(items, dict):
|
|
284
|
+
items = [items]
|
|
285
|
+
for it in items or []:
|
|
286
|
+
norm = self._normalize(it)
|
|
287
|
+
if norm:
|
|
288
|
+
rows.append(norm)
|
|
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])
|
|
307
|
+
|
|
308
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
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])
|
|
314
|
+
|
|
315
|
+
if status in {"CANCELED", "MATCHED"}:
|
|
316
|
+
self._delete([msg])
|
|
317
|
+
|
|
318
|
+
class MyTrade(DataStore):
|
|
319
|
+
"""User trades keyed by trade id."""
|
|
320
|
+
|
|
321
|
+
_KEYS = ["id"]
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
325
|
+
trade_id = entry.get("id")
|
|
326
|
+
if not trade_id:
|
|
327
|
+
return None
|
|
328
|
+
normalized = dict(entry)
|
|
329
|
+
for field in ("price", "size", "fee_rate_bps"):
|
|
330
|
+
value = normalized.get(field)
|
|
331
|
+
if value is None:
|
|
332
|
+
continue
|
|
333
|
+
try:
|
|
334
|
+
normalized[field] = float(value)
|
|
335
|
+
except (TypeError, ValueError):
|
|
336
|
+
pass
|
|
337
|
+
return normalized
|
|
338
|
+
|
|
339
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
340
|
+
normalized = self._normalize(msg) or {}
|
|
341
|
+
trade_id = normalized.get("id")
|
|
342
|
+
if not trade_id:
|
|
343
|
+
return
|
|
344
|
+
if self.get({"id": trade_id}):
|
|
345
|
+
self._update([normalized])
|
|
346
|
+
else:
|
|
347
|
+
self._insert([normalized])
|
|
348
|
+
|
|
349
|
+
class Trade(DataStore):
|
|
350
|
+
"""User trades keyed by trade id."""
|
|
351
|
+
|
|
352
|
+
_KEYS = ["hash"]
|
|
353
|
+
_MAXLEN = 500
|
|
354
|
+
|
|
355
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
356
|
+
payload = msg or {}
|
|
357
|
+
if payload:
|
|
358
|
+
if payload.get("event_type") == "last_trade_price":
|
|
359
|
+
transaction_hash = payload.get("transaction_hash")
|
|
360
|
+
if transaction_hash:
|
|
361
|
+
payload.update({"hash": transaction_hash})
|
|
362
|
+
payload.pop("transaction_hash", None)
|
|
363
|
+
else:
|
|
364
|
+
if payload.get("transactionHash"):
|
|
365
|
+
payload.update({"hash": payload.get("transactionHash")})
|
|
366
|
+
payload.pop("transactionHash", None)
|
|
367
|
+
|
|
368
|
+
self._insert([payload])
|
|
369
|
+
|
|
370
|
+
class Book(DataStore):
|
|
371
|
+
"""Full depth order book keyed by Polymarket token id."""
|
|
372
|
+
|
|
373
|
+
_KEYS = ["s", "S", "p"]
|
|
374
|
+
|
|
375
|
+
def _init(self) -> None:
|
|
376
|
+
self.id_to_alias: dict[str, str] = {}
|
|
377
|
+
|
|
378
|
+
def update_aliases(self, mapping: dict[str, str]) -> None:
|
|
379
|
+
if not mapping:
|
|
380
|
+
return
|
|
381
|
+
self.id_to_alias.update(mapping)
|
|
382
|
+
|
|
383
|
+
def _alias(self, asset_id: str | None) -> tuple[str, str | None] | tuple[None, None]:
|
|
384
|
+
if asset_id is None:
|
|
385
|
+
return None, None
|
|
386
|
+
alias = self.id_to_alias.get(asset_id)
|
|
387
|
+
return asset_id, alias
|
|
388
|
+
|
|
389
|
+
def _normalize_levels(
|
|
390
|
+
self,
|
|
391
|
+
entries: Iterable[dict[str, Any]] | None,
|
|
392
|
+
*,
|
|
393
|
+
side: str,
|
|
394
|
+
symbol: str,
|
|
395
|
+
alias: str | None,
|
|
396
|
+
) -> list[dict[str, Any]]:
|
|
397
|
+
if not entries:
|
|
398
|
+
return []
|
|
399
|
+
normalized: list[dict[str, Any]] = []
|
|
400
|
+
for entry in entries:
|
|
401
|
+
try:
|
|
402
|
+
price = float(entry["price"])
|
|
403
|
+
size = float(entry["size"])
|
|
404
|
+
except (KeyError, TypeError, ValueError):
|
|
405
|
+
continue
|
|
406
|
+
record = {"s": symbol, "S": side, "p": price, "q": size}
|
|
407
|
+
if alias is not None:
|
|
408
|
+
record["alias"] = alias
|
|
409
|
+
normalized.append(record)
|
|
410
|
+
return normalized
|
|
411
|
+
|
|
412
|
+
def _purge_missing_levels(
|
|
413
|
+
self, *, symbol: str, side: str, new_levels: list[dict[str, Any]]
|
|
414
|
+
) -> None:
|
|
415
|
+
"""Remove levels no longer present in the latest snapshot."""
|
|
416
|
+
existing = self.find({"s": symbol, "S": side})
|
|
417
|
+
if not existing:
|
|
418
|
+
return
|
|
419
|
+
new_prices = {lvl["p"] for lvl in new_levels}
|
|
420
|
+
stale = [
|
|
421
|
+
{"s": symbol, "S": side, "p": level["p"]}
|
|
422
|
+
for level in existing
|
|
423
|
+
if level.get("p") not in new_prices
|
|
424
|
+
]
|
|
425
|
+
if stale:
|
|
426
|
+
self._delete(stale)
|
|
427
|
+
|
|
428
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
429
|
+
msg_type = msg.get("event_type")
|
|
430
|
+
if msg_type not in {"book", "price_change"}:
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
if msg_type == "book":
|
|
434
|
+
asset_id = msg.get("asset_id") or msg.get("token_id")
|
|
435
|
+
symbol, alias = self._alias(asset_id)
|
|
436
|
+
if symbol is None:
|
|
437
|
+
return
|
|
438
|
+
bids = self._normalize_levels(msg.get("bids"), side="b", symbol=symbol, alias=alias)
|
|
439
|
+
asks = self._normalize_levels(msg.get("asks"), side="a", symbol=symbol, alias=alias)
|
|
440
|
+
self._purge_missing_levels(symbol=symbol, side="b", new_levels=bids)
|
|
441
|
+
self._purge_missing_levels(symbol=symbol, side="a", new_levels=asks)
|
|
442
|
+
if bids:
|
|
443
|
+
self._insert(bids)
|
|
444
|
+
if asks:
|
|
445
|
+
self._insert(asks)
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
price_changes = msg.get("price_changes") or []
|
|
449
|
+
updates: list[dict[str, Any]] = []
|
|
450
|
+
removals: list[dict[str, Any]] = []
|
|
451
|
+
for change in price_changes:
|
|
452
|
+
asset_id = change.get("asset_id") or change.get("token_id")
|
|
453
|
+
symbol, alias = self._alias(asset_id)
|
|
454
|
+
if symbol is None:
|
|
455
|
+
continue
|
|
456
|
+
side = "b" if change.get("side") == "BUY" else "a"
|
|
457
|
+
try:
|
|
458
|
+
price = float(change["price"])
|
|
459
|
+
size = float(change["size"])
|
|
460
|
+
except (KeyError, TypeError, ValueError):
|
|
461
|
+
continue
|
|
462
|
+
record = {"s": symbol, "S": side, "p": price}
|
|
463
|
+
if alias is not None:
|
|
464
|
+
record["alias"] = alias
|
|
465
|
+
if size == 0:
|
|
466
|
+
removals.append({"s": symbol, "S": side, "p": price})
|
|
467
|
+
else:
|
|
468
|
+
record["q"] = size
|
|
469
|
+
updates.append(record)
|
|
470
|
+
|
|
471
|
+
if removals:
|
|
472
|
+
self._delete(removals)
|
|
473
|
+
if updates:
|
|
474
|
+
self._update(updates)
|
|
475
|
+
|
|
476
|
+
def sorted(
|
|
477
|
+
self, query: Item | None = None, limit: int | None = None
|
|
478
|
+
) -> dict[str, list[Item]]:
|
|
479
|
+
return self._sorted(
|
|
480
|
+
item_key="S",
|
|
481
|
+
item_asc_key="a",
|
|
482
|
+
item_desc_key="b",
|
|
483
|
+
sort_key="p",
|
|
484
|
+
query=query,
|
|
485
|
+
limit=limit,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
@dataclass
|
|
489
|
+
class _SideBook:
|
|
490
|
+
is_ask: bool
|
|
491
|
+
levels: dict[float, tuple[str, str]] = field(default_factory=dict)
|
|
492
|
+
heap: list[tuple[float, float]] = field(default_factory=list)
|
|
493
|
+
|
|
494
|
+
def clear(self) -> None:
|
|
495
|
+
self.levels.clear()
|
|
496
|
+
self.heap.clear()
|
|
497
|
+
|
|
498
|
+
def update_levels(
|
|
499
|
+
self, updates: Iterable[dict[str, Any]] | None, *, snapshot: bool
|
|
500
|
+
) -> None:
|
|
501
|
+
if updates is None:
|
|
502
|
+
return
|
|
503
|
+
|
|
504
|
+
if snapshot:
|
|
505
|
+
self.clear()
|
|
506
|
+
|
|
507
|
+
for entry in updates:
|
|
508
|
+
price, size = self._extract(entry)
|
|
509
|
+
price_val = self._to_float(price)
|
|
510
|
+
size_val = self._to_float(size)
|
|
511
|
+
if price_val is None or size_val is None:
|
|
512
|
+
continue
|
|
513
|
+
|
|
514
|
+
if size_val <= 0:
|
|
515
|
+
self.levels.pop(price_val, None)
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
self.levels[price_val] = (str(price), str(size))
|
|
519
|
+
priority = price_val if self.is_ask else -price_val
|
|
520
|
+
heappush(self.heap, (priority, price_val))
|
|
521
|
+
|
|
522
|
+
def best(self) -> tuple[str, str] | None:
|
|
523
|
+
while self.heap:
|
|
524
|
+
_, price = self.heap[0]
|
|
525
|
+
level = self.levels.get(price)
|
|
526
|
+
if level is not None:
|
|
527
|
+
return level
|
|
528
|
+
heappop(self.heap)
|
|
529
|
+
return None
|
|
530
|
+
|
|
531
|
+
@staticmethod
|
|
532
|
+
def _extract(entry: Any) -> tuple[Any, Any]:
|
|
533
|
+
if isinstance(entry, dict):
|
|
534
|
+
price = entry.get("price", entry.get("p"))
|
|
535
|
+
size = entry.get("size", entry.get("q"))
|
|
536
|
+
return price, size
|
|
537
|
+
if isinstance(entry, (list, tuple)) and len(entry) >= 2:
|
|
538
|
+
return entry[0], entry[1]
|
|
539
|
+
return None, None
|
|
540
|
+
|
|
541
|
+
@staticmethod
|
|
542
|
+
def _to_float(value: Any) -> float | None:
|
|
543
|
+
try:
|
|
544
|
+
return float(value)
|
|
545
|
+
except (TypeError, ValueError):
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
class Price(DataStore):
|
|
549
|
+
_KEYS = ["s"]
|
|
550
|
+
|
|
551
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
552
|
+
payload = msg.get('payload') or {}
|
|
553
|
+
data = payload.get('data') or {}
|
|
554
|
+
symbol = payload.get('symbol')
|
|
555
|
+
|
|
556
|
+
if not symbol:
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
_next = self.get({'s': symbol}) or {}
|
|
560
|
+
_next_price = _next.get('p')
|
|
561
|
+
last_price = None
|
|
562
|
+
|
|
563
|
+
if data and isinstance(data, list):
|
|
564
|
+
last_price = data[-1].get('value')
|
|
565
|
+
if 'value' in payload:
|
|
566
|
+
last_price = payload.get('value')
|
|
567
|
+
|
|
568
|
+
if last_price is None:
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
record = {'s': symbol, 'p': last_price}
|
|
572
|
+
key = {'s': symbol}
|
|
573
|
+
if self.get(key):
|
|
574
|
+
self._update([record])
|
|
575
|
+
else:
|
|
576
|
+
self._insert([record])
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class BBO(DataStore):
|
|
580
|
+
_KEYS = ["s", "S"]
|
|
581
|
+
|
|
582
|
+
def _init(self) -> None:
|
|
583
|
+
self._book: dict[str, dict[str, _SideBook]] = {}
|
|
584
|
+
self.id_to_alias: dict[str, str] = {}
|
|
585
|
+
|
|
586
|
+
def update_aliases(self, mapping: dict[str, str]) -> None:
|
|
587
|
+
if not mapping:
|
|
588
|
+
return
|
|
589
|
+
self.id_to_alias.update(mapping)
|
|
590
|
+
|
|
591
|
+
def _alias(self, asset_id: str | None) -> tuple[str, str | None] | tuple[None, None]:
|
|
592
|
+
if asset_id is None:
|
|
593
|
+
return None, None
|
|
594
|
+
alias = self.id_to_alias.get(asset_id)
|
|
595
|
+
return asset_id, alias
|
|
596
|
+
|
|
597
|
+
def _side(self, symbol: str, side: str) -> _SideBook:
|
|
598
|
+
symbol_book = self._book.setdefault(symbol, {})
|
|
599
|
+
side_book = symbol_book.get(side)
|
|
600
|
+
if side_book is None:
|
|
601
|
+
side_book = _SideBook(is_ask=(side == "a"))
|
|
602
|
+
symbol_book[side] = side_book
|
|
603
|
+
return side_book
|
|
604
|
+
|
|
605
|
+
def _sync_side(
|
|
606
|
+
self, symbol: str, side: str, best: tuple[str, str] | None, alias: str | None
|
|
607
|
+
) -> None:
|
|
608
|
+
key = {"s": symbol, "S": side}
|
|
609
|
+
current = self.get(key)
|
|
610
|
+
|
|
611
|
+
if best is None:
|
|
612
|
+
if current:
|
|
613
|
+
self._delete([key])
|
|
614
|
+
return
|
|
615
|
+
|
|
616
|
+
price, size = best
|
|
617
|
+
payload = {"s": symbol, "S": side, "p": price, "q": size}
|
|
618
|
+
if alias is not None:
|
|
619
|
+
payload["alias"] = alias
|
|
620
|
+
|
|
621
|
+
if current:
|
|
622
|
+
cur_price = current.get("p")
|
|
623
|
+
cur_size = current.get("q")
|
|
624
|
+
cur_alias = current.get("alias")
|
|
625
|
+
|
|
626
|
+
if cur_price == price:
|
|
627
|
+
# price unchanged -> only update quantities / alias changes
|
|
628
|
+
if cur_size != size or (alias is not None and cur_alias != alias):
|
|
629
|
+
self._update([payload])
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
# price changed -> delete old then insert new level to trigger change watchers
|
|
633
|
+
self._delete([key])
|
|
634
|
+
|
|
635
|
+
self._insert([payload])
|
|
636
|
+
|
|
637
|
+
def _from_price_changes(self, msg: dict[str, Any]) -> None:
|
|
638
|
+
price_changes = msg.get("price_changes") or []
|
|
639
|
+
touched: dict[str, str | None] = {}
|
|
640
|
+
for change in price_changes:
|
|
641
|
+
asset_id = change.get("asset_id") or change.get("token_id")
|
|
642
|
+
symbol, alias = self._alias(asset_id)
|
|
643
|
+
if symbol is None:
|
|
644
|
+
continue
|
|
645
|
+
side = "b" if str(change.get("side") or "").upper() == "BUY" else "a"
|
|
646
|
+
side_book = self._side(symbol, side)
|
|
647
|
+
side_book.update_levels([change], snapshot=False)
|
|
648
|
+
touched[symbol] = alias
|
|
649
|
+
|
|
650
|
+
for symbol, alias in touched.items():
|
|
651
|
+
asks = self._side(symbol, "a")
|
|
652
|
+
bids = self._side(symbol, "b")
|
|
653
|
+
self._sync_side(symbol, "a", asks.best(), alias)
|
|
654
|
+
self._sync_side(symbol, "b", bids.best(), alias)
|
|
655
|
+
|
|
656
|
+
def _from_snapshot(self, msg: dict[str, Any]) -> None:
|
|
657
|
+
asset_id = msg.get("asset_id") or msg.get("token_id")
|
|
658
|
+
symbol, alias = self._alias(asset_id)
|
|
659
|
+
if symbol is None:
|
|
660
|
+
return
|
|
661
|
+
asks = self._side(symbol, "a")
|
|
662
|
+
bids = self._side(symbol, "b")
|
|
663
|
+
asks.update_levels(msg.get("asks"), snapshot=True)
|
|
664
|
+
bids.update_levels(msg.get("bids"), snapshot=True)
|
|
665
|
+
self._sync_side(symbol, "a", asks.best(), alias)
|
|
666
|
+
self._sync_side(symbol, "b", bids.best(), alias)
|
|
667
|
+
|
|
668
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
669
|
+
msg_type = (msg.get("event_type") or msg.get("type") or "").lower()
|
|
670
|
+
if msg_type == "book":
|
|
671
|
+
self._from_snapshot(msg)
|
|
672
|
+
elif msg_type == "price_change":
|
|
673
|
+
self._from_price_changes(msg)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
class Detail(DataStore):
|
|
677
|
+
"""Market metadata keyed by Polymarket token id."""
|
|
678
|
+
|
|
679
|
+
_KEYS = ["token_id"]
|
|
680
|
+
|
|
681
|
+
@staticmethod
|
|
682
|
+
def _normalize_entry(market: dict[str, Any], token: dict[str, Any]) -> dict[str, Any]:
|
|
683
|
+
slug = market.get("slug")
|
|
684
|
+
outcome = token.get("outcome")
|
|
685
|
+
alias = slug if outcome is None else f"{slug}:{outcome}"
|
|
686
|
+
|
|
687
|
+
tick_size = (
|
|
688
|
+
market.get("minimum_tick_size")
|
|
689
|
+
or market.get("orderPriceMinTickSize")
|
|
690
|
+
or market.get("order_price_min_tick_size")
|
|
691
|
+
)
|
|
692
|
+
step_size = (
|
|
693
|
+
market.get("minimum_order_size")
|
|
694
|
+
or market.get("orderMinSize")
|
|
695
|
+
or market.get("order_min_size")
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
try:
|
|
699
|
+
tick_size = float(tick_size) if tick_size is not None else None
|
|
700
|
+
except (TypeError, ValueError):
|
|
701
|
+
tick_size = None
|
|
702
|
+
try:
|
|
703
|
+
step_size = float(step_size) if step_size is not None else None
|
|
704
|
+
except (TypeError, ValueError):
|
|
705
|
+
step_size = None
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
"token_id": token.get("token_id") or token.get("id"),
|
|
709
|
+
"asset_id": token.get("token_id") or token.get("id"),
|
|
710
|
+
"alias": alias,
|
|
711
|
+
"question": market.get("question"),
|
|
712
|
+
"outcome": outcome,
|
|
713
|
+
"active": market.get("active"),
|
|
714
|
+
"closed": market.get("closed"),
|
|
715
|
+
"neg_risk": market.get("neg_risk"),
|
|
716
|
+
"tick_size": tick_size if tick_size is not None else 0.01,
|
|
717
|
+
"step_size": step_size if step_size is not None else 1.0,
|
|
718
|
+
"minimum_order_size": step_size if step_size is not None else 1.0,
|
|
719
|
+
"minimum_tick_size": tick_size if tick_size is not None else 0.01,
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
def on_response(self, markets: Iterable[dict[str, Any]]) -> dict[str, str]:
|
|
723
|
+
mapping: dict[str, str] = {}
|
|
724
|
+
records: list[dict[str, Any]] = []
|
|
725
|
+
for market in markets or []:
|
|
726
|
+
tokens = market.get("tokens") or []
|
|
727
|
+
if not tokens:
|
|
728
|
+
token_ids = market.get("clobTokenIds") or []
|
|
729
|
+
outcomes = market.get("outcomes") or []
|
|
730
|
+
|
|
731
|
+
if isinstance(token_ids, str):
|
|
732
|
+
try:
|
|
733
|
+
token_ids = json.loads(token_ids)
|
|
734
|
+
except json.JSONDecodeError:
|
|
735
|
+
token_ids = [token_ids]
|
|
736
|
+
if isinstance(outcomes, str):
|
|
737
|
+
try:
|
|
738
|
+
outcomes = json.loads(outcomes)
|
|
739
|
+
except json.JSONDecodeError:
|
|
740
|
+
outcomes = [outcomes]
|
|
741
|
+
|
|
742
|
+
if not isinstance(token_ids, list):
|
|
743
|
+
token_ids = [token_ids]
|
|
744
|
+
if not isinstance(outcomes, list):
|
|
745
|
+
outcomes = [outcomes]
|
|
746
|
+
|
|
747
|
+
tokens = [
|
|
748
|
+
{"token_id": tid, "outcome": outcomes[idx] if idx < len(outcomes) else None}
|
|
749
|
+
for idx, tid in enumerate(token_ids)
|
|
750
|
+
if tid
|
|
751
|
+
]
|
|
752
|
+
|
|
753
|
+
for token in tokens:
|
|
754
|
+
normalized = self._normalize_entry(market, token)
|
|
755
|
+
slug: str = market.get("slug")
|
|
756
|
+
# 取最后一个'-'之前部分
|
|
757
|
+
base_slug = slug.rsplit("-", 1)[0] if slug else slug
|
|
758
|
+
# Add or update additional fields from market
|
|
759
|
+
normalized.update({
|
|
760
|
+
"condition_id": market.get("conditionId"),
|
|
761
|
+
"slug": market.get("slug"),
|
|
762
|
+
"base_slug": base_slug,
|
|
763
|
+
"end_date": market.get("endDate"),
|
|
764
|
+
"start_date": market.get("startDate"),
|
|
765
|
+
"icon": market.get("icon"),
|
|
766
|
+
"image": market.get("image"),
|
|
767
|
+
"liquidity": market.get("liquidityNum") or market.get("liquidity"),
|
|
768
|
+
"volume": market.get("volumeNum") or market.get("volume"),
|
|
769
|
+
"accepting_orders": market.get("acceptingOrders"),
|
|
770
|
+
"spread": market.get("spread"),
|
|
771
|
+
"best_bid": market.get("bestBid"),
|
|
772
|
+
"best_ask": market.get("bestAsk"),
|
|
773
|
+
})
|
|
774
|
+
token_id = normalized.get("token_id")
|
|
775
|
+
if not token_id:
|
|
776
|
+
continue
|
|
777
|
+
records.append(normalized)
|
|
778
|
+
mapping[token_id] = normalized.get("alias") or token_id
|
|
779
|
+
|
|
780
|
+
self._update(records)
|
|
781
|
+
return mapping
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
class PolymarketDataStore(DataStoreCollection):
|
|
785
|
+
"""Polymarket-specific DataStore aggregate."""
|
|
786
|
+
|
|
787
|
+
def _init(self) -> None:
|
|
788
|
+
self._create("book", datastore_class=Book)
|
|
789
|
+
self._create("bbo", datastore_class=BBO)
|
|
790
|
+
self._create("detail", datastore_class=Detail)
|
|
791
|
+
self._create("position", datastore_class=Position)
|
|
792
|
+
self._create("order", datastore_class=Order)
|
|
793
|
+
self._create("mytrade", datastore_class=MyTrade)
|
|
794
|
+
self._create("fill", datastore_class=Fill)
|
|
795
|
+
self._create("trade", datastore_class=Trade)
|
|
796
|
+
self._create("price", datastore_class=Price)
|
|
797
|
+
|
|
798
|
+
@property
|
|
799
|
+
def book(self) -> Book:
|
|
800
|
+
"""Order Book DataStore
|
|
801
|
+
_key: k (asset_id), S (side), p (price)
|
|
802
|
+
|
|
803
|
+
.. code:: json
|
|
804
|
+
[{
|
|
805
|
+
"k": "asset_id",
|
|
806
|
+
"S": "b" | "a",
|
|
807
|
+
"p": "price",
|
|
808
|
+
"q": "size"
|
|
809
|
+
}]
|
|
810
|
+
"""
|
|
811
|
+
return self._get("book")
|
|
812
|
+
|
|
813
|
+
@property
|
|
814
|
+
def detail(self) -> Detail:
|
|
815
|
+
"""
|
|
816
|
+
Market metadata keyed by token id.
|
|
817
|
+
|
|
818
|
+
.. code:: json
|
|
819
|
+
|
|
820
|
+
[
|
|
821
|
+
{
|
|
822
|
+
"token_id": "14992165475527298486519422865149275159537493330633013685269145597531945526992",
|
|
823
|
+
"asset_id": "14992165475527298486519422865149275159537493330633013685269145597531945526992",
|
|
824
|
+
"alias": "Bitcoin Up or Down - November 12, 12:30AM-12:45AM ET:Down",
|
|
825
|
+
"question": "Bitcoin Up or Down - November 12, 12:30AM-12:45AM ET",
|
|
826
|
+
"outcome": "Down",
|
|
827
|
+
"active": true,
|
|
828
|
+
"closed": false,
|
|
829
|
+
"neg_risk": null,
|
|
830
|
+
"tick_size": 0.01,
|
|
831
|
+
"step_size": 5.0,
|
|
832
|
+
"minimum_order_size": 5.0,
|
|
833
|
+
"minimum_tick_size": 0.01,
|
|
834
|
+
"condition_id": "0xb64133e5ae9710fab2533cfd3c48cba142347e4bab36822964ca4cca4b7660d2",
|
|
835
|
+
"slug": "btc-updown-15m-1762925400",
|
|
836
|
+
"end_date": "2025-11-12T05:45:00Z",
|
|
837
|
+
"start_date": "2025-11-11T05:32:59.491174Z",
|
|
838
|
+
"icon": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
|
|
839
|
+
"image": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
|
|
840
|
+
"liquidity": 59948.1793,
|
|
841
|
+
"volume": 12214.600385,
|
|
842
|
+
"accepting_orders": true,
|
|
843
|
+
"spread": 0.01,
|
|
844
|
+
"best_bid": 0.5,
|
|
845
|
+
"best_ask": 0.51
|
|
846
|
+
}
|
|
847
|
+
]
|
|
848
|
+
"""
|
|
849
|
+
|
|
850
|
+
return self._get("detail")
|
|
851
|
+
|
|
852
|
+
@property
|
|
853
|
+
def position(self) -> Position:
|
|
854
|
+
"""
|
|
855
|
+
|
|
856
|
+
.. code:: python
|
|
857
|
+
|
|
858
|
+
[{
|
|
859
|
+
# 🔑 基础信息
|
|
860
|
+
"proxyWallet": "0x56687bf447db6ffa42ffe2204a05edaa20f55839", # 代理钱包地址(用于代表用户在链上的交易地址)
|
|
861
|
+
"asset": "<string>", # outcome token 资产地址或 symbol
|
|
862
|
+
"conditionId": "0xdd22472e552920b8438158ea7238bfadfa4f736aa4cee91a6b86c39ead110917", # 市场条件 ID(event 的唯一标识)
|
|
863
|
+
|
|
864
|
+
# 💰 交易与价格信息
|
|
865
|
+
"size": 123, # 当前持仓数量(仅在未平仓时存在)
|
|
866
|
+
"avgPrice": 123, # 平均买入价(每个 outcome token 的均价)
|
|
867
|
+
"curPrice": 123, # 当前市场价格
|
|
868
|
+
"initialValue": 123, # 初始建仓总价值(avgPrice × size)
|
|
869
|
+
"currentValue": 123, # 当前持仓市值(curPrice × size)
|
|
870
|
+
|
|
871
|
+
# 📊 盈亏指标
|
|
872
|
+
"cashPnl": 123, # 未实现盈亏(当前浮动盈亏)
|
|
873
|
+
"percentPnl": 123, # 未实现盈亏百分比
|
|
874
|
+
"realizedPnl": 123, # 已实现盈亏(平仓后的实际收益)
|
|
875
|
+
"percentRealizedPnl": 123, # 已实现盈亏百分比(相对成本的收益率)
|
|
876
|
+
|
|
877
|
+
# 🧮 累计交易信息
|
|
878
|
+
"totalBought": 123, # 累计买入数量(含历史)
|
|
879
|
+
|
|
880
|
+
# ⚙️ 状态标志
|
|
881
|
+
"redeemable": True, # 是否可赎回(True 表示市场已结算且你是赢家,可提取 USDC)
|
|
882
|
+
"mergeable": True, # 是否可合并(多笔相同 outcome 可合并为一笔)
|
|
883
|
+
"negativeRisk": True, # 是否为负风险组合(风险对冲导致净敞口为负)
|
|
884
|
+
|
|
885
|
+
# 🧠 市场元数据
|
|
886
|
+
"title": "<string>", # 市场标题(如 “Bitcoin up or down 15m”)
|
|
887
|
+
"slug": "<string>", # outcome 唯一 slug(对应前端页面路径的一部分)
|
|
888
|
+
"eventSlug": "<string>", # event slug(整个预测事件的唯一路径标识)
|
|
889
|
+
"icon": "<string>", # 图标 URL(一般为事件关联资产)
|
|
890
|
+
"outcome": "<string>", # 当前持有的 outcome 名称(例如 “Yes” 或 “No”)
|
|
891
|
+
"outcomeIndex": 123, # outcome 在该市场中的索引(0 或 1)
|
|
892
|
+
"oppositeOutcome": "<string>",# 对立 outcome 名称
|
|
893
|
+
"oppositeAsset": "<string>", # 对立 outcome token 地址
|
|
894
|
+
"endDate": "<string>", # 市场结束时间(UTC ISO 格式字符串)
|
|
895
|
+
}]
|
|
896
|
+
"""
|
|
897
|
+
|
|
898
|
+
return self._get("position")
|
|
899
|
+
|
|
900
|
+
@property
|
|
901
|
+
def orders(self) -> Order:
|
|
902
|
+
"""User orders keyed by order id.
|
|
903
|
+
|
|
904
|
+
Example row (from REST get_orders):
|
|
905
|
+
|
|
906
|
+
.. code:: json
|
|
907
|
+
{
|
|
908
|
+
"id": "0xd4359d…",
|
|
909
|
+
"status": "LIVE",
|
|
910
|
+
"owner": "<api-key>",
|
|
911
|
+
"maker_address": "0x…",
|
|
912
|
+
"market": "0x…",
|
|
913
|
+
"asset_id": "317234…",
|
|
914
|
+
"side": "BUY",
|
|
915
|
+
"original_size": 5.0,
|
|
916
|
+
"size_matched": 0.0,
|
|
917
|
+
"price": 0.02,
|
|
918
|
+
"outcome": "Up",
|
|
919
|
+
"order_type": "GTC",
|
|
920
|
+
"created_at": 1762912331
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
"""
|
|
924
|
+
|
|
925
|
+
return self._get("order")
|
|
926
|
+
|
|
927
|
+
@property
|
|
928
|
+
def mytrade(self) -> MyTrade:
|
|
929
|
+
"""User trade stream keyed by trade id.
|
|
930
|
+
|
|
931
|
+
Columns include Polymarket websocket ``trade`` payloads, e.g.
|
|
932
|
+
|
|
933
|
+
.. code:: json
|
|
934
|
+
{
|
|
935
|
+
"event_type": "trade",
|
|
936
|
+
"id": "28c4d2eb-bbea-40e7-a9f0-b2fdb56b2c2e",
|
|
937
|
+
"market": "0xbd31…",
|
|
938
|
+
"asset_id": "521143…",
|
|
939
|
+
"side": "BUY",
|
|
940
|
+
"price": 0.57,
|
|
941
|
+
"size": 10,
|
|
942
|
+
"status": "MATCHED",
|
|
943
|
+
"maker_orders": [ ... ]
|
|
944
|
+
}
|
|
945
|
+
"""
|
|
946
|
+
|
|
947
|
+
return self._get("trade")
|
|
948
|
+
|
|
949
|
+
@property
|
|
950
|
+
def price(self) -> Price:
|
|
951
|
+
"""Price DataStore
|
|
952
|
+
_key: s
|
|
953
|
+
"""
|
|
954
|
+
return self._get("price")
|
|
955
|
+
|
|
956
|
+
@property
|
|
957
|
+
def fill(self) -> Fill:
|
|
958
|
+
"""Maker-order fills keyed by ``order_id``.
|
|
959
|
+
|
|
960
|
+
A row is created whenever a trade arrives with ``status == 'MATCHED'``.
|
|
961
|
+
``matched_amount`` and ``price`` are stored as floats for quick PnL math.
|
|
962
|
+
|
|
963
|
+
.. code:: json
|
|
964
|
+
{
|
|
965
|
+
"order_id": "0xb46574626be7eb57a8fa643eac5623bdb2ec42104e2dc3441576a6ed8d0cc0ed",
|
|
966
|
+
"owner": "1aa9c6be-02d2-c021-c5fc-0c5b64ba8fd6",
|
|
967
|
+
"maker_address": "0x64A46A989363eb21DAB87CD53d57A4567Ccbc103",
|
|
968
|
+
"matched_amount": "1.35",
|
|
969
|
+
"price": "0.73",
|
|
970
|
+
"fee_rate_bps": "0",
|
|
971
|
+
"asset_id": "60833383978754019365794467018212448484210363665632025956221025028271757152271",
|
|
972
|
+
"outcome": "Up",
|
|
973
|
+
"outcome_index": 0,
|
|
974
|
+
"side": "BUY"
|
|
975
|
+
}
|
|
976
|
+
"""
|
|
977
|
+
|
|
978
|
+
return self._get("fill")
|
|
979
|
+
|
|
980
|
+
@property
|
|
981
|
+
def bbo(self) -> BBO:
|
|
982
|
+
"""Best Bid and Offer DataStore
|
|
983
|
+
_key: s (asset_id), S (side)
|
|
984
|
+
|
|
985
|
+
"""
|
|
986
|
+
return self._get("bbo")
|
|
987
|
+
|
|
988
|
+
@property
|
|
989
|
+
def trade(self) -> Trade:
|
|
990
|
+
"""
|
|
991
|
+
_key asset
|
|
992
|
+
MATCHED进行快速捕捉
|
|
993
|
+
.. code:: json
|
|
994
|
+
{
|
|
995
|
+
"asset": "12819879685513143002408869746992985182419696851931617234615358342350852997413",
|
|
996
|
+
"bio": "",
|
|
997
|
+
"conditionId": "0xea609d2c6bc2cb20e328be7c89f258b84b35bbe119b44e0a2cfc5f15e6642b3b",
|
|
998
|
+
"eventSlug": "btc-updown-15m-1763865000",
|
|
999
|
+
"icon": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
|
|
1000
|
+
"name": "infusion",
|
|
1001
|
+
"outcome": "Up",
|
|
1002
|
+
"outcomeIndex": 0,
|
|
1003
|
+
"price": 0.7,
|
|
1004
|
+
"profileImage": "",
|
|
1005
|
+
"proxyWallet": "0x2C060830B6F6B43174b1Cf8B4475db07703c1543",
|
|
1006
|
+
"pseudonym": "Frizzy-Graduate",
|
|
1007
|
+
"side": "BUY",
|
|
1008
|
+
"size": 5,
|
|
1009
|
+
"slug": "btc-updown-15m-1763865000",
|
|
1010
|
+
"timestamp": 1763865085,
|
|
1011
|
+
"title": "Bitcoin Up or Down - November 22, 9:30PM-9:45PM ET",
|
|
1012
|
+
"hash": "0xddea11d695e811686f83379d9269accf1be581fbcb542809c6c67a3cc3002488"
|
|
1013
|
+
}
|
|
1014
|
+
"""
|
|
1015
|
+
return self._get("trade")
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def onmessage(self, msg: Any, ws: ClientWebSocketResponse | None = None) -> None:
|
|
1019
|
+
# 判定msg是否为list
|
|
1020
|
+
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
1021
|
+
for m in lst_msg:
|
|
1022
|
+
if m == '':
|
|
1023
|
+
continue
|
|
1024
|
+
topic = m.get("topic") or ""
|
|
1025
|
+
if topic in {'crypto_prices_chainlink', 'crypto_prices'}:
|
|
1026
|
+
self.price._on_message(m)
|
|
1027
|
+
continue
|
|
1028
|
+
raw_type = m.get("event_type") or m.get("type")
|
|
1029
|
+
if not raw_type:
|
|
1030
|
+
continue
|
|
1031
|
+
msg_type = str(raw_type).lower()
|
|
1032
|
+
if msg_type in {"book", "price_change"}:
|
|
1033
|
+
self.book._on_message(m)
|
|
1034
|
+
elif msg_type == "order":
|
|
1035
|
+
self.orders._on_message(m)
|
|
1036
|
+
self.position._on_order(m)
|
|
1037
|
+
|
|
1038
|
+
elif msg_type == "trade":
|
|
1039
|
+
self.mytrade._on_message(m)
|
|
1040
|
+
# self.fill._on_trade(m)
|
|
1041
|
+
# self.position.on_trade(m)
|
|
1042
|
+
elif msg_type == 'orders_matched':
|
|
1043
|
+
payload = m.get("payload") or {}
|
|
1044
|
+
if not payload:
|
|
1045
|
+
continue
|
|
1046
|
+
trade_msg = dict(payload)
|
|
1047
|
+
if "asset_id" not in trade_msg and "asset" in trade_msg:
|
|
1048
|
+
trade_msg["asset_id"] = trade_msg["asset"]
|
|
1049
|
+
self.trade._on_message(trade_msg)
|
|
1050
|
+
|
|
1051
|
+
def onmessage_for_bbo(self, msg: Any, ws: ClientWebSocketResponse | None = None) -> None:
|
|
1052
|
+
# 判定msg是否为list
|
|
1053
|
+
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
1054
|
+
for m in lst_msg:
|
|
1055
|
+
raw_type = m.get("event_type") or m.get("type")
|
|
1056
|
+
if not raw_type:
|
|
1057
|
+
continue
|
|
1058
|
+
msg_type = str(raw_type).lower()
|
|
1059
|
+
if msg_type in {"book", "price_change"}:
|
|
1060
|
+
self.bbo._on_message(m)
|
|
1061
|
+
|
|
1062
|
+
def onmessage_for_last_trade(self, msg, ws = None):
|
|
1063
|
+
# 判定msg是否为list
|
|
1064
|
+
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
1065
|
+
for m in lst_msg:
|
|
1066
|
+
raw_type = m.get("event_type") or m.get("type")
|
|
1067
|
+
if not raw_type:
|
|
1068
|
+
continue
|
|
1069
|
+
msg_type = str(raw_type).lower()
|
|
1070
|
+
if msg_type == "last_trade_price":
|
|
1071
|
+
self.trade._on_message(m)
|