hyperquant 0.4__py3-none-any.whl → 0.5__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/models/ourbit.py +118 -66
- hyperquant/broker/ourbit.py +158 -119
- hyperquant/core.py +10 -6
- hyperquant/logkit.py +16 -1
- {hyperquant-0.4.dist-info → hyperquant-0.5.dist-info}/METADATA +1 -1
- {hyperquant-0.4.dist-info → hyperquant-0.5.dist-info}/RECORD +7 -7
- {hyperquant-0.4.dist-info → hyperquant-0.5.dist-info}/WHEEL +0 -0
@@ -592,7 +592,11 @@ class SpotBalance(DataStore):
|
|
592
592
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
593
593
|
data = msg.get("d", {})
|
594
594
|
item = self._fmt_ws(data)
|
595
|
-
|
595
|
+
av = float(item.get("available", 0))
|
596
|
+
if av == 0:
|
597
|
+
self._find_and_delete({'currency': item.get("currency")})
|
598
|
+
else:
|
599
|
+
self._update([item])
|
596
600
|
|
597
601
|
|
598
602
|
# SpotOrders: 现货订单数据存储
|
@@ -661,13 +665,16 @@ class SpotOrders(DataStore):
|
|
661
665
|
|
662
666
|
state = d.get("status")
|
663
667
|
|
664
|
-
if state ==
|
668
|
+
if state == 1:
|
665
669
|
item["state"] = "open"
|
666
670
|
self._insert([item])
|
667
671
|
|
668
|
-
elif state == 3:
|
669
|
-
|
670
|
-
|
672
|
+
elif state == 3 or state == 2:
|
673
|
+
if state == 3:
|
674
|
+
item["state"] = "partially_filled"
|
675
|
+
if state == 2:
|
676
|
+
item["state"] = "filled"
|
677
|
+
|
671
678
|
# 如果这三个字段存在追加
|
672
679
|
if d.get("singleDealId") and d.get("singleDealPrice") and d.get("singleDealQuantity"):
|
673
680
|
item.update({
|
@@ -686,6 +693,7 @@ class SpotOrders(DataStore):
|
|
686
693
|
|
687
694
|
|
688
695
|
|
696
|
+
|
689
697
|
class SpotBook(DataStore):
|
690
698
|
_KEYS = ["s", "S", 'p']
|
691
699
|
|
@@ -693,69 +701,110 @@ class SpotBook(DataStore):
|
|
693
701
|
# super().__init__()
|
694
702
|
self._time: int | None = None
|
695
703
|
self.limit = 1
|
704
|
+
self.loss = {} # 改为字典,按symbol跟踪
|
705
|
+
self.versions = {}
|
706
|
+
self.cache = []
|
696
707
|
|
697
708
|
def _onresponse(self, data: dict[str, Any]):
|
709
|
+
data = data.get("data")
|
710
|
+
symbol = data.get("symbol")
|
711
|
+
book_data = data.get("data")
|
712
|
+
asks = book_data.get("asks", [])
|
713
|
+
bids = book_data.get("bids", [])
|
714
|
+
version = int(data.get("version", None))
|
698
715
|
|
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
716
|
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
items: list
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
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
|
717
|
+
# 保存当前快照版本
|
718
|
+
self.versions[symbol] = version
|
719
|
+
|
720
|
+
# # 应用缓存的增量(只保留连续的部分)
|
721
|
+
# items: list = self.find({"s": symbol})
|
722
|
+
# items.sort(key=lambda x: x.get("fv", 0)) # 按 fromVersion 排序
|
723
|
+
# self._find_and_delete({"s": symbol})
|
724
|
+
|
725
|
+
# 处理缓存
|
726
|
+
items = [item for item in self.cache if item.get("s") == symbol]
|
727
|
+
items.sort(key=lambda x: x.get("fv", 0)) # 按 fromVersion 排序
|
728
|
+
self.cache = [item for item in self.cache if item.get("s") != symbol]
|
724
729
|
|
725
730
|
for side, S in ((asks, "a"), (bids, "b")):
|
726
|
-
for
|
727
|
-
|
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})
|
731
|
+
for item in side:
|
732
|
+
self._insert([{"s": symbol, "S": S, "p": item["p"], "q": item["q"]}])
|
739
733
|
|
740
734
|
if items:
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
735
|
+
min_version = min(item.get("fv", 0) for item in items)
|
736
|
+
max_version = max(item.get("tv", 0) for item in items)
|
737
|
+
# self.version = max_version
|
738
|
+
self.versions[symbol] = max_version
|
739
|
+
|
740
|
+
# if max_version == 0:
|
741
|
+
# print('vvv---')
|
742
|
+
# print(items)
|
743
|
+
|
744
|
+
if not (min_version <= self.versions[symbol] <= max_version):
|
745
|
+
self.loss[symbol] = True
|
746
|
+
logger.warning(f"SpotBook: Snapshot version {self.version} out of range ({min_version}, {max_version}) for symbol={symbol} (丢补丁)")
|
747
|
+
return
|
748
|
+
|
749
|
+
# 处理过往msg内容
|
750
|
+
self.loss[symbol] = False
|
751
|
+
for item in items:
|
752
|
+
fv, tv = item.get("fv", 0), item.get("tv", 0)
|
753
|
+
if self.versions[symbol] <= tv and self.versions[symbol] >= fv:
|
754
|
+
if float(item["q"]) == 0.0:
|
755
|
+
self._find_and_delete({"s": symbol, "S": item["S"], "p": item["p"]})
|
756
|
+
else:
|
757
|
+
self._insert([{ "s": symbol, "S": item["S"], "p": item["p"], "q": item["q"]}])
|
758
|
+
|
759
|
+
sort_data = self.sorted({'s': symbol}, self.limit)
|
760
|
+
asks = sort_data.get('a', [])
|
761
|
+
bids = sort_data.get('b', [])
|
762
|
+
self._find_and_delete({'s': symbol})
|
763
|
+
self._update(asks + bids)
|
764
|
+
|
765
|
+
else:
|
766
|
+
self.loss[symbol] = False
|
749
767
|
|
750
768
|
|
751
769
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
752
|
-
|
770
|
+
|
771
|
+
# ts = time.time() * 1000 # 预留时间戳(如需记录可用)
|
753
772
|
data = msg.get("d", {}) or {}
|
754
773
|
symbol = msg.get("s")
|
755
|
-
|
774
|
+
fv = int(data.get("fromVersion"))
|
775
|
+
tv = int(data.get("toVersion"))
|
776
|
+
if fv == 0 or tv == 0:
|
777
|
+
# print(f'发现fv或tv为0, msg:\n {msg}')
|
778
|
+
return
|
779
|
+
|
756
780
|
asks: list = data.get("asks", []) or []
|
757
781
|
bids: list = data.get("bids", []) or []
|
758
782
|
|
783
|
+
now_version = self.versions.get(symbol, None)
|
784
|
+
|
785
|
+
# 以下几张情况都会被认为正常
|
786
|
+
check_con = (
|
787
|
+
now_version is None or
|
788
|
+
fv <= now_version <= tv or
|
789
|
+
now_version + 1 == fv
|
790
|
+
)
|
791
|
+
|
792
|
+
if not check_con:
|
793
|
+
# logger.warning(f"(丢补丁) version:{now_version} fv:{fv} tv:{tv} ")
|
794
|
+
self.loss[symbol] = True # 暂时不这样做
|
795
|
+
|
796
|
+
|
797
|
+
|
798
|
+
if self.loss.get(symbol, True):
|
799
|
+
for item in asks:
|
800
|
+
self.cache.append({"s": symbol, "S": "a", "p": item["p"], "q": item["q"], "fv": fv, "tv": tv})
|
801
|
+
for item in bids:
|
802
|
+
self.cache.append({"s": symbol, "S": "b", "p": item["p"], "q": item["q"], "fv": fv, "tv": tv})
|
803
|
+
return
|
804
|
+
|
805
|
+
self.versions[symbol] = tv
|
806
|
+
|
807
|
+
|
759
808
|
to_delete, to_update = [], []
|
760
809
|
for side, S in ((asks, "a"), (bids, "b")):
|
761
810
|
for item in side:
|
@@ -929,8 +978,7 @@ class OurbitSpotDataStore(DataStoreCollection):
|
|
929
978
|
"""
|
930
979
|
现货账户余额数据流
|
931
980
|
|
932
|
-
|
933
|
-
|
981
|
+
_KEYS = ["currency"]
|
934
982
|
.. code:: python
|
935
983
|
|
936
984
|
[
|
@@ -984,30 +1032,34 @@ class OurbitSpotDataStore(DataStoreCollection):
|
|
984
1032
|
@property
|
985
1033
|
def orders(self) -> SpotOrders:
|
986
1034
|
"""
|
987
|
-
|
1035
|
+
现货订单数据流(SpotOrders)
|
988
1036
|
|
989
|
-
|
1037
|
+
Keys: ["order_id"]
|
990
1038
|
|
991
|
-
|
1039
|
+
说明:
|
1040
|
+
- 聚合 REST 当前订单与 WS 增量推送(频道: spot@private.orders)
|
1041
|
+
- 统一状态值:open, partially_filled, filled, canceled
|
992
1042
|
|
1043
|
+
Data structure:
|
1044
|
+
.. code:: python
|
993
1045
|
[
|
994
1046
|
{
|
995
1047
|
"order_id": "123456", # 订单ID
|
996
1048
|
"symbol": "BTC_USDT", # 交易对
|
997
1049
|
"currency": "USDT", # 币种
|
998
1050
|
"market": "BTC_USDT", # 市场
|
999
|
-
"trade_type": "buy", #
|
1000
|
-
"order_type": "limit", #
|
1051
|
+
"trade_type": "buy", # buy/sell
|
1052
|
+
"order_type": "limit", # limit/market
|
1001
1053
|
"price": "11000.0", # 委托价格
|
1002
1054
|
"quantity": "0.01", # 委托数量
|
1003
1055
|
"amount": "110.0", # 委托金额
|
1004
|
-
"deal_quantity": "0.01", #
|
1005
|
-
"deal_amount": "110.0", #
|
1006
|
-
"avg_price": "11000.0", #
|
1007
|
-
"state": "open", #
|
1008
|
-
"source": "api", # 来源
|
1056
|
+
"deal_quantity": "0.01", # 已成交数量(累计)
|
1057
|
+
"deal_amount": "110.0", # 已成交金额(累计)
|
1058
|
+
"avg_price": "11000.0", # 成交均价(累计)
|
1059
|
+
"state": "open", # open/partially_filled/filled/canceled
|
1009
1060
|
"fee": "0.01", # 手续费
|
1010
|
-
"
|
1061
|
+
"source": "api", # 来源
|
1062
|
+
"create_ts": 1625247600000,# 创建时间戳(毫秒)
|
1011
1063
|
"unique_id": "abcdefg" # 唯一标识
|
1012
1064
|
}
|
1013
1065
|
]
|
@@ -1015,7 +1067,7 @@ class OurbitSpotDataStore(DataStoreCollection):
|
|
1015
1067
|
return self._get("order", SpotOrders)
|
1016
1068
|
|
1017
1069
|
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
1018
|
-
|
1070
|
+
# print(msg, '\n')
|
1019
1071
|
channel = msg.get("c")
|
1020
1072
|
if 'msg' in msg:
|
1021
1073
|
if 'invalid' in msg['msg']:
|
hyperquant/broker/ourbit.py
CHANGED
@@ -26,7 +26,8 @@ class OurbitSwap:
|
|
26
26
|
return self
|
27
27
|
|
28
28
|
async def update(
|
29
|
-
self,
|
29
|
+
self,
|
30
|
+
update_type: Literal["position", "orders", "balance", "ticker", "all"] = "all",
|
30
31
|
):
|
31
32
|
"""由于交易所很多不支持ws推送,这里使用Rest"""
|
32
33
|
all_urls = [
|
@@ -34,7 +35,7 @@ class OurbitSwap:
|
|
34
35
|
f"{self.api_url}/api/v1/private/order/list/open_orders?page_size=200",
|
35
36
|
f"{self.api_url}/api/v1/private/account/assets",
|
36
37
|
f"{self.api_url}/api/v1/contract/ticker",
|
37
|
-
f"{self.api_url}/api/platform/spot/market/v2/symbols"
|
38
|
+
f"{self.api_url}/api/platform/spot/market/v2/symbols",
|
38
39
|
]
|
39
40
|
|
40
41
|
url_map = {
|
@@ -56,13 +57,8 @@ class OurbitSwap:
|
|
56
57
|
async def sub_tickers(self):
|
57
58
|
self.client.ws_connect(
|
58
59
|
self.ws_url,
|
59
|
-
send_json={
|
60
|
-
|
61
|
-
"param": {
|
62
|
-
"timezone": "UTC+8"
|
63
|
-
}
|
64
|
-
},
|
65
|
-
hdlr_json=self.store.onmessage
|
60
|
+
send_json={"method": "sub.tickers", "param": {"timezone": "UTC+8"}},
|
61
|
+
hdlr_json=self.store.onmessage,
|
66
62
|
)
|
67
63
|
|
68
64
|
async def sub_orderbook(self, symbols: str | list[str]):
|
@@ -74,26 +70,23 @@ class OurbitSwap:
|
|
74
70
|
|
75
71
|
for symbol in symbols:
|
76
72
|
step = self.store.detail.find({"symbol": symbol})[0].get("tick_size")
|
77
|
-
|
78
|
-
send_jsons.append(
|
79
|
-
|
80
|
-
|
81
|
-
"symbol": symbol,
|
82
|
-
"step": str(step)
|
73
|
+
|
74
|
+
send_jsons.append(
|
75
|
+
{
|
76
|
+
"method": "sub.depth.step",
|
77
|
+
"param": {"symbol": symbol, "step": str(step)},
|
83
78
|
}
|
84
|
-
|
79
|
+
)
|
85
80
|
|
86
81
|
await self.client.ws_connect(
|
87
|
-
self.ws_url,
|
88
|
-
send_json=send_jsons,
|
89
|
-
hdlr_json=self.store.onmessage
|
82
|
+
self.ws_url, send_json=send_jsons, hdlr_json=self.store.onmessage
|
90
83
|
)
|
91
84
|
|
92
85
|
async def sub_personal(self):
|
93
86
|
self.client.ws_connect(
|
94
87
|
self.ws_url,
|
95
|
-
send_json={
|
96
|
-
hdlr_json=self.store.onmessage
|
88
|
+
send_json={"method": "sub.personal.user.preference"},
|
89
|
+
hdlr_json=self.store.onmessage,
|
97
90
|
)
|
98
91
|
|
99
92
|
def ret_content(self, res: pybotters.FetchResult):
|
@@ -102,13 +95,15 @@ class OurbitSwap:
|
|
102
95
|
return res.data["data"]
|
103
96
|
case _:
|
104
97
|
raise Exception(f"Failed api {res.response.url}: {res.data}")
|
105
|
-
|
106
98
|
|
107
99
|
def fmt_price(self, symbol, price: float) -> float:
|
108
100
|
tick = self.store.detail.find({"symbol": symbol})[0].get("tick_size")
|
109
101
|
tick_dec = Decimal(str(tick))
|
110
102
|
price_dec = Decimal(str(price))
|
111
|
-
return float(
|
103
|
+
return float(
|
104
|
+
(price_dec / tick_dec).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
105
|
+
* tick_dec
|
106
|
+
)
|
112
107
|
|
113
108
|
async def place_order(
|
114
109
|
self,
|
@@ -135,14 +130,13 @@ class OurbitSwap:
|
|
135
130
|
raise ValueError("params err")
|
136
131
|
|
137
132
|
max_lev = self.store.detail.find({"symbol": symbol})[0].get("max_lev")
|
138
|
-
|
133
|
+
|
139
134
|
if usdt_amount is not None:
|
140
135
|
cs = self.store.detail.find({"symbol": symbol})[0].get("contract_sz")
|
141
136
|
size = max(int(usdt_amount / cs / price), 1)
|
142
137
|
|
143
138
|
if price is not None:
|
144
139
|
price = self.fmt_price(symbol, price)
|
145
|
-
|
146
140
|
|
147
141
|
leverage = min(max_lev, leverage)
|
148
142
|
|
@@ -165,22 +159,23 @@ class OurbitSwap:
|
|
165
159
|
data["price"] = str(price)
|
166
160
|
|
167
161
|
if "close" in side:
|
168
|
-
if side ==
|
162
|
+
if side == "close_buy":
|
169
163
|
data["side"] = 2
|
170
|
-
elif side ==
|
164
|
+
elif side == "close_sell":
|
171
165
|
data["side"] = 4
|
172
|
-
|
166
|
+
|
173
167
|
if position_id is None:
|
174
168
|
raise ValueError("position_id is required for closing position")
|
175
169
|
data["positionId"] = position_id
|
176
170
|
# import time
|
177
171
|
# print(time.time(), '下单')
|
178
|
-
res =
|
172
|
+
res = await self.client.fetch(
|
179
173
|
"POST", f"{self.api_url}/api/v1/private/order/create", data=data
|
180
174
|
)
|
181
175
|
return self.ret_content(res)
|
182
|
-
|
183
|
-
async def place_tpsl(
|
176
|
+
|
177
|
+
async def place_tpsl(
|
178
|
+
self,
|
184
179
|
position_id: int,
|
185
180
|
take_profit: Optional[float] = None,
|
186
181
|
stop_loss: Optional[float] = None,
|
@@ -214,12 +209,9 @@ class OurbitSwap:
|
|
214
209
|
data["takeProfitPrice"] = take_profit
|
215
210
|
if stop_loss is not None:
|
216
211
|
data["stopLossPrice"] = stop_loss
|
217
|
-
|
218
212
|
|
219
213
|
res = await self.client.fetch(
|
220
|
-
"POST",
|
221
|
-
f"{self.api_url}/api/v1/private/stoporder/place",
|
222
|
-
data=data
|
214
|
+
"POST", f"{self.api_url}/api/v1/private/stoporder/place", data=data
|
223
215
|
)
|
224
216
|
|
225
217
|
return self.ret_content(res)
|
@@ -312,7 +304,7 @@ class OurbitSwap:
|
|
312
304
|
|
313
305
|
class OurbitSpot:
|
314
306
|
|
315
|
-
def __init__(self, client: pybotters.Client):
|
307
|
+
def __init__(self, client: pybotters.Client, personal_msg_cb: callable=None):
|
316
308
|
"""
|
317
309
|
✅ 完成:
|
318
310
|
下单, 撤单, 查询资金, 查询持有订单, 查询历史订单
|
@@ -322,6 +314,7 @@ class OurbitSpot:
|
|
322
314
|
self.store = OurbitSpotDataStore()
|
323
315
|
self.api_url = "https://www.ourbit.com"
|
324
316
|
self.ws_url = "wss://www.ourbit.com/ws"
|
317
|
+
self.personal_msg_cb = personal_msg_cb
|
325
318
|
|
326
319
|
async def __aenter__(self) -> "OurbitSpot":
|
327
320
|
client = self.client
|
@@ -330,171 +323,217 @@ class OurbitSpot:
|
|
330
323
|
)
|
331
324
|
return self
|
332
325
|
|
333
|
-
async def update(
|
326
|
+
async def update(
|
327
|
+
self, update_type: Literal["orders", "balance", "ticker", "book", "all"] = "all"
|
328
|
+
):
|
334
329
|
|
335
330
|
all_urls = [
|
336
331
|
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
332
|
f"{self.api_url}/api/assetbussiness/asset/spot/statistic",
|
338
|
-
f"{self.api_url}/api/platform/spot/market/v2/tickers"
|
333
|
+
f"{self.api_url}/api/platform/spot/market/v2/tickers",
|
339
334
|
]
|
340
335
|
|
341
336
|
# orderTypes=1%2C2%2C3%2C4%2C5%2C100&pageNum=1&pageSize=100&states=0%2C1%2C3
|
342
|
-
|
337
|
+
|
343
338
|
url_map = {
|
344
339
|
"orders": [all_urls[0]],
|
345
340
|
"balance": [all_urls[1]],
|
346
341
|
"ticker": [all_urls[2]],
|
347
|
-
"all": all_urls
|
342
|
+
"all": all_urls,
|
348
343
|
}
|
349
344
|
|
350
345
|
try:
|
351
346
|
urls = url_map[update_type]
|
352
347
|
except KeyError:
|
353
348
|
raise ValueError(f"Unknown update type: {update_type}")
|
354
|
-
|
349
|
+
|
355
350
|
# 直接传协程进去,initialize 会自己 await
|
356
351
|
await self.store.initialize(*(self.client.get(url) for url in urls))
|
357
352
|
|
358
|
-
|
359
353
|
async def sub_personal(self):
|
360
354
|
"""订阅个人频道"""
|
361
355
|
# 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")
|
356
|
+
res = await self.client.fetch("GET", f"{self.api_url}/ucenter/api/ws_token")
|
367
357
|
|
358
|
+
token = res.data["data"].get("wsToken")
|
368
359
|
|
369
|
-
self.client.ws_connect(
|
360
|
+
app = self.client.ws_connect(
|
370
361
|
f"{self.ws_url}?wsToken={token}&platform=web",
|
371
362
|
send_json={
|
372
363
|
"method": "SUBSCRIPTION",
|
373
364
|
"params": [
|
374
365
|
"spot@private.orders",
|
375
366
|
"spot@private.trigger.orders",
|
376
|
-
"spot@private.balances"
|
367
|
+
"spot@private.balances",
|
377
368
|
],
|
378
|
-
"id": 1
|
369
|
+
"id": 1,
|
379
370
|
},
|
380
|
-
hdlr_json=
|
371
|
+
hdlr_json=(
|
372
|
+
self.store.onmessage
|
373
|
+
if self.personal_msg_cb is None
|
374
|
+
else [self.store.onmessage, self.personal_msg_cb]
|
375
|
+
),
|
381
376
|
)
|
382
377
|
|
378
|
+
await app._event.wait()
|
379
|
+
|
383
380
|
async def sub_orderbook(self, symbols: str | list[str]):
|
384
381
|
"""订阅订单簿深度数据
|
385
|
-
|
382
|
+
|
386
383
|
Args:
|
387
384
|
symbols: 交易对符号,可以是单个字符串或字符串列表
|
388
385
|
"""
|
386
|
+
import logging
|
387
|
+
|
388
|
+
logger = logging.getLogger("OurbitSpot")
|
389
|
+
|
389
390
|
if isinstance(symbols, str):
|
390
391
|
symbols = [symbols]
|
391
392
|
|
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
393
|
# 构建订阅参数
|
406
394
|
subscription_params = []
|
407
395
|
for symbol in symbols:
|
408
396
|
subscription_params.append(f"spot@public.increase.aggre.depth@{symbol}")
|
409
397
|
|
410
|
-
|
411
398
|
# 一次sub20个,超过需要分开订阅
|
412
399
|
for i in range(0, len(subscription_params), 20):
|
413
|
-
self.client.ws_connect(
|
414
|
-
|
400
|
+
wsapp = self.client.ws_connect(
|
401
|
+
"wss://www.ourbit.com/ws?platform=web",
|
415
402
|
send_json={
|
416
403
|
"method": "SUBSCRIPTION",
|
417
|
-
"params": subscription_params[i:i + 20],
|
418
|
-
"id": 2
|
404
|
+
"params": subscription_params[i : i + 20],
|
405
|
+
"id": 2,
|
419
406
|
},
|
420
|
-
hdlr_json=self.store.onmessage
|
407
|
+
hdlr_json=self.store.onmessage,
|
421
408
|
)
|
409
|
+
await wsapp._event.wait()
|
410
|
+
|
411
|
+
# await asyncio.sleep(1) # 等待ws连接稳定
|
412
|
+
|
413
|
+
# 并发获取每个交易对的初始深度数据
|
414
|
+
tasks = [
|
415
|
+
self.client.fetch(
|
416
|
+
"GET", f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}"
|
417
|
+
)
|
418
|
+
for symbol in symbols
|
419
|
+
]
|
420
|
+
|
421
|
+
# 等待所有请求完成
|
422
|
+
responses = await asyncio.gather(*tasks)
|
423
|
+
|
424
|
+
# 处理响应数据
|
425
|
+
for idx, response in enumerate(responses):
|
426
|
+
symbol = symbols[idx]
|
427
|
+
self.store.book._onresponse(response.data)
|
428
|
+
|
429
|
+
async def check_loss():
|
430
|
+
await asyncio.sleep(1)
|
431
|
+
while True:
|
432
|
+
loss = self.store.book.loss
|
433
|
+
for symbol, is_loss in loss.items():
|
434
|
+
if is_loss:
|
435
|
+
resp = await self.client.fetch(
|
436
|
+
"GET",
|
437
|
+
f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}",
|
438
|
+
)
|
439
|
+
self.store.book._onresponse(resp.data)
|
440
|
+
await asyncio.sleep(1)
|
441
|
+
|
442
|
+
asyncio.create_task(check_loss())
|
422
443
|
|
423
444
|
async def place_order(
|
424
445
|
self,
|
425
446
|
symbol: str,
|
426
447
|
side: Literal["buy", "sell"],
|
427
|
-
price: float,
|
448
|
+
price: float = None,
|
428
449
|
quantity: float = None,
|
429
450
|
order_type: Literal["market", "limit"] = "limit",
|
430
|
-
usdt_amount: float = None
|
451
|
+
usdt_amount: float = None,
|
431
452
|
):
|
432
453
|
"""现货下单
|
433
|
-
|
454
|
+
|
434
455
|
Args:
|
435
456
|
symbol: 交易对,如 "SOL_USDT"
|
436
457
|
side: 买卖方向 "buy" 或 "sell"
|
437
|
-
price:
|
458
|
+
price: 价格,市价单可为None
|
438
459
|
quantity: 数量
|
439
460
|
order_type: 订单类型 "market" 或 "limit"
|
440
461
|
usdt_amount: USDT金额,如果指定则根据价格计算数量
|
441
|
-
|
462
|
+
|
442
463
|
Returns:
|
443
464
|
订单响应数据
|
444
465
|
"""
|
466
|
+
# 参数检查
|
467
|
+
if order_type == "limit" and price is None:
|
468
|
+
raise ValueError("Limit orders require a price")
|
469
|
+
if quantity is None and usdt_amount is None:
|
470
|
+
raise ValueError("Either quantity or usdt_amount must be specified")
|
471
|
+
|
445
472
|
# 解析交易对
|
446
|
-
|
473
|
+
parts = symbol.split("_")
|
474
|
+
if len(parts) != 2:
|
475
|
+
raise ValueError(f"Invalid symbol format: {symbol}")
|
447
476
|
|
448
|
-
|
449
|
-
'name': currency
|
450
|
-
})
|
477
|
+
currency, market = parts
|
451
478
|
|
479
|
+
# 获取交易对详情
|
480
|
+
detail = self.store.detail.get({"name": currency})
|
452
481
|
if not detail:
|
453
482
|
raise ValueError(f"Unknown currency: {currency}")
|
454
483
|
|
455
|
-
price_scale = detail.get(
|
456
|
-
quantity_scale = detail.get(
|
484
|
+
price_scale = detail.get("price_scale")
|
485
|
+
quantity_scale = detail.get("quantity_scale")
|
457
486
|
|
458
|
-
|
459
|
-
# 如果指定了USDT金额,重新计算数量
|
460
|
-
if usdt_amount is not None:
|
461
|
-
if side == "buy":
|
462
|
-
quantity = usdt_amount / price
|
463
|
-
else:
|
464
|
-
# 卖出时usdt_amount表示要卖出的币种价值
|
465
|
-
quantity = usdt_amount / price
|
466
|
-
|
467
|
-
# 格式化价格和数量
|
468
|
-
if price_scale is not None:
|
469
|
-
price = round(price, price_scale)
|
470
|
-
|
471
|
-
if quantity_scale is not None:
|
472
|
-
quantity = round(quantity, quantity_scale)
|
473
|
-
|
474
487
|
# 构建请求数据
|
475
|
-
data = {
|
476
|
-
|
477
|
-
|
478
|
-
"tradeType": side.upper(),
|
479
|
-
"quantity": str(quantity),
|
480
|
-
}
|
481
|
-
|
488
|
+
data = {"currency": currency, "market": market, "tradeType": side.upper()}
|
489
|
+
|
490
|
+
# 处理市价单和限价单的不同参数
|
482
491
|
if order_type == "limit":
|
483
492
|
data["orderType"] = "LIMIT_ORDER"
|
484
|
-
data["price"] = str(
|
493
|
+
data["price"] = str(
|
494
|
+
round(price, price_scale) if price_scale is not None else price
|
495
|
+
)
|
496
|
+
|
497
|
+
# 计算并设置数量
|
498
|
+
if quantity is None and usdt_amount is not None and price:
|
499
|
+
quantity = usdt_amount / price
|
500
|
+
|
501
|
+
if quantity_scale is not None:
|
502
|
+
quantity = round(quantity, quantity_scale)
|
503
|
+
data["quantity"] = str(quantity)
|
504
|
+
|
485
505
|
elif order_type == "market":
|
486
506
|
data["orderType"] = "MARKET_ORDER"
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
507
|
+
|
508
|
+
# 市价单可以使用数量或金额,但不能同时使用
|
509
|
+
if usdt_amount is not None:
|
510
|
+
data["amount"] = str(usdt_amount)
|
511
|
+
else:
|
512
|
+
if quantity_scale is not None:
|
513
|
+
quantity = round(quantity, quantity_scale)
|
514
|
+
data["quantity"] = str(quantity)
|
515
|
+
|
516
|
+
if price:
|
517
|
+
data["price"] = str(price)
|
518
|
+
|
519
|
+
# 确定API端点
|
520
|
+
url = f'{self.api_url}/api/platform/spot/{"v4/order/place" if order_type == "market" else "order/place"}'
|
521
|
+
# print(f"Placing {symbol}: {data}")
|
522
|
+
# 发送请求
|
523
|
+
res = await self.client.fetch("POST", url, json=data)
|
524
|
+
|
495
525
|
# 处理响应
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
526
|
+
if res.data.get("msg") == "success":
|
527
|
+
return res.data["data"]
|
528
|
+
raise Exception(f"Failed to place order: {res.data}")
|
529
|
+
|
530
|
+
async def cancel_orders(self, order_ids: list[str]):
|
531
|
+
|
532
|
+
for order_id in order_ids:
|
533
|
+
url = f"{self.api_url}/api/platform/spot/order/cancel/v2?orderId={order_id}"
|
534
|
+
await self.client.fetch("DELETE", url)
|
535
|
+
|
536
|
+
async def cancel_order(self, order_id: str):
|
537
|
+
|
538
|
+
url = f"{self.api_url}/api/platform/spot/order/cancel/v2?orderId={order_id}"
|
539
|
+
res = await self.client.fetch("DELETE", url)
|
hyperquant/core.py
CHANGED
@@ -269,10 +269,10 @@ class ExchangeBase:
|
|
269
269
|
)
|
270
270
|
|
271
271
|
class Exchange(ExchangeBase):
|
272
|
-
def __init__(self, trade_symbols, fee=0.0002, initial_balance=10000, recorded=False):
|
272
|
+
def __init__(self, trade_symbols:list=[], fee=0.0002, initial_balance=10000, recorded=False):
|
273
273
|
super().__init__(initial_balance=initial_balance, recorded=recorded)
|
274
274
|
self.fee = fee
|
275
|
-
self.trade_symbols = trade_symbols
|
275
|
+
self.trade_symbols:list = trade_symbols
|
276
276
|
self.id_gen = 0
|
277
277
|
self.account['USDT'].update({
|
278
278
|
'hold': 0,
|
@@ -280,8 +280,12 @@ class Exchange(ExchangeBase):
|
|
280
280
|
'short': 0
|
281
281
|
})
|
282
282
|
for symbol in trade_symbols:
|
283
|
-
self.account[symbol] =
|
284
|
-
|
283
|
+
self.account[symbol] = self._act_template
|
284
|
+
|
285
|
+
@property
|
286
|
+
def _act_template(self):
|
287
|
+
return {'amount': 0, 'hold_price': 0, 'value': 0, 'price': 0,
|
288
|
+
'realised_profit': 0, 'unrealised_profit': 0, 'fee': 0}.copy()
|
285
289
|
|
286
290
|
def Trade(self, symbol, direction, price, amount, **kwargs):
|
287
291
|
if self.recorded and 'time' not in kwargs:
|
@@ -307,8 +311,7 @@ class Exchange(ExchangeBase):
|
|
307
311
|
|
308
312
|
if symbol not in self.trade_symbols:
|
309
313
|
self.trade_symbols.append(symbol)
|
310
|
-
self.account[symbol] =
|
311
|
-
'realised_profit': 0, 'unrealised_profit': 0, 'fee': 0}
|
314
|
+
self.account[symbol] = self._act_template
|
312
315
|
|
313
316
|
cover_amount = 0 if direction * self.account[symbol]['amount'] >= 0 else min(abs(self.account[symbol]['amount']), amount)
|
314
317
|
open_amount = amount - cover_amount
|
@@ -343,6 +346,7 @@ class Exchange(ExchangeBase):
|
|
343
346
|
|
344
347
|
if kwargs:
|
345
348
|
self.opt.update(kwargs)
|
349
|
+
self.account[symbol].update(kwargs)
|
346
350
|
|
347
351
|
# 记录账户总资产到 history
|
348
352
|
if self.recorded:
|
hyperquant/logkit.py
CHANGED
@@ -146,10 +146,25 @@ class MinConsoleHandler(logging.StreamHandler):
|
|
146
146
|
else:
|
147
147
|
super().emit(record)
|
148
148
|
|
149
|
+
# ====================================================================================================
|
150
|
+
# ** NullLogger 类,用于禁用日志 **
|
151
|
+
# ====================================================================================================
|
152
|
+
class NullLogger:
|
153
|
+
def debug(self, *a, **k): pass
|
154
|
+
def info(self, *a, **k): pass
|
155
|
+
def ok(self, *a, **k): pass
|
156
|
+
def warning(self, *a, **k): pass
|
157
|
+
def error(self, *a, **k): pass
|
158
|
+
def critical(self, *a, **k): pass
|
159
|
+
def exception(self, *a, **k): pass
|
160
|
+
def divider(self, *a, **k): pass
|
161
|
+
|
149
162
|
# ====================================================================================================
|
150
163
|
# ** 功能函数 **
|
151
164
|
# ====================================================================================================
|
152
|
-
def get_logger(name=None, file_path=None, show_time=False, use_color=True, timezone="Asia/Shanghai"
|
165
|
+
def get_logger(name=None, file_path=None, show_time=False, use_color=True, timezone="Asia/Shanghai", level: object = None, enable_console: bool = True, enabled: bool = True):
|
166
|
+
if not enabled:
|
167
|
+
return NullLogger()
|
153
168
|
if name is None:
|
154
169
|
name = '_'
|
155
170
|
logger_instance = Logger(name, show_time, use_color, timezone) # 传递时区参数
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hyperquant
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5
|
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,21 +1,21 @@
|
|
1
1
|
hyperquant/__init__.py,sha256=UpjiX4LS5jmrBc2kE8RiLR02eCfD8JDQrR1q8zkLNcQ,161
|
2
|
-
hyperquant/core.py,sha256=
|
2
|
+
hyperquant/core.py,sha256=iEI8qTNpyesB_w67SrKXeGoB9JllovBeJKI0EZFYew4,20631
|
3
3
|
hyperquant/db.py,sha256=i2TjkCbmH4Uxo7UTDvOYBfy973gLcGexdzuT_YcSeIE,6678
|
4
4
|
hyperquant/draw.py,sha256=up_lQ3pHeVLoNOyh9vPjgNwjD0M-6_IetSGviQUgjhY,54624
|
5
|
-
hyperquant/logkit.py,sha256=
|
5
|
+
hyperquant/logkit.py,sha256=nUo7nx5eONvK39GOhWwS41zNRL756P2J7-5xGzwXnTY,8462
|
6
6
|
hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
|
7
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=
|
9
|
+
hyperquant/broker/ourbit.py,sha256=Fza_nhfoSCf1Ulm7UHOlt969Wm37bKja9P5xpN93XqY,17902
|
10
10
|
hyperquant/broker/ws.py,sha256=umRzxwCaZaRIgIq4YY-AuA0wCXFT0uOBmQbIXFY8CK0,1555
|
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=QuKxUYlJzRC4zr0lNiz3dpireConbsRyOtOvA0_VTVA,39978
|
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.5.dist-info/METADATA,sha256=A-fC66EHBajjznMPB06z6oaMCWHEy3TTmUUXxK2CLcU,4316
|
20
|
+
hyperquant-0.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
21
|
+
hyperquant-0.5.dist-info/RECORD,,
|
File without changes
|