crypto-ws-api 2.0.19__tar.gz → 2.1.0__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.19 → crypto_ws_api-2.1.0}/CHANGELOG.md +18 -0
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.1.0}/PKG-INFO +7 -5
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.1.0}/README.md +4 -3
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.1.0}/crypto_ws_api/__init__.py +1 -1
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.1.0}/crypto_ws_api/demo.py +0 -7
- crypto_ws_api-2.1.0/crypto_ws_api/ws_api.toml.template +25 -0
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.1.0}/crypto_ws_api/ws_session.py +86 -77
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.1.0}/pyproject.toml +3 -2
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.1.0}/requirements.txt +2 -1
- crypto_ws_api-2.0.19/crypto_ws_api/ws_api.toml.template +0 -19
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.1.0}/.deepsource.toml +0 -0
- {crypto_ws_api-2.0.19 → crypto_ws_api-2.1.0}/LICENSE.md +0 -0
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## 2.1.0 - 2025-06-22
|
|
2
|
+
✨ refactor(ws_session.py): simplify `ws_login` and `request` methods by removing redundant `send_api_key` parameter.
|
|
3
|
+
|
|
4
|
+
## 2.0.21 - 2025-06-22
|
|
5
|
+
🔧 fix(README.md): Improve documentation layout for better readability
|
|
6
|
+
🔧 fix(ws_session.py): Handle `asyncio.CancelledError` gracefully in `start_wss` method
|
|
7
|
+
🔧 fix(ws_session.py): Replace `asyncio.CancelledError` with `KeyboardInterrupt` exception in `handle_request` method
|
|
8
|
+
🔧 fix(demo.py): Remove unnecessary `asyncio.CancelledError` handling to clean up error logging
|
|
9
|
+
🔧 fix(ws_api.toml.template): Update testnet URL for Binance websocket API
|
|
10
|
+
|
|
11
|
+
## 2.0.20 - 2025-04-24
|
|
12
|
+
* On `Binance` now API key type [Ed25519](https://www.binance.com/en/support/faq/detail/6b9a63f1e3384cf48a2eedb82767a69a) is used instead of `HMAC`
|
|
13
|
+
✨ feat(ws_session): implement `compose_binance_ws_auth` for Binance WebSocket authentication
|
|
14
|
+
🔧 fix(ws_session): update `generate_signature` for improved signature handling
|
|
15
|
+
🔧 fix(ws_session): handle `None` return in `request` method
|
|
16
|
+
🚀 chore(requirements): add `cryptography` dependency for signature handling
|
|
17
|
+
🔧 fix(pyproject): require Python version 3.10 or higher
|
|
18
|
+
|
|
1
19
|
## 2.0.19 - 2025-03-28
|
|
2
20
|
### Fix
|
|
3
21
|
* `ws_session.ws_login`: Incorrect exit from authentication procedure in case of unsuccessful attempt
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: crypto-ws-api
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Crypto WS API connector for ASYNC requests
|
|
5
5
|
Author-email: Jerry Fedorenko <jerry.fedorenko@yahoo.com>
|
|
6
|
-
Requires-Python: >=3.
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -16,6 +16,7 @@ Requires-Dist: platformdirs==4.3.6
|
|
|
16
16
|
Requires-Dist: toml~=0.10.2
|
|
17
17
|
Requires-Dist: websockets==15.0.1
|
|
18
18
|
Requires-Dist: ujson~=5.10.0
|
|
19
|
+
Requires-Dist: cryptography~=44.0.2
|
|
19
20
|
Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
|
|
20
21
|
|
|
21
22
|
<h1 align="center"><img align="center" src="https://user-images.githubusercontent.com/77513676/250364389-cbedc171-a930-4467-a0cd-21627a6a41ed.svg" width="75">Crypto WS API connector for ASYNC requests</h1>
|
|
@@ -25,14 +26,15 @@ Project-URL: Source, https://github.com/DogsTailFarmer/crypto-ws-api
|
|
|
25
26
|
<h3 align="center">Provides of connection management, keepalive and rate limits control</h3>
|
|
26
27
|
|
|
27
28
|
***
|
|
28
|
-
<a href="https://pypi.org/project/crypto-ws-api/"><img src="https://img.shields.io/pypi/v/crypto-ws-api" alt="PyPI version"></a>
|
|
29
|
-
<a href="https://codeclimate.com/github/DogsTailFarmer/crypto-ws-api/maintainability"><img src="https://api.codeclimate.com/v1/badges/2d2a654ba393eb88d911/maintainability" /></a>
|
|
29
|
+
<h1 align="center"><a href="https://pypi.org/project/crypto-ws-api/"><img src="https://img.shields.io/pypi/v/crypto-ws-api" alt="PyPI version"></a>
|
|
30
30
|
<a href="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api.svg/?label=resolved+issues&token=TXghPzbi0YWhkCLU8Q1tmDyQ"/></a>
|
|
31
31
|
<a href="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api.svg/?label=active+issues&token=TXghPzbi0YWhkCLU8Q1tmDyQ"/></a>
|
|
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
|
+
</h1>
|
|
35
|
+
|
|
34
36
|
***
|
|
35
|
-
For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex, :heavy_check_mark:HTX
|
|
37
|
+
For :heavy_check_mark:[Binance](https://accounts.binance.com/en/register?ref=FXQ6HY5O), :heavy_check_mark:[OKX](https://okx.com/join/2607649), :heavy_check_mark:[Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP), :heavy_check_mark:[HTX](https://www.htx.com/invite/en-us/1f?invite_code=9uaw3223)
|
|
36
38
|
***
|
|
37
39
|
|
|
38
40
|
## Features
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
<h3 align="center">Provides of connection management, keepalive and rate limits control</h3>
|
|
6
6
|
|
|
7
7
|
***
|
|
8
|
-
<a href="https://pypi.org/project/crypto-ws-api/"><img src="https://img.shields.io/pypi/v/crypto-ws-api" alt="PyPI version"></a>
|
|
9
|
-
<a href="https://codeclimate.com/github/DogsTailFarmer/crypto-ws-api/maintainability"><img src="https://api.codeclimate.com/v1/badges/2d2a654ba393eb88d911/maintainability" /></a>
|
|
8
|
+
<h1 align="center"><a href="https://pypi.org/project/crypto-ws-api/"><img src="https://img.shields.io/pypi/v/crypto-ws-api" alt="PyPI version"></a>
|
|
10
9
|
<a href="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api.svg/?label=resolved+issues&token=TXghPzbi0YWhkCLU8Q1tmDyQ"/></a>
|
|
11
10
|
<a href="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://app.deepsource.com/gh/DogsTailFarmer/crypto-ws-api.svg/?label=active+issues&token=TXghPzbi0YWhkCLU8Q1tmDyQ"/></a>
|
|
12
11
|
<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
12
|
<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>
|
|
13
|
+
</h1>
|
|
14
|
+
|
|
14
15
|
***
|
|
15
|
-
For :heavy_check_mark:Binance, :heavy_check_mark:OKX, :heavy_check_mark:Bitfinex, :heavy_check_mark:HTX
|
|
16
|
+
For :heavy_check_mark:[Binance](https://accounts.binance.com/en/register?ref=FXQ6HY5O), :heavy_check_mark:[OKX](https://okx.com/join/2607649), :heavy_check_mark:[Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP), :heavy_check_mark:[HTX](https://www.htx.com/invite/en-us/1f?invite_code=9uaw3223)
|
|
16
17
|
***
|
|
17
18
|
|
|
18
19
|
## Features
|
|
@@ -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.1.0"
|
|
17
17
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
import shutil
|
|
@@ -66,8 +66,6 @@ async def get_time(user_session: UserWSSession, _trade_id):
|
|
|
66
66
|
)
|
|
67
67
|
if res is None:
|
|
68
68
|
logger.warning(CONST_1)
|
|
69
|
-
except asyncio.CancelledError:
|
|
70
|
-
pass # Task cancellation should not be logged as an error
|
|
71
69
|
except Exception as _ex:
|
|
72
70
|
logger.error(CONST_2, _ex)
|
|
73
71
|
else:
|
|
@@ -87,8 +85,6 @@ async def current_average_price(user_session: UserWSSession, _trade_id):
|
|
|
87
85
|
)
|
|
88
86
|
if res is None:
|
|
89
87
|
logger.warning(CONST_1)
|
|
90
|
-
except asyncio.CancelledError:
|
|
91
|
-
pass # Task cancellation should not be logged as an error
|
|
92
88
|
except Exception as _ex:
|
|
93
89
|
logger.error(CONST_2, _ex)
|
|
94
90
|
else:
|
|
@@ -101,13 +97,10 @@ async def account_information(user_session: UserWSSession, _trade_id):
|
|
|
101
97
|
res = await user_session.handle_request(
|
|
102
98
|
_trade_id,
|
|
103
99
|
"account.status",
|
|
104
|
-
send_api_key=True,
|
|
105
100
|
_signed=True
|
|
106
101
|
)
|
|
107
102
|
if res is None:
|
|
108
103
|
logger.warning(CONST_1)
|
|
109
|
-
except asyncio.CancelledError:
|
|
110
|
-
pass # Task cancellation should not be logged as an error
|
|
111
104
|
except Exception as _ex:
|
|
112
105
|
logger.error(CONST_2, _ex)
|
|
113
106
|
else:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Parameters for crypto-ws-api connector
|
|
2
|
+
# Copyright © 2023-2025 Jerry Fedorenko aka VM
|
|
3
|
+
# __version__ = "2.0.21"
|
|
4
|
+
|
|
5
|
+
# region endpoint
|
|
6
|
+
[endpoint]
|
|
7
|
+
[endpoint.binance]
|
|
8
|
+
ws_api = 'wss://ws-api.binance.com:443/ws-api/v3'
|
|
9
|
+
ws_api_test = 'wss://ws-api.testnet.binance.vision:9443/ws-api/v3'
|
|
10
|
+
# endregion
|
|
11
|
+
|
|
12
|
+
# region Binance accounts
|
|
13
|
+
# How to Generate an Ed25519 Key Pair and Register then on Binance:
|
|
14
|
+
# https://www.binance.com/en/support/faq/detail/6b9a63f1e3384cf48a2eedb82767a69a
|
|
15
|
+
[[accounts]]
|
|
16
|
+
exchange = 'binance'
|
|
17
|
+
name = 'Demo - Binance'
|
|
18
|
+
api_key = '*********** Place API key there ************'
|
|
19
|
+
api_secret = """
|
|
20
|
+
-----BEGIN PRIVATE KEY-----
|
|
21
|
+
*********** Place Private Ed25519 key there ************
|
|
22
|
+
-----END PRIVATE KEY-----
|
|
23
|
+
"""
|
|
24
|
+
test_net = true
|
|
25
|
+
# endregion
|
|
@@ -15,6 +15,7 @@ import string
|
|
|
15
15
|
import random
|
|
16
16
|
from datetime import datetime, timezone
|
|
17
17
|
from urllib.parse import urlencode, urlparse
|
|
18
|
+
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
|
18
19
|
|
|
19
20
|
from websockets.asyncio.client import connect
|
|
20
21
|
from websockets import ConnectionClosed
|
|
@@ -49,20 +50,40 @@ def set_logger(name, log_file, file_level=logging.INFO, propagate=False):
|
|
|
49
50
|
|
|
50
51
|
logger = set_logger(__name__, Path(LOG_PATH, 'ws_session.log'))
|
|
51
52
|
|
|
53
|
+
|
|
52
54
|
def generate_signature(exchange: str, secret: str, data: str) -> str:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
digest_func
|
|
60
|
-
|
|
55
|
+
encoding = "utf-8"
|
|
56
|
+
if exchange == 'binance':
|
|
57
|
+
signature = load_pem_private_key(
|
|
58
|
+
data=bytes(secret, encoding=encoding), password=None).sign(data.encode("ascii")
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
digest_func = hashlib.sha384 if exchange == 'bitfinex' else hashlib.sha256
|
|
62
|
+
key = secret.encode(encoding=encoding)
|
|
63
|
+
signature = hmac.new(
|
|
64
|
+
bytes(key) if exchange == 'bybit' else key,
|
|
65
|
+
data.encode(encoding=encoding),
|
|
66
|
+
digest_func
|
|
67
|
+
)
|
|
61
68
|
if exchange in {"huobi", "okx"}:
|
|
62
|
-
|
|
69
|
+
signature = signature.digest()
|
|
70
|
+
|
|
71
|
+
if exchange in {"binance", "huobi", "okx"}:
|
|
72
|
+
return base64.b64encode(signature).decode()
|
|
63
73
|
else:
|
|
64
74
|
return signature.hexdigest()
|
|
65
75
|
|
|
76
|
+
def compose_binance_ws_auth(_id: str, api_key: str, api_secret: str) -> dict:
|
|
77
|
+
req = {"id": _id, "method": "session.logon"}
|
|
78
|
+
params = {
|
|
79
|
+
"apiKey": api_key,
|
|
80
|
+
"timestamp": int(time.time() * 1000)
|
|
81
|
+
}
|
|
82
|
+
payload = '&'.join(f"{key}={value}" for key, value in sorted(params.items()))
|
|
83
|
+
params["signature"] = generate_signature('binance', api_secret, payload)
|
|
84
|
+
req["params"] = params
|
|
85
|
+
return req
|
|
86
|
+
|
|
66
87
|
def compose_htx_ws_auth(endpoint, exchange, api_key, api_secret):
|
|
67
88
|
_params = {
|
|
68
89
|
"accessKey": api_key,
|
|
@@ -98,7 +119,6 @@ class UserWSS:
|
|
|
98
119
|
"_api_secret",
|
|
99
120
|
"_passphrase",
|
|
100
121
|
"_ws",
|
|
101
|
-
"_listen_key",
|
|
102
122
|
"_retry_after",
|
|
103
123
|
"ws_id",
|
|
104
124
|
"operational_status",
|
|
@@ -120,7 +140,6 @@ class UserWSS:
|
|
|
120
140
|
self._api_secret = api_secret
|
|
121
141
|
self._passphrase = passphrase
|
|
122
142
|
self._ws = None
|
|
123
|
-
self._listen_key = None
|
|
124
143
|
self._response_pool = {}
|
|
125
144
|
self._retry_after = int(time.time() * 1000) - 1
|
|
126
145
|
self.ws_id = ws_id
|
|
@@ -148,7 +167,7 @@ class UserWSS:
|
|
|
148
167
|
async def _ws_listener(self):
|
|
149
168
|
self.tasks_manage(self.ws_login())
|
|
150
169
|
async for msg in self._ws:
|
|
151
|
-
# logger.info(f"_ws_listener:
|
|
170
|
+
# logger.info(f"_ws_listener: ws_id: {self.ws_id} msg: {msg}")
|
|
152
171
|
if isinstance(msg, str):
|
|
153
172
|
try:
|
|
154
173
|
_msg = json.loads(msg)
|
|
@@ -172,40 +191,39 @@ class UserWSS:
|
|
|
172
191
|
else:
|
|
173
192
|
logger.warning(f"UserWSS: {self.ws_id}: {msg}")
|
|
174
193
|
await self.stop()
|
|
175
|
-
logger.warning(f"UserWSS: {self.ws_id}: _ws_listener stopped")
|
|
176
|
-
await self.stop()
|
|
177
194
|
|
|
178
195
|
async def start_wss(self):
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
try:
|
|
197
|
+
async for self._ws in connect(
|
|
198
|
+
self.endpoint,
|
|
199
|
+
logger=self.logger,
|
|
200
|
+
ping_interval=None if self.exchange in ('binance', 'huobi') else 20
|
|
201
|
+
):
|
|
202
|
+
try:
|
|
203
|
+
await self._ws_listener()
|
|
204
|
+
except ConnectionClosed as ex:
|
|
205
|
+
if ex.rcvd and ex.rcvd.code == 4000:
|
|
206
|
+
logger.info(f"WSS closed for {self.ws_id}")
|
|
207
|
+
break
|
|
208
|
+
else:
|
|
209
|
+
self.operational_status = False
|
|
210
|
+
[task.cancel() for task in self.tasks if not task.done()]
|
|
211
|
+
self.tasks.clear()
|
|
212
|
+
logger.warning(f"Restart UserWSS for {self.ws_id}")
|
|
213
|
+
continue
|
|
214
|
+
except Exception as ex:
|
|
215
|
+
logger.error(f"UserWSS start other exception: {ex}")
|
|
216
|
+
except asyncio.CancelledError:
|
|
217
|
+
await self.stop()
|
|
218
|
+
raise
|
|
198
219
|
|
|
199
220
|
async def ws_login(self):
|
|
200
|
-
res = await self.request(CONST_WS_START
|
|
221
|
+
res = await self.request(CONST_WS_START)
|
|
201
222
|
if res is None:
|
|
202
223
|
logger.warning(f"UserWSS: Not 'logged in' for {self.ws_id}")
|
|
203
224
|
await self.stop()
|
|
204
225
|
else:
|
|
205
|
-
if self.exchange == '
|
|
206
|
-
self._listen_key = res.get('listenKey')
|
|
207
|
-
self.tasks_manage(self.heartbeat(), f"heartbeat-{self.ws_id}")
|
|
208
|
-
elif self.exchange == 'huobi':
|
|
226
|
+
if self.exchange == 'huobi':
|
|
209
227
|
self.tasks_manage(self.htx_keepalive(), f"htx_keepalive-{self.ws_id}")
|
|
210
228
|
|
|
211
229
|
self.operational_status = True
|
|
@@ -213,10 +231,10 @@ class UserWSS:
|
|
|
213
231
|
self.tasks_manage(self._keepalive(), f"keepalive-{self.ws_id}")
|
|
214
232
|
logger.info(f"UserWSS: 'logged in' for {self.ws_id}")
|
|
215
233
|
|
|
216
|
-
async def request(self, method, _params=None,
|
|
234
|
+
async def request(self, method, _params=None, _signed=False):
|
|
217
235
|
"""
|
|
218
236
|
Construct and handling request/response to WS API endpoint, use a description of the methods on
|
|
219
|
-
https://developers.binance.com/docs/binance-
|
|
237
|
+
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api
|
|
220
238
|
:return: result: {} or None if temporary Out-of-Service state
|
|
221
239
|
"""
|
|
222
240
|
if self.request_limit_reached:
|
|
@@ -235,7 +253,7 @@ class UserWSS:
|
|
|
235
253
|
else:
|
|
236
254
|
_id = ''.join(e for e in r_id if e.isalnum())[-ID_LEN_LIMIT.get(self.exchange, 64):]
|
|
237
255
|
await self._ws.send(
|
|
238
|
-
json.dumps(self.compose_request(_id,
|
|
256
|
+
json.dumps(self.compose_request(_id, method, params, _signed))
|
|
239
257
|
)
|
|
240
258
|
await asyncio.sleep(0)
|
|
241
259
|
try:
|
|
@@ -243,8 +261,7 @@ class UserWSS:
|
|
|
243
261
|
except asyncio.exceptions.TimeoutError:
|
|
244
262
|
logger.warning(f"UserWSS: get response timeout error: {self.ws_id}")
|
|
245
263
|
await self.stop()
|
|
246
|
-
|
|
247
|
-
pass # Task cancellation should not be logged as an error
|
|
264
|
+
return None
|
|
248
265
|
else:
|
|
249
266
|
return res
|
|
250
267
|
|
|
@@ -256,10 +273,11 @@ class UserWSS:
|
|
|
256
273
|
return self._response_pool.pop(_id)
|
|
257
274
|
elif f"NoneResponse{self.ws_id}" in self._response_pool:
|
|
258
275
|
return self._response_pool.pop(f"NoneResponse{self.ws_id}", None)
|
|
276
|
+
return None
|
|
259
277
|
|
|
260
|
-
def compose_request(self, _id,
|
|
278
|
+
def compose_request(self, _id, method, params, signed):
|
|
261
279
|
if self.exchange == "binance":
|
|
262
|
-
return self._compose_binance_request(_id,
|
|
280
|
+
return self._compose_binance_request(_id, method, params, signed)
|
|
263
281
|
elif self.exchange == "okx":
|
|
264
282
|
return self._compose_okx_request(_id, method, params)
|
|
265
283
|
elif self.exchange == "bitfinex":
|
|
@@ -275,18 +293,17 @@ class UserWSS:
|
|
|
275
293
|
else:
|
|
276
294
|
return {"cid": _id, "ch": method, "params": params}
|
|
277
295
|
|
|
278
|
-
def _compose_binance_request(self, _id,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
params
|
|
287
|
-
|
|
288
|
-
req
|
|
289
|
-
return req
|
|
296
|
+
def _compose_binance_request(self, _id, method, params, signed):
|
|
297
|
+
if method == CONST_WS_START:
|
|
298
|
+
return compose_binance_ws_auth(_id, self._api_key, self._api_secret)
|
|
299
|
+
else:
|
|
300
|
+
req = {"id": _id, "method": method}
|
|
301
|
+
params = params or {}
|
|
302
|
+
if signed:
|
|
303
|
+
params["timestamp"] = int(time.time() * 1000)
|
|
304
|
+
if params:
|
|
305
|
+
req["params"] = params
|
|
306
|
+
return req
|
|
290
307
|
|
|
291
308
|
def _compose_okx_request(self, _id, method, params):
|
|
292
309
|
if method == CONST_WS_START:
|
|
@@ -329,18 +346,6 @@ class UserWSS:
|
|
|
329
346
|
logger.info(f"UserWSS order handling status restored for {self.ws_id}")
|
|
330
347
|
await asyncio.sleep(interval)
|
|
331
348
|
|
|
332
|
-
async def heartbeat(self, interval=60 * 30):
|
|
333
|
-
params = {
|
|
334
|
-
"listenKey": self._listen_key,
|
|
335
|
-
}
|
|
336
|
-
while self.operational_status is not None:
|
|
337
|
-
await self.request(
|
|
338
|
-
"userDataStream.ping",
|
|
339
|
-
params,
|
|
340
|
-
send_api_key=True,
|
|
341
|
-
)
|
|
342
|
-
await asyncio.sleep(interval)
|
|
343
|
-
|
|
344
349
|
async def htx_keepalive(self, interval=60):
|
|
345
350
|
await asyncio.sleep(interval * 10)
|
|
346
351
|
while True:
|
|
@@ -395,9 +400,10 @@ class UserWSS:
|
|
|
395
400
|
if msg.get('code') == 200 or msg.get('status') == 'ok':
|
|
396
401
|
return msg
|
|
397
402
|
|
|
398
|
-
|
|
403
|
+
self.htx_error_handle(msg)
|
|
404
|
+
return None
|
|
399
405
|
|
|
400
|
-
|
|
406
|
+
def htx_error_handle(self, msg):
|
|
401
407
|
if msg.get('code') == 500:
|
|
402
408
|
logger.warning(f"An issue occurred on exchange's side: {msg}")
|
|
403
409
|
elif msg.get('code') in {429, 4000}:
|
|
@@ -474,13 +480,13 @@ class UserWSS:
|
|
|
474
480
|
async def binance_error_handle(self, msg):
|
|
475
481
|
error_msg = msg.get('error')
|
|
476
482
|
logger.error(f"Malformed request: status: {error_msg}")
|
|
477
|
-
if msg.get('status')
|
|
483
|
+
if msg.get('status') in (401, 403):
|
|
478
484
|
await self.stop()
|
|
479
485
|
if msg.get('status') in (418, 429):
|
|
480
486
|
self._retry_after = error_msg.get('data', {}).get('retryAfter', int((time.time() + TIMEOUT) * 1000))
|
|
481
487
|
self.request_limit_reached = True
|
|
482
488
|
|
|
483
|
-
def _handle_rate_limits(self, rate_limits:
|
|
489
|
+
def _handle_rate_limits(self, rate_limits: list):
|
|
484
490
|
def retry_after():
|
|
485
491
|
return (int(time.time() / interval) + 1) * interval * 1000
|
|
486
492
|
for rl in rate_limits:
|
|
@@ -521,11 +527,14 @@ class UserWSSession:
|
|
|
521
527
|
trade_id: str,
|
|
522
528
|
method: str,
|
|
523
529
|
_params=None,
|
|
524
|
-
send_api_key=False,
|
|
525
530
|
_signed=False,
|
|
526
531
|
):
|
|
527
532
|
|
|
528
|
-
ws_id = f"{self.exchange}-{trade_id}
|
|
533
|
+
ws_id = f"{self.exchange}-{trade_id}"
|
|
534
|
+
if self.exchange in ('binance', 'vertex'):
|
|
535
|
+
ws_id = f"{ws_id}{'_ST' if _signed else '_SF'}"
|
|
536
|
+
else:
|
|
537
|
+
ws_id = f"{ws_id}-{method}"
|
|
529
538
|
|
|
530
539
|
if ws_id in self.user_wss:
|
|
531
540
|
user_wss = self.user_wss[ws_id]
|
|
@@ -557,8 +566,8 @@ class UserWSSession:
|
|
|
557
566
|
duration += DELAY
|
|
558
567
|
|
|
559
568
|
try:
|
|
560
|
-
return await user_wss.request(method=method, _params=_params,
|
|
561
|
-
except
|
|
569
|
+
return await user_wss.request(method=method, _params=_params, _signed=_signed)
|
|
570
|
+
except KeyboardInterrupt:
|
|
562
571
|
pass # Task cancellation should not be logged as an error
|
|
563
572
|
except Exception as ex:
|
|
564
573
|
logger.error(f"crypto_ws_api.ws_session.handle_request(): {ex}")
|
|
@@ -14,14 +14,15 @@ classifiers=["Programming Language :: Python :: 3",
|
|
|
14
14
|
"Operating System :: Microsoft :: Windows",
|
|
15
15
|
"Operating System :: MacOS"]
|
|
16
16
|
dynamic = ["version", "description"]
|
|
17
|
-
requires-python = ">=3.
|
|
17
|
+
requires-python = ">=3.10"
|
|
18
18
|
|
|
19
19
|
dependencies = [
|
|
20
20
|
"shortuuid~=1.0.13",
|
|
21
21
|
"platformdirs==4.3.6",
|
|
22
22
|
"toml~=0.10.2",
|
|
23
23
|
"websockets==15.0.1",
|
|
24
|
-
"ujson~=5.10.0"
|
|
24
|
+
"ujson~=5.10.0",
|
|
25
|
+
"cryptography~=44.0.2"
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
[tool.flit.module]
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# Parameters for crypto-ws-api connector
|
|
2
|
-
# Copyright © 2023 Jerry Fedorenko aka VM
|
|
3
|
-
# __version__ = "1.0.0"
|
|
4
|
-
|
|
5
|
-
# region endpoint
|
|
6
|
-
[endpoint]
|
|
7
|
-
[endpoint.binance]
|
|
8
|
-
ws_api = 'wss://ws-api.binance.com:443/ws-api/v3'
|
|
9
|
-
ws_api_test = 'wss://testnet.binance.vision/ws-api/v3'
|
|
10
|
-
# endregion
|
|
11
|
-
|
|
12
|
-
# region Binance accounts
|
|
13
|
-
[[accounts]]
|
|
14
|
-
exchange = 'binance'
|
|
15
|
-
name = 'Demo - Binance'
|
|
16
|
-
api_key = '*********** Place API key there ************'
|
|
17
|
-
api_secret = '*********** Place secret API key there ************'
|
|
18
|
-
test_net = true
|
|
19
|
-
# endregion
|
|
File without changes
|
|
File without changes
|