crypto-ws-api 2.0.13__tar.gz → 2.0.15__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.
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/.deepsource.toml +0 -0
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/CHANGELOG.md +13 -0
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/LICENSE.md +0 -0
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/PKG-INFO +7 -6
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/README.md +2 -1
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/crypto_ws_api/__init__.py +1 -1
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/crypto_ws_api/demo.py +1 -1
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/crypto_ws_api/ws_api.toml.template +0 -0
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/crypto_ws_api/ws_session.py +146 -66
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/pyproject.toml +3 -3
- {crypto-ws-api-2.0.13 → crypto_ws_api-2.0.15}/requirements.txt +1 -1
|
File without changes
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 2.0.15 - 2024-12-16
|
|
2
|
+
### Added for new features
|
|
3
|
+
* `HTX` implemented
|
|
4
|
+
|
|
5
|
+
### Update
|
|
6
|
+
* reused WS connection grouped by private and public methods
|
|
7
|
+
* `websockets`: bump to v14.1
|
|
8
|
+
* some minor improvements
|
|
9
|
+
|
|
10
|
+
## 2.0.14 - 2024-09-13
|
|
11
|
+
### Update
|
|
12
|
+
* `pyproject.toml`
|
|
13
|
+
|
|
1
14
|
## 2.0.13 - 2024-09-13
|
|
2
15
|
### Update
|
|
3
16
|
* Dependencies
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: crypto-ws-api
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.15
|
|
4
4
|
Summary: Crypto WS API connector for ASYNC requests
|
|
5
5
|
Author-email: Jerry Fedorenko <jerry.fedorenko@yahoo.com>
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -11,10 +11,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
11
11
|
Classifier: Operating System :: Unix
|
|
12
12
|
Classifier: Operating System :: Microsoft :: Windows
|
|
13
13
|
Classifier: Operating System :: MacOS
|
|
14
|
-
Requires-Dist: shortuuid~=1.0.
|
|
15
|
-
Requires-Dist: platformdirs
|
|
14
|
+
Requires-Dist: shortuuid~=1.0.13
|
|
15
|
+
Requires-Dist: platformdirs==3.11.0
|
|
16
16
|
Requires-Dist: toml~=0.10.2
|
|
17
|
-
Requires-Dist: websockets
|
|
17
|
+
Requires-Dist: websockets==14.1
|
|
18
18
|
Requires-Dist: ujson~=5.10.0
|
|
19
19
|
Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
|
|
20
20
|
|
|
@@ -32,7 +32,7 @@ Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
|
|
|
32
32
|
<a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_crypto-ws-api" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_crypto-ws-api&metric=alert_status"/></a>
|
|
33
33
|
<a href="https://pepy.tech/project/crypto-ws-api" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/crypto-ws-api"/></a>
|
|
34
34
|
***
|
|
35
|
-
For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex
|
|
35
|
+
For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex, :heavy_check_mark:HTX
|
|
36
36
|
***
|
|
37
37
|
|
|
38
38
|
## Features
|
|
@@ -40,6 +40,7 @@ Lightweight and efficient solution to utilize of all available methods** provide
|
|
|
40
40
|
* [Binance Websocket API v3](https://binance-docs.github.io/apidocs/websocket_api/en)
|
|
41
41
|
* [OKX Websocket API v5](https://www.okx.com/docs-v5/en/#overview-websocket)
|
|
42
42
|
* [Bitfinex Websocket Inputs](https://docs.bitfinex.com/reference/ws-auth-input)
|
|
43
|
+
* [HTX WS Trade](https://www.htx.com/en-us/opend/newApiPages/?id=8cb89359-77b5-11ed-9966-1928f079ab6)
|
|
43
44
|
|
|
44
45
|
** Not for channel by one-way subscription, for **_request <-> response_** mode only
|
|
45
46
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_crypto-ws-api" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_crypto-ws-api&metric=alert_status"/></a>
|
|
13
13
|
<a href="https://pepy.tech/project/crypto-ws-api" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/crypto-ws-api"/></a>
|
|
14
14
|
***
|
|
15
|
-
For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex
|
|
15
|
+
For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex, :heavy_check_mark:HTX
|
|
16
16
|
***
|
|
17
17
|
|
|
18
18
|
## Features
|
|
@@ -20,6 +20,7 @@ Lightweight and efficient solution to utilize of all available methods** provide
|
|
|
20
20
|
* [Binance Websocket API v3](https://binance-docs.github.io/apidocs/websocket_api/en)
|
|
21
21
|
* [OKX Websocket API v5](https://www.okx.com/docs-v5/en/#overview-websocket)
|
|
22
22
|
* [Bitfinex Websocket Inputs](https://docs.bitfinex.com/reference/ws-auth-input)
|
|
23
|
+
* [HTX WS Trade](https://www.htx.com/en-us/opend/newApiPages/?id=8cb89359-77b5-11ed-9966-1928f079ab6)
|
|
23
24
|
|
|
24
25
|
** Not for channel by one-way subscription, for **_request <-> response_** mode only
|
|
25
26
|
|
|
@@ -13,7 +13,7 @@ __maintainer__ = "Jerry Fedorenko"
|
|
|
13
13
|
__contact__ = "https://github.com/DogsTailFarmer"
|
|
14
14
|
__email__ = "jerry.fedorenko@yahoo.com"
|
|
15
15
|
__credits__ = ["https://github.com/DanyaSWorlD"]
|
|
16
|
-
__version__ = "2.0.
|
|
16
|
+
__version__ = "2.0.15"
|
|
17
17
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
import shutil
|
|
File without changes
|
|
@@ -6,39 +6,59 @@ import sys
|
|
|
6
6
|
import time
|
|
7
7
|
import logging
|
|
8
8
|
import ujson as json
|
|
9
|
+
from ujson import JSONDecodeError
|
|
9
10
|
import hmac
|
|
10
11
|
import hashlib
|
|
11
12
|
import base64
|
|
12
13
|
import string
|
|
13
14
|
import random
|
|
14
|
-
import
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from urllib.parse import urlencode, urlparse
|
|
15
17
|
|
|
18
|
+
from websockets.asyncio.client import connect
|
|
16
19
|
from websockets import ConnectionClosed
|
|
17
20
|
|
|
18
21
|
from enum import Enum
|
|
19
22
|
from crypto_ws_api import TIMEOUT, ID_LEN_LIMIT, DELAY
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
logger.level = logging.INFO
|
|
23
26
|
sys.tracebacklimit = 0
|
|
24
27
|
ALPHABET = string.ascii_letters + string.digits
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def generate_signature(exchange, api_secret, data):
|
|
29
|
-
if exchange == 'bitfinex':
|
|
30
|
-
sig = hmac.new(api_secret.encode("utf-8"), data.encode("utf-8"), hashlib.sha384).hexdigest()
|
|
31
|
-
elif exchange in ('huobi', 'okx'):
|
|
32
|
-
sig = hmac.new(api_secret.encode("utf-8"), data.encode("utf-8"), hashlib.sha256).digest()
|
|
33
|
-
sig = base64.b64encode(sig).decode()
|
|
34
|
-
elif exchange == 'binance_ws':
|
|
35
|
-
sig = hmac.new(api_secret.encode("ascii"), data.encode("ascii"), hashlib.sha256).hexdigest()
|
|
36
|
-
elif exchange == 'bybit':
|
|
37
|
-
sig = hmac.new(bytes(api_secret.encode("utf-8")), data.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
38
|
-
else:
|
|
39
|
-
sig = hmac.new(api_secret.encode("utf-8"), data.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
40
|
-
return sig
|
|
28
|
+
CONST_WS_START = "userDataStream.start"
|
|
29
|
+
|
|
41
30
|
|
|
31
|
+
def generate_signature(exchange: str, secret: str, data: str) -> str:
|
|
32
|
+
digest_func = hashlib.sha384 if exchange == 'bitfinex' else hashlib.sha256
|
|
33
|
+
encoding = "ascii" if exchange == 'binance_ws' else "utf-8"
|
|
34
|
+
key = secret.encode(encoding=encoding)
|
|
35
|
+
signature = hmac.new(
|
|
36
|
+
bytes(key) if exchange == 'bybit' else key,
|
|
37
|
+
data.encode(encoding=encoding),
|
|
38
|
+
digest_func
|
|
39
|
+
)
|
|
40
|
+
if exchange in {"huobi", "okx"}:
|
|
41
|
+
return base64.b64encode(signature.digest()).decode()
|
|
42
|
+
else:
|
|
43
|
+
return signature.hexdigest()
|
|
44
|
+
|
|
45
|
+
def compose_htx_ws_auth(endpoint, exchange, api_key, api_secret):
|
|
46
|
+
_params = {
|
|
47
|
+
"accessKey": api_key,
|
|
48
|
+
"signatureMethod": "HmacSHA256",
|
|
49
|
+
"signatureVersion": "2.1",
|
|
50
|
+
"timestamp": str(datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S"))
|
|
51
|
+
}
|
|
52
|
+
signature_payload = (f"GET\n{urlparse(endpoint).hostname}\n{urlparse(endpoint).path}\n"
|
|
53
|
+
f"{urlencode(_params)}")
|
|
54
|
+
signature = generate_signature(exchange, api_secret, signature_payload)
|
|
55
|
+
_params["authType"] = "api"
|
|
56
|
+
_params["signature"] = signature
|
|
57
|
+
return {
|
|
58
|
+
"action": "req",
|
|
59
|
+
"ch": "auth",
|
|
60
|
+
"params": _params
|
|
61
|
+
}
|
|
42
62
|
|
|
43
63
|
# https://binance-docs.github.io/apidocs/websocket_api/en/#rate-limits
|
|
44
64
|
class RateLimitInterval(Enum):
|
|
@@ -51,11 +71,11 @@ class RateLimitInterval(Enum):
|
|
|
51
71
|
class UserWSS:
|
|
52
72
|
__slots__ = (
|
|
53
73
|
"init",
|
|
54
|
-
"method",
|
|
55
74
|
"exchange",
|
|
56
75
|
"endpoint",
|
|
57
76
|
"_api_key",
|
|
58
77
|
"_api_secret",
|
|
78
|
+
"signed",
|
|
59
79
|
"_passphrase",
|
|
60
80
|
"_ws",
|
|
61
81
|
"_listen_key",
|
|
@@ -67,16 +87,17 @@ class UserWSS:
|
|
|
67
87
|
"in_event",
|
|
68
88
|
"_response_pool",
|
|
69
89
|
"tasks",
|
|
90
|
+
"ping"
|
|
70
91
|
)
|
|
71
92
|
|
|
72
|
-
def __init__(self,
|
|
93
|
+
def __init__(self, ws_id, exchange, endpoint, api_key, api_secret, signed, passphrase=None):
|
|
73
94
|
self.init = True
|
|
74
|
-
self.method = method
|
|
75
95
|
self.exchange = exchange
|
|
76
96
|
self.endpoint = endpoint
|
|
77
97
|
#
|
|
78
98
|
self._api_key = api_key
|
|
79
99
|
self._api_secret = api_secret
|
|
100
|
+
self.signed = signed
|
|
80
101
|
self._passphrase = passphrase
|
|
81
102
|
self._ws = None
|
|
82
103
|
self._listen_key = None
|
|
@@ -88,6 +109,7 @@ class UserWSS:
|
|
|
88
109
|
self.request_limit_reached = False
|
|
89
110
|
self.in_event = asyncio.Event()
|
|
90
111
|
self.tasks = set()
|
|
112
|
+
self.ping = 0
|
|
91
113
|
|
|
92
114
|
def tasks_manage(self, coro, name=None):
|
|
93
115
|
_t = asyncio.create_task(coro, name=name)
|
|
@@ -99,27 +121,39 @@ class UserWSS:
|
|
|
99
121
|
async for msg in self._ws:
|
|
100
122
|
# logger.info(f"_ws_listener: msg: {self.ws_id}: {msg}")
|
|
101
123
|
if isinstance(msg, str):
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
try:
|
|
125
|
+
_msg = json.loads(msg)
|
|
126
|
+
except JSONDecodeError:
|
|
127
|
+
logger.warning(f"UserWSS: {self.ws_id}: {msg}")
|
|
128
|
+
else:
|
|
129
|
+
res = await self._handle_msg(_msg)
|
|
130
|
+
# logger.info(f"_ws_listener: res: {self.ws_id}: {res}")
|
|
131
|
+
if res != 'pass':
|
|
132
|
+
if res is None:
|
|
133
|
+
self._response_pool[f"NoneResponse{self.ws_id}"] = None
|
|
134
|
+
elif self.exchange == 'binance':
|
|
135
|
+
self._response_pool[res.get('id')] = res.get('result')
|
|
136
|
+
elif self.exchange in {'okx', 'bitfinex'}:
|
|
137
|
+
self._response_pool[res.get('id') or self.ws_id] = res.get('data') or res
|
|
138
|
+
elif self.exchange == 'huobi':
|
|
139
|
+
self._response_pool[res.get('cid') or self.ws_id] = res.get('data')
|
|
140
|
+
self.in_event.set()
|
|
141
|
+
# logger.info(f"_ws_listener: _response_pool: {self._response_pool}")
|
|
142
|
+
await asyncio.sleep(0)
|
|
113
143
|
else:
|
|
114
144
|
logger.warning(f"UserWSS: {self.ws_id}: {msg}")
|
|
115
145
|
await self.stop()
|
|
116
146
|
|
|
117
147
|
async def start_wss(self):
|
|
118
|
-
async for self._ws in
|
|
148
|
+
async for self._ws in connect(
|
|
149
|
+
self.endpoint,
|
|
150
|
+
logger=logger,
|
|
151
|
+
ping_interval=None if self.exchange == 'huobi' else 20
|
|
152
|
+
):
|
|
119
153
|
try:
|
|
120
154
|
await self._ws_listener()
|
|
121
155
|
except ConnectionClosed as ex:
|
|
122
|
-
if ex.code == 4000:
|
|
156
|
+
if ex.rcvd and ex.rcvd.code == 4000:
|
|
123
157
|
logger.info(f"WSS closed for {self.ws_id}")
|
|
124
158
|
break
|
|
125
159
|
else:
|
|
@@ -129,10 +163,10 @@ class UserWSS:
|
|
|
129
163
|
logger.warning(f"Restart WSS for {self.ws_id}")
|
|
130
164
|
continue
|
|
131
165
|
except Exception as ex:
|
|
132
|
-
logger.error(f"WSS
|
|
166
|
+
logger.error(f"WSS start other exception: {ex}")
|
|
133
167
|
|
|
134
168
|
async def ws_login(self):
|
|
135
|
-
res = await self.request(
|
|
169
|
+
res = await self.request(CONST_WS_START, send_api_key=True)
|
|
136
170
|
if res is None:
|
|
137
171
|
logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
|
|
138
172
|
raise ConnectionClosed(None, None)
|
|
@@ -140,37 +174,37 @@ class UserWSS:
|
|
|
140
174
|
if self.exchange == 'binance':
|
|
141
175
|
self._listen_key = res.get('listenKey')
|
|
142
176
|
self.tasks_manage(self.heartbeat(), f"heartbeat-{self.ws_id}")
|
|
143
|
-
|
|
144
|
-
self.
|
|
177
|
+
elif self.exchange == 'huobi':
|
|
178
|
+
self.tasks_manage(self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
|
|
179
|
+
|
|
145
180
|
self.operational_status = True
|
|
146
181
|
self.order_handling = True
|
|
147
182
|
self.tasks_manage(self._keepalive(), f"keepalive-{self.ws_id}")
|
|
148
183
|
logger.info(f"UserWSS: 'logged in' for {self.ws_id}")
|
|
149
184
|
|
|
150
|
-
async def request(self,
|
|
185
|
+
async def request(self, method, _params=None, send_api_key=False, _signed=False):
|
|
151
186
|
"""
|
|
152
187
|
Construct and handling request/response to WS API endpoint, use a description of the methods on
|
|
153
188
|
https://developers.binance.com/docs/binance-trading-api/websocket_api#request-format
|
|
154
189
|
:return: result: {} or None if temporary Out-of-Service state
|
|
155
190
|
"""
|
|
156
|
-
method = _method or self.method
|
|
157
191
|
if self.request_limit_reached:
|
|
158
192
|
logger.warning(f"UserWSS {self.ws_id}: request limit reached, try later")
|
|
159
193
|
return None
|
|
160
|
-
if method !=
|
|
194
|
+
if method != CONST_WS_START and not self.operational_status:
|
|
161
195
|
logger.warning("UserWSS temporary in Out-of-Service state")
|
|
162
196
|
return None
|
|
163
|
-
if method in ('order.place', 'order.cancelReplace', 'order') and not self.order_handling:
|
|
197
|
+
if method in ('order.place', 'order.cancelReplace', 'order', 'create-order') and not self.order_handling:
|
|
164
198
|
logger.warning("UserWSS: exceeded order placement limit, try later")
|
|
165
199
|
return None
|
|
166
200
|
params = _params.copy() if _params else None
|
|
167
201
|
r_id = f"{self.exchange}{method}{''.join(random.choices(ALPHABET, k=8))}"
|
|
168
|
-
if self.exchange in ("okx", "bitfinex") and method ==
|
|
202
|
+
if self.exchange in ("okx", "bitfinex", "huobi") and method == CONST_WS_START:
|
|
169
203
|
_id = self.ws_id
|
|
170
204
|
else:
|
|
171
|
-
_id = ''.join(e for e in r_id if e.isalnum())[-ID_LEN_LIMIT
|
|
205
|
+
_id = ''.join(e for e in r_id if e.isalnum())[-ID_LEN_LIMIT.get(self.exchange, 64):]
|
|
172
206
|
await self._ws.send(
|
|
173
|
-
json.dumps(self.compose_request(_id,
|
|
207
|
+
json.dumps(self.compose_request(_id, send_api_key, method, params, _signed))
|
|
174
208
|
)
|
|
175
209
|
await asyncio.sleep(0)
|
|
176
210
|
try:
|
|
@@ -193,20 +227,28 @@ class UserWSS:
|
|
|
193
227
|
elif f"NoneResponse{self.ws_id}" in self._response_pool:
|
|
194
228
|
return self._response_pool.pop(f"NoneResponse{self.ws_id}", None)
|
|
195
229
|
|
|
196
|
-
def compose_request(self, _id,
|
|
230
|
+
def compose_request(self, _id, send_api_key, method, params, signed):
|
|
197
231
|
if self.exchange == "binance":
|
|
198
|
-
return self._compose_binance_request(_id,
|
|
232
|
+
return self._compose_binance_request(_id, send_api_key, method, params, signed)
|
|
199
233
|
elif self.exchange == "okx":
|
|
200
234
|
return self._compose_okx_request(_id, method, params)
|
|
201
|
-
elif self.exchange ==
|
|
235
|
+
elif self.exchange == "bitfinex":
|
|
202
236
|
return self._compose_bitfinex_request(_id, method, params)
|
|
237
|
+
elif self.exchange == "huobi":
|
|
238
|
+
return self._compose_htx_request(_id, method, params)
|
|
203
239
|
else:
|
|
204
240
|
raise ValueError(f"Unsupported exchange: {self.exchange}")
|
|
205
241
|
|
|
206
|
-
def
|
|
242
|
+
def _compose_htx_request(self, _id, method, params):
|
|
243
|
+
if method == CONST_WS_START:
|
|
244
|
+
return compose_htx_ws_auth(self.endpoint, self.exchange, self._api_key, self._api_secret)
|
|
245
|
+
else:
|
|
246
|
+
return {"cid": _id, "ch": method, "params": params}
|
|
247
|
+
|
|
248
|
+
def _compose_binance_request(self, _id, send_api_key, method, params, signed):
|
|
207
249
|
req = {"id": _id, "method": method}
|
|
208
250
|
params = params or {}
|
|
209
|
-
if
|
|
251
|
+
if send_api_key:
|
|
210
252
|
params["apiKey"] = self._api_key
|
|
211
253
|
if signed:
|
|
212
254
|
params["timestamp"] = int(time.time() * 1000)
|
|
@@ -217,7 +259,7 @@ class UserWSS:
|
|
|
217
259
|
return req
|
|
218
260
|
|
|
219
261
|
def _compose_okx_request(self, _id, method, params):
|
|
220
|
-
if method ==
|
|
262
|
+
if method == CONST_WS_START:
|
|
221
263
|
ts = int(time.time())
|
|
222
264
|
signature_payload = f"{ts}GET/users/self/verify"
|
|
223
265
|
signature = generate_signature(self.exchange, self._api_secret, signature_payload)
|
|
@@ -231,7 +273,7 @@ class UserWSS:
|
|
|
231
273
|
return {"id": _id, "op": method, "args": params if isinstance(params, list) else [params]}
|
|
232
274
|
|
|
233
275
|
def _compose_bitfinex_request(self, _id, method, params):
|
|
234
|
-
if method ==
|
|
276
|
+
if method == CONST_WS_START:
|
|
235
277
|
ts = int(time.time() * 1000)
|
|
236
278
|
data = f"AUTH{ts}"
|
|
237
279
|
return {
|
|
@@ -265,10 +307,21 @@ class UserWSS:
|
|
|
265
307
|
await self.request(
|
|
266
308
|
"userDataStream.ping",
|
|
267
309
|
params,
|
|
268
|
-
|
|
310
|
+
send_api_key=True,
|
|
269
311
|
)
|
|
270
312
|
await asyncio.sleep(interval)
|
|
271
313
|
|
|
314
|
+
async def htx_keepalive(self, interval=60):
|
|
315
|
+
await asyncio.sleep(interval * 10)
|
|
316
|
+
while True:
|
|
317
|
+
await asyncio.sleep(interval)
|
|
318
|
+
if self.ping:
|
|
319
|
+
break
|
|
320
|
+
else:
|
|
321
|
+
self.ping = 1
|
|
322
|
+
logger.warning("From HTX server PING timeout exceeded")
|
|
323
|
+
await self.stop()
|
|
324
|
+
|
|
272
325
|
async def stop(self):
|
|
273
326
|
"""
|
|
274
327
|
Stop data stream
|
|
@@ -278,7 +331,7 @@ class UserWSS:
|
|
|
278
331
|
self.init = True
|
|
279
332
|
[task.cancel() for task in self.tasks if not task.done()]
|
|
280
333
|
self.tasks.clear()
|
|
281
|
-
if self._ws
|
|
334
|
+
if self._ws:
|
|
282
335
|
await self._ws.close(code=4000)
|
|
283
336
|
gc.collect()
|
|
284
337
|
logger.info(f"User WSS for {self.ws_id} stopped")
|
|
@@ -292,11 +345,37 @@ class UserWSS:
|
|
|
292
345
|
return msg
|
|
293
346
|
elif self.exchange == 'okx':
|
|
294
347
|
if msg.get('code') != '0':
|
|
295
|
-
|
|
348
|
+
self.okx_error_handle(msg)
|
|
296
349
|
msg = None
|
|
297
350
|
return msg
|
|
298
351
|
elif self.exchange == 'bitfinex':
|
|
299
352
|
return await self.bitfinex_error_handle(msg)
|
|
353
|
+
elif self.exchange == 'huobi':
|
|
354
|
+
if msg.get('action') == 'ping':
|
|
355
|
+
pong = {
|
|
356
|
+
"action": "pong",
|
|
357
|
+
"data": {
|
|
358
|
+
"ts": msg.get('data').get('ts')
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
self.ping = 0
|
|
362
|
+
await self._ws.send(json.dumps(pong))
|
|
363
|
+
return 'pass'
|
|
364
|
+
|
|
365
|
+
if msg.get('code') == 200 or msg.get('status') == 'ok':
|
|
366
|
+
return msg
|
|
367
|
+
|
|
368
|
+
await self.htx_error_handle(msg)
|
|
369
|
+
|
|
370
|
+
async def htx_error_handle(self, msg):
|
|
371
|
+
if msg.get('code') == 500:
|
|
372
|
+
logger.warning(f"An issue occurred on exchange's side: {msg}")
|
|
373
|
+
elif msg.get('code') in {429, 4000}:
|
|
374
|
+
self._retry_after = int((time.time() + TIMEOUT) * 1000)
|
|
375
|
+
self.request_limit_reached = True
|
|
376
|
+
logger.warning(f"HTX WSS exceed limit: {msg}")
|
|
377
|
+
else:
|
|
378
|
+
logger.warning(f"Malformed request: status: {msg}")
|
|
300
379
|
|
|
301
380
|
# region BitfinexErrorHandle
|
|
302
381
|
async def bitfinex_error_handle(self, msg):
|
|
@@ -314,7 +393,7 @@ class UserWSS:
|
|
|
314
393
|
elif event == 'auth':
|
|
315
394
|
return msg if msg.get('status') == "OK" else None
|
|
316
395
|
elif msg.get('code'):
|
|
317
|
-
return
|
|
396
|
+
return self._handle_error_code(msg)
|
|
318
397
|
return 'pass'
|
|
319
398
|
|
|
320
399
|
async def _handle_info_event(self, msg):
|
|
@@ -325,7 +404,7 @@ class UserWSS:
|
|
|
325
404
|
logger.critical('Bitfinex WSS platform: version change detected')
|
|
326
405
|
return 'pass'
|
|
327
406
|
|
|
328
|
-
|
|
407
|
+
def _handle_error_code(self, msg):
|
|
329
408
|
code = msg.get('code')
|
|
330
409
|
if code == 10305:
|
|
331
410
|
logger.warning('UserWSS Bitfinex: Reached limit of open channels')
|
|
@@ -333,7 +412,6 @@ class UserWSS:
|
|
|
333
412
|
self.request_limit_reached = True
|
|
334
413
|
else:
|
|
335
414
|
logger.warning(f"Malformed request for {self.ws_id}: {msg}")
|
|
336
|
-
return None
|
|
337
415
|
|
|
338
416
|
@staticmethod
|
|
339
417
|
def _transform_list_message(msg):
|
|
@@ -352,15 +430,16 @@ class UserWSS:
|
|
|
352
430
|
}
|
|
353
431
|
# endregion
|
|
354
432
|
|
|
355
|
-
|
|
433
|
+
def okx_error_handle(self, msg):
|
|
356
434
|
if msg.get('code') == '1':
|
|
357
|
-
logger.warning(f"
|
|
435
|
+
logger.warning(f"OKX User WSS operation failed: {msg}")
|
|
358
436
|
elif msg.get('code') == '63999':
|
|
359
437
|
logger.warning(f"An issue occurred on exchange's side: {msg}")
|
|
360
438
|
elif msg.get('code') == '60014':
|
|
361
439
|
self._retry_after = int((time.time() + TIMEOUT) * 1000)
|
|
362
440
|
self.request_limit_reached = True
|
|
363
|
-
|
|
441
|
+
else:
|
|
442
|
+
logger.warning(f"Malformed request: status: {msg}")
|
|
364
443
|
|
|
365
444
|
async def binance_error_handle(self, msg):
|
|
366
445
|
error_msg = msg.get('error')
|
|
@@ -396,7 +475,7 @@ class UserWSSession:
|
|
|
396
475
|
)
|
|
397
476
|
|
|
398
477
|
def __init__(self, exchange, endpoint, api_key, api_secret, passphrase=None):
|
|
399
|
-
if exchange not in ('binance', 'okx', 'bitfinex'):
|
|
478
|
+
if exchange not in ('binance', 'okx', 'bitfinex', 'huobi'):
|
|
400
479
|
raise UserWarning(f"UserWSSession: exchange {exchange} not serviced")
|
|
401
480
|
self.exchange = exchange
|
|
402
481
|
self.endpoint = endpoint
|
|
@@ -412,19 +491,20 @@ class UserWSSession:
|
|
|
412
491
|
trade_id: str,
|
|
413
492
|
method: str,
|
|
414
493
|
_params=None,
|
|
415
|
-
|
|
494
|
+
send_api_key=False,
|
|
416
495
|
_signed=False,
|
|
417
496
|
):
|
|
418
497
|
ws_id = f"{self.exchange}-{trade_id}-{method}"
|
|
498
|
+
|
|
419
499
|
user_wss = self.user_wss.setdefault(
|
|
420
500
|
ws_id,
|
|
421
501
|
UserWSS(
|
|
422
|
-
method,
|
|
423
502
|
ws_id,
|
|
424
503
|
self.exchange,
|
|
425
504
|
self.endpoint,
|
|
426
505
|
self._api_key,
|
|
427
506
|
self._api_secret,
|
|
507
|
+
_signed,
|
|
428
508
|
self._passphrase
|
|
429
509
|
)
|
|
430
510
|
)
|
|
@@ -444,7 +524,7 @@ class UserWSSession:
|
|
|
444
524
|
duration += DELAY
|
|
445
525
|
|
|
446
526
|
try:
|
|
447
|
-
return await user_wss.request(_params=_params,
|
|
527
|
+
return await user_wss.request(method=method, _params=_params, send_api_key=send_api_key, _signed=_signed)
|
|
448
528
|
except asyncio.CancelledError:
|
|
449
529
|
pass # Task cancellation should not be logged as an error
|
|
450
530
|
except Exception as ex:
|
|
@@ -17,10 +17,10 @@ dynamic = ["version", "description"]
|
|
|
17
17
|
requires-python = ">=3.8"
|
|
18
18
|
|
|
19
19
|
dependencies = [
|
|
20
|
-
"shortuuid~=1.0.
|
|
21
|
-
"platformdirs
|
|
20
|
+
"shortuuid~=1.0.13",
|
|
21
|
+
"platformdirs==3.11.0",
|
|
22
22
|
"toml~=0.10.2",
|
|
23
|
-
"websockets
|
|
23
|
+
"websockets==14.1",
|
|
24
24
|
"ujson~=5.10.0"
|
|
25
25
|
]
|
|
26
26
|
|