hyperquant 0.3__py3-none-any.whl → 0.4__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.
@@ -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", "px"]
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
 
@@ -137,6 +143,21 @@ class Ticker(DataStore):
137
143
  class Orders(DataStore):
138
144
  _KEYS = ["order_id"]
139
145
 
146
+ def _fmt(self, order:dict):
147
+ return {
148
+ "order_id": order.get("orderId"),
149
+ "symbol": order.get("symbol"),
150
+ "px": order.get("price"),
151
+ "vol": order.get("vol"),
152
+ "lev": order.get("leverage"),
153
+ "side": "buy" if order.get("side") == 1 else "sell",
154
+ "deal_vol": order.get("dealVol"),
155
+ "deal_avg_px": order.get("dealAvgPrice"),
156
+ "create_ts": order.get("createTime"),
157
+ "update_ts": order.get("updateTime"),
158
+ "state": "open"
159
+ }
160
+
140
161
  # {'success': True, 'code': 0, 'data': [{'orderId': '219108574599630976', 'symbol': 'SOL_USDT', 'positionId': 0, 'price': 190, 'priceStr': '190', 'vol': 1, 'leverage': 20, 'side': 1, 'category': 1, 'orderType': 1, 'dealAvgPrice': 0, 'dealAvgPriceStr': '0', 'dealVol': 0, 'orderMargin': 0.09652, 'takerFee': 0, 'makerFee': 0, 'profit': 0, 'feeCurrency': 'USDT', 'openType': 1, 'state': 2, 'externalOid': '_m_2228b23a75204e1982b301e44d439cbb', 'errorCode': 0, 'usedMargin': 0, 'createTime': 1756277955008, 'updateTime': 1756277955037, 'positionMode': 1, 'version': 1, 'showCancelReason': 0, 'showProfitRateShare': 0, 'voucher': False}]}
141
162
  def _onresponse(self, data: dict[str, Any]):
142
163
  orders = data.get("data", [])
@@ -144,25 +165,33 @@ class Orders(DataStore):
144
165
  data_to_insert: list[Item] = []
145
166
  for order in orders:
146
167
  order: dict[str, Any] = order
147
-
148
- data_to_insert.append(
149
- {
150
- "order_id": order.get("orderId"),
151
- "symbol": order.get("symbol"),
152
- "px": order.get("priceStr"),
153
- "vol": order.get("vol"),
154
- "lev": order.get("leverage"),
155
- "side": "buy" if order.get("side") == 1 else "sell",
156
- "deal_vol": order.get("dealVol"),
157
- "deal_avg_px": order.get("dealAvgPriceStr"),
158
- "create_ts": order.get("createTime"),
159
- "update_ts": order.get("updateTime"),
160
- }
161
- )
168
+ data_to_insert.append(self._fmt(order))
162
169
 
163
170
  self._clear()
164
171
  self._update(data_to_insert)
165
-
172
+
173
+ def _on_message(self, msg: dict[str, Any]) -> None:
174
+ data:dict = msg.get("data", {})
175
+ if msg.get('channel') == 'push.personal.order':
176
+ state = data.get("state")
177
+ if state == 2:
178
+ order = self._fmt(data)
179
+ order["state"] = "open"
180
+ self._insert([order])
181
+ elif state == 3:
182
+ order = self._fmt(data)
183
+ order["state"] = "filled"
184
+ self._update([order])
185
+ self._find_and_delete({
186
+ "order_id": order.get("order_id")
187
+ })
188
+ elif state == 4:
189
+ order = self._fmt(data)
190
+ order["state"] = "canceled"
191
+ self._update([order])
192
+ self._find_and_delete({
193
+ "order_id": order.get("order_id")
194
+ })
166
195
 
167
196
  class Detail(DataStore):
168
197
  _KEYS = ["symbol"]
@@ -193,15 +222,9 @@ class Detail(DataStore):
193
222
  class Position(DataStore):
194
223
  _KEYS = ["position_id"]
195
224
  # {"success":true,"code":0,"data":[{"positionId":5355366,"symbol":"SOL_USDT","positionType":1,"openType":1,"state":1,"holdVol":1,"frozenVol":0,"closeVol":0,"holdAvgPrice":203.44,"holdAvgPriceFullyScale":"203.44","openAvgPrice":203.44,"openAvgPriceFullyScale":"203.44","closeAvgPrice":0,"liquidatePrice":194.07,"oim":0.10253376,"im":0.10253376,"holdFee":0,"realised":-0.0008,"leverage":20,"marginRatio":0.0998,"createTime":1756275984696,"updateTime":1756275984696,"autoAddIm":false,"version":1,"profitRatio":0,"newOpenAvgPrice":203.44,"newCloseAvgPrice":0,"closeProfitLoss":0,"fee":0.00081376}]}
196
- def _onresponse(self, data: dict[str, Any]):
197
- positions = data.get("data", [])
198
- if positions:
199
- data_to_insert: list[Item] = []
200
- for position in positions:
201
- position: dict[str, Any] = position
202
-
203
- data_to_insert.append(
204
- {
225
+
226
+ def _fmt(self, position:dict):
227
+ return {
205
228
  "position_id": position.get("positionId"),
206
229
  "symbol": position.get("symbol"),
207
230
  "side": "short" if position.get("positionType") == 2 else "long",
@@ -222,37 +245,63 @@ class Position(DataStore):
222
245
  "margin_ratio": position.get("marginRatio"),
223
246
  "create_ts": position.get("createTime"),
224
247
  "update_ts": position.get("updateTime"),
225
- }
248
+ }
249
+
250
+ def _onresponse(self, data: dict[str, Any]):
251
+ positions = data.get("data", [])
252
+ if positions:
253
+ data_to_insert: list[Item] = []
254
+ for position in positions:
255
+ position: dict[str, Any] = position
256
+
257
+ data_to_insert.append(
258
+ self._fmt(position)
226
259
  )
227
260
 
228
261
  self._clear()
229
262
  self._insert(data_to_insert)
263
+ else:
264
+ self._clear()
265
+
266
+ def _on_message(self, msg: dict[str, Any]) -> None:
267
+ data:dict = msg.get("data", {})
268
+ state = data.get("state")
269
+ position_id = data.get("positionId")
270
+ if state == 3:
271
+ self._find_and_delete({"position_id": position_id})
272
+ return
273
+
274
+ self._update([self._fmt(data)])
230
275
 
231
276
  class Balance(DataStore):
232
277
  _KEYS = ["currency"]
233
278
 
279
+ def _fmt(self, balance: dict) -> dict:
280
+ return {
281
+ "available_balance": balance.get("availableBalance"),
282
+ "bonus": balance.get("bonus"),
283
+ "currency": balance.get("currency"),
284
+ "frozen_balance": balance.get("frozenBalance"),
285
+ "last_bonus": balance.get("lastBonus"),
286
+ "position_margin": balance.get("positionMargin"),
287
+ "wallet_balance": balance.get("walletBalance"),
288
+ }
289
+
234
290
  def _onresponse(self, data: dict[str, Any]):
235
291
  balances = data.get("data", [])
236
292
  if balances:
237
293
  data_to_insert: list[Item] = []
238
294
  for balance in balances:
239
295
  balance: dict[str, Any] = balance
240
- data_to_insert.append({
241
- "currency": balance.get("currency"),
242
- "position_margin": balance.get("positionMargin"),
243
- "available_balance": balance.get("availableBalance"),
244
- "cash_balance": balance.get("cashBalance"),
245
- "frozen_balance": balance.get("frozenBalance"),
246
- "equity": balance.get("equity"),
247
- "unrealized": balance.get("unrealized"),
248
- "bonus": balance.get("bonus"),
249
- "last_bonus": balance.get("lastBonus"),
250
- "wallet_balance": balance.get("walletBalance"),
251
- "voucher": balance.get("voucher"),
252
- "voucher_using": balance.get("voucherUsing"),
253
- })
296
+ data_to_insert.append(self._fmt(balance))
254
297
  self._clear()
255
298
  self._insert(data_to_insert)
299
+
300
+ def _on_message(self, msg: dict[str, Any]) -> None:
301
+ data: dict = msg.get("data", {})
302
+ self._update([self._fmt(data)])
303
+
304
+
256
305
 
257
306
  class OurbitSwapDataStore(DataStoreCollection):
258
307
  """
@@ -316,6 +365,12 @@ class OurbitSwapDataStore(DataStoreCollection):
316
365
  self.book._on_message(msg)
317
366
  if channel == "push.tickers":
318
367
  self.ticker._on_message(msg)
368
+ if channel == "push.personal.position":
369
+ self.position._on_message(msg)
370
+ if channel == "push.personal.order":
371
+ self.orders._on_message(msg)
372
+ if channel == "push.personal.asset":
373
+ self.balance._on_message(msg)
319
374
  else:
320
375
  logger.debug(f"未知的channel: {channel}")
321
376
 
@@ -376,6 +431,7 @@ class OurbitSwapDataStore(DataStoreCollection):
376
431
  "px": "110152.5", # 价格
377
432
  "sz": "53539", # 数量
378
433
  "count": 1 # 订单数量
434
+ "i" 0 # 价格档位索引
379
435
  },
380
436
  {
381
437
  "symbol": "BTC_USDT", # 交易对
@@ -383,6 +439,7 @@ class OurbitSwapDataStore(DataStoreCollection):
383
439
  "px": "110152.4", # 价格
384
440
  "sz": "76311", # 数量
385
441
  "count": 1 # 订单数量
442
+ "i" 0 # 价格档位索引
386
443
  }
387
444
  ]
388
445
  """
@@ -434,7 +491,7 @@ class OurbitSwapDataStore(DataStoreCollection):
434
491
  "side": "buy",
435
492
  "price": "110152.5",
436
493
  "size": "0.1",
437
- "status": "open",
494
+ "state": "open", // ("open", "closed", "canceled")
438
495
  "create_ts": 1625247600000,
439
496
  "update_ts": 1625247600000
440
497
  }
@@ -446,57 +503,552 @@ class OurbitSwapDataStore(DataStoreCollection):
446
503
  def position(self) -> Position:
447
504
  """
448
505
  持仓数据
449
-
450
506
  Data structure:
451
- .. code:: python
452
- [
453
- {
454
- "position_id": "123456",
455
- "symbol": "BTC_USDT",
456
- "side": "long",
457
- "open_type": "limit",
458
- "state": "open",
459
- "hold_vol": "0.1",
460
- "frozen_vol": "0.0",
461
- "close_vol": "0.0",
462
- "hold_avg_price": "110152.5",
463
- "open_avg_price": "110152.5",
464
- "close_avg_price": "0.0",
465
- "liquidate_price": "100000.0",
466
- "oim": "0.0",
467
- "im": "0.0",
468
- "hold_fee": "0.0",
469
- "realised": "0.0",
470
- "leverage": "10",
471
- "margin_ratio": "0.1",
472
- "create_ts": 1625247600000,
473
- "update_ts": 1625247600000
474
- }
475
- ]
507
+
508
+ .. code:: json
509
+
510
+ [
511
+ {
512
+ "position_id": "123456",
513
+ "symbol": "BTC_USDT",
514
+ "side": "long",
515
+ "open_type": "limit",
516
+ "state": "open",
517
+ "hold_vol": "0.1",
518
+ "frozen_vol": "0.0",
519
+ "close_vol": "0.0",
520
+ "hold_avg_price": "110152.5",
521
+ "open_avg_price": "110152.5",
522
+ "close_avg_price": "0.0",
523
+ "liquidate_price": "100000.0",
524
+ "oim": "0.0",
525
+ "im": "0.0",
526
+ "hold_fee": "0.0",
527
+ "realised": "0.0",
528
+ "leverage": "10",
529
+ "margin_ratio": "0.1",
530
+ "create_ts": 1625247600000,
531
+ "update_ts": 1625247600000
532
+ }
533
+ ]
476
534
  """
477
535
  return self._get("position", Position)
478
536
 
479
537
  @property
480
538
  def balance(self) -> Balance:
481
- """账户余额数据
539
+ @property
540
+ def balance(self) -> Balance:
541
+ """账户余额数据
542
+
543
+ Data structure:
544
+
545
+ .. code:: python
546
+
547
+ [
548
+ {
549
+ "currency": "USDT", # 币种
550
+ "position_margin": 0.3052, # 持仓保证金
551
+ "available_balance": 19.7284, # 可用余额
552
+ "frozen_balance": 0, # 冻结余额
553
+ "bonus": 0, # 奖励
554
+ "last_bonus": 0, # 最后奖励
555
+ "wallet_balance": 20.0337 # 钱包余额
556
+ }
557
+ ]
558
+ """
559
+ return self._get("balance", Balance)
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._find_and_delete({'s': symbol})
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"]})
482
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._find_and_delete({'s': symbol})
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
+
483
932
  Data structure:
933
+
484
934
  .. code:: python
935
+
485
936
  [
486
937
  {
487
- "currency": "USDT", # 币种
488
- "position_margin": 0.3052, # 持仓保证金
489
- "available_balance": 19.7284, # 可用余额
490
- "cash_balance": 19.7284, # 现金余额
491
- "frozen_balance": 0, # 冻结余额
492
- "equity": 19.9442, # 权益
493
- "unrealized": -0.0895, # 未实现盈亏
494
- "bonus": 0, # 奖励
495
- "last_bonus": 0, # 最后奖励
496
- "wallet_balance": 20.0337, # 钱包余额
497
- "voucher": 0, # 代金券
498
- "voucher_using": 0 # 使用中的代金券
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": "" # 币种图标
499
948
  }
500
949
  ]
501
950
  """
502
- return self._get("balance", Balance)
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
+
1019
+ channel = msg.get("c")
1020
+ if 'msg' in msg:
1021
+ if 'invalid' in msg['msg']:
1022
+ logger.warning(f"WebSocket message invalid: {msg['msg']}")
1023
+ return
1024
+
1025
+ if channel is None:
1026
+ return
1027
+
1028
+ if 'increase.aggre.depth' in channel:
1029
+ self.book._on_message(msg)
1030
+
1031
+ if 'spot@private.orders' in channel:
1032
+ self.orders._on_message(msg)
1033
+
1034
+ if 'spot@private.balances' in channel:
1035
+ self.balance._on_message(msg)
1036
+
1037
+ if 'ticker' in channel:
1038
+ self.ticker._on_message(msg)
1039
+
1040
+ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
1041
+ """Initialize DataStore from HTTP response data."""
1042
+ for f in asyncio.as_completed(aws):
1043
+ res = await f
1044
+ data = await res.json()
1045
+ if res.url.path == "/api/platform/spot/market/depth":
1046
+ self.book._onresponse(data)
1047
+ if res.url.path == "/api/platform/spot/market/v2/tickers":
1048
+ self.ticker._onresponse(data)
1049
+ if res.url.path == "/api/assetbussiness/asset/spot/statistic":
1050
+ self.balance._onresponse(data)
1051
+ if res.url.path == "/api/platform/spot/order/current/orders/v2":
1052
+ self.orders._onresponse(data)
1053
+ if res.url.path == "/api/platform/spot/market/v2/symbols":
1054
+ self.detail._onresponse(data)