unicex 0.13.17__py3-none-any.whl → 0.16.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.
Files changed (63) hide show
  1. unicex/__init__.py +36 -2
  2. unicex/_abc/exchange_info.py +1 -1
  3. unicex/_abc/uni_client.py +31 -1
  4. unicex/_abc/uni_websocket_manager.py +1 -1
  5. unicex/_base/client.py +7 -0
  6. unicex/_base/websocket.py +31 -9
  7. unicex/binance/adapter.py +7 -16
  8. unicex/binance/client.py +25 -25
  9. unicex/binance/uni_websocket_manager.py +6 -2
  10. unicex/bingx/__init__.py +27 -0
  11. unicex/bingx/adapter.py +284 -0
  12. unicex/bingx/client.py +521 -0
  13. unicex/bingx/exchange_info.py +22 -0
  14. unicex/bingx/uni_client.py +191 -0
  15. unicex/bingx/uni_websocket_manager.py +283 -0
  16. unicex/bingx/user_websocket.py +7 -0
  17. unicex/bingx/websocket_manager.py +118 -0
  18. unicex/bitget/adapter.py +2 -2
  19. unicex/bitget/client.py +64 -64
  20. unicex/bitget/uni_websocket_manager.py +27 -6
  21. unicex/bitget/websocket_manager.py +2 -4
  22. unicex/bybit/adapter.py +1 -0
  23. unicex/bybit/client.py +4 -4
  24. unicex/bybit/exchange_info.py +1 -1
  25. unicex/bybit/uni_websocket_manager.py +33 -4
  26. unicex/bybit/websocket_manager.py +8 -24
  27. unicex/enums.py +19 -3
  28. unicex/extra.py +37 -35
  29. unicex/gate/adapter.py +113 -0
  30. unicex/gate/client.py +9 -9
  31. unicex/gate/uni_client.py +1 -3
  32. unicex/gate/uni_websocket_manager.py +47 -9
  33. unicex/hyperliquid/adapter.py +1 -0
  34. unicex/hyperliquid/client.py +12 -12
  35. unicex/hyperliquid/uni_client.py +4 -7
  36. unicex/hyperliquid/uni_websocket_manager.py +6 -2
  37. unicex/kucoin/__init__.py +27 -0
  38. unicex/kucoin/adapter.py +181 -0
  39. unicex/kucoin/client.py +135 -0
  40. unicex/kucoin/exchange_info.py +50 -0
  41. unicex/kucoin/uni_client.py +208 -0
  42. unicex/kucoin/uni_websocket_manager.py +273 -0
  43. unicex/kucoin/user_websocket.py +7 -0
  44. unicex/kucoin/websocket_manager.py +11 -0
  45. unicex/mapper.py +62 -21
  46. unicex/mexc/adapter.py +104 -0
  47. unicex/mexc/client.py +7 -7
  48. unicex/mexc/uni_client.py +1 -3
  49. unicex/mexc/uni_websocket_manager.py +31 -9
  50. unicex/mexc/websocket_manager.py +27 -6
  51. unicex/okx/adapter.py +51 -0
  52. unicex/okx/client.py +15 -15
  53. unicex/okx/exchange_info.py +2 -2
  54. unicex/okx/uni_websocket_manager.py +50 -9
  55. unicex/okx/websocket_manager.py +119 -166
  56. unicex/types.py +14 -10
  57. unicex/utils.py +44 -1
  58. {unicex-0.13.17.dist-info → unicex-0.16.5.dist-info}/METADATA +7 -5
  59. unicex-0.16.5.dist-info/RECORD +109 -0
  60. unicex-0.13.17.dist-info/RECORD +0 -93
  61. {unicex-0.13.17.dist-info → unicex-0.16.5.dist-info}/WHEEL +0 -0
  62. {unicex-0.13.17.dist-info → unicex-0.16.5.dist-info}/licenses/LICENSE +0 -0
  63. {unicex-0.13.17.dist-info → unicex-0.16.5.dist-info}/top_level.txt +0 -0
@@ -15,13 +15,13 @@ type CallbackType = Callable[[Any], Awaitable[None]]
15
15
  class WebsocketManager:
16
16
  """Менеджер асинхронных вебсокетов для Okx."""
17
17
 
18
- _BASE_URL = "wss://ws.okx.com:8443/ws"
18
+ _BASE_URL: str = "wss://ws.okx.com:8443/ws"
19
19
  """Базовый URL вебсокетов на Okx."""
20
20
 
21
- _PUBLIC_URL = _BASE_URL + "/v5/public"
21
+ _PUBLIC_URL: str = _BASE_URL + "/v5/public"
22
22
  """Публичный URL вебсокетов на Okx."""
23
23
 
24
- _BUSINESS_URL = _BASE_URL + "/v5/business"
24
+ _BUSINESS_URL: str = _BASE_URL + "/v5/business"
25
25
  """Бизнес-URL вебсокетов на Okx. (для топиков trades-all и candle)"""
26
26
 
27
27
  def __init__(self, client: Client | None = None, **ws_kwargs: Any) -> None:
@@ -34,6 +34,15 @@ class WebsocketManager:
34
34
  self.client = client
35
35
  self._ws_kwargs = {"ping_message": "ping", **ws_kwargs}
36
36
 
37
+ def _build_subscription_message(self, args: list[dict[str, Any]]) -> str:
38
+ """Формирует JSON-сообщение подписки."""
39
+ return json.dumps(
40
+ {
41
+ "op": "subscribe",
42
+ "args": args,
43
+ }
44
+ )
45
+
37
46
  def instruments(
38
47
  self,
39
48
  callback: CallbackType,
@@ -50,16 +59,13 @@ class WebsocketManager:
50
59
  Возвращает:
51
60
  `Websocket`: Объект для управления вебсокет соединением.
52
61
  """
53
- subscription_message = json.dumps(
54
- {
55
- "op": "subscribe",
56
- "args": [
57
- {
58
- "channel": "instruments",
59
- "instType": inst_type,
60
- }
61
- ],
62
- }
62
+ subscription_message = self._build_subscription_message(
63
+ [
64
+ {
65
+ "channel": "instruments",
66
+ "instType": inst_type,
67
+ }
68
+ ]
63
69
  )
64
70
 
65
71
  return Websocket(
@@ -85,16 +91,13 @@ class WebsocketManager:
85
91
  Возвращает:
86
92
  `Websocket`: Объект для управления вебсокет соединением.
87
93
  """
88
- subscription_message = json.dumps(
89
- {
90
- "op": "subscribe",
91
- "args": [
92
- {
93
- "channel": "open-interest",
94
- "instId": inst_id,
95
- }
96
- ],
97
- }
94
+ subscription_message = self._build_subscription_message(
95
+ [
96
+ {
97
+ "channel": "open-interest",
98
+ "instId": inst_id,
99
+ }
100
+ ]
98
101
  )
99
102
 
100
103
  return Websocket(
@@ -120,16 +123,13 @@ class WebsocketManager:
120
123
  Возвращает:
121
124
  `Websocket`: Объект для управления вебсокет соединением.
122
125
  """
123
- subscription_message = json.dumps(
124
- {
125
- "op": "subscribe",
126
- "args": [
127
- {
128
- "channel": "funding-rate",
129
- "instId": inst_id,
130
- }
131
- ],
132
- }
126
+ subscription_message = self._build_subscription_message(
127
+ [
128
+ {
129
+ "channel": "funding-rate",
130
+ "instId": inst_id,
131
+ }
132
+ ]
133
133
  )
134
134
 
135
135
  return Websocket(
@@ -155,16 +155,13 @@ class WebsocketManager:
155
155
  Возвращает:
156
156
  `Websocket`: Объект для управления вебсокет соединением.
157
157
  """
158
- subscription_message = json.dumps(
159
- {
160
- "op": "subscribe",
161
- "args": [
162
- {
163
- "channel": "price-limit",
164
- "instId": inst_id,
165
- }
166
- ],
167
- }
158
+ subscription_message = self._build_subscription_message(
159
+ [
160
+ {
161
+ "channel": "price-limit",
162
+ "instId": inst_id,
163
+ }
164
+ ]
168
165
  )
169
166
 
170
167
  return Websocket(
@@ -190,16 +187,13 @@ class WebsocketManager:
190
187
  Возвращает:
191
188
  `Websocket`: Объект для управления вебсокет соединением.
192
189
  """
193
- subscription_message = json.dumps(
194
- {
195
- "op": "subscribe",
196
- "args": [
197
- {
198
- "channel": "opt-summary",
199
- "instFamily": inst_family,
200
- }
201
- ],
202
- }
190
+ subscription_message = self._build_subscription_message(
191
+ [
192
+ {
193
+ "channel": "opt-summary",
194
+ "instFamily": inst_family,
195
+ }
196
+ ]
203
197
  )
204
198
 
205
199
  return Websocket(
@@ -244,12 +238,7 @@ class WebsocketManager:
244
238
  if inst_id:
245
239
  args["instId"] = inst_id
246
240
 
247
- subscription_message = json.dumps(
248
- {
249
- "op": "subscribe",
250
- "args": [args],
251
- }
252
- )
241
+ subscription_message = self._build_subscription_message([args])
253
242
 
254
243
  return Websocket(
255
244
  callback=callback,
@@ -274,16 +263,13 @@ class WebsocketManager:
274
263
  Возвращает:
275
264
  `Websocket`: Объект для управления вебсокет соединением.
276
265
  """
277
- subscription_message = json.dumps(
278
- {
279
- "op": "subscribe",
280
- "args": [
281
- {
282
- "channel": "mark-price",
283
- "instId": inst_id,
284
- }
285
- ],
286
- }
266
+ subscription_message = self._build_subscription_message(
267
+ [
268
+ {
269
+ "channel": "mark-price",
270
+ "instId": inst_id,
271
+ }
272
+ ]
287
273
  )
288
274
 
289
275
  return Websocket(
@@ -309,16 +295,13 @@ class WebsocketManager:
309
295
  Возвращает:
310
296
  `Websocket`: Объект для управления вебсокет соединением.
311
297
  """
312
- subscription_message = json.dumps(
313
- {
314
- "op": "subscribe",
315
- "args": [
316
- {
317
- "channel": "index-tickers",
318
- "instId": inst_id,
319
- }
320
- ],
321
- }
298
+ subscription_message = self._build_subscription_message(
299
+ [
300
+ {
301
+ "channel": "index-tickers",
302
+ "instId": inst_id,
303
+ }
304
+ ]
322
305
  )
323
306
 
324
307
  return Websocket(
@@ -375,16 +358,13 @@ class WebsocketManager:
375
358
  `Websocket`: Объект для управления вебсокет соединением.
376
359
  """
377
360
  channel = f"mark-price-candle{interval}"
378
- subscription_message = json.dumps(
379
- {
380
- "op": "subscribe",
381
- "args": [
382
- {
383
- "channel": channel,
384
- "instId": inst_id,
385
- }
386
- ],
387
- }
361
+ subscription_message = self._build_subscription_message(
362
+ [
363
+ {
364
+ "channel": channel,
365
+ "instId": inst_id,
366
+ }
367
+ ]
388
368
  )
389
369
 
390
370
  return Websocket(
@@ -440,16 +420,13 @@ class WebsocketManager:
440
420
  `Websocket`: Объект для управления вебсокет соединением.
441
421
  """
442
422
  channel = f"index-candle{interval}"
443
- subscription_message = json.dumps(
444
- {
445
- "op": "subscribe",
446
- "args": [
447
- {
448
- "channel": channel,
449
- "instId": inst_id,
450
- }
451
- ],
452
- }
423
+ subscription_message = self._build_subscription_message(
424
+ [
425
+ {
426
+ "channel": channel,
427
+ "instId": inst_id,
428
+ }
429
+ ]
453
430
  )
454
431
 
455
432
  return Websocket(
@@ -475,16 +452,13 @@ class WebsocketManager:
475
452
  Возвращает:
476
453
  `Websocket`: Объект для управления вебсокет соединением.
477
454
  """
478
- subscription_message = json.dumps(
479
- {
480
- "op": "subscribe",
481
- "args": [
482
- {
483
- "channel": "liquidation-orders",
484
- "instType": inst_type,
485
- }
486
- ],
487
- }
455
+ subscription_message = self._build_subscription_message(
456
+ [
457
+ {
458
+ "channel": "liquidation-orders",
459
+ "instType": inst_type,
460
+ }
461
+ ]
488
462
  )
489
463
 
490
464
  return Websocket(
@@ -520,12 +494,7 @@ class WebsocketManager:
520
494
  if inst_family:
521
495
  args["instFamily"] = inst_family
522
496
 
523
- subscription_message = json.dumps(
524
- {
525
- "op": "subscribe",
526
- "args": [args],
527
- }
528
- )
497
+ subscription_message = self._build_subscription_message([args])
529
498
 
530
499
  return Websocket(
531
500
  callback=callback,
@@ -550,16 +519,13 @@ class WebsocketManager:
550
519
  Возвращает:
551
520
  `Websocket`: Объект для управления вебсокет соединением.
552
521
  """
553
- subscription_message = json.dumps(
554
- {
555
- "op": "subscribe",
556
- "args": [
557
- {
558
- "channel": "tickers",
559
- "instId": inst_id,
560
- }
561
- ],
562
- }
522
+ subscription_message = self._build_subscription_message(
523
+ [
524
+ {
525
+ "channel": "tickers",
526
+ "instId": inst_id,
527
+ }
528
+ ]
563
529
  )
564
530
 
565
531
  return Websocket(
@@ -615,17 +581,13 @@ class WebsocketManager:
615
581
  Возвращает:
616
582
  `Websocket`: Объект для управления вебсокет соединением.
617
583
  """
618
- channel = f"candle{interval}"
619
- subscription_message = json.dumps(
620
- {
621
- "op": "subscribe",
622
- "args": [
623
- {
624
- "channel": channel,
625
- "instId": inst_id,
626
- }
627
- ],
628
- }
584
+ subscription_message = self._build_subscription_message(
585
+ [
586
+ {
587
+ "channel": f"candle{interval}",
588
+ "instId": inst_id,
589
+ }
590
+ ]
629
591
  )
630
592
 
631
593
  return Websocket(
@@ -651,16 +613,13 @@ class WebsocketManager:
651
613
  Возвращает:
652
614
  `Websocket`: Объект для управления вебсокет соединением.
653
615
  """
654
- subscription_message = json.dumps(
655
- {
656
- "op": "subscribe",
657
- "args": [
658
- {
659
- "channel": "trades",
660
- "instId": inst_id,
661
- }
662
- ],
663
- }
616
+ subscription_message = self._build_subscription_message(
617
+ [
618
+ {
619
+ "channel": "trades",
620
+ "instId": inst_id,
621
+ }
622
+ ]
664
623
  )
665
624
 
666
625
  return Websocket(
@@ -686,16 +645,13 @@ class WebsocketManager:
686
645
  Возвращает:
687
646
  `Websocket`: Объект для управления вебсокет соединением.
688
647
  """
689
- subscription_message = json.dumps(
690
- {
691
- "op": "subscribe",
692
- "args": [
693
- {
694
- "channel": "trades-all",
695
- "instId": inst_id,
696
- }
697
- ],
698
- }
648
+ subscription_message = self._build_subscription_message(
649
+ [
650
+ {
651
+ "channel": "trades-all",
652
+ "instId": inst_id,
653
+ }
654
+ ]
699
655
  )
700
656
 
701
657
  return Websocket(
@@ -723,16 +679,13 @@ class WebsocketManager:
723
679
  Возвращает:
724
680
  `Websocket`: Объект для управления вебсокет соединением.
725
681
  """
726
- subscription_message = json.dumps(
727
- {
728
- "op": "subscribe",
729
- "args": [
730
- {
731
- "channel": channel,
732
- "instId": inst_id,
733
- }
734
- ],
735
- }
682
+ subscription_message = self._build_subscription_message(
683
+ [
684
+ {
685
+ "channel": channel,
686
+ "instId": inst_id,
687
+ }
688
+ ]
736
689
  )
737
690
 
738
691
  return Websocket(
unicex/types.py CHANGED
@@ -5,9 +5,9 @@ __all__ = [
5
5
  "TickerDailyItem",
6
6
  "KlineDict",
7
7
  "TradeDict",
8
- "AggTradeDict",
9
8
  "RequestMethod",
10
9
  "LoggerLike",
10
+ "NumberLike",
11
11
  "OpenInterestDict",
12
12
  "OpenInterestItem",
13
13
  "TickerInfoItem",
@@ -26,6 +26,13 @@ type LoggerLike = LoggingLogger | loguru.Logger
26
26
  type RequestMethod = Literal["GET", "POST", "PUT", "DELETE", "PATCH"]
27
27
  """Типы методов HTTP запросов."""
28
28
 
29
+ type NumberLike = str | int | float
30
+ """
31
+ Числовое значение для аргументов API-клиентов.
32
+ API бирж принимают числа как str, int или float — значение
33
+ передаётся без преобразований и сериализуется HTTP-клиентом.
34
+ """
35
+
29
36
 
30
37
  class TickerDailyItem(TypedDict):
31
38
  """Статистика одного тикера за последние 24 часа."""
@@ -71,10 +78,10 @@ class KlineDict(TypedDict):
71
78
  q: float
72
79
  """Объем свечи. В долларах."""
73
80
 
74
- T: int | None
81
+ T: int | None # `None` means untrackable
75
82
  """Время закрытия. В миллисекундах."""
76
83
 
77
- x: bool | None
84
+ x: bool | None # `None` means untrackable
78
85
  """Флаг закрыта ли свеча."""
79
86
 
80
87
 
@@ -97,12 +104,6 @@ class TradeDict(TypedDict):
97
104
  """Объем сделки. В монетах."""
98
105
 
99
106
 
100
- class AggTradeDict(TradeDict):
101
- """Модель агрегированной сделки."""
102
-
103
- pass
104
-
105
-
106
107
  class OpenInterestItem(TypedDict):
107
108
  """Модель одного элемента открытого интереса."""
108
109
 
@@ -110,7 +111,10 @@ class OpenInterestItem(TypedDict):
110
111
  """Время. В миллисекундах."""
111
112
 
112
113
  v: float
113
- """Открытый интерес. В монетах."""
114
+ """Открытый интерес."""
115
+
116
+ u: Literal["coins", "usd"]
117
+ """Единица измерения открытого интереса."""
114
118
 
115
119
 
116
120
  type OpenInterestDict = dict[str, OpenInterestItem]
unicex/utils.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Модуль, который предоставляет дополнительные функции, которые нужны для внутренного использования в библиотеке."""
2
2
 
3
3
  __all__ = [
4
+ "get_timestamp",
4
5
  "dict_to_query_string",
5
6
  "generate_hmac_sha256_signature",
6
7
  "sort_params_by_alphabetical_order",
@@ -9,13 +10,15 @@ __all__ = [
9
10
  "catch_adapter_errors",
10
11
  "decorate_all_methods",
11
12
  "symbol_to_exchange_format",
13
+ "validate_single_symbol_args",
12
14
  ]
13
15
 
14
16
  import base64
15
17
  import hashlib
16
18
  import hmac
17
19
  import json
18
- from collections.abc import Callable, Iterable
20
+ import time
21
+ from collections.abc import Callable, Iterable, Sequence
19
22
  from functools import wraps
20
23
  from typing import Any, Literal
21
24
  from urllib.parse import urlencode
@@ -24,6 +27,19 @@ from unicex.enums import Exchange, MarketType
24
27
  from unicex.exceptions import AdapterError
25
28
 
26
29
 
30
+ def get_timestamp(milliseconds: bool = True) -> int:
31
+ """Возвращает текущее время в миллисекундах или секундах.
32
+
33
+ Параметры:
34
+ milliseconds (`bool`, опционально): Если True, возвращает время в миллисекундах.
35
+ Если False, возвращает время в секундах.
36
+
37
+ Возвращает:
38
+ `int`: Текущее время в миллисекундах или секундах.
39
+ """
40
+ return int(time.time() * 1000) if milliseconds else int(time.time())
41
+
42
+
27
43
  def filter_params(params: dict) -> dict:
28
44
  """Фильтрует параметры запроса, удаляя None-значения.
29
45
 
@@ -215,4 +231,31 @@ def symbol_to_exchange_format(
215
231
  return symbol.removesuffix("USDT") # Вот тут мб и не так, там вроде что-то к USDC
216
232
  else:
217
233
  return symbol.removesuffix("USDT") # Вот тут мб и не так, там вроде что-то к USDC
234
+ elif exchange == Exchange.KUCOIN:
235
+ if market_type == MarketType.FUTURES:
236
+ if symbol_upper == "BTCUSDT":
237
+ return "XBTUSDTM"
238
+ return symbol_upper + "M"
239
+ else:
240
+ return symbol_upper.replace("USDT", "-USDT")
241
+ elif exchange == Exchange.BINGX:
242
+ return symbol_upper.replace("USDT", "-USDT")
218
243
  return symbol_upper
244
+
245
+
246
+ def validate_single_symbol_args(
247
+ symbol: str | None = None, symbols: Sequence[str] | None = None
248
+ ) -> None:
249
+ """Проверяет, что передан ровно один из аргументов symbol/symbols.
250
+
251
+ Параметры:
252
+ symbol (`str | None`, опционально): Одиночный торговый символ.
253
+ symbols (`Sequence[str] | None`, опционально): Список торговых символов.
254
+
255
+ Возвращает:
256
+ `None`: Ничего не возвращает, выбрасывает ValueError при нарушении условий.
257
+ """
258
+ if symbol and symbols:
259
+ raise ValueError("Parameters symbol and symbols cannot be used together")
260
+ if not (symbol or symbols):
261
+ raise ValueError("Either symbol or symbols must be provided")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unicex
3
- Version: 0.13.17
3
+ Version: 0.16.5
4
4
  Summary: Unified Crypto Exchange API
5
5
  Author-email: LoveBloodAndDiamonds <ayazshakirzyanov27@gmail.com>
6
6
  License: BSD 3-Clause License
@@ -56,12 +56,14 @@ Dynamic: license-file
56
56
  | Exchange | Client | Auth | WS Manager | User WS | Uni Client | Uni WS Manager | ExchangeInfo |
57
57
  |-----------------|--------|------|------------|---------|------------|----------------|--------------|
58
58
  | **Binance** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
59
- | **Bitget** | ✓ | ✓ | ✓ | | ✓ | | ✓ |
59
+ | **Bitget** | ✓ | ✓ | ✓ | | ✓ || ✓ |
60
60
  | **Bybit** | ✓ | ✓ | ✓ | | ✓ | ✓ | ✓ |
61
- | **Gateio** | ✓ | ✓ | ✓ | | ✓ | | ✓ |
61
+ | **Gateio** | ✓ | ✓ | ✓ | | ✓ || ✓ |
62
62
  | **Hyperliquid** | ✓ | ✓ | ✓ | ✓ | ✓ | | |
63
- | **Mexc** | ✓ | ✓ | ✓ | | ✓ | | ✓ |
64
- | **Okx** | ✓ | ✓ | ✓ | | ✓ | | ✓ |
63
+ | **Mexc** | ✓ | ✓ | ✓ | | ✓ || ✓ |
64
+ | **Okx** | ✓ | ✓ | ✓ | | ✓ || ✓ |
65
+ | **Kucoin** | | | | | ✓ | | |
66
+ | **BingX** | | | | | ✓ | | |
65
67
  ---
66
68
 
67
69