hyperquant 1.34__tar.gz → 1.36__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.34 → hyperquant-1.36}/PKG-INFO +1 -1
- {hyperquant-1.34 → hyperquant-1.36}/pyproject.toml +1 -1
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/auth.py +60 -2
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/polymarket.py +257 -21
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/polymarket.py +86 -26
- {hyperquant-1.34 → hyperquant-1.36}/uv.lock +1 -1
- {hyperquant-1.34 → hyperquant-1.36}/.gitignore +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/README.md +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/requirements-dev.lock +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/requirements.lock +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/core.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/db.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.34 → hyperquant-1.36}/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.36
|
|
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
|
|
@@ -18,6 +20,50 @@ class Position(DataStore):
|
|
|
18
20
|
def _on_response(self, msg: Item) -> None:
|
|
19
21
|
self._update(msg)
|
|
20
22
|
|
|
23
|
+
def on_trade(self, trade: Item) -> None:
|
|
24
|
+
status = str(trade.get("status") or "").upper()
|
|
25
|
+
if status not in {"MATCHED"}:
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
asset_id = trade.get("asset_id")
|
|
29
|
+
outcome = trade.get("outcome")
|
|
30
|
+
side = str(trade.get("side") or "").upper()
|
|
31
|
+
size_raw = trade.get("size")
|
|
32
|
+
|
|
33
|
+
if not asset_id or not outcome or side not in {"BUY", "SELL"}:
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
size = float(size_raw)
|
|
38
|
+
except (TypeError, ValueError):
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
key = {"asset": asset_id, "outcome": outcome}
|
|
42
|
+
existing = self.get(key) or {}
|
|
43
|
+
|
|
44
|
+
cur_size = float(existing.get("size") or 0.0)
|
|
45
|
+
cur_total_bought = float(existing.get("totalBought") or 0.0)
|
|
46
|
+
|
|
47
|
+
if side == "BUY":
|
|
48
|
+
new_size = cur_size + size
|
|
49
|
+
total_bought = cur_total_bought + size
|
|
50
|
+
else: # SELL
|
|
51
|
+
new_size = cur_size - size
|
|
52
|
+
total_bought = cur_total_bought
|
|
53
|
+
|
|
54
|
+
rec: dict[str, Any] = {
|
|
55
|
+
"asset": asset_id,
|
|
56
|
+
"outcome": outcome,
|
|
57
|
+
"side": side,
|
|
58
|
+
"size": new_size,
|
|
59
|
+
"totalBought": total_bought,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if existing:
|
|
63
|
+
self._update([rec])
|
|
64
|
+
else:
|
|
65
|
+
self._insert([rec])
|
|
66
|
+
|
|
21
67
|
|
|
22
68
|
class Fill(DataStore):
|
|
23
69
|
"""Fill records keyed by maker order id."""
|
|
@@ -157,13 +203,17 @@ class MyTrade(DataStore):
|
|
|
157
203
|
class Trade(DataStore):
|
|
158
204
|
"""User trades keyed by trade id."""
|
|
159
205
|
|
|
160
|
-
_KEYS = ["
|
|
206
|
+
_KEYS = ["asset_id"]
|
|
161
207
|
_MAXLEN = 500
|
|
162
208
|
|
|
163
209
|
|
|
164
210
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
165
|
-
payload = msg
|
|
211
|
+
payload = msg or {}
|
|
166
212
|
if payload:
|
|
213
|
+
size = float(payload.get("size"))
|
|
214
|
+
price = float(payload.get("price"))
|
|
215
|
+
# share = size / price
|
|
216
|
+
# payload['shares'] = share
|
|
167
217
|
self._insert([payload])
|
|
168
218
|
|
|
169
219
|
class Book(DataStore):
|
|
@@ -284,6 +334,163 @@ class Book(DataStore):
|
|
|
284
334
|
limit=limit,
|
|
285
335
|
)
|
|
286
336
|
|
|
337
|
+
@dataclass
|
|
338
|
+
class _SideBook:
|
|
339
|
+
is_ask: bool
|
|
340
|
+
levels: dict[float, tuple[str, str]] = field(default_factory=dict)
|
|
341
|
+
heap: list[tuple[float, float]] = field(default_factory=list)
|
|
342
|
+
|
|
343
|
+
def clear(self) -> None:
|
|
344
|
+
self.levels.clear()
|
|
345
|
+
self.heap.clear()
|
|
346
|
+
|
|
347
|
+
def update_levels(
|
|
348
|
+
self, updates: Iterable[dict[str, Any]] | None, *, snapshot: bool
|
|
349
|
+
) -> None:
|
|
350
|
+
if updates is None:
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
if snapshot:
|
|
354
|
+
self.clear()
|
|
355
|
+
|
|
356
|
+
for entry in updates:
|
|
357
|
+
price, size = self._extract(entry)
|
|
358
|
+
price_val = self._to_float(price)
|
|
359
|
+
size_val = self._to_float(size)
|
|
360
|
+
if price_val is None or size_val is None:
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
if size_val <= 0:
|
|
364
|
+
self.levels.pop(price_val, None)
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
self.levels[price_val] = (str(price), str(size))
|
|
368
|
+
priority = price_val if self.is_ask else -price_val
|
|
369
|
+
heappush(self.heap, (priority, price_val))
|
|
370
|
+
|
|
371
|
+
def best(self) -> tuple[str, str] | None:
|
|
372
|
+
while self.heap:
|
|
373
|
+
_, price = self.heap[0]
|
|
374
|
+
level = self.levels.get(price)
|
|
375
|
+
if level is not None:
|
|
376
|
+
return level
|
|
377
|
+
heappop(self.heap)
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
@staticmethod
|
|
381
|
+
def _extract(entry: Any) -> tuple[Any, Any]:
|
|
382
|
+
if isinstance(entry, dict):
|
|
383
|
+
price = entry.get("price", entry.get("p"))
|
|
384
|
+
size = entry.get("size", entry.get("q"))
|
|
385
|
+
return price, size
|
|
386
|
+
if isinstance(entry, (list, tuple)) and len(entry) >= 2:
|
|
387
|
+
return entry[0], entry[1]
|
|
388
|
+
return None, None
|
|
389
|
+
|
|
390
|
+
@staticmethod
|
|
391
|
+
def _to_float(value: Any) -> float | None:
|
|
392
|
+
try:
|
|
393
|
+
return float(value)
|
|
394
|
+
except (TypeError, ValueError):
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class BBO(DataStore):
|
|
399
|
+
_KEYS = ["s", "S"]
|
|
400
|
+
|
|
401
|
+
def _init(self) -> None:
|
|
402
|
+
self._book: dict[str, dict[str, _SideBook]] = {}
|
|
403
|
+
self.id_to_alias: dict[str, str] = {}
|
|
404
|
+
|
|
405
|
+
def update_aliases(self, mapping: dict[str, str]) -> None:
|
|
406
|
+
if not mapping:
|
|
407
|
+
return
|
|
408
|
+
self.id_to_alias.update(mapping)
|
|
409
|
+
|
|
410
|
+
def _alias(self, asset_id: str | None) -> tuple[str, str | None] | tuple[None, None]:
|
|
411
|
+
if asset_id is None:
|
|
412
|
+
return None, None
|
|
413
|
+
alias = self.id_to_alias.get(asset_id)
|
|
414
|
+
return asset_id, alias
|
|
415
|
+
|
|
416
|
+
def _side(self, symbol: str, side: str) -> _SideBook:
|
|
417
|
+
symbol_book = self._book.setdefault(symbol, {})
|
|
418
|
+
side_book = symbol_book.get(side)
|
|
419
|
+
if side_book is None:
|
|
420
|
+
side_book = _SideBook(is_ask=(side == "a"))
|
|
421
|
+
symbol_book[side] = side_book
|
|
422
|
+
return side_book
|
|
423
|
+
|
|
424
|
+
def _sync_side(
|
|
425
|
+
self, symbol: str, side: str, best: tuple[str, str] | None, alias: str | None
|
|
426
|
+
) -> None:
|
|
427
|
+
key = {"s": symbol, "S": side}
|
|
428
|
+
current = self.get(key)
|
|
429
|
+
|
|
430
|
+
if best is None:
|
|
431
|
+
if current:
|
|
432
|
+
self._delete([key])
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
price, size = best
|
|
436
|
+
payload = {"s": symbol, "S": side, "p": price, "q": size}
|
|
437
|
+
if alias is not None:
|
|
438
|
+
payload["alias"] = alias
|
|
439
|
+
|
|
440
|
+
if current:
|
|
441
|
+
cur_price = current.get("p")
|
|
442
|
+
cur_size = current.get("q")
|
|
443
|
+
cur_alias = current.get("alias")
|
|
444
|
+
|
|
445
|
+
if cur_price == price:
|
|
446
|
+
# price unchanged -> only update quantities / alias changes
|
|
447
|
+
if cur_size != size or (alias is not None and cur_alias != alias):
|
|
448
|
+
self._update([payload])
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
# price changed -> delete old then insert new level to trigger change watchers
|
|
452
|
+
self._delete([key])
|
|
453
|
+
|
|
454
|
+
self._insert([payload])
|
|
455
|
+
|
|
456
|
+
def _from_price_changes(self, msg: dict[str, Any]) -> None:
|
|
457
|
+
price_changes = msg.get("price_changes") or []
|
|
458
|
+
touched: dict[str, str | None] = {}
|
|
459
|
+
for change in price_changes:
|
|
460
|
+
asset_id = change.get("asset_id") or change.get("token_id")
|
|
461
|
+
symbol, alias = self._alias(asset_id)
|
|
462
|
+
if symbol is None:
|
|
463
|
+
continue
|
|
464
|
+
side = "b" if str(change.get("side") or "").upper() == "BUY" else "a"
|
|
465
|
+
side_book = self._side(symbol, side)
|
|
466
|
+
side_book.update_levels([change], snapshot=False)
|
|
467
|
+
touched[symbol] = alias
|
|
468
|
+
|
|
469
|
+
for symbol, alias in touched.items():
|
|
470
|
+
asks = self._side(symbol, "a")
|
|
471
|
+
bids = self._side(symbol, "b")
|
|
472
|
+
self._sync_side(symbol, "a", asks.best(), alias)
|
|
473
|
+
self._sync_side(symbol, "b", bids.best(), alias)
|
|
474
|
+
|
|
475
|
+
def _from_snapshot(self, msg: dict[str, Any]) -> None:
|
|
476
|
+
asset_id = msg.get("asset_id") or msg.get("token_id")
|
|
477
|
+
symbol, alias = self._alias(asset_id)
|
|
478
|
+
if symbol is None:
|
|
479
|
+
return
|
|
480
|
+
asks = self._side(symbol, "a")
|
|
481
|
+
bids = self._side(symbol, "b")
|
|
482
|
+
asks.update_levels(msg.get("asks"), snapshot=True)
|
|
483
|
+
bids.update_levels(msg.get("bids"), snapshot=True)
|
|
484
|
+
self._sync_side(symbol, "a", asks.best(), alias)
|
|
485
|
+
self._sync_side(symbol, "b", bids.best(), alias)
|
|
486
|
+
|
|
487
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
488
|
+
msg_type = (msg.get("event_type") or msg.get("type") or "").lower()
|
|
489
|
+
if msg_type == "book":
|
|
490
|
+
self._from_snapshot(msg)
|
|
491
|
+
elif msg_type == "price_change":
|
|
492
|
+
self._from_price_changes(msg)
|
|
493
|
+
|
|
287
494
|
|
|
288
495
|
class Detail(DataStore):
|
|
289
496
|
"""Market metadata keyed by Polymarket token id."""
|
|
@@ -398,6 +605,7 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
398
605
|
|
|
399
606
|
def _init(self) -> None:
|
|
400
607
|
self._create("book", datastore_class=Book)
|
|
608
|
+
self._create("bbo", datastore_class=BBO)
|
|
401
609
|
self._create("detail", datastore_class=Detail)
|
|
402
610
|
self._create("position", datastore_class=Position)
|
|
403
611
|
self._create("order", datastore_class=Order)
|
|
@@ -579,31 +787,47 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
579
787
|
|
|
580
788
|
return self._get("fill")
|
|
581
789
|
|
|
790
|
+
@property
|
|
791
|
+
def bbo(self) -> BBO:
|
|
792
|
+
"""Best Bid and Offer DataStore
|
|
793
|
+
_key: s (asset_id), S (side)
|
|
794
|
+
|
|
795
|
+
"""
|
|
796
|
+
return self._get("bbo")
|
|
797
|
+
|
|
582
798
|
@property
|
|
583
799
|
def trade(self) -> Trade:
|
|
584
800
|
"""
|
|
585
801
|
_key asset
|
|
586
|
-
|
|
802
|
+
MATCHED进行快速捕捉
|
|
587
803
|
.. code:: json
|
|
588
804
|
{
|
|
589
|
-
"
|
|
590
|
-
"
|
|
591
|
-
"
|
|
592
|
-
"
|
|
593
|
-
"
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
"
|
|
604
|
-
"
|
|
605
|
-
"
|
|
606
|
-
"
|
|
805
|
+
"asset_id": "52114319501245915516055106046884209969926127482827954674443846427813813222426",
|
|
806
|
+
"event_type": "trade",
|
|
807
|
+
"id": "28c4d2eb-bbea-40e7-a9f0-b2fdb56b2c2e",
|
|
808
|
+
"last_update": "1672290701",
|
|
809
|
+
"maker_orders": [
|
|
810
|
+
{
|
|
811
|
+
"asset_id": "52114319501245915516055106046884209969926127482827954674443846427813813222426",
|
|
812
|
+
"matched_amount": "10",
|
|
813
|
+
"order_id": "0xff354cd7ca7539dfa9c28d90943ab5779a4eac34b9b37a757d7b32bdfb11790b",
|
|
814
|
+
"outcome": "YES",
|
|
815
|
+
"owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
816
|
+
"price": "0.57"
|
|
817
|
+
}
|
|
818
|
+
],
|
|
819
|
+
"market": "0xbd31dc8a20211944f6b70f31557f1001557b59905b7738480ca09bd4532f84af",
|
|
820
|
+
"matchtime": "1672290701",
|
|
821
|
+
"outcome": "YES",
|
|
822
|
+
"owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
823
|
+
"price": "0.57",
|
|
824
|
+
"side": "BUY",
|
|
825
|
+
"size": "10",
|
|
826
|
+
"status": "MATCHED",
|
|
827
|
+
"taker_order_id": "0x06bc63e346ed4ceddce9efd6b3af37c8f8f440c92fe7da6b2d0f9e4ccbc50c42",
|
|
828
|
+
"timestamp": "1672290701",
|
|
829
|
+
"trade_owner": "9180014b-33c8-9240-a14b-bdca11c0a465",
|
|
830
|
+
"type": "TRADE"
|
|
607
831
|
}
|
|
608
832
|
"""
|
|
609
833
|
return self._get("trade")
|
|
@@ -624,5 +848,17 @@ class PolymarketDataStore(DataStoreCollection):
|
|
|
624
848
|
elif msg_type == "trade":
|
|
625
849
|
self.trade._on_message(m)
|
|
626
850
|
self.fill._on_trade(m)
|
|
851
|
+
self.position.on_trade(m)
|
|
627
852
|
elif msg_type == 'orders_matched':
|
|
628
853
|
self.trade._on_message(m)
|
|
854
|
+
|
|
855
|
+
def onmessage_for_bbo(self, msg: Any, ws: ClientWebSocketResponse | None = None) -> None:
|
|
856
|
+
# 判定msg是否为list
|
|
857
|
+
lst_msg = msg if isinstance(msg, list) else [msg]
|
|
858
|
+
for m in lst_msg:
|
|
859
|
+
raw_type = m.get("event_type") or m.get("type")
|
|
860
|
+
if not raw_type:
|
|
861
|
+
continue
|
|
862
|
+
msg_type = str(raw_type).lower()
|
|
863
|
+
if msg_type in {"book", "price_change"}:
|
|
864
|
+
self.bbo._on_message(m)
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
from contextlib import suppress
|
|
5
5
|
from datetime import UTC, datetime
|
|
6
6
|
from functools import lru_cache
|
|
7
|
+
import os
|
|
7
8
|
from typing import Any, Iterable, Iterator, Literal, Mapping, Sequence
|
|
8
9
|
|
|
9
10
|
import json
|
|
@@ -38,6 +39,14 @@ DEFAULT_POLYGON_RPCS = (
|
|
|
38
39
|
"https://rpc.ankr.com/polygon",
|
|
39
40
|
)
|
|
40
41
|
|
|
42
|
+
def parse_field(value):
|
|
43
|
+
"""尝试将字符串 JSON 转为对象,否则原样返回"""
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
try:
|
|
46
|
+
return json.loads(value)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
return value
|
|
49
|
+
return value
|
|
41
50
|
|
|
42
51
|
def _iter_offsets(window: int) -> Iterator[int]:
|
|
43
52
|
yield 0
|
|
@@ -94,7 +103,7 @@ class Polymarket:
|
|
|
94
103
|
self._ws_personal: pybotters.ws.WebSocketApp | None = None
|
|
95
104
|
self.auth = False
|
|
96
105
|
|
|
97
|
-
self._ensure_session_entry(private_key=private_key, funder=funder)
|
|
106
|
+
self._ensure_session_entry(private_key=private_key, funder=funder, chain_id=chain_id)
|
|
98
107
|
|
|
99
108
|
async def __aenter__(self) -> "Polymarket":
|
|
100
109
|
if self.auth:
|
|
@@ -214,12 +223,12 @@ class Polymarket:
|
|
|
214
223
|
self.store.orders._on_response(orders)
|
|
215
224
|
|
|
216
225
|
|
|
217
|
-
|
|
218
226
|
|
|
219
227
|
async def sub_books(
|
|
220
228
|
self,
|
|
221
229
|
token_ids: Sequence[str] | str,
|
|
222
230
|
wsapp: pybotters.ws.WebSocketApp | None = None,
|
|
231
|
+
only_bbo: bool = False,
|
|
223
232
|
) -> pybotters.ws.WebSocketApp:
|
|
224
233
|
"""Subscribe to public order-book updates for the provided token ids."""
|
|
225
234
|
|
|
@@ -227,11 +236,12 @@ class Polymarket:
|
|
|
227
236
|
payload = {"type": "market", "assets_ids": tokens}
|
|
228
237
|
if wsapp:
|
|
229
238
|
await wsapp.current_ws.send_json(payload)
|
|
230
|
-
|
|
239
|
+
hdrl_json = self.store.onmessage_for_bbo if only_bbo else self.store.onmessage
|
|
240
|
+
|
|
231
241
|
self._ws_public = self.client.ws_connect(
|
|
232
242
|
self.ws_public,
|
|
233
243
|
send_json=payload,
|
|
234
|
-
hdlr_json=
|
|
244
|
+
hdlr_json=hdrl_json,
|
|
235
245
|
)
|
|
236
246
|
await self._ws_public._event.wait()
|
|
237
247
|
return self._ws_public
|
|
@@ -266,6 +276,7 @@ class Polymarket:
|
|
|
266
276
|
self._ws_personal = self.client.ws_connect(
|
|
267
277
|
"wss://ws-subscriptions-clob.polymarket.com/ws/user",
|
|
268
278
|
hdlr_json=effective_cb,
|
|
279
|
+
heartbeat=30,
|
|
269
280
|
auth=None,
|
|
270
281
|
)
|
|
271
282
|
await self._ws_personal._event.wait()
|
|
@@ -298,7 +309,7 @@ class Polymarket:
|
|
|
298
309
|
wsapp = self.client.ws_connect(
|
|
299
310
|
RTS_DATA_ENDPOINT,
|
|
300
311
|
hdlr_str=callback,
|
|
301
|
-
heartbeat=
|
|
312
|
+
heartbeat=30
|
|
302
313
|
)
|
|
303
314
|
await wsapp._event.wait()
|
|
304
315
|
await wsapp.current_ws.send_json(payload)
|
|
@@ -314,9 +325,12 @@ class Polymarket:
|
|
|
314
325
|
return await self._rest("GET", f"/markets/{market_id}")
|
|
315
326
|
|
|
316
327
|
async def get_market_by_slug(self, slug: str) -> Any:
|
|
317
|
-
"""Fetch a market using its human-readable slug.
|
|
318
|
-
|
|
319
|
-
|
|
328
|
+
"""Fetch a market using its human-readable slug.
|
|
329
|
+
https://docs.polymarket.com/api-reference/markets/get-market-by-slug
|
|
330
|
+
"""
|
|
331
|
+
market:dict = await self._rest("GET", f"/slug/{slug}", host='https://gamma-api.polymarket.com/markets')
|
|
332
|
+
market = {k: parse_field(v) for k, v in market.items()}
|
|
333
|
+
return market
|
|
320
334
|
|
|
321
335
|
async def get_order_book(self, token_id: str) -> Any:
|
|
322
336
|
return await self._rest("GET", "/book", params={"token_id": token_id})
|
|
@@ -1169,14 +1183,7 @@ class Polymarket:
|
|
|
1169
1183
|
if not event:
|
|
1170
1184
|
continue
|
|
1171
1185
|
|
|
1172
|
-
|
|
1173
|
-
"""尝试将字符串 JSON 转为对象,否则原样返回"""
|
|
1174
|
-
if isinstance(value, str):
|
|
1175
|
-
try:
|
|
1176
|
-
return json.loads(value)
|
|
1177
|
-
except json.JSONDecodeError:
|
|
1178
|
-
return value
|
|
1179
|
-
return value
|
|
1186
|
+
|
|
1180
1187
|
|
|
1181
1188
|
event = {k: parse_field(v) for k, v in event.items()}
|
|
1182
1189
|
|
|
@@ -1316,6 +1323,7 @@ class Polymarket:
|
|
|
1316
1323
|
*,
|
|
1317
1324
|
private_key: str | None,
|
|
1318
1325
|
funder: str | None,
|
|
1326
|
+
chain_id: int | None,
|
|
1319
1327
|
) -> None:
|
|
1320
1328
|
session = getattr(self.client, "_session", None)
|
|
1321
1329
|
if session is None:
|
|
@@ -1328,27 +1336,66 @@ class Polymarket:
|
|
|
1328
1336
|
if not entry and not private_key:
|
|
1329
1337
|
return
|
|
1330
1338
|
|
|
1339
|
+
packed = entry[2] if len(entry) > 2 else None
|
|
1340
|
+
if not isinstance(packed, (list, tuple)):
|
|
1341
|
+
packed = None
|
|
1342
|
+
|
|
1343
|
+
def _packed_value(idx: int) -> Any | None:
|
|
1344
|
+
if packed is None:
|
|
1345
|
+
return None
|
|
1346
|
+
if idx >= len(packed):
|
|
1347
|
+
return None
|
|
1348
|
+
value = packed[idx]
|
|
1349
|
+
if isinstance(value, str):
|
|
1350
|
+
value = value.strip()
|
|
1351
|
+
return value or None
|
|
1352
|
+
|
|
1353
|
+
packed_api_key = _packed_value(0)
|
|
1354
|
+
packed_api_secret = _packed_value(1)
|
|
1355
|
+
packed_passphrase = _packed_value(2)
|
|
1356
|
+
packed_chain_id = _packed_value(3)
|
|
1357
|
+
packed_wallet = _packed_value(4)
|
|
1358
|
+
|
|
1331
1359
|
while len(entry) < 3:
|
|
1332
1360
|
entry.append("")
|
|
1333
1361
|
|
|
1334
|
-
existing_pk = entry[0]
|
|
1362
|
+
existing_pk = entry[0] if entry else None
|
|
1335
1363
|
normalized_pk: str | None = None
|
|
1336
|
-
|
|
1364
|
+
candidate_pk = private_key or existing_pk
|
|
1365
|
+
if candidate_pk:
|
|
1366
|
+
candidate_pk = str(candidate_pk)
|
|
1337
1367
|
normalized_pk = (
|
|
1338
|
-
|
|
1339
|
-
)
|
|
1340
|
-
elif existing_pk:
|
|
1341
|
-
normalized_pk = (
|
|
1342
|
-
existing_pk if existing_pk.startswith("0x") else f"0x{existing_pk}"
|
|
1368
|
+
candidate_pk if candidate_pk.startswith("0x") else f"0x{candidate_pk}"
|
|
1343
1369
|
)
|
|
1344
1370
|
|
|
1345
1371
|
if not normalized_pk:
|
|
1346
1372
|
raise RuntimeError("Polymarket需要钱包私钥 (apis['polymarket'][0])")
|
|
1347
1373
|
|
|
1348
1374
|
entry[0] = normalized_pk
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1375
|
+
|
|
1376
|
+
existing_wallet = entry[2] if isinstance(entry[2], str) and entry[2] else None
|
|
1377
|
+
effective_wallet = funder or packed_wallet or existing_wallet or self.funder
|
|
1378
|
+
if effective_wallet:
|
|
1379
|
+
entry[2] = effective_wallet
|
|
1380
|
+
self.funder = effective_wallet
|
|
1381
|
+
else:
|
|
1382
|
+
entry[2] = ""
|
|
1383
|
+
|
|
1384
|
+
derived_chain_id: int | None = None
|
|
1385
|
+
if packed_chain_id is not None:
|
|
1386
|
+
try:
|
|
1387
|
+
derived_chain_id = int(packed_chain_id)
|
|
1388
|
+
except (TypeError, ValueError):
|
|
1389
|
+
derived_chain_id = None
|
|
1390
|
+
if chain_id is None and derived_chain_id is not None:
|
|
1391
|
+
self.chain_id = derived_chain_id
|
|
1392
|
+
|
|
1393
|
+
if packed_api_key and packed_api_secret and packed_passphrase:
|
|
1394
|
+
session.__dict__["_polymarket_api_creds"] = {
|
|
1395
|
+
"api_key": packed_api_key,
|
|
1396
|
+
"api_secret": packed_api_secret,
|
|
1397
|
+
"api_passphrase": packed_passphrase,
|
|
1398
|
+
}
|
|
1352
1399
|
|
|
1353
1400
|
apis[API_NAME] = entry
|
|
1354
1401
|
session.__dict__["_apis"] = apis
|
|
@@ -1356,6 +1403,19 @@ class Polymarket:
|
|
|
1356
1403
|
session.__dict__["_polymarket_signature_type"] = self.signature_type
|
|
1357
1404
|
self.auth = True
|
|
1358
1405
|
|
|
1406
|
+
@staticmethod
|
|
1407
|
+
def load_poly_api():
|
|
1408
|
+
from dotenv import load_dotenv
|
|
1409
|
+
|
|
1410
|
+
load_dotenv()
|
|
1411
|
+
pk = os.getenv("PK")
|
|
1412
|
+
api_key = os.getenv("CLOB_API_KEY")
|
|
1413
|
+
api_secret = os.getenv("CLOB_API_SECRET")
|
|
1414
|
+
passphrase = os.getenv("CLOB_API_PASSPHRASE")
|
|
1415
|
+
chain_id = os.getenv("CHAIN_ID") or 137
|
|
1416
|
+
wallet_address = os.getenv("POLY_WALLET_ADDRESS")
|
|
1417
|
+
return [pk, "", (api_key, api_secret, passphrase, chain_id, wallet_address)]
|
|
1418
|
+
|
|
1359
1419
|
@lru_cache(maxsize=8)
|
|
1360
1420
|
def _get_web3(rpc_url: str):
|
|
1361
1421
|
from web3 import Web3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|