exchanges-wrapper 2.1.40__tar.gz → 2.1.42__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.40 → exchanges_wrapper-2.1.42}/PKG-INFO +8 -5
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/README.md +4 -1
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/__init__.py +14 -2
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/client.py +49 -31
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/exch_srv.py +24 -24
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/http_client.py +24 -5
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/lib.py +1 -1
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/parsers/bitfinex.py +29 -11
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/parsers/bybit.py +2 -2
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/parsers/okx.py +20 -15
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/web_sockets.py +96 -70
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/pyproject.toml +7 -4
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/LICENSE.md +0 -0
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/definitions.py +0 -0
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/errors.py +0 -0
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/events.py +0 -0
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/martin/__init__.py +0 -0
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/parsers/huobi.py +0 -0
- {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/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.42
|
|
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.
|
|
15
|
+
Requires-Dist: crypto-ws-api==2.1.3
|
|
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.0
|
|
19
19
|
Requires-Dist: expiringdict~=1.2.2
|
|
20
20
|
Requires-Dist: betterproto==2.0.0b7
|
|
21
21
|
Requires-Dist: grpclib~=0.4.8
|
|
@@ -28,12 +28,15 @@ Project-URL: Source, https://github.com/DogsTailFarmer/exchanges-wrapper
|
|
|
28
28
|
<h3 align="center">For SPOT markets</h3>
|
|
29
29
|
|
|
30
30
|
***
|
|
31
|
-
<a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
|
|
31
|
+
<h1 align="center"><a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
|
|
32
32
|
<a href="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper.svg/?label=resolved+issues&token=XuG5PmzMiKlDL921-qREIuX_"/></a>
|
|
33
33
|
<a href="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper.svg/?label=active+issues&token=XuG5PmzMiKlDL921-qREIuX_"/></a>
|
|
34
34
|
<a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_exchanges-wrapper" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_exchanges-wrapper&metric=alert_status"/></a>
|
|
35
35
|
<a href="https://pepy.tech/project/exchanges-wrapper" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/exchanges-wrapper/month"/></a>
|
|
36
|
+
</h1>
|
|
37
|
+
|
|
36
38
|
***
|
|
39
|
+
|
|
37
40
|
On `Binance`: now API key type [Ed25519](https://www.binance.com/en/support/faq/detail/6b9a63f1e3384cf48a2eedb82767a69a) is used instead of `HMAC`
|
|
38
41
|
|
|
39
42
|
From `2.1.34` must be updated `exch_srv_cfg.toml` from `exchanges_wrapper/exch_srv_cfg.toml.template`
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
<h3 align="center">For SPOT markets</h3>
|
|
6
6
|
|
|
7
7
|
***
|
|
8
|
-
<a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
|
|
8
|
+
<h1 align="center"><a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
|
|
9
9
|
<a href="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper.svg/?label=resolved+issues&token=XuG5PmzMiKlDL921-qREIuX_"/></a>
|
|
10
10
|
<a href="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper.svg/?label=active+issues&token=XuG5PmzMiKlDL921-qREIuX_"/></a>
|
|
11
11
|
<a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_exchanges-wrapper" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_exchanges-wrapper&metric=alert_status"/></a>
|
|
12
12
|
<a href="https://pepy.tech/project/exchanges-wrapper" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/exchanges-wrapper/month"/></a>
|
|
13
|
+
</h1>
|
|
14
|
+
|
|
13
15
|
***
|
|
16
|
+
|
|
14
17
|
On `Binance`: now API key type [Ed25519](https://www.binance.com/en/support/faq/detail/6b9a63f1e3384cf48a2eedb82767a69a) is used instead of `HMAC`
|
|
15
18
|
|
|
16
19
|
From `2.1.34` must be updated `exch_srv_cfg.toml` from `exchanges_wrapper/exch_srv_cfg.toml.template`
|
|
@@ -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.42"
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
import shutil
|
|
@@ -23,9 +23,21 @@ from grpclib.utils import graceful_exit
|
|
|
23
23
|
from grpclib import exceptions
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
26
|
-
'
|
|
26
|
+
'__version__',
|
|
27
|
+
'Server',
|
|
28
|
+
'GRPCError',
|
|
29
|
+
'Status',
|
|
30
|
+
'Channel',
|
|
31
|
+
'graceful_exit',
|
|
32
|
+
'exceptions',
|
|
33
|
+
'LOG_PATH',
|
|
34
|
+
'WORK_PATH',
|
|
35
|
+
'LOG_FILE',
|
|
36
|
+
'CONFIG_FILE',
|
|
37
|
+
'DEBUG_LOG'
|
|
27
38
|
]
|
|
28
39
|
|
|
40
|
+
DEBUG_LOG = 'debug' # The exchange for which log files, separated by trade_id with DEBUG level, will be generated
|
|
29
41
|
WORK_PATH = Path(Path.home(), ".MartinBinance")
|
|
30
42
|
CONFIG_PATH = Path(WORK_PATH, "config")
|
|
31
43
|
CONFIG_FILE = Path(CONFIG_PATH, "exch_srv_cfg.toml")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import Union
|
|
2
|
+
from typing import Union, Dict
|
|
3
3
|
import decimal
|
|
4
4
|
import math
|
|
5
5
|
import asyncio
|
|
@@ -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', 'loaded', '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']
|
|
@@ -98,15 +113,10 @@ class Client:
|
|
|
98
113
|
self.main_account_uid = None
|
|
99
114
|
self.ledgers_id = []
|
|
100
115
|
self.ts_start = {}
|
|
101
|
-
self.tasks =
|
|
116
|
+
self.tasks: dict[str, set] = {}
|
|
102
117
|
self.request_event = asyncio.Event()
|
|
103
118
|
self.request_event.set()
|
|
104
119
|
|
|
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
120
|
async def fetch_object(self, key):
|
|
111
121
|
res = None
|
|
112
122
|
while res is None:
|
|
@@ -144,8 +154,8 @@ class Client:
|
|
|
144
154
|
logger.info(f"Info for {self.exchange}:{symbol} loaded successfully")
|
|
145
155
|
|
|
146
156
|
async def close(self):
|
|
147
|
-
if self.http
|
|
148
|
-
await self.http.
|
|
157
|
+
if self.http:
|
|
158
|
+
await self.http.close_session()
|
|
149
159
|
|
|
150
160
|
@property
|
|
151
161
|
def events(self):
|
|
@@ -154,9 +164,8 @@ class Client:
|
|
|
154
164
|
self._events = Events() # skipcq: PYL-W0201
|
|
155
165
|
return self._events
|
|
156
166
|
|
|
157
|
-
|
|
167
|
+
def start_user_events_listener(self, _trade_id, symbol):
|
|
158
168
|
logger.info(f"Start '{self.exchange}' user events listener for {_trade_id}")
|
|
159
|
-
user_data_stream = None
|
|
160
169
|
if self.exchange == 'binance':
|
|
161
170
|
user_data_stream = UserEventsDataStream(self, self.endpoint_ws_api, self.exchange, _trade_id)
|
|
162
171
|
elif self.exchange == 'bitfinex':
|
|
@@ -173,24 +182,27 @@ class Client:
|
|
|
173
182
|
)
|
|
174
183
|
elif self.exchange == 'bybit':
|
|
175
184
|
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)
|
|
185
|
+
else:
|
|
186
|
+
raise UserWarning(f"User Data Stream: exchange {self.exchange} not serviced")
|
|
187
|
+
|
|
188
|
+
self.data_streams[_trade_id] |= {user_data_stream}
|
|
189
|
+
|
|
190
|
+
trade_tasks = self.tasks.pop(_trade_id, set())
|
|
191
|
+
tasks_manage(trade_tasks, user_data_stream.start(), f"user_data_stream-{self.exchange}-{_trade_id}")
|
|
192
|
+
self.tasks[_trade_id] = trade_tasks
|
|
186
193
|
|
|
187
194
|
def start_market_events_listener(self, _trade_id):
|
|
188
195
|
_events = self.events.registered_streams.get(self.exchange, {}).get(_trade_id, set())
|
|
189
196
|
if self.exchange == 'binance':
|
|
190
197
|
market_data_stream = MarketEventsDataStream(self, self.endpoint_ws_public, self.exchange, _trade_id)
|
|
191
198
|
self.data_streams[_trade_id] |= {market_data_stream}
|
|
192
|
-
|
|
199
|
+
|
|
200
|
+
trade_tasks = self.tasks.pop(_trade_id, set())
|
|
201
|
+
tasks_manage(trade_tasks, market_data_stream.start(), f"market_data_stream-{self.exchange}-{_trade_id}")
|
|
202
|
+
self.tasks[_trade_id] = trade_tasks
|
|
203
|
+
|
|
193
204
|
else:
|
|
205
|
+
trade_tasks = self.tasks.pop(_trade_id, set())
|
|
194
206
|
for channel in _events:
|
|
195
207
|
# https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url
|
|
196
208
|
if self.exchange == 'okx' and 'kline' in channel:
|
|
@@ -200,15 +212,21 @@ class Client:
|
|
|
200
212
|
#
|
|
201
213
|
market_data_stream = MarketEventsDataStream(self, _endpoint, self.exchange, _trade_id, channel)
|
|
202
214
|
self.data_streams[_trade_id] |= {market_data_stream}
|
|
203
|
-
|
|
215
|
+
tasks_manage(
|
|
216
|
+
trade_tasks, market_data_stream.start(), f"market_data_stream-{self.exchange}-{channel}-{_trade_id}"
|
|
217
|
+
)
|
|
218
|
+
self.tasks[_trade_id] = trade_tasks
|
|
204
219
|
|
|
205
220
|
async def stop_events_listener(self, _trade_id):
|
|
206
221
|
logger.info(f"Stop events listener data streams for {_trade_id}")
|
|
207
222
|
stopped_data_stream = self.data_streams.pop(_trade_id, set())
|
|
208
223
|
for data_stream in stopped_data_stream:
|
|
209
224
|
await data_stream.stop()
|
|
225
|
+
if trade_tasks := self.tasks.pop(_trade_id, set()):
|
|
226
|
+
await tasks_cancel(trade_tasks, _logger=logger)
|
|
227
|
+
|
|
210
228
|
if self.user_session:
|
|
211
|
-
await self.user_session.stop()
|
|
229
|
+
await self.user_session.stop(_trade_id)
|
|
212
230
|
|
|
213
231
|
def assert_symbol_exists(self, symbol):
|
|
214
232
|
if self.loaded and symbol not in self.symbols:
|
|
@@ -587,7 +605,7 @@ class Client:
|
|
|
587
605
|
"/api/v3/klines", params=params, signed=False
|
|
588
606
|
)
|
|
589
607
|
elif self.exchange == 'bitfinex':
|
|
590
|
-
params = {'limit': limit, 'sort': -1}
|
|
608
|
+
params: Dict[str, Union[str, int]] = {'limit': limit, 'sort': -1}
|
|
591
609
|
if start_time:
|
|
592
610
|
params["start"] = str(start_time)
|
|
593
611
|
if end_time:
|
|
@@ -835,7 +853,7 @@ class Client:
|
|
|
835
853
|
**params,
|
|
836
854
|
)
|
|
837
855
|
if res:
|
|
838
|
-
timeout = STATUS_TIMEOUT / 0.1
|
|
856
|
+
timeout = int(STATUS_TIMEOUT / 0.1)
|
|
839
857
|
while not self.active_orders.get(int(res)) and timeout:
|
|
840
858
|
timeout -= 1
|
|
841
859
|
await asyncio.sleep(0.1)
|
|
@@ -908,7 +926,7 @@ class Client:
|
|
|
908
926
|
)
|
|
909
927
|
if b_res is None:
|
|
910
928
|
fallback_warning(self.exchange, symbol)
|
|
911
|
-
b_res =
|
|
929
|
+
b_res = await self.http.send_api_call(
|
|
912
930
|
ORDER_ENDPOINT,
|
|
913
931
|
params=params,
|
|
914
932
|
signed=True,
|
|
@@ -1030,7 +1048,7 @@ class Client:
|
|
|
1030
1048
|
**params
|
|
1031
1049
|
)
|
|
1032
1050
|
if res and isinstance(res, list) and res[6] == 'SUCCESS':
|
|
1033
|
-
timeout = STATUS_TIMEOUT / 0.1
|
|
1051
|
+
timeout = int(STATUS_TIMEOUT / 0.1)
|
|
1034
1052
|
while timeout:
|
|
1035
1053
|
timeout -= 1
|
|
1036
1054
|
if self.active_orders.get(order_id, {}).get('cancelled', False):
|
|
@@ -1046,7 +1064,7 @@ class Client:
|
|
|
1046
1064
|
signed=True
|
|
1047
1065
|
)
|
|
1048
1066
|
if res:
|
|
1049
|
-
timeout = STATUS_TIMEOUT / 0.1
|
|
1067
|
+
timeout = int(STATUS_TIMEOUT / 0.1)
|
|
1050
1068
|
while not self.active_orders.get(order_id, {}).get('cancelled', False) and timeout:
|
|
1051
1069
|
timeout -= 1
|
|
1052
1070
|
await asyncio.sleep(0.1)
|
|
@@ -1182,7 +1200,7 @@ class Client:
|
|
|
1182
1200
|
signed=True,
|
|
1183
1201
|
data=params,
|
|
1184
1202
|
)
|
|
1185
|
-
ids_canceled = [int(
|
|
1203
|
+
ids_canceled = [int(order['ordId']) for order in res if order['sCode'] == '0']
|
|
1186
1204
|
orders_canceled[:] = [i for i in orders_canceled if i['orderId'] in ids_canceled]
|
|
1187
1205
|
binance_res.extend(orders_canceled)
|
|
1188
1206
|
elif self.exchange == 'bybit':
|
|
@@ -4,7 +4,9 @@ from typing import Any, AsyncGenerator
|
|
|
4
4
|
|
|
5
5
|
import grpclib.exceptions
|
|
6
6
|
|
|
7
|
+
# noinspection PyPep8Naming
|
|
7
8
|
from exchanges_wrapper import __version__ as VER_EW
|
|
9
|
+
# noinspection PyPep8Naming
|
|
8
10
|
from crypto_ws_api import __version__ as VER_CW
|
|
9
11
|
import time
|
|
10
12
|
import weakref
|
|
@@ -27,9 +29,13 @@ from exchanges_wrapper.lib import (
|
|
|
27
29
|
REST_RATE_LIMIT_INTERVAL,
|
|
28
30
|
FILTER_TYPE_MAP,
|
|
29
31
|
)
|
|
30
|
-
from exchanges_wrapper.martin import
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
from exchanges_wrapper.martin import (
|
|
33
|
+
StreamResponse,
|
|
34
|
+
SimpleResponse,
|
|
35
|
+
OnKlinesUpdateResponse,
|
|
36
|
+
OnTickerUpdateResponse,
|
|
37
|
+
FetchOrderBookResponse,
|
|
38
|
+
)
|
|
33
39
|
#
|
|
34
40
|
HEARTBEAT = 1 # sec
|
|
35
41
|
MAX_QUEUE_SIZE = 100
|
|
@@ -136,10 +142,8 @@ class Martin(mr.MartinBase):
|
|
|
136
142
|
|
|
137
143
|
try:
|
|
138
144
|
await asyncio.wait_for(open_client.client.load(request.symbol), timeout=HEARTBEAT * 60)
|
|
139
|
-
except asyncio.CancelledError:
|
|
140
|
-
pass # Task cancellation should not be logged as an error
|
|
141
145
|
except asyncio.exceptions.TimeoutError:
|
|
142
|
-
await OpenClient.get_client(client_id).client.http.
|
|
146
|
+
await OpenClient.get_client(client_id).client.http.close_session()
|
|
143
147
|
OpenClient.remove_client(request.account_name)
|
|
144
148
|
raise GRPCError(status=Status.UNAVAILABLE, message=f"'{open_client.name}' timeout error")
|
|
145
149
|
except Exception as ex:
|
|
@@ -186,7 +190,7 @@ class Martin(mr.MartinBase):
|
|
|
186
190
|
|
|
187
191
|
try:
|
|
188
192
|
res = await asyncio.wait_for(getattr(client, client_method_name)(**kwargs), timeout=90)
|
|
189
|
-
except
|
|
193
|
+
except KeyboardInterrupt:
|
|
190
194
|
raise GRPCError(status=Status.UNAVAILABLE, message=f"{msg_header} Server Shutdown")
|
|
191
195
|
except asyncio.exceptions.TimeoutError:
|
|
192
196
|
self.log_and_raise_grpc_error(msg_header, Status.DEADLINE_EXCEEDED, "timeout error")
|
|
@@ -227,7 +231,7 @@ class Martin(mr.MartinBase):
|
|
|
227
231
|
server_time = res.get('serverTime')
|
|
228
232
|
return mr.FetchServerTimeResponse(server_time=server_time)
|
|
229
233
|
|
|
230
|
-
async def one_click_arrival_deposit(self, request: mr.MarketRequest) -> mr.SimpleResponse
|
|
234
|
+
async def one_click_arrival_deposit(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
231
235
|
res, _, _ = await self.send_request('one_click_arrival_deposit', request, tx_id=request.symbol)
|
|
232
236
|
return mr.SimpleResponse(success=True, result=json.dumps(str(res)))
|
|
233
237
|
|
|
@@ -297,7 +301,7 @@ class Martin(mr.MartinBase):
|
|
|
297
301
|
await _queue.put(weakref.ref(event)())
|
|
298
302
|
logger.debug(f"{msg_header}: {trades}")
|
|
299
303
|
|
|
300
|
-
async def cancel_all_orders(self, request: mr.MarketRequest) -> mr.SimpleResponse
|
|
304
|
+
async def cancel_all_orders(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
301
305
|
response = mr.SimpleResponse()
|
|
302
306
|
|
|
303
307
|
res, _, _ = await self.send_request(
|
|
@@ -754,7 +758,7 @@ class Martin(mr.MartinBase):
|
|
|
754
758
|
)
|
|
755
759
|
logger.info(f"Start WS streams for {open_client.name}")
|
|
756
760
|
client.start_market_events_listener(request.trade_id)
|
|
757
|
-
|
|
761
|
+
client.start_user_events_listener(request.trade_id, request.symbol)
|
|
758
762
|
response.success = True
|
|
759
763
|
return response
|
|
760
764
|
|
|
@@ -763,7 +767,7 @@ class Martin(mr.MartinBase):
|
|
|
763
767
|
if open_client := OpenClient.get_client(request.client_id):
|
|
764
768
|
client = open_client.client
|
|
765
769
|
logger.info(f"StopStream request for {request.symbol} on {client.exchange}")
|
|
766
|
-
await
|
|
770
|
+
await stop_stream_ex(client, request.trade_id)
|
|
767
771
|
response.success = True
|
|
768
772
|
else:
|
|
769
773
|
response.success = False
|
|
@@ -780,13 +784,13 @@ class Martin(mr.MartinBase):
|
|
|
780
784
|
return response
|
|
781
785
|
|
|
782
786
|
async def client_restart(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
783
|
-
if session := OpenClient.get_client(request.client_id).client.http
|
|
784
|
-
await session.
|
|
787
|
+
if session := OpenClient.get_client(request.client_id).client.http:
|
|
788
|
+
await session.close_session()
|
|
785
789
|
OpenClient.remove_client(request.account_name)
|
|
786
790
|
return mr.SimpleResponse(success=True)
|
|
787
791
|
|
|
788
792
|
|
|
789
|
-
async def
|
|
793
|
+
async def stop_stream_ex(client, trade_id):
|
|
790
794
|
await client.stop_events_listener(trade_id)
|
|
791
795
|
client.events.unregister(client.exchange, trade_id)
|
|
792
796
|
[await _queue.put(trade_id) for _queue in client.stream_queue.get(trade_id, [])]
|
|
@@ -802,10 +806,7 @@ async def event_handler(_queue, client, trade_id, _event_type, event):
|
|
|
802
806
|
except asyncio.QueueFull:
|
|
803
807
|
logger.warning(f"For {_event_type} asyncio queue full and wold be closed")
|
|
804
808
|
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}")
|
|
809
|
+
await stop_stream_ex(client, trade_id)
|
|
809
810
|
|
|
810
811
|
|
|
811
812
|
def is_port_in_use(port: int) -> bool:
|
|
@@ -825,8 +826,8 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
|
|
|
825
826
|
await server.wait_closed()
|
|
826
827
|
|
|
827
828
|
for oc in OpenClient.open_clients:
|
|
828
|
-
if oc.client.http
|
|
829
|
-
await oc.client.http.
|
|
829
|
+
if oc.client.http:
|
|
830
|
+
await oc.client.http.close_session()
|
|
830
831
|
|
|
831
832
|
[task.cancel() for task in asyncio.all_tasks() if not task.done() and task is not asyncio.current_task()]
|
|
832
833
|
|
|
@@ -834,12 +835,11 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
|
|
|
834
835
|
def main():
|
|
835
836
|
try:
|
|
836
837
|
asyncio.run(amain())
|
|
837
|
-
except
|
|
838
|
+
except grpclib.exceptions.StreamTerminatedError:
|
|
838
839
|
pass # Task cancellation should not be logged as an error
|
|
839
|
-
except Exception as
|
|
840
|
-
print(f"Exception: {
|
|
840
|
+
except Exception as expt:
|
|
841
|
+
print(f"Exception: {expt}")
|
|
841
842
|
print(traceback.format_exc())
|
|
842
843
|
|
|
843
|
-
|
|
844
844
|
if __name__ == '__main__':
|
|
845
845
|
main()
|
|
@@ -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,9 +69,14 @@ 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
|
-
self.session = aiohttp.ClientSession(timeout=TIMEOUT)
|
|
74
|
+
self.session = aiohttp.ClientSession(trust_env=True, timeout=TIMEOUT)
|
|
75
|
+
|
|
76
|
+
async def close_session(self):
|
|
77
|
+
if self.session:
|
|
78
|
+
await self.session.close()
|
|
79
|
+
self.session = None
|
|
60
80
|
|
|
61
81
|
async def handle_errors(self, response, path=None):
|
|
62
82
|
if response.status >= 500:
|
|
@@ -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):
|
|
@@ -170,7 +189,7 @@ class HttpClient:
|
|
|
170
189
|
async def _bitfinex_request(self, path, method, signed, send_api_key, endpoint, timeout, **kwargs):
|
|
171
190
|
_endpoint = endpoint or self.endpoint
|
|
172
191
|
bfx_post = (method == 'POST' and kwargs) or "params" in kwargs
|
|
173
|
-
_params = json.dumps(kwargs) if bfx_post else
|
|
192
|
+
_params = json.dumps(kwargs) if bfx_post else {}
|
|
174
193
|
url = f'{_endpoint}/{path}'
|
|
175
194
|
query_kwargs = {"headers": {"Accept": AJ}}
|
|
176
195
|
if kwargs and not bfx_post:
|
|
@@ -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"
|
|
@@ -4,6 +4,7 @@ Parser for convert Bitfinex REST API/WSS response to Binance like result
|
|
|
4
4
|
import time
|
|
5
5
|
from decimal import Decimal
|
|
6
6
|
import logging
|
|
7
|
+
from typing import Dict, List, Union
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
9
10
|
|
|
@@ -21,9 +22,9 @@ class OrderBook:
|
|
|
21
22
|
self.asks[str(i[0])] = str(abs(i[2]))
|
|
22
23
|
|
|
23
24
|
def get_book(self) -> dict:
|
|
24
|
-
bids = list(
|
|
25
|
+
bids = [list(item) for item in self.bids.items()]
|
|
25
26
|
bids.sort(key=lambda x: float(x[0]), reverse=True)
|
|
26
|
-
asks = list(
|
|
27
|
+
asks = [list(item) for item in self.asks.items()]
|
|
27
28
|
asks.sort(key=lambda x: float(x[0]), reverse=False)
|
|
28
29
|
return {
|
|
29
30
|
'stream': f"{self.symbol}@depth5",
|
|
@@ -270,15 +271,32 @@ def orders(res: list, response_type=None, cancelled=False) -> list:
|
|
|
270
271
|
return binance_orders
|
|
271
272
|
|
|
272
273
|
|
|
273
|
-
def order_book(res:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
274
|
+
def order_book(res: List[List[float]]) -> Dict[str, Union[int, List[List[str]]]]:
|
|
275
|
+
"""
|
|
276
|
+
Processes a list of raw order book entries into a structured dictionary.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
res: A list of order entries, where each entry is a list like
|
|
280
|
+
[price (float), quantity (float), side_indicator (float)].
|
|
281
|
+
side_indicator > 0 for bids, < 0 for asks.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
A dictionary representing the order book with 'lastUpdateId', 'bids', and 'asks'.
|
|
285
|
+
"""
|
|
286
|
+
binance_order_book: Dict[str, Union[int, List[List[str]]]] = {
|
|
287
|
+
"lastUpdateId": int(time.time() * 1000)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
bids: List[List[str]] = []
|
|
291
|
+
asks: List[List[str]] = []
|
|
292
|
+
|
|
277
293
|
for i in res:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
294
|
+
# Assuming i[2] determines bid/ask side and its absolute value is the quantity
|
|
295
|
+
if i[2] > 0: # This means it's a bid (positive quantity indicator)
|
|
296
|
+
bids.append([str(i[0]), str(i[2])]) # price, quantity
|
|
297
|
+
else: # This means it's an ask (negative quantity indicator or zero)
|
|
298
|
+
asks.append([str(i[0]), str(abs(i[2]))]) # price, absolute quantity
|
|
299
|
+
|
|
282
300
|
binance_order_book['bids'] = bids
|
|
283
301
|
binance_order_book['asks'] = asks
|
|
284
302
|
return binance_order_book
|
|
@@ -435,7 +453,7 @@ def ticker(res: list, symbol: str = None) -> dict:
|
|
|
435
453
|
|
|
436
454
|
|
|
437
455
|
def on_funds_update(res: list) -> dict:
|
|
438
|
-
binance_funds = {
|
|
456
|
+
binance_funds: Dict[str, Union[str, int, List]] = {
|
|
439
457
|
'e': 'outboundAccountPosition',
|
|
440
458
|
'E': int(time.time() * 1000),
|
|
441
459
|
'u': int(time.time() * 1000),
|
|
@@ -23,8 +23,8 @@ class OrderBook:
|
|
|
23
23
|
'stream': f"{self.symbol}@depth5",
|
|
24
24
|
'data': {
|
|
25
25
|
'lastUpdateId': self.last_update_id,
|
|
26
|
-
'bids': list(
|
|
27
|
-
'asks': list(
|
|
26
|
+
'bids': [list(item) for item in self.bids.items()][:5],
|
|
27
|
+
'asks': [list(item) for item in self.asks.items()][:5],
|
|
28
28
|
},
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -3,6 +3,7 @@ Parser for convert OKX REST API/WSS V5 response to Binance like result
|
|
|
3
3
|
"""
|
|
4
4
|
import time
|
|
5
5
|
from decimal import Decimal
|
|
6
|
+
from typing import Dict, List, Union
|
|
6
7
|
import logging
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
@@ -14,7 +15,7 @@ def fetch_server_time(res: list) -> dict | None:
|
|
|
14
15
|
return None
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
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:
|
|
18
19
|
symbols = []
|
|
19
20
|
symbols_price = {}
|
|
20
21
|
for pair in tickers:
|
|
@@ -97,7 +98,7 @@ def orders(res: list, response_type=None) -> list:
|
|
|
97
98
|
return binance_orders
|
|
98
99
|
|
|
99
100
|
|
|
100
|
-
def order(res:
|
|
101
|
+
def order(res: dict, response_type=None) -> dict:
|
|
101
102
|
symbol = res.get('instId').replace('-', '')
|
|
102
103
|
order_id = int(res.get('ordId'))
|
|
103
104
|
order_list_id = -1
|
|
@@ -185,7 +186,7 @@ def order(res: {}, response_type=None) -> {}:
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
|
|
188
|
-
def place_order_response(res:
|
|
189
|
+
def place_order_response(res: dict, req: dict) -> dict:
|
|
189
190
|
return {
|
|
190
191
|
"symbol": req["instId"].replace('-', ''),
|
|
191
192
|
"orderId": int(res["ordId"]),
|
|
@@ -214,10 +215,14 @@ def account_balances(res: list) -> dict:
|
|
|
214
215
|
return {"balances": balances}
|
|
215
216
|
|
|
216
217
|
|
|
217
|
-
def order_book(res: dict) ->
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
def order_book(res: dict) -> Dict[str, Union[int, List[List[str]]]]:
|
|
219
|
+
binance_order_book: Dict[str, Union[int, List[List[str]]]] = {
|
|
220
|
+
"lastUpdateId": int(time.time() * 1000)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
bids: List[List[str]] = []
|
|
224
|
+
asks: List[List[str]] = []
|
|
225
|
+
|
|
221
226
|
[asks.append(ask[:2]) for ask in res.get('asks')]
|
|
222
227
|
binance_order_book['asks'] = asks
|
|
223
228
|
[bids.append(bid[:2]) for bid in res.get('bids')]
|
|
@@ -225,7 +230,7 @@ def order_book(res: dict) -> dict:
|
|
|
225
230
|
return binance_order_book
|
|
226
231
|
|
|
227
232
|
|
|
228
|
-
def ticker_price_change_statistics(res:
|
|
233
|
+
def ticker_price_change_statistics(res: dict) -> dict:
|
|
229
234
|
price_change = str(Decimal(res.get('last')) - Decimal(res.get('open24h')))
|
|
230
235
|
price_change_percent = str(100 * (Decimal(res.get('last')) - Decimal(res.get('open24h'))) /
|
|
231
236
|
Decimal(res.get('open24h')))
|
|
@@ -258,14 +263,14 @@ def ticker_price_change_statistics(res: {}) -> {}:
|
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
|
|
261
|
-
def fetch_symbol_price_ticker(res:
|
|
266
|
+
def fetch_symbol_price_ticker(res: dict, symbol) -> dict:
|
|
262
267
|
return {
|
|
263
268
|
"symbol": symbol,
|
|
264
269
|
"price": res.get('last')
|
|
265
270
|
}
|
|
266
271
|
|
|
267
272
|
|
|
268
|
-
def ticker(res:
|
|
273
|
+
def ticker(res: dict) -> dict:
|
|
269
274
|
symbol = res.get('instId').replace('-', '')
|
|
270
275
|
return {
|
|
271
276
|
'stream': f"{symbol.lower()}@miniTicker",
|
|
@@ -339,7 +344,7 @@ def interval2value(_interval: str) -> int:
|
|
|
339
344
|
return resolution.get(_interval, 0)
|
|
340
345
|
|
|
341
346
|
|
|
342
|
-
def candle(res: list, symbol: str = None, ch_type: str = None) ->
|
|
347
|
+
def candle(res: list, symbol: str = None, ch_type: str = None) -> dict:
|
|
343
348
|
symbol = symbol.replace('-', '').lower()
|
|
344
349
|
start_time = int(res[0])
|
|
345
350
|
_interval = ch_type.replace('kline_', '')
|
|
@@ -373,7 +378,7 @@ def candle(res: list, symbol: str = None, ch_type: str = None) -> {}:
|
|
|
373
378
|
}
|
|
374
379
|
|
|
375
380
|
|
|
376
|
-
def order_book_ws(res:
|
|
381
|
+
def order_book_ws(res: dict, symbol: str) -> dict:
|
|
377
382
|
symbol = symbol.replace('-', '').lower()
|
|
378
383
|
return {
|
|
379
384
|
'stream': f"{symbol}@depth5",
|
|
@@ -381,7 +386,7 @@ def order_book_ws(res: {}, symbol: str) -> {}:
|
|
|
381
386
|
}
|
|
382
387
|
|
|
383
388
|
|
|
384
|
-
def on_funds_update(res:
|
|
389
|
+
def on_funds_update(res: dict) -> dict:
|
|
385
390
|
event_time = int(time.time() * 1000)
|
|
386
391
|
data = res.get('details')
|
|
387
392
|
funds = []
|
|
@@ -403,7 +408,7 @@ def on_funds_update(res: {}) -> {}:
|
|
|
403
408
|
}
|
|
404
409
|
|
|
405
410
|
|
|
406
|
-
def on_order_update(res:
|
|
411
|
+
def on_order_update(res: dict) -> dict:
|
|
407
412
|
# print(f"on_order_update.res: {res}")
|
|
408
413
|
order_quantity = res.get('sz')
|
|
409
414
|
order_price = res.get('px')
|
|
@@ -459,7 +464,7 @@ def on_order_update(res: {}) -> {}:
|
|
|
459
464
|
}
|
|
460
465
|
|
|
461
466
|
|
|
462
|
-
def on_balance_update(res: list, buffer: dict, transfer: bool) ->
|
|
467
|
+
def on_balance_update(res: list, buffer: dict, transfer: bool) -> tuple:
|
|
463
468
|
res_diff = []
|
|
464
469
|
for i in res:
|
|
465
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} 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:
|
|
@@ -246,21 +263,23 @@ class EventsDataStream:
|
|
|
246
263
|
try:
|
|
247
264
|
await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
|
|
248
265
|
except (ConnectionClosed, asyncio.exceptions.TimeoutError):
|
|
249
|
-
|
|
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")
|
|
276
|
+
self.logger.warning("From HTX server PING timeout exceeded")
|
|
260
277
|
await self.websocket.close()
|
|
261
278
|
|
|
262
279
|
|
|
263
280
|
class MarketEventsDataStream(EventsDataStream):
|
|
281
|
+
__slots__ = ('channel', 'candles_max_time', 'endpoint')
|
|
282
|
+
|
|
264
283
|
def __init__(self, client, endpoint, exchange, trade_id, channel=None):
|
|
265
284
|
super().__init__(client, endpoint, exchange, trade_id)
|
|
266
285
|
self.channel = channel
|
|
@@ -271,7 +290,7 @@ class MarketEventsDataStream(EventsDataStream):
|
|
|
271
290
|
self.endpoint = f"{endpoint}/stream?streams={combined_streams}"
|
|
272
291
|
|
|
273
292
|
async def start_wss(self):
|
|
274
|
-
logger.info(f"Start market WSS {self.channel or ''} for {self.exchange}")
|
|
293
|
+
self.logger.info(f"Start market WSS {self.channel or ''} for {self.exchange}")
|
|
275
294
|
symbol = None
|
|
276
295
|
ch_type = str()
|
|
277
296
|
request = {}
|
|
@@ -330,12 +349,12 @@ class MarketEventsDataStream(EventsDataStream):
|
|
|
330
349
|
elif ch_type == 'depth5':
|
|
331
350
|
request = {'sub': f"market.{symbol}.depth.step0"}
|
|
332
351
|
|
|
333
|
-
|
|
352
|
+
tasks_manage(self.tasks, self.htx_keepalive(interval=30), f"htx_keepalive-{symbol}-{ch_type}")
|
|
334
353
|
|
|
335
354
|
await self.ws_listener(request, symbol, ch_type)
|
|
336
355
|
|
|
337
356
|
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}")
|
|
357
|
+
# self.logger.info(f"MARKET_handle_event.content: symbol: {symbol}, ch_type: {ch_type}, content: {content}")
|
|
339
358
|
self.try_count = 0
|
|
340
359
|
if self.exchange == 'bitfinex':
|
|
341
360
|
if 'candles' in ch_type:
|
|
@@ -390,6 +409,8 @@ class MarketEventsDataStream(EventsDataStream):
|
|
|
390
409
|
|
|
391
410
|
|
|
392
411
|
class HbpPrivateEventsDataStream(EventsDataStream):
|
|
412
|
+
__slots__ = ('symbol',)
|
|
413
|
+
|
|
393
414
|
def __init__(self, client, endpoint, exchange, trade_id, symbol):
|
|
394
415
|
super().__init__(client, endpoint, exchange, trade_id)
|
|
395
416
|
self.symbol = symbol
|
|
@@ -423,7 +444,7 @@ class HbpPrivateEventsDataStream(EventsDataStream):
|
|
|
423
444
|
"action": "sub",
|
|
424
445
|
"ch": f"trade.clearing#{self.symbol.lower()}#0"
|
|
425
446
|
}
|
|
426
|
-
|
|
447
|
+
tasks_manage(self.tasks, self.htx_keepalive(), f"htx_keepalive-user-{self.symbol}")
|
|
427
448
|
await self.ws_listener(request, symbol=self.symbol)
|
|
428
449
|
|
|
429
450
|
async def _handle_event(self, msg_data, *args):
|
|
@@ -446,11 +467,12 @@ class HbpPrivateEventsDataStream(EventsDataStream):
|
|
|
446
467
|
content = hbp.on_order_update(self.client.active_orders[order_id])
|
|
447
468
|
|
|
448
469
|
if content:
|
|
449
|
-
logger.debug(f"HTXPrivateEvents.content: {content}")
|
|
470
|
+
self.logger.debug(f"HTXPrivateEvents.content: {content}")
|
|
450
471
|
await self.client.events.wrap_event(content).fire(self.trade_id)
|
|
451
472
|
|
|
452
473
|
|
|
453
474
|
class BfxPrivateEventsDataStream(EventsDataStream):
|
|
475
|
+
__slots__ = ()
|
|
454
476
|
|
|
455
477
|
async def start_wss(self):
|
|
456
478
|
ts = int(time.time() * 1000)
|
|
@@ -505,6 +527,8 @@ class BfxPrivateEventsDataStream(EventsDataStream):
|
|
|
505
527
|
|
|
506
528
|
|
|
507
529
|
class OkxPrivateEventsDataStream(EventsDataStream):
|
|
530
|
+
__slots__ = ('symbol',)
|
|
531
|
+
|
|
508
532
|
def __init__(self, client, endpoint, exchange, trade_id, symbol):
|
|
509
533
|
super().__init__(client, endpoint, exchange, trade_id)
|
|
510
534
|
self.symbol = symbol
|
|
@@ -561,6 +585,7 @@ class OkxPrivateEventsDataStream(EventsDataStream):
|
|
|
561
585
|
|
|
562
586
|
|
|
563
587
|
class BBTPrivateEventsDataStream(EventsDataStream):
|
|
588
|
+
__slots__ = ()
|
|
564
589
|
|
|
565
590
|
async def start_wss(self):
|
|
566
591
|
ts = int((time.time() + 1) * 1000)
|
|
@@ -591,7 +616,7 @@ class BBTPrivateEventsDataStream(EventsDataStream):
|
|
|
591
616
|
await self.client.events.wrap_event(content).fire(self.trade_id)
|
|
592
617
|
content = None
|
|
593
618
|
elif ch_type == 'order.spot':
|
|
594
|
-
# logger.info(f"_handle_event: ch_type: {ch_type}, msg_data: {msg_data}")
|
|
619
|
+
# self.logger.info(f"_handle_event: ch_type: {ch_type}, msg_data: {msg_data}")
|
|
595
620
|
event = msg_data[0]
|
|
596
621
|
if event.get('orderStatus') in ("Cancelled", "PartiallyFilledCanceled"):
|
|
597
622
|
self.client.wss_buffer[f"oc-{event.get('orderId')}"] = bbt.order(event, response_type=True)
|
|
@@ -603,6 +628,7 @@ class BBTPrivateEventsDataStream(EventsDataStream):
|
|
|
603
628
|
|
|
604
629
|
|
|
605
630
|
class UserEventsDataStream(EventsDataStream):
|
|
631
|
+
__slots__ = ()
|
|
606
632
|
|
|
607
633
|
async def start_wss(self):
|
|
608
634
|
await self.websocket.send(
|
|
@@ -620,5 +646,5 @@ class UserEventsDataStream(EventsDataStream):
|
|
|
620
646
|
await self.ws_listener(request)
|
|
621
647
|
|
|
622
648
|
async def _handle_event(self, content):
|
|
623
|
-
# logger.debug(f"UserEventsDataStream._handle_event.content: {content}")
|
|
649
|
+
# self.logger.debug(f"UserEventsDataStream._handle_event.content: {content}")
|
|
624
650
|
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.
|
|
23
|
+
"crypto-ws-api==2.1.3",
|
|
21
24
|
"pyotp==2.9.0",
|
|
22
|
-
"simplejson==3.20.
|
|
23
|
-
"aiohttp~=3.
|
|
25
|
+
"simplejson==3.20.2",
|
|
26
|
+
"aiohttp~=3.13.0",
|
|
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
|
{exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/exch_srv_cfg.toml.template
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|