hyperquant 1.35__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.35 → hyperquant-1.36}/PKG-INFO +1 -1
- {hyperquant-1.35 → hyperquant-1.36}/pyproject.toml +1 -1
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/polymarket.py +179 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/polymarket.py +4 -3
- {hyperquant-1.35 → hyperquant-1.36}/uv.lock +1 -1
- {hyperquant-1.35 → hyperquant-1.36}/.gitignore +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/README.md +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/requirements-dev.lock +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/requirements.lock +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/auth.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/bitmart.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/deepcoin.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/lighter.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/apexpro.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/bitmart.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/deepcoin.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/lighter.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/core.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/db.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.35 → hyperquant-1.36}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.35 → 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
|
|
@@ -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)
|
|
@@ -223,12 +223,12 @@ class Polymarket:
|
|
|
223
223
|
self.store.orders._on_response(orders)
|
|
224
224
|
|
|
225
225
|
|
|
226
|
-
|
|
227
226
|
|
|
228
227
|
async def sub_books(
|
|
229
228
|
self,
|
|
230
229
|
token_ids: Sequence[str] | str,
|
|
231
230
|
wsapp: pybotters.ws.WebSocketApp | None = None,
|
|
231
|
+
only_bbo: bool = False,
|
|
232
232
|
) -> pybotters.ws.WebSocketApp:
|
|
233
233
|
"""Subscribe to public order-book updates for the provided token ids."""
|
|
234
234
|
|
|
@@ -236,11 +236,12 @@ class Polymarket:
|
|
|
236
236
|
payload = {"type": "market", "assets_ids": tokens}
|
|
237
237
|
if wsapp:
|
|
238
238
|
await wsapp.current_ws.send_json(payload)
|
|
239
|
-
|
|
239
|
+
hdrl_json = self.store.onmessage_for_bbo if only_bbo else self.store.onmessage
|
|
240
|
+
|
|
240
241
|
self._ws_public = self.client.ws_connect(
|
|
241
242
|
self.ws_public,
|
|
242
243
|
send_json=payload,
|
|
243
|
-
hdlr_json=
|
|
244
|
+
hdlr_json=hdrl_json,
|
|
244
245
|
)
|
|
245
246
|
await self._ws_public._event.wait()
|
|
246
247
|
return self._ws_public
|
|
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
|
|
File without changes
|