hyperquant 1.3__tar.gz → 1.44__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.3 → hyperquant-1.44}/PKG-INFO +2 -2
- {hyperquant-1.3 → hyperquant-1.44}/pyproject.toml +2 -2
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/auth.py +60 -2
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/polymarket.py +397 -12
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/polymarket.py +893 -53
- {hyperquant-1.3 → hyperquant-1.44}/uv.lock +2 -2
- {hyperquant-1.3 → hyperquant-1.44}/.gitignore +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/README.md +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/requirements-dev.lock +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/requirements.lock +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/core.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/db.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.3 → hyperquant-1.44}/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.44
|
|
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
|
|
@@ -22,7 +22,7 @@ Requires-Dist: lighter-sdk
|
|
|
22
22
|
Requires-Dist: numpy>=1.21.0
|
|
23
23
|
Requires-Dist: pandas>=2.2.3
|
|
24
24
|
Requires-Dist: py-clob-client>=0.28.0
|
|
25
|
-
Requires-Dist: pybotters>=1.
|
|
25
|
+
Requires-Dist: pybotters>=1.10
|
|
26
26
|
Requires-Dist: pyecharts>=2.0.8
|
|
27
27
|
Requires-Dist: python-dotenv>=1.2.1
|
|
28
28
|
Requires-Dist: web3>=7.14.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hyperquant"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.44"
|
|
4
4
|
description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "MissinA", email = "1421329142@qq.com" }
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
|
13
13
|
"cryptography>=44.0.2",
|
|
14
14
|
"numpy>=1.21.0", # Added numpy as a new dependency
|
|
15
15
|
"duckdb>=1.2.2",
|
|
16
|
-
"pybotters>=1.
|
|
16
|
+
"pybotters>=1.10",
|
|
17
17
|
"lighter-sdk",
|
|
18
18
|
"eth-account>=0.10.0",
|
|
19
19
|
"web3>=7.14.0",
|
|
@@ -499,9 +499,67 @@ class Auth:
|
|
|
499
499
|
if not creds:
|
|
500
500
|
return args
|
|
501
501
|
|
|
502
|
+
if isinstance(creds, tuple):
|
|
503
|
+
creds = list(creds)
|
|
504
|
+
session.__dict__["_apis"][api_name] = creds
|
|
505
|
+
|
|
502
506
|
private_key = creds[0] if len(creds) > 0 and creds[0] else None
|
|
503
|
-
|
|
504
|
-
|
|
507
|
+
if private_key:
|
|
508
|
+
pk_str = str(private_key)
|
|
509
|
+
if not pk_str.startswith("0x"):
|
|
510
|
+
pk_str = f"0x{pk_str}"
|
|
511
|
+
try:
|
|
512
|
+
session.__dict__["_apis"][api_name][0] = pk_str
|
|
513
|
+
except Exception:
|
|
514
|
+
pass
|
|
515
|
+
private_key = pk_str
|
|
516
|
+
|
|
517
|
+
packed_extra = creds[2] if len(creds) > 2 else None
|
|
518
|
+
packed_api_key = packed_api_secret = packed_passphrase = None
|
|
519
|
+
packed_chain_id = packed_wallet = None
|
|
520
|
+
if isinstance(packed_extra, (list, tuple)):
|
|
521
|
+
def _packed_value(idx: int):
|
|
522
|
+
if idx >= len(packed_extra):
|
|
523
|
+
return None
|
|
524
|
+
value = packed_extra[idx]
|
|
525
|
+
if isinstance(value, str):
|
|
526
|
+
value = value.strip()
|
|
527
|
+
return value or None
|
|
528
|
+
|
|
529
|
+
packed_api_key = _packed_value(0)
|
|
530
|
+
packed_api_secret = _packed_value(1)
|
|
531
|
+
packed_passphrase = _packed_value(2)
|
|
532
|
+
packed_chain_id = _packed_value(3)
|
|
533
|
+
packed_wallet = _packed_value(4)
|
|
534
|
+
elif isinstance(packed_extra, str):
|
|
535
|
+
packed_wallet = packed_extra or None
|
|
536
|
+
|
|
537
|
+
existing_chain_id = session.__dict__.get("_polymarket_chain_id")
|
|
538
|
+
if existing_chain_id is None:
|
|
539
|
+
chain_id = 137
|
|
540
|
+
if packed_chain_id is not None:
|
|
541
|
+
try:
|
|
542
|
+
chain_id = int(packed_chain_id)
|
|
543
|
+
except (TypeError, ValueError):
|
|
544
|
+
chain_id = 137
|
|
545
|
+
session.__dict__["_polymarket_chain_id"] = chain_id
|
|
546
|
+
else:
|
|
547
|
+
chain_id = existing_chain_id
|
|
548
|
+
|
|
549
|
+
api_meta = session.__dict__.get("_polymarket_api_creds") or {}
|
|
550
|
+
if (not api_meta.get("api_key") or not api_meta.get("api_secret") or not api_meta.get("api_passphrase")) and (
|
|
551
|
+
packed_api_key and packed_api_secret and packed_passphrase
|
|
552
|
+
):
|
|
553
|
+
api_meta = {
|
|
554
|
+
"api_key": packed_api_key,
|
|
555
|
+
"api_secret": packed_api_secret,
|
|
556
|
+
"api_passphrase": packed_passphrase,
|
|
557
|
+
}
|
|
558
|
+
session.__dict__["_polymarket_api_creds"] = api_meta
|
|
559
|
+
|
|
560
|
+
if packed_wallet and len(creds) > 2 and isinstance(creds[2], (list, tuple)):
|
|
561
|
+
creds[2] = packed_wallet
|
|
562
|
+
|
|
505
563
|
api_key = api_meta.get("api_key")
|
|
506
564
|
api_secret = api_meta.get("api_secret")
|
|
507
565
|
api_passphrase = api_meta.get("api_passphrase")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from heapq import heappop, heappush
|
|
4
6
|
from typing import TYPE_CHECKING, Any, Iterable
|
|
5
7
|
|
|
6
8
|
from pybotters.store import DataStore, DataStoreCollection
|
|
@@ -16,7 +18,73 @@ class Position(DataStore):
|
|
|
16
18
|
_KEYS = ["asset", "outcome"]
|
|
17
19
|
|
|
18
20
|
def _on_response(self, msg: Item) -> None:
|
|
19
|
-
|
|
21
|
+
if msg:
|
|
22
|
+
self._clear()
|
|
23
|
+
self._update(msg)
|
|
24
|
+
|
|
25
|
+
def on_trade(self, trade: Item) -> None:
|
|
26
|
+
status = str(trade.get("status") or "").upper()
|
|
27
|
+
if status not in {"MATCHED"}:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
asset_id = trade.get("asset_id")
|
|
31
|
+
outcome = trade.get("outcome")
|
|
32
|
+
side = str(trade.get("side") or "").upper()
|
|
33
|
+
size_raw = trade.get("size")
|
|
34
|
+
price_raw = trade.get("price")
|
|
35
|
+
|
|
36
|
+
if not asset_id or not outcome or side not in {"BUY", "SELL"}:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
size = float(size_raw)
|
|
41
|
+
except (TypeError, ValueError):
|
|
42
|
+
return
|
|
43
|
+
try:
|
|
44
|
+
price = float(price_raw)
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
price = None
|
|
47
|
+
|
|
48
|
+
key = {"asset": asset_id, "outcome": outcome}
|
|
49
|
+
existing = self.get(key) or {}
|
|
50
|
+
|
|
51
|
+
cur_size = float(existing.get("size") or 0.0)
|
|
52
|
+
cur_total_bought = float(existing.get("totalBought") or 0.0)
|
|
53
|
+
cur_avg_price = float(existing.get("avgPrice") or 0.0)
|
|
54
|
+
cur_cost = cur_size * cur_avg_price
|
|
55
|
+
|
|
56
|
+
if side == "BUY":
|
|
57
|
+
new_size = cur_size + size
|
|
58
|
+
total_bought = cur_total_bought + size
|
|
59
|
+
# 未拿到成交价时使用当前均价兜底,避免均价被拉低
|
|
60
|
+
effective_price = price if price is not None else cur_avg_price
|
|
61
|
+
new_cost = cur_cost + size * effective_price
|
|
62
|
+
else: # SELL
|
|
63
|
+
new_size = cur_size - size
|
|
64
|
+
total_bought = cur_total_bought
|
|
65
|
+
# 卖出按照当前均价释放成本
|
|
66
|
+
new_cost = cur_cost - min(size, cur_size) * cur_avg_price
|
|
67
|
+
|
|
68
|
+
if new_size <= 0:
|
|
69
|
+
new_size = 0.0
|
|
70
|
+
avg_price = 0.0
|
|
71
|
+
new_cost = 0.0
|
|
72
|
+
else:
|
|
73
|
+
avg_price = max(new_cost, 0.0) / new_size
|
|
74
|
+
|
|
75
|
+
rec: dict[str, Any] = {
|
|
76
|
+
"asset": asset_id,
|
|
77
|
+
"outcome": outcome,
|
|
78
|
+
"side": side,
|
|
79
|
+
"size": new_size,
|
|
80
|
+
"totalBought": total_bought,
|
|
81
|
+
"avgPrice": avg_price,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if existing:
|
|
85
|
+
self._update([rec])
|
|
86
|
+
else:
|
|
87
|
+
self._insert([rec])
|
|
20
88
|
|
|
21
89
|
|
|
22
90
|
class Fill(DataStore):
|
|
@@ -123,7 +191,7 @@ class Order(DataStore):
|
|
|
123
191
|
self._insert([norm])
|
|
124
192
|
|
|
125
193
|
|
|
126
|
-
class
|
|
194
|
+
class MyTrade(DataStore):
|
|
127
195
|
"""User trades keyed by trade id."""
|
|
128
196
|
|
|
129
197
|
_KEYS = ["id"]
|
|
@@ -154,6 +222,26 @@ class Trade(DataStore):
|
|
|
154
222
|
else:
|
|
155
223
|
self._insert([normalized])
|
|
156
224
|
|
|
225
|
+
class Trade(DataStore):
|
|
226
|
+
"""User trades keyed by trade id."""
|
|
227
|
+
|
|
228
|
+
_KEYS = ["hash"]
|
|
229
|
+
_MAXLEN = 500
|
|
230
|
+
|
|
231
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
232
|
+
payload = msg or {}
|
|
233
|
+
if payload:
|
|
234
|
+
if payload.get("event_type") == "last_trade_price":
|
|
235
|
+
transaction_hash = payload.get("transaction_hash")
|
|
236
|
+
if transaction_hash:
|
|
237
|
+
payload.update({"hash": transaction_hash})
|
|
238
|
+
payload.pop("transaction_hash", None)
|
|
239
|
+
else:
|
|
240
|
+
if payload.get("transactionHash"):
|
|
241
|
+
payload.update({"hash": payload.get("transactionHash")})
|
|
242
|
+
payload.pop("transactionHash", None)
|
|
243
|
+
|
|
244
|
+
self._insert([payload])
|
|
157
245
|
|
|
158
246
|
class Book(DataStore):
|
|
159
247
|
"""Full depth order book keyed by Polymarket token id."""
|
|
@@ -197,19 +285,36 @@ class Book(DataStore):
|
|
|
197
285
|
normalized.append(record)
|
|
198
286
|
return normalized
|
|
199
287
|
|
|
288
|
+
def _purge_missing_levels(
|
|
289
|
+
self, *, symbol: str, side: str, new_levels: list[dict[str, Any]]
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Remove levels no longer present in the latest snapshot."""
|
|
292
|
+
existing = self.find({"s": symbol, "S": side})
|
|
293
|
+
if not existing:
|
|
294
|
+
return
|
|
295
|
+
new_prices = {lvl["p"] for lvl in new_levels}
|
|
296
|
+
stale = [
|
|
297
|
+
{"s": symbol, "S": side, "p": level["p"]}
|
|
298
|
+
for level in existing
|
|
299
|
+
if level.get("p") not in new_prices
|
|
300
|
+
]
|
|
301
|
+
if stale:
|
|
302
|
+
self._delete(stale)
|
|
303
|
+
|
|
200
304
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
201
305
|
msg_type = msg.get("event_type")
|
|
202
306
|
if msg_type not in {"book", "price_change"}:
|
|
203
307
|
return
|
|
204
308
|
|
|
205
|
-
asset_id = msg.get("asset_id") or msg.get("token_id")
|
|
206
|
-
symbol, alias = self._alias(asset_id)
|
|
207
|
-
if symbol is None:
|
|
208
|
-
return
|
|
209
|
-
|
|
210
309
|
if msg_type == "book":
|
|
310
|
+
asset_id = msg.get("asset_id") or msg.get("token_id")
|
|
311
|
+
symbol, alias = self._alias(asset_id)
|
|
312
|
+
if symbol is None:
|
|
313
|
+
return
|
|
211
314
|
bids = self._normalize_levels(msg.get("bids"), side="b", symbol=symbol, alias=alias)
|
|
212
315
|
asks = self._normalize_levels(msg.get("asks"), side="a", symbol=symbol, alias=alias)
|
|
316
|
+
self._purge_missing_levels(symbol=symbol, side="b", new_levels=bids)
|
|
317
|
+
self._purge_missing_levels(symbol=symbol, side="a", new_levels=asks)
|
|
213
318
|
if bids:
|
|
214
319
|
self._insert(bids)
|
|
215
320
|
if asks:
|
|
@@ -220,6 +325,10 @@ class Book(DataStore):
|
|
|
220
325
|
updates: list[dict[str, Any]] = []
|
|
221
326
|
removals: list[dict[str, Any]] = []
|
|
222
327
|
for change in price_changes:
|
|
328
|
+
asset_id = change.get("asset_id") or change.get("token_id")
|
|
329
|
+
symbol, alias = self._alias(asset_id)
|
|
330
|
+
if symbol is None:
|
|
331
|
+
continue
|
|
223
332
|
side = "b" if change.get("side") == "BUY" else "a"
|
|
224
333
|
try:
|
|
225
334
|
price = float(change["price"])
|
|
@@ -252,6 +361,193 @@ class Book(DataStore):
|
|
|
252
361
|
limit=limit,
|
|
253
362
|
)
|
|
254
363
|
|
|
364
|
+
@dataclass
|
|
365
|
+
class _SideBook:
|
|
366
|
+
is_ask: bool
|
|
367
|
+
levels: dict[float, tuple[str, str]] = field(default_factory=dict)
|
|
368
|
+
heap: list[tuple[float, float]] = field(default_factory=list)
|
|
369
|
+
|
|
370
|
+
def clear(self) -> None:
|
|
371
|
+
self.levels.clear()
|
|
372
|
+
self.heap.clear()
|
|
373
|
+
|
|
374
|
+
def update_levels(
|
|
375
|
+
self, updates: Iterable[dict[str, Any]] | None, *, snapshot: bool
|
|
376
|
+
) -> None:
|
|
377
|
+
if updates is None:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
if snapshot:
|
|
381
|
+
self.clear()
|
|
382
|
+
|
|
383
|
+
for entry in updates:
|
|
384
|
+
price, size = self._extract(entry)
|
|
385
|
+
price_val = self._to_float(price)
|
|
386
|
+
size_val = self._to_float(size)
|
|
387
|
+
if price_val is None or size_val is None:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
if size_val <= 0:
|
|
391
|
+
self.levels.pop(price_val, None)
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
self.levels[price_val] = (str(price), str(size))
|
|
395
|
+
priority = price_val if self.is_ask else -price_val
|
|
396
|
+
heappush(self.heap, (priority, price_val))
|
|
397
|
+
|
|
398
|
+
def best(self) -> tuple[str, str] | None:
|
|
399
|
+
while self.heap:
|
|
400
|
+
_, price = self.heap[0]
|
|
401
|
+
level = self.levels.get(price)
|
|
402
|
+
if level is not None:
|
|
403
|
+
return level
|
|
404
|
+
heappop(self.heap)
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
@staticmethod
|
|
408
|
+
def _extract(entry: Any) -> tuple[Any, Any]:
|
|
409
|
+
if isinstance(entry, dict):
|
|
410
|
+
price = entry.get("price", entry.get("p"))
|
|
411
|
+
size = entry.get("size", entry.get("q"))
|
|
412
|
+
return price, size
|
|
413
|
+
if isinstance(entry, (list, tuple)) and len(entry) >= 2:
|
|
414
|
+
return entry[0], entry[1]
|
|
415
|
+
return None, None
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
def _to_float(value: Any) -> float | None:
|
|
419
|
+
try:
|
|
420
|
+
return float(value)
|
|
421
|
+
except (TypeError, ValueError):
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
class Price(DataStore):
|
|
425
|
+
_KEYS = ["s"]
|
|
426
|
+
|
|
427
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
428
|
+
payload = msg.get('payload') or {}
|
|
429
|
+
data = payload.get('data') or {}
|
|
430
|
+
symbol = payload.get('symbol')
|
|
431
|
+
|
|
432
|
+
if not symbol:
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
_next = self.get({'s': symbol}) or {}
|
|
436
|
+
_next_price = _next.get('p')
|
|
437
|
+
last_price = None
|
|
438
|
+
|
|
439
|
+
if data and isinstance(data, list):
|
|
440
|
+
last_price = data[-1].get('value')
|
|
441
|
+
if 'value' in payload:
|
|
442
|
+
last_price = payload.get('value')
|
|
443
|
+
|
|
444
|
+
if last_price is None:
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
record = {'s': symbol, 'p': last_price}
|
|
448
|
+
key = {'s': symbol}
|
|
449
|
+
if self.get(key):
|
|
450
|
+
self._update([record])
|
|
451
|
+
else:
|
|
452
|
+
self._insert([record])
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class BBO(DataStore):
|
|
456
|
+
_KEYS = ["s", "S"]
|
|
457
|
+
|
|
458
|
+
def _init(self) -> None:
|
|
459
|
+
self._book: dict[str, dict[str, _SideBook]] = {}
|
|
460
|
+
self.id_to_alias: dict[str, str] = {}
|
|
461
|
+
|
|
462
|
+
def update_aliases(self, mapping: dict[str, str]) -> None:
|
|
463
|
+
if not mapping:
|
|
464
|
+
return
|
|
465
|
+
self.id_to_alias.update(mapping)
|
|
466
|
+
|
|
467
|
+
def _alias(self, asset_id: str | None) -> tuple[str, str | None] | tuple[None, None]:
|
|
468
|
+
if asset_id is None:
|
|
469
|
+
return None, None
|
|
470
|
+
alias = self.id_to_alias.get(asset_id)
|
|
471
|
+
return asset_id, alias
|
|
472
|
+
|
|
473
|
+
def _side(self, symbol: str, side: str) -> _SideBook:
|
|
474
|
+
symbol_book = self._book.setdefault(symbol, {})
|
|
475
|
+
side_book = symbol_book.get(side)
|
|
476
|
+
if side_book is None:
|
|
477
|
+
side_book = _SideBook(is_ask=(side == "a"))
|
|
478
|
+
symbol_book[side] = side_book
|
|
479
|
+
return side_book
|
|
480
|
+
|
|
481
|
+
def _sync_side(
|
|
482
|
+
self, symbol: str, side: str, best: tuple[str, str] | None, alias: str | None
|
|
483
|
+
) -> None:
|
|
484
|
+
key = {"s": symbol, "S": side}
|
|
485
|
+
current = self.get(key)
|
|
486
|
+
|
|
487
|
+
if best is None:
|
|
488
|
+
if current:
|
|
489
|
+
self._delete([key])
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
price, size = best
|
|
493
|
+
payload = {"s": symbol, "S": side, "p": price, "q": size}
|
|
494
|
+
if alias is not None:
|
|
495
|
+
payload["alias"] = alias
|
|
496
|
+
|
|
497
|
+
if current:
|
|
498
|
+
cur_price = current.get("p")
|
|
499
|
+
cur_size = current.get("q")
|
|
500
|
+
cur_alias = current.get("alias")
|
|
501
|
+
|
|
502
|
+
if cur_price == price:
|
|
503
|
+
# price unchanged -> only update quantities / alias changes
|
|
504
|
+
if cur_size != size or (alias is not None and cur_alias != alias):
|
|
505
|
+
self._update([payload])
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
# price changed -> delete old then insert new level to trigger change watchers
|
|
509
|
+
self._delete([key])
|
|
510
|
+
|
|
511
|
+
self._insert([payload])
|
|
512
|
+
|
|
513
|
+
def _from_price_changes(self, msg: dict[str, Any]) -> None:
|
|
514
|
+
price_changes = msg.get("price_changes") or []
|
|
515
|
+
touched: dict[str, str | None] = {}
|
|
516
|
+
for change in price_changes:
|
|
517
|
+
asset_id = change.get("asset_id") or change.get("token_id")
|
|
518
|
+
symbol, alias = self._alias(asset_id)
|
|
519
|
+
if symbol is None:
|
|
520
|
+
continue
|
|
521
|
+
side = "b" if str(change.get("side") or "").upper() == "BUY" else "a"
|
|
522
|
+
side_book = self._side(symbol, side)
|
|
523
|
+
side_book.update_levels([change], snapshot=False)
|
|
524
|
+
touched[symbol] = alias
|
|
525
|
+
|
|
526
|
+
for symbol, alias in touched.items():
|
|
527
|
+
asks = self._side(symbol, "a")
|
|
528
|
+
bids = self._side(symbol, "b")
|
|
529
|
+
self._sync_side(symbol, "a", asks.best(), alias)
|
|
530
|
+
self._sync_side(symbol, "b", bids.best(), alias)
|
|
531
|
+
|
|
532
|
+
def _from_snapshot(self, msg: dict[str, Any]) -> None:
|
|
533
|
+
asset_id = msg.get("asset_id") or msg.get("token_id")
|
|
534
|
+
symbol, alias = self._alias(asset_id)
|
|
535
|
+
if symbol is None:
|
|
536
|
+
return
|
|
537
|
+
asks = self._side(symbol, "a")
|
|
538
|
+
bids = self._side(symbol, "b")
|
|
539
|
+
asks.update_levels(msg.get("asks"), snapshot=True)
|
|
540
|
+
bids.update_levels(msg.get("bids"), snapshot=True)
|
|
541
|
+
self._sync_side(symbol, "a", asks.best(), alias)
|
|
542
|
+
self._sync_side(symbol, "b", bids.best(), alias)
|
|
543
|
+
|
|
544
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
545
|
+
msg_type = (msg.get("event_type") or msg.get("type") or "").lower()
|
|
546
|
+
if msg_type == "book":
|
|
547
|
+
self._from_snapshot(msg)
|
|
548
|
+
elif msg_type == "price_change":
|
|
549
|
+
self._from_price_changes(msg)
|
|
550
|
+
|
|
255
551
|
|
|
256
552
|
class Detail(DataStore):
|
|
257
553
|
"""Market metadata keyed by Polymarket token id."""
|
|
@@ -260,7 +556,7 @@ class Detail(DataStore):
|
|
|
260
556
|
|
|
261
557
|
@staticmethod
|
|
262
558
|
def _normalize_entry(market: dict[str, Any], token: dict[str, Any]) -> dict[str, Any]:
|
|
263
|
-
slug = market.get("
|
|
559
|
+
slug = market.get("slug")
|
|
264
560
|
outcome = token.get("outcome")
|
|
265
561
|
alias = slug if outcome is None else f"{slug}:{outcome}"
|
|
266
562
|
|
|
@@ -332,10 +628,14 @@ class Detail(DataStore):
|
|
|
332
628
|
|
|
333
629
|
for token in tokens:
|
|
334
630
|
normalized = self._normalize_entry(market, token)
|
|
631
|
+
slug: str = market.get("slug")
|
|
632
|
+
# 取最后一个'-'之前部分
|
|
633
|
+
base_slug = slug.rsplit("-", 1)[0] if slug else slug
|
|
335
634
|
# Add or update additional fields from market
|
|
336
635
|
normalized.update({
|
|
337
636
|
"condition_id": market.get("conditionId"),
|
|
338
637
|
"slug": market.get("slug"),
|
|
638
|
+
"base_slug": base_slug,
|
|
339
639
|
"end_date": market.get("endDate"),
|
|
340
640
|
"start_date": market.get("startDate"),
|
|
341
641
|
"icon": market.get("icon"),
|
|
@@ -362,11 +662,14 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
362
662
|
|
|
363
663
|
def _init(self) -> None:
|
|
364
664
|
self._create("book", datastore_class=Book)
|
|
665
|
+
self._create("bbo", datastore_class=BBO)
|
|
365
666
|
self._create("detail", datastore_class=Detail)
|
|
366
667
|
self._create("position", datastore_class=Position)
|
|
367
668
|
self._create("order", datastore_class=Order)
|
|
368
|
-
self._create("
|
|
669
|
+
self._create("mytrade", datastore_class=MyTrade)
|
|
369
670
|
self._create("fill", datastore_class=Fill)
|
|
671
|
+
self._create("trade", datastore_class=Trade)
|
|
672
|
+
self._create("price", datastore_class=Price)
|
|
370
673
|
|
|
371
674
|
@property
|
|
372
675
|
def book(self) -> Book:
|
|
@@ -497,7 +800,7 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
497
800
|
return self._get("order")
|
|
498
801
|
|
|
499
802
|
@property
|
|
500
|
-
def
|
|
803
|
+
def mytrade(self) -> MyTrade:
|
|
501
804
|
"""User trade stream keyed by trade id.
|
|
502
805
|
|
|
503
806
|
Columns include Polymarket websocket ``trade`` payloads, e.g.
|
|
@@ -517,6 +820,13 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
517
820
|
"""
|
|
518
821
|
|
|
519
822
|
return self._get("trade")
|
|
823
|
+
|
|
824
|
+
@property
|
|
825
|
+
def price(self) -> Price:
|
|
826
|
+
"""Price DataStore
|
|
827
|
+
_key: s
|
|
828
|
+
"""
|
|
829
|
+
return self._get("price")
|
|
520
830
|
|
|
521
831
|
@property
|
|
522
832
|
def fill(self) -> Fill:
|
|
@@ -541,11 +851,55 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
541
851
|
"""
|
|
542
852
|
|
|
543
853
|
return self._get("fill")
|
|
854
|
+
|
|
855
|
+
@property
|
|
856
|
+
def bbo(self) -> BBO:
|
|
857
|
+
"""Best Bid and Offer DataStore
|
|
858
|
+
_key: s (asset_id), S (side)
|
|
859
|
+
|
|
860
|
+
"""
|
|
861
|
+
return self._get("bbo")
|
|
862
|
+
|
|
863
|
+
@property
|
|
864
|
+
def trade(self) -> Trade:
|
|
865
|
+
"""
|
|
866
|
+
_key asset
|
|
867
|
+
MATCHED进行快速捕捉
|
|
868
|
+
.. code:: json
|
|
869
|
+
{
|
|
870
|
+
"asset": "12819879685513143002408869746992985182419696851931617234615358342350852997413",
|
|
871
|
+
"bio": "",
|
|
872
|
+
"conditionId": "0xea609d2c6bc2cb20e328be7c89f258b84b35bbe119b44e0a2cfc5f15e6642b3b",
|
|
873
|
+
"eventSlug": "btc-updown-15m-1763865000",
|
|
874
|
+
"icon": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
|
|
875
|
+
"name": "infusion",
|
|
876
|
+
"outcome": "Up",
|
|
877
|
+
"outcomeIndex": 0,
|
|
878
|
+
"price": 0.7,
|
|
879
|
+
"profileImage": "",
|
|
880
|
+
"proxyWallet": "0x2C060830B6F6B43174b1Cf8B4475db07703c1543",
|
|
881
|
+
"pseudonym": "Frizzy-Graduate",
|
|
882
|
+
"side": "BUY",
|
|
883
|
+
"size": 5,
|
|
884
|
+
"slug": "btc-updown-15m-1763865000",
|
|
885
|
+
"timestamp": 1763865085,
|
|
886
|
+
"title": "Bitcoin Up or Down - November 22, 9:30PM-9:45PM ET",
|
|
887
|
+
"hash": "0xddea11d695e811686f83379d9269accf1be581fbcb542809c6c67a3cc3002488"
|
|
888
|
+
}
|
|
889
|
+
"""
|
|
890
|
+
return self._get("trade")
|
|
891
|
+
|
|
544
892
|
|
|
545
893
|
def onmessage(self, msg: Any, ws: ClientWebSocketResponse | None = None) -> None:
|
|
546
894
|
# 判定msg是否为list
|
|
547
895
|
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
548
896
|
for m in lst_msg:
|
|
897
|
+
if m == '':
|
|
898
|
+
continue
|
|
899
|
+
topic = m.get("topic") or ""
|
|
900
|
+
if topic in {'crypto_prices_chainlink', 'crypto_prices'}:
|
|
901
|
+
self.price._on_message(m)
|
|
902
|
+
continue
|
|
549
903
|
raw_type = m.get("event_type") or m.get("type")
|
|
550
904
|
if not raw_type:
|
|
551
905
|
continue
|
|
@@ -553,7 +907,38 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
553
907
|
if msg_type in {"book", "price_change"}:
|
|
554
908
|
self.book._on_message(m)
|
|
555
909
|
elif msg_type == "order":
|
|
556
|
-
self.
|
|
910
|
+
self.orders._on_message(m)
|
|
557
911
|
elif msg_type == "trade":
|
|
558
|
-
self.
|
|
912
|
+
self.mytrade._on_message(m)
|
|
559
913
|
self.fill._on_trade(m)
|
|
914
|
+
self.position.on_trade(m)
|
|
915
|
+
elif msg_type == 'orders_matched':
|
|
916
|
+
payload = m.get("payload") or {}
|
|
917
|
+
if not payload:
|
|
918
|
+
continue
|
|
919
|
+
trade_msg = dict(payload)
|
|
920
|
+
if "asset_id" not in trade_msg and "asset" in trade_msg:
|
|
921
|
+
trade_msg["asset_id"] = trade_msg["asset"]
|
|
922
|
+
self.trade._on_message(trade_msg)
|
|
923
|
+
|
|
924
|
+
def onmessage_for_bbo(self, msg: Any, ws: ClientWebSocketResponse | None = None) -> None:
|
|
925
|
+
# 判定msg是否为list
|
|
926
|
+
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
927
|
+
for m in lst_msg:
|
|
928
|
+
raw_type = m.get("event_type") or m.get("type")
|
|
929
|
+
if not raw_type:
|
|
930
|
+
continue
|
|
931
|
+
msg_type = str(raw_type).lower()
|
|
932
|
+
if msg_type in {"book", "price_change"}:
|
|
933
|
+
self.bbo._on_message(m)
|
|
934
|
+
|
|
935
|
+
def onmessage_for_last_trade(self, msg, ws = None):
|
|
936
|
+
# 判定msg是否为list
|
|
937
|
+
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
938
|
+
for m in lst_msg:
|
|
939
|
+
raw_type = m.get("event_type") or m.get("type")
|
|
940
|
+
if not raw_type:
|
|
941
|
+
continue
|
|
942
|
+
msg_type = str(raw_type).lower()
|
|
943
|
+
if msg_type == "last_trade_price":
|
|
944
|
+
self.trade._on_message(m)
|