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.
Files changed (44) hide show
  1. {hyperquant-1.34 → hyperquant-1.36}/PKG-INFO +1 -1
  2. {hyperquant-1.34 → hyperquant-1.36}/pyproject.toml +1 -1
  3. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/auth.py +60 -2
  4. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/polymarket.py +257 -21
  5. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/polymarket.py +86 -26
  6. {hyperquant-1.34 → hyperquant-1.36}/uv.lock +1 -1
  7. {hyperquant-1.34 → hyperquant-1.36}/.gitignore +0 -0
  8. {hyperquant-1.34 → hyperquant-1.36}/README.md +0 -0
  9. {hyperquant-1.34 → hyperquant-1.36}/requirements-dev.lock +0 -0
  10. {hyperquant-1.34 → hyperquant-1.36}/requirements.lock +0 -0
  11. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/__init__.py +0 -0
  12. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/bitget.py +0 -0
  13. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/bitmart.py +0 -0
  14. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/coinw.py +0 -0
  15. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/deepcoin.py +0 -0
  16. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/edgex.py +0 -0
  17. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/hyperliquid.py +0 -0
  18. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lbank.py +0 -0
  19. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  20. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lib/hpstore.py +0 -0
  21. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  22. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lib/util.py +0 -0
  23. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/lighter.py +0 -0
  24. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/apexpro.py +0 -0
  25. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/bitget.py +0 -0
  26. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/bitmart.py +0 -0
  27. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/coinw.py +0 -0
  28. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/deepcoin.py +0 -0
  29. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/edgex.py +0 -0
  30. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  31. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/lbank.py +0 -0
  32. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/lighter.py +0 -0
  33. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/models/ourbit.py +0 -0
  34. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/ourbit.py +0 -0
  35. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/broker/ws.py +0 -0
  36. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/core.py +0 -0
  37. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/datavison/_util.py +0 -0
  38. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/datavison/binance.py +0 -0
  39. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/datavison/coinglass.py +0 -0
  40. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/datavison/okx.py +0 -0
  41. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/db.py +0 -0
  42. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/draw.py +0 -0
  43. {hyperquant-1.34 → hyperquant-1.36}/src/hyperquant/logkit.py +0 -0
  44. {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.34
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperquant"
3
- version = "1.34"
3
+ version = "1.36"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -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
- chain_id = session.__dict__.get("_polymarket_chain_id", 137)
504
- api_meta = session.__dict__.get("_polymarket_api_creds", {})
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 = ["asset"]
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.get("payload") or {}
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
- "asset": "28343900911859986062638672238388649192722581036873866155064320792236495661418",
590
- "bio": "",
591
- "conditionId": "0x730a593d5b63a39d316139a7f4bc1dd9fd05d858f6d96f659629c371bc81cd4d",
592
- "eventSlug": "btc-updown-15m-1762951500",
593
- "icon": "https://polymarket-upload.s3.us-east-2.amazonaws.com/BTC+fullsize.png",
594
- "name": "",
595
- "outcome": "Down",
596
- "outcomeIndex": 1,
597
- "price": 0.11,
598
- "profileImage": "",
599
- "proxyWallet": "0x7459f51304037Da66c47CeA58F078D7f603fD011",
600
- "pseudonym": "",
601
- "side": "SELL",
602
- "size": 84,
603
- "slug": "btc-updown-15m-1762951500",
604
- "timestamp": 1762952057,
605
- "title": "Bitcoin Up or Down - November 12, 7:45AM-8:00AM ET",
606
- "transactionHash": "0x6853cab8901779c494fe15f0eb3350aebfb182125347ca67b8bd9706babbddca"
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=self.store.onmessage,
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=5
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
- # https://gamma-api.polymarket.com/markets/slug/{slug}
319
- return await self._rest("GET", f"/slug/{slug}", host='https://gamma-api.polymarket.com/markets')
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
- def parse_field(value):
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
- if private_key:
1364
+ candidate_pk = private_key or existing_pk
1365
+ if candidate_pk:
1366
+ candidate_pk = str(candidate_pk)
1337
1367
  normalized_pk = (
1338
- private_key if private_key.startswith("0x") else f"0x{private_key}"
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
- if funder:
1350
- entry[2] = funder
1351
- self.funder = entry[2] or self.funder
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
@@ -945,7 +945,7 @@ wheels = [
945
945
 
946
946
  [[package]]
947
947
  name = "hyperquant"
948
- version = "1.33"
948
+ version = "1.35"
949
949
  source = { editable = "." }
950
950
  dependencies = [
951
951
  { name = "aiohttp" },
File without changes
File without changes
File without changes