hyperquant 1.48__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.

Potentially problematic release.


This version of hyperquant might be problematic. Click here for more details.

Files changed (42) hide show
  1. hyperquant/__init__.py +8 -0
  2. hyperquant/broker/auth.py +972 -0
  3. hyperquant/broker/bitget.py +311 -0
  4. hyperquant/broker/bitmart.py +720 -0
  5. hyperquant/broker/coinw.py +487 -0
  6. hyperquant/broker/deepcoin.py +651 -0
  7. hyperquant/broker/edgex.py +500 -0
  8. hyperquant/broker/hyperliquid.py +570 -0
  9. hyperquant/broker/lbank.py +661 -0
  10. hyperquant/broker/lib/edgex_sign.py +455 -0
  11. hyperquant/broker/lib/hpstore.py +252 -0
  12. hyperquant/broker/lib/hyper_types.py +48 -0
  13. hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
  14. hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
  15. hyperquant/broker/lib/util.py +22 -0
  16. hyperquant/broker/lighter.py +679 -0
  17. hyperquant/broker/models/apexpro.py +150 -0
  18. hyperquant/broker/models/bitget.py +359 -0
  19. hyperquant/broker/models/bitmart.py +635 -0
  20. hyperquant/broker/models/coinw.py +724 -0
  21. hyperquant/broker/models/deepcoin.py +809 -0
  22. hyperquant/broker/models/edgex.py +1053 -0
  23. hyperquant/broker/models/hyperliquid.py +284 -0
  24. hyperquant/broker/models/lbank.py +557 -0
  25. hyperquant/broker/models/lighter.py +868 -0
  26. hyperquant/broker/models/ourbit.py +1155 -0
  27. hyperquant/broker/models/polymarket.py +1071 -0
  28. hyperquant/broker/ourbit.py +550 -0
  29. hyperquant/broker/polymarket.py +2399 -0
  30. hyperquant/broker/ws.py +132 -0
  31. hyperquant/core.py +513 -0
  32. hyperquant/datavison/_util.py +18 -0
  33. hyperquant/datavison/binance.py +111 -0
  34. hyperquant/datavison/coinglass.py +237 -0
  35. hyperquant/datavison/okx.py +177 -0
  36. hyperquant/db.py +191 -0
  37. hyperquant/draw.py +1200 -0
  38. hyperquant/logkit.py +205 -0
  39. hyperquant/notikit.py +124 -0
  40. hyperquant-1.48.dist-info/METADATA +32 -0
  41. hyperquant-1.48.dist-info/RECORD +42 -0
  42. hyperquant-1.48.dist-info/WHEEL +4 -0
@@ -0,0 +1,1155 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+ from typing import TYPE_CHECKING, Any, Awaitable
7
+
8
+ import aiohttp
9
+ from pybotters.store import DataStore, DataStoreCollection
10
+
11
+ if TYPE_CHECKING:
12
+ from pybotters.typedefs import Item
13
+ from pybotters.ws import ClientWebSocketResponse
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class Book(DataStore):
20
+ """深度数据存储类,用于处理订单簿深度信息
21
+
22
+ Channel: push.depth.step
23
+
24
+ 用于存储和管理订单簿深度数据,包含买卖盘的价格和数量信息
25
+ Keys: ["s", "S", "p"]
26
+ - s: 交易对符号
27
+ - S: 买卖方向 (A: ask卖出, B: bid买入)
28
+ - p: 价格
29
+
30
+
31
+ """
32
+
33
+ _KEYS = ["s", "S", "p"]
34
+
35
+ def _init(self) -> None:
36
+ # super().__init__()
37
+ self._time: int | None = None
38
+ self.limit = 1
39
+
40
+ def _on_message(self, msg: dict[str, Any]) -> None:
41
+
42
+ symbol = msg.get("symbol")
43
+ data = msg.get("data", {})
44
+ asks = data.get("asks", [])
45
+ bids = data.get("bids", [])
46
+ # 提速 默认 5当前
47
+ asks = asks[:self.limit]
48
+ bids = bids[:self.limit]
49
+
50
+ timestamp = data.get("ct") # 使用服务器时间
51
+
52
+ data_to_insert: list[Item] = []
53
+
54
+ # 先删除旧的订单簿数据
55
+ self._find_and_delete({"s": symbol})
56
+
57
+ # 处理买卖盘数据
58
+ for side_id, levels in (("b", bids), ("a", asks)):
59
+ for i, level in enumerate(levels):
60
+ # level格式: [price, size, count]
61
+ if len(level) >= 3:
62
+ price, size, count = level[0:3]
63
+ data_to_insert.append(
64
+ {
65
+ "s": symbol,
66
+ "S": side_id,
67
+ "p": str(price),
68
+ "q": str(size),
69
+ "ct": count,
70
+ "i": i
71
+ }
72
+ )
73
+
74
+ # 插入新的订单簿数据
75
+ self._insert(data_to_insert)
76
+ self._time = timestamp
77
+
78
+ @property
79
+ def time(self) -> int | None:
80
+ """返回最后更新时间"""
81
+ return self._time
82
+
83
+ def sorted(
84
+ self, query: Item | None = None, limit: int | None = None
85
+ ) -> dict[str, list[Item]]:
86
+
87
+ return self._sorted(
88
+ item_key="S",
89
+ item_asc_key="a", # asks 升序
90
+ item_desc_key="b", # bids 降序
91
+ sort_key="p",
92
+ query=query,
93
+ limit=limit,
94
+ )
95
+
96
+
97
+ class Ticker(DataStore):
98
+ _KEYS = ["symbol"]
99
+
100
+ def _on_message(self, data: dict[str, Any]):
101
+ self._onresponse(data)
102
+
103
+ def _onresponse(self, data: dict[str, Any]):
104
+ tickers = data.get("data", [])
105
+ if tickers:
106
+ data_to_insert: list[Item] = []
107
+ for ticker in tickers:
108
+ ticker: dict[str, Any] = ticker
109
+ for ticker in tickers:
110
+ data_to_insert.append(
111
+ {
112
+ "amount24": ticker.get("amount24"),
113
+ "fair_price": ticker.get("fairPrice"),
114
+ "high24_price": ticker.get("high24Price"),
115
+ "index_price": ticker.get("indexPrice"),
116
+ "last_price": ticker.get("lastPrice"),
117
+ "lower24_price": ticker.get("lower24Price"),
118
+ "max_bid_price": ticker.get("maxBidPrice"),
119
+ "min_ask_price": ticker.get("minAskPrice"),
120
+ "rise_fall_rate": ticker.get("riseFallRate"),
121
+ "symbol": ticker.get("symbol"),
122
+ "timestamp": ticker.get("timestamp"),
123
+ "volume24": ticker.get("volume24"),
124
+ }
125
+ )
126
+ # self._clear()
127
+ self._insert(data_to_insert)
128
+
129
+
130
+ class Orders(DataStore):
131
+ _KEYS = ["order_id"]
132
+
133
+ def _fmt(self, order:dict):
134
+ return {
135
+ "order_id": order.get("orderId"),
136
+ "position_id": order.get("positionId"),
137
+ "symbol": order.get("symbol"),
138
+ "price": order.get("price"),
139
+ "vol": order.get("vol"),
140
+ "lev": order.get("leverage"),
141
+ "side": "buy" if order.get("side") == 1 else "sell",
142
+ "deal_quantity": order.get("dealVol"),
143
+ "avg_price": order.get("dealAvgPrice"),
144
+ "create_ts": order.get("createTime"),
145
+ "update_ts": order.get("updateTime"),
146
+ "fee": order.get("makerFee"),
147
+ "profit": order.get("profit"),
148
+ "used_margin": order.get("usedMargin"),
149
+ "state": "open"
150
+ }
151
+
152
+ # {'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}]}
153
+ def _onresponse(self, data: dict[str, Any]):
154
+ orders = data.get("data", [])
155
+ if orders:
156
+ data_to_insert: list[Item] = []
157
+ for order in orders:
158
+ order: dict[str, Any] = order
159
+ data_to_insert.append(self._fmt(order))
160
+
161
+ self._clear()
162
+ self._update(data_to_insert)
163
+
164
+ def _on_message(self, msg: dict[str, Any]) -> None:
165
+ data:dict = msg.get("data", {})
166
+ if msg.get('channel') == 'push.personal.order':
167
+ state = data.get("state")
168
+ if state == 2:
169
+ order = self._fmt(data)
170
+ order["state"] = "open"
171
+ self._insert([order])
172
+ elif state == 3:
173
+ order = self._fmt(data)
174
+ order["state"] = "filled"
175
+ self._update([order])
176
+ self._find_and_delete({
177
+ "order_id": order.get("order_id")
178
+ })
179
+ elif state == 4:
180
+ order = self._fmt(data)
181
+ order["state"] = "canceled"
182
+ self._update([order])
183
+ self._find_and_delete({
184
+ "order_id": order.get("order_id")
185
+ })
186
+ else:
187
+ order = self._fmt(data)
188
+ order["state"] = f"unknown_{state}"
189
+ self._update([order])
190
+ self._find_and_delete({
191
+ "order_id": order.get("order_id")
192
+ })
193
+
194
+ class Detail(DataStore):
195
+ _KEYS = ["symbol"]
196
+
197
+ def _on_message(self, data: dict[str, Any]):
198
+ self._onresponse(data)
199
+
200
+ def _onresponse(self, data: dict[str, Any]):
201
+ details: dict = data.get("data", {})
202
+ data_to_insert: list[Item] = []
203
+ if details:
204
+ for detail in details:
205
+ data_to_insert.append(
206
+ {
207
+ "symbol": detail.get("symbol"),
208
+ "ft": detail.get("ft"),
209
+ "max_lev": detail.get("maxL"),
210
+ "tick_size": detail.get("pu"),
211
+ "vol_unit": detail.get("vu"),
212
+ "io": detail.get("io"),
213
+ "contract_sz": detail.get("cs"),
214
+ "minv": detail.get("minV"),
215
+ "maxv": detail.get("maxV"),
216
+ "online_time": detail.get("tcd")
217
+ }
218
+ )
219
+ self._update(data_to_insert)
220
+
221
+ class Position(DataStore):
222
+ _KEYS = ["position_id"]
223
+ # {"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}]}
224
+
225
+ def _fmt(self, position:dict):
226
+ return {
227
+ "position_id": position.get("positionId"),
228
+ "symbol": position.get("symbol"),
229
+ "side": "short" if position.get("positionType") == 2 else "long",
230
+ "open_type": position.get("openType"),
231
+ "state": position.get("state"),
232
+ "hold_vol": position.get("holdVol"),
233
+ "frozen_vol": position.get("frozenVol"),
234
+ "close_vol": position.get("closeVol"),
235
+ "hold_avg_price": position.get("holdAvgPriceFullyScale"),
236
+ "open_avg_price": position.get("openAvgPriceFullyScale"),
237
+ "close_avg_price": str(position.get("closeAvgPrice")),
238
+ "liquidate_price": str(position.get("liquidatePrice")),
239
+ "oim": position.get("oim"),
240
+ "im": position.get("im"),
241
+ "hold_fee": position.get("holdFee"),
242
+ "realised": position.get("realised"),
243
+ "leverage": position.get("leverage"),
244
+ "margin_ratio": position.get("marginRatio"),
245
+ "create_ts": position.get("createTime"),
246
+ "update_ts": position.get("updateTime"),
247
+ }
248
+
249
+ def _onresponse(self, data: dict[str, Any]):
250
+ positions = data.get("data", [])
251
+ if positions:
252
+ data_to_insert: list[Item] = []
253
+ for position in positions:
254
+ position: dict[str, Any] = position
255
+
256
+ data_to_insert.append(
257
+ self._fmt(position)
258
+ )
259
+
260
+ self._clear()
261
+ self._insert(data_to_insert)
262
+ else:
263
+ self._clear()
264
+
265
+ def _on_message(self, msg: dict[str, Any]) -> None:
266
+ data:dict = msg.get("data", {})
267
+ state = data.get("state")
268
+ position_id = data.get("positionId")
269
+ if state == 3:
270
+ self._find_and_delete({"position_id": position_id})
271
+ return
272
+
273
+ self._update([self._fmt(data)])
274
+
275
+ class Balance(DataStore):
276
+ _KEYS = ["currency"]
277
+
278
+ def _fmt(self, balance: dict) -> dict:
279
+ return {
280
+ "available_balance": balance.get("availableBalance"),
281
+ "bonus": balance.get("bonus"),
282
+ "currency": balance.get("currency"),
283
+ "frozen_balance": balance.get("frozenBalance"),
284
+ "last_bonus": balance.get("lastBonus"),
285
+ "position_margin": balance.get("positionMargin"),
286
+ "wallet_balance": balance.get("walletBalance"),
287
+ }
288
+
289
+ def _onresponse(self, data: dict[str, Any]):
290
+ balances = data.get("data", [])
291
+ if balances:
292
+ data_to_insert: list[Item] = []
293
+ for balance in balances:
294
+ balance: dict[str, Any] = balance
295
+ data_to_insert.append(self._fmt(balance))
296
+ self._clear()
297
+ self._insert(data_to_insert)
298
+
299
+ def _on_message(self, msg: dict[str, Any]) -> None:
300
+ data: dict = msg.get("data", {})
301
+ self._update([self._fmt(data)])
302
+
303
+
304
+
305
+ class OurbitSwapDataStore(DataStoreCollection):
306
+ """
307
+ Ourbit DataStoreCollection
308
+
309
+ REST API:
310
+ - 地址: https://futures.ourbit.com
311
+ - 合约详情
312
+ GET /api/v1/contract/detailV2?client=web
313
+ - ticker
314
+ GET /api/v1/contract/ticker
315
+ - open_orders
316
+ GET /api/v1/private/order/list/open_orders?page_size=200
317
+ - open_positions
318
+ GET /api/v1/private/position/open_positions
319
+
320
+ WebSocket API:
321
+ - 地址: wss://futures.ourbit.com/edge or /ws
322
+ - 支持频道:
323
+ * 深度数据(Book): push.depth.step
324
+ * 行情数据(Ticker): push.tickers
325
+
326
+ 示例订阅 JSON:
327
+
328
+ .. code:: json
329
+
330
+ {
331
+ "method": "sub.depth.step",
332
+ "param": {
333
+ "symbol": "BTC_USDT",
334
+ "step": "0.1"
335
+ }
336
+ }
337
+
338
+ .. code:: json
339
+
340
+ {
341
+ "method": "sub.tickers",
342
+ "param": {
343
+ "timezone": "UTC+8"
344
+ }
345
+ }
346
+
347
+ TODO:
348
+ - 添加 trades、ticker、candle 等其他数据流
349
+ """
350
+
351
+ def _init(self) -> None:
352
+ self._create("book", datastore_class=Book)
353
+ self._create("detail", datastore_class=Detail)
354
+ self._create("ticker", datastore_class=Ticker)
355
+ self._create("orders", datastore_class=Orders)
356
+ self._create("position", datastore_class=Position)
357
+ self._create("balance", datastore_class=Balance)
358
+ # TODO: 添加其他数据流,如 trades, ticker, candle 等
359
+
360
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
361
+ channel = msg.get("channel")
362
+
363
+ if channel == "push.depth.step":
364
+ self.book._on_message(msg)
365
+ if channel == "push.tickers":
366
+ self.ticker._on_message(msg)
367
+ if channel == "push.personal.position":
368
+ self.position._on_message(msg)
369
+ if channel == "push.personal.order":
370
+ self.orders._on_message(msg)
371
+ if channel == "push.personal.asset":
372
+ self.balance._on_message(msg)
373
+ else:
374
+ logger.debug(f"未知的channel: {channel}")
375
+
376
+ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
377
+ """Initialize DataStore from HTTP response data."""
378
+ for f in asyncio.as_completed(aws):
379
+ res = await f
380
+ data = await res.json()
381
+ if res.url.path == "/api/v1/contract/detailV2":
382
+ self.detail._onresponse(data)
383
+ if res.url.path == "/api/v1/contract/ticker":
384
+ self.ticker._onresponse(data)
385
+ if res.url.path == "/api/v1/private/order/list/open_orders":
386
+ self.orders._onresponse(data)
387
+ if res.url.path == "/api/v1/private/position/open_positions":
388
+ self.position._onresponse(data)
389
+ if res.url.path == "/api/v1/private/account/assets":
390
+ self.balance._onresponse(data)
391
+
392
+ @property
393
+ def detail(self) -> Detail:
394
+ """合约详情
395
+ Data structure:
396
+ .. code:: python
397
+ [
398
+ {
399
+ "symbol": "BTC_USDT", # 交易对
400
+ "ft": 100, # 合约面值
401
+ "max_lev": 100, # 最大杠杆
402
+ "tick_size": 0.1, # 最小变动价位
403
+ "vol_unit": 1, # 合约单位
404
+ "io": ["binance", "mexc"], # 交易所列表
405
+ "contract_sz": 1,
406
+ "minv": 1,
407
+ "maxv": 10000,
408
+ "online_time": 1625247600000 # 上线时间
409
+ }
410
+ ]
411
+ """
412
+ return self._get("detail", Detail)
413
+
414
+ @property
415
+ def book(self) -> Book:
416
+ """订单簿深度数据流
417
+
418
+ 提供实时订单簿深度数据,包含买卖双方价格和数量信息
419
+
420
+ Data type: Mutable
421
+
422
+ Keys: ("s", "S", "p")
423
+ - s: 交易对符号,如 "BTC_USDT"
424
+ - S: 买卖方向,"a" 表示卖单(ask),"b" 表示买单(bid)
425
+ - p: 价格
426
+
427
+ Data structure:
428
+
429
+ .. code:: python
430
+
431
+ [
432
+ {
433
+ "s": "BTC_USDT", # 交易对符号
434
+ "S": "a", # 卖单方向(ask)
435
+ "p": "110152.5", # 价格
436
+ "q": "53539", # 数量
437
+ "ct": 1, # 该价格的订单数量
438
+ "i": 0 # 价格档位索引(从0开始)
439
+ },
440
+ {
441
+ "s": "BTC_USDT", # 交易对符号
442
+ "S": "b", # 买单方向(bid)
443
+ "p": "110152.4", # 价格
444
+ "q": "76311", # 数量
445
+ "ct": 1, # 该价格的订单数量
446
+ "i": 0 # 价格档位索引(从0开始)
447
+ }
448
+ ]
449
+ """
450
+ return self._get("book", Book)
451
+
452
+ @property
453
+ def ticker(self) -> Ticker:
454
+ """市场行情数据流
455
+
456
+ Data type: Mutable
457
+
458
+ Keys: ("symbol",)
459
+
460
+ Data structure:
461
+
462
+ .. code:: python
463
+
464
+ [
465
+ {
466
+ "symbol": "BTC_USDT", # 交易对
467
+ "last_price": "110152.5", # 最新价格
468
+ "index_price": "110000.0", # 指数价格
469
+ "fair_price": "110100.0", # 公允价格
470
+ "high24_price": "115000.0", # 24小时最高价
471
+ "lower24_price": "105000.0", # 24小时最低价
472
+ "volume24": "1500", # 24小时交易量
473
+ "amount24": "165000000", # 24小时交易额
474
+ "rise_fall_rate": "0.05", # 涨跌幅
475
+ "max_bid_price": "110150.0", # 买一价
476
+ "min_ask_price": "110155.0", # 卖一价
477
+ "timestamp": 1625247600000 # 时间戳
478
+ }
479
+ ]
480
+ """
481
+ return self._get("ticker", Ticker)
482
+
483
+ @property
484
+ def orders(self) -> Orders:
485
+ """
486
+ 订单数据
487
+ Data structure:
488
+
489
+ .. code:: json
490
+
491
+ [
492
+ {
493
+ "id": "123456",
494
+ "symbol": "BTC_USDT",
495
+ "side": "buy",
496
+ "price": "110152.5",
497
+ "size": "0.1",
498
+ "state": "open", // ("open", "closed", "canceled")
499
+ "create_ts": 1625247600000,
500
+ "update_ts": 1625247600000
501
+ }
502
+ ]
503
+ """
504
+ return self._get("orders", Orders)
505
+
506
+ @property
507
+ def position(self) -> Position:
508
+ """
509
+ 持仓数据
510
+ Data structure:
511
+
512
+ .. code:: json
513
+
514
+ [
515
+ {
516
+ "position_id": "123456",
517
+ "symbol": "BTC_USDT",
518
+ "side": "long",
519
+ "open_type": "limit",
520
+ "state": "open",
521
+ "hold_vol": "0.1",
522
+ "frozen_vol": "0.0",
523
+ "close_vol": "0.0",
524
+ "hold_avg_price": "110152.5",
525
+ "open_avg_price": "110152.5",
526
+ "close_avg_price": "0.0",
527
+ "liquidate_price": "100000.0",
528
+ "oim": "0.0",
529
+ "im": "0.0",
530
+ "hold_fee": "0.0",
531
+ "realised": "0.0",
532
+ "leverage": "10",
533
+ "margin_ratio": "0.1",
534
+ "create_ts": 1625247600000,
535
+ "update_ts": 1625247600000
536
+ }
537
+ ]
538
+ """
539
+ return self._get("position", Position)
540
+
541
+ @property
542
+ def balance(self) -> Balance:
543
+ @property
544
+ def balance(self) -> Balance:
545
+ """账户余额数据
546
+
547
+ Data structure:
548
+
549
+ .. code:: python
550
+
551
+ [
552
+ {
553
+ "currency": "USDT", # 币种
554
+ "position_margin": 0.3052, # 持仓保证金
555
+ "available_balance": 19.7284, # 可用余额
556
+ "frozen_balance": 0, # 冻结余额
557
+ "bonus": 0, # 奖励
558
+ "last_bonus": 0, # 最后奖励
559
+ "wallet_balance": 20.0337 # 钱包余额
560
+ }
561
+ ]
562
+ """
563
+ return self._get("balance", Balance)
564
+ return self._get("balance", Balance)
565
+
566
+ # SpotBalance: 现货账户余额数据存储
567
+
568
+ class SpotBalance(DataStore):
569
+ _KEYS = ["currency"]
570
+
571
+ def _fmt(self, balance: dict) -> dict:
572
+ return {
573
+ "currency": balance.get("currency"),
574
+ "available": balance.get("available"),
575
+ "frozen": balance.get("frozen"),
576
+ "amount": balance.get("amount"),
577
+ "avg_price": balance.get("avgPrice"),
578
+ }
579
+
580
+ def _fmt_ws(self, balance: dict) -> dict:
581
+ return {
582
+ "currency": balance.get("s"),
583
+ "available": balance.get("av"),
584
+ "frozen": balance.get("fr"),
585
+ "amount": balance.get("to"),
586
+ "avg_price": balance.get("ap"),
587
+ }
588
+
589
+ def _onresponse(self, data: dict[str, Any]):
590
+ balances = data.get("data", [])
591
+ items = [self._fmt(b) for b in balances]
592
+ if items:
593
+ self._clear()
594
+ self._insert(items)
595
+
596
+ def _on_message(self, msg: dict[str, Any]) -> None:
597
+ data = msg.get("d", {})
598
+ item = self._fmt_ws(data)
599
+ av = float(item.get("available", 0))
600
+ if av == 0:
601
+ self._find_and_delete({'currency': item.get("currency")})
602
+ else:
603
+ self._update([item])
604
+
605
+
606
+ # SpotOrders: 现货订单数据存储
607
+ class SpotOrders(DataStore):
608
+ _KEYS = ["order_id"]
609
+
610
+
611
+ def _fmt(self, order: dict) -> dict:
612
+ # 状态映射:1=open, 2=filled(整单成交), 3=partially_filled, 4=canceled
613
+ state_num = order.get("state") or order.get("status")
614
+ if state_num == 1:
615
+ state_txt = "open"
616
+ elif state_num == 2:
617
+ state_txt = "filled" # ✔ 2 才是整单成交
618
+ elif state_num == 3:
619
+ state_txt = "partially_filled"
620
+ elif state_num == 4:
621
+ state_txt = "canceled"
622
+ else:
623
+ state_txt = "unknown"
624
+
625
+ return {
626
+ "order_id": order.get("id") or order.get("orderId"),
627
+ "symbol": order.get("symbol") or order.get("s"),
628
+ "currency": order.get("currency"),
629
+ "market": order.get("market"),
630
+ "trade_type": order.get("tradeType"),
631
+ "order_type": order.get("orderType"),
632
+ "price": order.get("price"),
633
+ "quantity": order.get("quantity"),
634
+ "amount": order.get("amount"),
635
+ "deal_quantity": order.get("dealQuantity"),
636
+ "deal_amount": order.get("dealAmount"),
637
+ "avg_price": order.get("avgPrice"),
638
+ "state": state_txt,
639
+ "source": order.get("source") or order.get("internal"),
640
+ "fee": order.get("fee"),
641
+ "create_ts": order.get("createTime"),
642
+ "unique_id": order.get("uniqueId"),
643
+ }
644
+
645
+
646
+ def _onresponse(self, data: dict[str, Any]):
647
+ orders = (data.get("data") or {}).get("resultList", [])
648
+ items = [self._fmt(order) for order in orders]
649
+ self._clear()
650
+ if items:
651
+ self._insert(items)
652
+
653
+ def _on_message(self, msg: dict[str, Any]) -> None:
654
+ d: dict = msg.get("d", {})
655
+
656
+ # 基础字段
657
+ item = {
658
+ "order_id": d.get("id"),
659
+ "symbol": msg.get("s") or d.get("symbol"),
660
+ "trade_type": d.get("tradeType"),
661
+ "order_type": d.get("orderType"),
662
+ "price": d.get("price"),
663
+ "quantity": d.get("quantity"),
664
+ "amount": d.get("amount"),
665
+ "remain_quantity": d.get("remainQ"),
666
+ "remain_amount": d.get("remainA"),
667
+ "client_order_id": d.get("clientOrderId"),
668
+ "is_taker": d.get("isTaker"),
669
+ "create_ts": d.get("createTime"),
670
+ "source": d.get("internal"),
671
+ }
672
+
673
+ state = d.get("status")
674
+
675
+
676
+
677
+ # 成交片段(部分/完全)
678
+ if d.get("singleDealPrice"):
679
+ # 单片段信息(可能多次推送;需做增量累计 + 去重)
680
+ single_id = d.get("singleDealId")
681
+ single_px = d.get("singleDealPrice")
682
+ single_qty = d.get("singleDealQuantity")
683
+ try:
684
+ px_i = float(single_px) if single_px is not None else 0.0
685
+ qty_i = float(single_qty) if single_qty is not None else 0.0
686
+ except Exception:
687
+ px_i, qty_i = 0.0, 0.0
688
+
689
+ old = self.get({"order_id": d.get("id")})
690
+ old_qty = float(old.get("deal_quantity") or 0.0) if old else 0.0
691
+ old_avg = float(old.get("avg_price") or 0.0) if old else 0.0
692
+ old_last_single = old.get("last_single_id") if old else None
693
+
694
+ # 去重:若与上一片段 ID 相同,认为是重复推送,直接按状态更新不累计
695
+ if old and single_id and old_last_single == single_id:
696
+ new_qty = old_qty
697
+ new_avg = old_avg
698
+ else:
699
+ # VWAP 累计
700
+ new_qty = old_qty + qty_i
701
+ if new_qty > 0:
702
+ new_avg = (old_avg * old_qty + px_i * qty_i) / new_qty
703
+ else:
704
+ new_avg = px_i
705
+
706
+ # 写回
707
+ item.update({
708
+ "avg_price": str(new_avg) if new_qty > 0 else old.get("avg_price") if old else None,
709
+ "deal_quantity": str(new_qty) if new_qty > 0 else old.get("deal_quantity") if old else None,
710
+ "single_id": single_id,
711
+ "last_single_id": single_id,
712
+ })
713
+
714
+ # 状态文本:2=filled(整单), 3=partially_filled
715
+ # item["state"] = "filled" if state == 2 else "partially_filled"
716
+ if state == 2:
717
+ item["state"] = "filled"
718
+ elif state == 3:
719
+ item["state"] = "partially_filled"
720
+ else:
721
+ item["state"] = "unknown_"+str(state)
722
+
723
+ self._update([item])
724
+
725
+ # 整单成交 或者 部分取消 → 删除
726
+ if state == 2 or 'unknown' in item["state"]:
727
+ self._find_and_delete({"order_id": d.get("id")})
728
+ return
729
+ else:
730
+ # 新建 / 已挂出
731
+ if state == 1:
732
+ item["state"] = "open"
733
+ self._insert([item])
734
+ return
735
+
736
+ elif state == 4:
737
+ item["state"] = "canceled"
738
+ self._update([item])
739
+ self._find_and_delete({"order_id": d.get("id")})
740
+ return
741
+ else:
742
+
743
+ # 未知状态:更新后删除,避免脏数据残留
744
+ item["state"] = "unknown_"+str(state)
745
+ self._update([item])
746
+ self._find_and_delete({"order_id": d.get("id")})
747
+
748
+
749
+
750
+ class SpotBook(DataStore):
751
+ _KEYS = ["s", "S", 'p']
752
+
753
+ def _init(self) -> None:
754
+ # super().__init__()
755
+ self._time: int | None = None
756
+ self.limit = 1
757
+ self.loss = {} # 改为字典,按symbol跟踪
758
+ self.versions = {}
759
+ self.cache = []
760
+
761
+ def _onresponse(self, data: dict[str, Any]):
762
+ data = data.get("data")
763
+ symbol = data.get("symbol")
764
+ book_data = data.get("data")
765
+ asks = book_data.get("asks", [])
766
+ bids = book_data.get("bids", [])
767
+ version = int(data.get("version", None))
768
+
769
+
770
+ # 保存当前快照版本
771
+ self.versions[symbol] = version
772
+
773
+ # # 应用缓存的增量(只保留连续的部分)
774
+ # items: list = self.find({"s": symbol})
775
+ # items.sort(key=lambda x: x.get("fv", 0)) # 按 fromVersion 排序
776
+ # self._find_and_delete({"s": symbol})
777
+
778
+ # 应为我们先连接的ws, 所以可能有缓存需要去处理
779
+ items = [item for item in self.cache if item.get("s") == symbol]
780
+ items.sort(key=lambda x: x.get("fv", 0)) # 按 fromVersion 排序
781
+ self.cache = [item for item in self.cache if item.get("s") != symbol]
782
+
783
+ for side, S in ((asks, "a"), (bids, "b")):
784
+ for item in side:
785
+ self._insert([{"s": symbol, "S": S, "p": item["p"], "q": item["q"]}])
786
+
787
+ if items:
788
+ min_version = min(item.get("fv", 0) for item in items)
789
+ max_version = max(item.get("tv", 0) for item in items)
790
+ # self.version = max_version
791
+ self.versions[symbol] = max_version
792
+
793
+ # if max_version == 0:
794
+ # print('vvv---')
795
+ # print(items)
796
+
797
+ if not (min_version <= self.versions[symbol] <= max_version):
798
+ self.loss[symbol] = True
799
+ logger.warning(f"SpotBook: Snapshot version {self.version} out of range ({min_version}, {max_version}) for symbol={symbol} (丢补丁)")
800
+ return
801
+
802
+ # 处理过往msg内容
803
+ self.loss[symbol] = False
804
+ for item in items:
805
+ fv, tv = item.get("fv", 0), item.get("tv", 0)
806
+ if self.versions[symbol] <= tv and self.versions[symbol] >= fv:
807
+ if float(item["q"]) == 0.0:
808
+ self._find_and_delete({"s": symbol, "S": item["S"], "p": item["p"]})
809
+ else:
810
+ self._insert([{ "s": symbol, "S": item["S"], "p": item["p"], "q": item["q"]}])
811
+
812
+ sort_data = self.sorted({'s': symbol}, self.limit)
813
+ asks = sort_data.get('a', [])
814
+ bids = sort_data.get('b', [])
815
+ self._find_and_delete({'s': symbol})
816
+ self._update(asks + bids)
817
+
818
+ else:
819
+ self.loss[symbol] = False
820
+
821
+
822
+ def _on_message(self, msg: dict[str, Any]) -> None:
823
+
824
+ # ts = time.time() * 1000 # 预留时间戳(如需记录可用)
825
+ data = msg.get("d", {}) or {}
826
+ symbol = msg.get("s")
827
+ fv = int(data.get("fromVersion"))
828
+ tv = int(data.get("toVersion"))
829
+ if fv == 0 or tv == 0:
830
+ # print(f'发现fv或tv为0, msg:\n {msg}')
831
+ return
832
+
833
+ asks: list = data.get("asks", []) or []
834
+ bids: list = data.get("bids", []) or []
835
+
836
+ now_version = self.versions.get(symbol, None)
837
+
838
+ # 以下几张情况都会被认为正常
839
+ check_con = (
840
+ now_version is None or
841
+ fv <= now_version <= tv or
842
+ now_version + 1 == fv
843
+ )
844
+
845
+ if not check_con:
846
+ # logger.warning(f"(丢补丁) version:{now_version} fv:{fv} tv:{tv} ")
847
+ self.loss[symbol] = True # 暂时不这样做
848
+
849
+
850
+
851
+ if self.loss.get(symbol, True):
852
+ for item in asks:
853
+ self.cache.append({"s": symbol, "S": "a", "p": item["p"], "q": item["q"], "fv": fv, "tv": tv})
854
+ for item in bids:
855
+ self.cache.append({"s": symbol, "S": "b", "p": item["p"], "q": item["q"], "fv": fv, "tv": tv})
856
+ return
857
+
858
+ self.versions[symbol] = tv
859
+
860
+
861
+ to_delete, to_update = [], []
862
+ for side, S in ((asks, "a"), (bids, "b")):
863
+ for item in side:
864
+ if float(item["q"]) == 0.0:
865
+ to_delete.append({"s": symbol, "S": S, "p": item["p"]})
866
+ else:
867
+ to_update.append({"s": symbol, "S": S, "p": item["p"], "q": item["q"]})
868
+
869
+ self._delete(to_delete)
870
+ self._insert(to_update)
871
+
872
+ sort_data = self.sorted({'s': symbol}, self.limit)
873
+ asks = sort_data.get('a', [])
874
+ bids = sort_data.get('b', [])
875
+ self._find_and_delete({'s': symbol})
876
+ self._update(asks + bids)
877
+
878
+ # print(f'处理耗时: {time.time()*1000 - ts:.2f} ms')
879
+
880
+
881
+
882
+ def sorted(
883
+ self, query: Item | None = None, limit: int | None = None
884
+ ) -> dict[str, list[Item]]:
885
+ return self._sorted(
886
+ item_key="S",
887
+ item_asc_key="a",
888
+ item_desc_key="b",
889
+ sort_key="p",
890
+ query=query,
891
+ limit=limit,
892
+ )
893
+
894
+ @property
895
+ def time(self) -> int | None:
896
+ """返回最后更新时间"""
897
+ return self._time
898
+
899
+
900
+
901
+ class SpotTicker(DataStore):
902
+ _KEYS = ["symbol"]
903
+
904
+ def _fmt(self, t: dict[str, Any]) -> dict[str, Any]:
905
+ # 根据示例:
906
+ # { 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" }
907
+ return {
908
+ "id": t.get("id"),
909
+ "symbol": t.get("sb"),
910
+ "last_price": t.get("c"),
911
+ "open_price": t.get("o"),
912
+ "high_price": t.get("h"),
913
+ "low_price": t.get("l"),
914
+ "volume24": t.get("q"),
915
+ "amount24": t.get("a"),
916
+ "rise_fall_rate": t.get("r8") if t.get("r8") is not None else t.get("tzr"),
917
+ }
918
+
919
+ def _onresponse(self, data: dict[str, Any] | list[dict[str, Any]]):
920
+ # 支持 data 为:
921
+ # - 直接为 list[dict]
922
+ # - {"data": list[dict]}
923
+ # - {"d": list[dict]}
924
+ payload = data
925
+ if isinstance(data, dict):
926
+ payload = data.get("data") or data.get("d") or data
927
+ if not isinstance(payload, list):
928
+ payload = [payload]
929
+ items = [self._fmt(t) for t in payload if isinstance(t, dict)]
930
+ if not items:
931
+ return
932
+ self._clear()
933
+ self._insert(items)
934
+
935
+ def _on_message(self, msg: dict[str, Any]) -> None:
936
+ # 兼容 WS:
937
+ # { "c": "increase.tickers", "d": { ...ticker... } }
938
+ d = msg.get("d") or msg.get("data") or msg
939
+ if not isinstance(d, dict):
940
+ return
941
+ item = self._fmt(d)
942
+ if not item.get("symbol"):
943
+ return
944
+ # 覆盖式更新该 symbol
945
+ self._find_and_delete({"symbol": item["symbol"]})
946
+ self._insert([item])
947
+
948
+ class SpotDetail(DataStore):
949
+ _KEYS = ["name"]
950
+
951
+ def _fmt(self, detail: dict) -> dict:
952
+ return {
953
+ "id": detail.get("id"), # 唯一ID
954
+ "name": detail.get("vn"), # 虚拟币简称
955
+ "name_abbr": detail.get("vna"), # 虚拟币全称
956
+ "final_name": detail.get("fn"), # 法币符号/展示名
957
+ "sort": detail.get("srt"), # 排序字段
958
+ "status": detail.get("sts"), # 状态 (1=可用, 0=不可用)
959
+ "type": detail.get("tp"), # 类型 (NEW=新币种)
960
+ "internal_id": detail.get("in"), # 内部唯一流水号
961
+ "first_online_time": detail.get("fot"), # 首次上线时间
962
+ "online_time": detail.get("ot"), # 上线时间
963
+ "coin_partition": detail.get("cp"), # 所属交易区分类
964
+ "price_scale": detail.get("ps"), # 价格小数位数
965
+ "quantity_scale": detail.get("qs"), # 数量小数位数
966
+ "contract_decimal_mode": detail.get("cdm"), # 合约精度模式
967
+ "contract_address": detail.get("ca"), # 代币合约地址
968
+ }
969
+
970
+ def _onresponse(self, data: dict[str, Any]):
971
+ details = data.get("data", {}).get('USDT')
972
+ if not details:
973
+ return
974
+
975
+ items = [self._fmt(detail) for detail in details]
976
+ self._clear()
977
+ self._insert(items)
978
+
979
+
980
+ class OurbitSpotDataStore(DataStoreCollection):
981
+ """
982
+ Ourbit DataStoreCollection Spot
983
+ """
984
+ def _init(self) -> None:
985
+ self._create("book", datastore_class=SpotBook)
986
+ self._create("ticker", datastore_class=SpotTicker)
987
+ self._create("balance", datastore_class=SpotBalance)
988
+ self._create("order", datastore_class=SpotOrders)
989
+ self._create("detail", datastore_class=SpotDetail)
990
+
991
+ @property
992
+ def book(self) -> SpotBook:
993
+ """
994
+ 获取现货订单簿
995
+ .. code:: json
996
+ [
997
+ {
998
+ "s": "BTC_USDT",
999
+ "S": "a",
1000
+ "p": "110152.5",
1001
+ "q": "53539"
1002
+ }
1003
+ ]
1004
+
1005
+ """
1006
+ return self._get("book")
1007
+
1008
+ @property
1009
+ def ticker(self) -> SpotTicker:
1010
+ """
1011
+ 获取现货 Ticker
1012
+ .. code:: json
1013
+ [
1014
+ {
1015
+ "symbol": "WCT_USDT",
1016
+ "last_price": "0.3002",
1017
+ "open_price": "0.2974",
1018
+ "high_price": "0.3035",
1019
+ "low_price": "0.292",
1020
+ "volume24": "1217506.41",
1021
+ "amount24": "363548.8205",
1022
+ "rise_fall_rate": "0.0094",
1023
+ "id": "dc893d07ca8345008db4d874da726a15"
1024
+ }
1025
+ ]
1026
+ """
1027
+ return self._get("ticker")
1028
+
1029
+ @property
1030
+ def balance(self) -> SpotBalance:
1031
+ """
1032
+ 现货账户余额数据流
1033
+
1034
+ _KEYS = ["currency"]
1035
+ .. code:: python
1036
+
1037
+ [
1038
+ {
1039
+ "currency": "USDT", # 币种
1040
+ "available": "100.0", # 可用余额
1041
+ "frozen": "0.0", # 冻结余额
1042
+ "usdt_available": "100.0",# USDT 可用余额
1043
+ "usdt_frozen": "0.0", # USDT 冻结余额
1044
+ "amount": "100.0", # 总金额
1045
+ "avg_price": "1.0", # 平均价格
1046
+ "last_price": "1.0", # 最新价格
1047
+ "hidden_small": False, # 是否隐藏小额资产
1048
+ "icon": "" # 币种图标
1049
+ }
1050
+ ]
1051
+ """
1052
+ return self._get("balance", SpotBalance)
1053
+
1054
+ @property
1055
+ def detail(self) -> SpotDetail:
1056
+ """
1057
+ 现货交易对详情数据流
1058
+
1059
+ Data structure:
1060
+
1061
+ .. code:: python
1062
+
1063
+ [
1064
+ {
1065
+ "id": "3aada397655d44d69f4fc899b9c88531", # 唯一ID
1066
+ "name": "USD1", # 虚拟币简称
1067
+ "name_abbr": "USD1", # 虚拟币全称
1068
+ "final_name": "USD1", # 法币符号/展示名
1069
+ "sort": 57, # 排序字段
1070
+ "status": 1, # 状态 (1=可用, 0=不可用)
1071
+ "type": "NEW", # 类型 (NEW=新币种)
1072
+ "internal_id": "F20250508113754813fb3qX9NPxRoNUF", # 内部唯一流水号
1073
+ "first_online_time": 1746676200000, # 首次上线时间
1074
+ "online_time": 1746676200000, # 上线时间
1075
+ "coin_partition": ["ob_trade_zone_defi"], # 所属交易区分类
1076
+ "price_scale": 4, # 价格小数位数
1077
+ "quantity_scale": 2, # 数量小数位数
1078
+ "contract_decimal_mode": 1, # 合约精度模式
1079
+ "contract_address": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d" # 代币合约地址
1080
+ }
1081
+ ]
1082
+ """
1083
+ return self._get("detail", SpotDetail)
1084
+
1085
+ @property
1086
+ def orders(self) -> SpotOrders:
1087
+ """
1088
+ 现货订单数据流
1089
+
1090
+ Data structure:
1091
+
1092
+ .. code:: python
1093
+
1094
+ [
1095
+ {
1096
+ "order_id": "123456", # 订单ID
1097
+ "symbol": "BTC_USDT", # 交易对
1098
+ "currency": "USDT", # 币种
1099
+ "market": "BTC_USDT", # 市场
1100
+ "trade_type": "buy", # 交易类型
1101
+ "order_type": "limit", # 订单类型
1102
+ "price": "11000.0", # 委托价格
1103
+ "quantity": "0.01", # 委托数量
1104
+ "amount": "110.0", # 委托金额
1105
+ "deal_quantity": "0.01", # 成交数量
1106
+ "deal_amount": "110.0", # 成交金额
1107
+ "avg_price": "11000.0", # 成交均价
1108
+ "state": "open", # 订单状态
1109
+ "source": "api", # 来源
1110
+ "fee": "0.01", # 手续费
1111
+ "create_ts": 1625247600000,# 创建时间戳
1112
+ "unique_id": "abcdefg" # 唯一标识
1113
+ }
1114
+ ]
1115
+ """
1116
+ return self._get("order", SpotOrders)
1117
+
1118
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
1119
+ # print(msg, '\n')
1120
+ channel = msg.get("c")
1121
+ if 'msg' in msg:
1122
+ if 'invalid' in msg['msg']:
1123
+ logger.warning(f"WebSocket message invalid: {msg['msg']}")
1124
+ return
1125
+
1126
+ if channel is None:
1127
+ return
1128
+
1129
+ if 'increase.aggre.depth' in channel:
1130
+ self.book._on_message(msg)
1131
+
1132
+ if 'spot@private.orders' in channel:
1133
+ self.orders._on_message(msg)
1134
+
1135
+ if 'spot@private.balances' in channel:
1136
+ self.balance._on_message(msg)
1137
+
1138
+ if 'ticker' in channel:
1139
+ self.ticker._on_message(msg)
1140
+
1141
+ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
1142
+ """Initialize DataStore from HTTP response data."""
1143
+ for f in asyncio.as_completed(aws):
1144
+ res = await f
1145
+ data = await res.json()
1146
+ if res.url.path == "/api/platform/spot/market/depth":
1147
+ self.book._onresponse(data)
1148
+ if res.url.path == "/api/platform/spot/market/v2/tickers":
1149
+ self.ticker._onresponse(data)
1150
+ if res.url.path == "/api/assetbussiness/asset/spot/statistic":
1151
+ self.balance._onresponse(data)
1152
+ if res.url.path == "/api/platform/spot/order/current/orders/v2":
1153
+ self.orders._onresponse(data)
1154
+ if res.url.path == "/api/platform/spot/market/v2/symbols":
1155
+ self.detail._onresponse(data)