hyperquant 1.54__tar.gz → 1.55__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 (46) hide show
  1. {hyperquant-1.54 → hyperquant-1.55}/PKG-INFO +1 -1
  2. {hyperquant-1.54 → hyperquant-1.55}/pyproject.toml +1 -1
  3. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/polymarket.py +277 -3
  4. {hyperquant-1.54 → hyperquant-1.55}/uv.lock +1 -1
  5. {hyperquant-1.54 → hyperquant-1.55}/.gitignore +0 -0
  6. {hyperquant-1.54 → hyperquant-1.55}/README.md +0 -0
  7. {hyperquant-1.54 → hyperquant-1.55}/requirements-dev.lock +0 -0
  8. {hyperquant-1.54 → hyperquant-1.55}/requirements.lock +0 -0
  9. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/__init__.py +0 -0
  10. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/auth.py +0 -0
  11. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/bitget.py +0 -0
  12. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/bitmart.py +0 -0
  13. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/coinw.py +0 -0
  14. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/deepcoin.py +0 -0
  15. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/edgex.py +0 -0
  16. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/hyperliquid.py +0 -0
  17. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/lbank.py +0 -0
  18. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  19. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/lib/hpstore.py +0 -0
  20. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  21. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/lib/polymarket/ctfAbi.py +0 -0
  22. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/lib/polymarket/safeAbi.py +0 -0
  23. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/lib/util.py +0 -0
  24. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/lighter.py +0 -0
  25. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/apexpro.py +0 -0
  26. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/bitget.py +0 -0
  27. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/bitmart.py +0 -0
  28. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/coinw.py +0 -0
  29. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/deepcoin.py +0 -0
  30. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/edgex.py +0 -0
  31. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  32. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/lbank.py +0 -0
  33. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/lighter.py +0 -0
  34. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/ourbit.py +0 -0
  35. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/models/polymarket.py +0 -0
  36. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/ourbit.py +0 -0
  37. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/broker/ws.py +0 -0
  38. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/core.py +0 -0
  39. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/datavison/_util.py +0 -0
  40. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/datavison/binance.py +0 -0
  41. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/datavison/coinglass.py +0 -0
  42. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/datavison/okx.py +0 -0
  43. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/db.py +0 -0
  44. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/draw.py +0 -0
  45. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/logkit.py +0 -0
  46. {hyperquant-1.54 → hyperquant-1.55}/src/hyperquant/notikit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 1.54
3
+ Version: 1.55
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.54"
3
+ version = "1.55"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -7,7 +7,8 @@ from datetime import UTC, datetime, timedelta
7
7
  from functools import lru_cache
8
8
  import os
9
9
  import time
10
- from typing import Any, Iterable, Iterator, Literal, Mapping, Sequence
10
+ from typing import Any, Awaitable, Callable, Iterable, Iterator, Literal, Mapping, Sequence
11
+ from urllib.parse import urlsplit, urlunsplit
11
12
 
12
13
  import json
13
14
 
@@ -259,6 +260,8 @@ class Polymarket:
259
260
  self._ws_personal: pybotters.ws.WebSocketApp | None = None
260
261
  self.auth = False
261
262
  self._tick_size_cache: dict[str, tuple[str, float]] = {}
263
+ self._forward_orders_cfg: dict[str, Any] | None = None
264
+ self._forward_ws: dict[str, Any] | None = None
262
265
 
263
266
  self._ensure_session_entry(private_key=private_key, funder=funder, chain_id=chain_id)
264
267
 
@@ -276,6 +279,7 @@ class Polymarket:
276
279
  await self._ws_public.current_ws.close()
277
280
  self._ws_public = None
278
281
  self._ws_public_ready.clear()
282
+ await self._close_forward_ws()
279
283
 
280
284
  async def prewarm_connections(
281
285
  self,
@@ -296,6 +300,109 @@ class Polymarket:
296
300
  except Exception:
297
301
  continue
298
302
 
303
+ async def mount_forward_orders(
304
+ self,
305
+ *,
306
+ mode: Literal["rest", "ws"],
307
+ url: str,
308
+ region: str = "afr",
309
+ session: str = "orders",
310
+ ws_timeout: float = 30.0,
311
+ batch_mode: Literal["step", "batch"] = "step",
312
+ concurrency: int = 5,
313
+ callback: Callable[[dict[str, Any]], Awaitable[None] | None] | None = None,
314
+ ) -> None:
315
+ """Mount forward execution for place_orders/cancel_orders."""
316
+ cfg = {
317
+ "mode": mode,
318
+ "url": url,
319
+ "region": region,
320
+ "session": session,
321
+ "ws_timeout": float(ws_timeout),
322
+ "batch_mode": batch_mode,
323
+ "concurrency": int(concurrency),
324
+ "callback": callback,
325
+ }
326
+ if mode == "rest":
327
+ cfg["url"] = self._normalize_forward_batch_url(url)
328
+ await self._close_forward_ws()
329
+ else:
330
+ cfg["url"] = self._normalize_forward_ws_url(url)
331
+ await self._ensure_forward_ws_connected(cfg)
332
+ self._forward_orders_cfg = cfg
333
+
334
+ async def unmount_forward_orders(self) -> None:
335
+ """Disable forward execution and fallback to direct CLOB calls."""
336
+ await self._close_forward_ws()
337
+ self._forward_orders_cfg = None
338
+
339
+ async def _close_forward_ws(self) -> None:
340
+ state = self._forward_ws
341
+ self._forward_ws = None
342
+ if not state:
343
+ return
344
+ task: asyncio.Task | None = state.get("listener")
345
+ ws: aiohttp.ClientWebSocketResponse | None = state.get("ws")
346
+ if task:
347
+ task.cancel()
348
+ try:
349
+ await task
350
+ except asyncio.CancelledError:
351
+ pass
352
+ except Exception:
353
+ pass
354
+ if ws and not ws.closed:
355
+ with suppress(Exception):
356
+ await ws.close()
357
+
358
+ async def _forward_ws_listener(self, ws: aiohttp.ClientWebSocketResponse, cfg: Mapping[str, Any]) -> None:
359
+ callback = cfg.get("callback")
360
+ while True:
361
+ msg = await ws.receive()
362
+ if msg.type == aiohttp.WSMsgType.TEXT:
363
+ try:
364
+ data = json.loads(msg.data)
365
+ except Exception:
366
+ continue
367
+ if callback:
368
+ try:
369
+ ret = callback(data)
370
+ if asyncio.iscoroutine(ret):
371
+ await ret
372
+ except Exception as exc:
373
+ self.logger.warning("forward ws callback error: %s", exc)
374
+ continue
375
+ if msg.type in {aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED}:
376
+ break
377
+ if msg.type == aiohttp.WSMsgType.ERROR:
378
+ break
379
+
380
+ async def _ensure_forward_ws_connected(self, cfg: Mapping[str, Any] | None = None) -> None:
381
+ current = cfg or self._forward_orders_cfg
382
+ if not current or current.get("mode") != "ws":
383
+ return
384
+ state = self._forward_ws
385
+ if state:
386
+ ws: aiohttp.ClientWebSocketResponse | None = state.get("ws")
387
+ task: asyncio.Task | None = state.get("listener")
388
+ if ws and not ws.closed and task and not task.done():
389
+ return
390
+ await self._close_forward_ws()
391
+
392
+ session: aiohttp.ClientSession = getattr(self.client, "_session", None)
393
+ if session is None:
394
+ raise RuntimeError("pybotters client session missing")
395
+
396
+ ws_url = current["url"]
397
+ sep = "&" if ("?" in ws_url) else "?"
398
+ ws_url = f"{ws_url}{sep}region={current.get('region', 'afr')}&session={current.get('session', 'orders')}"
399
+ ws = await session.ws_connect(ws_url, heartbeat=30)
400
+ # Consume hello once to align message stream.
401
+ with suppress(Exception):
402
+ await asyncio.wait_for(ws.receive(), timeout=float(current.get("ws_timeout", 30.0)))
403
+ listener = asyncio.create_task(self._forward_ws_listener(ws, current))
404
+ self._forward_ws = {"ws": ws, "listener": listener}
405
+
299
406
  # ------------------------------------------------------------------
300
407
  # Store helpers
301
408
 
@@ -1358,6 +1465,101 @@ class Polymarket:
1358
1465
  except Exception:
1359
1466
  return await resp.text()
1360
1467
 
1468
+ def _normalize_forward_batch_url(self, forward_url: str) -> str:
1469
+ parts = urlsplit(forward_url)
1470
+ if not parts.scheme or not parts.netloc:
1471
+ raise ValueError("forward_url must be a valid absolute URL")
1472
+ path = (parts.path or "").rstrip("/")
1473
+ if path.endswith("/forward-batch") or path.endswith("/forward-batch-step"):
1474
+ target_path = path
1475
+ elif path.endswith("/forward"):
1476
+ target_path = f"{path[:-len('/forward')]}/forward-batch-step"
1477
+ else:
1478
+ target_path = f"{path}/forward-batch-step" if path else "/forward-batch-step"
1479
+ return urlunsplit((parts.scheme, parts.netloc, target_path, parts.query, parts.fragment))
1480
+
1481
+ def _normalize_forward_ws_url(self, forward_url: str) -> str:
1482
+ parts = urlsplit(forward_url)
1483
+ if parts.scheme not in {"ws", "wss"} or not parts.netloc:
1484
+ raise ValueError("forward ws url must be a valid ws:// or wss:// URL")
1485
+ path = (parts.path or "").rstrip("/")
1486
+ if not path.endswith("/ws-forward"):
1487
+ path = f"{path}/ws-forward" if path else "/ws-forward"
1488
+ return urlunsplit((parts.scheme, parts.netloc, path, parts.query, parts.fragment))
1489
+
1490
+ def _build_l2_signed_headers(
1491
+ self,
1492
+ *,
1493
+ signer_addr: str,
1494
+ api_key: str,
1495
+ api_secret: str,
1496
+ api_passphrase: str,
1497
+ method: str,
1498
+ path: str,
1499
+ payload: Any,
1500
+ ) -> dict[str, str]:
1501
+ import base64
1502
+ import hashlib
1503
+ import hmac
1504
+
1505
+ ts = int(time.time())
1506
+ serialized = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
1507
+ msg = f"{ts}{method.upper()}{path}{serialized}"
1508
+ secret_bytes = base64.urlsafe_b64decode(api_secret)
1509
+ sig = hmac.new(secret_bytes, msg.encode("utf-8"), hashlib.sha256).digest()
1510
+ sign_b64 = base64.urlsafe_b64encode(sig).decode("utf-8")
1511
+ return {
1512
+ "POLY_ADDRESS": signer_addr,
1513
+ "POLY_SIGNATURE": sign_b64,
1514
+ "POLY_TIMESTAMP": str(ts),
1515
+ "POLY_API_KEY": api_key,
1516
+ "POLY_PASSPHRASE": api_passphrase,
1517
+ "Content-Type": "application/json",
1518
+ }
1519
+
1520
+ async def _submit_forward_requests(self, requests_payload: Sequence[Mapping[str, Any]]) -> Any:
1521
+ cfg = self._forward_orders_cfg
1522
+ if not cfg:
1523
+ raise RuntimeError("forward is not mounted")
1524
+ session: aiohttp.ClientSession = getattr(self.client, "_session", None)
1525
+ if session is None:
1526
+ raise RuntimeError("pybotters client session missing")
1527
+
1528
+ if cfg["mode"] == "rest":
1529
+ endpoint = cfg["url"]
1530
+ async with session.post(endpoint, json={"requests": list(requests_payload)}) as resp:
1531
+ if resp.status >= 400:
1532
+ text = await resp.text()
1533
+ raise RuntimeError(f"Forward batch failed: {resp.status} {text}")
1534
+ try:
1535
+ return await resp.json()
1536
+ except Exception:
1537
+ return await resp.text()
1538
+
1539
+ await self._ensure_forward_ws_connected(cfg)
1540
+ state = self._forward_ws
1541
+ ws: aiohttp.ClientWebSocketResponse | None = state.get("ws") if state else None
1542
+ if ws is None or ws.closed:
1543
+ raise RuntimeError("forward ws not connected")
1544
+
1545
+ req_id = f"hq-{int(time.time() * 1000)}"
1546
+ msg_type = "batch" if cfg.get("batch_mode") == "batch" else "batch-step"
1547
+ payload: dict[str, Any] = {
1548
+ "type": msg_type,
1549
+ "id": req_id,
1550
+ "requests": list(requests_payload),
1551
+ }
1552
+ if msg_type == "batch":
1553
+ payload["concurrency"] = int(cfg.get("concurrency", 5))
1554
+ await ws.send_json(payload)
1555
+ return {
1556
+ "mode": "ws",
1557
+ "queued": True,
1558
+ "request_id": req_id,
1559
+ "batch_mode": msg_type,
1560
+ "count": len(requests_payload),
1561
+ }
1562
+
1361
1563
  async def post_orders(
1362
1564
  self,
1363
1565
  orders: Iterable[tuple[Mapping[str, Any], str]],
@@ -1447,6 +1649,41 @@ class Polymarket:
1447
1649
  }
1448
1650
  )
1449
1651
 
1652
+ if self._forward_orders_cfg:
1653
+ session: aiohttp.ClientSession = getattr(self.client, "_session", None)
1654
+ if session is None:
1655
+ raise RuntimeError("pybotters client session missing")
1656
+ creds = getattr(session, "_polymarket_api_creds", None)
1657
+ if not creds:
1658
+ raise RuntimeError("Polymarket API creds missing; call create_or_derive_api_creds")
1659
+ api_key = creds.get("api_key")
1660
+ api_secret = creds.get("api_secret")
1661
+ api_passphrase = creds.get("api_passphrase")
1662
+ if not (api_key and api_secret and api_passphrase):
1663
+ raise RuntimeError("Polymarket API creds incomplete")
1664
+
1665
+ target_url = f"{self.rest_api.rstrip('/')}/order"
1666
+ requests_payload: list[dict[str, Any]] = []
1667
+ for payload in result_body:
1668
+ headers = self._build_l2_signed_headers(
1669
+ signer_addr=signer_addr,
1670
+ api_key=api_key,
1671
+ api_secret=api_secret,
1672
+ api_passphrase=api_passphrase,
1673
+ method="POST",
1674
+ path="/order",
1675
+ payload=payload,
1676
+ )
1677
+ requests_payload.append(
1678
+ {
1679
+ "method": "POST",
1680
+ "url": target_url,
1681
+ "headers": headers,
1682
+ "body": payload,
1683
+ }
1684
+ )
1685
+ return await self._submit_forward_requests(requests_payload)
1686
+
1450
1687
  return await self._signed_request_via_session("POST", "/orders", result_body)
1451
1688
 
1452
1689
  async def cancel(self, order_id: str) -> Any:
@@ -1455,11 +1692,48 @@ class Polymarket:
1455
1692
  """
1456
1693
  return await self._signed_request_via_session("DELETE", "/order", {"orderID": order_id})
1457
1694
 
1458
- async def cancel_orders(self, order_ids: Sequence[str]) -> Any:
1695
+ async def cancel_orders(
1696
+ self,
1697
+ order_ids: Sequence[str],
1698
+ ) -> Any:
1459
1699
  """
1460
1700
  {'not_canceled': {}, 'canceled': ['0xb3507e9fda9541c3e038afcb4f24b96efcfa667d46cf5e9e52c41620711818df', '0x4b9c3ee4dee8653f15f716653e8ac83f0a086a38597e6ec4b72be2389c79b8b4']}
1461
1701
  """
1462
- return await self._signed_request_via_session("DELETE", "/orders", list(order_ids))
1702
+ payload = list(order_ids)
1703
+ if self._forward_orders_cfg:
1704
+ session: aiohttp.ClientSession = getattr(self.client, "_session", None)
1705
+ if session is None:
1706
+ raise RuntimeError("pybotters client session missing")
1707
+ creds = getattr(session, "_polymarket_api_creds", None)
1708
+ if not creds:
1709
+ raise RuntimeError("Polymarket API creds missing; call create_or_derive_api_creds")
1710
+ api_key = creds.get("api_key")
1711
+ api_secret = creds.get("api_secret")
1712
+ api_passphrase = creds.get("api_passphrase")
1713
+ if not (api_key and api_secret and api_passphrase):
1714
+ raise RuntimeError("Polymarket API creds incomplete")
1715
+
1716
+ _, _, signer_addr = self._get_signing_context()
1717
+ headers = self._build_l2_signed_headers(
1718
+ signer_addr=signer_addr,
1719
+ api_key=api_key,
1720
+ api_secret=api_secret,
1721
+ api_passphrase=api_passphrase,
1722
+ method="DELETE",
1723
+ path="/orders",
1724
+ payload=payload,
1725
+ )
1726
+ requests_payload = [
1727
+ {
1728
+ "method": "DELETE",
1729
+ "url": f"{self.rest_api.rstrip('/')}/orders",
1730
+ "headers": headers,
1731
+ "body": payload,
1732
+ }
1733
+ ]
1734
+ return await self._submit_forward_requests(requests_payload)
1735
+
1736
+ return await self._signed_request_via_session("DELETE", "/orders", payload)
1463
1737
 
1464
1738
  async def cancel_all(self) -> Any:
1465
1739
  """
@@ -694,7 +694,7 @@ wheels = [
694
694
 
695
695
  [[package]]
696
696
  name = "hyperquant"
697
- version = "1.53"
697
+ version = "1.54"
698
698
  source = { editable = "." }
699
699
  dependencies = [
700
700
  { name = "aiohttp" },
File without changes
File without changes
File without changes