hyperquant 1.35__tar.gz → 1.37__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.35 → hyperquant-1.37}/PKG-INFO +1 -1
  2. {hyperquant-1.35 → hyperquant-1.37}/pyproject.toml +1 -1
  3. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/polymarket.py +179 -0
  4. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/polymarket.py +53 -16
  5. {hyperquant-1.35 → hyperquant-1.37}/uv.lock +1 -1
  6. {hyperquant-1.35 → hyperquant-1.37}/.gitignore +0 -0
  7. {hyperquant-1.35 → hyperquant-1.37}/README.md +0 -0
  8. {hyperquant-1.35 → hyperquant-1.37}/requirements-dev.lock +0 -0
  9. {hyperquant-1.35 → hyperquant-1.37}/requirements.lock +0 -0
  10. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/__init__.py +0 -0
  11. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/auth.py +0 -0
  12. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/bitget.py +0 -0
  13. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/bitmart.py +0 -0
  14. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/coinw.py +0 -0
  15. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/deepcoin.py +0 -0
  16. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/edgex.py +0 -0
  17. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/hyperliquid.py +0 -0
  18. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/lbank.py +0 -0
  19. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  20. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/lib/hpstore.py +0 -0
  21. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  22. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/lib/util.py +0 -0
  23. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/lighter.py +0 -0
  24. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/apexpro.py +0 -0
  25. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/bitget.py +0 -0
  26. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/bitmart.py +0 -0
  27. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/coinw.py +0 -0
  28. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/deepcoin.py +0 -0
  29. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/edgex.py +0 -0
  30. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  31. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/lbank.py +0 -0
  32. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/lighter.py +0 -0
  33. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/models/ourbit.py +0 -0
  34. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/ourbit.py +0 -0
  35. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/broker/ws.py +0 -0
  36. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/core.py +0 -0
  37. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/datavison/_util.py +0 -0
  38. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/datavison/binance.py +0 -0
  39. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/datavison/coinglass.py +0 -0
  40. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/datavison/okx.py +0 -0
  41. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/db.py +0 -0
  42. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/draw.py +0 -0
  43. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/logkit.py +0 -0
  44. {hyperquant-1.35 → hyperquant-1.37}/src/hyperquant/notikit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 1.35
3
+ Version: 1.37
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.35"
3
+ version = "1.37"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -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
@@ -332,6 +334,163 @@ class Book(DataStore):
332
334
  limit=limit,
333
335
  )
334
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
+
335
494
 
336
495
  class Detail(DataStore):
337
496
  """Market metadata keyed by Polymarket token id."""
@@ -446,6 +605,7 @@ class PolymarketDataStore(DataStoreCollection):
446
605
 
447
606
  def _init(self) -> None:
448
607
  self._create("book", datastore_class=Book)
608
+ self._create("bbo", datastore_class=BBO)
449
609
  self._create("detail", datastore_class=Detail)
450
610
  self._create("position", datastore_class=Position)
451
611
  self._create("order", datastore_class=Order)
@@ -627,6 +787,14 @@ class PolymarketDataStore(DataStoreCollection):
627
787
 
628
788
  return self._get("fill")
629
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
+
630
798
  @property
631
799
  def trade(self) -> Trade:
632
800
  """
@@ -683,3 +851,14 @@ class PolymarketDataStore(DataStoreCollection):
683
851
  self.position.on_trade(m)
684
852
  elif msg_type == 'orders_matched':
685
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)
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  from contextlib import suppress
5
- from datetime import UTC, datetime
5
+ from datetime import UTC, datetime, timedelta
6
6
  from functools import lru_cache
7
7
  import os
8
8
  from typing import Any, Iterable, Iterator, Literal, Mapping, Sequence
@@ -12,6 +12,7 @@ import json
12
12
  import aiohttp
13
13
  import pybotters
14
14
  import pybotters.ws
15
+ import pytz
15
16
 
16
17
  from .models.polymarket import PolymarketDataStore
17
18
  from .auth import Auth
@@ -22,6 +23,7 @@ GAMMA_EVENTS_API = "https://gamma-api.polymarket.com/events"
22
23
  DEFAULT_DATA_ENDPOINT = "https://data-api.polymarket.com"
23
24
  RTS_DATA_ENDPOINT = "wss://ws-live-data.polymarket.com/"
24
25
  DEFAULT_BASE_SLUG = "btc-updown-15m"
26
+ HOURLY_BITCOIN_BASE_SLUG = "bitcoin-up-or-down"
25
27
  DEFAULT_INTERVAL = 15 * 60
26
28
  DEFAULT_WINDOW = 8
27
29
  API_NAME = "polymarket"
@@ -38,6 +40,7 @@ DEFAULT_POLYGON_RPCS = (
38
40
  "https://polygon-rpc.com",
39
41
  "https://rpc.ankr.com/polygon",
40
42
  )
43
+ _EASTERN_TZ = pytz.timezone("US/Eastern")
41
44
 
42
45
  def parse_field(value):
43
46
  """尝试将字符串 JSON 转为对象,否则原样返回"""
@@ -73,6 +76,21 @@ def _accepting_orders(market: Mapping[str, Any]) -> bool:
73
76
  return bool(accepting)
74
77
 
75
78
 
79
+ def _compose_hourly_slug(base_slug: str, *, now: datetime | None = None) -> str:
80
+ tz_now = now or datetime.now(_EASTERN_TZ)
81
+ if tz_now.tzinfo is None:
82
+ tz_now = _EASTERN_TZ.localize(tz_now)
83
+ else:
84
+ tz_now = tz_now.astimezone(_EASTERN_TZ)
85
+
86
+ tz_now = (tz_now + timedelta(seconds=5)).replace(minute=0, second=0, microsecond=0)
87
+ month_str = tz_now.strftime("%B").lower()
88
+ day = tz_now.day
89
+ hour_12 = tz_now.strftime("%I").lstrip("0") or "0"
90
+ am_pm = tz_now.strftime("%p").lower()
91
+ return f"{base_slug}-{month_str}-{day}-{hour_12}{am_pm}-et"
92
+
93
+
76
94
  class Polymarket:
77
95
  """Polymarket CLOB client with REST helpers, stores and WS subscriptions."""
78
96
 
@@ -223,12 +241,12 @@ class Polymarket:
223
241
  self.store.orders._on_response(orders)
224
242
 
225
243
 
226
-
227
244
 
228
245
  async def sub_books(
229
246
  self,
230
247
  token_ids: Sequence[str] | str,
231
248
  wsapp: pybotters.ws.WebSocketApp | None = None,
249
+ only_bbo: bool = False,
232
250
  ) -> pybotters.ws.WebSocketApp:
233
251
  """Subscribe to public order-book updates for the provided token ids."""
234
252
 
@@ -236,11 +254,12 @@ class Polymarket:
236
254
  payload = {"type": "market", "assets_ids": tokens}
237
255
  if wsapp:
238
256
  await wsapp.current_ws.send_json(payload)
239
-
257
+ hdrl_json = self.store.onmessage_for_bbo if only_bbo else self.store.onmessage
258
+
240
259
  self._ws_public = self.client.ws_connect(
241
260
  self.ws_public,
242
261
  send_json=payload,
243
- hdlr_json=self.store.onmessage,
262
+ hdlr_json=hdrl_json,
244
263
  )
245
264
  await self._ws_public._event.wait()
246
265
  return self._ws_public
@@ -1170,6 +1189,33 @@ class Polymarket:
1170
1189
  https://docs.polymarket.com/api-reference/markets/get-market-by-id
1171
1190
  """
1172
1191
 
1192
+ async def _try_slug(slug: str | None) -> tuple[str, dict, dict] | None:
1193
+ if not slug:
1194
+ return None
1195
+ event = await self._fetch_event(slug)
1196
+ if not event:
1197
+ return None
1198
+
1199
+ event = {k: parse_field(v) for k, v in event.items()}
1200
+ for market in event.get("markets", []):
1201
+ if not _accepting_orders(market):
1202
+ continue
1203
+ market = {k: parse_field(v) for k, v in market.items()}
1204
+ return slug, event, market
1205
+ return None
1206
+
1207
+ if base_slug == HOURLY_BITCOIN_BASE_SLUG:
1208
+ hourly_slug = _compose_hourly_slug(base_slug)
1209
+ hourly_match = await _try_slug(hourly_slug)
1210
+ if hourly_match:
1211
+ return hourly_match
1212
+
1213
+ # 1小时市场等特殊 slug(比如 bitcoin-up-or-down-november-18-10am-et)
1214
+ # 直接传入完整 slug 即可,不再拼接时间戳
1215
+ direct_match = await _try_slug(base_slug)
1216
+ if direct_match:
1217
+ return direct_match
1218
+
1173
1219
  now_ts = int(datetime.now(UTC).timestamp())
1174
1220
  base_ts = (now_ts // interval) * interval
1175
1221
 
@@ -1178,18 +1224,9 @@ class Polymarket:
1178
1224
  if ts < 0:
1179
1225
  continue
1180
1226
  slug = f"{base_slug}-{ts}"
1181
- event = await self._fetch_event(slug)
1182
- if not event:
1183
- continue
1184
-
1185
-
1186
-
1187
- event = {k: parse_field(v) for k, v in event.items()}
1188
-
1189
- for market in event.get("markets", []):
1190
- if _accepting_orders(market):
1191
- market = {k: parse_field(v) for k, v in market.items()}
1192
- return slug, event, market
1227
+ result = await _try_slug(slug)
1228
+ if result:
1229
+ return result
1193
1230
 
1194
1231
  raise RuntimeError(
1195
1232
  f"未在 {base_slug} 的 +/-{window} 个区间内找到可交易的市场"
@@ -945,7 +945,7 @@ wheels = [
945
945
 
946
946
  [[package]]
947
947
  name = "hyperquant"
948
- version = "1.34"
948
+ version = "1.36"
949
949
  source = { editable = "." }
950
950
  dependencies = [
951
951
  { name = "aiohttp" },
File without changes
File without changes
File without changes