hyperquant 0.3__py3-none-any.whl → 0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyperquant/broker/auth.py +47 -10
- hyperquant/broker/hyperliquid.py +0 -2
- hyperquant/broker/models/ourbit.py +687 -83
- hyperquant/broker/ourbit.py +336 -31
- hyperquant/broker/ws.py +37 -1
- hyperquant/core.py +10 -6
- hyperquant/logkit.py +16 -1
- {hyperquant-0.3.dist-info → hyperquant-0.5.dist-info}/METADATA +1 -1
- {hyperquant-0.3.dist-info → hyperquant-0.5.dist-info}/RECORD +10 -10
- {hyperquant-0.3.dist-info → hyperquant-0.5.dist-info}/WHEEL +0 -0
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import logging
|
5
|
+
import time
|
5
6
|
from typing import TYPE_CHECKING, Any, Awaitable
|
6
7
|
|
7
8
|
import aiohttp
|
@@ -29,7 +30,7 @@ class Book(DataStore):
|
|
29
30
|
|
30
31
|
"""
|
31
32
|
|
32
|
-
_KEYS = ["symbol", "side",
|
33
|
+
_KEYS = ["symbol", "side", "i"]
|
33
34
|
|
34
35
|
def _init(self) -> None:
|
35
36
|
# super().__init__()
|
@@ -41,6 +42,10 @@ class Book(DataStore):
|
|
41
42
|
data = msg.get("data", {})
|
42
43
|
asks = data.get("asks", [])
|
43
44
|
bids = data.get("bids", [])
|
45
|
+
# 提速 默认 5当前
|
46
|
+
asks = asks[:5]
|
47
|
+
bids = bids[:5]
|
48
|
+
|
44
49
|
timestamp = data.get("ct") # 使用服务器时间
|
45
50
|
|
46
51
|
data_to_insert: list[Item] = []
|
@@ -50,7 +55,7 @@ class Book(DataStore):
|
|
50
55
|
|
51
56
|
# 处理买卖盘数据
|
52
57
|
for side_id, levels in (("B", bids), ("A", asks)):
|
53
|
-
for level in levels:
|
58
|
+
for i, level in enumerate(levels):
|
54
59
|
# level格式: [price, size, count]
|
55
60
|
if len(level) >= 3:
|
56
61
|
price, size, count = level[0:3]
|
@@ -61,6 +66,7 @@ class Book(DataStore):
|
|
61
66
|
"px": str(price),
|
62
67
|
"sz": str(size),
|
63
68
|
"count": count,
|
69
|
+
"i": i
|
64
70
|
}
|
65
71
|
)
|
66
72
|
|
@@ -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
|
-
|
197
|
-
|
198
|
-
|
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
|
-
"
|
494
|
+
"state": "open", // ("open", "closed", "canceled")
|
438
495
|
"create_ts": 1625247600000,
|
439
496
|
"update_ts": 1625247600000
|
440
497
|
}
|
@@ -446,57 +503,604 @@ class OurbitSwapDataStore(DataStoreCollection):
|
|
446
503
|
def position(self) -> Position:
|
447
504
|
"""
|
448
505
|
持仓数据
|
449
|
-
|
450
506
|
Data structure:
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
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
|
+
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])
|
600
|
+
|
601
|
+
|
602
|
+
# SpotOrders: 现货订单数据存储
|
603
|
+
class SpotOrders(DataStore):
|
604
|
+
_KEYS = ["order_id"]
|
605
|
+
|
606
|
+
|
607
|
+
def _fmt(self, order: dict) -> dict:
|
608
|
+
state = order.get("state")
|
609
|
+
if state == 1 or state == 2:
|
610
|
+
state = "open"
|
611
|
+
elif state == 3:
|
612
|
+
state = "filled"
|
613
|
+
elif state == 4:
|
614
|
+
state = "canceled"
|
615
|
+
|
616
|
+
return {
|
617
|
+
"order_id": order.get("id"),
|
618
|
+
"symbol": order.get("symbol"),
|
619
|
+
"currency": order.get("currency"),
|
620
|
+
"market": order.get("market"),
|
621
|
+
"trade_type": order.get("tradeType"),
|
622
|
+
"order_type": order.get("orderType"),
|
623
|
+
"price": order.get("price"),
|
624
|
+
"quantity": order.get("quantity"),
|
625
|
+
"amount": order.get("amount"),
|
626
|
+
"deal_quantity": order.get("dealQuantity"),
|
627
|
+
"deal_amount": order.get("dealAmount"),
|
628
|
+
"avg_price": order.get("avgPrice"),
|
629
|
+
"state": order.get("state"),
|
630
|
+
"source": order.get("source"),
|
631
|
+
"fee": order.get("fee"),
|
632
|
+
"create_ts": order.get("createTime"),
|
633
|
+
"unique_id": order.get("uniqueId"),
|
634
|
+
}
|
635
|
+
|
636
|
+
|
637
|
+
|
638
|
+
def _onresponse(self, data: dict[str, Any]):
|
639
|
+
orders = (data.get("data") or {}).get("resultList", [])
|
640
|
+
items = [self._fmt(order) for order in orders]
|
641
|
+
self._clear()
|
642
|
+
if items:
|
643
|
+
self._insert(items)
|
644
|
+
|
645
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
646
|
+
d:dict = msg.get("d", {})
|
647
|
+
|
648
|
+
|
649
|
+
item = {
|
650
|
+
"order_id": d.get("id"),
|
651
|
+
"symbol": msg.get("s") or d.get("symbol"),
|
652
|
+
"trade_type": d.get("tradeType"),
|
653
|
+
"order_type": d.get("orderType"),
|
654
|
+
"price": d.get("price"),
|
655
|
+
"quantity": d.get("quantity"),
|
656
|
+
"amount": d.get("amount"),
|
657
|
+
"remain_quantity": d.get("remainQ"),
|
658
|
+
"remain_amount": d.get("remainA"),
|
659
|
+
"state": d.get("status"),
|
660
|
+
"client_order_id": d.get("clientOrderId"),
|
661
|
+
"is_taker": d.get("isTaker"),
|
662
|
+
"create_ts": d.get("createTime"),
|
663
|
+
"source": d.get("internal"),
|
664
|
+
}
|
665
|
+
|
666
|
+
state = d.get("status")
|
667
|
+
|
668
|
+
if state == 1:
|
669
|
+
item["state"] = "open"
|
670
|
+
self._insert([item])
|
482
671
|
|
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
|
+
|
678
|
+
# 如果这三个字段存在追加
|
679
|
+
if d.get("singleDealId") and d.get("singleDealPrice") and d.get("singleDealQuantity"):
|
680
|
+
item.update({
|
681
|
+
"unique_id": d.get("singleDealId"),
|
682
|
+
"avg_price": d.get("singleDealPrice"),
|
683
|
+
"deal_quantity": d.get("singleDealQuantity"),
|
684
|
+
})
|
685
|
+
|
686
|
+
self._update([item])
|
687
|
+
self._find_and_delete({ "order_id": d.get("id") })
|
688
|
+
|
689
|
+
elif state == 4:
|
690
|
+
item["state"] = "canceled"
|
691
|
+
self._update([item])
|
692
|
+
self._find_and_delete({ "order_id": d.get("id") })
|
693
|
+
|
694
|
+
|
695
|
+
|
696
|
+
|
697
|
+
class SpotBook(DataStore):
|
698
|
+
_KEYS = ["s", "S", 'p']
|
699
|
+
|
700
|
+
def _init(self) -> None:
|
701
|
+
# super().__init__()
|
702
|
+
self._time: int | None = None
|
703
|
+
self.limit = 1
|
704
|
+
self.loss = {} # 改为字典,按symbol跟踪
|
705
|
+
self.versions = {}
|
706
|
+
self.cache = []
|
707
|
+
|
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))
|
715
|
+
|
716
|
+
|
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]
|
729
|
+
|
730
|
+
for side, S in ((asks, "a"), (bids, "b")):
|
731
|
+
for item in side:
|
732
|
+
self._insert([{"s": symbol, "S": S, "p": item["p"], "q": item["q"]}])
|
733
|
+
|
734
|
+
if items:
|
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
|
767
|
+
|
768
|
+
|
769
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
770
|
+
|
771
|
+
# ts = time.time() * 1000 # 预留时间戳(如需记录可用)
|
772
|
+
data = msg.get("d", {}) or {}
|
773
|
+
symbol = msg.get("s")
|
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
|
+
|
780
|
+
asks: list = data.get("asks", []) or []
|
781
|
+
bids: list = data.get("bids", []) or []
|
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
|
+
|
808
|
+
to_delete, to_update = [], []
|
809
|
+
for side, S in ((asks, "a"), (bids, "b")):
|
810
|
+
for item in side:
|
811
|
+
if float(item["q"]) == 0.0:
|
812
|
+
to_delete.append({"s": symbol, "S": S, "p": item["p"]})
|
813
|
+
else:
|
814
|
+
to_update.append({"s": symbol, "S": S, "p": item["p"], "q": item["q"]})
|
815
|
+
|
816
|
+
self._delete(to_delete)
|
817
|
+
self._insert(to_update)
|
818
|
+
|
819
|
+
sort_data = self.sorted({'s': symbol}, self.limit)
|
820
|
+
asks = sort_data.get('a', [])
|
821
|
+
bids = sort_data.get('b', [])
|
822
|
+
self._find_and_delete({'s': symbol})
|
823
|
+
self._update(asks + bids)
|
824
|
+
|
825
|
+
# print(f'处理耗时: {time.time()*1000 - ts:.2f} ms')
|
826
|
+
|
827
|
+
|
828
|
+
|
829
|
+
def sorted(
|
830
|
+
self, query: Item | None = None, limit: int | None = None
|
831
|
+
) -> dict[str, list[Item]]:
|
832
|
+
return self._sorted(
|
833
|
+
item_key="S",
|
834
|
+
item_asc_key="a",
|
835
|
+
item_desc_key="b",
|
836
|
+
sort_key="p",
|
837
|
+
query=query,
|
838
|
+
limit=limit,
|
839
|
+
)
|
840
|
+
|
841
|
+
@property
|
842
|
+
def time(self) -> int | None:
|
843
|
+
"""返回最后更新时间"""
|
844
|
+
return self._time
|
845
|
+
|
846
|
+
|
847
|
+
|
848
|
+
class SpotTicker(DataStore):
|
849
|
+
_KEYS = ["symbol"]
|
850
|
+
|
851
|
+
def _fmt(self, t: dict[str, Any]) -> dict[str, Any]:
|
852
|
+
# 根据示例:
|
853
|
+
# { 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" }
|
854
|
+
return {
|
855
|
+
"id": t.get("id"),
|
856
|
+
"symbol": t.get("sb"),
|
857
|
+
"last_price": t.get("c"),
|
858
|
+
"open_price": t.get("o"),
|
859
|
+
"high_price": t.get("h"),
|
860
|
+
"low_price": t.get("l"),
|
861
|
+
"volume24": t.get("q"),
|
862
|
+
"amount24": t.get("a"),
|
863
|
+
"rise_fall_rate": t.get("r8") if t.get("r8") is not None else t.get("tzr"),
|
864
|
+
}
|
865
|
+
|
866
|
+
def _onresponse(self, data: dict[str, Any] | list[dict[str, Any]]):
|
867
|
+
# 支持 data 为:
|
868
|
+
# - 直接为 list[dict]
|
869
|
+
# - {"data": list[dict]}
|
870
|
+
# - {"d": list[dict]}
|
871
|
+
payload = data
|
872
|
+
if isinstance(data, dict):
|
873
|
+
payload = data.get("data") or data.get("d") or data
|
874
|
+
if not isinstance(payload, list):
|
875
|
+
payload = [payload]
|
876
|
+
items = [self._fmt(t) for t in payload if isinstance(t, dict)]
|
877
|
+
if not items:
|
878
|
+
return
|
879
|
+
self._clear()
|
880
|
+
self._insert(items)
|
881
|
+
|
882
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
883
|
+
# 兼容 WS:
|
884
|
+
# { "c": "increase.tickers", "d": { ...ticker... } }
|
885
|
+
d = msg.get("d") or msg.get("data") or msg
|
886
|
+
if not isinstance(d, dict):
|
887
|
+
return
|
888
|
+
item = self._fmt(d)
|
889
|
+
if not item.get("symbol"):
|
890
|
+
return
|
891
|
+
# 覆盖式更新该 symbol
|
892
|
+
self._find_and_delete({"symbol": item["symbol"]})
|
893
|
+
self._insert([item])
|
894
|
+
|
895
|
+
class SpotDetail(DataStore):
|
896
|
+
_KEYS = ["name"]
|
897
|
+
|
898
|
+
def _fmt(self, detail: dict) -> dict:
|
899
|
+
return {
|
900
|
+
"id": detail.get("id"), # 唯一ID
|
901
|
+
"name": detail.get("vn"), # 虚拟币简称
|
902
|
+
"name_abbr": detail.get("vna"), # 虚拟币全称
|
903
|
+
"final_name": detail.get("fn"), # 法币符号/展示名
|
904
|
+
"sort": detail.get("srt"), # 排序字段
|
905
|
+
"status": detail.get("sts"), # 状态 (1=可用, 0=不可用)
|
906
|
+
"type": detail.get("tp"), # 类型 (NEW=新币种)
|
907
|
+
"internal_id": detail.get("in"), # 内部唯一流水号
|
908
|
+
"first_online_time": detail.get("fot"), # 首次上线时间
|
909
|
+
"online_time": detail.get("ot"), # 上线时间
|
910
|
+
"coin_partition": detail.get("cp"), # 所属交易区分类
|
911
|
+
"price_scale": detail.get("ps"), # 价格小数位数
|
912
|
+
"quantity_scale": detail.get("qs"), # 数量小数位数
|
913
|
+
"contract_decimal_mode": detail.get("cdm"), # 合约精度模式
|
914
|
+
"contract_address": detail.get("ca"), # 代币合约地址
|
915
|
+
}
|
916
|
+
|
917
|
+
def _onresponse(self, data: dict[str, Any]):
|
918
|
+
details = data.get("data", {}).get('USDT')
|
919
|
+
if not details:
|
920
|
+
return
|
921
|
+
|
922
|
+
items = [self._fmt(detail) for detail in details]
|
923
|
+
self._clear()
|
924
|
+
self._insert(items)
|
925
|
+
|
926
|
+
|
927
|
+
class OurbitSpotDataStore(DataStoreCollection):
|
928
|
+
"""
|
929
|
+
Ourbit DataStoreCollection Spot
|
930
|
+
"""
|
931
|
+
def _init(self) -> None:
|
932
|
+
self._create("book", datastore_class=SpotBook)
|
933
|
+
self._create("ticker", datastore_class=SpotTicker)
|
934
|
+
self._create("balance", datastore_class=SpotBalance)
|
935
|
+
self._create("order", datastore_class=SpotOrders)
|
936
|
+
self._create("detail", datastore_class=SpotDetail)
|
937
|
+
|
938
|
+
@property
|
939
|
+
def book(self) -> SpotBook:
|
940
|
+
"""
|
941
|
+
获取现货订单簿
|
942
|
+
.. code:: json
|
943
|
+
[
|
944
|
+
{
|
945
|
+
"s": "BTC_USDT",
|
946
|
+
"S": "a",
|
947
|
+
"p": "110152.5",
|
948
|
+
"q": "53539"
|
949
|
+
}
|
950
|
+
]
|
951
|
+
|
952
|
+
"""
|
953
|
+
return self._get("book")
|
954
|
+
|
955
|
+
@property
|
956
|
+
def ticker(self) -> SpotTicker:
|
957
|
+
"""
|
958
|
+
获取现货 Ticker
|
959
|
+
.. code:: json
|
960
|
+
[
|
961
|
+
{
|
962
|
+
"symbol": "WCT_USDT",
|
963
|
+
"last_price": "0.3002",
|
964
|
+
"open_price": "0.2974",
|
965
|
+
"high_price": "0.3035",
|
966
|
+
"low_price": "0.292",
|
967
|
+
"volume24": "1217506.41",
|
968
|
+
"amount24": "363548.8205",
|
969
|
+
"rise_fall_rate": "0.0094",
|
970
|
+
"id": "dc893d07ca8345008db4d874da726a15"
|
971
|
+
}
|
972
|
+
]
|
973
|
+
"""
|
974
|
+
return self._get("ticker")
|
975
|
+
|
976
|
+
@property
|
977
|
+
def balance(self) -> SpotBalance:
|
978
|
+
"""
|
979
|
+
现货账户余额数据流
|
980
|
+
|
981
|
+
_KEYS = ["currency"]
|
982
|
+
.. code:: python
|
983
|
+
|
984
|
+
[
|
985
|
+
{
|
986
|
+
"currency": "USDT", # 币种
|
987
|
+
"available": "100.0", # 可用余额
|
988
|
+
"frozen": "0.0", # 冻结余额
|
989
|
+
"usdt_available": "100.0",# USDT 可用余额
|
990
|
+
"usdt_frozen": "0.0", # USDT 冻结余额
|
991
|
+
"amount": "100.0", # 总金额
|
992
|
+
"avg_price": "1.0", # 平均价格
|
993
|
+
"last_price": "1.0", # 最新价格
|
994
|
+
"hidden_small": False, # 是否隐藏小额资产
|
995
|
+
"icon": "" # 币种图标
|
996
|
+
}
|
997
|
+
]
|
998
|
+
"""
|
999
|
+
return self._get("balance", SpotBalance)
|
1000
|
+
|
1001
|
+
@property
|
1002
|
+
def detail(self) -> SpotDetail:
|
1003
|
+
"""
|
1004
|
+
现货交易对详情数据流
|
1005
|
+
|
483
1006
|
Data structure:
|
1007
|
+
|
484
1008
|
.. code:: python
|
1009
|
+
|
485
1010
|
[
|
486
1011
|
{
|
487
|
-
"
|
488
|
-
"
|
489
|
-
"
|
490
|
-
"
|
491
|
-
"
|
492
|
-
"
|
493
|
-
"
|
494
|
-
"
|
495
|
-
"
|
496
|
-
"
|
497
|
-
"
|
498
|
-
"
|
1012
|
+
"id": "3aada397655d44d69f4fc899b9c88531", # 唯一ID
|
1013
|
+
"name": "USD1", # 虚拟币简称
|
1014
|
+
"name_abbr": "USD1", # 虚拟币全称
|
1015
|
+
"final_name": "USD1", # 法币符号/展示名
|
1016
|
+
"sort": 57, # 排序字段
|
1017
|
+
"status": 1, # 状态 (1=可用, 0=不可用)
|
1018
|
+
"type": "NEW", # 类型 (NEW=新币种)
|
1019
|
+
"internal_id": "F20250508113754813fb3qX9NPxRoNUF", # 内部唯一流水号
|
1020
|
+
"first_online_time": 1746676200000, # 首次上线时间
|
1021
|
+
"online_time": 1746676200000, # 上线时间
|
1022
|
+
"coin_partition": ["ob_trade_zone_defi"], # 所属交易区分类
|
1023
|
+
"price_scale": 4, # 价格小数位数
|
1024
|
+
"quantity_scale": 2, # 数量小数位数
|
1025
|
+
"contract_decimal_mode": 1, # 合约精度模式
|
1026
|
+
"contract_address": "0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d" # 代币合约地址
|
499
1027
|
}
|
500
1028
|
]
|
501
1029
|
"""
|
502
|
-
return self._get("
|
1030
|
+
return self._get("detail", SpotDetail)
|
1031
|
+
|
1032
|
+
@property
|
1033
|
+
def orders(self) -> SpotOrders:
|
1034
|
+
"""
|
1035
|
+
现货订单数据流(SpotOrders)
|
1036
|
+
|
1037
|
+
Keys: ["order_id"]
|
1038
|
+
|
1039
|
+
说明:
|
1040
|
+
- 聚合 REST 当前订单与 WS 增量推送(频道: spot@private.orders)
|
1041
|
+
- 统一状态值:open, partially_filled, filled, canceled
|
1042
|
+
|
1043
|
+
Data structure:
|
1044
|
+
.. code:: python
|
1045
|
+
[
|
1046
|
+
{
|
1047
|
+
"order_id": "123456", # 订单ID
|
1048
|
+
"symbol": "BTC_USDT", # 交易对
|
1049
|
+
"currency": "USDT", # 币种
|
1050
|
+
"market": "BTC_USDT", # 市场
|
1051
|
+
"trade_type": "buy", # buy/sell
|
1052
|
+
"order_type": "limit", # limit/market
|
1053
|
+
"price": "11000.0", # 委托价格
|
1054
|
+
"quantity": "0.01", # 委托数量
|
1055
|
+
"amount": "110.0", # 委托金额
|
1056
|
+
"deal_quantity": "0.01", # 已成交数量(累计)
|
1057
|
+
"deal_amount": "110.0", # 已成交金额(累计)
|
1058
|
+
"avg_price": "11000.0", # 成交均价(累计)
|
1059
|
+
"state": "open", # open/partially_filled/filled/canceled
|
1060
|
+
"fee": "0.01", # 手续费
|
1061
|
+
"source": "api", # 来源
|
1062
|
+
"create_ts": 1625247600000,# 创建时间戳(毫秒)
|
1063
|
+
"unique_id": "abcdefg" # 唯一标识
|
1064
|
+
}
|
1065
|
+
]
|
1066
|
+
"""
|
1067
|
+
return self._get("order", SpotOrders)
|
1068
|
+
|
1069
|
+
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
1070
|
+
# print(msg, '\n')
|
1071
|
+
channel = msg.get("c")
|
1072
|
+
if 'msg' in msg:
|
1073
|
+
if 'invalid' in msg['msg']:
|
1074
|
+
logger.warning(f"WebSocket message invalid: {msg['msg']}")
|
1075
|
+
return
|
1076
|
+
|
1077
|
+
if channel is None:
|
1078
|
+
return
|
1079
|
+
|
1080
|
+
if 'increase.aggre.depth' in channel:
|
1081
|
+
self.book._on_message(msg)
|
1082
|
+
|
1083
|
+
if 'spot@private.orders' in channel:
|
1084
|
+
self.orders._on_message(msg)
|
1085
|
+
|
1086
|
+
if 'spot@private.balances' in channel:
|
1087
|
+
self.balance._on_message(msg)
|
1088
|
+
|
1089
|
+
if 'ticker' in channel:
|
1090
|
+
self.ticker._on_message(msg)
|
1091
|
+
|
1092
|
+
async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
|
1093
|
+
"""Initialize DataStore from HTTP response data."""
|
1094
|
+
for f in asyncio.as_completed(aws):
|
1095
|
+
res = await f
|
1096
|
+
data = await res.json()
|
1097
|
+
if res.url.path == "/api/platform/spot/market/depth":
|
1098
|
+
self.book._onresponse(data)
|
1099
|
+
if res.url.path == "/api/platform/spot/market/v2/tickers":
|
1100
|
+
self.ticker._onresponse(data)
|
1101
|
+
if res.url.path == "/api/assetbussiness/asset/spot/statistic":
|
1102
|
+
self.balance._onresponse(data)
|
1103
|
+
if res.url.path == "/api/platform/spot/order/current/orders/v2":
|
1104
|
+
self.orders._onresponse(data)
|
1105
|
+
if res.url.path == "/api/platform/spot/market/v2/symbols":
|
1106
|
+
self.detail._onresponse(data)
|