unicex 0.13.17__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.
- unicex/__init__.py +200 -0
- unicex/_abc/__init__.py +11 -0
- unicex/_abc/exchange_info.py +216 -0
- unicex/_abc/uni_client.py +329 -0
- unicex/_abc/uni_websocket_manager.py +294 -0
- unicex/_base/__init__.py +9 -0
- unicex/_base/client.py +214 -0
- unicex/_base/websocket.py +261 -0
- unicex/binance/__init__.py +27 -0
- unicex/binance/adapter.py +202 -0
- unicex/binance/client.py +1577 -0
- unicex/binance/exchange_info.py +62 -0
- unicex/binance/uni_client.py +188 -0
- unicex/binance/uni_websocket_manager.py +166 -0
- unicex/binance/user_websocket.py +186 -0
- unicex/binance/websocket_manager.py +912 -0
- unicex/bitget/__init__.py +27 -0
- unicex/bitget/adapter.py +188 -0
- unicex/bitget/client.py +2514 -0
- unicex/bitget/exchange_info.py +48 -0
- unicex/bitget/uni_client.py +198 -0
- unicex/bitget/uni_websocket_manager.py +275 -0
- unicex/bitget/user_websocket.py +7 -0
- unicex/bitget/websocket_manager.py +232 -0
- unicex/bybit/__init__.py +27 -0
- unicex/bybit/adapter.py +208 -0
- unicex/bybit/client.py +1876 -0
- unicex/bybit/exchange_info.py +53 -0
- unicex/bybit/uni_client.py +200 -0
- unicex/bybit/uni_websocket_manager.py +291 -0
- unicex/bybit/user_websocket.py +7 -0
- unicex/bybit/websocket_manager.py +339 -0
- unicex/enums.py +273 -0
- unicex/exceptions.py +64 -0
- unicex/extra.py +335 -0
- unicex/gate/__init__.py +27 -0
- unicex/gate/adapter.py +178 -0
- unicex/gate/client.py +1667 -0
- unicex/gate/exchange_info.py +55 -0
- unicex/gate/uni_client.py +214 -0
- unicex/gate/uni_websocket_manager.py +269 -0
- unicex/gate/user_websocket.py +7 -0
- unicex/gate/websocket_manager.py +513 -0
- unicex/hyperliquid/__init__.py +27 -0
- unicex/hyperliquid/adapter.py +261 -0
- unicex/hyperliquid/client.py +2315 -0
- unicex/hyperliquid/exchange_info.py +119 -0
- unicex/hyperliquid/uni_client.py +325 -0
- unicex/hyperliquid/uni_websocket_manager.py +269 -0
- unicex/hyperliquid/user_websocket.py +7 -0
- unicex/hyperliquid/websocket_manager.py +393 -0
- unicex/mapper.py +111 -0
- unicex/mexc/__init__.py +27 -0
- unicex/mexc/_spot_ws_proto/PrivateAccountV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PrivateDealsV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PrivateOrdersV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PublicAggreBookTickerV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PublicAggreDealsV3Api_pb2.py +40 -0
- unicex/mexc/_spot_ws_proto/PublicAggreDepthsV3Api_pb2.py +40 -0
- unicex/mexc/_spot_ws_proto/PublicBookTickerBatchV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PublicBookTickerV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PublicDealsV3Api_pb2.py +40 -0
- unicex/mexc/_spot_ws_proto/PublicFuture_pb2.py +103 -0
- unicex/mexc/_spot_ws_proto/PublicIncreaseDepthsBatchV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PublicIncreaseDepthsV3Api_pb2.py +40 -0
- unicex/mexc/_spot_ws_proto/PublicLimitDepthsV3Api_pb2.py +40 -0
- unicex/mexc/_spot_ws_proto/PublicMiniTickerV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PublicMiniTickersV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PublicSpotKlineV3Api_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/PushDataV3ApiWrapper_pb2.py +38 -0
- unicex/mexc/_spot_ws_proto/__init__.py +335 -0
- unicex/mexc/adapter.py +239 -0
- unicex/mexc/client.py +846 -0
- unicex/mexc/exchange_info.py +47 -0
- unicex/mexc/uni_client.py +211 -0
- unicex/mexc/uni_websocket_manager.py +269 -0
- unicex/mexc/user_websocket.py +7 -0
- unicex/mexc/websocket_manager.py +456 -0
- unicex/okx/__init__.py +27 -0
- unicex/okx/adapter.py +150 -0
- unicex/okx/client.py +2864 -0
- unicex/okx/exchange_info.py +47 -0
- unicex/okx/uni_client.py +202 -0
- unicex/okx/uni_websocket_manager.py +269 -0
- unicex/okx/user_websocket.py +7 -0
- unicex/okx/websocket_manager.py +743 -0
- unicex/types.py +164 -0
- unicex/utils.py +218 -0
- unicex-0.13.17.dist-info/METADATA +243 -0
- unicex-0.13.17.dist-info/RECORD +93 -0
- unicex-0.13.17.dist-info/WHEEL +5 -0
- unicex-0.13.17.dist-info/licenses/LICENSE +28 -0
- unicex-0.13.17.dist-info/top_level.txt +1 -0
|
@@ -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,188 @@
|
|
|
1
|
+
__all__ = ["UniClient"]
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from typing import overload
|
|
5
|
+
|
|
6
|
+
from unicex._abc import IUniClient
|
|
7
|
+
from unicex.enums import Exchange, MarketType, Timeframe
|
|
8
|
+
from unicex.types import KlineDict, OpenInterestItem, TickerDailyDict
|
|
9
|
+
|
|
10
|
+
from .adapter import Adapter
|
|
11
|
+
from .client import Client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UniClient(IUniClient[Client]):
|
|
15
|
+
"""Унифицированный клиент для работы с Binance API."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def _client_cls(self) -> type[Client]:
|
|
19
|
+
"""Возвращает класс клиента для Binance.
|
|
20
|
+
|
|
21
|
+
Возвращает:
|
|
22
|
+
type[Client]: Класс клиента для Binance.
|
|
23
|
+
"""
|
|
24
|
+
return Client
|
|
25
|
+
|
|
26
|
+
async def tickers(self, only_usdt: bool = True) -> list[str]:
|
|
27
|
+
"""Возвращает список тикеров.
|
|
28
|
+
|
|
29
|
+
Параметры:
|
|
30
|
+
only_usdt (bool): Если True, возвращает только тикеры в паре к USDT.
|
|
31
|
+
|
|
32
|
+
Возвращает:
|
|
33
|
+
list[str]: Список тикеров.
|
|
34
|
+
"""
|
|
35
|
+
raw_data = await self._client.ticker_price()
|
|
36
|
+
return Adapter.tickers(raw_data=raw_data, only_usdt=only_usdt) # type: ignore | raw_data is list[dict] if symbol param is not ommited
|
|
37
|
+
|
|
38
|
+
async def futures_tickers(self, only_usdt: bool = True) -> list[str]:
|
|
39
|
+
"""Возвращает список тикеров.
|
|
40
|
+
|
|
41
|
+
Параметры:
|
|
42
|
+
only_usdt (bool): Если True, возвращает только тикеры в паре к USDT.
|
|
43
|
+
|
|
44
|
+
Возвращает:
|
|
45
|
+
list[str]: Список тикеров.
|
|
46
|
+
"""
|
|
47
|
+
raw_data = await self._client.futures_ticker_price()
|
|
48
|
+
return Adapter.tickers(raw_data=raw_data, only_usdt=only_usdt) # type: ignore | raw_data is list[dict] if symbol param is not ommited
|
|
49
|
+
|
|
50
|
+
async def last_price(self) -> dict[str, float]:
|
|
51
|
+
"""Возвращает последнюю цену для каждого тикера.
|
|
52
|
+
|
|
53
|
+
Возвращает:
|
|
54
|
+
dict[str, float]: Словарь с последними ценами для каждого тикера.
|
|
55
|
+
"""
|
|
56
|
+
raw_data = await self._client.ticker_price()
|
|
57
|
+
return Adapter.last_price(raw_data) # type: ignore | raw_data is list[dict] if symbol param is not ommited
|
|
58
|
+
|
|
59
|
+
async def futures_last_price(self) -> dict[str, float]:
|
|
60
|
+
"""Возвращает последнюю цену для каждого тикера.
|
|
61
|
+
|
|
62
|
+
Возвращает:
|
|
63
|
+
dict[str, float]: Словарь с последними ценами для каждого тикера.
|
|
64
|
+
"""
|
|
65
|
+
raw_data = await self._client.futures_ticker_price()
|
|
66
|
+
return Adapter.last_price(raw_data) # type: ignore | raw_data is list[dict] if symbol param is not ommited
|
|
67
|
+
|
|
68
|
+
async def ticker_24hr(self) -> TickerDailyDict:
|
|
69
|
+
"""Возвращает статистику за последние 24 часа для каждого тикера.
|
|
70
|
+
|
|
71
|
+
Возвращает:
|
|
72
|
+
TickerDailyDict: Словарь с статистикой за последние 24 часа для каждого тикера.
|
|
73
|
+
"""
|
|
74
|
+
raw_data = await self._client.ticker_24hr()
|
|
75
|
+
return Adapter.ticker_24hr(raw_data=raw_data) # type: ignore | raw_data is list[dict] if symbol param is not ommited
|
|
76
|
+
|
|
77
|
+
async def futures_ticker_24hr(self) -> TickerDailyDict:
|
|
78
|
+
"""Возвращает статистику за последние 24 часа для каждого тикера.
|
|
79
|
+
|
|
80
|
+
Возвращает:
|
|
81
|
+
TickerDailyDict: Словарь с статистикой за последние 24 часа для каждого тикера.
|
|
82
|
+
"""
|
|
83
|
+
raw_data = await self._client.futures_ticker_24hr()
|
|
84
|
+
return Adapter.ticker_24hr(raw_data=raw_data) # type: ignore | raw_data is list[dict] if symbol param is not ommited
|
|
85
|
+
|
|
86
|
+
async def klines(
|
|
87
|
+
self,
|
|
88
|
+
symbol: str,
|
|
89
|
+
interval: Timeframe | str,
|
|
90
|
+
limit: int | None = None,
|
|
91
|
+
start_time: int | None = None,
|
|
92
|
+
end_time: int | None = None,
|
|
93
|
+
) -> list[KlineDict]:
|
|
94
|
+
"""Возвращает список свечей для тикера.
|
|
95
|
+
|
|
96
|
+
Параметры:
|
|
97
|
+
symbol (str): Название тикера.
|
|
98
|
+
limit (int | None): Количество свечей.
|
|
99
|
+
interval (Timeframe | str): Таймфрейм свечей.
|
|
100
|
+
start_time (int | None): Время начала периода в миллисекундах.
|
|
101
|
+
end_time (int | None): Время окончания периода в миллисекундах.
|
|
102
|
+
|
|
103
|
+
Возвращает:
|
|
104
|
+
list[KlineDict]: Список свечей для тикера.
|
|
105
|
+
"""
|
|
106
|
+
interval = (
|
|
107
|
+
interval.to_exchange_format(Exchange.BINANCE, MarketType.SPOT)
|
|
108
|
+
if isinstance(interval, Timeframe)
|
|
109
|
+
else interval
|
|
110
|
+
)
|
|
111
|
+
raw_data = await self._client.klines(
|
|
112
|
+
symbol=symbol,
|
|
113
|
+
interval=interval,
|
|
114
|
+
limit=limit,
|
|
115
|
+
start_time=start_time,
|
|
116
|
+
end_time=end_time,
|
|
117
|
+
)
|
|
118
|
+
return Adapter.klines(raw_data=raw_data, symbol=symbol)
|
|
119
|
+
|
|
120
|
+
async def futures_klines(
|
|
121
|
+
self,
|
|
122
|
+
symbol: str,
|
|
123
|
+
interval: Timeframe | str,
|
|
124
|
+
limit: int | None = None,
|
|
125
|
+
start_time: int | None = None,
|
|
126
|
+
end_time: int | None = None,
|
|
127
|
+
) -> list[KlineDict]:
|
|
128
|
+
"""Возвращает список свечей для тикера.
|
|
129
|
+
|
|
130
|
+
Параметры:
|
|
131
|
+
symbol (str): Название тикера.
|
|
132
|
+
limit (int | None): Количество свечей.
|
|
133
|
+
interval (Timeframe | str): Таймфрейм свечей.
|
|
134
|
+
start_time (int | None): Время начала периода в миллисекундах.
|
|
135
|
+
end_time (int | None): Время окончания периода в миллисекундах.
|
|
136
|
+
|
|
137
|
+
Возвращает:
|
|
138
|
+
list[KlineDict]: Список свечей для тикера.
|
|
139
|
+
"""
|
|
140
|
+
interval = (
|
|
141
|
+
interval.to_exchange_format(Exchange.BINANCE, MarketType.FUTURES)
|
|
142
|
+
if isinstance(interval, Timeframe)
|
|
143
|
+
else interval
|
|
144
|
+
)
|
|
145
|
+
raw_data = await self._client.futures_klines(
|
|
146
|
+
symbol=symbol,
|
|
147
|
+
interval=interval,
|
|
148
|
+
limit=limit,
|
|
149
|
+
start_time=start_time,
|
|
150
|
+
end_time=end_time,
|
|
151
|
+
)
|
|
152
|
+
return Adapter.klines(raw_data=raw_data, symbol=symbol)
|
|
153
|
+
|
|
154
|
+
@overload
|
|
155
|
+
async def funding_rate(self, symbol: str) -> float: ...
|
|
156
|
+
|
|
157
|
+
@overload
|
|
158
|
+
async def funding_rate(self, symbol: None) -> dict[str, float]: ...
|
|
159
|
+
|
|
160
|
+
@overload
|
|
161
|
+
async def funding_rate(self) -> dict[str, float]: ...
|
|
162
|
+
|
|
163
|
+
async def funding_rate(self, symbol: str | None = None) -> dict[str, float] | float:
|
|
164
|
+
"""Возвращает ставку финансирования для тикера или всех тикеров, если тикер не указан.
|
|
165
|
+
|
|
166
|
+
- Параметры:
|
|
167
|
+
symbol (`str | None`): Название тикера (Опционально).
|
|
168
|
+
|
|
169
|
+
Возвращает:
|
|
170
|
+
`dict[str, float] | float`: Ставка финансирования для тикера или словарь со ставками для всех тикеров.
|
|
171
|
+
"""
|
|
172
|
+
raw_data = await self._client.futures_mark_price(symbol=symbol)
|
|
173
|
+
adapted_data = Adapter.funding_rate(raw_data if isinstance(raw_data, list) else [raw_data]) # type: ignore[arg-type]
|
|
174
|
+
return adapted_data[symbol] if symbol else adapted_data
|
|
175
|
+
|
|
176
|
+
async def open_interest(self, symbol: str = None) -> OpenInterestItem: # type: ignore[reportArgumentType] | We should provide our exception message
|
|
177
|
+
"""Возвращает объем открытого интереса для тикера.
|
|
178
|
+
|
|
179
|
+
Параметры:
|
|
180
|
+
symbol (`str`): Название тикера.
|
|
181
|
+
|
|
182
|
+
Возвращает:
|
|
183
|
+
`OpenInterestItem`: Словарь со временем и объемом открытого интереса в монетах.
|
|
184
|
+
"""
|
|
185
|
+
if not symbol:
|
|
186
|
+
raise ValueError("Symbol is required for binance open interest")
|
|
187
|
+
raw_data = await self._client.open_interest(symbol=symbol)
|
|
188
|
+
return Adapter.open_interest(raw_data)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
__all__ = ["UniWebsocketManager"]
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from unicex._abc import IUniWebsocketManager
|
|
7
|
+
from unicex._base import Websocket
|
|
8
|
+
from unicex.enums import Exchange, MarketType, Timeframe
|
|
9
|
+
from unicex.types import LoggerLike
|
|
10
|
+
|
|
11
|
+
from .adapter import Adapter
|
|
12
|
+
from .client import Client
|
|
13
|
+
from .uni_client import UniClient
|
|
14
|
+
from .websocket_manager import WebsocketManager
|
|
15
|
+
|
|
16
|
+
type CallbackType = Callable[[Any], Awaitable[None]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class UniWebsocketManager(IUniWebsocketManager):
|
|
20
|
+
"""Реализация менеджера асинхронных унифицированных вебсокетов для биржи Binance."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self, client: Client | UniClient | None = None, logger: LoggerLike | None = None
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Инициализирует унифицированный менеджер вебсокетов.
|
|
26
|
+
|
|
27
|
+
Параметры:
|
|
28
|
+
client (`Client | UniClient | None`): Клиент Binance или унифицированный клиент. Нужен для подключения к приватным топикам.
|
|
29
|
+
logger (`LoggerLike | None`): Логгер для записи логов.
|
|
30
|
+
"""
|
|
31
|
+
super().__init__(client=client, logger=logger)
|
|
32
|
+
self._websocket_manager = WebsocketManager(self._client) # type: ignore
|
|
33
|
+
self._adapter = Adapter()
|
|
34
|
+
|
|
35
|
+
def klines(
|
|
36
|
+
self,
|
|
37
|
+
callback: CallbackType,
|
|
38
|
+
timeframe: Timeframe,
|
|
39
|
+
symbol: str | None = None,
|
|
40
|
+
symbols: list[str] | None = None,
|
|
41
|
+
) -> Websocket:
|
|
42
|
+
"""Создаёт вебсокет для получения свечей на споте с унификацией сообщений.
|
|
43
|
+
|
|
44
|
+
Параметры:
|
|
45
|
+
callback (`CallbackType`): Асинхронная функция обработки адаптированных сообщений.
|
|
46
|
+
timeframe (`Timeframe`): Временной интервал свечей (унифицированный).
|
|
47
|
+
symbol (`str | None`): Один символ для подписки.
|
|
48
|
+
symbols (`list[str] | None`): Список символов для мультиплекс‑подключения.
|
|
49
|
+
|
|
50
|
+
Должен быть указан либо `symbol`, либо `symbols`.
|
|
51
|
+
|
|
52
|
+
Возвращает:
|
|
53
|
+
`Websocket`: Экземпляр вебсокета для управления соединением.
|
|
54
|
+
"""
|
|
55
|
+
wrapper = self._make_wrapper(self._adapter.klines_message, callback)
|
|
56
|
+
return self._websocket_manager.klines(
|
|
57
|
+
callback=wrapper,
|
|
58
|
+
symbol=symbol,
|
|
59
|
+
symbols=symbols,
|
|
60
|
+
interval=timeframe.to_exchange_format(Exchange.BINANCE, MarketType.SPOT),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def futures_klines(
|
|
64
|
+
self,
|
|
65
|
+
callback: CallbackType,
|
|
66
|
+
timeframe: Timeframe,
|
|
67
|
+
symbol: str | None = None,
|
|
68
|
+
symbols: list[str] | None = None,
|
|
69
|
+
) -> Websocket:
|
|
70
|
+
"""Создаёт вебсокет для получения свечей на фьючерсах с унификацией сообщений.
|
|
71
|
+
|
|
72
|
+
Параметры:
|
|
73
|
+
callback (`CallbackType`): Асинхронная функция обработки адаптированных сообщений.
|
|
74
|
+
timeframe (`Timeframe`): Временной интервал свечей (унифицированный).
|
|
75
|
+
symbol (`str | None`): Один символ для подписки.
|
|
76
|
+
symbols (`list[str] | None`): Список символов для мультиплекс‑подключения.
|
|
77
|
+
|
|
78
|
+
Должен быть указан либо `symbol`, либо `symbols`.
|
|
79
|
+
|
|
80
|
+
Возвращает:
|
|
81
|
+
`Websocket`: Экземпляр вебсокета для управления соединением.
|
|
82
|
+
"""
|
|
83
|
+
wrapper = self._make_wrapper(self._adapter.klines_message, callback)
|
|
84
|
+
return self._websocket_manager.futures_klines(
|
|
85
|
+
callback=wrapper,
|
|
86
|
+
symbol=symbol,
|
|
87
|
+
symbols=symbols,
|
|
88
|
+
interval=timeframe.to_exchange_format(Exchange.BINANCE, MarketType.FUTURES),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def trades(
|
|
92
|
+
self, callback: CallbackType, symbol: str | None = None, symbols: list[str] | None = None
|
|
93
|
+
) -> Websocket:
|
|
94
|
+
"""Создаёт вебсокет для получения сделок на споте с унификацией сообщений.
|
|
95
|
+
|
|
96
|
+
Параметры:
|
|
97
|
+
callback (`CallbackType`): Асинхронная функция обработки адаптированных сообщений.
|
|
98
|
+
symbol (`str | None`): Один символ для подписки.
|
|
99
|
+
symbols (`list[str] | None`): Список символов для мультиплекс‑подключения.
|
|
100
|
+
|
|
101
|
+
Должен быть указан либо `symbol`, либо `symbols`.
|
|
102
|
+
|
|
103
|
+
Возвращает:
|
|
104
|
+
`Websocket`: Экземпляр вебсокета для управления соединением.
|
|
105
|
+
"""
|
|
106
|
+
wrapper = self._make_wrapper(self._adapter.trades_message, callback)
|
|
107
|
+
return self._websocket_manager.trade(callback=wrapper, symbol=symbol, symbols=symbols)
|
|
108
|
+
|
|
109
|
+
def aggtrades(
|
|
110
|
+
self, callback: CallbackType, symbol: str | None = None, symbols: list[str] | None = None
|
|
111
|
+
) -> Websocket:
|
|
112
|
+
"""Создаёт вебсокет для получения агрегированных сделок на споте с унификацией сообщений.
|
|
113
|
+
|
|
114
|
+
Параметры:
|
|
115
|
+
callback (`CallbackType`): Асинхронная функция обработки адаптированных сообщений.
|
|
116
|
+
symbol (`str | None`): Один символ для подписки.
|
|
117
|
+
symbols (`list[str] | None`): Список символов для мультиплекс‑подключения.
|
|
118
|
+
|
|
119
|
+
Должен быть указан либо `symbol`, либо `symbols`.
|
|
120
|
+
|
|
121
|
+
Возвращает:
|
|
122
|
+
`Websocket`: Экземпляр вебсокета для управления соединением.
|
|
123
|
+
"""
|
|
124
|
+
wrapper = self._make_wrapper(self._adapter.aggtrades_message, callback)
|
|
125
|
+
return self._websocket_manager.agg_trade(callback=wrapper, symbol=symbol, symbols=symbols)
|
|
126
|
+
|
|
127
|
+
def futures_trades(
|
|
128
|
+
self, callback: CallbackType, symbol: str | None = None, symbols: list[str] | None = None
|
|
129
|
+
) -> Websocket:
|
|
130
|
+
"""Создаёт вебсокет для получения сделок на фьючерсах с унификацией сообщений.
|
|
131
|
+
|
|
132
|
+
Параметры:
|
|
133
|
+
callback (`CallbackType`): Асинхронная функция обработки
|
|
134
|
+
адаптированных сообщений.
|
|
135
|
+
symbol (`str | None`): Один символ для подписки.
|
|
136
|
+
symbols (`list[str] | None`): Список символов для мультиплекс‑подключения.
|
|
137
|
+
|
|
138
|
+
Должен быть указан либо `symbol`, либо `symbols`.
|
|
139
|
+
|
|
140
|
+
Возвращает:
|
|
141
|
+
`Websocket`: Экземпляр вебсокета для управления соединением.
|
|
142
|
+
"""
|
|
143
|
+
wrapper = self._make_wrapper(self._adapter.trades_message, callback)
|
|
144
|
+
return self._websocket_manager.futures_trade(
|
|
145
|
+
callback=wrapper, symbol=symbol, symbols=symbols
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def futures_aggtrades(
|
|
149
|
+
self, callback: CallbackType, symbol: str | None = None, symbols: list[str] | None = None
|
|
150
|
+
) -> Websocket:
|
|
151
|
+
"""Создаёт вебсокет для получения агрегированных сделок на фьючерсах с унификацией сообщений.
|
|
152
|
+
|
|
153
|
+
Параметры:
|
|
154
|
+
callback (`CallbackType`): Асинхронная функция обработки адаптированных сообщений.
|
|
155
|
+
symbol (`str | None`): Один символ для подписки.
|
|
156
|
+
symbols (`list[str] | None`): Список символов для мультиплекс‑подключения.
|
|
157
|
+
|
|
158
|
+
Должен быть указан либо `symbol`, либо `symbols`.
|
|
159
|
+
|
|
160
|
+
Возвращает:
|
|
161
|
+
`Websocket`: Экземпляр вебсокета для управления соединением.
|
|
162
|
+
"""
|
|
163
|
+
wrapper = self._make_wrapper(self._adapter.aggtrades_message, callback)
|
|
164
|
+
return self._websocket_manager.futures_agg_trade(
|
|
165
|
+
callback=wrapper, symbol=symbol, symbols=symbols
|
|
166
|
+
)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
__all__ = ["UserWebsocket"]
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
from loguru import logger as _logger
|
|
8
|
+
|
|
9
|
+
from unicex._base import Websocket
|
|
10
|
+
from unicex.exceptions import NotSupported
|
|
11
|
+
from unicex.types import LoggerLike
|
|
12
|
+
|
|
13
|
+
from .client import Client
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserWebsocket:
|
|
17
|
+
"""Пользовательский вебсокет Binance с авто‑продлением listenKey.
|
|
18
|
+
|
|
19
|
+
Поддержка типов аккаунта: "SPOT" и "FUTURES" (USDT‑M фьючерсы).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_BASE_SPOT_URL: str = "wss://stream.binance.com:9443"
|
|
23
|
+
"""Базовый URL для вебсокета на спот."""
|
|
24
|
+
|
|
25
|
+
_BASE_FUTURES_URL: str = "wss://fstream.binance.com"
|
|
26
|
+
"""Базовый URL для вебсокета на фьючерсы."""
|
|
27
|
+
|
|
28
|
+
_RENEW_INTERVAL: int = 30 * 60
|
|
29
|
+
"""Интервал продления listenKey (сек.)"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
callback: Callable[[Any], Awaitable[None]],
|
|
34
|
+
client: Client,
|
|
35
|
+
type: Literal["SPOT", "FUTURES"],
|
|
36
|
+
logger: LoggerLike | None = None,
|
|
37
|
+
**kwargs: Any, # Не дадим сломаться, если юзер передал ненужные аргументы
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Инициализирует пользовательский вебсокет для работы с биржей Binance.
|
|
40
|
+
|
|
41
|
+
Параметры:
|
|
42
|
+
callback (`Callable`): Асинхронная функция обратного вызова, которая принимает сообщение с вебсокета.
|
|
43
|
+
client (`Client`): Авторизованный клиент Binance.
|
|
44
|
+
type (`str`): Тип аккаунта ("SPOT" | "FUTURES").
|
|
45
|
+
logger (`LoggerLike | None`): Логгер для записи логов.
|
|
46
|
+
"""
|
|
47
|
+
self._callback = callback
|
|
48
|
+
self._client = client
|
|
49
|
+
self._type = type
|
|
50
|
+
|
|
51
|
+
self._listen_key: str | None = None
|
|
52
|
+
self._ws: Websocket | None = None
|
|
53
|
+
self._keepalive_task: asyncio.Task | None = None
|
|
54
|
+
|
|
55
|
+
self._logger = logger or _logger
|
|
56
|
+
|
|
57
|
+
self._running = False
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def _create_ws_url(cls, type: Literal["SPOT", "FUTURES"], listen_key: str) -> str:
|
|
61
|
+
"""Создает URL для подключения к WebSocket."""
|
|
62
|
+
if type == "FUTURES":
|
|
63
|
+
return f"{cls._BASE_FUTURES_URL}/ws/{listen_key}"
|
|
64
|
+
if type == "SPOT":
|
|
65
|
+
return f"{cls._BASE_SPOT_URL}/ws/{listen_key}"
|
|
66
|
+
raise NotSupported(f"Account type '{type}' not supported")
|
|
67
|
+
|
|
68
|
+
async def start(self) -> None:
|
|
69
|
+
"""Запускает пользовательский стрим с автопродлением listenKey."""
|
|
70
|
+
self._running = True
|
|
71
|
+
self._listen_key = await self._create_listen_key()
|
|
72
|
+
await self._start_ws(self._create_ws_url(self._type, self._listen_key)) # type: ignore
|
|
73
|
+
|
|
74
|
+
# Фоновое продление ключа прослушивания
|
|
75
|
+
self._keepalive_task = asyncio.create_task(self._keepalive_loop())
|
|
76
|
+
|
|
77
|
+
async def stop(self) -> None:
|
|
78
|
+
"""Останавливает стрим и закрывает listenKey."""
|
|
79
|
+
self._running = False
|
|
80
|
+
|
|
81
|
+
# Останавливаем вебсокет
|
|
82
|
+
try:
|
|
83
|
+
if isinstance(self._ws, Websocket):
|
|
84
|
+
await self._ws.stop()
|
|
85
|
+
except Exception as e:
|
|
86
|
+
self._logger.error(f"Error stopping WebSocket: {e}")
|
|
87
|
+
|
|
88
|
+
# Ожидаем завершения фонового продления ключа прослушивания
|
|
89
|
+
if isinstance(self._keepalive_task, asyncio.Task):
|
|
90
|
+
try:
|
|
91
|
+
self._keepalive_task.cancel()
|
|
92
|
+
await self._keepalive_task
|
|
93
|
+
except asyncio.CancelledError:
|
|
94
|
+
pass
|
|
95
|
+
except Exception as e:
|
|
96
|
+
self._logger.error(f"Error stopping keepalive task: {e}")
|
|
97
|
+
|
|
98
|
+
# Закрываем ключ прослушивания
|
|
99
|
+
try:
|
|
100
|
+
if self._listen_key:
|
|
101
|
+
await self._close_listen_key(self._listen_key)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
self._logger.error(f"Error closing listenKey: {e}")
|
|
104
|
+
finally:
|
|
105
|
+
self._listen_key = None
|
|
106
|
+
|
|
107
|
+
self._logger.info("User websocket stopped")
|
|
108
|
+
|
|
109
|
+
async def restart(self) -> None:
|
|
110
|
+
"""Перезапускает WebSocket для User Data Stream."""
|
|
111
|
+
await self.stop()
|
|
112
|
+
await self.start()
|
|
113
|
+
|
|
114
|
+
async def _start_ws(self, ws_url: str) -> None:
|
|
115
|
+
"""Запускает WebSocket для User Data Stream."""
|
|
116
|
+
self._ws = Websocket(callback=self._callback, url=ws_url, no_message_reconnect_timeout=None)
|
|
117
|
+
await self._ws.start()
|
|
118
|
+
self._logger.info(f"User websocket started: ...{ws_url[-5:]}")
|
|
119
|
+
|
|
120
|
+
async def _keepalive_loop(self) -> None:
|
|
121
|
+
"""Фоновый цикл продления listenKey и восстановления сессии при необходимости."""
|
|
122
|
+
while self._running:
|
|
123
|
+
try:
|
|
124
|
+
if self._type == "FUTURES":
|
|
125
|
+
response = await self._renew_listen_key()
|
|
126
|
+
listen_key = response.get("listenKey") if isinstance(response, dict) else None
|
|
127
|
+
if not listen_key:
|
|
128
|
+
raise RuntimeError(f"Can not renew listenKey: {response}")
|
|
129
|
+
|
|
130
|
+
if listen_key != self._listen_key:
|
|
131
|
+
self._logger.info(
|
|
132
|
+
f"Listen key changed: {self._listen_key} -> {listen_key}. Restarting websocket"
|
|
133
|
+
)
|
|
134
|
+
await self.restart()
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
elif self._type == "SPOT":
|
|
138
|
+
await self._renew_listen_key()
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
raise NotSupported(f"Account type '{self._type}' not supported")
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self._logger.error(f"Error while keeping alive: {e}")
|
|
145
|
+
await self.restart()
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
# Ждём до следующего продления
|
|
149
|
+
for _ in range(self._RENEW_INTERVAL):
|
|
150
|
+
if not self._running:
|
|
151
|
+
return
|
|
152
|
+
await asyncio.sleep(1)
|
|
153
|
+
|
|
154
|
+
async def _create_listen_key(self) -> str:
|
|
155
|
+
"""Создает новый listenKey для User Data Stream в зависимости от типа аккаунта."""
|
|
156
|
+
if self._type == "FUTURES":
|
|
157
|
+
resp = await self._client.futures_listen_key()
|
|
158
|
+
elif self._type == "SPOT":
|
|
159
|
+
resp = await self._client.listen_key()
|
|
160
|
+
else:
|
|
161
|
+
raise NotSupported(f"Account type '{self._type}' not supported")
|
|
162
|
+
|
|
163
|
+
key = resp.get("listenKey") if isinstance(resp, dict) else None
|
|
164
|
+
if not key:
|
|
165
|
+
raise RuntimeError(f"Can not create listenKey: {resp}")
|
|
166
|
+
return key
|
|
167
|
+
|
|
168
|
+
async def _renew_listen_key(self) -> dict:
|
|
169
|
+
"""Продлевает listenKey. Возвращает новый ключ, если сервер его выдал."""
|
|
170
|
+
if not isinstance(self._listen_key, str):
|
|
171
|
+
raise RuntimeError("listenKey is not a string")
|
|
172
|
+
if self._type == "FUTURES":
|
|
173
|
+
return await self._client.futures_renew_listen_key()
|
|
174
|
+
elif self._type == "SPOT":
|
|
175
|
+
return await self._client.renew_listen_key(self._listen_key)
|
|
176
|
+
else:
|
|
177
|
+
raise NotSupported(f"Account type '{self._type}' not supported")
|
|
178
|
+
|
|
179
|
+
async def _close_listen_key(self, listen_key: str) -> None:
|
|
180
|
+
"""Закрывает listenKey."""
|
|
181
|
+
if self._type == "FUTURES":
|
|
182
|
+
await self._client.futures_close_listen_key()
|
|
183
|
+
elif self._type == "SPOT":
|
|
184
|
+
await self._client.close_listen_key(listen_key)
|
|
185
|
+
else:
|
|
186
|
+
raise NotSupported(f"Account type '{self._type}' not supported")
|