unicex 0.5.0__py3-none-any.whl → 0.8.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.
Files changed (100) hide show
  1. unicex/__init__.py +56 -124
  2. unicex/_abc/__init__.py +2 -0
  3. unicex/_abc/exchange_info.py +176 -0
  4. unicex/_abc/uni_client.py +2 -2
  5. unicex/_base/client.py +2 -2
  6. unicex/binance/__init__.py +12 -0
  7. unicex/binance/adapter.py +2 -2
  8. unicex/binance/exchange_info.py +12 -0
  9. unicex/bitget/__init__.py +14 -4
  10. unicex/bitget/adapter.py +1 -1
  11. unicex/bitget/exchange_info.py +12 -0
  12. unicex/bitget/uni_websocket_manager.py +1 -1
  13. unicex/bybit/__init__.py +12 -0
  14. unicex/bybit/adapter.py +2 -2
  15. unicex/bybit/exchange_info.py +12 -0
  16. unicex/bybit/uni_client.py +2 -2
  17. unicex/enums.py +16 -5
  18. unicex/extra.py +84 -6
  19. unicex/gateio/__init__.py +12 -0
  20. unicex/gateio/adapter.py +4 -4
  21. unicex/gateio/exchange_info.py +12 -0
  22. unicex/hyperliquid/__init__.py +12 -0
  23. unicex/hyperliquid/adapter.py +151 -30
  24. unicex/hyperliquid/client.py +2208 -125
  25. unicex/hyperliquid/exchange_info.py +100 -0
  26. unicex/hyperliquid/uni_client.py +176 -22
  27. unicex/mapper.py +35 -33
  28. unicex/mexc/__init__.py +12 -0
  29. unicex/mexc/adapter.py +30 -13
  30. unicex/mexc/exchange_info.py +32 -0
  31. unicex/mexc/uni_client.py +6 -0
  32. unicex/okx/__init__.py +12 -0
  33. unicex/okx/adapter.py +25 -9
  34. unicex/okx/client.py +2599 -26
  35. unicex/okx/exchange_info.py +50 -0
  36. unicex/okx/uni_client.py +10 -10
  37. unicex/types.py +31 -0
  38. unicex-0.8.0.dist-info/METADATA +192 -0
  39. unicex-0.8.0.dist-info/RECORD +75 -0
  40. unicex/bitrue/__init__.py +0 -15
  41. unicex/bitrue/adapter.py +0 -8
  42. unicex/bitrue/client.py +0 -128
  43. unicex/bitrue/uni_client.py +0 -151
  44. unicex/bitrue/uni_websocket_manager.py +0 -269
  45. unicex/bitrue/user_websocket.py +0 -7
  46. unicex/bitrue/websocket_manager.py +0 -11
  47. unicex/bitunix/__init__.py +0 -15
  48. unicex/bitunix/adapter.py +0 -8
  49. unicex/bitunix/client.py +0 -8
  50. unicex/bitunix/uni_client.py +0 -151
  51. unicex/bitunix/uni_websocket_manager.py +0 -269
  52. unicex/bitunix/user_websocket.py +0 -7
  53. unicex/bitunix/websocket_manager.py +0 -11
  54. unicex/btse/__init__.py +0 -15
  55. unicex/btse/adapter.py +0 -8
  56. unicex/btse/client.py +0 -123
  57. unicex/btse/uni_client.py +0 -151
  58. unicex/btse/uni_websocket_manager.py +0 -269
  59. unicex/btse/user_websocket.py +0 -7
  60. unicex/btse/websocket_manager.py +0 -11
  61. unicex/kcex/__init__.py +0 -15
  62. unicex/kcex/adapter.py +0 -8
  63. unicex/kcex/client.py +0 -8
  64. unicex/kcex/uni_client.py +0 -151
  65. unicex/kcex/uni_websocket_manager.py +0 -269
  66. unicex/kcex/user_websocket.py +0 -7
  67. unicex/kcex/websocket_manager.py +0 -11
  68. unicex/kraken/__init__.py +0 -15
  69. unicex/kraken/adapter.py +0 -8
  70. unicex/kraken/client.py +0 -165
  71. unicex/kraken/uni_client.py +0 -151
  72. unicex/kraken/uni_websocket_manager.py +0 -269
  73. unicex/kraken/user_websocket.py +0 -7
  74. unicex/kraken/websocket_manager.py +0 -11
  75. unicex/kucoin/__init__.py +0 -15
  76. unicex/kucoin/adapter.py +0 -8
  77. unicex/kucoin/client.py +0 -120
  78. unicex/kucoin/uni_client.py +0 -151
  79. unicex/kucoin/uni_websocket_manager.py +0 -269
  80. unicex/kucoin/user_websocket.py +0 -7
  81. unicex/kucoin/websocket_manager.py +0 -11
  82. unicex/weex/__init__.py +0 -15
  83. unicex/weex/adapter.py +0 -8
  84. unicex/weex/client.py +0 -8
  85. unicex/weex/uni_client.py +0 -151
  86. unicex/weex/uni_websocket_manager.py +0 -269
  87. unicex/weex/user_websocket.py +0 -7
  88. unicex/weex/websocket_manager.py +0 -11
  89. unicex/xt/__init__.py +0 -15
  90. unicex/xt/adapter.py +0 -8
  91. unicex/xt/client.py +0 -8
  92. unicex/xt/uni_client.py +0 -151
  93. unicex/xt/uni_websocket_manager.py +0 -269
  94. unicex/xt/user_websocket.py +0 -7
  95. unicex/xt/websocket_manager.py +0 -11
  96. unicex-0.5.0.dist-info/METADATA +0 -170
  97. unicex-0.5.0.dist-info/RECORD +0 -123
  98. {unicex-0.5.0.dist-info → unicex-0.8.0.dist-info}/WHEEL +0 -0
  99. {unicex-0.5.0.dist-info → unicex-0.8.0.dist-info}/licenses/LICENSE +0 -0
  100. {unicex-0.5.0.dist-info → unicex-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,12 @@
1
+ __all__ = ["ExchangeInfo"]
2
+
3
+ from unicex._abc import IExchangeInfo
4
+
5
+
6
+ class ExchangeInfo(IExchangeInfo):
7
+ """Предзагружает информацию о тикерах для биржи Bybit."""
8
+
9
+ @classmethod
10
+ async def _load_exchange_info(cls) -> None:
11
+ """Загружает информацию о бирже."""
12
+ ...
@@ -33,7 +33,7 @@ class UniClient(IUniClient[Client]):
33
33
  list[str]: Список тикеров.
34
34
  """
35
35
  raw_data = await self._client.tickers("spot")
36
- return Adapter.tickers(raw_data)
36
+ return Adapter.tickers(raw_data, only_usdt)
37
37
 
38
38
  async def futures_tickers(self, only_usdt: bool = True) -> list[str]:
39
39
  """Возвращает список тикеров.
@@ -45,7 +45,7 @@ class UniClient(IUniClient[Client]):
45
45
  list[str]: Список тикеров.
46
46
  """
47
47
  raw_data = await self._client.tickers("linear")
48
- return Adapter.tickers(raw_data)
48
+ return Adapter.tickers(raw_data, only_usdt)
49
49
 
50
50
  async def last_price(self) -> dict[str, float]:
51
51
  """Возвращает последнюю цену для каждого тикера.
unicex/enums.py CHANGED
@@ -26,18 +26,13 @@ class Exchange(StrEnum):
26
26
 
27
27
  BINANCE = "BINANCE"
28
28
  BITGET = "BITGET"
29
- BITRUE = "BITRUE"
30
29
  BITUNIX = "BITUNIX"
31
- BTSE = "BTSE"
32
30
  BYBIT = "BYBIT"
33
31
  GATEIO = "GATEIO"
34
32
  HYPERLIQUID = "HYPERLIQUID"
35
33
  KCEX = "KCEX"
36
- KRAKEN = "KRAKEN"
37
- KUCOIN = "KUCOIN"
38
34
  MEXC = "MEXC"
39
35
  OKX = "OKX"
40
- WEEX = "WEEX"
41
36
  XT = "XT"
42
37
 
43
38
  def __add__(self, market_type: "MarketType") -> tuple["Exchange", "MarketType"]:
@@ -227,6 +222,22 @@ class Timeframe(StrEnum):
227
222
  Timeframe.WEEK_1: "7d",
228
223
  Timeframe.MONTH_1: "30d",
229
224
  },
225
+ Exchange.HYPERLIQUID: {
226
+ Timeframe.MIN_1: "1m",
227
+ Timeframe.MIN_3: "3m",
228
+ Timeframe.MIN_5: "5m",
229
+ Timeframe.MIN_15: "15m",
230
+ Timeframe.MIN_30: "30m",
231
+ Timeframe.HOUR_1: "1h",
232
+ Timeframe.HOUR_2: "2h",
233
+ Timeframe.HOUR_4: "4h",
234
+ Timeframe.HOUR_8: "8h",
235
+ Timeframe.HOUR_12: "12h",
236
+ Timeframe.DAY_1: "1d",
237
+ Timeframe.DAY_3: "3d",
238
+ Timeframe.WEEK_1: "1w",
239
+ Timeframe.MONTH_1: "1M",
240
+ },
230
241
  }
231
242
 
232
243
  def to_exchange_format(self, exchange: Exchange, market_type: MarketType | None = None) -> str:
unicex/extra.py CHANGED
@@ -8,6 +8,8 @@ __all__ = [
8
8
  "generate_tv_link",
9
9
  "generate_cg_link",
10
10
  "make_humanreadable",
11
+ "normalize_ticker",
12
+ "normalize_symbol",
11
13
  ]
12
14
 
13
15
  import time
@@ -32,7 +34,7 @@ def percent_greater(higher: float, lower: float) -> float:
32
34
  `float`: На сколько процентов `higher` больше `lower`.
33
35
  """
34
36
  if lower == 0:
35
- return float("inf")
37
+ return 0.0 # Не будем возвращать float('inf'), чтобы не ломать логику приложения
36
38
  return (higher / lower - 1) * 100
37
39
 
38
40
 
@@ -51,7 +53,7 @@ def percent_less(higher: float, lower: float) -> float:
51
53
  `float`: На сколько процентов `lower` меньше `higher`.
52
54
  """
53
55
  if lower == 0:
54
- return float("inf")
56
+ return 0.0 # Не будем возвращать float('inf'), чтобы не ломать логику приложения
55
57
  return (1 - lower / higher) * 100
56
58
 
57
59
 
@@ -91,6 +93,76 @@ class TimeoutTracker[T]:
91
93
  self._blocked_items[item] = time.time() + duration
92
94
 
93
95
 
96
+ def normalize_ticker(raw_ticker: str) -> str:
97
+ """Нормализует тикер и возвращает базовую валюту (например, `BTC`).
98
+
99
+ Эта функция принимает тикер в различных форматах (с разделителями, постфиксом SWAP,
100
+ в верхнем или нижнем регистре) и приводит его к стандартному виду — только базовый актив.
101
+
102
+ Примеры:
103
+ ```python
104
+ normalize_ticker("BTC-USDT") # "BTC"
105
+ normalize_ticker("BTC-USDT-SWAP") # "BTC"
106
+ normalize_ticker("btc_usdt") # "BTC"
107
+ normalize_ticker("BTCUSDT") # "BTC"
108
+ normalize_ticker("BTC") # "BTC"
109
+ ```
110
+
111
+ Параметры:
112
+ raw_ticker (`str`): Исходный тикер в любом из распространённых форматов.
113
+
114
+ Возвращает:
115
+ `str`: Базовый актив в верхнем регистре (например, `"BTC"`).
116
+ """
117
+ ticker = raw_ticker.upper()
118
+
119
+ # Удаляем постфиксы SWAP
120
+ if ticker.endswith(("SWAP", "-SWAP", "_SWAP", ".SWAP")):
121
+ ticker = (
122
+ ticker.removesuffix("-SWAP")
123
+ .removesuffix("_SWAP")
124
+ .removesuffix(".SWAP")
125
+ .removesuffix("SWAP")
126
+ )
127
+
128
+ # Удаляем разделители
129
+ ticker = ticker.translate(str.maketrans("", "", "-_."))
130
+
131
+ # Убираем суффикс валюты котировки
132
+ for quote in ("USDT", "USDC"):
133
+ if ticker.endswith(quote):
134
+ ticker = ticker.removesuffix(quote)
135
+ break
136
+
137
+ return ticker
138
+
139
+
140
+ def normalize_symbol(raw_ticker: str, quote: Literal["USDT", "USDC"] = "USDT") -> str:
141
+ """Нормализует тикер до унифицированного символа (например, `BTCUSDT`).
142
+
143
+ Функция принимает тикер в любом из популярных форматов и возвращает полный символ,
144
+ состоящий из базовой валюты и указанной валюты котировки (`USDT` или `USDC`).
145
+
146
+ Примеры:
147
+ ```python
148
+ normalize_symbol("BTC-USDT") # "BTCUSDT"
149
+ normalize_symbol("BTC") # "BTCUSDT"
150
+ normalize_symbol("btc_usdt_swap") # "BTCUSDT"
151
+ normalize_symbol("ETH", "USDC") # "ETHUSDC"
152
+ ```
153
+
154
+ Параметры:
155
+ raw_ticker (`str`): Исходный тикер в любом из распространённых форматов.
156
+ quote (`Literal["USDT", "USDC"]`, optional): Валюта котировки.
157
+ По умолчанию `"USDT"`.
158
+
159
+ Возвращает:
160
+ `str`: Символ в унифицированном формате, например `"BTCUSDT"`.
161
+ """
162
+ base = normalize_ticker(raw_ticker)
163
+ return f"{base}{quote}"
164
+
165
+
94
166
  def generate_ex_link(exchange: Exchange, market_type: MarketType, symbol: str):
95
167
  """Генерирует ссылку на биржу.
96
168
 
@@ -102,7 +174,8 @@ def generate_ex_link(exchange: Exchange, market_type: MarketType, symbol: str):
102
174
  Возвращает:
103
175
  `str`: Ссылка на биржу.
104
176
  """
105
- ticker = symbol.removesuffix("USDT").removesuffix("_USDT").removesuffix("-USDT")
177
+ symbol = normalize_symbol(symbol)
178
+ ticker = normalize_ticker(symbol)
106
179
  if exchange == Exchange.BINANCE:
107
180
  if market_type == MarketType.FUTURES:
108
181
  return f"https://www.binance.com/en/futures/{symbol}"
@@ -152,7 +225,7 @@ def generate_ex_link(exchange: Exchange, market_type: MarketType, symbol: str):
152
225
  if market_type == MarketType.FUTURES:
153
226
  return f"https://app.hyperliquid.xyz/trade/{ticker}"
154
227
  else:
155
- return f"https://www.kcex.com/ru-RU/exchange/{ticker}/USDC"
228
+ return f"https://app.hyperliquid.xyz/trade/{ticker}/USDC"
156
229
  else:
157
230
  raise NotSupported(f"Exchange {exchange} is not supported")
158
231
 
@@ -168,6 +241,7 @@ def generate_tv_link(exchange: Exchange, market_type: MarketType, symbol: str) -
168
241
  Возвращает:
169
242
  `str`: Ссылка для TradingView.
170
243
  """
244
+ symbol = normalize_symbol(symbol)
171
245
  if market_type == MarketType.FUTURES:
172
246
  return f"https://www.tradingview.com/chart/?symbol={exchange}:{symbol}.P"
173
247
  else:
@@ -187,6 +261,8 @@ def generate_cg_link(exchange: Exchange, market_type: MarketType, symbol: str) -
187
261
  """
188
262
  base_url = "https://www.coinglass.com/tv/ru"
189
263
 
264
+ symbol = normalize_symbol(symbol)
265
+
190
266
  if market_type == MarketType.FUTURES:
191
267
  match exchange:
192
268
  case Exchange.OKX:
@@ -196,9 +272,11 @@ def generate_cg_link(exchange: Exchange, market_type: MarketType, symbol: str) -
196
272
  case Exchange.BITGET:
197
273
  return f"{base_url}/{exchange.capitalize()}_{symbol}_UMCBL"
198
274
  case Exchange.GATEIO:
199
- return f"{base_url}/{exchange.capitalize()}_{symbol.replace('USDT', '_USDT')}"
275
+ return f"{base_url}/Gate_{symbol.replace('USDT', '_USDT')}"
200
276
  case Exchange.BITUNIX:
201
277
  return f"{base_url}/{exchange.capitalize()}_{symbol}"
278
+ case Exchange.HYPERLIQUID:
279
+ return f"{base_url}/{exchange.capitalize()}_{symbol.replace('USDT', '-USD')}"
202
280
  case _:
203
281
  return f"{base_url}/{exchange.capitalize()}_{symbol}"
204
282
  else:
@@ -206,7 +284,7 @@ def generate_cg_link(exchange: Exchange, market_type: MarketType, symbol: str) -
206
284
  if exchange == Exchange.OKX:
207
285
  return f"{base_url}/SPOT_{exchange.upper()}_{symbol.replace('USDT', '-USDT')}"
208
286
  # Для остальных бирж ссылки нет → возвращаем заглушку
209
- return base_url
287
+ return generate_cg_link(exchange, MarketType.FUTURES, symbol)
210
288
 
211
289
 
212
290
  def make_humanreadable(value: float, locale: Literal["ru", "en"] = "ru") -> str:
unicex/gateio/__init__.py CHANGED
@@ -6,10 +6,22 @@ __all__ = [
6
6
  "UserWebsocket",
7
7
  "WebsocketManager",
8
8
  "UniWebsocketManager",
9
+ "ExchangeInfo",
9
10
  ]
10
11
 
11
12
  from .client import Client
13
+ from .exchange_info import ExchangeInfo
12
14
  from .uni_client import UniClient
13
15
  from .uni_websocket_manager import UniWebsocketManager
14
16
  from .user_websocket import UserWebsocket
15
17
  from .websocket_manager import WebsocketManager
18
+
19
+
20
+ async def load_exchange_info() -> None:
21
+ """Загружает информацию о бирже Gateio."""
22
+ await ExchangeInfo.load_exchange_info()
23
+
24
+
25
+ async def start_exchange_info(parse_interval_seconds: int = 60 * 60) -> None:
26
+ """Запускает процесс обновления информации о бирже Gateio."""
27
+ await ExchangeInfo.start(parse_interval_seconds)
unicex/gateio/adapter.py CHANGED
@@ -19,7 +19,7 @@ class Adapter:
19
19
  """Адаптер для унификации данных с Gateio API."""
20
20
 
21
21
  @staticmethod
22
- def tickers(raw_data: list[dict], only_usdt: bool = True) -> list[str]:
22
+ def tickers(raw_data: list[dict], only_usdt: bool) -> list[str]:
23
23
  """Преобразует сырой ответ о тикерах в список символов.
24
24
 
25
25
  Параметры:
@@ -32,11 +32,11 @@ class Adapter:
32
32
  return [
33
33
  item["currency_pair"]
34
34
  for item in raw_data
35
- if not only_usdt or item["currency_pair"].endswith("USDT")
35
+ if item["currency_pair"].endswith("USDT") or not only_usdt
36
36
  ]
37
37
 
38
38
  @staticmethod
39
- def futures_tickers(raw_data: list[dict], only_usdt: bool = True) -> list[str]:
39
+ def futures_tickers(raw_data: list[dict], only_usdt: bool) -> list[str]:
40
40
  """Преобразует сырой ответ о фьючерсных тикерах в список символов.
41
41
 
42
42
  Параметры:
@@ -49,7 +49,7 @@ class Adapter:
49
49
  return [
50
50
  item["contract"]
51
51
  for item in raw_data
52
- if not only_usdt or item["contract"].endswith("USDT")
52
+ if item["contract"].endswith("USDT") or not only_usdt
53
53
  ]
54
54
 
55
55
  @staticmethod
@@ -0,0 +1,12 @@
1
+ __all__ = ["ExchangeInfo"]
2
+
3
+ from unicex._abc import IExchangeInfo
4
+
5
+
6
+ class ExchangeInfo(IExchangeInfo):
7
+ """Предзагружает информацию о тикерах для биржи Gateio."""
8
+
9
+ @classmethod
10
+ async def _load_exchange_info(cls) -> None:
11
+ """Загружает информацию о бирже."""
12
+ ...
@@ -6,10 +6,22 @@ __all__ = [
6
6
  "UserWebsocket",
7
7
  "WebsocketManager",
8
8
  "UniWebsocketManager",
9
+ "ExchangeInfo",
9
10
  ]
10
11
 
11
12
  from .client import Client
13
+ from .exchange_info import ExchangeInfo
12
14
  from .uni_client import UniClient
13
15
  from .uni_websocket_manager import UniWebsocketManager
14
16
  from .user_websocket import UserWebsocket
15
17
  from .websocket_manager import WebsocketManager
18
+
19
+
20
+ async def load_exchange_info() -> None:
21
+ """Загружает информацию о бирже Hyperliquid."""
22
+ await ExchangeInfo.load_exchange_info()
23
+
24
+
25
+ async def start_exchange_info(parse_interval_seconds: int = 60 * 60) -> None:
26
+ """Запускает процесс обновления информации о бирже Hyperliquid."""
27
+ await ExchangeInfo.start(parse_interval_seconds)
@@ -3,80 +3,130 @@ __all__ = ["Adapter"]
3
3
  import time
4
4
 
5
5
  from unicex.types import (
6
+ KlineDict,
6
7
  OpenInterestDict,
7
8
  OpenInterestItem,
8
9
  TickerDailyDict,
9
10
  TickerDailyItem,
10
11
  )
11
- from unicex.utils import catch_adapter_errors, decorate_all_methods
12
12
 
13
+ # from unicex.utils import catch_adapter_errors, decorate_all_methods
14
+ from .exchange_info import ExchangeInfo
13
15
 
14
- @decorate_all_methods(catch_adapter_errors)
16
+
17
+ # @decorate_all_methods(catch_adapter_errors)
15
18
  class Adapter:
16
19
  """Адаптер для унификации данных с Hyperliquid API."""
17
20
 
18
21
  @staticmethod
19
- def tickers(raw_data: list) -> list[str]:
22
+ def tickers(raw_data: dict, resolve_symbols: bool) -> list[str]:
20
23
  """Преобразует данные Hyperliquid в список спотовых тикеров.
21
24
 
22
25
  Параметры:
23
- raw_data (list): Сырой ответ с биржи.
26
+ raw_data (dict): Сырой ответ с биржи.
27
+ resolve_symbols (bool): Если True, тикеры маппятся из вида "@123" в "BTC".
24
28
 
25
29
  Возвращает:
26
- list[str]: Список тикеров (например, "PURR/USDC").
30
+ list[str]: Список тикеров (например, "@123").
27
31
  """
28
- raise NotImplementedError()
32
+ if resolve_symbols:
33
+ return [ExchangeInfo.resolve_spot_symbol(item["name"]) for item in raw_data["universe"]]
34
+ else:
35
+ return [item["name"] for item in raw_data["universe"]]
29
36
 
30
37
  @staticmethod
31
- def futures_tickers(raw_data: list) -> list[str]:
38
+ def futures_tickers(raw_data: dict) -> list[str]:
32
39
  """Преобразует данные Hyperliquid в список фьючерсных тикеров.
33
40
 
34
41
  Параметры:
35
- raw_data (list): Сырой ответ с биржи.
42
+ raw_data (dict): Сырой ответ с биржи.
36
43
 
37
44
  Возвращает:
38
- list[str]: Список тикеров.
45
+ list[str]: Список тикеров (например, "@123").
39
46
  """
40
- universe = raw_data[0]["universe"]
41
- return [item["name"] for item in universe]
47
+ return [item["name"] for item in raw_data["universe"]]
42
48
 
43
49
  @staticmethod
44
- def last_price(raw_data: list) -> dict[str, float]:
50
+ def last_price(raw_data: dict, resolve_symbols: bool) -> dict[str, float]:
45
51
  """Преобразует данные о последних ценах (spot) в унифицированный формат.
46
52
 
47
53
  Параметры:
48
- raw_data (list): Сырой ответ с биржи.
54
+ raw_data (dict): Сырой ответ с биржи.
55
+ resolve_symbols (bool): Если True, тикеры маппятся из вида "@123" в "BTC".
49
56
 
50
57
  Возвращает:
51
58
  dict[str, float]: Словарь тикеров и последних цен.
52
59
  """
53
- raise NotImplementedError()
60
+ if resolve_symbols:
61
+ return {
62
+ ExchangeInfo.resolve_spot_symbol(token): float(price)
63
+ for token, price in raw_data.items()
64
+ if token.startswith("@")
65
+ }
66
+ else:
67
+ return {
68
+ token: float(price) for token, price in raw_data.items() if token.startswith("@")
69
+ }
54
70
 
55
71
  @staticmethod
56
- def futures_last_price(raw_data: list) -> dict[str, float]:
72
+ def futures_last_price(raw_data: dict) -> dict[str, float]:
57
73
  """Преобразует данные о последних ценах (futures) в унифицированный формат.
58
74
 
59
75
  Параметры:
60
- raw_data (list): Сырой ответ с биржи.
76
+ raw_data (dict): Сырой ответ с биржи.
61
77
 
62
78
  Возвращает:
63
79
  dict[str, float]: Словарь тикеров и последних цен.
64
80
  """
65
- universe = raw_data[0]["universe"]
66
- metrics = raw_data[1]
67
- return {universe[i]["name"]: float(item["markPx"]) for i, item in enumerate(metrics)}
81
+ return {k: v for k, v in raw_data.items() if not k.startswith("@")}
68
82
 
69
83
  @staticmethod
70
- def ticker_24hr(raw_data: list) -> TickerDailyDict:
84
+ def ticker_24hr(raw_data: list, resolve_symbols: bool) -> TickerDailyDict:
71
85
  """Преобразует 24-часовую статистику (spot) в унифицированный формат.
72
86
 
73
87
  Параметры:
74
88
  raw_data (list): Сырой ответ с биржи.
89
+ resolve_symbols (bool): Если True, тикеры маппятся из вида "@123" в "BTC".
75
90
 
76
91
  Возвращает:
77
92
  TickerDailyDict: Словарь тикеров и их статистики.
78
93
  """
79
- raise NotImplementedError()
94
+ metrics = raw_data[1]
95
+ result: TickerDailyDict = {}
96
+
97
+ for item in metrics:
98
+ try:
99
+ coin = item["coin"]
100
+
101
+ if resolve_symbols:
102
+ coin = ExchangeInfo.resolve_spot_symbol(coin) or coin
103
+
104
+ prev_day_px = float(item.get("prevDayPx") or "0")
105
+ mid_px = float(item.get("midPx") or "0")
106
+ mark_px = float(item.get("markPx") or "0")
107
+ day_ntl_vlm = float(item.get("dayNtlVlm") or "0")
108
+
109
+ p = ((mark_px - prev_day_px) / prev_day_px * 100) if prev_day_px else 0.0
110
+ v = (day_ntl_vlm / mid_px) if mid_px else 0.0
111
+ q = day_ntl_vlm
112
+
113
+ if coin in result:
114
+ # В случае с конфликтом оставляем ту монету, в которой больше дневного объема, т.к
115
+ # для 6 монет (на 07.10.2025) встречаются пары к USDH, которые повторяют идентификатор.
116
+ # Проблема не критичная - т.к. флаг resolve_symbols по идее использовать должен редко.
117
+ prev_ticker_daily = result[coin]
118
+ curr_ticker_daily = TickerDailyItem(p=p, v=v, q=q)
119
+ if prev_ticker_daily["q"] > curr_ticker_daily["q"]:
120
+ result[coin] = prev_ticker_daily
121
+ else:
122
+ result[coin] = curr_ticker_daily
123
+ else:
124
+ result[coin] = TickerDailyItem(p=p, v=v, q=q)
125
+
126
+ except (KeyError, TypeError, ValueError):
127
+ continue
128
+
129
+ return result
80
130
 
81
131
  @staticmethod
82
132
  def futures_ticker_24hr(raw_data: list) -> TickerDailyDict:
@@ -90,16 +140,87 @@ class Adapter:
90
140
  """
91
141
  universe = raw_data[0]["universe"]
92
142
  metrics = raw_data[1]
93
- return {
94
- universe[i]["name"]: TickerDailyItem(
95
- p=(float(item["markPx"]) - float(item["prevDayPx"]))
96
- / float(item["prevDayPx"])
97
- * 100,
98
- v=float(item["dayNtlVlm"]) / float(item["oraclePx"]),
99
- q=float(item["dayNtlVlm"]),
143
+
144
+ result: TickerDailyDict = {}
145
+
146
+ for i, item in enumerate(metrics):
147
+ try:
148
+ prev_day_px = float(item.get("prevDayPx", 0) or "0")
149
+ oracle_px = float(item.get("oraclePx", 0) or "0")
150
+ mark_px = float(item.get("markPx", 0) or "0")
151
+ day_ntl_vlm = float(item.get("dayNtlVlm", 0) or "0")
152
+
153
+ p = ((mark_px - prev_day_px) / prev_day_px * 100) if prev_day_px else 0.0
154
+ v = (day_ntl_vlm / oracle_px) if oracle_px else 0.0
155
+ q = day_ntl_vlm
156
+
157
+ result[universe[i]["name"]] = TickerDailyItem(p=p, v=v, q=q)
158
+ except (KeyError, TypeError, ValueError):
159
+ continue
160
+
161
+ return result
162
+
163
+ @staticmethod
164
+ def klines(raw_data: list[dict], resolve_symbols: bool) -> list[KlineDict]:
165
+ """Преобразует сырой ответ, в котором содержатся данные о свечах, в унифицированный формат.
166
+
167
+ Параметры:
168
+ raw_data (list[dict]): Сырой ответ с биржи.
169
+ resolve_symbols (bool): Если True, тикер маппится из вида "@123" в "BTC".
170
+
171
+ Возвращает:
172
+ list[KlineDict]: Список словарей, где каждый словарь содержит данные о свече.
173
+ """
174
+ return [
175
+ KlineDict(
176
+ s=kline["s"]
177
+ if not resolve_symbols
178
+ else ExchangeInfo.resolve_spot_symbol(kline["s"]),
179
+ t=kline["t"],
180
+ o=float(kline["o"]),
181
+ h=float(kline["h"]),
182
+ l=float(kline["l"]),
183
+ c=float(kline["c"]),
184
+ v=float(kline["v"]),
185
+ q=float(kline["v"]) * float(kline["c"]),
186
+ T=kline["T"],
187
+ x=None,
100
188
  )
101
- for i, item in enumerate(metrics)
102
- }
189
+ for kline in sorted(
190
+ raw_data,
191
+ key=lambda x: int(x["t"]),
192
+ )
193
+ ]
194
+
195
+ @staticmethod
196
+ def futures_klines(raw_data: list[dict]) -> list[KlineDict]:
197
+ """Преобразует сырой ответ, в котором содержатся данные о свечах, в унифицированный формат.
198
+
199
+ Параметры:
200
+ raw_data (list[dict]): Сырой ответ с биржи.
201
+ symbol (str): Символ тикера.
202
+
203
+ Возвращает:
204
+ list[KlineDict]: Список словарей, где каждый словарь содержит данные о свече.
205
+ """
206
+ return [
207
+ KlineDict(
208
+ s=kline["s"],
209
+ t=kline["t"],
210
+ o=float(kline["o"]),
211
+ h=float(kline["h"]),
212
+ l=float(kline["l"]),
213
+ c=float(kline["c"]),
214
+ v=float(kline["v"]),
215
+ q=float(kline["v"]) * float(kline["c"]),
216
+ T=kline["T"],
217
+ x=None,
218
+ )
219
+ for kline in sorted(
220
+ raw_data,
221
+ key=lambda x: int(x["t"]),
222
+ )
223
+ ]
103
224
 
104
225
  @staticmethod
105
226
  def funding_rate(raw_data: list) -> dict[str, float]: