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.
@@ -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
- self._update([item])
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 == 2 or state == 1:
668
+ if state == 1:
665
669
  item["state"] = "open"
666
670
  self._insert([item])
667
671
 
668
- elif state == 3:
669
- item["state"] = "filled"
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
- 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
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 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})
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
- 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._find_and_delete({'s': symbol})
747
- self._update(asks + bids)
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
- ts = time.time() * 1000 # 预留时间戳(如需记录可用)
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
- Data structure:
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
- Data structure:
1037
+ Keys: ["order_id"]
990
1038
 
991
- .. code:: python
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
- "create_ts": 1625247600000,# 创建时间戳
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']:
@@ -26,7 +26,8 @@ class OurbitSwap:
26
26
  return self
27
27
 
28
28
  async def update(
29
- self, update_type: Literal["position", "orders", "balance", "ticker", "all"] = "all"
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
- "method": "sub.tickers",
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
- "method": "sub.depth.step",
80
- "param": {
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={ "method": "sub.personal.user.preference"},
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((price_dec / tick_dec).quantize(Decimal("1"), rounding=ROUND_HALF_UP) * tick_dec)
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 == 'close_buy':
162
+ if side == "close_buy":
169
163
  data["side"] = 2
170
- elif side == 'close_sell':
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 = await self.client.fetch(
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(self,
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(self, update_type: Literal["orders", "balance", "ticker", "book", "all"] = "all"):
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=self.store.onmessage
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
- 'wss://www.ourbit.com/ws?platform=web',
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
- currency, market = symbol.split("_")
473
+ parts = symbol.split("_")
474
+ if len(parts) != 2:
475
+ raise ValueError(f"Invalid symbol format: {symbol}")
447
476
 
448
- detail = self.store.detail.get({
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('price_scale')
456
- quantity_scale = detail.get('quantity_scale')
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
- "currency": currency,
477
- "market": market,
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(price)
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
- res = await self.client.fetch(
490
- "POST",
491
- f"{self.api_url}/api/platform/spot/order/place",
492
- json=data
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
- match res.data:
497
- case {"msg": 'success'}:
498
- return res.data["data"]
499
- case _:
500
- raise Exception(f"Failed to place order: {res.data}")
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] = {'amount': 0, 'hold_price': 0, 'value': 0, 'price': 0,
284
- 'realised_profit': 0, 'unrealised_profit': 0, 'fee': 0}
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] = {'amount': 0, 'hold_price': 0, 'value': 0, 'price': 0,
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") -> Logger:
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.4
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=7XrpuHvccWl9lNyVihqaptupqUMsG3xYmQr8eEDrwS4,20610
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=WALpXpIA3Ywr5DxKKK3k5EKubZ2h-ISGfc5dUReQUBQ,7795
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=BWXH-FQoBZoEzBKYY0mCXYW9Iy3EgHTYan4McFp4-R8,15952
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=-XgxQ9JB-hk7r6u2CmXsNx4055kpYr0lZWh0Su6SWIA,37539
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.4.dist-info/METADATA,sha256=CEAtZ3dZLsujTB7cj0BGAFOd9MUKd_EDGpgru09ozww,4316
20
- hyperquant-0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- hyperquant-0.4.dist-info/RECORD,,
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,,