hyperquant 0.43__tar.gz → 0.45__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-0.43 → hyperquant-0.45}/PKG-INFO +1 -1
- {hyperquant-0.43 → hyperquant-0.45}/pyproject.toml +1 -1
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/broker/models/ourbit.py +65 -49
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/broker/ourbit.py +80 -67
- {hyperquant-0.43 → hyperquant-0.45}/tmp.py +18 -3
- {hyperquant-0.43 → hyperquant-0.45}/uv.lock +1 -1
- {hyperquant-0.43 → hyperquant-0.45}/.gitignore +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/.python-version +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/README.md +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/data/logs/notikit.log +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/pub.sh +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/requirements-dev.lock +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/requirements.lock +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/__init__.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/broker/auth.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/core.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/db.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/draw.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/logkit.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/src/hyperquant/notikit.py +0 -0
- {hyperquant-0.43 → hyperquant-0.45}/test.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hyperquant
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.45
|
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
|
@@ -697,69 +697,85 @@ class SpotBook(DataStore):
|
|
697
697
|
# super().__init__()
|
698
698
|
self._time: int | None = None
|
699
699
|
self.limit = 1
|
700
|
+
self.loss = True
|
701
|
+
self.version = None
|
700
702
|
|
701
703
|
def _onresponse(self, data: dict[str, Any]):
|
704
|
+
data = data.get("data")
|
705
|
+
symbol = data.get("symbol")
|
706
|
+
book_data = data.get("data")
|
707
|
+
asks = book_data.get("asks", [])
|
708
|
+
bids = book_data.get("bids", [])
|
709
|
+
version = int(data.get("version", 0))
|
710
|
+
|
711
|
+
# 保存当前快照版本
|
712
|
+
self.version = version
|
713
|
+
|
714
|
+
# 应用缓存的增量(只保留连续的部分)
|
715
|
+
items: list = self.find({"s": symbol})
|
716
|
+
items.sort(key=lambda x: x.get("fv", 0)) # 按 fromVersion 排序
|
702
717
|
|
703
|
-
|
704
|
-
symbol
|
705
|
-
|
706
|
-
or top.get("symbol")
|
707
|
-
or (top.get("data") or {}).get("symbol")
|
708
|
-
)
|
709
|
-
|
710
|
-
inner = top.get("data") or top
|
711
|
-
asks = inner.get("asks") or []
|
712
|
-
bids = inner.get("bids") or []
|
713
|
-
|
714
|
-
items: list[Item] = []
|
715
|
-
if symbol:
|
716
|
-
# Snapshot semantics: rebuild entries for this symbol
|
717
|
-
self._find_and_delete({"s": symbol})
|
718
|
-
|
719
|
-
def extract_pq(level: Any) -> tuple[Any, Any] | None:
|
720
|
-
# Accept dict {"p": x, "q": y} or list/tuple [p, q, ...]
|
721
|
-
if isinstance(level, dict):
|
722
|
-
p = level.get("p")
|
723
|
-
q = level.get("q")
|
724
|
-
return (p, q)
|
725
|
-
if isinstance(level, (list, tuple)) and len(level) >= 2:
|
726
|
-
return (level[0], level[1])
|
727
|
-
return None
|
728
|
-
|
718
|
+
# 清空旧数据,插入快照
|
719
|
+
self._find_and_delete({"s": symbol})
|
720
|
+
|
729
721
|
for side, S in ((asks, "a"), (bids, "b")):
|
730
|
-
for
|
731
|
-
|
732
|
-
if not pq:
|
733
|
-
continue
|
734
|
-
p, q = pq
|
735
|
-
if p is None or q is None:
|
736
|
-
continue
|
737
|
-
try:
|
738
|
-
if float(q) == 0.0:
|
739
|
-
continue
|
740
|
-
except (TypeError, ValueError):
|
741
|
-
continue
|
742
|
-
items.append({"s": symbol, "S": S, "p": p, "q": q})
|
722
|
+
for item in side:
|
723
|
+
self._insert([{"s": symbol, "S": S, "p": item["p"], "q": item["q"]}])
|
743
724
|
|
744
725
|
if items:
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
726
|
+
min_version = min(item.get("fv", 0) for item in items)
|
727
|
+
max_version = max(item.get("tv", 0) for item in items)
|
728
|
+
self.version = max_version
|
729
|
+
|
730
|
+
if not (min_version <= self.version <= max_version):
|
731
|
+
self.loss = True
|
732
|
+
logger.warning(f"SpotBook: Snapshot version {self.version} out of range ({min_version}, {max_version}) for symbol={symbol} (丢补丁)")
|
733
|
+
return
|
734
|
+
|
735
|
+
self.loss = False
|
736
|
+
for item in items:
|
737
|
+
fv, tv = item.get("fv", 0), item.get("tv", 0)
|
738
|
+
if self.version <= tv and self.version >= fv:
|
739
|
+
self._insert([{ "s": symbol, "S": item["S"], "p": item["p"], "q": item["q"]}])
|
740
|
+
else:
|
741
|
+
self.loss = False
|
753
742
|
|
754
743
|
|
755
744
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
745
|
+
|
756
746
|
ts = time.time() * 1000 # 预留时间戳(如需记录可用)
|
757
747
|
data = msg.get("d", {}) or {}
|
758
748
|
symbol = msg.get("s")
|
759
|
-
|
749
|
+
fv = int(data.get("fromVersion"))
|
750
|
+
tv = int(data.get("toVersion"))
|
760
751
|
asks: list = data.get("asks", []) or []
|
761
752
|
bids: list = data.get("bids", []) or []
|
762
753
|
|
754
|
+
# 以下几张情况都会被认为正常
|
755
|
+
check_con = (
|
756
|
+
self.version is None or
|
757
|
+
fv <= self.version <= tv or
|
758
|
+
self.version + 1 == fv
|
759
|
+
)
|
760
|
+
|
761
|
+
if not check_con:
|
762
|
+
logger.warning(f"(丢补丁) version:{self.version} fv:{fv} tv:{tv} msg:\n {msg}")
|
763
|
+
# self.loss = True # 暂时不这样做
|
764
|
+
return
|
765
|
+
|
766
|
+
self.version = tv
|
767
|
+
|
768
|
+
if self.loss:
|
769
|
+
for item in asks:
|
770
|
+
self._insert(
|
771
|
+
[{"s": symbol, "S": "a", "p": item["p"], "q": item["q"], "fv": fv, "tv": tv}]
|
772
|
+
)
|
773
|
+
for item in bids:
|
774
|
+
self._insert(
|
775
|
+
[{"s": symbol, "S": "b", "p": item["p"], "q": item["q"], "fv": fv, "tv": tv}]
|
776
|
+
)
|
777
|
+
return
|
778
|
+
|
763
779
|
to_delete, to_update = [], []
|
764
780
|
for side, S in ((asks, "a"), (bids, "b")):
|
765
781
|
for item in side:
|
@@ -777,7 +793,7 @@ class SpotBook(DataStore):
|
|
777
793
|
self._find_and_delete({'s': symbol})
|
778
794
|
self._update(asks + bids)
|
779
795
|
|
780
|
-
|
796
|
+
print(f'处理耗时: {time.time()*1000 - ts:.2f} ms')
|
781
797
|
|
782
798
|
|
783
799
|
|
@@ -389,31 +389,20 @@ class OurbitSpot:
|
|
389
389
|
Args:
|
390
390
|
symbols: 交易对符号,可以是单个字符串或字符串列表
|
391
391
|
"""
|
392
|
+
import logging
|
393
|
+
logger = logging.getLogger("OurbitSpot")
|
394
|
+
|
392
395
|
if isinstance(symbols, str):
|
393
396
|
symbols = [symbols]
|
394
397
|
|
395
|
-
# 并发获取每个交易对的初始深度数据
|
396
|
-
tasks = [
|
397
|
-
self.client.fetch('GET', f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}")
|
398
|
-
for symbol in symbols
|
399
|
-
]
|
400
|
-
|
401
|
-
# 等待所有请求完成
|
402
|
-
responses = await asyncio.gather(*tasks)
|
403
|
-
|
404
|
-
# 处理响应数据
|
405
|
-
for response in responses:
|
406
|
-
self.store.book._onresponse(response.data)
|
407
|
-
|
408
398
|
# 构建订阅参数
|
409
399
|
subscription_params = []
|
410
400
|
for symbol in symbols:
|
411
401
|
subscription_params.append(f"spot@public.increase.aggre.depth@{symbol}")
|
412
402
|
|
413
|
-
|
414
403
|
# 一次sub20个,超过需要分开订阅
|
415
404
|
for i in range(0, len(subscription_params), 20):
|
416
|
-
self.client.ws_connect(
|
405
|
+
wsapp = self.client.ws_connect(
|
417
406
|
'wss://www.ourbit.com/ws?platform=web',
|
418
407
|
send_json={
|
419
408
|
"method": "SUBSCRIPTION",
|
@@ -422,12 +411,38 @@ class OurbitSpot:
|
|
422
411
|
},
|
423
412
|
hdlr_json=self.store.onmessage
|
424
413
|
)
|
414
|
+
await wsapp._event.wait()
|
415
|
+
|
416
|
+
# await asyncio.sleep(2) # 等待ws连接稳定
|
417
|
+
|
418
|
+
# 并发获取每个交易对的初始深度数据
|
419
|
+
tasks = [
|
420
|
+
self.client.fetch('GET', f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}")
|
421
|
+
for symbol in symbols
|
422
|
+
]
|
423
|
+
|
424
|
+
# 等待所有请求完成
|
425
|
+
responses = await asyncio.gather(*tasks)
|
426
|
+
|
427
|
+
# 处理响应数据
|
428
|
+
for idx, response in enumerate(responses):
|
429
|
+
symbol = symbols[idx]
|
430
|
+
self.store.book._onresponse(response.data)
|
431
|
+
# Check if initialization failed (loss is False), retry if so
|
432
|
+
retry_count = 0
|
433
|
+
while self.store.book.loss is True:
|
434
|
+
logger.warning(f"Orderbook initialization failed for {symbol}, refetching depth snapshot (retry {retry_count+1})")
|
435
|
+
await asyncio.sleep(0.1)
|
436
|
+
response_retry = await self.client.fetch('GET', f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}")
|
437
|
+
self.store.book._onresponse(response_retry.data)
|
438
|
+
retry_count += 1
|
439
|
+
|
425
440
|
|
426
441
|
async def place_order(
|
427
442
|
self,
|
428
443
|
symbol: str,
|
429
444
|
side: Literal["buy", "sell"],
|
430
|
-
price: float,
|
445
|
+
price: float = None,
|
431
446
|
quantity: float = None,
|
432
447
|
order_type: Literal["market", "limit"] = "limit",
|
433
448
|
usdt_amount: float = None
|
@@ -437,7 +452,7 @@ class OurbitSpot:
|
|
437
452
|
Args:
|
438
453
|
symbol: 交易对,如 "SOL_USDT"
|
439
454
|
side: 买卖方向 "buy" 或 "sell"
|
440
|
-
price:
|
455
|
+
price: 价格,市价单可为None
|
441
456
|
quantity: 数量
|
442
457
|
order_type: 订单类型 "market" 或 "limit"
|
443
458
|
usdt_amount: USDT金额,如果指定则根据价格计算数量
|
@@ -445,74 +460,72 @@ class OurbitSpot:
|
|
445
460
|
Returns:
|
446
461
|
订单响应数据
|
447
462
|
"""
|
463
|
+
# 参数检查
|
464
|
+
if order_type == "limit" and price is None:
|
465
|
+
raise ValueError("Limit orders require a price")
|
466
|
+
if quantity is None and usdt_amount is None:
|
467
|
+
raise ValueError("Either quantity or usdt_amount must be specified")
|
468
|
+
|
448
469
|
# 解析交易对
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
470
|
+
parts = symbol.split("_")
|
471
|
+
if len(parts) != 2:
|
472
|
+
raise ValueError(f"Invalid symbol format: {symbol}")
|
473
|
+
|
474
|
+
currency, market = parts
|
475
|
+
|
476
|
+
# 获取交易对详情
|
477
|
+
detail = self.store.detail.get({"name": currency})
|
455
478
|
if not detail:
|
456
479
|
raise ValueError(f"Unknown currency: {currency}")
|
457
|
-
|
458
|
-
price_scale = detail.get(
|
459
|
-
quantity_scale = detail.get(
|
460
|
-
|
461
|
-
use_quantity = True if quantity is not None else False
|
462
|
-
|
463
|
-
# 如果指定了USDT金额,重新计算数量
|
464
|
-
if usdt_amount is not None:
|
465
|
-
if side == "buy":
|
466
|
-
quantity = usdt_amount / price
|
467
|
-
else:
|
468
|
-
# 卖出时usdt_amount表示要卖出的币种价值
|
469
|
-
quantity = usdt_amount / price
|
470
|
-
|
471
|
-
# 格式化价格和数量
|
472
|
-
if price_scale is not None:
|
473
|
-
price = round(price, price_scale)
|
474
|
-
|
475
|
-
if quantity_scale is not None:
|
476
|
-
quantity = round(quantity, quantity_scale)
|
480
|
+
|
481
|
+
price_scale = detail.get("price_scale")
|
482
|
+
quantity_scale = detail.get("quantity_scale")
|
477
483
|
|
478
484
|
# 构建请求数据
|
479
485
|
data = {
|
480
486
|
"currency": currency,
|
481
487
|
"market": market,
|
482
|
-
"tradeType": side.upper()
|
483
|
-
"quantity": str(quantity),
|
488
|
+
"tradeType": side.upper()
|
484
489
|
}
|
485
490
|
|
491
|
+
# 处理市价单和限价单的不同参数
|
486
492
|
if order_type == "limit":
|
487
493
|
data["orderType"] = "LIMIT_ORDER"
|
488
|
-
data["price"] = str(price)
|
494
|
+
data["price"] = str(round(price, price_scale) if price_scale is not None else price)
|
495
|
+
|
496
|
+
# 计算并设置数量
|
497
|
+
if quantity is None and usdt_amount is not None and price:
|
498
|
+
quantity = usdt_amount / price
|
499
|
+
|
500
|
+
if quantity_scale is not None:
|
501
|
+
quantity = round(quantity, quantity_scale)
|
502
|
+
data["quantity"] = str(quantity)
|
503
|
+
|
489
504
|
elif order_type == "market":
|
490
505
|
data["orderType"] = "MARKET_ORDER"
|
491
|
-
|
492
|
-
#
|
493
|
-
if not
|
494
|
-
del data["quantity"]
|
506
|
+
|
507
|
+
# 市价单可以使用数量或金额,但不能同时使用
|
508
|
+
if usdt_amount is not None:
|
495
509
|
data["amount"] = str(usdt_amount)
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
510
|
+
else:
|
511
|
+
if quantity_scale is not None:
|
512
|
+
quantity = round(quantity, quantity_scale)
|
513
|
+
data["quantity"] = str(quantity)
|
514
|
+
|
515
|
+
if price:
|
516
|
+
data["price"] = str(price)
|
502
517
|
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
)
|
518
|
+
# 确定API端点
|
519
|
+
url = f'{self.api_url}/api/platform/spot/{"v4/order/place" if order_type == "market" else "order/place"}'
|
520
|
+
|
521
|
+
# 发送请求
|
522
|
+
res = await self.client.fetch("POST", url, json=data)
|
508
523
|
|
509
524
|
# 处理响应
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
raise Exception(f"Failed to place order: {res.data}")
|
515
|
-
|
525
|
+
if res.data.get("msg") == "success":
|
526
|
+
return res.data["data"]
|
527
|
+
raise Exception(f"Failed to place order: {res.data}")
|
528
|
+
|
516
529
|
async def cancel_orders(self, order_ids: list[str]):
|
517
530
|
|
518
531
|
for order_id in order_ids:
|
@@ -133,9 +133,24 @@ async def test_cancel_order():
|
|
133
133
|
# await ob_spot.sub_personal()
|
134
134
|
# await ob_spot.update('all')
|
135
135
|
|
136
|
-
oid = await ob_spot.place_order('SOL_USDT', '
|
136
|
+
oid = await ob_spot.place_order('SOL_USDT', 'sell', order_type='market', quantity=0.03 )
|
137
137
|
print(oid)
|
138
|
-
await ob_spot.cancel_orders([oid])
|
138
|
+
# await ob_spot.cancel_orders([oid])
|
139
|
+
|
140
|
+
|
141
|
+
async def test_orderbook():
|
142
|
+
async with pybotters.Client(apis={
|
143
|
+
"ourbit": [
|
144
|
+
"WEB3bf088f8b2f2fae07592fe1a6240e2d798100a9cb2a91f8fda1056b6865ab3ee"
|
145
|
+
]
|
146
|
+
}) as client:
|
147
|
+
ob_spot = OurbitSpot(client)
|
148
|
+
await ob_spot.__aenter__()
|
149
|
+
await ob_spot.sub_orderbook(['XRP_USDT'])
|
150
|
+
while True:
|
151
|
+
await ob_spot.store.book.wait()
|
152
|
+
print(ob_spot.store.book.find())
|
153
|
+
|
139
154
|
|
140
155
|
if __name__ == "__main__":
|
141
|
-
asyncio.run(
|
156
|
+
asyncio.run(test_orderbook())
|
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
|