hyperquant 0.7__py3-none-any.whl → 0.9__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.

@@ -0,0 +1,724 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ from typing import TYPE_CHECKING, Any, Awaitable
6
+
7
+ import aiohttp
8
+ from pybotters.store import DataStore, DataStoreCollection
9
+
10
+ if TYPE_CHECKING:
11
+ from pybotters.typedefs import Item
12
+ from pybotters.ws import ClientWebSocketResponse
13
+
14
+
15
+ class Book(DataStore):
16
+ """CoinW 合约订单簿数据存储。
17
+
18
+ WebSocket 频道: futures/depth
19
+
20
+ 消息示例(来源: https://www.coinw.com/api-doc/futures-trading/market/subscribe-order-book)
21
+
22
+ .. code:: json
23
+
24
+ {
25
+ "biz": "futures",
26
+ "pairCode": "BTC",
27
+ "type": "depth",
28
+ "data": {
29
+ "asks": [{"p": "95640.3", "m": "0.807"}, ...],
30
+ "bids": [{"p": "95640.2", "m": "0.068"}, ...]
31
+ }
32
+ }
33
+ """
34
+
35
+ _KEYS = ["s", "S", "p", "q"]
36
+
37
+ def _init(self) -> None:
38
+ self.limit: int | None = None
39
+ self._last_update: float = 0.0
40
+
41
+ def _on_message(self, msg: dict[str, Any]) -> None:
42
+ data = msg.get("data")
43
+ if not isinstance(data, dict):
44
+ return
45
+
46
+ asks = data.get("asks") or []
47
+ bids = data.get("bids") or []
48
+ if not asks and not bids:
49
+ return
50
+
51
+ symbol = (
52
+ msg.get("pairCode")
53
+ or data.get("pairCode")
54
+ or msg.get("symbol")
55
+ or data.get("symbol")
56
+ )
57
+ if not symbol:
58
+ return
59
+
60
+ if self.limit is not None:
61
+ asks = asks[: self.limit]
62
+ bids = bids[: self.limit]
63
+
64
+ entries: list[dict[str, Any]] = []
65
+ for side, levels in (("a", asks), ("b", bids)):
66
+ for level in levels:
67
+ price = level.get("p") or level.get("price")
68
+ size = level.get("m") or level.get("q") or level.get("size")
69
+ if price is None or size is None:
70
+ continue
71
+ entries.append(
72
+ {
73
+ "s": str(symbol),
74
+ "S": side,
75
+ "p": str(price),
76
+ "q": str(size),
77
+ }
78
+ )
79
+
80
+ if not entries:
81
+ return
82
+
83
+ self._find_and_delete({"s": str(symbol)})
84
+ self._insert(entries)
85
+ self._last_update = time.time()
86
+
87
+ def sorted(
88
+ self, query: Item | None = None, limit: int | None = None
89
+ ) -> dict[str, list[Item]]:
90
+ return self._sorted(
91
+ item_key="S",
92
+ item_asc_key="a",
93
+ item_desc_key="b",
94
+ sort_key="p",
95
+ query=query,
96
+ limit=limit,
97
+ )
98
+
99
+ @property
100
+ def last_update(self) -> float:
101
+ return self._last_update
102
+
103
+
104
+ class Detail(DataStore):
105
+ """CoinW 合约信息数据存储。
106
+
107
+ 文档: https://www.coinw.com/api-doc/futures-trading/market/get-instrument-information
108
+ """
109
+
110
+ _KEYS = ["name"]
111
+
112
+ @staticmethod
113
+ def _transform(entry: dict[str, Any]) -> dict[str, Any] | None:
114
+ if not entry:
115
+ return None
116
+ transformed = dict(entry)
117
+ base = entry.get("name") or entry.get("base")
118
+ quote = entry.get("quote")
119
+ pricePrecision = entry.get("pricePrecision")
120
+ transformed['tick_size'] = 10 ** (-int(pricePrecision))
121
+ transformed['step_size'] = entry.get("oneLotSize")
122
+ if base and quote:
123
+ transformed.setdefault(
124
+ "symbol", f"{str(base).upper()}_{str(quote).upper()}"
125
+ )
126
+ return transformed
127
+
128
+ def _onresponse(self, data: Any) -> None:
129
+ if data is None:
130
+ self._clear()
131
+ return
132
+
133
+ entries: list[dict[str, Any]]
134
+ if isinstance(data, dict):
135
+ entries = data.get("data") or []
136
+ else:
137
+ entries = data
138
+
139
+ items: list[dict[str, Any]] = []
140
+ for entry in entries:
141
+ if not isinstance(entry, dict):
142
+ continue
143
+ transformed = self._transform(entry)
144
+ if transformed:
145
+ items.append(transformed)
146
+
147
+ self._clear()
148
+ if items:
149
+ self._insert(items)
150
+
151
+
152
+ class Ticker(DataStore):
153
+ """CoinW 24h 交易摘要数据存储。
154
+
155
+ 文档: https://www.coinw.com/api-doc/futures-trading/market/get-last-trade-summary-of-all-instruments
156
+ """
157
+
158
+ _KEYS = ["name"]
159
+
160
+ @staticmethod
161
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
162
+ instrument = entry.get("instrument") or entry.get("symbol") or entry.get("pairCode")
163
+ if not instrument:
164
+ return None
165
+ normalized = dict(entry)
166
+ normalized["instrument"] = str(instrument).upper()
167
+ return normalized
168
+
169
+ def _onresponse(self, data: Any) -> None:
170
+ if isinstance(data, dict):
171
+ entries = data.get("data") or []
172
+ else:
173
+ entries = data
174
+
175
+ self._update(entries)
176
+
177
+ def _on_message(self, msg: dict[str, Any]) -> None:
178
+ data = msg.get("data")
179
+ entries: list[dict[str, Any]] = []
180
+ if isinstance(data, list):
181
+ entries = data
182
+ elif isinstance(data, dict):
183
+ entries = data.get("data") or data.get("tickers") or []
184
+
185
+ items: list[dict[str, Any]] = []
186
+ for entry in entries:
187
+ if not isinstance(entry, dict):
188
+ continue
189
+ normalized = self._normalize(entry)
190
+ if normalized:
191
+ items.append(normalized)
192
+
193
+ if not items:
194
+ return
195
+
196
+ instruments = [{"instrument": item["instrument"]} for item in items]
197
+ self._delete(instruments)
198
+ self._insert(items)
199
+
200
+
201
+ class Orders(DataStore):
202
+ """CoinW 当前订单数据存储。"""
203
+
204
+ _KEYS = ["id"]
205
+
206
+ @staticmethod
207
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
208
+ order_id = entry.get("id")
209
+ if order_id is None:
210
+ return None
211
+ normalized = dict(entry)
212
+ normalized["id"] = str(order_id)
213
+ return normalized
214
+
215
+ def _onresponse(self, data: Any) -> None:
216
+ payload = []
217
+ if isinstance(data, dict):
218
+ inner = data.get("data")
219
+ if isinstance(inner, dict):
220
+ payload = inner.get("rows") or []
221
+ elif isinstance(inner, list):
222
+ payload = inner
223
+ elif isinstance(data, list):
224
+ payload = data
225
+
226
+ items: list[dict[str, Any]] = []
227
+ for entry in payload or []:
228
+ if not isinstance(entry, dict):
229
+ continue
230
+ normalized = self._normalize(entry)
231
+ if normalized:
232
+ items.append(normalized)
233
+
234
+ self._clear()
235
+ if items:
236
+ self._insert(items)
237
+
238
+ def _on_message(self, msg: dict[str, Any]) -> None:
239
+ data = msg.get("data")
240
+ if isinstance(data, dict) and data.get("result") is not None:
241
+ return
242
+
243
+ entries: list[dict[str, Any]] = []
244
+ if isinstance(data, list):
245
+ entries = data
246
+ elif isinstance(data, dict):
247
+ entries = data.get("rows") or data.get("data") or []
248
+
249
+ if not entries:
250
+ return
251
+
252
+ to_insert: list[dict[str, Any]] = []
253
+ to_delete: list[dict[str, Any]] = []
254
+
255
+ for entry in entries:
256
+ if not isinstance(entry, dict):
257
+ continue
258
+ normalized = self._normalize(entry)
259
+ if not normalized:
260
+ continue
261
+
262
+ status = str(normalized.get("status") or "").lower()
263
+ order_status = str(normalized.get("orderStatus") or "").lower()
264
+ remove = status in {"close", "cancel", "canceled"} or order_status in {
265
+ "finish",
266
+ "cancel",
267
+ }
268
+
269
+ # query = {"id": normalized["id"]}
270
+ to_delete.append(normalized)
271
+ if not remove:
272
+ to_insert.append(normalized)
273
+
274
+ if to_delete:
275
+ self._delete(to_delete)
276
+ if to_insert:
277
+ self._insert(to_insert)
278
+
279
+
280
+ class Position(DataStore):
281
+ """CoinW 当前持仓数据存储。"""
282
+
283
+ _KEYS = ["openId"]
284
+
285
+
286
+ def _onresponse(self, data: Any) -> None:
287
+ payload = []
288
+ if isinstance(data, dict):
289
+ payload = data.get("data") or []
290
+ elif isinstance(data, list):
291
+ payload = data
292
+
293
+ items: list[dict[str, Any]] = []
294
+ for entry in payload or []:
295
+ if not isinstance(entry, dict):
296
+ continue
297
+ entry['openId'] = str(entry.get("id"))
298
+ items.append(entry)
299
+
300
+ self._clear()
301
+ if items:
302
+ self._insert(items)
303
+
304
+ def _on_message(self, msg: dict[str, Any]) -> None:
305
+ data = msg.get("data")
306
+
307
+ if isinstance(data, dict) and data.get("result") is not None:
308
+ return
309
+
310
+ entries: list[dict[str, Any]] = []
311
+ if isinstance(data, list):
312
+ entries = data
313
+ elif isinstance(data, dict):
314
+ entries = data.get("rows") or data.get("data") or []
315
+
316
+ if not entries:
317
+ return
318
+
319
+ to_insert: list[dict[str, Any]] = []
320
+ to_update: list[dict[str, Any]] = []
321
+ to_delete: list[dict[str, Any]] = []
322
+
323
+ for entry in entries:
324
+ if not isinstance(entry, dict):
325
+ continue
326
+ normalized = entry
327
+
328
+
329
+ if normalized.get("status") == 'close':
330
+ to_delete.append(normalized)
331
+ continue
332
+
333
+ if self.find(normalized):
334
+ to_update.append(normalized)
335
+ else:
336
+ to_insert.append(normalized)
337
+
338
+ if to_delete:
339
+ self._delete(to_delete)
340
+ if to_update:
341
+ self._update(to_update)
342
+ if to_insert:
343
+ self._insert(to_insert)
344
+
345
+
346
+ class Balance(DataStore):
347
+ """CoinW 合约账户资产数据存储。"""
348
+
349
+ _KEYS = ["currency"]
350
+
351
+ @staticmethod
352
+ def _normalize_rest(entry: dict[str, Any]) -> dict[str, Any]:
353
+ currency = "USDT"
354
+ normalized = {
355
+ "currency": currency,
356
+ "availableMargin": entry.get("availableMargin"),
357
+ "availableUsdt": entry.get("availableUsdt"),
358
+ "almightyGold": entry.get("almightyGold"),
359
+ "alMargin": entry.get("alMargin"),
360
+ "alFreeze": entry.get("alFreeze"),
361
+ "time": entry.get("time"),
362
+ "userId": entry.get("userId"),
363
+ }
364
+ if "available" not in normalized:
365
+ normalized["available"] = entry.get("availableUsdt")
366
+ normalized["availableMargin"] = entry.get("availableMargin")
367
+ normalized["margin"] = entry.get("alMargin")
368
+ normalized["freeze"] = entry.get("alFreeze")
369
+ return {k: v for k, v in normalized.items() if v is not None}
370
+
371
+ @staticmethod
372
+ def _normalize_ws(entry: dict[str, Any]) -> dict[str, Any] | None:
373
+ currency = entry.get("currency")
374
+ if not currency:
375
+ return None
376
+ currency_str = str(currency).upper()
377
+ normalized = dict(entry)
378
+ normalized["currency"] = currency_str
379
+ # 对齐 REST 字段
380
+ if "availableUsdt" not in normalized and "available" in normalized:
381
+ normalized["availableUsdt"] = normalized["available"]
382
+ if "alMargin" not in normalized and "margin" in normalized:
383
+ normalized["alMargin"] = normalized["margin"]
384
+ if "alFreeze" not in normalized and "freeze" in normalized:
385
+ normalized["alFreeze"] = normalized["freeze"]
386
+ return normalized
387
+
388
+ def _onresponse(self, data: Any) -> None:
389
+ entry = None
390
+ if isinstance(data, dict):
391
+ entry = data.get("data")
392
+ if not isinstance(entry, dict):
393
+ entry = {}
394
+
395
+ self._clear()
396
+ normalized = self._normalize_rest(entry)
397
+ self._insert([normalized])
398
+
399
+ def _on_message(self, msg: dict[str, Any]) -> None:
400
+ data = msg.get("data")
401
+ if isinstance(data, dict) and data.get("result") is not None:
402
+ return
403
+
404
+ entries: list[dict[str, Any]] = []
405
+ if isinstance(data, list):
406
+ entries = data
407
+ elif isinstance(data, dict):
408
+ entries = data.get("rows") or data.get("data") or []
409
+
410
+ if not entries:
411
+ return
412
+
413
+ normalized_items: list[dict[str, Any]] = []
414
+ for entry in entries:
415
+ if not isinstance(entry, dict):
416
+ continue
417
+ normalized = self._normalize_ws(entry)
418
+ if normalized:
419
+ normalized_items.append(normalized)
420
+
421
+ if not normalized_items:
422
+ return
423
+
424
+ currencies = [{"currency": item["currency"]} for item in normalized_items]
425
+ self._delete(currencies)
426
+ self._insert(normalized_items)
427
+
428
+
429
+ class CoinwFuturesDataStore(DataStoreCollection):
430
+ """CoinW 合约交易 DataStoreCollection。
431
+
432
+ - REST: https://api.coinw.com/v1/perpum/instruments
433
+ - WebSocket: wss://ws.futurescw.com/perpum (depth)
434
+ """
435
+
436
+ def _init(self) -> None:
437
+ self._create("book", datastore_class=Book)
438
+ self._create("detail", datastore_class=Detail)
439
+ self._create("ticker", datastore_class=Ticker)
440
+ self._create("orders", datastore_class=Orders)
441
+ self._create("position", datastore_class=Position)
442
+ self._create("balance", datastore_class=Balance)
443
+
444
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
445
+ msg_type = msg.get("type")
446
+ # print(msg)
447
+ if msg_type == "depth":
448
+ self.book._on_message(msg)
449
+ elif msg_type == "order":
450
+ self.orders._on_message(msg)
451
+ elif msg_type == "position" or msg_type == "position_change":
452
+ self.position._on_message(msg)
453
+ elif msg_type == "assets":
454
+ self.balance._on_message(msg)
455
+ elif msg_type == "ticker":
456
+ self.ticker._on_message(msg)
457
+
458
+ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
459
+ for fut in asyncio.as_completed(aws):
460
+ res = await fut
461
+ data = await res.json()
462
+ if res.url.path == "/v1/perpum/instruments":
463
+ self.detail._onresponse(data)
464
+ elif res.url.path == "/v1/perpumPublic/tickers":
465
+ self.ticker._onresponse(data)
466
+ elif res.url.path == "/v1/perpum/orders/open":
467
+ self.orders._onresponse(data)
468
+ elif res.url.path == "/v1/perpum/positions/all":
469
+ self.position._onresponse(data)
470
+ elif res.url.path == "/v1/perpum/account/getUserAssets":
471
+ self.balance._onresponse(data)
472
+
473
+ @property
474
+ def book(self) -> Book:
475
+ """订单簿深度数据流。
476
+
477
+ 数据来源:
478
+ - WebSocket: ``type == "depth"`` (参考 https://www.coinw.com/api-doc/futures-trading/market/subscribe-order-book)
479
+
480
+ 数据结构(节选)::
481
+
482
+ {
483
+ "s": "BTC",
484
+ "S": "a", # 卖单
485
+ "p": "95640.3",
486
+ "q": "0.807"
487
+ }
488
+ """
489
+
490
+ return self._get("book", Book)
491
+
492
+ @property
493
+ def detail(self) -> Detail:
494
+ """合约基础信息数据流。
495
+
496
+ 响应示例(节选):
497
+
498
+ .. code:: json
499
+
500
+ {
501
+ "base": "btc",
502
+ "closeSpread": 0.0002,
503
+ "commissionRate": 0.0006,
504
+ "configBo": {
505
+ "margins": {
506
+ "100": 0.075,
507
+ "5": 0.00375,
508
+ "50": 0.0375,
509
+ "20": 0.015,
510
+ "10": 0.0075
511
+ },
512
+ "simulatedMargins": {
513
+ "5": 0.00375,
514
+ "20": 0.015,
515
+ "10": 0.0075
516
+ }
517
+ },
518
+ "createdDate": 1548950400000,
519
+ "defaultLeverage": 20,
520
+ "defaultStopLossRate": 0.99,
521
+ "defaultStopProfitRate": 100,
522
+ "depthPrecision": "0.1,1,10,50,100",
523
+ "iconUrl": "https://hkto-prod.oss-accelerate.aliyuncs.com/4dfca512e957e14f05da07751a96061cf4bfd5df438504f65287fa0a8c3cadb6.svg",
524
+ "id": 1,
525
+ "indexId": 1,
526
+ "leverage": [
527
+ 5,
528
+ 10,
529
+ 20,
530
+ 50,
531
+ 100,
532
+ 125,
533
+ 200
534
+ ],
535
+ "makerFee": "0.0001",
536
+ "maxLeverage": 200,
537
+ "maxPosition": 20000,
538
+ "minLeverage": 1,
539
+ "minSize": 1,
540
+ "name": "BTC",
541
+ "oneLotMargin": 1,
542
+ "oneLotSize": 0.001,
543
+ "oneMaxPosition": 15000,
544
+ "openSpread": 0.0003,
545
+ "orderLimitMaxRate": 0.05,
546
+ "orderLimitMinRate": 0.05,
547
+ "orderMarketLimitAmount": 10,
548
+ "orderPlanLimitAmount": 30,
549
+ "partitionIds": "2013,2011",
550
+ "platform": 0,
551
+ "pricePrecision": 1,
552
+ "quote": "usdt",
553
+ "selected": 0,
554
+ "settledAt": 1761062400000,
555
+ "settledPeriod": 8,
556
+ "settlementRate": 0.0004,
557
+ "sort": 1,
558
+ "status": "online",
559
+ "stopCrossPositionRate": 0.1,
560
+ "stopSurplusRate": 0.01,
561
+ "takerFee": "0.0006",
562
+ "updatedDate": 1752040118000,
563
+ "symbol": "BTC_USDT",
564
+ "tick_size": 1.0,
565
+ "step_size": 0.001
566
+ }
567
+ """
568
+
569
+ return self._get("detail", Detail)
570
+
571
+ @property
572
+ def ticker(self) -> Ticker:
573
+ """24小时交易摘要数据流。
574
+
575
+ .. code:: json
576
+
577
+ {
578
+ 'fair_price': 97072.4,
579
+ 'max_leverage': 125,
580
+ 'total_volume': 0.003,
581
+ 'price_coin': 'btc',
582
+ 'contract_id': 1,
583
+ 'base_coin': 'btc',
584
+ 'high': 98001.5,
585
+ 'rise_fall_rate': 0.012275,
586
+ 'low': 95371.4,
587
+ 'name': 'BTCUSDT',
588
+ 'contract_size': 0.001,
589
+ 'quote_coin': 'usdt',
590
+ 'last_price': 97072.4
591
+ }
592
+
593
+ """
594
+ return self._get("ticker", Ticker)
595
+
596
+ @property
597
+ def orders(self) -> Orders:
598
+ """当前订单数据流。
599
+
600
+ 数据来源:
601
+ - REST: ``GET /v1/perpum/orders/open``
602
+ - WebSocket: ``type == "order"``
603
+
604
+ 数据结构(节选)::
605
+
606
+ {
607
+ "currentPiece": "1",
608
+ "leverage": "50",
609
+ "originalType": "plan",
610
+ "processStatus": 0,
611
+ "contractType": 1,
612
+ "frozenFee": "0",
613
+ "openPrice": "175",
614
+ "orderStatus": "unFinish",
615
+ "instrument": "SOL",
616
+ "quantityUnit": 1,
617
+ "source": "web",
618
+ "updatedDate": 1761109078404,
619
+ "positionModel": 1,
620
+ "posType": "plan",
621
+ "baseSize": "0.1",
622
+ "quote": "usdt",
623
+ "liquidateBy": "manual",
624
+ "makerFee": "0.0001",
625
+ "totalPiece": "1",
626
+ "tradePiece": "0",
627
+ "orderPrice": "175",
628
+ "id": "33309055657317395",
629
+ "direction": "long",
630
+ "margin": "0.35",
631
+ "indexPrice": "185.68",
632
+ "quantity": "1",
633
+ "takerFee": "0.0006",
634
+ "userId": "1757458",
635
+ "cancelPiece": "0",
636
+ "createdDate": 1761109078404,
637
+ "positionMargin": "0.35",
638
+ "base": "sol",
639
+ "status": "open"
640
+ }
641
+ """
642
+
643
+ return self._get("orders", Orders)
644
+
645
+ @property
646
+ def position(self) -> Position:
647
+ """当前持仓数据流。
648
+
649
+ 数据来源:
650
+ - REST: ``GET /v1/perpum/positions``
651
+ - WebSocket: ``type == "position"``
652
+
653
+ .. code:: json
654
+
655
+ {
656
+ "currentPiece": "0",
657
+ "isProfession": 0,
658
+ "leverage": "10",
659
+ "originalType": "execute",
660
+ "orderId": "33309059291614824",
661
+ "contractType": 1,
662
+ "openId": "2435521222638707873",
663
+ "fee": "0.00020724",
664
+ "openPrice": "0.3456",
665
+ "orderStatus": "finish",
666
+ "instrument": "JUP",
667
+ "quantityUnit": 1,
668
+ "source": "api",
669
+ "updatedDate": 1761192795412,
670
+ "positionModel": 1,
671
+ "feeRate": "0.0006",
672
+ "netProfit": "-0.00040724",
673
+ "baseSize": "1",
674
+ "quote": "usdt",
675
+ "liquidateBy": "manual",
676
+ "totalPiece": "1",
677
+ "orderPrice": "0",
678
+ "id": "23469279597150213",
679
+ "fundingSettle": "0",
680
+ "direction": "long",
681
+ "margin": "0.03435264",
682
+ "takerMaker": 1,
683
+ "indexPrice": "0.3455",
684
+ "quantity": "0.03456",
685
+ "userId": "1757458",
686
+ "closedPiece": "1",
687
+ "createdDate": 1761192793000,
688
+ "hedgeId": "23469279597150214",
689
+ "closePrice": "0.3454",
690
+ "positionMargin": "0.03435264",
691
+ "base": "jup",
692
+ "realPrice": "0.3454",
693
+ "status": "close"
694
+ }
695
+ """
696
+
697
+ return self._get("position", Position)
698
+
699
+ @property
700
+ def balance(self) -> Balance:
701
+ """合约账户资产数据流。
702
+
703
+ 数据来源:
704
+ - REST: ``GET /v1/perpum/account/getUserAssets``
705
+ - WebSocket: ``type == "assets"``
706
+
707
+ .. code:: json
708
+
709
+ {
710
+ "currency": "USDT",
711
+ "availableMargin": 0.0,
712
+ "availableUsdt": 0,
713
+ "almightyGold": 0.0,
714
+ "alMargin": 0,
715
+ "alFreeze": 0,
716
+ "time": 1761055905797,
717
+ "userId": 1757458,
718
+ "available": 0,
719
+ "margin": 0,
720
+ "freeze": 0
721
+ }
722
+ """
723
+
724
+ return self._get("balance", Balance)