unicex 0.11.0__py3-none-any.whl → 0.13.0__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.
@@ -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:
@@ -1,12 +1,62 @@
1
1
  __all__ = ["ExchangeInfo"]
2
2
 
3
+
4
+ import aiohttp
5
+
3
6
  from unicex._abc import IExchangeInfo
7
+ from unicex.types import TickerInfoItem
8
+
9
+ from .client import Client
4
10
 
5
11
 
6
12
  class ExchangeInfo(IExchangeInfo):
7
13
  """Предзагружает информацию о тикерах для биржи Binance."""
8
14
 
15
+ exchange_name = "Binance"
16
+ """Название биржи, на которой работает класс."""
17
+
9
18
  @classmethod
10
- async def _load_exchange_info(cls) -> None:
11
- """Загружает информацию о бирже."""
12
- ...
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
@@ -1,12 +1,48 @@
1
1
  __all__ = ["ExchangeInfo"]
2
2
 
3
+ import aiohttp
4
+
3
5
  from unicex._abc import IExchangeInfo
6
+ from unicex.types import TickerInfoItem
7
+
8
+ from .client import Client
4
9
 
5
10
 
6
11
  class ExchangeInfo(IExchangeInfo):
7
12
  """Предзагружает информацию о тикерах для биржи Bitget."""
8
13
 
14
+ exchange_name = "Bitget"
15
+ """Название биржи, на которой работает класс."""
16
+
9
17
  @classmethod
10
- async def _load_exchange_info(cls) -> None:
11
- """Загружает информацию о бирже."""
12
- ...
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
@@ -1,12 +1,51 @@
1
1
  __all__ = ["ExchangeInfo"]
2
2
 
3
+ import aiohttp
4
+
3
5
  from unicex._abc import IExchangeInfo
6
+ from unicex.types import TickerInfoItem
7
+
8
+ from .client import Client
4
9
 
5
10
 
6
11
  class ExchangeInfo(IExchangeInfo):
7
12
  """Предзагружает информацию о тикерах для биржи Bybit."""
8
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
+
9
33
  @classmethod
10
- async def _load_exchange_info(cls) -> None:
11
- """Загружает информацию о бирже."""
12
- ...
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
unicex/gateio/client.py CHANGED
@@ -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:
@@ -1,12 +1,55 @@
1
1
  __all__ = ["ExchangeInfo"]
2
2
 
3
+ import aiohttp
4
+
3
5
  from unicex._abc import IExchangeInfo
6
+ from unicex.types import TickerInfoItem
7
+
8
+ from .client import Client
4
9
 
5
10
 
6
11
  class ExchangeInfo(IExchangeInfo):
7
12
  """Предзагружает информацию о тикерах для биржи Gateio."""
8
13
 
14
+ exchange_name = "Gateio"
15
+ """Название биржи, на которой работает класс."""
16
+
9
17
  @classmethod
10
- async def _load_exchange_info(cls) -> None:
11
- """Загружает информацию о бирже."""
12
- ...
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:
@@ -5,28 +5,43 @@ import aiohttp
5
5
  from unicex._abc import IExchangeInfo
6
6
  from unicex.types import TickerInfoItem
7
7
 
8
+ from .client import Client
9
+
8
10
 
9
11
  class ExchangeInfo(IExchangeInfo):
10
12
  """Предзагружает информацию о тикерах для биржи Mexc."""
11
13
 
14
+ exchange_name = "Mexc"
15
+ """Название биржи, на которой работает класс."""
16
+
17
+ @classmethod
18
+ async def _load_spot_exchange_info(cls, session: aiohttp.ClientSession) -> None:
19
+ """Загружает информацию о бирже для спотового рынка."""
20
+ exchange_info = await Client(session).exchange_info()
21
+ tickers_info = {}
22
+ for el in exchange_info["symbols"]:
23
+ tickers_info[el["symbol"]] = TickerInfoItem(
24
+ tick_precision=int(el["quotePrecision"]),
25
+ tick_step=None,
26
+ size_precision=int(el["baseAssetPrecision"]),
27
+ size_step=None,
28
+ contract_size=1,
29
+ )
30
+
31
+ cls._tickers_info = tickers_info
32
+
12
33
  @classmethod
13
- async def _load_exchange_info(cls) -> None:
14
- """Загружает информацию о бирже."""
15
- futures_tickers_info = {}
16
- async with aiohttp.ClientSession() as session:
17
- url = "https://contract.mexc.com/api/v1/contract/detail"
18
- async with session.get(url) as response:
19
- data = await response.json()
20
- for el in data["data"]:
21
- futures_tickers_info[el["symbol"]] = TickerInfoItem(
22
- tick_precision=cls._step_size_to_precision(el["priceUnit"]),
23
- size_precision=el["amountScale"],
24
- contract_size=el["contractSize"],
25
- min_market_size=el["minVol"],
26
- max_market_size=el["maxVol"],
27
- min_limit_size=el["minVol"],
28
- max_limit_size=el["maxVol"],
29
- )
30
-
31
- cls._futures_tickers_info = futures_tickers_info
32
- cls._logger.debug("Mexc futures exchange info loaded")
34
+ async def _load_futures_exchange_info(cls, session: aiohttp.ClientSession) -> None:
35
+ """Загружает информацию о бирже для фьючерсного рынка."""
36
+ exchange_info = await Client(session).futures_contract_detail()
37
+ tickers_info = {}
38
+ for el in exchange_info["data"]:
39
+ tickers_info[el["symbol"]] = TickerInfoItem(
40
+ tick_precision=None,
41
+ tick_step=el["priceUnit"],
42
+ size_precision=None,
43
+ size_step=el["contractSize"],
44
+ contract_size=el["contractSize"],
45
+ )
46
+
47
+ cls._futures_tickers_info = tickers_info
@@ -5,46 +5,43 @@ import aiohttp
5
5
  from unicex._abc import IExchangeInfo
6
6
  from unicex.types import TickerInfoItem
7
7
 
8
+ from .client import Client
9
+
8
10
 
9
11
  class ExchangeInfo(IExchangeInfo):
10
12
  """Предзагружает информацию о тикерах для биржи Okx."""
11
13
 
14
+ exchange_name = "Okx"
15
+ """Название биржи, на которой работает класс."""
16
+
17
+ @classmethod
18
+ async def _load_spot_exchange_info(cls, session: aiohttp.ClientSession) -> None:
19
+ """Загружает информацию о бирже для спотового рынка."""
20
+ tickers_info = {}
21
+ exchange_info = await Client(session).get_instruments("SPOT")
22
+ for el in exchange_info["data"]:
23
+ tickers_info[el["instId"]] = TickerInfoItem(
24
+ tick_precision=None,
25
+ tick_step=float(el["tickSz"]),
26
+ size_precision=None,
27
+ size_step=float(el["lotSz"]),
28
+ contract_size=1,
29
+ )
30
+
31
+ cls._tickers_info = tickers_info
32
+
12
33
  @classmethod
13
- async def _load_exchange_info(cls) -> None:
14
- """Загружает информацию о бирже."""
15
- async with aiohttp.ClientSession() as session:
16
- tickers_info = {}
17
- url = "https://www.okx.com/api/v5/public/instruments?instType=SPOT"
18
- async with session.get(url) as response:
19
- data = await response.json()
20
- for el in data["data"]:
21
- tickers_info[el["instId"]] = TickerInfoItem(
22
- tick_precision=cls._step_size_to_precision(el["tickSz"]),
23
- size_precision=cls._step_size_to_precision(el["lotSz"]),
24
- contract_size=1,
25
- min_market_size=float(el["minSz"]),
26
- max_market_size=float(el["maxMktSz"]),
27
- min_limit_size=float(el["minSz"]),
28
- max_limit_size=float(el["maxLmtSz"]),
29
- )
30
-
31
- cls._tickers_info = tickers_info
32
- cls._logger.debug("Okx spot exchange info loaded")
33
-
34
- futures_tickers_info = {}
35
- url = "https://www.okx.com/api/v5/public/instruments?instType=SWAP"
36
- async with session.get(url) as response:
37
- data = await response.json()
38
- for el in data["data"]:
39
- futures_tickers_info[el["instId"]] = TickerInfoItem(
40
- tick_precision=cls._step_size_to_precision(el["tickSz"]),
41
- size_precision=cls._step_size_to_precision(el["lotSz"]),
42
- contract_size=float(el["ctVal"]),
43
- min_market_size=el["minSz"],
44
- max_market_size=el["maxMktSz"],
45
- min_limit_size=el["minSz"],
46
- max_limit_size=el["maxLmtSz"],
47
- )
48
-
49
- cls._futures_tickers_info = futures_tickers_info
50
- cls._logger.debug("Okx futures exchange info loaded")
34
+ async def _load_futures_exchange_info(cls, session: aiohttp.ClientSession) -> None:
35
+ """Загружает информацию о бирже для фьючерсного рынка."""
36
+ tickers_info = {}
37
+ exchange_info = await Client(session).get_instruments("SWAP")
38
+ for el in exchange_info["data"]:
39
+ tickers_info[el["instId"]] = TickerInfoItem(
40
+ tick_precision=None,
41
+ tick_step=float(el["tickSz"]),
42
+ size_precision=None,
43
+ size_step=float(el["lotSz"]) * float(el["ctVal"]),
44
+ contract_size=float(el["ctVal"]),
45
+ )
46
+
47
+ cls._futures_tickers_info = tickers_info
unicex/types.py CHANGED
@@ -119,6 +119,8 @@ type OpenInterestDict = dict[str, OpenInterestItem]
119
119
 
120
120
 
121
121
  class LiquidationDict(TypedDict):
122
+ """Модель ликвидации."""
123
+
122
124
  t: int
123
125
  """Время. В миллисекундах."""
124
126
 
@@ -140,29 +142,28 @@ type AccountType = Literal["SPOT", "FUTURES"]
140
142
 
141
143
 
142
144
  class TickerInfoItem(TypedDict):
143
- """Информация о размерах тиков, ступеней цены и множителя контракта (если есть) для тикера."""
145
+ """Информация о размерах тиков, ступеней цены и множителя контракта (если есть) для тикера.
146
+
147
+ На некоторых биржах удобнее делать округление через precisions, на некоторых через step,
148
+ потому что иногда встречаются шаги, которые не являются степенью 10. Поэтому обязательно
149
+ должны быть определены tick_precision ИЛИ tick_step, а так же size_precision ИЛИ size_step.
150
+ """
144
151
 
145
- tick_precision: int
152
+ tick_precision: int | None
146
153
  """Количество знаков после запятой для цены."""
147
154
 
148
- size_precision: int
155
+ tick_step: float | None
156
+ """Шаг одного деления для цены."""
157
+
158
+ size_precision: int | None
149
159
  """Количество знаков после запятой для объема."""
150
160
 
161
+ size_step: float | None
162
+ """Шаг одного деления для объема."""
163
+
151
164
  contract_size: float | None
152
165
  """Множитель контракта (если есть)."""
153
166
 
154
- min_market_size: float | None
155
- """Минимальный размер рыночного ордера в монетах (если есть)."""
156
-
157
- max_market_size: float | None
158
- """Максимальный размер рыночного ордера в монетах (если есть)."""
159
-
160
- min_limit_size: float | None
161
- """Минимальный размер лимитного ордера в монетах (если есть)."""
162
-
163
- max_limit_size: float | None
164
- """Максимальный размер лимитного ордера в монетах (если есть)."""
165
-
166
167
 
167
168
  type TickersInfoDict = dict[str, TickerInfoItem]
168
169
  """Информация о размерах тиков, ступеней цены и множителя контракта (если есть) для всех тикеров."""
@@ -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
+ ```
@@ -3,10 +3,10 @@ unicex/enums.py,sha256=8E_Nb57kriOif57XSLnW8joFufbthZTJ7tcExKWf1Wg,9633
3
3
  unicex/exceptions.py,sha256=r-xZzX78VuxVnI5pe99AM8FIiGcdIUDcF5CaTkQ4NE0,2213
4
4
  unicex/extra.py,sha256=MZRSsDRok05KZCqKur-hjOexZuoZ-tC9J6e-EIZr_lw,13824
5
5
  unicex/mapper.py,sha256=zOuInRQGJnSnwRI5yJ_axx-0svGn-nOqgLr7XXSlq14,4915
6
- unicex/types.py,sha256=cfuS07QVOgAhZsdME6CeY4lqO8KBBfrf_CJMN9DMB_s,4638
6
+ unicex/types.py,sha256=quGNpQm-_lSkijRKz04A5lPVxQ2PYrFzggYSPiJGzXI,4728
7
7
  unicex/utils.py,sha256=dwU1VYuP2xcMpzaETtNQerL1V8Y_JH8H8EsLJ__-M4s,8050
8
8
  unicex/_abc/__init__.py,sha256=fxZjNFJFeFwWTXz8iSDe7eCWwE6xfFwFwAuG6l-TI8A,289
9
- unicex/_abc/exchange_info.py,sha256=j0nbTeF9-_LwHGDZLBHYF2Dd1DYBCQ-DlUO5N5tZgEY,7541
9
+ unicex/_abc/exchange_info.py,sha256=NHBQAjknIRebyjl3musrvneN5u9vKYXjAca5PmTvJTw,9545
10
10
  unicex/_abc/uni_client.py,sha256=ZjxK8aqCGLUUYy1UQTM9EvWn1IXwMkH2Db8sZrs1e_I,13728
11
11
  unicex/_abc/uni_websocket_manager.py,sha256=yYKypPkIe3rKfWBuTsS8rkwIPljpd1588CYDkeTOYqE,9905
12
12
  unicex/_base/__init__.py,sha256=0TmevATGnRB3qow6tkCR8dQKNZCWKeib6YQjNJ4a1b0,236
@@ -15,7 +15,7 @@ unicex/_base/websocket.py,sha256=7IUIO2-KyjTswK3nTkRI7uQkNq6NYS_EhaG7tJQCPeg,114
15
15
  unicex/binance/__init__.py,sha256=sDk4ZjakRdpFMaMSpOCfqjf6ZPfAS9tlrt4WlDHtDkw,932
16
16
  unicex/binance/adapter.py,sha256=JbUFyjnDAFtyuYYrh90YeOvQOZQ6faim0nWS6U0NxXw,8799
17
17
  unicex/binance/client.py,sha256=1qPx0uRT4prC6saLBQ55pXDWcWTCKhYEwVIysiihPgU,60984
18
- unicex/binance/exchange_info.py,sha256=_1AEa9B8Id4aj4-0VPDBL3rWE0yqwj0KFwT9N97OSeA,353
18
+ unicex/binance/exchange_info.py,sha256=LNDkgBC5HB3JxtIBi39puqDg6LIVWqIWjT-6akDxtMs,2437
19
19
  unicex/binance/uni_client.py,sha256=W4yxiU0kkJKPJjimhv4KAWreuEBwt7GgrWXefcw5BEA,8365
20
20
  unicex/binance/uni_websocket_manager.py,sha256=FywEuUt3CiDcAQPo3ItdW2pPgbLDvVpVYFM2D_QscPU,8679
21
21
  unicex/binance/user_websocket.py,sha256=HJ_3VZV0Bil0vfsdLrZElXm-gUqd8mGzXzfQ0_sHjFc,7861
@@ -23,7 +23,7 @@ unicex/binance/websocket_manager.py,sha256=Idrq0Qzi14QgpIG5d77S-r1h_BvRXphcxY2BT
23
23
  unicex/bitget/__init__.py,sha256=8govSOEyWjA62js-ZTQIiSYWSmcEUFSC9hVTpS8eosk,929
24
24
  unicex/bitget/adapter.py,sha256=frQBOKFsIB8mXc_Ime2-Iby_nRQlSpzXjshC5AxoDTA,7741
25
25
  unicex/bitget/client.py,sha256=RbOrW8Q21OwYFac2xrKQURdYSAkVQ0jj18cDoLj5Edk,90155
26
- unicex/bitget/exchange_info.py,sha256=oR0L60Ew4QDkNle5-e7CBmWBwTj-5IbW57CkEeTGDMw,352
26
+ unicex/bitget/exchange_info.py,sha256=_UMvAqP0zcpmv9dkovkFxrXLlol6q8_v7-0sy6FSfrE,1959
27
27
  unicex/bitget/uni_client.py,sha256=MrXAmthTDTEQZ1ZY3LuqkCKL1bw_mKHMdiV4XiRFO-M,8641
28
28
  unicex/bitget/uni_websocket_manager.py,sha256=y-HXrREy_ruNeUzdRv5nHwBZQgbupxzp1UV-IFOpN_8,9793
29
29
  unicex/bitget/user_websocket.py,sha256=tlkv7Rmsw_FSfCJnEMOK_9jRsXRk2Ah_slqG8C-uhuo,129
@@ -31,15 +31,15 @@ unicex/bitget/websocket_manager.py,sha256=2OhH2gKy2gN03oYeCFvf3_l6RA29dDOvpajGhb
31
31
  unicex/bybit/__init__.py,sha256=SrMBh6K5zUt4JheWUpNUYNb1NCDr2ujTFv4IDguaGZI,926
32
32
  unicex/bybit/adapter.py,sha256=U6QP2eGefZqfXq7H4P1G9M23HieAjmylb5FnrHISn5s,8147
33
33
  unicex/bybit/client.py,sha256=U1mCcF-Mg2spiKlp0ucIA-kDOisYLO0edRS6XC16iNo,61014
34
- unicex/bybit/exchange_info.py,sha256=36WiJ49TxpSdZpeMwZdjQIkRdf2wJXahdkNxVaNjNRE,351
34
+ unicex/bybit/exchange_info.py,sha256=6DySi-61kyU1Wpfp7G-5Zu7G1bFfcR4qo2oO5d0YutI,2200
35
35
  unicex/bybit/uni_client.py,sha256=0wmIRRgofuJXWvZMB1gHwuIEfoWuPhLJXthmK1f9w40,8382
36
36
  unicex/bybit/uni_websocket_manager.py,sha256=OpnvWD0xZ8T_By0HZCSg3jWoZqScRATAsku4IIBqhlw,10252
37
37
  unicex/bybit/user_websocket.py,sha256=IGGEnwyWs5jOppgK_R7SisBDvsiF1_piTswBrdQOgDg,128
38
38
  unicex/bybit/websocket_manager.py,sha256=ePHvngoqoVPr6p-BayoEGhCuRK-cJ69CAPzF94qTalQ,15404
39
39
  unicex/gateio/__init__.py,sha256=dsKvhQhDcw4_w0S4c9IjKkCoOg4DCUtSecEUOlfstug,929
40
40
  unicex/gateio/adapter.py,sha256=PE5lXQORAKLhzFZBtxcarNIt2xEcDrPrphxWS0k0wEk,6842
41
- unicex/gateio/client.py,sha256=LS-g44vSY8FHCnDjFKzM8gIoPwWpKv15JxQIgwSietI,53732
42
- unicex/gateio/exchange_info.py,sha256=x_jZT14vMt2Cv4AtGhkFiAuCv43t4hwqQ4tiEjJ-HPY,352
41
+ unicex/gateio/client.py,sha256=bhWkYBn5B4JKpLRfQERTVzisiCSg7mMi6mM0kh4Hes4,53760
42
+ unicex/gateio/exchange_info.py,sha256=ANzfe4mqxtLnj2TBJJxoc31KUosvxdApp1_xYrRNQDs,2300
43
43
  unicex/gateio/uni_client.py,sha256=btfI-dozBRsbjEMvSlUX8zp6SOvIS1xDxk_5hNqRfz4,9648
44
44
  unicex/gateio/uni_websocket_manager.py,sha256=3KAHhgJNISTOZRBTwbZPigc8iq_IHS19NwnuAsnfeXg,9329
45
45
  unicex/gateio/user_websocket.py,sha256=4qZX9N2RjlJ-e25Eszz12OeCM17j5DdXVimBVaLj53w,129
@@ -47,7 +47,7 @@ unicex/gateio/websocket_manager.py,sha256=phtHbvAGQD3mtewCUxBuuD1Nj0FXN6oZrd7tnm
47
47
  unicex/hyperliquid/__init__.py,sha256=qGTAkwfXLvknvHET_iA7Qml3jkxxxA0moU_98nGTcVU,944
48
48
  unicex/hyperliquid/adapter.py,sha256=0aULPGDhppjbRvShbR49GNt6qmw4o_CmrFi_xGVRLHg,10612
49
49
  unicex/hyperliquid/client.py,sha256=8YnPh_6Hj8vFYcPbMaPae6OfroFzn28VI6iL3FQfa84,84255
50
- unicex/hyperliquid/exchange_info.py,sha256=OBaBpAEij2Fv6q10rbQwGzBEU-5xGkCze_slEo9Q2N0,4128
50
+ unicex/hyperliquid/exchange_info.py,sha256=D1h1fvH9DycRq-xDTZ_-Y1NC2JCp165IyWMXs39VjFI,4936
51
51
  unicex/hyperliquid/uni_client.py,sha256=4jv2uC076PBeq-EzCKvwaEEMk_M3HpsWA6iwNepJg7E,15674
52
52
  unicex/hyperliquid/uni_websocket_manager.py,sha256=AzR_8Aq98NkCA1oc2IiH02ysMOYLaisEmcuuaafeaJw,9334
53
53
  unicex/hyperliquid/user_websocket.py,sha256=BKD9ap2bx5DwpkkwwecfOTVedrZBR9eMAITgCBgg02w,134
@@ -55,7 +55,7 @@ unicex/hyperliquid/websocket_manager.py,sha256=GNBQT4ihk1XsTAaas8lxAfo3ASBGs_8EV
55
55
  unicex/mexc/__init__.py,sha256=lltANqM_2P-fmF5j8o5-pjmORPuK6C5sVjcQhuUU_R0,923
56
56
  unicex/mexc/adapter.py,sha256=uaJ6157wTrMuf72thIggDJHiaeRVYyH0qMIn1x5Mrp8,9820
57
57
  unicex/mexc/client.py,sha256=oJSpz-8hvA1WMCN2B7oqI2lXEx1_VJLAn8jbzhW8mF0,30950
58
- unicex/mexc/exchange_info.py,sha256=Nqzn4b791H3DLXsI6YZiY34VOZgHTG7evfRNWQnd9Nk,1329
58
+ unicex/mexc/exchange_info.py,sha256=z2bQsVU0ciXV2_DFkueZHo1X35KIK2alD-7ZZjNg5Kc,1763
59
59
  unicex/mexc/uni_client.py,sha256=0unhbU3WfqrMsV_y15VnIFjwUzQLVtp1tx6spV6cHKI,9477
60
60
  unicex/mexc/uni_websocket_manager.py,sha256=MAKyGyit8Ve4doM7y8cTQbVVbpzlJx2HcHaSkLG4Jp4,9327
61
61
  unicex/mexc/user_websocket.py,sha256=l77-e6i0B2btd7a5IcCytbgswnV171NqOhunTcbaq48,127
@@ -81,13 +81,13 @@ unicex/mexc/_spot_ws_proto/__init__.py,sha256=L8Jft1713_M8CLR9drgSjLBdY_46sPT3O9
81
81
  unicex/okx/__init__.py,sha256=Ljbw3AP0YrPF5bIPJi_3JP3B_czR9xurYHI24rgWk9M,920
82
82
  unicex/okx/adapter.py,sha256=zXSLieRC80KY56ey1X-ss51uQ0QzouDxXppjcofLNRQ,5351
83
83
  unicex/okx/client.py,sha256=2XL4YWPdRZYRMfLF1vSpkmwSd8Dxn8uyw5vCwDJab48,90969
84
- unicex/okx/exchange_info.py,sha256=q2DSVDq-JYN72LA5d_RX0C73Ha_xAxjeOobX8IxjpkM,2257
84
+ unicex/okx/exchange_info.py,sha256=gkTwYnXgswa1FGLXdKo9qLYqZA0BS9VefpALhR4_t-Q,1772
85
85
  unicex/okx/uni_client.py,sha256=E_Wod0JSGt1K6k1mAIWnOv350pELbv-nic7g1KgOuos,8694
86
86
  unicex/okx/uni_websocket_manager.py,sha256=b4f_QjA64DJmENQdIGb5IOVc7kvit7KMCdWeCmRbxGY,9326
87
87
  unicex/okx/user_websocket.py,sha256=8c9kpm-xVa729pW93OKUGLHaE9MY0uzEpjIgNIFRF80,126
88
88
  unicex/okx/websocket_manager.py,sha256=wROXTUDqKzOE-wDnCtXso_MC4SzfPuPols5aPg_Z3y4,26027
89
- unicex-0.11.0.dist-info/licenses/LICENSE,sha256=lNNK4Vqak9cXm6qVJLhbqS7iR_BMj6k7fd7XQ6l1k54,1507
90
- unicex-0.11.0.dist-info/METADATA,sha256=w8bEftaxRniVPd4owyacH8aW6_usW0XoFA7k8oIbxRA,9148
91
- unicex-0.11.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
92
- unicex-0.11.0.dist-info/top_level.txt,sha256=_7rar-0OENIg4KRy6cgjWiebFYAJhjKEcMggAocGWG4,7
93
- unicex-0.11.0.dist-info/RECORD,,
89
+ unicex-0.13.0.dist-info/licenses/LICENSE,sha256=lNNK4Vqak9cXm6qVJLhbqS7iR_BMj6k7fd7XQ6l1k54,1507
90
+ unicex-0.13.0.dist-info/METADATA,sha256=mcRAXzA8_-E8J4FPnOX8HStWwYigLbvL_6E9IyCvt90,11752
91
+ unicex-0.13.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
92
+ unicex-0.13.0.dist-info/top_level.txt,sha256=_7rar-0OENIg4KRy6cgjWiebFYAJhjKEcMggAocGWG4,7
93
+ unicex-0.13.0.dist-info/RECORD,,