hyperquant 0.37__py3-none-any.whl → 0.38__py3-none-any.whl
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/broker/auth.py +45 -12
- hyperquant/broker/models/ourbit.py +500 -2
- hyperquant/broker/ourbit.py +191 -1
- hyperquant/broker/ws.py +5 -0
- {hyperquant-0.37.dist-info → hyperquant-0.38.dist-info}/METADATA +1 -1
- {hyperquant-0.37.dist-info → hyperquant-0.38.dist-info}/RECORD +7 -7
- {hyperquant-0.37.dist-info → hyperquant-0.38.dist-info}/WHEEL +0 -0
hyperquant/broker/auth.py
CHANGED
@@ -28,28 +28,61 @@ class Auth:
|
|
28
28
|
|
29
29
|
# 时间戳 & body
|
30
30
|
now_ms = int(time.time() * 1000)
|
31
|
-
raw_body_for_sign =
|
31
|
+
raw_body_for_sign = (
|
32
|
+
data
|
33
|
+
if isinstance(data, str)
|
34
|
+
else pyjson.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
35
|
+
)
|
32
36
|
|
33
37
|
# 签名
|
34
38
|
mid_hash = md5_hex(f"{token}{now_ms}")[7:]
|
35
39
|
final_hash = md5_hex(f"{now_ms}{raw_body_for_sign}{mid_hash}")
|
36
40
|
|
37
41
|
# 设置 headers
|
38
|
-
headers.update(
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
42
|
+
headers.update(
|
43
|
+
{
|
44
|
+
"Authorization": token,
|
45
|
+
"Language": "Chinese",
|
46
|
+
"language": "Chinese",
|
47
|
+
"Content-Type": "application/json",
|
48
|
+
"x-ourbit-sign": final_hash,
|
49
|
+
"x-ourbit-nonce": str(now_ms),
|
50
|
+
}
|
51
|
+
)
|
46
52
|
|
47
53
|
# 更新 kwargs.body,保证发出去的与签名一致
|
48
54
|
kwargs.update({"data": raw_body_for_sign})
|
49
|
-
|
55
|
+
|
50
56
|
return args
|
51
|
-
|
52
57
|
|
53
|
-
|
58
|
+
@staticmethod
|
59
|
+
def ourbit_spot(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
|
60
|
+
method: str = args[0]
|
61
|
+
url: URL = args[1]
|
62
|
+
data = kwargs.get("data") or {}
|
63
|
+
headers: CIMultiDict = kwargs["headers"]
|
64
|
+
|
65
|
+
# 从 session 里取 token
|
66
|
+
session = kwargs["session"]
|
67
|
+
token = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
|
68
|
+
cookie = f"uc_token={token}; u_id={token}; "
|
69
|
+
headers.update({"cookie": cookie})
|
70
|
+
|
71
|
+
# wss消息增加参数
|
72
|
+
# if headers.get("Upgrade") == "websocket":
|
73
|
+
# args = (method, url)
|
74
|
+
# # 拼接 token
|
75
|
+
# q = dict(url.query)
|
76
|
+
# q["token"] = token
|
77
|
+
# url = url.with_query(q)
|
78
|
+
|
79
|
+
|
80
|
+
return args
|
54
81
|
|
55
82
|
|
83
|
+
pybotters.auth.Hosts.items["futures.ourbit.com"] = pybotters.auth.Item(
|
84
|
+
"ourbit", Auth.ourbit
|
85
|
+
)
|
86
|
+
pybotters.auth.Hosts.items["www.ourbit.com"] = pybotters.auth.Item(
|
87
|
+
"ourbit", Auth.ourbit_spot
|
88
|
+
)
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import logging
|
5
|
+
import time
|
5
6
|
from typing import TYPE_CHECKING, Any, Awaitable
|
6
7
|
|
7
8
|
import aiohttp
|
@@ -29,7 +30,7 @@ class Book(DataStore):
|
|
29
30
|
|
30
31
|
"""
|
31
32
|
|
32
|
-
_KEYS = ["symbol", "side",
|
33
|
+
_KEYS = ["symbol", "side", "i"]
|
33
34
|
|
34
35
|
def _init(self) -> None:
|
35
36
|
# super().__init__()
|
@@ -41,6 +42,10 @@ class Book(DataStore):
|
|
41
42
|
data = msg.get("data", {})
|
42
43
|
asks = data.get("asks", [])
|
43
44
|
bids = data.get("bids", [])
|
45
|
+
# 提速 默认 5当前
|
46
|
+
asks = asks[:5]
|
47
|
+
bids = bids[:5]
|
48
|
+
|
44
49
|
timestamp = data.get("ct") # 使用服务器时间
|
45
50
|
|
46
51
|
data_to_insert: list[Item] = []
|
@@ -50,7 +55,7 @@ class Book(DataStore):
|
|
50
55
|
|
51
56
|
# 处理买卖盘数据
|
52
57
|
for side_id, levels in (("B", bids), ("A", asks)):
|
53
|
-
for level in levels:
|
58
|
+
for i, level in enumerate(levels):
|
54
59
|
# level格式: [price, size, count]
|
55
60
|
if len(level) >= 3:
|
56
61
|
price, size, count = level[0:3]
|
@@ -61,6 +66,7 @@ class Book(DataStore):
|
|
61
66
|
"px": str(price),
|
62
67
|
"sz": str(size),
|
63
68
|
"count": count,
|
69
|
+
"i": i
|
64
70
|
}
|
65
71
|
)
|
66
72
|
|
@@ -295,6 +301,8 @@ class Balance(DataStore):
|
|
295
301
|
data: dict = msg.get("data", {})
|
296
302
|
self._update([self._fmt(data)])
|
297
303
|
|
304
|
+
|
305
|
+
|
298
306
|
class OurbitSwapDataStore(DataStoreCollection):
|
299
307
|
"""
|
300
308
|
Ourbit DataStoreCollection
|
@@ -423,6 +431,7 @@ class OurbitSwapDataStore(DataStoreCollection):
|
|
423
431
|
"px": "110152.5", # 价格
|
424
432
|
"sz": "53539", # 数量
|
425
433
|
"count": 1 # 订单数量
|
434
|
+
"i" 0 # 价格档位索引
|
426
435
|
},
|
427
436
|
{
|
428
437
|
"symbol": "BTC_USDT", # 交易对
|
@@ -430,6 +439,7 @@ class OurbitSwapDataStore(DataStoreCollection):
|
|
430
439
|
"px": "110152.4", # 价格
|
431
440
|
"sz": "76311", # 数量
|
432
441
|
"count": 1 # 订单数量
|
442
|
+
"i" 0 # 价格档位索引
|
433
443
|
}
|
434
444
|
]
|
435
445
|
"""
|
@@ -548,3 +558,491 @@ class OurbitSwapDataStore(DataStoreCollection):
|
|
548
558
|
"""
|
549
559
|
return self._get("balance", Balance)
|
550
560
|
return self._get("balance", Balance)
|
561
|
+
|
562
|
+
# SpotBalance: 现货账户余额数据存储
|
563
|
+
|
564
|
+
class SpotBalance(DataStore):
|
565
|
+
_KEYS = ["currency"]
|
566
|
+
|
567
|
+
def _fmt(self, balance: dict) -> dict:
|
568
|
+
return {
|
569
|
+
"currency": balance.get("currency"),
|
570
|
+
"available": balance.get("available"),
|
571
|
+
"frozen": balance.get("frozen"),
|
572
|
+
"amount": balance.get("amount"),
|
573
|
+
"avg_price": balance.get("avgPrice"),
|
574
|
+
}
|
575
|
+
|
576
|
+
def _fmt_ws(self, balance: dict) -> dict:
|
577
|
+
return {
|
578
|
+
"currency": balance.get("s"),
|
579
|
+
"available": balance.get("av"),
|
580
|
+
"frozen": balance.get("fr"),
|
581
|
+
"amount": balance.get("to"),
|
582
|
+
"avg_price": balance.get("ap"),
|
583
|
+
}
|
584
|
+
|
585
|
+
def _onresponse(self, data: dict[str, Any]):
|
586
|
+
balances = data.get("data", [])
|
587
|
+
items = [self._fmt(b) for b in balances]
|
588
|
+
if items:
|
589
|
+
self._clear()
|
590
|
+
self._insert(items)
|
591
|
+
|
592
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
593
|
+
data = msg.get("d", {})
|
594
|
+
item = self._fmt_ws(data)
|
595
|
+
self._update([item])
|
596
|
+
|
597
|
+
|
598
|
+
# SpotOrders: 现货订单数据存储
|
599
|
+
class SpotOrders(DataStore):
|
600
|
+
_KEYS = ["order_id"]
|
601
|
+
|
602
|
+
|
603
|
+
def _fmt(self, order: dict) -> dict:
|
604
|
+
state = order.get("state")
|
605
|
+
if state == 1 or state == 2:
|
606
|
+
state = "open"
|
607
|
+
elif state == 3:
|
608
|
+
state = "filled"
|
609
|
+
elif state == 4:
|
610
|
+
state = "canceled"
|
611
|
+
|
612
|
+
return {
|
613
|
+
"order_id": order.get("id"),
|
614
|
+
"symbol": order.get("symbol"),
|
615
|
+
"currency": order.get("currency"),
|
616
|
+
"market": order.get("market"),
|
617
|
+
"trade_type": order.get("tradeType"),
|
618
|
+
"order_type": order.get("orderType"),
|
619
|
+
"price": order.get("price"),
|
620
|
+
"quantity": order.get("quantity"),
|
621
|
+
"amount": order.get("amount"),
|
622
|
+
"deal_quantity": order.get("dealQuantity"),
|
623
|
+
"deal_amount": order.get("dealAmount"),
|
624
|
+
"avg_price": order.get("avgPrice"),
|
625
|
+
"state": order.get("state"),
|
626
|
+
"source": order.get("source"),
|
627
|
+
"fee": order.get("fee"),
|
628
|
+
"create_ts": order.get("createTime"),
|
629
|
+
"unique_id": order.get("uniqueId"),
|
630
|
+
}
|
631
|
+
|
632
|
+
|
633
|
+
|
634
|
+
def _onresponse(self, data: dict[str, Any]):
|
635
|
+
orders = (data.get("data") or {}).get("resultList", [])
|
636
|
+
items = [self._fmt(order) for order in orders]
|
637
|
+
self._clear()
|
638
|
+
if items:
|
639
|
+
self._insert(items)
|
640
|
+
|
641
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
642
|
+
d:dict = msg.get("d", {})
|
643
|
+
|
644
|
+
|
645
|
+
item = {
|
646
|
+
"order_id": d.get("id"),
|
647
|
+
"symbol": msg.get("s") or d.get("symbol"),
|
648
|
+
"trade_type": d.get("tradeType"),
|
649
|
+
"order_type": d.get("orderType"),
|
650
|
+
"price": d.get("price"),
|
651
|
+
"quantity": d.get("quantity"),
|
652
|
+
"amount": d.get("amount"),
|
653
|
+
"remain_quantity": d.get("remainQ"),
|
654
|
+
"remain_amount": d.get("remainA"),
|
655
|
+
"state": d.get("status"),
|
656
|
+
"client_order_id": d.get("clientOrderId"),
|
657
|
+
"is_taker": d.get("isTaker"),
|
658
|
+
"create_ts": d.get("createTime"),
|
659
|
+
"source": d.get("internal"),
|
660
|
+
}
|
661
|
+
|
662
|
+
state = d.get("status")
|
663
|
+
|
664
|
+
if state == 2 or state == 1:
|
665
|
+
item["state"] = "open"
|
666
|
+
self._insert([item])
|
667
|
+
|
668
|
+
elif state == 3:
|
669
|
+
item["state"] = "filled"
|
670
|
+
|
671
|
+
# 如果这三个字段存在追加
|
672
|
+
if d.get("singleDealId") and d.get("singleDealPrice") and d.get("singleDealQuantity"):
|
673
|
+
item.update({
|
674
|
+
"unique_id": d.get("singleDealId"),
|
675
|
+
"avg_price": d.get("singleDealPrice"),
|
676
|
+
"deal_quantity": d.get("singleDealQuantity"),
|
677
|
+
})
|
678
|
+
|
679
|
+
self._update([item])
|
680
|
+
self._find_and_delete({ "order_id": d.get("id") })
|
681
|
+
|
682
|
+
elif state == 4:
|
683
|
+
item["state"] = "canceled"
|
684
|
+
self._update([item])
|
685
|
+
self._find_and_delete({ "order_id": d.get("id") })
|
686
|
+
|
687
|
+
|
688
|
+
|
689
|
+
class SpotBook(DataStore):
|
690
|
+
_KEYS = ["s", "S", 'p']
|
691
|
+
|
692
|
+
def _init(self) -> None:
|
693
|
+
# super().__init__()
|
694
|
+
self._time: int | None = None
|
695
|
+
self.limit = 1
|
696
|
+
|
697
|
+
def _onresponse(self, data: dict[str, Any]):
|
698
|
+
|
699
|
+
top = data.get("data") or data.get("d") or data
|
700
|
+
symbol = (
|
701
|
+
top.get("s")
|
702
|
+
or top.get("symbol")
|
703
|
+
or (top.get("data") or {}).get("symbol")
|
704
|
+
)
|
705
|
+
|
706
|
+
inner = top.get("data") or top
|
707
|
+
asks = inner.get("asks") or []
|
708
|
+
bids = inner.get("bids") or []
|
709
|
+
|
710
|
+
items: list[Item] = []
|
711
|
+
if symbol:
|
712
|
+
# Snapshot semantics: rebuild entries for this symbol
|
713
|
+
self._find_and_delete({"s": symbol})
|
714
|
+
|
715
|
+
def extract_pq(level: Any) -> tuple[Any, Any] | None:
|
716
|
+
# Accept dict {"p": x, "q": y} or list/tuple [p, q, ...]
|
717
|
+
if isinstance(level, dict):
|
718
|
+
p = level.get("p")
|
719
|
+
q = level.get("q")
|
720
|
+
return (p, q)
|
721
|
+
if isinstance(level, (list, tuple)) and len(level) >= 2:
|
722
|
+
return (level[0], level[1])
|
723
|
+
return None
|
724
|
+
|
725
|
+
for side, S in ((asks, "a"), (bids, "b")):
|
726
|
+
for level in side:
|
727
|
+
pq = extract_pq(level)
|
728
|
+
if not pq:
|
729
|
+
continue
|
730
|
+
p, q = pq
|
731
|
+
if p is None or q is None:
|
732
|
+
continue
|
733
|
+
try:
|
734
|
+
if float(q) == 0.0:
|
735
|
+
continue
|
736
|
+
except (TypeError, ValueError):
|
737
|
+
continue
|
738
|
+
items.append({"s": symbol, "S": S, "p": p, "q": q})
|
739
|
+
|
740
|
+
if items:
|
741
|
+
self._insert(items)
|
742
|
+
|
743
|
+
sort_data = self.sorted({'s': symbol}, self.limit)
|
744
|
+
asks = sort_data.get('a', [])
|
745
|
+
bids = sort_data.get('b', [])
|
746
|
+
self._clear()
|
747
|
+
self._update(asks + bids)
|
748
|
+
|
749
|
+
|
750
|
+
|
751
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
752
|
+
ts = time.time() * 1000 # 预留时间戳(如需记录可用)
|
753
|
+
data = msg.get("d", {}) or {}
|
754
|
+
symbol = msg.get("s")
|
755
|
+
|
756
|
+
asks: list = data.get("asks", []) or []
|
757
|
+
bids: list = data.get("bids", []) or []
|
758
|
+
|
759
|
+
to_delete, to_update = [], []
|
760
|
+
for side, S in ((asks, "a"), (bids, "b")):
|
761
|
+
for item in side:
|
762
|
+
if float(item["q"]) == 0.0:
|
763
|
+
to_delete.append({"s": symbol, "S": S, "p": item["p"]})
|
764
|
+
else:
|
765
|
+
to_update.append({"s": symbol, "S": S, "p": item["p"], "q": item["q"]})
|
766
|
+
|
767
|
+
self._delete(to_delete)
|
768
|
+
self._insert(to_update)
|
769
|
+
|
770
|
+
sort_data = self.sorted({'s': symbol}, self.limit)
|
771
|
+
asks = sort_data.get('a', [])
|
772
|
+
bids = sort_data.get('b', [])
|
773
|
+
self._clear()
|
774
|
+
self._update(asks + bids)
|
775
|
+
|
776
|
+
# print(f'处理耗时: {time.time()*1000 - ts:.2f} ms')
|
777
|
+
|
778
|
+
|
779
|
+
|
780
|
+
def sorted(
|
781
|
+
self, query: Item | None = None, limit: int | None = None
|
782
|
+
) -> dict[str, list[Item]]:
|
783
|
+
return self._sorted(
|
784
|
+
item_key="S",
|
785
|
+
item_asc_key="a",
|
786
|
+
item_desc_key="b",
|
787
|
+
sort_key="p",
|
788
|
+
query=query,
|
789
|
+
limit=limit,
|
790
|
+
)
|
791
|
+
|
792
|
+
@property
|
793
|
+
def time(self) -> int | None:
|
794
|
+
"""返回最后更新时间"""
|
795
|
+
return self._time
|
796
|
+
|
797
|
+
|
798
|
+
|
799
|
+
class SpotTicker(DataStore):
|
800
|
+
_KEYS = ["symbol"]
|
801
|
+
|
802
|
+
def _fmt(self, t: dict[str, Any]) -> dict[str, Any]:
|
803
|
+
# 根据示例:
|
804
|
+
# { id: "...", sb: "WCT_USDT", r8: "0.0094", tzr: "0.0094", c: "0.3002", h: "0.3035", l: "0.292", o: "0.2974", q: "1217506.41", a: "363548.8205" }
|
805
|
+
return {
|
806
|
+
"id": t.get("id"),
|
807
|
+
"symbol": t.get("sb"),
|
808
|
+
"last_price": t.get("c"),
|
809
|
+
"open_price": t.get("o"),
|
810
|
+
"high_price": t.get("h"),
|
811
|
+
"low_price": t.get("l"),
|
812
|
+
"volume24": t.get("q"),
|
813
|
+
"amount24": t.get("a"),
|
814
|
+
"rise_fall_rate": t.get("r8") if t.get("r8") is not None else t.get("tzr"),
|
815
|
+
}
|
816
|
+
|
817
|
+
def _onresponse(self, data: dict[str, Any] | list[dict[str, Any]]):
|
818
|
+
# 支持 data 为:
|
819
|
+
# - 直接为 list[dict]
|
820
|
+
# - {"data": list[dict]}
|
821
|
+
# - {"d": list[dict]}
|
822
|
+
payload = data
|
823
|
+
if isinstance(data, dict):
|
824
|
+
payload = data.get("data") or data.get("d") or data
|
825
|
+
if not isinstance(payload, list):
|
826
|
+
payload = [payload]
|
827
|
+
items = [self._fmt(t) for t in payload if isinstance(t, dict)]
|
828
|
+
if not items:
|
829
|
+
return
|
830
|
+
self._clear()
|
831
|
+
self._insert(items)
|
832
|
+
|
833
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
834
|
+
# 兼容 WS:
|
835
|
+
# { "c": "increase.tickers", "d": { ...ticker... } }
|
836
|
+
d = msg.get("d") or msg.get("data") or msg
|
837
|
+
if not isinstance(d, dict):
|
838
|
+
return
|
839
|
+
item = self._fmt(d)
|
840
|
+
if not item.get("symbol"):
|
841
|
+
return
|
842
|
+
# 覆盖式更新该 symbol
|
843
|
+
self._find_and_delete({"symbol": item["symbol"]})
|
844
|
+
self._insert([item])
|
845
|
+
|
846
|
+
class SpotDetail(DataStore):
|
847
|
+
_KEYS = ["name"]
|
848
|
+
|
849
|
+
def _fmt(self, detail: dict) -> dict:
|
850
|
+
return {
|
851
|
+
"id": detail.get("id"), # 唯一ID
|
852
|
+
"name": detail.get("vn"), # 虚拟币简称
|
853
|
+
"name_abbr": detail.get("vna"), # 虚拟币全称
|
854
|
+
"final_name": detail.get("fn"), # 法币符号/展示名
|
855
|
+
"sort": detail.get("srt"), # 排序字段
|
856
|
+
"status": detail.get("sts"), # 状态 (1=可用, 0=不可用)
|
857
|
+
"type": detail.get("tp"), # 类型 (NEW=新币种)
|
858
|
+
"internal_id": detail.get("in"), # 内部唯一流水号
|
859
|
+
"first_online_time": detail.get("fot"), # 首次上线时间
|
860
|
+
"online_time": detail.get("ot"), # 上线时间
|
861
|
+
"coin_partition": detail.get("cp"), # 所属交易区分类
|
862
|
+
"price_scale": detail.get("ps"), # 价格小数位数
|
863
|
+
"quantity_scale": detail.get("qs"), # 数量小数位数
|
864
|
+
"contract_decimal_mode": detail.get("cdm"), # 合约精度模式
|
865
|
+
"contract_address": detail.get("ca"), # 代币合约地址
|
866
|
+
}
|
867
|
+
|
868
|
+
def _onresponse(self, data: dict[str, Any]):
|
869
|
+
details = data.get("data", {}).get('USDT')
|
870
|
+
if not details:
|
871
|
+
return
|
872
|
+
|
873
|
+
items = [self._fmt(detail) for detail in details]
|
874
|
+
self._clear()
|
875
|
+
self._insert(items)
|
876
|
+
|
877
|
+
|
878
|
+
class OurbitSpotDataStore(DataStoreCollection):
|
879
|
+
"""
|
880
|
+
Ourbit DataStoreCollection Spot
|
881
|
+
"""
|
882
|
+
def _init(self) -> None:
|
883
|
+
self._create("book", datastore_class=SpotBook)
|
884
|
+
self._create("ticker", datastore_class=SpotTicker)
|
885
|
+
self._create("balance", datastore_class=SpotBalance)
|
886
|
+
self._create("order", datastore_class=SpotOrders)
|
887
|
+
self._create("detail", datastore_class=SpotDetail)
|
888
|
+
|
889
|
+
@property
|
890
|
+
def book(self) -> SpotBook:
|
891
|
+
"""
|
892
|
+
获取现货订单簿
|
893
|
+
.. code:: json
|
894
|
+
[
|
895
|
+
{
|
896
|
+
"s": "BTC_USDT",
|
897
|
+
"S": "a",
|
898
|
+
"p": "110152.5",
|
899
|
+
"q": "53539"
|
900
|
+
}
|
901
|
+
]
|
902
|
+
|
903
|
+
"""
|
904
|
+
return self._get("book")
|
905
|
+
|
906
|
+
@property
|
907
|
+
def ticker(self) -> SpotTicker:
|
908
|
+
"""
|
909
|
+
获取现货 Ticker
|
910
|
+
.. code:: json
|
911
|
+
[
|
912
|
+
{
|
913
|
+
"symbol": "WCT_USDT",
|
914
|
+
"last_price": "0.3002",
|
915
|
+
"open_price": "0.2974",
|
916
|
+
"high_price": "0.3035",
|
917
|
+
"low_price": "0.292",
|
918
|
+
"volume24": "1217506.41",
|
919
|
+
"amount24": "363548.8205",
|
920
|
+
"rise_fall_rate": "0.0094",
|
921
|
+
"id": "dc893d07ca8345008db4d874da726a15"
|
922
|
+
}
|
923
|
+
]
|
924
|
+
"""
|
925
|
+
return self._get("ticker")
|
926
|
+
|
927
|
+
@property
|
928
|
+
def balance(self) -> SpotBalance:
|
929
|
+
"""
|
930
|
+
现货账户余额数据流
|
931
|
+
|
932
|
+
Data structure:
|
933
|
+
|
934
|
+
.. code:: python
|
935
|
+
|
936
|
+
[
|
937
|
+
{
|
938
|
+
"currency": "USDT", # 币种
|
939
|
+
"available": "100.0", # 可用余额
|
940
|
+
"frozen": "0.0", # 冻结余额
|
941
|
+
"usdt_available": "100.0",# USDT 可用余额
|
942
|
+
"usdt_frozen": "0.0", # USDT 冻结余额
|
943
|
+
"amount": "100.0", # 总金额
|
944
|
+
"avg_price": "1.0", # 平均价格
|
945
|
+
"last_price": "1.0", # 最新价格
|
946
|
+
"hidden_small": False, # 是否隐藏小额资产
|
947
|
+
"icon": "" # 币种图标
|
948
|
+
}
|
949
|
+
]
|
950
|
+
"""
|
951
|
+
return self._get("balance", SpotBalance)
|
952
|
+
|
953
|
+
@property
|
954
|
+
def detail(self) -> SpotDetail:
|
955
|
+
"""
|
956
|
+
现货交易对详情数据流
|
957
|
+
|
958
|
+
Data structure:
|
959
|
+
|
960
|
+
.. code:: python
|
961
|
+
|
962
|
+
[
|
963
|
+
{
|
964
|
+
"id": "3aada397655d44d69f4fc899b9c88531", # 唯一ID
|
965
|
+
"name": "USD1", # 虚拟币简称
|
966
|
+
"name_abbr": "USD1", # 虚拟币全称
|
967
|
+
"final_name": "USD1", # 法币符号/展示名
|
968
|
+
"sort": 57, # 排序字段
|
969
|
+
"status": 1, # 状态 (1=可用, 0=不可用)
|
970
|
+
"type": "NEW", # 类型 (NEW=新币种)
|
971
|
+
"internal_id": "F20250508113754813fb3qX9NPxRoNUF", # 内部唯一流水号
|
972
|
+
"first_online_time": 1746676200000, # 首次上线时间
|
973
|
+
"online_time": 1746676200000, # 上线时间
|
974
|
+
"coin_partition": ["ob_trade_zone_defi"], # 所属交易区分类
|
975
|
+
"price_scale": 4, # 价格小数位数
|
976
|
+
"quantity_scale": 2, # 数量小数位数
|
977
|
+
"contract_decimal_mode": 1, # 合约精度模式
|
978
|
+
"contract_address": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d" # 代币合约地址
|
979
|
+
}
|
980
|
+
]
|
981
|
+
"""
|
982
|
+
return self._get("detail", SpotDetail)
|
983
|
+
|
984
|
+
@property
|
985
|
+
def orders(self) -> SpotOrders:
|
986
|
+
"""
|
987
|
+
现货订单数据流
|
988
|
+
|
989
|
+
Data structure:
|
990
|
+
|
991
|
+
.. code:: python
|
992
|
+
|
993
|
+
[
|
994
|
+
{
|
995
|
+
"order_id": "123456", # 订单ID
|
996
|
+
"symbol": "BTC_USDT", # 交易对
|
997
|
+
"currency": "USDT", # 币种
|
998
|
+
"market": "BTC_USDT", # 市场
|
999
|
+
"trade_type": "buy", # 交易类型
|
1000
|
+
"order_type": "limit", # 订单类型
|
1001
|
+
"price": "11000.0", # 委托价格
|
1002
|
+
"quantity": "0.01", # 委托数量
|
1003
|
+
"amount": "110.0", # 委托金额
|
1004
|
+
"deal_quantity": "0.01", # 成交数量
|
1005
|
+
"deal_amount": "110.0", # 成交金额
|
1006
|
+
"avg_price": "11000.0", # 成交均价
|
1007
|
+
"state": "open", # 订单状态
|
1008
|
+
"source": "api", # 来源
|
1009
|
+
"fee": "0.01", # 手续费
|
1010
|
+
"create_ts": 1625247600000,# 创建时间戳
|
1011
|
+
"unique_id": "abcdefg" # 唯一标识
|
1012
|
+
}
|
1013
|
+
]
|
1014
|
+
"""
|
1015
|
+
return self._get("order", SpotOrders)
|
1016
|
+
|
1017
|
+
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
1018
|
+
channel = msg.get("c")
|
1019
|
+
|
1020
|
+
if channel is None:
|
1021
|
+
return
|
1022
|
+
if 'increase.aggre.depth' in channel:
|
1023
|
+
self.book._on_message(msg)
|
1024
|
+
|
1025
|
+
if 'spot@private.orders' in channel:
|
1026
|
+
self.orders._on_message(msg)
|
1027
|
+
|
1028
|
+
if 'spot@private.balances' in channel:
|
1029
|
+
self.balance._on_message(msg)
|
1030
|
+
|
1031
|
+
if 'ticker' in channel:
|
1032
|
+
self.ticker._on_message(msg)
|
1033
|
+
|
1034
|
+
async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
|
1035
|
+
"""Initialize DataStore from HTTP response data."""
|
1036
|
+
for f in asyncio.as_completed(aws):
|
1037
|
+
res = await f
|
1038
|
+
data = await res.json()
|
1039
|
+
if res.url.path == "/api/platform/spot/market/depth":
|
1040
|
+
self.book._onresponse(data)
|
1041
|
+
if res.url.path == "/api/platform/spot/market/v2/tickers":
|
1042
|
+
self.ticker._onresponse(data)
|
1043
|
+
if res.url.path == "/api/assetbussiness/asset/spot/statistic":
|
1044
|
+
self.balance._onresponse(data)
|
1045
|
+
if res.url.path == "/api/platform/spot/order/current/orders/v2":
|
1046
|
+
self.orders._onresponse(data)
|
1047
|
+
if res.url.path == "/api/platform/spot/market/v2/symbols":
|
1048
|
+
self.detail._onresponse(data)
|
hyperquant/broker/ourbit.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
+
import asyncio
|
1
2
|
from typing import Literal, Optional
|
2
3
|
import pybotters
|
3
|
-
from .models.ourbit import OurbitSwapDataStore
|
4
|
+
from .models.ourbit import OurbitSwapDataStore, OurbitSpotDataStore
|
4
5
|
from decimal import Decimal, ROUND_HALF_UP
|
5
6
|
|
6
7
|
|
@@ -33,6 +34,7 @@ class OurbitSwap:
|
|
33
34
|
f"{self.api_url}/api/v1/private/order/list/open_orders?page_size=200",
|
34
35
|
f"{self.api_url}/api/v1/private/account/assets",
|
35
36
|
f"{self.api_url}/api/v1/contract/ticker",
|
37
|
+
f"{self.api_url}/api/platform/spot/market/v2/symbols"
|
36
38
|
]
|
37
39
|
|
38
40
|
url_map = {
|
@@ -306,3 +308,191 @@ class OurbitSwap:
|
|
306
308
|
f"{self.api_url}/api/v1/private/order/deal_details/{order_id}",
|
307
309
|
)
|
308
310
|
return self.ret_content(res)
|
311
|
+
|
312
|
+
|
313
|
+
class OurbitSpot:
|
314
|
+
|
315
|
+
def __init__(self, client: pybotters.Client):
|
316
|
+
"""
|
317
|
+
✅ 完成:
|
318
|
+
下单, 撤单, 查询资金, 查询持有订单, 查询历史订单
|
319
|
+
|
320
|
+
"""
|
321
|
+
self.client = client
|
322
|
+
self.store = OurbitSpotDataStore()
|
323
|
+
self.api_url = "https://www.ourbit.com"
|
324
|
+
self.ws_url = "wss://www.ourbit.com/ws"
|
325
|
+
|
326
|
+
async def __aenter__(self) -> "OurbitSpot":
|
327
|
+
client = self.client
|
328
|
+
await self.store.initialize(
|
329
|
+
client.get(f"{self.api_url}/api/platform/spot/market/v2/symbols")
|
330
|
+
)
|
331
|
+
return self
|
332
|
+
|
333
|
+
async def update(self, update_type: Literal["orders", "balance", "ticker", "book", "all"] = "all"):
|
334
|
+
|
335
|
+
all_urls = [
|
336
|
+
f"{self.api_url}/api/platform/spot/order/current/orders/v2?orderTypes=1%2C2%2C3%2C4%2C5%2C100&pageNum=1&pageSize=100&states=0%2C1%2C3",
|
337
|
+
f"{self.api_url}/api/assetbussiness/asset/spot/statistic",
|
338
|
+
f"{self.api_url}/api/platform/spot/market/v2/tickers"
|
339
|
+
]
|
340
|
+
|
341
|
+
# orderTypes=1%2C2%2C3%2C4%2C5%2C100&pageNum=1&pageSize=100&states=0%2C1%2C3
|
342
|
+
|
343
|
+
url_map = {
|
344
|
+
"orders": [all_urls[0]],
|
345
|
+
"balance": [all_urls[1]],
|
346
|
+
"ticker": [all_urls[2]],
|
347
|
+
"all": all_urls
|
348
|
+
}
|
349
|
+
|
350
|
+
try:
|
351
|
+
urls = url_map[update_type]
|
352
|
+
except KeyError:
|
353
|
+
raise ValueError(f"Unknown update type: {update_type}")
|
354
|
+
|
355
|
+
# 直接传协程进去,initialize 会自己 await
|
356
|
+
await self.store.initialize(*(self.client.get(url) for url in urls))
|
357
|
+
|
358
|
+
|
359
|
+
async def sub_personal(self):
|
360
|
+
"""订阅个人频道"""
|
361
|
+
# https://www.ourbit.com/ucenter/api/ws_token
|
362
|
+
res = await self.client.fetch(
|
363
|
+
'GET', f"{self.api_url}/ucenter/api/ws_token"
|
364
|
+
)
|
365
|
+
|
366
|
+
token = res.data['data'].get("wsToken")
|
367
|
+
|
368
|
+
|
369
|
+
self.client.ws_connect(
|
370
|
+
f"{self.ws_url}?wsToken={token}&platform=web",
|
371
|
+
send_json={
|
372
|
+
"method": "SUBSCRIPTION",
|
373
|
+
"params": [
|
374
|
+
"spot@private.orders",
|
375
|
+
"spot@private.trigger.orders",
|
376
|
+
"spot@private.balances"
|
377
|
+
],
|
378
|
+
"id": 1
|
379
|
+
},
|
380
|
+
hdlr_json=self.store.onmessage
|
381
|
+
)
|
382
|
+
|
383
|
+
async def sub_orderbook(self, symbols: str | list[str]):
|
384
|
+
"""订阅订单簿深度数据
|
385
|
+
|
386
|
+
Args:
|
387
|
+
symbols: 交易对符号,可以是单个字符串或字符串列表
|
388
|
+
"""
|
389
|
+
if isinstance(symbols, str):
|
390
|
+
symbols = [symbols]
|
391
|
+
|
392
|
+
# 并发获取每个交易对的初始深度数据
|
393
|
+
tasks = [
|
394
|
+
self.client.fetch('GET', f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}")
|
395
|
+
for symbol in symbols
|
396
|
+
]
|
397
|
+
|
398
|
+
# 等待所有请求完成
|
399
|
+
responses = await asyncio.gather(*tasks)
|
400
|
+
|
401
|
+
# 处理响应数据
|
402
|
+
for response in responses:
|
403
|
+
self.store.book._onresponse(response.data)
|
404
|
+
|
405
|
+
# 构建订阅参数
|
406
|
+
subscription_params = []
|
407
|
+
for symbol in symbols:
|
408
|
+
subscription_params.append(f"spot@public.increase.aggre.depth@{symbol}")
|
409
|
+
|
410
|
+
# 订阅WebSocket深度数据
|
411
|
+
self.client.ws_connect(
|
412
|
+
'wss://www.ourbit.com/ws?platform=web',
|
413
|
+
send_json={
|
414
|
+
"method": "SUBSCRIPTION",
|
415
|
+
"params": subscription_params,
|
416
|
+
"id": 2
|
417
|
+
},
|
418
|
+
hdlr_json=self.store.onmessage
|
419
|
+
)
|
420
|
+
|
421
|
+
async def place_order(
|
422
|
+
self,
|
423
|
+
symbol: str,
|
424
|
+
side: Literal["buy", "sell"],
|
425
|
+
price: float,
|
426
|
+
quantity: float = None,
|
427
|
+
order_type: Literal["market", "limit"] = "limit",
|
428
|
+
usdt_amount: float = None
|
429
|
+
):
|
430
|
+
"""现货下单
|
431
|
+
|
432
|
+
Args:
|
433
|
+
symbol: 交易对,如 "SOL_USDT"
|
434
|
+
side: 买卖方向 "buy" 或 "sell"
|
435
|
+
price: 价格
|
436
|
+
quantity: 数量
|
437
|
+
order_type: 订单类型 "market" 或 "limit"
|
438
|
+
usdt_amount: USDT金额,如果指定则根据价格计算数量
|
439
|
+
|
440
|
+
Returns:
|
441
|
+
订单响应数据
|
442
|
+
"""
|
443
|
+
# 解析交易对
|
444
|
+
currency, market = symbol.split("_")
|
445
|
+
|
446
|
+
detail = self.store.detail.get({
|
447
|
+
'name': currency
|
448
|
+
})
|
449
|
+
|
450
|
+
if not detail:
|
451
|
+
raise ValueError(f"Unknown currency: {currency}")
|
452
|
+
|
453
|
+
price_scale = detail.get('price_scale')
|
454
|
+
quantity_scale = detail.get('quantity_scale')
|
455
|
+
|
456
|
+
|
457
|
+
# 如果指定了USDT金额,重新计算数量
|
458
|
+
if usdt_amount is not None:
|
459
|
+
if side == "buy":
|
460
|
+
quantity = usdt_amount / price
|
461
|
+
else:
|
462
|
+
# 卖出时usdt_amount表示要卖出的币种价值
|
463
|
+
quantity = usdt_amount / price
|
464
|
+
|
465
|
+
# 格式化价格和数量
|
466
|
+
if price_scale is not None:
|
467
|
+
price = round(price, price_scale)
|
468
|
+
|
469
|
+
if quantity_scale is not None:
|
470
|
+
quantity = round(quantity, quantity_scale)
|
471
|
+
|
472
|
+
# 构建请求数据
|
473
|
+
data = {
|
474
|
+
"currency": currency,
|
475
|
+
"market": market,
|
476
|
+
"tradeType": side.upper(),
|
477
|
+
"quantity": str(quantity),
|
478
|
+
}
|
479
|
+
|
480
|
+
if order_type == "limit":
|
481
|
+
data["orderType"] = "LIMIT_ORDER"
|
482
|
+
data["price"] = str(price)
|
483
|
+
elif order_type == "market":
|
484
|
+
data["orderType"] = "MARKET_ORDER"
|
485
|
+
# 市价单通常不需要价格参数
|
486
|
+
|
487
|
+
res = await self.client.fetch(
|
488
|
+
"POST",
|
489
|
+
f"{self.api_url}/api/platform/spot/order/place",
|
490
|
+
json=data
|
491
|
+
)
|
492
|
+
|
493
|
+
# 处理响应
|
494
|
+
match res.data:
|
495
|
+
case {"msg": 'success'}:
|
496
|
+
return res.data["data"]
|
497
|
+
case _:
|
498
|
+
raise Exception(f"Failed to place order: {res.data}")
|
hyperquant/broker/ws.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
import asyncio
|
2
2
|
import pybotters
|
3
3
|
from pybotters.ws import ClientWebSocketResponse, logger
|
4
|
+
from pybotters.auth import Hosts
|
5
|
+
import yarl
|
6
|
+
|
4
7
|
|
5
8
|
class Heartbeat:
|
6
9
|
@staticmethod
|
@@ -34,4 +37,6 @@ class WssAuth:
|
|
34
37
|
else:
|
35
38
|
logger.warning(f"WebSocket login failed: {data}")
|
36
39
|
|
40
|
+
|
41
|
+
|
37
42
|
pybotters.ws.AuthHosts.items['futures.ourbit.com'] = pybotters.auth.Item("ourbit", WssAuth.ourbit)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hyperquant
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.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
|
@@ -4,18 +4,18 @@ hyperquant/db.py,sha256=i2TjkCbmH4Uxo7UTDvOYBfy973gLcGexdzuT_YcSeIE,6678
|
|
4
4
|
hyperquant/draw.py,sha256=up_lQ3pHeVLoNOyh9vPjgNwjD0M-6_IetSGviQUgjhY,54624
|
5
5
|
hyperquant/logkit.py,sha256=WALpXpIA3Ywr5DxKKK3k5EKubZ2h-ISGfc5dUReQUBQ,7795
|
6
6
|
hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
|
7
|
-
hyperquant/broker/auth.py,sha256=
|
7
|
+
hyperquant/broker/auth.py,sha256=oA9Yw1I59-u0Tnoj2e4wUup5q8V5T2qpga5RKbiAiZI,2614
|
8
8
|
hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
|
9
|
-
hyperquant/broker/ourbit.py,sha256=
|
10
|
-
hyperquant/broker/ws.py,sha256=
|
9
|
+
hyperquant/broker/ourbit.py,sha256=zXPJmdxQV7PyJIG_eX5dd6MBKy1xcUUggB7rZIAFouY,15835
|
10
|
+
hyperquant/broker/ws.py,sha256=QlidyxbVRvY_muOt3t4ycWcRP693zOTDJu2cLMo6PkI,1287
|
11
11
|
hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
|
12
12
|
hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
|
13
13
|
hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
|
14
|
-
hyperquant/broker/models/ourbit.py,sha256=
|
14
|
+
hyperquant/broker/models/ourbit.py,sha256=uh23LIgElDbXu8ps1mRyUEUE_7rytQkcu278lJYJ3-s,37321
|
15
15
|
hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
|
16
16
|
hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
|
17
17
|
hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
|
18
18
|
hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
|
19
|
-
hyperquant-0.
|
20
|
-
hyperquant-0.
|
21
|
-
hyperquant-0.
|
19
|
+
hyperquant-0.38.dist-info/METADATA,sha256=n9rIctGu54x9VTLHoSsRf2mtFNlen8lxqZrZmtgeSeI,4317
|
20
|
+
hyperquant-0.38.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
21
|
+
hyperquant-0.38.dist-info/RECORD,,
|
File without changes
|