hyperquant 1.36__tar.gz → 1.38__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.36 → hyperquant-1.38}/PKG-INFO +1 -1
  2. {hyperquant-1.36 → hyperquant-1.38}/pyproject.toml +1 -1
  3. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/polymarket.py +44 -0
  4. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/polymarket.py +133 -13
  5. {hyperquant-1.36 → hyperquant-1.38}/uv.lock +1 -1
  6. {hyperquant-1.36 → hyperquant-1.38}/.gitignore +0 -0
  7. {hyperquant-1.36 → hyperquant-1.38}/README.md +0 -0
  8. {hyperquant-1.36 → hyperquant-1.38}/requirements-dev.lock +0 -0
  9. {hyperquant-1.36 → hyperquant-1.38}/requirements.lock +0 -0
  10. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/__init__.py +0 -0
  11. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/auth.py +0 -0
  12. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/bitget.py +0 -0
  13. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/bitmart.py +0 -0
  14. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/coinw.py +0 -0
  15. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/deepcoin.py +0 -0
  16. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/edgex.py +0 -0
  17. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/hyperliquid.py +0 -0
  18. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lbank.py +0 -0
  19. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  20. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lib/hpstore.py +0 -0
  21. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  22. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lib/util.py +0 -0
  23. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/lighter.py +0 -0
  24. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/apexpro.py +0 -0
  25. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/bitget.py +0 -0
  26. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/bitmart.py +0 -0
  27. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/coinw.py +0 -0
  28. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/deepcoin.py +0 -0
  29. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/edgex.py +0 -0
  30. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  31. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/lbank.py +0 -0
  32. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/lighter.py +0 -0
  33. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/models/ourbit.py +0 -0
  34. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/ourbit.py +0 -0
  35. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/broker/ws.py +0 -0
  36. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/core.py +0 -0
  37. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/datavison/_util.py +0 -0
  38. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/datavison/binance.py +0 -0
  39. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/datavison/coinglass.py +0 -0
  40. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/datavison/okx.py +0 -0
  41. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/db.py +0 -0
  42. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/draw.py +0 -0
  43. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/logkit.py +0 -0
  44. {hyperquant-1.36 → hyperquant-1.38}/src/hyperquant/notikit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 1.36
3
+ Version: 1.38
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.36"
3
+ version = "1.38"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -394,6 +394,36 @@ class _SideBook:
394
394
  except (TypeError, ValueError):
395
395
  return None
396
396
 
397
+ class Price(DataStore):
398
+ _KEYS = ["s"]
399
+
400
+ def _on_message(self, msg: dict[str, Any]) -> None:
401
+ payload = msg.get('payload') or {}
402
+ data = payload.get('data') or {}
403
+ symbol = payload.get('symbol')
404
+
405
+ if not symbol:
406
+ return
407
+
408
+ _next = self.get({'s': symbol}) or {}
409
+ _next_price = _next.get('p')
410
+ last_price = None
411
+
412
+ if data and isinstance(data, list):
413
+ last_price = data[-1].get('value')
414
+ if 'value' in payload:
415
+ last_price = payload.get('value')
416
+
417
+ if last_price is None:
418
+ return
419
+
420
+ record = {'s': symbol, 'p': last_price}
421
+ key = {'s': symbol}
422
+ if self.get(key):
423
+ self._update([record])
424
+ else:
425
+ self._insert([record])
426
+
397
427
 
398
428
  class BBO(DataStore):
399
429
  _KEYS = ["s", "S"]
@@ -612,6 +642,7 @@ class PolymarketDataStore(DataStoreCollection):
612
642
  self._create("mytrade", datastore_class=MyTrade)
613
643
  self._create("fill", datastore_class=Fill)
614
644
  self._create("trade", datastore_class=Trade)
645
+ self._create("price", datastore_class=Price)
615
646
 
616
647
  @property
617
648
  def book(self) -> Book:
@@ -762,6 +793,13 @@ class PolymarketDataStore(DataStoreCollection):
762
793
  """
763
794
 
764
795
  return self._get("trade")
796
+
797
+ @property
798
+ def price(self) -> Price:
799
+ """Price DataStore
800
+ _key: s
801
+ """
802
+ return self._get("price")
765
803
 
766
804
  @property
767
805
  def fill(self) -> Fill:
@@ -837,6 +875,12 @@ class PolymarketDataStore(DataStoreCollection):
837
875
  # 判定msg是否为list
838
876
  lst_msg = msg if isinstance(msg, list) else [msg]
839
877
  for m in lst_msg:
878
+ if m == '':
879
+ continue
880
+ topic = m.get("topic") or ""
881
+ if topic in {'crypto_prices_chainlink', 'crypto_prices'}:
882
+ self.price._on_message(m)
883
+ continue
840
884
  raw_type = m.get("event_type") or m.get("type")
841
885
  if not raw_type:
842
886
  continue
@@ -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
 
@@ -222,6 +240,89 @@ class Polymarket:
222
240
  orders = results["orders"]
223
241
  self.store.orders._on_response(orders)
224
242
 
243
+ async def sub_rts_prices(
244
+ self,
245
+ symbols: Sequence[str] | str | None = None,
246
+ *,
247
+ source: Literal["chainlink", "binance"] = "chainlink",
248
+ server_filter: bool = False,
249
+ ) -> pybotters.ws.WebSocketApp:
250
+ """Subscribe to Polymarket RTDS prices (Chainlink or Binance sources).
251
+
252
+ Parameters
253
+ ----------
254
+ symbols
255
+ Requested symbols (Chainlink prefers ``eth/usd`` format, Binance
256
+ uses ``ethusdt``).
257
+ source
258
+ Either ``"chainlink"`` (default) or ``"binance"``.
259
+ server_filter
260
+ When ``True`` the request payload includes the filter exactly as the
261
+ docs specify (e.g. ``{"symbol":"btc/usd"}``). In practice the
262
+ server sometimes stops streaming after returning the first snapshot
263
+ when filters are present, so the default behaviour is to subscribe
264
+ to the full feed and filter locally.
265
+ """
266
+
267
+ if isinstance(symbols, str):
268
+ requested = [symbols]
269
+ elif symbols:
270
+ requested = list(symbols)
271
+ else:
272
+ requested = []
273
+
274
+ target_symbols = {s.lower() for s in requested if s}
275
+
276
+ if source == "chainlink":
277
+ topic = "crypto_prices_chainlink"
278
+ sub_type = "*"
279
+ if server_filter and target_symbols:
280
+ if len(target_symbols) == 1:
281
+ filters = json.dumps({"symbol": next(iter(target_symbols))})
282
+ else:
283
+ filters = json.dumps({"symbols": sorted(target_symbols)})
284
+ else:
285
+ filters = None
286
+ else:
287
+ topic = "crypto_prices"
288
+ sub_type = "update"
289
+ filters = None
290
+ if server_filter and target_symbols:
291
+ filters = ",".join(sorted(target_symbols))
292
+
293
+ subscription: dict[str, Any] = {"topic": topic, "type": sub_type}
294
+ if filters:
295
+ subscription["filters"] = filters
296
+
297
+ payload = {
298
+ "action": "subscribe",
299
+ "subscriptions": [subscription],
300
+ }
301
+
302
+ def callback(msg, ws):
303
+ if not msg:
304
+ return
305
+ try:
306
+ data = json.loads(msg)
307
+ except json.JSONDecodeError:
308
+ return
309
+
310
+ payload = data.get("payload") or {}
311
+ symbol = str(payload.get("symbol") or "").lower()
312
+ if (not server_filter) and target_symbols and symbol and symbol not in target_symbols:
313
+ return
314
+
315
+ self.store.onmessage(data, ws)
316
+
317
+ wsapp = self.client.ws_connect(
318
+ RTS_DATA_ENDPOINT,
319
+ hdlr_str=callback,
320
+ heartbeat=5,
321
+ )
322
+
323
+ await wsapp._event.wait()
324
+ await wsapp.current_ws.send_json(payload)
325
+ return wsapp
225
326
 
226
327
 
227
328
  async def sub_books(
@@ -899,6 +1000,7 @@ class Polymarket:
899
1000
  if isinstance(fee_resp, dict):
900
1001
  return int(fee_resp.get("base_fee", 0))
901
1002
  return int(fee_resp or 0)
1003
+
902
1004
 
903
1005
  async def _signed_request_via_session(
904
1006
  self, method: str, path: str, body: Mapping[str, Any] | list[Any] | None
@@ -1171,6 +1273,33 @@ class Polymarket:
1171
1273
  https://docs.polymarket.com/api-reference/markets/get-market-by-id
1172
1274
  """
1173
1275
 
1276
+ async def _try_slug(slug: str | None) -> tuple[str, dict, dict] | None:
1277
+ if not slug:
1278
+ return None
1279
+ event = await self._fetch_event(slug)
1280
+ if not event:
1281
+ return None
1282
+
1283
+ event = {k: parse_field(v) for k, v in event.items()}
1284
+ for market in event.get("markets", []):
1285
+ if not _accepting_orders(market):
1286
+ continue
1287
+ market = {k: parse_field(v) for k, v in market.items()}
1288
+ return slug, event, market
1289
+ return None
1290
+
1291
+ if base_slug == HOURLY_BITCOIN_BASE_SLUG:
1292
+ hourly_slug = _compose_hourly_slug(base_slug)
1293
+ hourly_match = await _try_slug(hourly_slug)
1294
+ if hourly_match:
1295
+ return hourly_match
1296
+
1297
+ # 1小时市场等特殊 slug(比如 bitcoin-up-or-down-november-18-10am-et)
1298
+ # 直接传入完整 slug 即可,不再拼接时间戳
1299
+ direct_match = await _try_slug(base_slug)
1300
+ if direct_match:
1301
+ return direct_match
1302
+
1174
1303
  now_ts = int(datetime.now(UTC).timestamp())
1175
1304
  base_ts = (now_ts // interval) * interval
1176
1305
 
@@ -1179,18 +1308,9 @@ class Polymarket:
1179
1308
  if ts < 0:
1180
1309
  continue
1181
1310
  slug = f"{base_slug}-{ts}"
1182
- event = await self._fetch_event(slug)
1183
- if not event:
1184
- continue
1185
-
1186
-
1187
-
1188
- event = {k: parse_field(v) for k, v in event.items()}
1189
-
1190
- for market in event.get("markets", []):
1191
- if _accepting_orders(market):
1192
- market = {k: parse_field(v) for k, v in market.items()}
1193
- return slug, event, market
1311
+ result = await _try_slug(slug)
1312
+ if result:
1313
+ return result
1194
1314
 
1195
1315
  raise RuntimeError(
1196
1316
  f"未在 {base_slug} 的 +/-{window} 个区间内找到可交易的市场"
@@ -945,7 +945,7 @@ wheels = [
945
945
 
946
946
  [[package]]
947
947
  name = "hyperquant"
948
- version = "1.35"
948
+ version = "1.37"
949
949
  source = { editable = "." }
950
950
  dependencies = [
951
951
  { name = "aiohttp" },
File without changes
File without changes
File without changes