hyperquant 1.3__tar.gz → 1.4__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.4}/PKG-INFO +1 -1
- {hyperquant-1.3 → hyperquant-1.4}/pyproject.toml +1 -1
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/auth.py +60 -2
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/polymarket.py +382 -11
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/polymarket.py +492 -47
- {hyperquant-1.3 → hyperquant-1.4}/uv.lock +1 -1
- {hyperquant-1.3 → hyperquant-1.4}/.gitignore +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/README.md +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/requirements-dev.lock +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/requirements.lock +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/core.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/db.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.3 → hyperquant-1.4}/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.4
|
|
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
|
|
@@ -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,21 @@ 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 = ["asset_id"]
|
|
229
|
+
_MAXLEN = 500
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
233
|
+
payload = msg or {}
|
|
234
|
+
if payload:
|
|
235
|
+
size = float(payload.get("size"))
|
|
236
|
+
price = float(payload.get("price"))
|
|
237
|
+
# share = size / price
|
|
238
|
+
# payload['shares'] = share
|
|
239
|
+
self._insert([payload])
|
|
157
240
|
|
|
158
241
|
class Book(DataStore):
|
|
159
242
|
"""Full depth order book keyed by Polymarket token id."""
|
|
@@ -197,19 +280,36 @@ class Book(DataStore):
|
|
|
197
280
|
normalized.append(record)
|
|
198
281
|
return normalized
|
|
199
282
|
|
|
283
|
+
def _purge_missing_levels(
|
|
284
|
+
self, *, symbol: str, side: str, new_levels: list[dict[str, Any]]
|
|
285
|
+
) -> None:
|
|
286
|
+
"""Remove levels no longer present in the latest snapshot."""
|
|
287
|
+
existing = self.find({"s": symbol, "S": side})
|
|
288
|
+
if not existing:
|
|
289
|
+
return
|
|
290
|
+
new_prices = {lvl["p"] for lvl in new_levels}
|
|
291
|
+
stale = [
|
|
292
|
+
{"s": symbol, "S": side, "p": level["p"]}
|
|
293
|
+
for level in existing
|
|
294
|
+
if level.get("p") not in new_prices
|
|
295
|
+
]
|
|
296
|
+
if stale:
|
|
297
|
+
self._delete(stale)
|
|
298
|
+
|
|
200
299
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
201
300
|
msg_type = msg.get("event_type")
|
|
202
301
|
if msg_type not in {"book", "price_change"}:
|
|
203
302
|
return
|
|
204
303
|
|
|
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
304
|
if msg_type == "book":
|
|
305
|
+
asset_id = msg.get("asset_id") or msg.get("token_id")
|
|
306
|
+
symbol, alias = self._alias(asset_id)
|
|
307
|
+
if symbol is None:
|
|
308
|
+
return
|
|
211
309
|
bids = self._normalize_levels(msg.get("bids"), side="b", symbol=symbol, alias=alias)
|
|
212
310
|
asks = self._normalize_levels(msg.get("asks"), side="a", symbol=symbol, alias=alias)
|
|
311
|
+
self._purge_missing_levels(symbol=symbol, side="b", new_levels=bids)
|
|
312
|
+
self._purge_missing_levels(symbol=symbol, side="a", new_levels=asks)
|
|
213
313
|
if bids:
|
|
214
314
|
self._insert(bids)
|
|
215
315
|
if asks:
|
|
@@ -220,6 +320,10 @@ class Book(DataStore):
|
|
|
220
320
|
updates: list[dict[str, Any]] = []
|
|
221
321
|
removals: list[dict[str, Any]] = []
|
|
222
322
|
for change in price_changes:
|
|
323
|
+
asset_id = change.get("asset_id") or change.get("token_id")
|
|
324
|
+
symbol, alias = self._alias(asset_id)
|
|
325
|
+
if symbol is None:
|
|
326
|
+
continue
|
|
223
327
|
side = "b" if change.get("side") == "BUY" else "a"
|
|
224
328
|
try:
|
|
225
329
|
price = float(change["price"])
|
|
@@ -252,6 +356,193 @@ class Book(DataStore):
|
|
|
252
356
|
limit=limit,
|
|
253
357
|
)
|
|
254
358
|
|
|
359
|
+
@dataclass
|
|
360
|
+
class _SideBook:
|
|
361
|
+
is_ask: bool
|
|
362
|
+
levels: dict[float, tuple[str, str]] = field(default_factory=dict)
|
|
363
|
+
heap: list[tuple[float, float]] = field(default_factory=list)
|
|
364
|
+
|
|
365
|
+
def clear(self) -> None:
|
|
366
|
+
self.levels.clear()
|
|
367
|
+
self.heap.clear()
|
|
368
|
+
|
|
369
|
+
def update_levels(
|
|
370
|
+
self, updates: Iterable[dict[str, Any]] | None, *, snapshot: bool
|
|
371
|
+
) -> None:
|
|
372
|
+
if updates is None:
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
if snapshot:
|
|
376
|
+
self.clear()
|
|
377
|
+
|
|
378
|
+
for entry in updates:
|
|
379
|
+
price, size = self._extract(entry)
|
|
380
|
+
price_val = self._to_float(price)
|
|
381
|
+
size_val = self._to_float(size)
|
|
382
|
+
if price_val is None or size_val is None:
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
if size_val <= 0:
|
|
386
|
+
self.levels.pop(price_val, None)
|
|
387
|
+
continue
|
|
388
|
+
|
|
389
|
+
self.levels[price_val] = (str(price), str(size))
|
|
390
|
+
priority = price_val if self.is_ask else -price_val
|
|
391
|
+
heappush(self.heap, (priority, price_val))
|
|
392
|
+
|
|
393
|
+
def best(self) -> tuple[str, str] | None:
|
|
394
|
+
while self.heap:
|
|
395
|
+
_, price = self.heap[0]
|
|
396
|
+
level = self.levels.get(price)
|
|
397
|
+
if level is not None:
|
|
398
|
+
return level
|
|
399
|
+
heappop(self.heap)
|
|
400
|
+
return None
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def _extract(entry: Any) -> tuple[Any, Any]:
|
|
404
|
+
if isinstance(entry, dict):
|
|
405
|
+
price = entry.get("price", entry.get("p"))
|
|
406
|
+
size = entry.get("size", entry.get("q"))
|
|
407
|
+
return price, size
|
|
408
|
+
if isinstance(entry, (list, tuple)) and len(entry) >= 2:
|
|
409
|
+
return entry[0], entry[1]
|
|
410
|
+
return None, None
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _to_float(value: Any) -> float | None:
|
|
414
|
+
try:
|
|
415
|
+
return float(value)
|
|
416
|
+
except (TypeError, ValueError):
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
class Price(DataStore):
|
|
420
|
+
_KEYS = ["s"]
|
|
421
|
+
|
|
422
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
423
|
+
payload = msg.get('payload') or {}
|
|
424
|
+
data = payload.get('data') or {}
|
|
425
|
+
symbol = payload.get('symbol')
|
|
426
|
+
|
|
427
|
+
if not symbol:
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
_next = self.get({'s': symbol}) or {}
|
|
431
|
+
_next_price = _next.get('p')
|
|
432
|
+
last_price = None
|
|
433
|
+
|
|
434
|
+
if data and isinstance(data, list):
|
|
435
|
+
last_price = data[-1].get('value')
|
|
436
|
+
if 'value' in payload:
|
|
437
|
+
last_price = payload.get('value')
|
|
438
|
+
|
|
439
|
+
if last_price is None:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
record = {'s': symbol, 'p': last_price}
|
|
443
|
+
key = {'s': symbol}
|
|
444
|
+
if self.get(key):
|
|
445
|
+
self._update([record])
|
|
446
|
+
else:
|
|
447
|
+
self._insert([record])
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class BBO(DataStore):
|
|
451
|
+
_KEYS = ["s", "S"]
|
|
452
|
+
|
|
453
|
+
def _init(self) -> None:
|
|
454
|
+
self._book: dict[str, dict[str, _SideBook]] = {}
|
|
455
|
+
self.id_to_alias: dict[str, str] = {}
|
|
456
|
+
|
|
457
|
+
def update_aliases(self, mapping: dict[str, str]) -> None:
|
|
458
|
+
if not mapping:
|
|
459
|
+
return
|
|
460
|
+
self.id_to_alias.update(mapping)
|
|
461
|
+
|
|
462
|
+
def _alias(self, asset_id: str | None) -> tuple[str, str | None] | tuple[None, None]:
|
|
463
|
+
if asset_id is None:
|
|
464
|
+
return None, None
|
|
465
|
+
alias = self.id_to_alias.get(asset_id)
|
|
466
|
+
return asset_id, alias
|
|
467
|
+
|
|
468
|
+
def _side(self, symbol: str, side: str) -> _SideBook:
|
|
469
|
+
symbol_book = self._book.setdefault(symbol, {})
|
|
470
|
+
side_book = symbol_book.get(side)
|
|
471
|
+
if side_book is None:
|
|
472
|
+
side_book = _SideBook(is_ask=(side == "a"))
|
|
473
|
+
symbol_book[side] = side_book
|
|
474
|
+
return side_book
|
|
475
|
+
|
|
476
|
+
def _sync_side(
|
|
477
|
+
self, symbol: str, side: str, best: tuple[str, str] | None, alias: str | None
|
|
478
|
+
) -> None:
|
|
479
|
+
key = {"s": symbol, "S": side}
|
|
480
|
+
current = self.get(key)
|
|
481
|
+
|
|
482
|
+
if best is None:
|
|
483
|
+
if current:
|
|
484
|
+
self._delete([key])
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
price, size = best
|
|
488
|
+
payload = {"s": symbol, "S": side, "p": price, "q": size}
|
|
489
|
+
if alias is not None:
|
|
490
|
+
payload["alias"] = alias
|
|
491
|
+
|
|
492
|
+
if current:
|
|
493
|
+
cur_price = current.get("p")
|
|
494
|
+
cur_size = current.get("q")
|
|
495
|
+
cur_alias = current.get("alias")
|
|
496
|
+
|
|
497
|
+
if cur_price == price:
|
|
498
|
+
# price unchanged -> only update quantities / alias changes
|
|
499
|
+
if cur_size != size or (alias is not None and cur_alias != alias):
|
|
500
|
+
self._update([payload])
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
# price changed -> delete old then insert new level to trigger change watchers
|
|
504
|
+
self._delete([key])
|
|
505
|
+
|
|
506
|
+
self._insert([payload])
|
|
507
|
+
|
|
508
|
+
def _from_price_changes(self, msg: dict[str, Any]) -> None:
|
|
509
|
+
price_changes = msg.get("price_changes") or []
|
|
510
|
+
touched: dict[str, str | None] = {}
|
|
511
|
+
for change in price_changes:
|
|
512
|
+
asset_id = change.get("asset_id") or change.get("token_id")
|
|
513
|
+
symbol, alias = self._alias(asset_id)
|
|
514
|
+
if symbol is None:
|
|
515
|
+
continue
|
|
516
|
+
side = "b" if str(change.get("side") or "").upper() == "BUY" else "a"
|
|
517
|
+
side_book = self._side(symbol, side)
|
|
518
|
+
side_book.update_levels([change], snapshot=False)
|
|
519
|
+
touched[symbol] = alias
|
|
520
|
+
|
|
521
|
+
for symbol, alias in touched.items():
|
|
522
|
+
asks = self._side(symbol, "a")
|
|
523
|
+
bids = self._side(symbol, "b")
|
|
524
|
+
self._sync_side(symbol, "a", asks.best(), alias)
|
|
525
|
+
self._sync_side(symbol, "b", bids.best(), alias)
|
|
526
|
+
|
|
527
|
+
def _from_snapshot(self, msg: dict[str, Any]) -> None:
|
|
528
|
+
asset_id = msg.get("asset_id") or msg.get("token_id")
|
|
529
|
+
symbol, alias = self._alias(asset_id)
|
|
530
|
+
if symbol is None:
|
|
531
|
+
return
|
|
532
|
+
asks = self._side(symbol, "a")
|
|
533
|
+
bids = self._side(symbol, "b")
|
|
534
|
+
asks.update_levels(msg.get("asks"), snapshot=True)
|
|
535
|
+
bids.update_levels(msg.get("bids"), snapshot=True)
|
|
536
|
+
self._sync_side(symbol, "a", asks.best(), alias)
|
|
537
|
+
self._sync_side(symbol, "b", bids.best(), alias)
|
|
538
|
+
|
|
539
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
540
|
+
msg_type = (msg.get("event_type") or msg.get("type") or "").lower()
|
|
541
|
+
if msg_type == "book":
|
|
542
|
+
self._from_snapshot(msg)
|
|
543
|
+
elif msg_type == "price_change":
|
|
544
|
+
self._from_price_changes(msg)
|
|
545
|
+
|
|
255
546
|
|
|
256
547
|
class Detail(DataStore):
|
|
257
548
|
"""Market metadata keyed by Polymarket token id."""
|
|
@@ -260,7 +551,7 @@ class Detail(DataStore):
|
|
|
260
551
|
|
|
261
552
|
@staticmethod
|
|
262
553
|
def _normalize_entry(market: dict[str, Any], token: dict[str, Any]) -> dict[str, Any]:
|
|
263
|
-
slug = market.get("
|
|
554
|
+
slug = market.get("slug")
|
|
264
555
|
outcome = token.get("outcome")
|
|
265
556
|
alias = slug if outcome is None else f"{slug}:{outcome}"
|
|
266
557
|
|
|
@@ -332,10 +623,14 @@ class Detail(DataStore):
|
|
|
332
623
|
|
|
333
624
|
for token in tokens:
|
|
334
625
|
normalized = self._normalize_entry(market, token)
|
|
626
|
+
slug: str = market.get("slug")
|
|
627
|
+
# 取最后一个'-'之前部分
|
|
628
|
+
base_slug = slug.rsplit("-", 1)[0] if slug else slug
|
|
335
629
|
# Add or update additional fields from market
|
|
336
630
|
normalized.update({
|
|
337
631
|
"condition_id": market.get("conditionId"),
|
|
338
632
|
"slug": market.get("slug"),
|
|
633
|
+
"base_slug": base_slug,
|
|
339
634
|
"end_date": market.get("endDate"),
|
|
340
635
|
"start_date": market.get("startDate"),
|
|
341
636
|
"icon": market.get("icon"),
|
|
@@ -362,11 +657,14 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
362
657
|
|
|
363
658
|
def _init(self) -> None:
|
|
364
659
|
self._create("book", datastore_class=Book)
|
|
660
|
+
self._create("bbo", datastore_class=BBO)
|
|
365
661
|
self._create("detail", datastore_class=Detail)
|
|
366
662
|
self._create("position", datastore_class=Position)
|
|
367
663
|
self._create("order", datastore_class=Order)
|
|
368
|
-
self._create("
|
|
664
|
+
self._create("mytrade", datastore_class=MyTrade)
|
|
369
665
|
self._create("fill", datastore_class=Fill)
|
|
666
|
+
self._create("trade", datastore_class=Trade)
|
|
667
|
+
self._create("price", datastore_class=Price)
|
|
370
668
|
|
|
371
669
|
@property
|
|
372
670
|
def book(self) -> Book:
|
|
@@ -497,7 +795,7 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
497
795
|
return self._get("order")
|
|
498
796
|
|
|
499
797
|
@property
|
|
500
|
-
def
|
|
798
|
+
def mytrade(self) -> MyTrade:
|
|
501
799
|
"""User trade stream keyed by trade id.
|
|
502
800
|
|
|
503
801
|
Columns include Polymarket websocket ``trade`` payloads, e.g.
|
|
@@ -517,6 +815,13 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
517
815
|
"""
|
|
518
816
|
|
|
519
817
|
return self._get("trade")
|
|
818
|
+
|
|
819
|
+
@property
|
|
820
|
+
def price(self) -> Price:
|
|
821
|
+
"""Price DataStore
|
|
822
|
+
_key: s
|
|
823
|
+
"""
|
|
824
|
+
return self._get("price")
|
|
520
825
|
|
|
521
826
|
@property
|
|
522
827
|
def fill(self) -> Fill:
|
|
@@ -541,11 +846,63 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
541
846
|
"""
|
|
542
847
|
|
|
543
848
|
return self._get("fill")
|
|
849
|
+
|
|
850
|
+
@property
|
|
851
|
+
def bbo(self) -> BBO:
|
|
852
|
+
"""Best Bid and Offer DataStore
|
|
853
|
+
_key: s (asset_id), S (side)
|
|
854
|
+
|
|
855
|
+
"""
|
|
856
|
+
return self._get("bbo")
|
|
857
|
+
|
|
858
|
+
@property
|
|
859
|
+
def trade(self) -> Trade:
|
|
860
|
+
"""
|
|
861
|
+
_key asset
|
|
862
|
+
MATCHED进行快速捕捉
|
|
863
|
+
.. code:: json
|
|
864
|
+
{
|
|
865
|
+
"asset_id": "52114319501245915516055106046884209969926127482827954674443846427813813222426",
|
|
866
|
+
"event_type": "trade",
|
|
867
|
+
"id": "28c4d2eb-bbea-40e7-a9f0-b2fdb56b2c2e",
|
|
868
|
+
"last_update": "1672290701",
|
|
869
|
+
"maker_orders": [
|
|
870
|
+
{
|
|
871
|
+
"asset_id": "52114319501245915516055106046884209969926127482827954674443846427813813222426",
|
|
872
|
+
"matched_amount": "10",
|
|
873
|
+
"order_id": "0xff354cd7ca7539dfa9c28d90943ab5779a4eac34b9b37a757d7b32bdfb11790b",
|
|
874
|
+
"outcome": "YES",
|
|
875
|
+
"owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
876
|
+
"price": "0.57"
|
|
877
|
+
}
|
|
878
|
+
],
|
|
879
|
+
"market": "0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af",
|
|
880
|
+
"matchtime": "1672290701",
|
|
881
|
+
"outcome": "YES",
|
|
882
|
+
"owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
883
|
+
"price": "0.57",
|
|
884
|
+
"side": "BUY",
|
|
885
|
+
"size": "10",
|
|
886
|
+
"status": "MATCHED",
|
|
887
|
+
"taker_order_id": "0x06bc63e346ed4ceddce9efd6b3af37c8f8f440c92fe7da6b2d0f9e4ccbc50c42",
|
|
888
|
+
"timestamp": "1672290701",
|
|
889
|
+
"trade_owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
890
|
+
"type": "TRADE"
|
|
891
|
+
}
|
|
892
|
+
"""
|
|
893
|
+
return self._get("trade")
|
|
894
|
+
|
|
544
895
|
|
|
545
896
|
def onmessage(self, msg: Any, ws: ClientWebSocketResponse | None = None) -> None:
|
|
546
897
|
# 判定msg是否为list
|
|
547
898
|
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
548
899
|
for m in lst_msg:
|
|
900
|
+
if m == '':
|
|
901
|
+
continue
|
|
902
|
+
topic = m.get("topic") or ""
|
|
903
|
+
if topic in {'crypto_prices_chainlink', 'crypto_prices'}:
|
|
904
|
+
self.price._on_message(m)
|
|
905
|
+
continue
|
|
549
906
|
raw_type = m.get("event_type") or m.get("type")
|
|
550
907
|
if not raw_type:
|
|
551
908
|
continue
|
|
@@ -553,7 +910,21 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
553
910
|
if msg_type in {"book", "price_change"}:
|
|
554
911
|
self.book._on_message(m)
|
|
555
912
|
elif msg_type == "order":
|
|
556
|
-
self.
|
|
913
|
+
self.orders._on_message(m)
|
|
557
914
|
elif msg_type == "trade":
|
|
558
915
|
self.trade._on_message(m)
|
|
559
916
|
self.fill._on_trade(m)
|
|
917
|
+
self.position.on_trade(m)
|
|
918
|
+
elif msg_type == 'orders_matched':
|
|
919
|
+
self.trade._on_message(m)
|
|
920
|
+
|
|
921
|
+
def onmessage_for_bbo(self, msg: Any, ws: ClientWebSocketResponse | None = None) -> None:
|
|
922
|
+
# 判定msg是否为list
|
|
923
|
+
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
924
|
+
for m in lst_msg:
|
|
925
|
+
raw_type = m.get("event_type") or m.get("type")
|
|
926
|
+
if not raw_type:
|
|
927
|
+
continue
|
|
928
|
+
msg_type = str(raw_type).lower()
|
|
929
|
+
if msg_type in {"book", "price_change"}:
|
|
930
|
+
self.bbo._on_message(m)
|