unicex 0.11.0__tar.gz → 0.13.0__tar.gz

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 (104) hide show
  1. {unicex-0.11.0/unicex.egg-info → unicex-0.13.0}/PKG-INFO +57 -7
  2. {unicex-0.11.0 → unicex-0.13.0}/README.md +56 -6
  3. {unicex-0.11.0 → unicex-0.13.0}/pyproject.toml +1 -1
  4. {unicex-0.11.0 → unicex-0.13.0}/unicex/_abc/exchange_info.py +75 -35
  5. unicex-0.13.0/unicex/binance/exchange_info.py +62 -0
  6. unicex-0.13.0/unicex/bitget/exchange_info.py +48 -0
  7. unicex-0.13.0/unicex/bybit/exchange_info.py +51 -0
  8. {unicex-0.11.0 → unicex-0.13.0}/unicex/gateio/client.py +2 -2
  9. unicex-0.13.0/unicex/gateio/exchange_info.py +55 -0
  10. {unicex-0.11.0 → unicex-0.13.0}/unicex/hyperliquid/exchange_info.py +29 -10
  11. unicex-0.13.0/unicex/mexc/exchange_info.py +47 -0
  12. unicex-0.13.0/unicex/okx/exchange_info.py +47 -0
  13. {unicex-0.11.0 → unicex-0.13.0}/unicex/types.py +16 -15
  14. {unicex-0.11.0 → unicex-0.13.0/unicex.egg-info}/PKG-INFO +57 -7
  15. unicex-0.11.0/unicex/binance/exchange_info.py +0 -12
  16. unicex-0.11.0/unicex/bitget/exchange_info.py +0 -12
  17. unicex-0.11.0/unicex/bybit/exchange_info.py +0 -12
  18. unicex-0.11.0/unicex/gateio/exchange_info.py +0 -12
  19. unicex-0.11.0/unicex/mexc/exchange_info.py +0 -32
  20. unicex-0.11.0/unicex/okx/exchange_info.py +0 -50
  21. {unicex-0.11.0 → unicex-0.13.0}/LICENSE +0 -0
  22. {unicex-0.11.0 → unicex-0.13.0}/setup.cfg +0 -0
  23. {unicex-0.11.0 → unicex-0.13.0}/unicex/__init__.py +0 -0
  24. {unicex-0.11.0 → unicex-0.13.0}/unicex/_abc/__init__.py +0 -0
  25. {unicex-0.11.0 → unicex-0.13.0}/unicex/_abc/uni_client.py +0 -0
  26. {unicex-0.11.0 → unicex-0.13.0}/unicex/_abc/uni_websocket_manager.py +0 -0
  27. {unicex-0.11.0 → unicex-0.13.0}/unicex/_base/__init__.py +0 -0
  28. {unicex-0.11.0 → unicex-0.13.0}/unicex/_base/client.py +0 -0
  29. {unicex-0.11.0 → unicex-0.13.0}/unicex/_base/websocket.py +0 -0
  30. {unicex-0.11.0 → unicex-0.13.0}/unicex/binance/__init__.py +0 -0
  31. {unicex-0.11.0 → unicex-0.13.0}/unicex/binance/adapter.py +0 -0
  32. {unicex-0.11.0 → unicex-0.13.0}/unicex/binance/client.py +0 -0
  33. {unicex-0.11.0 → unicex-0.13.0}/unicex/binance/uni_client.py +0 -0
  34. {unicex-0.11.0 → unicex-0.13.0}/unicex/binance/uni_websocket_manager.py +0 -0
  35. {unicex-0.11.0 → unicex-0.13.0}/unicex/binance/user_websocket.py +0 -0
  36. {unicex-0.11.0 → unicex-0.13.0}/unicex/binance/websocket_manager.py +0 -0
  37. {unicex-0.11.0 → unicex-0.13.0}/unicex/bitget/__init__.py +0 -0
  38. {unicex-0.11.0 → unicex-0.13.0}/unicex/bitget/adapter.py +0 -0
  39. {unicex-0.11.0 → unicex-0.13.0}/unicex/bitget/client.py +0 -0
  40. {unicex-0.11.0 → unicex-0.13.0}/unicex/bitget/uni_client.py +0 -0
  41. {unicex-0.11.0 → unicex-0.13.0}/unicex/bitget/uni_websocket_manager.py +0 -0
  42. {unicex-0.11.0 → unicex-0.13.0}/unicex/bitget/user_websocket.py +0 -0
  43. {unicex-0.11.0 → unicex-0.13.0}/unicex/bitget/websocket_manager.py +0 -0
  44. {unicex-0.11.0 → unicex-0.13.0}/unicex/bybit/__init__.py +0 -0
  45. {unicex-0.11.0 → unicex-0.13.0}/unicex/bybit/adapter.py +0 -0
  46. {unicex-0.11.0 → unicex-0.13.0}/unicex/bybit/client.py +0 -0
  47. {unicex-0.11.0 → unicex-0.13.0}/unicex/bybit/uni_client.py +0 -0
  48. {unicex-0.11.0 → unicex-0.13.0}/unicex/bybit/uni_websocket_manager.py +0 -0
  49. {unicex-0.11.0 → unicex-0.13.0}/unicex/bybit/user_websocket.py +0 -0
  50. {unicex-0.11.0 → unicex-0.13.0}/unicex/bybit/websocket_manager.py +0 -0
  51. {unicex-0.11.0 → unicex-0.13.0}/unicex/enums.py +0 -0
  52. {unicex-0.11.0 → unicex-0.13.0}/unicex/exceptions.py +0 -0
  53. {unicex-0.11.0 → unicex-0.13.0}/unicex/extra.py +0 -0
  54. {unicex-0.11.0 → unicex-0.13.0}/unicex/gateio/__init__.py +0 -0
  55. {unicex-0.11.0 → unicex-0.13.0}/unicex/gateio/adapter.py +0 -0
  56. {unicex-0.11.0 → unicex-0.13.0}/unicex/gateio/uni_client.py +0 -0
  57. {unicex-0.11.0 → unicex-0.13.0}/unicex/gateio/uni_websocket_manager.py +0 -0
  58. {unicex-0.11.0 → unicex-0.13.0}/unicex/gateio/user_websocket.py +0 -0
  59. {unicex-0.11.0 → unicex-0.13.0}/unicex/gateio/websocket_manager.py +0 -0
  60. {unicex-0.11.0 → unicex-0.13.0}/unicex/hyperliquid/__init__.py +0 -0
  61. {unicex-0.11.0 → unicex-0.13.0}/unicex/hyperliquid/adapter.py +0 -0
  62. {unicex-0.11.0 → unicex-0.13.0}/unicex/hyperliquid/client.py +0 -0
  63. {unicex-0.11.0 → unicex-0.13.0}/unicex/hyperliquid/uni_client.py +0 -0
  64. {unicex-0.11.0 → unicex-0.13.0}/unicex/hyperliquid/uni_websocket_manager.py +0 -0
  65. {unicex-0.11.0 → unicex-0.13.0}/unicex/hyperliquid/user_websocket.py +0 -0
  66. {unicex-0.11.0 → unicex-0.13.0}/unicex/hyperliquid/websocket_manager.py +0 -0
  67. {unicex-0.11.0 → unicex-0.13.0}/unicex/mapper.py +0 -0
  68. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/__init__.py +0 -0
  69. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PrivateAccountV3Api_pb2.py +0 -0
  70. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PrivateDealsV3Api_pb2.py +0 -0
  71. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PrivateOrdersV3Api_pb2.py +0 -0
  72. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicAggreBookTickerV3Api_pb2.py +0 -0
  73. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicAggreDealsV3Api_pb2.py +0 -0
  74. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicAggreDepthsV3Api_pb2.py +0 -0
  75. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicBookTickerBatchV3Api_pb2.py +0 -0
  76. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicBookTickerV3Api_pb2.py +0 -0
  77. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicDealsV3Api_pb2.py +0 -0
  78. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicFuture_pb2.py +0 -0
  79. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicIncreaseDepthsBatchV3Api_pb2.py +0 -0
  80. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicIncreaseDepthsV3Api_pb2.py +0 -0
  81. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicLimitDepthsV3Api_pb2.py +0 -0
  82. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicMiniTickerV3Api_pb2.py +0 -0
  83. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicMiniTickersV3Api_pb2.py +0 -0
  84. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PublicSpotKlineV3Api_pb2.py +0 -0
  85. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/PushDataV3ApiWrapper_pb2.py +0 -0
  86. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/_spot_ws_proto/__init__.py +0 -0
  87. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/adapter.py +0 -0
  88. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/client.py +0 -0
  89. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/uni_client.py +0 -0
  90. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/uni_websocket_manager.py +0 -0
  91. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/user_websocket.py +0 -0
  92. {unicex-0.11.0 → unicex-0.13.0}/unicex/mexc/websocket_manager.py +0 -0
  93. {unicex-0.11.0 → unicex-0.13.0}/unicex/okx/__init__.py +0 -0
  94. {unicex-0.11.0 → unicex-0.13.0}/unicex/okx/adapter.py +0 -0
  95. {unicex-0.11.0 → unicex-0.13.0}/unicex/okx/client.py +0 -0
  96. {unicex-0.11.0 → unicex-0.13.0}/unicex/okx/uni_client.py +0 -0
  97. {unicex-0.11.0 → unicex-0.13.0}/unicex/okx/uni_websocket_manager.py +0 -0
  98. {unicex-0.11.0 → unicex-0.13.0}/unicex/okx/user_websocket.py +0 -0
  99. {unicex-0.11.0 → unicex-0.13.0}/unicex/okx/websocket_manager.py +0 -0
  100. {unicex-0.11.0 → unicex-0.13.0}/unicex/utils.py +0 -0
  101. {unicex-0.11.0 → unicex-0.13.0}/unicex.egg-info/SOURCES.txt +0 -0
  102. {unicex-0.11.0 → unicex-0.13.0}/unicex.egg-info/dependency_links.txt +0 -0
  103. {unicex-0.11.0 → unicex-0.13.0}/unicex.egg-info/requires.txt +0 -0
  104. {unicex-0.11.0 → unicex-0.13.0}/unicex.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unicex
3
- Version: 0.11.0
3
+ Version: 0.13.0
4
4
  Summary: Unified Crypto Exchange API
5
5
  Author-email: LoveBloodAndDiamonds <ayazshakirzyanov27@gmail.com>
6
6
  License: BSD 3-Clause License
@@ -49,18 +49,18 @@ Dynamic: license-file
49
49
 
50
50
  # Unified Crypto Exchange API
51
51
 
52
- `unicex` — асинхронная библиотека для работы с криптовалютными биржами, реализующая унифицированный интерфейс поверх «сырых» REST и WebSocket API разных бирж.
52
+ `unicex` — асинхронная библиотека для работы с криптовалютными биржами, реализующая унифицированный интерфейс поверх «сырых» REST и WebSocket API разных бирж. Поддерживает спотовый и USDT-фьючерсный рынки.
53
53
 
54
54
  ## ✅ Статус реализации
55
55
 
56
56
  | Exchange | Client | Auth | WS Manager | User WS | Uni Client | Uni WS Manager | ExchangeInfo |
57
57
  |-----------------|--------|------|------------|---------|------------|----------------|--------------|
58
- | **Binance** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
59
- | **Bitget** | ✓ | ✓ | ✓ | | ✓ | | |
60
- | **Bybit** | ✓ | ✓ | ✓ | | ✓ | ✓ | |
61
- | **Gateio** | ✓ | ✓ | ✓ | | ✓ | | |
58
+ | **Binance** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ ||
59
+ | **Bitget** | ✓ | ✓ | ✓ | | ✓ | ||
60
+ | **Bybit** | ✓ | ✓ | ✓ | | ✓ | ✓ ||
61
+ | **Gateio** | ✓ | ✓ | ✓ | | ✓ | ||
62
62
  | **Hyperliquid** | ✓ | ✓ | ✓ | ✓ | ✓ | | |
63
- | **Mexc** | ✓ | ✓ | ✓ | | ✓ | | |
63
+ | **Mexc** | ✓ | ✓ | ✓ | | ✓ | ||
64
64
  | **Okx** | ✓ | ✓ | ✓ | | ✓ | | ✓ |
65
65
  ---
66
66
 
@@ -191,3 +191,53 @@ async def callback(trade: TradeDict) -> None:
191
191
  if __name__ == "__main__":
192
192
  asyncio.run(main())
193
193
  ```
194
+
195
+
196
+ ### Пример: Округление цен используя фоновый класс ExchangeInfo
197
+
198
+
199
+ ```python
200
+ import asyncio
201
+ from unicex import start_exchanges_info, get_exchange_info, Exchange
202
+
203
+
204
+ async def main() -> None:
205
+ # ⏳ Запускаем фоновые процессы, которые собирают рыночные параметры всех бирж:
206
+ # - количество знаков после точки для цены и объема
207
+ # - множители контрактов для фьючерсов
208
+ await start_exchanges_info()
209
+
210
+ # Небольшая пауза, чтобы данные успели подгрузиться
211
+ await asyncio.sleep(1)
212
+
213
+ # 1️⃣ Пример 1: Округление цены для фьючерсов OKX
214
+ okx_exchange_info = get_exchange_info(Exchange.OKX)
215
+ okx_rounded_price = okx_exchange_info.round_futures_price("BTC-USDT-SWAP", 123456.1234567890)
216
+ print(okx_rounded_price) # >> 123456.1
217
+
218
+ # 2️⃣ Пример 2: Округление объема для спота Binance
219
+ binance_exchange_info = get_exchange_info(Exchange.BINANCE)
220
+ binance_rounded_quantity = binance_exchange_info.round_quantity("BTCUSDT", 1.123456789)
221
+ print(binance_rounded_quantity) # >> 1.12345
222
+
223
+ # 3️⃣ Пример 3: Получение множителя контракта (например, Mexc Futures)
224
+ mexc_exchange_info = get_exchange_info(Exchange.MEXC)
225
+ mexc_contract_multiplier = mexc_exchange_info.get_futures_ticker_info("BTC_USDT")["contract_size"]
226
+ print(mexc_contract_multiplier) # >> 0.0001
227
+
228
+ # 4️⃣ Пример 4: Реальное применение — вычисляем тейк-профит вручную
229
+ # Допустим, позиция открыта по 123123.1 USDT, хотим +3.5% тейк-профит:
230
+ take_profit_raw = 123123.1 * 1.035
231
+ print("До округления:", take_profit_raw) # >> 127432.40849999999
232
+
233
+ # Биржа требует цену в допустимом формате — округляем:
234
+ take_profit = okx_exchange_info.round_futures_price("BTC-USDT-SWAP", take_profit_raw)
235
+ print("После округления:", take_profit) # >> 127432.4
236
+
237
+ # Теперь это число можно безопасно передать в API без ошибок:
238
+ # await client.create_order(symbol="BTC-USDT-SWAP", price=take_profit, ...)
239
+
240
+
241
+ if __name__ == "__main__":
242
+ asyncio.run(main())
243
+ ```
@@ -1,17 +1,17 @@
1
1
  # Unified Crypto Exchange API
2
2
 
3
- `unicex` — асинхронная библиотека для работы с криптовалютными биржами, реализующая унифицированный интерфейс поверх «сырых» REST и WebSocket API разных бирж.
3
+ `unicex` — асинхронная библиотека для работы с криптовалютными биржами, реализующая унифицированный интерфейс поверх «сырых» REST и WebSocket API разных бирж. Поддерживает спотовый и USDT-фьючерсный рынки.
4
4
 
5
5
  ## ✅ Статус реализации
6
6
 
7
7
  | Exchange | Client | Auth | WS Manager | User WS | Uni Client | Uni WS Manager | ExchangeInfo |
8
8
  |-----------------|--------|------|------------|---------|------------|----------------|--------------|
9
- | **Binance** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
10
- | **Bitget** | ✓ | ✓ | ✓ | | ✓ | | |
11
- | **Bybit** | ✓ | ✓ | ✓ | | ✓ | ✓ | |
12
- | **Gateio** | ✓ | ✓ | ✓ | | ✓ | | |
9
+ | **Binance** | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ ||
10
+ | **Bitget** | ✓ | ✓ | ✓ | | ✓ | ||
11
+ | **Bybit** | ✓ | ✓ | ✓ | | ✓ | ✓ ||
12
+ | **Gateio** | ✓ | ✓ | ✓ | | ✓ | ||
13
13
  | **Hyperliquid** | ✓ | ✓ | ✓ | ✓ | ✓ | | |
14
- | **Mexc** | ✓ | ✓ | ✓ | | ✓ | | |
14
+ | **Mexc** | ✓ | ✓ | ✓ | | ✓ | ||
15
15
  | **Okx** | ✓ | ✓ | ✓ | | ✓ | | ✓ |
16
16
  ---
17
17
 
@@ -142,3 +142,53 @@ async def callback(trade: TradeDict) -> None:
142
142
  if __name__ == "__main__":
143
143
  asyncio.run(main())
144
144
  ```
145
+
146
+
147
+ ### Пример: Округление цен используя фоновый класс ExchangeInfo
148
+
149
+
150
+ ```python
151
+ import asyncio
152
+ from unicex import start_exchanges_info, get_exchange_info, Exchange
153
+
154
+
155
+ async def main() -> None:
156
+ # ⏳ Запускаем фоновые процессы, которые собирают рыночные параметры всех бирж:
157
+ # - количество знаков после точки для цены и объема
158
+ # - множители контрактов для фьючерсов
159
+ await start_exchanges_info()
160
+
161
+ # Небольшая пауза, чтобы данные успели подгрузиться
162
+ await asyncio.sleep(1)
163
+
164
+ # 1️⃣ Пример 1: Округление цены для фьючерсов OKX
165
+ okx_exchange_info = get_exchange_info(Exchange.OKX)
166
+ okx_rounded_price = okx_exchange_info.round_futures_price("BTC-USDT-SWAP", 123456.1234567890)
167
+ print(okx_rounded_price) # >> 123456.1
168
+
169
+ # 2️⃣ Пример 2: Округление объема для спота Binance
170
+ binance_exchange_info = get_exchange_info(Exchange.BINANCE)
171
+ binance_rounded_quantity = binance_exchange_info.round_quantity("BTCUSDT", 1.123456789)
172
+ print(binance_rounded_quantity) # >> 1.12345
173
+
174
+ # 3️⃣ Пример 3: Получение множителя контракта (например, Mexc Futures)
175
+ mexc_exchange_info = get_exchange_info(Exchange.MEXC)
176
+ mexc_contract_multiplier = mexc_exchange_info.get_futures_ticker_info("BTC_USDT")["contract_size"]
177
+ print(mexc_contract_multiplier) # >> 0.0001
178
+
179
+ # 4️⃣ Пример 4: Реальное применение — вычисляем тейк-профит вручную
180
+ # Допустим, позиция открыта по 123123.1 USDT, хотим +3.5% тейк-профит:
181
+ take_profit_raw = 123123.1 * 1.035
182
+ print("До округления:", take_profit_raw) # >> 127432.40849999999
183
+
184
+ # Биржа требует цену в допустимом формате — округляем:
185
+ take_profit = okx_exchange_info.round_futures_price("BTC-USDT-SWAP", take_profit_raw)
186
+ print("После округления:", take_profit) # >> 127432.4
187
+
188
+ # Теперь это число можно безопасно передать в API без ошибок:
189
+ # await client.create_order(symbol="BTC-USDT-SWAP", price=take_profit, ...)
190
+
191
+
192
+ if __name__ == "__main__":
193
+ asyncio.run(main())
194
+ ```
@@ -4,7 +4,7 @@ name = "unicex"
4
4
  # • PATCH (x.y.Z) → увеличивается при багфиксе, который не ломает совместимость.
5
5
  # • MINOR (x.Y.z) → увеличивается при добавлении новой функциональности, но без ломающих изменений (backward-compatible).
6
6
  # • MAJOR (X.y.z) → увеличивается при изменениях, которые ломают обратную совместимость.
7
- version = "0.11.0"
7
+ version = "0.13.0"
8
8
 
9
9
  description = "Unified Crypto Exchange API "
10
10
  readme = "README.md"
@@ -1,10 +1,12 @@
1
1
  __all__ = ["IExchangeInfo"]
2
2
 
3
3
  import asyncio
4
+ import math
4
5
  from abc import ABC, abstractmethod
5
6
  from decimal import Decimal
6
- from typing import TYPE_CHECKING
7
+ from typing import TYPE_CHECKING, ClassVar
7
8
 
9
+ import aiohttp
8
10
  from loguru import logger
9
11
 
10
12
  from unicex.enums import MarketType
@@ -32,6 +34,9 @@ class IExchangeInfo(ABC):
32
34
  _logger: "loguru.Logger"
33
35
  """Логгер для записи сообщений о работе с биржей."""
34
36
 
37
+ exchange_name: ClassVar[str] = "not_defined_exchange"
38
+ """Название биржи, на которой работает класс."""
39
+
35
40
  def __init_subclass__(cls, **kwargs):
36
41
  """Инициализация подкласса. Функция нужна, чтобы у каждого наследника была своя копия атрибутов."""
37
42
  super().__init_subclass__(**kwargs)
@@ -63,7 +68,7 @@ class IExchangeInfo(ABC):
63
68
  try:
64
69
  await cls.load_exchange_info()
65
70
  except Exception as e:
66
- cls._logger.error(f"Error loading exchange data: {e}")
71
+ cls._logger.error(f"Error loading exchange data for {cls.exchange_name}: {e}")
67
72
  for _ in range(update_interval_seconds):
68
73
  if not cls._running:
69
74
  break
@@ -72,9 +77,33 @@ class IExchangeInfo(ABC):
72
77
  @classmethod
73
78
  async def load_exchange_info(cls) -> None:
74
79
  """Принудительно вызывает загрузку информации о бирже."""
75
- await cls._load_exchange_info()
80
+ async with aiohttp.ClientSession() as session:
81
+ try:
82
+ await cls._load_spot_exchange_info(session)
83
+ cls._logger.debug(f"Loaded spot exchange data for {cls.exchange_name} ")
84
+ except Exception as e:
85
+ cls._logger.error(f"Error loading spot exchange data for {cls.exchange_name}: {e}")
86
+ try:
87
+ await cls._load_futures_exchange_info(session)
88
+ cls._logger.debug(f"Loaded futures exchange data for {cls.exchange_name} ")
89
+ except Exception as e:
90
+ cls._logger.error(
91
+ f"Error loading futures exchange data for {cls.exchange_name}: {e}"
92
+ )
76
93
  cls._loaded = True
77
94
 
95
+ @classmethod
96
+ @abstractmethod
97
+ async def _load_spot_exchange_info(cls, session: aiohttp.ClientSession) -> None:
98
+ """Загружает информацию о бирже для спотового рынка."""
99
+ ...
100
+
101
+ @classmethod
102
+ @abstractmethod
103
+ async def _load_futures_exchange_info(cls, session: aiohttp.ClientSession) -> None:
104
+ """Загружает информацию о бирже для фьючерсного рынка."""
105
+ ...
106
+
78
107
  @classmethod
79
108
  def get_ticker_info(
80
109
  cls, symbol: str, market_type: MarketType = MarketType.SPOT
@@ -92,39 +121,41 @@ class IExchangeInfo(ABC):
92
121
  """Возвращает информацию о тикере фьючерсов по его символу."""
93
122
  return cls.get_ticker_info(symbol, MarketType.FUTURES)
94
123
 
95
- @classmethod
96
- @abstractmethod
97
- async def _load_exchange_info(cls) -> None:
98
- """Загружает информацию о бирже."""
99
- ...
100
-
101
124
  @classmethod
102
125
  def round_price(
103
126
  cls, symbol: str, price: float, market_type: MarketType = MarketType.SPOT
104
- ) -> float:
127
+ ) -> float: # type: ignore
105
128
  """Округляет цену до ближайшего возможного значения."""
106
129
  try:
107
130
  if market_type == MarketType.SPOT:
108
131
  precision = cls._tickers_info[symbol]["tick_precision"]
132
+ step = cls._tickers_info[symbol]["tick_step"]
109
133
  else:
110
134
  precision = cls._futures_tickers_info[symbol]["tick_precision"]
135
+ step = cls._futures_tickers_info[symbol]["tick_step"]
136
+ if precision:
137
+ return cls._floor_round(price, precision)
138
+ return cls._floor_to_step(price, step) # type: ignore
111
139
  except KeyError as e:
112
140
  cls._handle_key_error(e, symbol)
113
- return round(price, precision)
114
141
 
115
142
  @classmethod
116
143
  def round_quantity(
117
144
  cls, symbol: str, quantity: float, market_type: MarketType = MarketType.SPOT
118
- ) -> float:
145
+ ) -> float: # type: ignore
119
146
  """Округляет объем до ближайшего возможного значения."""
120
147
  try:
121
148
  if market_type == MarketType.SPOT:
122
149
  precision = cls._tickers_info[symbol]["size_precision"]
150
+ step = cls._tickers_info[symbol]["size_step"]
123
151
  else:
124
152
  precision = cls._futures_tickers_info[symbol]["size_precision"]
153
+ step = cls._futures_tickers_info[symbol]["size_step"]
154
+ if precision:
155
+ return cls._floor_round(quantity, precision)
156
+ return cls._floor_to_step(quantity, step) # type: ignore
125
157
  except KeyError as e:
126
158
  cls._handle_key_error(e, symbol)
127
- return round(quantity, precision)
128
159
 
129
160
  @classmethod
130
161
  def round_futures_price(cls, symbol: str, price: float) -> float:
@@ -137,31 +168,40 @@ class IExchangeInfo(ABC):
137
168
  return cls.round_quantity(symbol, quantity, MarketType.FUTURES)
138
169
 
139
170
  @staticmethod
140
- def _step_size_to_precision(tick_size: str | int | float) -> int:
141
- """Возвращает precision для round(x, precision) по шагу цены/объёма.
171
+ def _floor_to_step(value: float, step: float) -> float:
172
+ """Округляет число вниз до ближайшего кратного шага.
173
+
174
+ Принимает:
175
+ value (float): Исходное число.
176
+ step: (float): Шаг округления (> 0).
177
+
178
+ Возвращает:
179
+ Число, округлённое вниз до кратного step.
142
180
 
143
- Работает только для шагов — степеней 10.
144
181
  Примеры:
145
- "0.0001" -> 4
146
- "0.01" -> 2
147
- "0.1" -> 1
148
- "1" -> 0
149
- "10" -> -1
150
- "100" -> -2
182
+ >>> floor_to_step(0.16, 0.05)
183
+ 0.15
184
+ >>> floor_to_step(16, 5)
185
+ 15
186
+ >>> floor_to_step(1.2345, 0.01)
187
+ 1.23
188
+ >>> floor_to_step(-1.23, 0.1)
189
+ -1.3
190
+ >>> floor_to_step(100, 25)
191
+ 100
192
+
151
193
  """
152
- d = Decimal(str(tick_size)).normalize()
153
- if d <= 0:
154
- raise ValueError("tick_size must be > 0")
155
-
156
- t = d.as_tuple()
157
- # Степень десяти даёт один значащий разряд = 1 (1eN)
158
- if t.digits == (1,):
159
- return -t.exponent # type: ignore
160
-
161
- # Иначе это не степень 10 (например, 0.5, 5 и т.п.)
162
- raise ValueError(
163
- f"tick_size={tick_size} is not a power of 10; cannot map to round() precision."
164
- )
194
+ if step <= 0:
195
+ raise ValueError("step must be > 0")
196
+ result = math.floor(value / step) * step
197
+ digits = abs(Decimal(str(step)).as_tuple().exponent) # type: ignore
198
+ return round(result, digits)
199
+
200
+ @staticmethod
201
+ def _floor_round(value: float, digits: int) -> float:
202
+ """Округляет число вниз до указанного количества знаков после запятой."""
203
+ factor = 10**digits
204
+ return math.floor(value * factor) / factor
165
205
 
166
206
  @classmethod
167
207
  def _handle_key_error(cls, exception: KeyError, symbol: str) -> None:
@@ -0,0 +1,62 @@
1
+ __all__ = ["ExchangeInfo"]
2
+
3
+
4
+ import aiohttp
5
+
6
+ from unicex._abc import IExchangeInfo
7
+ from unicex.types import TickerInfoItem
8
+
9
+ from .client import Client
10
+
11
+
12
+ class ExchangeInfo(IExchangeInfo):
13
+ """Предзагружает информацию о тикерах для биржи Binance."""
14
+
15
+ exchange_name = "Binance"
16
+ """Название биржи, на которой работает класс."""
17
+
18
+ @classmethod
19
+ async def _load_spot_exchange_info(cls, session: aiohttp.ClientSession) -> None:
20
+ """Загружает информацию о бирже для спотового рынка."""
21
+ exchange_info = await Client(session).exchange_info()
22
+ tickers_info: dict[str, TickerInfoItem] = {}
23
+ for symbol_info in exchange_info["symbols"]:
24
+ filters = {
25
+ flt["filterType"]: flt
26
+ for flt in symbol_info.get("filters", [])
27
+ if "filterType" in flt
28
+ }
29
+ price_filter = filters["PRICE_FILTER"]
30
+ lot_size_filter = filters["LOT_SIZE"]
31
+ tickers_info[symbol_info["symbol"]] = TickerInfoItem(
32
+ tick_step=float(price_filter["tickSize"]),
33
+ tick_precision=None,
34
+ size_step=float(lot_size_filter["stepSize"]),
35
+ size_precision=None,
36
+ contract_size=1,
37
+ )
38
+
39
+ cls._tickers_info = tickers_info
40
+
41
+ @classmethod
42
+ async def _load_futures_exchange_info(cls, session: aiohttp.ClientSession) -> None:
43
+ """Загружает информацию о бирже для фьючерсного рынка."""
44
+ exchange_info = await Client(session).futures_exchange_info()
45
+ tickers_info: dict[str, TickerInfoItem] = {}
46
+ for symbol_info in exchange_info["symbols"]:
47
+ filters = {
48
+ flt["filterType"]: flt
49
+ for flt in symbol_info.get("filters", [])
50
+ if "filterType" in flt
51
+ }
52
+ price_filter = filters["PRICE_FILTER"]
53
+ lot_size_filter = filters["LOT_SIZE"]
54
+ tickers_info[symbol_info["symbol"]] = TickerInfoItem(
55
+ tick_step=float(price_filter["tickSize"]),
56
+ tick_precision=None,
57
+ size_step=float(lot_size_filter["stepSize"]),
58
+ size_precision=None,
59
+ contract_size=1,
60
+ )
61
+
62
+ cls._futures_tickers_info = tickers_info
@@ -0,0 +1,48 @@
1
+ __all__ = ["ExchangeInfo"]
2
+
3
+ import aiohttp
4
+
5
+ from unicex._abc import IExchangeInfo
6
+ from unicex.types import TickerInfoItem
7
+
8
+ from .client import Client
9
+
10
+
11
+ class ExchangeInfo(IExchangeInfo):
12
+ """Предзагружает информацию о тикерах для биржи Bitget."""
13
+
14
+ exchange_name = "Bitget"
15
+ """Название биржи, на которой работает класс."""
16
+
17
+ @classmethod
18
+ async def _load_spot_exchange_info(cls, session: aiohttp.ClientSession) -> None:
19
+ """Загружает информацию о бирже для спотового рынка."""
20
+ exchange_info = await Client(session).get_symbol_info()
21
+ tickers_info: dict[str, TickerInfoItem] = {}
22
+ for symbol_info in exchange_info["data"]:
23
+ tickers_info[symbol_info["symbol"]] = TickerInfoItem(
24
+ tick_precision=int(symbol_info["pricePrecision"]),
25
+ tick_step=None,
26
+ size_precision=int(symbol_info["quantityPrecision"]),
27
+ size_step=None,
28
+ contract_size=1,
29
+ )
30
+
31
+ cls._tickers_info = tickers_info
32
+
33
+ @classmethod
34
+ async def _load_futures_exchange_info(cls, session: aiohttp.ClientSession) -> None:
35
+ """Загружает информацию о бирже для фьючерсного рынка."""
36
+ tickers_info: dict[str, TickerInfoItem] = {}
37
+ exchange_info = await Client(session).futures_get_contracts("USDT-FUTURES")
38
+ for symbol_info in exchange_info["data"]:
39
+ symbol = symbol_info["symbol"]
40
+ tickers_info[symbol] = TickerInfoItem(
41
+ tick_precision=int(symbol_info["pricePlace"]),
42
+ tick_step=None,
43
+ size_precision=int(symbol_info["volumePlace"]),
44
+ size_step=None,
45
+ contract_size=float(symbol_info["sizeMultiplier"]),
46
+ )
47
+
48
+ cls._futures_tickers_info = tickers_info
@@ -0,0 +1,51 @@
1
+ __all__ = ["ExchangeInfo"]
2
+
3
+ import aiohttp
4
+
5
+ from unicex._abc import IExchangeInfo
6
+ from unicex.types import TickerInfoItem
7
+
8
+ from .client import Client
9
+
10
+
11
+ class ExchangeInfo(IExchangeInfo):
12
+ """Предзагружает информацию о тикерах для биржи Bybit."""
13
+
14
+ exchange_name = "Bybit"
15
+ """Название биржи, на которой работает класс."""
16
+
17
+ @classmethod
18
+ async def _load_spot_exchange_info(cls, session: aiohttp.ClientSession) -> None:
19
+ """Загружает информацию о бирже для спотового рынка."""
20
+ exchange_info = await Client(session).instruments_info("spot")
21
+ tickers_info: dict[str, TickerInfoItem] = {}
22
+ for symbol_info in exchange_info["result"]["list"]:
23
+ tickers_info[symbol_info["symbol"]] = TickerInfoItem(
24
+ tick_step=float(symbol_info["priceFilter"]["tickSize"]),
25
+ tick_precision=None,
26
+ size_step=float(symbol_info["lotSizeFilter"]["basePrecision"]),
27
+ size_precision=None,
28
+ contract_size=1,
29
+ )
30
+
31
+ cls._tickers_info = tickers_info
32
+
33
+ @classmethod
34
+ async def _load_futures_exchange_info(cls, session: aiohttp.ClientSession) -> None:
35
+ """Загружает информацию о бирже для фьючерсного рынка."""
36
+ exchange_info = await Client(session).instruments_info("linear", limit=1000)
37
+ tickers_info: dict[str, TickerInfoItem] = {}
38
+ for symbol_info in exchange_info["result"]["list"]:
39
+ try:
40
+ tickers_info[symbol_info["symbol"]] = TickerInfoItem(
41
+ tick_step=float(symbol_info["priceFilter"]["tickSize"]),
42
+ tick_precision=None,
43
+ size_step=float(symbol_info["lotSizeFilter"]["qtyStep"]),
44
+ size_precision=None,
45
+ contract_size=1,
46
+ )
47
+ except ValueError as e:
48
+ cls._logger.trace(
49
+ f"ValueError on {cls.exchange_name} by {symbol_info['symbol']}: {e}"
50
+ )
51
+ cls._futures_tickers_info = tickers_info
@@ -4,7 +4,7 @@ import hashlib
4
4
  import hmac
5
5
  import json
6
6
  import time
7
- from typing import Any
7
+ from typing import Any, Literal
8
8
 
9
9
  from unicex._base import BaseClient
10
10
  from unicex.exceptions import NotAuthorized
@@ -691,7 +691,7 @@ class Client(BaseClient):
691
691
 
692
692
  async def futures_contracts(
693
693
  self,
694
- settle: str,
694
+ settle: Literal["usdt", "btc"],
695
695
  limit: int | None = None,
696
696
  offset: int | None = None,
697
697
  ) -> dict:
@@ -0,0 +1,55 @@
1
+ __all__ = ["ExchangeInfo"]
2
+
3
+ import aiohttp
4
+
5
+ from unicex._abc import IExchangeInfo
6
+ from unicex.types import TickerInfoItem
7
+
8
+ from .client import Client
9
+
10
+
11
+ class ExchangeInfo(IExchangeInfo):
12
+ """Предзагружает информацию о тикерах для биржи Gateio."""
13
+
14
+ exchange_name = "Gateio"
15
+ """Название биржи, на которой работает класс."""
16
+
17
+ @classmethod
18
+ async def _load_spot_exchange_info(cls, session: aiohttp.ClientSession) -> None:
19
+ """Загружает информацию о бирже для спотового рынка."""
20
+ currency_pairs = await Client(session).currency_pairs()
21
+ tickers_info: dict[str, TickerInfoItem] = {}
22
+ for symbol_info in currency_pairs:
23
+ try:
24
+ tickers_info[symbol_info.get("id")] = TickerInfoItem(
25
+ tick_precision=int(symbol_info["precision"]),
26
+ tick_step=None,
27
+ size_precision=int(symbol_info["amount_precision"]),
28
+ size_step=None,
29
+ contract_size=1,
30
+ )
31
+ except ValueError as e:
32
+ cls._logger.trace(
33
+ f"ValueError on {cls.exchange_name} by {symbol_info['symbol']}: {e}"
34
+ )
35
+
36
+ cls._tickers_info = tickers_info
37
+
38
+ @classmethod
39
+ async def _load_futures_exchange_info(cls, session: aiohttp.ClientSession) -> None:
40
+ """Загружает информацию о бирже для фьючерсного рынка."""
41
+ contracts = await Client(session).futures_contracts("usdt")
42
+ tickers_info: dict[str, TickerInfoItem] = {}
43
+ for contract in contracts:
44
+ try:
45
+ tickers_info[contract.get("name")] = TickerInfoItem(
46
+ tick_precision=None,
47
+ tick_step=float(contract["order_price_round"]),
48
+ size_precision=None,
49
+ size_step=float(contract["quanto_multiplier"]),
50
+ contract_size=float(contract["quanto_multiplier"]),
51
+ )
52
+ except ValueError as e:
53
+ cls._logger.trace(f"ValueError on {cls.exchange_name} by {contract['name']}: {e}")
54
+
55
+ cls._futures_tickers_info = tickers_info
@@ -1,6 +1,9 @@
1
1
  __all__ = ["ExchangeInfo"]
2
2
 
3
+ import aiohttp
4
+
3
5
  from unicex._abc import IExchangeInfo
6
+ from unicex.types import TickerInfoItem
4
7
 
5
8
  from .client import Client
6
9
 
@@ -8,6 +11,9 @@ from .client import Client
8
11
  class ExchangeInfo(IExchangeInfo):
9
12
  """Предзагружает информацию о тикерах для биржи Hyperliquid."""
10
13
 
14
+ exchange_name = "Hyperliquid"
15
+ """Название биржи, на которой работает класс."""
16
+
11
17
  _spot_meta: dict = {}
12
18
  """Словарь с метаинформацией о спотовом рынке."""
13
19
 
@@ -20,17 +26,30 @@ class ExchangeInfo(IExchangeInfo):
20
26
  _futures_meta: dict = {}
21
27
  """Словарь с метаинформацией о фьючерсном рынке."""
22
28
 
29
+ # DOCS: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size
30
+
31
+ @classmethod
32
+ async def _load_spot_exchange_info(cls, session: aiohttp.ClientSession) -> None:
33
+ """Загружает информацию о бирже для спотового рынка."""
34
+ cls._spot_meta = await Client(session).spot_metadata()
35
+ cls._build_spot_mappings(cls._spot_meta)
36
+
37
+ tickers_info: dict[str, TickerInfoItem] = {}
38
+ for symbol_info in cls._spot_meta["tokens"]:
39
+ tickers_info[symbol_info["name"]] = TickerInfoItem(
40
+ tick_step=None,
41
+ tick_precision=int(symbol_info["weiDecimals"]),
42
+ size_step=None,
43
+ size_precision=int(symbol_info["szDecimals"]),
44
+ contract_size=1,
45
+ )
46
+
47
+ cls._tickers_info = tickers_info
48
+
23
49
  @classmethod
24
- async def _load_exchange_info(cls) -> None:
25
- """Загружает информацию о бирже."""
26
- client = await Client.create()
27
- async with client as conn:
28
- cls._spot_meta = await conn.spot_metadata()
29
- cls._build_spot_mappings(cls._spot_meta)
30
- cls._logger.debug("Hyperliquid spot exchange info loaded")
31
-
32
- cls._futures_meta = await conn.perp_metadata()
33
- cls._logger.debug("Hyperliquid futures exchange info loaded")
50
+ async def _load_futures_exchange_info(cls, session: aiohttp.ClientSession) -> None:
51
+ """Загружает информацию о бирже для фьючерсного рынка."""
52
+ cls._futures_meta = await Client(session).perp_metadata()
34
53
 
35
54
  @classmethod
36
55
  def _build_spot_mappings(cls, spot_meta: dict) -> None: