exchanges-wrapper 2.1.41__tar.gz → 2.1.43__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.
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/PKG-INFO +4 -4
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/__init__.py +4 -2
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/client.py +67 -52
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/exch_srv.py +55 -56
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/http_client.py +22 -3
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/lib.py +1 -1
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/okx.py +11 -11
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/web_sockets.py +99 -72
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/pyproject.toml +7 -4
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/LICENSE.md +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/README.md +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/definitions.py +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/errors.py +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/events.py +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/martin/__init__.py +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/bitfinex.py +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/bybit.py +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/huobi.py +0 -0
- {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/proto/martin.proto +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: exchanges-wrapper
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.43
|
|
4
4
|
Summary: REST API and WebSocket asyncio wrapper with grpc powered multiplexer server
|
|
5
5
|
Author-email: Thomas Marchand <thomas.marchand@tuta.io>, Jerry Fedorenko <jerry.fedorenko@yahoo.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -12,10 +12,10 @@ Classifier: Operating System :: Unix
|
|
|
12
12
|
Classifier: Operating System :: Microsoft :: Windows
|
|
13
13
|
Classifier: Operating System :: MacOS
|
|
14
14
|
License-File: LICENSE.md
|
|
15
|
-
Requires-Dist: crypto-ws-api==2.1.
|
|
15
|
+
Requires-Dist: crypto-ws-api==2.1.4
|
|
16
16
|
Requires-Dist: pyotp==2.9.0
|
|
17
|
-
Requires-Dist: simplejson==3.20.
|
|
18
|
-
Requires-Dist: aiohttp~=3.
|
|
17
|
+
Requires-Dist: simplejson==3.20.2
|
|
18
|
+
Requires-Dist: aiohttp~=3.13.2
|
|
19
19
|
Requires-Dist: expiringdict~=1.2.2
|
|
20
20
|
Requires-Dist: betterproto==2.0.0b7
|
|
21
21
|
Requires-Dist: grpclib~=0.4.8
|
|
@@ -12,7 +12,7 @@ __maintainer__ = "Jerry Fedorenko"
|
|
|
12
12
|
__contact__ = "https://github.com/DogsTailFarmer"
|
|
13
13
|
__email__ = "jerry.fedorenko@yahoo.com"
|
|
14
14
|
__credits__ = ["https://github.com/DanyaSWorlD"]
|
|
15
|
-
__version__ = "2.1.
|
|
15
|
+
__version__ = "2.1.43"
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
import shutil
|
|
@@ -33,9 +33,11 @@ __all__ = [
|
|
|
33
33
|
'LOG_PATH',
|
|
34
34
|
'WORK_PATH',
|
|
35
35
|
'LOG_FILE',
|
|
36
|
-
'CONFIG_FILE'
|
|
36
|
+
'CONFIG_FILE',
|
|
37
|
+
'DEBUG_LOG'
|
|
37
38
|
]
|
|
38
39
|
|
|
40
|
+
DEBUG_LOG = 'debug' # The exchange for which log files, separated by trade_id with DEBUG level, will be generated
|
|
39
41
|
WORK_PATH = Path(Path.home(), ".MartinBinance")
|
|
40
42
|
CONFIG_PATH = Path(WORK_PATH, "config")
|
|
41
43
|
CONFIG_FILE = Path(CONFIG_PATH, "exch_srv_cfg.toml")
|
|
@@ -25,16 +25,18 @@ import exchanges_wrapper.parsers.bitfinex as bfx
|
|
|
25
25
|
import exchanges_wrapper.parsers.huobi as hbp
|
|
26
26
|
import exchanges_wrapper.parsers.okx as okx
|
|
27
27
|
import exchanges_wrapper.parsers.bybit as bbt
|
|
28
|
-
from crypto_ws_api.ws_session import UserWSSession
|
|
28
|
+
from crypto_ws_api.ws_session import UserWSSession, tasks_manage, tasks_cancel
|
|
29
29
|
|
|
30
30
|
logger = logging.getLogger(__name__)
|
|
31
31
|
|
|
32
32
|
STATUS_TIMEOUT = 5 # sec, also use for lifetime limit for inactive order (Bitfinex) as 60 * STATUS_TIMEOUT
|
|
33
33
|
ORDER_ENDPOINT = "/api/v3/order"
|
|
34
34
|
|
|
35
|
+
|
|
35
36
|
def fallback_warning(exchange, symbol=None):
|
|
36
37
|
logger.warning(f"Called by: {inspect.stack()[1][3]}: {exchange}:{symbol}: Fallback to HTTP API call")
|
|
37
38
|
|
|
39
|
+
|
|
38
40
|
def truncate(f, n):
|
|
39
41
|
return math.floor(f * 10 ** n) / 10 ** n
|
|
40
42
|
|
|
@@ -44,6 +46,19 @@ def any2str(_x) -> str:
|
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
class Client:
|
|
49
|
+
__slots__ = (
|
|
50
|
+
'exchange', 'sub_account', 'test_net', 'api_key', 'api_secret',
|
|
51
|
+
'passphrase', 'endpoint_api_public', 'endpoint_ws_public',
|
|
52
|
+
'endpoint_api_auth', 'endpoint_ws_auth', 'endpoint_ws_api',
|
|
53
|
+
'ws_add_on', 'master_email', 'master_name', 'two_fa', 'http',
|
|
54
|
+
'user_session', 'symbols', 'highest_precision',
|
|
55
|
+
'rate_limits', 'data_streams', 'active_orders', 'wss_buffer',
|
|
56
|
+
'stream_queue', 'on_order_update_queues', 'account_id',
|
|
57
|
+
'account_uid', 'main_account_id', 'main_account_uid',
|
|
58
|
+
'ledgers_id', 'ts_start', 'tasks', 'request_event',
|
|
59
|
+
'_events'
|
|
60
|
+
)
|
|
61
|
+
|
|
47
62
|
def __init__(self, acc: dict):
|
|
48
63
|
self.exchange = acc['exchange']
|
|
49
64
|
self.sub_account = acc['sub_account']
|
|
@@ -83,7 +98,6 @@ class Client:
|
|
|
83
98
|
else:
|
|
84
99
|
self.user_session = None
|
|
85
100
|
#
|
|
86
|
-
self.loaded = False
|
|
87
101
|
self.symbols = {}
|
|
88
102
|
self.highest_precision = None
|
|
89
103
|
self.rate_limits = None
|
|
@@ -98,15 +112,10 @@ class Client:
|
|
|
98
112
|
self.main_account_uid = None
|
|
99
113
|
self.ledgers_id = []
|
|
100
114
|
self.ts_start = {}
|
|
101
|
-
self.tasks =
|
|
115
|
+
self.tasks: dict[str, set] = {}
|
|
102
116
|
self.request_event = asyncio.Event()
|
|
103
117
|
self.request_event.set()
|
|
104
118
|
|
|
105
|
-
def tasks_manage(self, coro):
|
|
106
|
-
_t = asyncio.create_task(coro)
|
|
107
|
-
self.tasks.add(_t)
|
|
108
|
-
_t.add_done_callback(self.tasks.discard)
|
|
109
|
-
|
|
110
119
|
async def fetch_object(self, key):
|
|
111
120
|
res = None
|
|
112
121
|
while res is None:
|
|
@@ -140,12 +149,11 @@ class Client:
|
|
|
140
149
|
logger.info(f"Main ByBit account UID: {self.main_account_uid}, sub-UID: {self.account_uid}")
|
|
141
150
|
# load rate limits
|
|
142
151
|
self.rate_limits = infos["rateLimits"]
|
|
143
|
-
self.loaded = True
|
|
144
152
|
logger.info(f"Info for {self.exchange}:{symbol} loaded successfully")
|
|
145
153
|
|
|
146
154
|
async def close(self):
|
|
147
|
-
if self.http
|
|
148
|
-
await self.http.
|
|
155
|
+
if self.http:
|
|
156
|
+
await self.http.close_session()
|
|
149
157
|
|
|
150
158
|
@property
|
|
151
159
|
def events(self):
|
|
@@ -154,9 +162,8 @@ class Client:
|
|
|
154
162
|
self._events = Events() # skipcq: PYL-W0201
|
|
155
163
|
return self._events
|
|
156
164
|
|
|
157
|
-
|
|
165
|
+
def start_user_events_listener(self, _trade_id, symbol):
|
|
158
166
|
logger.info(f"Start '{self.exchange}' user events listener for {_trade_id}")
|
|
159
|
-
user_data_stream = None
|
|
160
167
|
if self.exchange == 'binance':
|
|
161
168
|
user_data_stream = UserEventsDataStream(self, self.endpoint_ws_api, self.exchange, _trade_id)
|
|
162
169
|
elif self.exchange == 'bitfinex':
|
|
@@ -173,24 +180,27 @@ class Client:
|
|
|
173
180
|
)
|
|
174
181
|
elif self.exchange == 'bybit':
|
|
175
182
|
user_data_stream = BBTPrivateEventsDataStream(self, self.endpoint_ws_auth, self.exchange, _trade_id)
|
|
176
|
-
|
|
177
|
-
self.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
break
|
|
185
|
-
await asyncio.sleep(0.1)
|
|
183
|
+
else:
|
|
184
|
+
raise UserWarning(f"User Data Stream: exchange {self.exchange} not serviced")
|
|
185
|
+
|
|
186
|
+
self.data_streams[_trade_id] |= {user_data_stream}
|
|
187
|
+
|
|
188
|
+
trade_tasks = self.tasks.pop(_trade_id, set())
|
|
189
|
+
tasks_manage(trade_tasks, user_data_stream.start(), f"user_data_stream-{self.exchange}-{_trade_id}")
|
|
190
|
+
self.tasks[_trade_id] = trade_tasks
|
|
186
191
|
|
|
187
192
|
def start_market_events_listener(self, _trade_id):
|
|
188
193
|
_events = self.events.registered_streams.get(self.exchange, {}).get(_trade_id, set())
|
|
189
194
|
if self.exchange == 'binance':
|
|
190
195
|
market_data_stream = MarketEventsDataStream(self, self.endpoint_ws_public, self.exchange, _trade_id)
|
|
191
196
|
self.data_streams[_trade_id] |= {market_data_stream}
|
|
192
|
-
|
|
197
|
+
|
|
198
|
+
trade_tasks = self.tasks.pop(_trade_id, set())
|
|
199
|
+
tasks_manage(trade_tasks, market_data_stream.start(), f"market_data_stream-{self.exchange}-{_trade_id}")
|
|
200
|
+
self.tasks[_trade_id] = trade_tasks
|
|
201
|
+
|
|
193
202
|
else:
|
|
203
|
+
trade_tasks = self.tasks.pop(_trade_id, set())
|
|
194
204
|
for channel in _events:
|
|
195
205
|
# https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url
|
|
196
206
|
if self.exchange == 'okx' and 'kline' in channel:
|
|
@@ -200,18 +210,24 @@ class Client:
|
|
|
200
210
|
#
|
|
201
211
|
market_data_stream = MarketEventsDataStream(self, _endpoint, self.exchange, _trade_id, channel)
|
|
202
212
|
self.data_streams[_trade_id] |= {market_data_stream}
|
|
203
|
-
|
|
213
|
+
tasks_manage(
|
|
214
|
+
trade_tasks, market_data_stream.start(), f"market_data_stream-{self.exchange}-{channel}-{_trade_id}"
|
|
215
|
+
)
|
|
216
|
+
self.tasks[_trade_id] = trade_tasks
|
|
204
217
|
|
|
205
218
|
async def stop_events_listener(self, _trade_id):
|
|
206
219
|
logger.info(f"Stop events listener data streams for {_trade_id}")
|
|
207
220
|
stopped_data_stream = self.data_streams.pop(_trade_id, set())
|
|
208
221
|
for data_stream in stopped_data_stream:
|
|
209
222
|
await data_stream.stop()
|
|
223
|
+
if trade_tasks := self.tasks.pop(_trade_id, set()):
|
|
224
|
+
await tasks_cancel(trade_tasks, _logger=logger)
|
|
225
|
+
|
|
210
226
|
if self.user_session:
|
|
211
|
-
await self.user_session.stop()
|
|
227
|
+
await self.user_session.stop(_trade_id)
|
|
212
228
|
|
|
213
229
|
def assert_symbol_exists(self, symbol):
|
|
214
|
-
if
|
|
230
|
+
if symbol not in self.symbols:
|
|
215
231
|
raise ExchangePyError(f"Symbol {symbol} is not valid according to the loaded exchange infos")
|
|
216
232
|
|
|
217
233
|
def symbol_to_bfx(self, symbol) -> str:
|
|
@@ -261,32 +277,31 @@ class Client:
|
|
|
261
277
|
def refine_amount(self, symbol, amount: Union[str, Decimal], _quote=False):
|
|
262
278
|
if type(amount) is str: # to save time for developers
|
|
263
279
|
amount = Decimal(amount)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
280
|
+
|
|
281
|
+
precision = self.symbols[symbol]["baseAssetPrecision"]
|
|
282
|
+
lot_size_filter = self.symbols[symbol]["filters"]["LOT_SIZE"]
|
|
283
|
+
step_size = Decimal(lot_size_filter["stepSize"])
|
|
284
|
+
# noinspection PyStringFormat
|
|
285
|
+
amount = (
|
|
286
|
+
(f"%.{precision}f" % truncate(amount if _quote else (amount - amount % step_size), precision))
|
|
287
|
+
.rstrip("0")
|
|
288
|
+
.rstrip(".")
|
|
289
|
+
)
|
|
274
290
|
return amount
|
|
275
291
|
|
|
276
292
|
def refine_price(self, symbol, price: Union[str, Decimal]):
|
|
277
293
|
if isinstance(price, str): # to save time for developers
|
|
278
294
|
price = Decimal(price)
|
|
279
295
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
)
|
|
296
|
+
precision = self.symbols[symbol]["baseAssetPrecision"]
|
|
297
|
+
price_filter = self.symbols[symbol]["filters"]["PRICE_FILTER"]
|
|
298
|
+
price = price - (price % Decimal(price_filter["tickSize"]))
|
|
299
|
+
# noinspection PyStringFormat
|
|
300
|
+
price = (
|
|
301
|
+
(f"%.{precision}f" % truncate(price, precision))
|
|
302
|
+
.rstrip("0")
|
|
303
|
+
.rstrip(".")
|
|
304
|
+
)
|
|
290
305
|
return price
|
|
291
306
|
|
|
292
307
|
def assert_symbol(self, symbol):
|
|
@@ -835,7 +850,7 @@ class Client:
|
|
|
835
850
|
**params,
|
|
836
851
|
)
|
|
837
852
|
if res:
|
|
838
|
-
timeout = STATUS_TIMEOUT / 0.1
|
|
853
|
+
timeout = int(STATUS_TIMEOUT / 0.1)
|
|
839
854
|
while not self.active_orders.get(int(res)) and timeout:
|
|
840
855
|
timeout -= 1
|
|
841
856
|
await asyncio.sleep(0.1)
|
|
@@ -908,7 +923,7 @@ class Client:
|
|
|
908
923
|
)
|
|
909
924
|
if b_res is None:
|
|
910
925
|
fallback_warning(self.exchange, symbol)
|
|
911
|
-
b_res =
|
|
926
|
+
b_res = await self.http.send_api_call(
|
|
912
927
|
ORDER_ENDPOINT,
|
|
913
928
|
params=params,
|
|
914
929
|
signed=True,
|
|
@@ -1030,7 +1045,7 @@ class Client:
|
|
|
1030
1045
|
**params
|
|
1031
1046
|
)
|
|
1032
1047
|
if res and isinstance(res, list) and res[6] == 'SUCCESS':
|
|
1033
|
-
timeout = STATUS_TIMEOUT / 0.1
|
|
1048
|
+
timeout = int(STATUS_TIMEOUT / 0.1)
|
|
1034
1049
|
while timeout:
|
|
1035
1050
|
timeout -= 1
|
|
1036
1051
|
if self.active_orders.get(order_id, {}).get('cancelled', False):
|
|
@@ -1046,7 +1061,7 @@ class Client:
|
|
|
1046
1061
|
signed=True
|
|
1047
1062
|
)
|
|
1048
1063
|
if res:
|
|
1049
|
-
timeout = STATUS_TIMEOUT / 0.1
|
|
1064
|
+
timeout = int(STATUS_TIMEOUT / 0.1)
|
|
1050
1065
|
while not self.active_orders.get(order_id, {}).get('cancelled', False) and timeout:
|
|
1051
1066
|
timeout -= 1
|
|
1052
1067
|
await asyncio.sleep(0.1)
|
|
@@ -1182,7 +1197,7 @@ class Client:
|
|
|
1182
1197
|
signed=True,
|
|
1183
1198
|
data=params,
|
|
1184
1199
|
)
|
|
1185
|
-
ids_canceled = [int(
|
|
1200
|
+
ids_canceled = [int(order['ordId']) for order in res if order['sCode'] == '0']
|
|
1186
1201
|
orders_canceled[:] = [i for i in orders_canceled if i['orderId'] in ids_canceled]
|
|
1187
1202
|
binance_res.extend(orders_canceled)
|
|
1188
1203
|
elif self.exchange == 'bybit':
|
|
@@ -8,6 +8,8 @@ import grpclib.exceptions
|
|
|
8
8
|
from exchanges_wrapper import __version__ as VER_EW
|
|
9
9
|
# noinspection PyPep8Naming
|
|
10
10
|
from crypto_ws_api import __version__ as VER_CW
|
|
11
|
+
from crypto_ws_api.ws_session import set_logger
|
|
12
|
+
|
|
11
13
|
import time
|
|
12
14
|
import weakref
|
|
13
15
|
import gc
|
|
@@ -16,8 +18,9 @@ import asyncio
|
|
|
16
18
|
import functools
|
|
17
19
|
# noinspection PyPackageRequirements
|
|
18
20
|
import ujson as json
|
|
19
|
-
import logging
|
|
21
|
+
import logging
|
|
20
22
|
from decimal import Decimal
|
|
23
|
+
import ctypes, ctypes.util
|
|
21
24
|
|
|
22
25
|
import exchanges_wrapper.martin as mr
|
|
23
26
|
from exchanges_wrapper import WORK_PATH, LOG_FILE, errors, Server, Status, GRPCError, graceful_exit
|
|
@@ -29,32 +32,24 @@ from exchanges_wrapper.lib import (
|
|
|
29
32
|
REST_RATE_LIMIT_INTERVAL,
|
|
30
33
|
FILTER_TYPE_MAP,
|
|
31
34
|
)
|
|
32
|
-
from exchanges_wrapper.martin import
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
from exchanges_wrapper.martin import (
|
|
36
|
+
StreamResponse,
|
|
37
|
+
SimpleResponse,
|
|
38
|
+
OnKlinesUpdateResponse,
|
|
39
|
+
OnTickerUpdateResponse,
|
|
40
|
+
FetchOrderBookResponse,
|
|
41
|
+
)
|
|
35
42
|
#
|
|
36
43
|
HEARTBEAT = 1 # sec
|
|
37
44
|
MAX_QUEUE_SIZE = 100
|
|
38
45
|
WSS_TICKER_TIMEOUT = 600 # sec
|
|
39
46
|
#
|
|
40
|
-
logger = logging.
|
|
41
|
-
formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
|
|
42
|
-
#
|
|
43
|
-
fh = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1000000, backupCount=10)
|
|
44
|
-
fh.setFormatter(formatter)
|
|
45
|
-
fh.setLevel(logging.DEBUG)
|
|
46
|
-
#
|
|
47
|
-
sh = logging.StreamHandler()
|
|
48
|
-
sh.setFormatter(formatter)
|
|
49
|
-
sh.setLevel(logging.INFO)
|
|
50
|
-
#
|
|
51
|
-
root_logger = logging.getLogger()
|
|
52
|
-
root_logger.setLevel(min([fh.level, sh.level]))
|
|
53
|
-
root_logger.addHandler(fh)
|
|
54
|
-
root_logger.addHandler(sh)
|
|
55
|
-
|
|
56
|
-
logging.basicConfig()
|
|
47
|
+
logger = set_logger(__name__, LOG_FILE, file_level=logging.DEBUG, set_root_logger=True)
|
|
57
48
|
logging.getLogger('hpack').setLevel(logging.INFO)
|
|
49
|
+
logging.getLogger('grpclib.protocol').setLevel(logging.INFO)
|
|
50
|
+
|
|
51
|
+
def malloc_trim(trim_type: int = 0):
|
|
52
|
+
ctypes.CDLL(ctypes.util.find_library('c')).malloc_trim(trim_type)
|
|
58
53
|
|
|
59
54
|
|
|
60
55
|
class OpenClient:
|
|
@@ -90,9 +85,9 @@ class OpenClient:
|
|
|
90
85
|
return res
|
|
91
86
|
|
|
92
87
|
@classmethod
|
|
93
|
-
def remove_client(cls,
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
def remove_client(cls, _id):
|
|
89
|
+
# noinspection PyTypeHints
|
|
90
|
+
cls.open_clients[:] = [i for i in cls.open_clients if id(i) != _id]
|
|
96
91
|
|
|
97
92
|
# noinspection PyPep8Naming,PyMethodMayBeStatic
|
|
98
93
|
class Martin(mr.MartinBase):
|
|
@@ -139,8 +134,8 @@ class Martin(mr.MartinBase):
|
|
|
139
134
|
try:
|
|
140
135
|
await asyncio.wait_for(open_client.client.load(request.symbol), timeout=HEARTBEAT * 60)
|
|
141
136
|
except asyncio.exceptions.TimeoutError:
|
|
142
|
-
await OpenClient.get_client(client_id).client.http.
|
|
143
|
-
OpenClient.remove_client(
|
|
137
|
+
await OpenClient.get_client(client_id).client.http.close_session()
|
|
138
|
+
OpenClient.remove_client(client_id)
|
|
144
139
|
raise GRPCError(status=Status.UNAVAILABLE, message=f"'{open_client.name}' timeout error")
|
|
145
140
|
except Exception as ex:
|
|
146
141
|
logger.warning(f"OpenClientConnection for '{open_client.name}' exception: {ex}")
|
|
@@ -227,7 +222,7 @@ class Martin(mr.MartinBase):
|
|
|
227
222
|
server_time = res.get('serverTime')
|
|
228
223
|
return mr.FetchServerTimeResponse(server_time=server_time)
|
|
229
224
|
|
|
230
|
-
async def one_click_arrival_deposit(self, request: mr.MarketRequest) -> mr.SimpleResponse
|
|
225
|
+
async def one_click_arrival_deposit(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
231
226
|
res, _, _ = await self.send_request('one_click_arrival_deposit', request, tx_id=request.symbol)
|
|
232
227
|
return mr.SimpleResponse(success=True, result=json.dumps(str(res)))
|
|
233
228
|
|
|
@@ -254,7 +249,7 @@ class Martin(mr.MartinBase):
|
|
|
254
249
|
|
|
255
250
|
async def fetch_order(self, request: mr.FetchOrderRequest) -> mr.FetchOrderResponse:
|
|
256
251
|
response = mr.FetchOrderResponse()
|
|
257
|
-
res,
|
|
252
|
+
res, client, msg_header = await self.send_request(
|
|
258
253
|
'fetch_order',
|
|
259
254
|
request,
|
|
260
255
|
rate_limit=True,
|
|
@@ -265,14 +260,15 @@ class Martin(mr.MartinBase):
|
|
|
265
260
|
)
|
|
266
261
|
logger.debug(f"{msg_header}: {res}")
|
|
267
262
|
|
|
268
|
-
if
|
|
269
|
-
request.
|
|
270
|
-
|
|
263
|
+
if _queue := client.on_order_update_queues.get(request.trade_id):
|
|
264
|
+
if res and request.filled_update_call and Decimal(res['executedQty']):
|
|
265
|
+
request.order_id = res['orderId']
|
|
266
|
+
await self.create_trade_stream_event(request, res, _queue)
|
|
271
267
|
response.from_pydict(res)
|
|
272
268
|
return response
|
|
273
269
|
|
|
274
|
-
async def create_trade_stream_event(self, request, order):
|
|
275
|
-
trades,
|
|
270
|
+
async def create_trade_stream_event(self, request, order, _queue):
|
|
271
|
+
trades, _, msg_header = await self.send_request(
|
|
276
272
|
'fetch_order_trade_list',
|
|
277
273
|
request,
|
|
278
274
|
trade_id=request.trade_id,
|
|
@@ -280,8 +276,6 @@ class Martin(mr.MartinBase):
|
|
|
280
276
|
order_id=request.order_id
|
|
281
277
|
)
|
|
282
278
|
|
|
283
|
-
_queue = client.on_order_update_queues.get(request.trade_id)
|
|
284
|
-
|
|
285
279
|
for trade in trades:
|
|
286
280
|
trade['updateTime'] = trade.pop('time')
|
|
287
281
|
trade |= {
|
|
@@ -297,7 +291,7 @@ class Martin(mr.MartinBase):
|
|
|
297
291
|
await _queue.put(weakref.ref(event)())
|
|
298
292
|
logger.debug(f"{msg_header}: {trades}")
|
|
299
293
|
|
|
300
|
-
async def cancel_all_orders(self, request: mr.MarketRequest) -> mr.SimpleResponse
|
|
294
|
+
async def cancel_all_orders(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
301
295
|
response = mr.SimpleResponse()
|
|
302
296
|
|
|
303
297
|
res, _, _ = await self.send_request(
|
|
@@ -525,10 +519,12 @@ class Martin(mr.MartinBase):
|
|
|
525
519
|
_event_type = f"{_symbol}@miniTicker"
|
|
526
520
|
client.events.register_event(functools.partial(event_handler, _queue, client, request.trade_id, _event_type),
|
|
527
521
|
_event_type, client.exchange, request.trade_id)
|
|
522
|
+
Martin.ticker_update_time[request.trade_id] = time.time()
|
|
528
523
|
while True:
|
|
529
524
|
_event = await _queue.get()
|
|
530
525
|
if isinstance(_event, str) and _event == request.trade_id:
|
|
531
526
|
client.stream_queue.get(request.trade_id, set()).discard(_queue)
|
|
527
|
+
Martin.ticker_update_time.pop(request.trade_id, None)
|
|
532
528
|
logger.info(f"OnTickerUpdate: Stop loop for {open_client.name}: {request.symbol}")
|
|
533
529
|
return
|
|
534
530
|
else:
|
|
@@ -754,7 +750,7 @@ class Martin(mr.MartinBase):
|
|
|
754
750
|
)
|
|
755
751
|
logger.info(f"Start WS streams for {open_client.name}")
|
|
756
752
|
client.start_market_events_listener(request.trade_id)
|
|
757
|
-
|
|
753
|
+
client.start_user_events_listener(request.trade_id, request.symbol)
|
|
758
754
|
response.success = True
|
|
759
755
|
return response
|
|
760
756
|
|
|
@@ -763,37 +759,43 @@ class Martin(mr.MartinBase):
|
|
|
763
759
|
if open_client := OpenClient.get_client(request.client_id):
|
|
764
760
|
client = open_client.client
|
|
765
761
|
logger.info(f"StopStream request for {request.symbol} on {client.exchange}")
|
|
766
|
-
await
|
|
762
|
+
await stop_stream_ex(client, request.trade_id)
|
|
767
763
|
response.success = True
|
|
768
764
|
else:
|
|
769
765
|
response.success = False
|
|
770
766
|
return response
|
|
771
767
|
|
|
772
768
|
async def check_stream(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
773
|
-
|
|
774
|
-
check_time = time.time() -
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
769
|
+
last_update = Martin.ticker_update_time.get(request.trade_id, 0)
|
|
770
|
+
check_time = time.time() - last_update
|
|
771
|
+
success = check_time < WSS_TICKER_TIMEOUT
|
|
772
|
+
response = mr.SimpleResponse(success=success)
|
|
773
|
+
if not success:
|
|
774
|
+
Martin.ticker_update_time.pop(request.trade_id, None)
|
|
779
775
|
logger.warning(f"CheckStream request failed for {request.trade_id}")
|
|
780
776
|
return response
|
|
781
777
|
|
|
782
778
|
async def client_restart(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
779
|
+
await self.stop_stream(request)
|
|
780
|
+
if client := OpenClient.get_client(request.client_id).client:
|
|
781
|
+
if user_session := client.user_session:
|
|
782
|
+
await user_session.stop(request.trade_id)
|
|
783
|
+
if session := client.http:
|
|
784
|
+
await session.close_session()
|
|
785
|
+
OpenClient.remove_client(request.client_id)
|
|
786
786
|
return mr.SimpleResponse(success=True)
|
|
787
787
|
|
|
788
788
|
|
|
789
|
-
async def
|
|
789
|
+
async def stop_stream_ex(client, trade_id):
|
|
790
790
|
await client.stop_events_listener(trade_id)
|
|
791
791
|
client.events.unregister(client.exchange, trade_id)
|
|
792
792
|
[await _queue.put(trade_id) for _queue in client.stream_queue.get(trade_id, [])]
|
|
793
793
|
await asyncio.sleep(0)
|
|
794
794
|
client.on_order_update_queues.pop(trade_id, None)
|
|
795
795
|
client.stream_queue.pop(trade_id, None)
|
|
796
|
+
Martin.ticker_update_time.pop(trade_id, None)
|
|
796
797
|
gc.collect(generation=2)
|
|
798
|
+
malloc_trim()
|
|
797
799
|
|
|
798
800
|
|
|
799
801
|
async def event_handler(_queue, client, trade_id, _event_type, event):
|
|
@@ -802,10 +804,7 @@ async def event_handler(_queue, client, trade_id, _event_type, event):
|
|
|
802
804
|
except asyncio.QueueFull:
|
|
803
805
|
logger.warning(f"For {_event_type} asyncio queue full and wold be closed")
|
|
804
806
|
client.stream_queue.get(trade_id, set()).discard(_queue)
|
|
805
|
-
await
|
|
806
|
-
global MAX_QUEUE_SIZE
|
|
807
|
-
MAX_QUEUE_SIZE += int(MAX_QUEUE_SIZE / 10)
|
|
808
|
-
logger.info(f"MAX_QUEUE_SIZE was updated: new value is {MAX_QUEUE_SIZE}")
|
|
807
|
+
await stop_stream_ex(client, trade_id)
|
|
809
808
|
|
|
810
809
|
|
|
811
810
|
def is_port_in_use(port: int) -> bool:
|
|
@@ -825,8 +824,8 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
|
|
|
825
824
|
await server.wait_closed()
|
|
826
825
|
|
|
827
826
|
for oc in OpenClient.open_clients:
|
|
828
|
-
if oc.client.http
|
|
829
|
-
await oc.client.http.
|
|
827
|
+
if oc.client.http:
|
|
828
|
+
await oc.client.http.close_session()
|
|
830
829
|
|
|
831
830
|
[task.cancel() for task in asyncio.all_tasks() if not task.done() and task is not asyncio.current_task()]
|
|
832
831
|
|
|
@@ -836,8 +835,8 @@ def main():
|
|
|
836
835
|
asyncio.run(amain())
|
|
837
836
|
except grpclib.exceptions.StreamTerminatedError:
|
|
838
837
|
pass # Task cancellation should not be logged as an error
|
|
839
|
-
except Exception as
|
|
840
|
-
print(f"Exception: {
|
|
838
|
+
except Exception as expt:
|
|
839
|
+
print(f"Exception: {expt}")
|
|
841
840
|
print(traceback.format_exc())
|
|
842
841
|
|
|
843
842
|
|
|
@@ -29,6 +29,21 @@ ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW = "Timestamp for this request is outside of th
|
|
|
29
29
|
TIMEOUT = aiohttp.ClientTimeout(total=60)
|
|
30
30
|
|
|
31
31
|
class HttpClient:
|
|
32
|
+
__slots__ = (
|
|
33
|
+
'api_key',
|
|
34
|
+
'api_secret',
|
|
35
|
+
'passphrase',
|
|
36
|
+
'endpoint',
|
|
37
|
+
'exchange',
|
|
38
|
+
'test_net',
|
|
39
|
+
'rate_limit_reached',
|
|
40
|
+
'rest_cycle_lock',
|
|
41
|
+
'session',
|
|
42
|
+
'_session_mutex',
|
|
43
|
+
'ex_imps',
|
|
44
|
+
'rate_limit_handler'
|
|
45
|
+
)
|
|
46
|
+
|
|
32
47
|
def __init__(self, params: dict):
|
|
33
48
|
self.api_key = params['api_key']
|
|
34
49
|
self.api_secret = params['api_secret']
|
|
@@ -54,10 +69,15 @@ class HttpClient:
|
|
|
54
69
|
}
|
|
55
70
|
|
|
56
71
|
async def _create_session_if_required(self):
|
|
57
|
-
if
|
|
72
|
+
if not self.session:
|
|
58
73
|
async with self._session_mutex:
|
|
59
74
|
self.session = aiohttp.ClientSession(trust_env=True, timeout=TIMEOUT)
|
|
60
75
|
|
|
76
|
+
async def close_session(self):
|
|
77
|
+
if self.session:
|
|
78
|
+
await self.session.close()
|
|
79
|
+
self.session = None
|
|
80
|
+
|
|
61
81
|
async def handle_errors(self, response, path=None):
|
|
62
82
|
if response.status >= 500:
|
|
63
83
|
raise ExchangeError(
|
|
@@ -141,13 +161,12 @@ class HttpClient:
|
|
|
141
161
|
await self._create_session_if_required()
|
|
142
162
|
try:
|
|
143
163
|
async with self.session.request(method, url, timeout=timeout, **query_kwargs) as response:
|
|
144
|
-
|
|
145
164
|
if self.exchange == 'bybit':
|
|
146
165
|
self.rate_limit_handler.update(path, response.headers)
|
|
147
166
|
|
|
148
167
|
return await self.handle_errors(response, path)
|
|
149
168
|
except (aiohttp.ClientConnectionError, asyncio.exceptions.TimeoutError):
|
|
150
|
-
await self.
|
|
169
|
+
await self.close_session()
|
|
151
170
|
raise ExchangeError("HTTP ClientConnectionError, the connection will be restored")
|
|
152
171
|
|
|
153
172
|
async def _binance_request(self, path, method, signed, send_api_key, endpoint, timeout, **kwargs):
|
|
@@ -25,7 +25,7 @@ FILTER_TYPE_MAP = {
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class OrderTradesEvent:
|
|
28
|
-
def __init__(self, event_data:
|
|
28
|
+
def __init__(self, event_data: dict):
|
|
29
29
|
self.symbol = event_data["symbol"]
|
|
30
30
|
self.client_order_id = event_data["clientOrderId"]
|
|
31
31
|
self.side = "BUY" if event_data["isBuyer"] else "SELL"
|
|
@@ -15,7 +15,7 @@ def fetch_server_time(res: list) -> dict | None:
|
|
|
15
15
|
return None
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def exchange_info(server_time: int, trading_symbol: list, tickers: list, symbol_t) ->
|
|
18
|
+
def exchange_info(server_time: int, trading_symbol: list, tickers: list, symbol_t) -> dict:
|
|
19
19
|
symbols = []
|
|
20
20
|
symbols_price = {}
|
|
21
21
|
for pair in tickers:
|
|
@@ -98,7 +98,7 @@ def orders(res: list, response_type=None) -> list:
|
|
|
98
98
|
return binance_orders
|
|
99
99
|
|
|
100
100
|
|
|
101
|
-
def order(res:
|
|
101
|
+
def order(res: dict, response_type=None) -> dict:
|
|
102
102
|
symbol = res.get('instId').replace('-', '')
|
|
103
103
|
order_id = int(res.get('ordId'))
|
|
104
104
|
order_list_id = -1
|
|
@@ -186,7 +186,7 @@ def order(res: {}, response_type=None) -> {}:
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
|
|
189
|
-
def place_order_response(res:
|
|
189
|
+
def place_order_response(res: dict, req: dict) -> dict:
|
|
190
190
|
return {
|
|
191
191
|
"symbol": req["instId"].replace('-', ''),
|
|
192
192
|
"orderId": int(res["ordId"]),
|
|
@@ -230,7 +230,7 @@ def order_book(res: dict) -> Dict[str, Union[int, List[List[str]]]]:
|
|
|
230
230
|
return binance_order_book
|
|
231
231
|
|
|
232
232
|
|
|
233
|
-
def ticker_price_change_statistics(res:
|
|
233
|
+
def ticker_price_change_statistics(res: dict) -> dict:
|
|
234
234
|
price_change = str(Decimal(res.get('last')) - Decimal(res.get('open24h')))
|
|
235
235
|
price_change_percent = str(100 * (Decimal(res.get('last')) - Decimal(res.get('open24h'))) /
|
|
236
236
|
Decimal(res.get('open24h')))
|
|
@@ -263,14 +263,14 @@ def ticker_price_change_statistics(res: {}) -> {}:
|
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
|
|
266
|
-
def fetch_symbol_price_ticker(res:
|
|
266
|
+
def fetch_symbol_price_ticker(res: dict, symbol) -> dict:
|
|
267
267
|
return {
|
|
268
268
|
"symbol": symbol,
|
|
269
269
|
"price": res.get('last')
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
|
|
273
|
-
def ticker(res:
|
|
273
|
+
def ticker(res: dict) -> dict:
|
|
274
274
|
symbol = res.get('instId').replace('-', '')
|
|
275
275
|
return {
|
|
276
276
|
'stream': f"{symbol.lower()}@miniTicker",
|
|
@@ -344,7 +344,7 @@ def interval2value(_interval: str) -> int:
|
|
|
344
344
|
return resolution.get(_interval, 0)
|
|
345
345
|
|
|
346
346
|
|
|
347
|
-
def candle(res: list, symbol: str = None, ch_type: str = None) ->
|
|
347
|
+
def candle(res: list, symbol: str = None, ch_type: str = None) -> dict:
|
|
348
348
|
symbol = symbol.replace('-', '').lower()
|
|
349
349
|
start_time = int(res[0])
|
|
350
350
|
_interval = ch_type.replace('kline_', '')
|
|
@@ -378,7 +378,7 @@ def candle(res: list, symbol: str = None, ch_type: str = None) -> {}:
|
|
|
378
378
|
}
|
|
379
379
|
|
|
380
380
|
|
|
381
|
-
def order_book_ws(res:
|
|
381
|
+
def order_book_ws(res: dict, symbol: str) -> dict:
|
|
382
382
|
symbol = symbol.replace('-', '').lower()
|
|
383
383
|
return {
|
|
384
384
|
'stream': f"{symbol}@depth5",
|
|
@@ -386,7 +386,7 @@ def order_book_ws(res: {}, symbol: str) -> {}:
|
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
|
|
389
|
-
def on_funds_update(res:
|
|
389
|
+
def on_funds_update(res: dict) -> dict:
|
|
390
390
|
event_time = int(time.time() * 1000)
|
|
391
391
|
data = res.get('details')
|
|
392
392
|
funds = []
|
|
@@ -408,7 +408,7 @@ def on_funds_update(res: {}) -> {}:
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
|
|
411
|
-
def on_order_update(res:
|
|
411
|
+
def on_order_update(res: dict) -> dict:
|
|
412
412
|
# print(f"on_order_update.res: {res}")
|
|
413
413
|
order_quantity = res.get('sz')
|
|
414
414
|
order_price = res.get('px')
|
|
@@ -464,7 +464,7 @@ def on_order_update(res: {}) -> {}:
|
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
|
|
467
|
-
def on_balance_update(res: list, buffer: dict, transfer: bool) ->
|
|
467
|
+
def on_balance_update(res: list, buffer: dict, transfer: bool) -> tuple:
|
|
468
468
|
res_diff = []
|
|
469
469
|
for i in res:
|
|
470
470
|
asset = i.get('ccy')
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import asyncio
|
|
3
|
+
import gc
|
|
4
|
+
import logging
|
|
5
|
+
|
|
3
6
|
# noinspection PyPackageRequirements
|
|
4
7
|
import ujson as json
|
|
5
|
-
import logging.handlers
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
import time
|
|
8
10
|
from decimal import Decimal
|
|
@@ -18,28 +20,36 @@ import exchanges_wrapper.parsers.bitfinex as bfx
|
|
|
18
20
|
import exchanges_wrapper.parsers.huobi as hbp
|
|
19
21
|
import exchanges_wrapper.parsers.okx as okx
|
|
20
22
|
import exchanges_wrapper.parsers.bybit as bbt
|
|
21
|
-
from crypto_ws_api.ws_session import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
sh.setFormatter(formatter)
|
|
33
|
-
sh.setLevel(logging.INFO)
|
|
34
|
-
|
|
35
|
-
logger.addHandler(fh)
|
|
36
|
-
logger.addHandler(sh)
|
|
37
|
-
logger.propagate = False
|
|
38
|
-
|
|
23
|
+
from crypto_ws_api.ws_session import (
|
|
24
|
+
set_logger,
|
|
25
|
+
generate_signature,
|
|
26
|
+
compose_htx_ws_auth,
|
|
27
|
+
compose_binance_ws_auth,
|
|
28
|
+
tasks_manage,
|
|
29
|
+
tasks_cancel
|
|
30
|
+
)
|
|
31
|
+
from exchanges_wrapper import LOG_PATH, DEBUG_LOG
|
|
32
|
+
|
|
33
|
+
logger = set_logger(__name__, Path(LOG_PATH, 'websockets.log'))
|
|
39
34
|
sys.tracebacklimit = 0
|
|
40
35
|
|
|
41
36
|
|
|
42
37
|
class EventsDataStream:
|
|
38
|
+
__slots__ = (
|
|
39
|
+
'client',
|
|
40
|
+
'endpoint',
|
|
41
|
+
'exchange',
|
|
42
|
+
'trade_id',
|
|
43
|
+
'websocket',
|
|
44
|
+
'try_count',
|
|
45
|
+
'wss_event_buffer',
|
|
46
|
+
'_order_book',
|
|
47
|
+
'_price',
|
|
48
|
+
'tasks',
|
|
49
|
+
'ping',
|
|
50
|
+
'logger'
|
|
51
|
+
)
|
|
52
|
+
|
|
43
53
|
def __init__(self, client, endpoint, exchange, trade_id):
|
|
44
54
|
self.client = client
|
|
45
55
|
self.endpoint = endpoint
|
|
@@ -51,51 +61,54 @@ class EventsDataStream:
|
|
|
51
61
|
self._order_book = None
|
|
52
62
|
self._price = None
|
|
53
63
|
self.tasks = set()
|
|
54
|
-
self.wss_started = False
|
|
55
64
|
self.ping = 0
|
|
65
|
+
self.logger = logger
|
|
66
|
+
|
|
67
|
+
if exchange == DEBUG_LOG:
|
|
68
|
+
log_key = f"web_s_{trade_id}"
|
|
69
|
+
if log_key in logging.root.manager.loggerDict:
|
|
70
|
+
self.logger = logging.root.manager.loggerDict[log_key]
|
|
71
|
+
else:
|
|
72
|
+
self.logger = set_logger(log_key, Path(LOG_PATH, f"{log_key}.log"), logging.DEBUG)
|
|
56
73
|
|
|
57
74
|
async def start(self):
|
|
58
75
|
async for self.websocket in connect(
|
|
59
76
|
self.endpoint,
|
|
60
|
-
logger=logger,
|
|
77
|
+
logger=self.logger,
|
|
61
78
|
ping_interval=None if self.exchange in ('binance', 'huobi') else 20
|
|
62
79
|
):
|
|
80
|
+
await self.cycle_init()
|
|
63
81
|
start_time = datetime.now(timezone.utc).replace(tzinfo=None)
|
|
64
82
|
try:
|
|
65
83
|
await self.start_wss()
|
|
66
84
|
except ConnectionClosed as ex:
|
|
85
|
+
self.websocket = None
|
|
67
86
|
ct = str(datetime.now(timezone.utc).replace(tzinfo=None) - start_time).rsplit('.')[0]
|
|
68
|
-
logger.info(f"WSS life time for {self.exchange} is {ct}")
|
|
69
|
-
self.tasks_cancel()
|
|
87
|
+
self.logger.info(f"WSS life time for {self.exchange}:{self.trade_id} is {ct}")
|
|
70
88
|
if ex.rcvd and ex.rcvd.code == 4000:
|
|
71
|
-
logger.info(f"WSS closed for {self.exchange}:{self.trade_id}")
|
|
89
|
+
self.logger.info(f"WSS closed for {self.exchange}:{self.trade_id}")
|
|
72
90
|
break
|
|
73
91
|
else:
|
|
74
|
-
logger.warning(f"Restart WSS for {self.exchange}: {ex}")
|
|
92
|
+
self.logger.warning(f"Restart WSS for {self.exchange}: {ex}")
|
|
75
93
|
continue
|
|
76
94
|
except Exception as ex:
|
|
77
|
-
self.
|
|
78
|
-
logger.error(f"WSS start() other exception: {ex}")
|
|
95
|
+
self.logger.error(f"WSS start() other exception: {ex}")
|
|
79
96
|
|
|
80
97
|
async def start_wss(self):
|
|
81
98
|
pass # meant to be overridden in a subclass
|
|
82
99
|
|
|
100
|
+
async def cycle_init(self):
|
|
101
|
+
await tasks_cancel(self.tasks, _logger=logger)
|
|
102
|
+
self.wss_event_buffer.clear()
|
|
103
|
+
gc.collect()
|
|
104
|
+
|
|
83
105
|
async def stop(self):
|
|
84
106
|
"""
|
|
85
107
|
Stop data stream
|
|
86
108
|
"""
|
|
87
|
-
self.tasks_cancel()
|
|
88
109
|
if self.websocket:
|
|
89
110
|
await self.websocket.close(code=4000)
|
|
90
|
-
|
|
91
|
-
def tasks_cancel(self):
|
|
92
|
-
[task.cancel() for task in self.tasks if not task.done()]
|
|
93
|
-
self.tasks.clear()
|
|
94
|
-
|
|
95
|
-
def tasks_manage(self, coro):
|
|
96
|
-
_t = asyncio.create_task(coro)
|
|
97
|
-
self.tasks.add(_t)
|
|
98
|
-
_t.add_done_callback(self.tasks.discard)
|
|
111
|
+
await self.cycle_init()
|
|
99
112
|
|
|
100
113
|
async def _handle_event(self, *args):
|
|
101
114
|
pass # meant to be overridden in a subclass
|
|
@@ -109,8 +122,11 @@ class EventsDataStream:
|
|
|
109
122
|
await self._handle_event(event)
|
|
110
123
|
elif msg_data.get("status") == 200:
|
|
111
124
|
result = msg_data.get("result")
|
|
112
|
-
if isinstance(result, dict) and
|
|
113
|
-
self.
|
|
125
|
+
if isinstance(result, dict) and 'authorizedSince' in result:
|
|
126
|
+
self.logger.info(f"Binance User Data Stream started for {self.trade_id}")
|
|
127
|
+
else:
|
|
128
|
+
self.logger.warning(f"Reconnecting Binance User Data Stream for {self.trade_id}, msg_data: {msg_data}")
|
|
129
|
+
raise ConnectionClosed(None, None)
|
|
114
130
|
elif self.exchange == 'bybit':
|
|
115
131
|
if _data := msg_data.get('data'):
|
|
116
132
|
if ch_type == 'depth5':
|
|
@@ -127,15 +143,17 @@ class EventsDataStream:
|
|
|
127
143
|
return
|
|
128
144
|
elif ((msg_data.get("ret_msg") == "subscribe" or msg_data.get("op") in ("auth", "subscribe"))
|
|
129
145
|
and msg_data.get("success")):
|
|
130
|
-
|
|
146
|
+
tasks_manage(
|
|
147
|
+
self.tasks, self.bybit_heartbeat(ch_type or "private"), f"bybit_heartbeat-{symbol}-{ch_type}"
|
|
148
|
+
)
|
|
131
149
|
if msg_data["op"] == "subscribe" and msg_data["success"] and not msg_data["ret_msg"]:
|
|
132
|
-
self.
|
|
150
|
+
self.logger.info(f"ByBit User Data Stream started for {self.trade_id}")
|
|
133
151
|
elif ((msg_data.get("ret_msg") == "subscribe" or msg_data.get("op") in ("auth", "subscribe"))
|
|
134
152
|
and not msg_data.get("success")):
|
|
135
|
-
logger.warning(f"Reconnecting ByBit WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
|
|
153
|
+
self.logger.warning(f"Reconnecting ByBit WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
|
|
136
154
|
raise ConnectionClosed(None, None)
|
|
137
155
|
else:
|
|
138
|
-
logger.info(f"ByBit undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
|
|
156
|
+
self.logger.info(f"ByBit undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
|
|
139
157
|
elif self.exchange == 'okx':
|
|
140
158
|
if (not ch_type and
|
|
141
159
|
msg_data.get('arg', {}).get('channel') in ('account', 'orders', 'balance_and_position')
|
|
@@ -146,46 +164,45 @@ class EventsDataStream:
|
|
|
146
164
|
elif msg_data.get("event") == "login" and msg_data.get("code") == "0":
|
|
147
165
|
return
|
|
148
166
|
elif msg_data.get("event") == "subscribe" and msg_data.get('arg', {}).get('channel') == 'orders':
|
|
149
|
-
self.
|
|
167
|
+
self.logger.info(f"OKX User Data Stream started for {self.trade_id}")
|
|
150
168
|
elif msg_data.get("event") in ("login", "error") and msg_data.get("code") != "0":
|
|
151
|
-
logger.warning(f"Reconnecting OKX WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
|
|
169
|
+
self.logger.warning(f"Reconnecting OKX WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
|
|
152
170
|
raise ConnectionClosed(None, None)
|
|
153
171
|
else:
|
|
154
|
-
logger.debug(f"OKX undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
|
|
172
|
+
self.logger.debug(f"OKX undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
|
|
155
173
|
elif self.exchange == 'bitfinex':
|
|
156
174
|
# info and error handling
|
|
157
175
|
if isinstance(msg_data, dict):
|
|
158
176
|
if msg_data.get('version') and msg_data.get('version') != 2:
|
|
159
|
-
logger.critical('Change WSS version detected')
|
|
177
|
+
self.logger.critical('Change WSS version detected')
|
|
160
178
|
if msg_data.get('platform') and msg_data.get('platform').get('status') != 1:
|
|
161
|
-
logger.warning(f"Exchange in maintenance mode, trying reconnect. Exchange info: {msg}")
|
|
179
|
+
self.logger.warning(f"Exchange in maintenance mode, trying reconnect. Exchange info: {msg}")
|
|
162
180
|
await asyncio.sleep(60)
|
|
163
181
|
raise ConnectionClosed(None, None)
|
|
164
182
|
elif 'code' in msg_data:
|
|
165
183
|
code = msg_data.get('code')
|
|
166
184
|
if code == 10300:
|
|
167
|
-
logger.warning('WSS Subscription failed (generic)')
|
|
185
|
+
self.logger.warning('WSS Subscription failed (generic)')
|
|
168
186
|
raise ConnectionClosed(None, None)
|
|
169
187
|
elif code == 10301:
|
|
170
|
-
logger.error('WSS Already subscribed')
|
|
188
|
+
self.logger.error('WSS Already subscribed')
|
|
171
189
|
elif code == 10302:
|
|
172
190
|
raise UserWarning(f"WSS Unknown channel {ch_type}")
|
|
173
191
|
elif code == 10305:
|
|
174
192
|
raise UserWarning('WSS Reached limit of open channels')
|
|
175
193
|
elif code == 20051:
|
|
176
|
-
logger.warning('WSS reconnection request received from exchange')
|
|
194
|
+
self.logger.warning('WSS reconnection request received from exchange')
|
|
177
195
|
raise ConnectionClosed(None, None)
|
|
178
196
|
elif code == 20060:
|
|
179
|
-
logger.info('WSS entering in maintenance mode, trying reconnect after 120s')
|
|
197
|
+
self.logger.info('WSS entering in maintenance mode, trying reconnect after 120s')
|
|
180
198
|
await asyncio.sleep(120)
|
|
181
199
|
raise ConnectionClosed(None, None)
|
|
182
200
|
elif msg_data.get('event') == 'subscribed':
|
|
183
201
|
chan_id = msg_data.get('chanId')
|
|
184
|
-
logger.info(f"bitfinex, ch_type: {ch_type}, chan_id: {chan_id}")
|
|
202
|
+
self.logger.info(f"bitfinex, ch_type: {ch_type}, chan_id: {chan_id}")
|
|
185
203
|
elif msg_data.get('event') == 'auth' and msg_data.get('status') == 'OK':
|
|
186
204
|
chan_id = msg_data.get('chanId')
|
|
187
|
-
self.
|
|
188
|
-
logger.info(f"bitfinex, user stream chan_id: {chan_id}")
|
|
205
|
+
self.logger.info(f"Bitfinex, user stream chan_id: {chan_id}")
|
|
189
206
|
|
|
190
207
|
# data handling
|
|
191
208
|
elif isinstance(msg_data, list) and len(msg_data) == 2 and msg_data[1] == 'hb':
|
|
@@ -196,7 +213,7 @@ class EventsDataStream:
|
|
|
196
213
|
else:
|
|
197
214
|
await self._handle_event(msg_data, symbol, ch_type)
|
|
198
215
|
else:
|
|
199
|
-
logger.debug(f"Bitfinex undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
|
|
216
|
+
self.logger.debug(f"Bitfinex undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
|
|
200
217
|
elif self.exchange == 'huobi':
|
|
201
218
|
if ping := msg_data.get('ping'):
|
|
202
219
|
self.ping = 0
|
|
@@ -222,16 +239,16 @@ class EventsDataStream:
|
|
|
222
239
|
await self._handle_event(msg_data, symbol, ch_type)
|
|
223
240
|
elif msg_data.get('action') in ('req', 'sub') and msg_data.get('code') == 200:
|
|
224
241
|
if msg_data.get('ch') == f"trade.clearing#{symbol.lower()}#0":
|
|
225
|
-
self.
|
|
242
|
+
self.logger.info(f"HTX User Data Stream started for {self.trade_id}")
|
|
226
243
|
elif 'subbed' in msg_data and msg_data.get('status') == 'ok':
|
|
227
|
-
logger.info(f"Huobi WSS started: {msg_data['subbed']}")
|
|
244
|
+
self.logger.info(f"Huobi WSS started: {msg_data['subbed']}")
|
|
228
245
|
elif (msg_data.get('action') == 'sub' and
|
|
229
246
|
msg_data.get('code') == 500 and
|
|
230
247
|
msg_data.get('message') == '系统异常:'):
|
|
231
|
-
logger.warning(f"Reconnecting Huobi user {ch_type} channel")
|
|
248
|
+
self.logger.warning(f"Reconnecting Huobi user {ch_type} channel")
|
|
232
249
|
raise ConnectionClosed(None, None)
|
|
233
250
|
else:
|
|
234
|
-
logger.debug(f"Huobi undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
|
|
251
|
+
self.logger.debug(f"Huobi undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
|
|
235
252
|
|
|
236
253
|
async def ws_listener(self, request=None, symbol=None, ch_type=str()):
|
|
237
254
|
if request:
|
|
@@ -245,22 +262,25 @@ class EventsDataStream:
|
|
|
245
262
|
await asyncio.sleep(interval)
|
|
246
263
|
try:
|
|
247
264
|
await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
|
|
248
|
-
except (ConnectionClosed, asyncio.exceptions.TimeoutError):
|
|
249
|
-
|
|
265
|
+
except (ConnectionClosed, asyncio.exceptions.TimeoutError, AttributeError):
|
|
266
|
+
break
|
|
250
267
|
|
|
251
268
|
async def htx_keepalive(self, interval=60):
|
|
252
|
-
await asyncio.sleep(interval
|
|
269
|
+
await asyncio.sleep(interval)
|
|
253
270
|
while True:
|
|
254
271
|
await asyncio.sleep(interval)
|
|
255
272
|
if self.ping:
|
|
256
273
|
break
|
|
257
274
|
else:
|
|
258
275
|
self.ping = 1
|
|
259
|
-
logger.warning("From HTX server PING timeout exceeded")
|
|
260
|
-
|
|
276
|
+
self.logger.warning("From HTX server PING timeout exceeded")
|
|
277
|
+
if self.websocket:
|
|
278
|
+
await self.websocket.close()
|
|
261
279
|
|
|
262
280
|
|
|
263
281
|
class MarketEventsDataStream(EventsDataStream):
|
|
282
|
+
__slots__ = ('channel', 'candles_max_time', 'endpoint')
|
|
283
|
+
|
|
264
284
|
def __init__(self, client, endpoint, exchange, trade_id, channel=None):
|
|
265
285
|
super().__init__(client, endpoint, exchange, trade_id)
|
|
266
286
|
self.channel = channel
|
|
@@ -271,7 +291,7 @@ class MarketEventsDataStream(EventsDataStream):
|
|
|
271
291
|
self.endpoint = f"{endpoint}/stream?streams={combined_streams}"
|
|
272
292
|
|
|
273
293
|
async def start_wss(self):
|
|
274
|
-
logger.info(f"Start market WSS {self.channel or ''} for {self.exchange}")
|
|
294
|
+
self.logger.info(f"Start market WSS {self.channel or ''} for {self.exchange}")
|
|
275
295
|
symbol = None
|
|
276
296
|
ch_type = str()
|
|
277
297
|
request = {}
|
|
@@ -330,12 +350,12 @@ class MarketEventsDataStream(EventsDataStream):
|
|
|
330
350
|
elif ch_type == 'depth5':
|
|
331
351
|
request = {'sub': f"market.{symbol}.depth.step0"}
|
|
332
352
|
|
|
333
|
-
|
|
353
|
+
tasks_manage(self.tasks, self.htx_keepalive(interval=30), f"htx_keepalive-{symbol}-{ch_type}")
|
|
334
354
|
|
|
335
355
|
await self.ws_listener(request, symbol, ch_type)
|
|
336
356
|
|
|
337
357
|
async def _handle_event(self, content, symbol=None, ch_type=str()):
|
|
338
|
-
# logger.info(f"MARKET_handle_event.content: symbol: {symbol}, ch_type: {ch_type}, content: {content}")
|
|
358
|
+
# self.logger.info(f"MARKET_handle_event.content: symbol: {symbol}, ch_type: {ch_type}, content: {content}")
|
|
339
359
|
self.try_count = 0
|
|
340
360
|
if self.exchange == 'bitfinex':
|
|
341
361
|
if 'candles' in ch_type:
|
|
@@ -390,6 +410,8 @@ class MarketEventsDataStream(EventsDataStream):
|
|
|
390
410
|
|
|
391
411
|
|
|
392
412
|
class HbpPrivateEventsDataStream(EventsDataStream):
|
|
413
|
+
__slots__ = ('symbol',)
|
|
414
|
+
|
|
393
415
|
def __init__(self, client, endpoint, exchange, trade_id, symbol):
|
|
394
416
|
super().__init__(client, endpoint, exchange, trade_id)
|
|
395
417
|
self.symbol = symbol
|
|
@@ -423,7 +445,7 @@ class HbpPrivateEventsDataStream(EventsDataStream):
|
|
|
423
445
|
"action": "sub",
|
|
424
446
|
"ch": f"trade.clearing#{self.symbol.lower()}#0"
|
|
425
447
|
}
|
|
426
|
-
|
|
448
|
+
tasks_manage(self.tasks, self.htx_keepalive(), f"htx_keepalive-user-{self.symbol}")
|
|
427
449
|
await self.ws_listener(request, symbol=self.symbol)
|
|
428
450
|
|
|
429
451
|
async def _handle_event(self, msg_data, *args):
|
|
@@ -446,11 +468,12 @@ class HbpPrivateEventsDataStream(EventsDataStream):
|
|
|
446
468
|
content = hbp.on_order_update(self.client.active_orders[order_id])
|
|
447
469
|
|
|
448
470
|
if content:
|
|
449
|
-
logger.debug(f"HTXPrivateEvents.content: {content}")
|
|
471
|
+
self.logger.debug(f"HTXPrivateEvents.content: {content}")
|
|
450
472
|
await self.client.events.wrap_event(content).fire(self.trade_id)
|
|
451
473
|
|
|
452
474
|
|
|
453
475
|
class BfxPrivateEventsDataStream(EventsDataStream):
|
|
476
|
+
__slots__ = ()
|
|
454
477
|
|
|
455
478
|
async def start_wss(self):
|
|
456
479
|
ts = int(time.time() * 1000)
|
|
@@ -505,6 +528,8 @@ class BfxPrivateEventsDataStream(EventsDataStream):
|
|
|
505
528
|
|
|
506
529
|
|
|
507
530
|
class OkxPrivateEventsDataStream(EventsDataStream):
|
|
531
|
+
__slots__ = ('symbol',)
|
|
532
|
+
|
|
508
533
|
def __init__(self, client, endpoint, exchange, trade_id, symbol):
|
|
509
534
|
super().__init__(client, endpoint, exchange, trade_id)
|
|
510
535
|
self.symbol = symbol
|
|
@@ -561,6 +586,7 @@ class OkxPrivateEventsDataStream(EventsDataStream):
|
|
|
561
586
|
|
|
562
587
|
|
|
563
588
|
class BBTPrivateEventsDataStream(EventsDataStream):
|
|
589
|
+
__slots__ = ()
|
|
564
590
|
|
|
565
591
|
async def start_wss(self):
|
|
566
592
|
ts = int((time.time() + 1) * 1000)
|
|
@@ -591,7 +617,7 @@ class BBTPrivateEventsDataStream(EventsDataStream):
|
|
|
591
617
|
await self.client.events.wrap_event(content).fire(self.trade_id)
|
|
592
618
|
content = None
|
|
593
619
|
elif ch_type == 'order.spot':
|
|
594
|
-
# logger.info(f"_handle_event: ch_type: {ch_type}, msg_data: {msg_data}")
|
|
620
|
+
# self.logger.info(f"_handle_event: ch_type: {ch_type}, msg_data: {msg_data}")
|
|
595
621
|
event = msg_data[0]
|
|
596
622
|
if event.get('orderStatus') in ("Cancelled", "PartiallyFilledCanceled"):
|
|
597
623
|
self.client.wss_buffer[f"oc-{event.get('orderId')}"] = bbt.order(event, response_type=True)
|
|
@@ -603,6 +629,7 @@ class BBTPrivateEventsDataStream(EventsDataStream):
|
|
|
603
629
|
|
|
604
630
|
|
|
605
631
|
class UserEventsDataStream(EventsDataStream):
|
|
632
|
+
__slots__ = ()
|
|
606
633
|
|
|
607
634
|
async def start_wss(self):
|
|
608
635
|
await self.websocket.send(
|
|
@@ -620,5 +647,5 @@ class UserEventsDataStream(EventsDataStream):
|
|
|
620
647
|
await self.ws_listener(request)
|
|
621
648
|
|
|
622
649
|
async def _handle_event(self, content):
|
|
623
|
-
# logger.debug(f"UserEventsDataStream._handle_event.content: {content}")
|
|
650
|
+
# self.logger.debug(f"UserEventsDataStream._handle_event.content: {content}")
|
|
624
651
|
await self.client.events.wrap_event(content).fire(self.trade_id)
|
|
@@ -4,7 +4,10 @@ build-backend = "flit_core.buildapi"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "exchanges-wrapper"
|
|
7
|
-
authors = [
|
|
7
|
+
authors = [
|
|
8
|
+
{name = "Thomas Marchand", email = "thomas.marchand@tuta.io"},
|
|
9
|
+
{name = "Jerry Fedorenko", email = "jerry.fedorenko@yahoo.com"}
|
|
10
|
+
]
|
|
8
11
|
readme = "README.md"
|
|
9
12
|
license = {file = "LICENSE.md"}
|
|
10
13
|
classifiers=["Programming Language :: Python :: 3",
|
|
@@ -17,10 +20,10 @@ dynamic = ["version", "description"]
|
|
|
17
20
|
requires-python = ">=3.10"
|
|
18
21
|
|
|
19
22
|
dependencies = [
|
|
20
|
-
"crypto-ws-api==2.1.
|
|
23
|
+
"crypto-ws-api==2.1.4",
|
|
21
24
|
"pyotp==2.9.0",
|
|
22
|
-
"simplejson==3.20.
|
|
23
|
-
"aiohttp~=3.
|
|
25
|
+
"simplejson==3.20.2",
|
|
26
|
+
"aiohttp~=3.13.2",
|
|
24
27
|
"expiringdict~=1.2.2",
|
|
25
28
|
"betterproto==2.0.0b7",
|
|
26
29
|
"grpclib~=0.4.8"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/exch_srv_cfg.toml.template
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|