ccxt 4.4.88__py2.py3-none-any.whl → 4.4.91__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccxt/__init__.py +1 -3
- ccxt/abstract/bitget.py +58 -0
- ccxt/abstract/bitrue.py +65 -65
- ccxt/abstract/cryptocom.py +2 -0
- ccxt/abstract/luno.py +1 -0
- ccxt/async_support/__init__.py +1 -3
- ccxt/async_support/base/exchange.py +6 -3
- ccxt/async_support/base/ws/client.py +173 -64
- ccxt/async_support/base/ws/future.py +23 -50
- ccxt/async_support/binance.py +2 -2
- ccxt/async_support/bingx.py +55 -29
- ccxt/async_support/bitget.py +469 -147
- ccxt/async_support/bitmex.py +2 -1
- ccxt/async_support/bitrue.py +72 -66
- ccxt/async_support/bitvavo.py +34 -0
- ccxt/async_support/btcalpha.py +35 -0
- ccxt/async_support/btcbox.py +35 -0
- ccxt/async_support/btcmarkets.py +35 -0
- ccxt/async_support/btcturk.py +35 -0
- ccxt/async_support/bybit.py +9 -3
- ccxt/async_support/cex.py +61 -0
- ccxt/async_support/coinbase.py +1 -3
- ccxt/async_support/cryptocom.py +66 -2
- ccxt/async_support/cryptomus.py +1 -1
- ccxt/async_support/delta.py +2 -2
- ccxt/async_support/digifinex.py +39 -99
- ccxt/async_support/exmo.py +14 -7
- ccxt/async_support/gate.py +14 -7
- ccxt/async_support/hashkey.py +15 -28
- ccxt/async_support/hollaex.py +27 -22
- ccxt/async_support/hyperliquid.py +104 -53
- ccxt/async_support/kraken.py +54 -50
- ccxt/async_support/luno.py +87 -1
- ccxt/async_support/mexc.py +1 -0
- ccxt/async_support/modetrade.py +2 -2
- ccxt/async_support/okx.py +2 -1
- ccxt/async_support/paradex.py +1 -1
- ccxt/async_support/phemex.py +16 -8
- ccxt/async_support/tradeogre.py +3 -3
- ccxt/async_support/xt.py +1 -1
- ccxt/base/exchange.py +20 -8
- ccxt/binance.py +2 -2
- ccxt/bingx.py +55 -29
- ccxt/bitget.py +469 -147
- ccxt/bitmex.py +2 -1
- ccxt/bitrue.py +72 -66
- ccxt/bitvavo.py +34 -0
- ccxt/btcalpha.py +35 -0
- ccxt/btcbox.py +35 -0
- ccxt/btcmarkets.py +35 -0
- ccxt/btcturk.py +35 -0
- ccxt/bybit.py +9 -3
- ccxt/cex.py +61 -0
- ccxt/coinbase.py +1 -3
- ccxt/cryptocom.py +66 -2
- ccxt/cryptomus.py +1 -1
- ccxt/delta.py +2 -2
- ccxt/digifinex.py +39 -99
- ccxt/exmo.py +13 -7
- ccxt/gate.py +14 -7
- ccxt/hashkey.py +15 -28
- ccxt/hollaex.py +27 -22
- ccxt/hyperliquid.py +104 -53
- ccxt/kraken.py +53 -50
- ccxt/luno.py +87 -1
- ccxt/mexc.py +1 -0
- ccxt/modetrade.py +2 -2
- ccxt/okx.py +2 -1
- ccxt/paradex.py +1 -1
- ccxt/phemex.py +16 -8
- ccxt/pro/__init__.py +1 -127
- ccxt/pro/bitstamp.py +1 -1
- ccxt/pro/bybit.py +6 -136
- ccxt/pro/coinbase.py +2 -0
- ccxt/pro/cryptocom.py +27 -0
- ccxt/pro/kraken.py +249 -267
- ccxt/pro/mexc.py +0 -1
- ccxt/tradeogre.py +3 -3
- ccxt/xt.py +1 -1
- {ccxt-4.4.88.dist-info → ccxt-4.4.91.dist-info}/METADATA +64 -23
- {ccxt-4.4.88.dist-info → ccxt-4.4.91.dist-info}/RECORD +84 -101
- ccxt/abstract/coinlist.py +0 -57
- ccxt/async_support/base/ws/aiohttp_client.py +0 -147
- ccxt/async_support/bitcoincom.py +0 -18
- ccxt/async_support/bitfinex1.py +0 -1711
- ccxt/async_support/bitpanda.py +0 -17
- ccxt/async_support/coinlist.py +0 -2542
- ccxt/async_support/poloniexfutures.py +0 -1875
- ccxt/bitcoincom.py +0 -18
- ccxt/bitfinex1.py +0 -1710
- ccxt/bitpanda.py +0 -17
- ccxt/coinlist.py +0 -2542
- ccxt/poloniexfutures.py +0 -1875
- ccxt/pro/bitcoincom.py +0 -35
- ccxt/pro/bitfinex1.py +0 -635
- ccxt/pro/bitpanda.py +0 -16
- ccxt/pro/poloniexfutures.py +0 -1004
- ccxt/pro/wazirx.py +0 -766
- {ccxt-4.4.88.dist-info → ccxt-4.4.91.dist-info}/LICENSE.txt +0 -0
- {ccxt-4.4.88.dist-info → ccxt-4.4.91.dist-info}/WHEEL +0 -0
- {ccxt-4.4.88.dist-info → ccxt-4.4.91.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,31 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
orjson = None
|
4
|
+
try:
|
5
|
+
import orjson as orjson
|
6
|
+
except ImportError:
|
7
|
+
pass
|
8
|
+
|
9
|
+
import json
|
10
|
+
|
11
|
+
from asyncio import sleep, ensure_future, wait_for, TimeoutError, BaseEventLoop, Future as asyncioFuture
|
12
|
+
from .functions import milliseconds, iso8601, deep_extend, is_json_encoded_object
|
13
|
+
from ccxt import NetworkError, RequestTimeout
|
6
14
|
from ccxt.async_support.base.ws.future import Future
|
7
|
-
from
|
15
|
+
from ccxt.async_support.base.ws.functions import gunzip, inflate
|
16
|
+
from typing import Dict
|
17
|
+
|
18
|
+
from aiohttp import WSMsgType
|
19
|
+
|
8
20
|
|
9
21
|
class Client(object):
|
10
22
|
|
11
23
|
url = None
|
12
24
|
ws = None
|
13
|
-
futures = {}
|
25
|
+
futures: Dict[str, Future] = {}
|
14
26
|
options = {} # ws-specific options
|
15
27
|
subscriptions = {}
|
16
28
|
rejections = {}
|
17
|
-
message_queue = {}
|
18
|
-
useMessageQueue = False
|
19
29
|
on_message_callback = None
|
20
30
|
on_error_callback = None
|
21
31
|
on_close_callback = None
|
@@ -32,14 +42,14 @@ class Client(object):
|
|
32
42
|
maxPingPongMisses = 2.0 # how many missed pongs to raise a timeout
|
33
43
|
lastPong = None
|
34
44
|
ping = None # ping-function if defined
|
45
|
+
proxy = None
|
35
46
|
verbose = False # verbose output
|
36
47
|
gunzip = False
|
37
48
|
inflate = False
|
38
49
|
throttle = None
|
39
50
|
connecting = False
|
40
|
-
asyncio_loop = None
|
51
|
+
asyncio_loop: BaseEventLoop = None
|
41
52
|
ping_looper = None
|
42
|
-
receive_looper = None
|
43
53
|
|
44
54
|
def __init__(self, url, on_message_callback, on_error_callback, on_close_callback, on_connected_callback, config={}):
|
45
55
|
defaults = {
|
@@ -70,37 +80,19 @@ class Client(object):
|
|
70
80
|
if message_hash in self.rejections:
|
71
81
|
future.reject(self.rejections[message_hash])
|
72
82
|
del self.rejections[message_hash]
|
73
|
-
del self.message_queue[message_hash]
|
74
|
-
return future
|
75
|
-
if self.useMessageQueue and message_hash in self.message_queue:
|
76
|
-
queue = self.message_queue[message_hash]
|
77
|
-
if len(queue):
|
78
|
-
future.resolve(queue.popleft())
|
79
|
-
del self.futures[message_hash]
|
80
83
|
return future
|
81
84
|
|
82
85
|
def resolve(self, result, message_hash):
|
83
86
|
if self.verbose and message_hash is None:
|
84
87
|
self.log(iso8601(milliseconds()), 'resolve received None messageHash')
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
queue = self.message_queue[message_hash]
|
90
|
-
queue.append(result)
|
91
|
-
if message_hash in self.futures:
|
92
|
-
future = self.futures[message_hash]
|
93
|
-
future.resolve(queue.popleft())
|
94
|
-
del self.futures[message_hash]
|
95
|
-
else:
|
96
|
-
if message_hash in self.futures:
|
97
|
-
future = self.futures[message_hash]
|
98
|
-
future.resolve(result)
|
99
|
-
del self.futures[message_hash]
|
88
|
+
if message_hash in self.futures:
|
89
|
+
future = self.futures[message_hash]
|
90
|
+
future.resolve(result)
|
91
|
+
del self.futures[message_hash]
|
100
92
|
return result
|
101
93
|
|
102
94
|
def reject(self, result, message_hash=None):
|
103
|
-
if message_hash:
|
95
|
+
if message_hash is not None:
|
104
96
|
if message_hash in self.futures:
|
105
97
|
future = self.futures[message_hash]
|
106
98
|
future.reject(result)
|
@@ -113,19 +105,38 @@ class Client(object):
|
|
113
105
|
self.reject(result, message_hash)
|
114
106
|
return result
|
115
107
|
|
116
|
-
|
108
|
+
def receive_loop(self):
|
117
109
|
if self.verbose:
|
118
110
|
self.log(iso8601(milliseconds()), 'receive loop')
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
self.
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
self.
|
111
|
+
if not self.closed():
|
112
|
+
# let's drain the aiohttp buffer to avoid latency
|
113
|
+
if len(self.buffer) > 1:
|
114
|
+
size_delta = 0
|
115
|
+
while len(self.buffer) > 1:
|
116
|
+
message, size = self.buffer.popleft()
|
117
|
+
size_delta += size
|
118
|
+
self.handle_message(message)
|
119
|
+
# we must update the size of the last message inside WebSocketDataQueue
|
120
|
+
# self.receive() calls WebSocketDataQueue.read() that calls WebSocketDataQueue._read_from_buffer()
|
121
|
+
# which updates the size of the buffer, the _size will overflow and pause the transport
|
122
|
+
# make sure to set the enviroment variable AIOHTTP_NO_EXTENSIONS=Y to check
|
123
|
+
# print(self.connection._conn.protocol._payload._size)
|
124
|
+
self.buffer[0] = (self.buffer[0][0], self.buffer[0][1] + size_delta)
|
125
|
+
|
126
|
+
task = self.asyncio_loop.create_task(self.receive())
|
127
|
+
|
128
|
+
def after_interrupt(resolved: asyncioFuture):
|
129
|
+
exception = resolved.exception()
|
130
|
+
if exception is None:
|
131
|
+
self.handle_message(resolved.result())
|
132
|
+
self.asyncio_loop.call_soon(self.receive_loop)
|
133
|
+
else:
|
134
|
+
error = NetworkError(str(exception))
|
135
|
+
if self.verbose:
|
136
|
+
self.log(iso8601(milliseconds()), 'receive_loop', 'Exception', error)
|
137
|
+
self.reset(error)
|
138
|
+
|
139
|
+
task.add_done_callback(after_interrupt)
|
129
140
|
|
130
141
|
async def open(self, session, backoff_delay=0):
|
131
142
|
# exponential backoff for consequent connections if necessary
|
@@ -146,7 +157,7 @@ class Client(object):
|
|
146
157
|
self.on_connected_callback(self)
|
147
158
|
# run both loops forever
|
148
159
|
self.ping_looper = ensure_future(self.ping_loop(), loop=self.asyncio_loop)
|
149
|
-
self.
|
160
|
+
self.asyncio_loop.call_soon(self.receive_loop)
|
150
161
|
except TimeoutError:
|
151
162
|
# connection timeout
|
152
163
|
error = RequestTimeout('Connection timeout')
|
@@ -160,6 +171,13 @@ class Client(object):
|
|
160
171
|
self.log(iso8601(milliseconds()), 'NetworkError', error)
|
161
172
|
self.on_error(error)
|
162
173
|
|
174
|
+
@property
|
175
|
+
def buffer(self):
|
176
|
+
# looks like they exposed it in C
|
177
|
+
# this means we can bypass it
|
178
|
+
# https://github.com/aio-libs/aiohttp/blob/master/aiohttp/_websocket/reader_c.pxd#L53C24-L53C31
|
179
|
+
return self.connection._conn.protocol._payload._buffer
|
180
|
+
|
163
181
|
def connect(self, session, backoff_delay=0):
|
164
182
|
if not self.connection and not self.connecting:
|
165
183
|
self.connecting = True
|
@@ -170,7 +188,7 @@ class Client(object):
|
|
170
188
|
if self.verbose:
|
171
189
|
self.log(iso8601(milliseconds()), 'on_error', error)
|
172
190
|
self.error = error
|
173
|
-
self.
|
191
|
+
self.reject(error)
|
174
192
|
self.on_error_callback(self, error)
|
175
193
|
if not self.closed():
|
176
194
|
ensure_future(self.close(1006), loop=self.asyncio_loop)
|
@@ -179,36 +197,127 @@ class Client(object):
|
|
179
197
|
if self.verbose:
|
180
198
|
self.log(iso8601(milliseconds()), 'on_close', code)
|
181
199
|
if not self.error:
|
182
|
-
self.
|
200
|
+
self.reject(NetworkError('Connection closed by remote server, closing code ' + str(code)))
|
183
201
|
self.on_close_callback(self, code)
|
184
|
-
|
185
|
-
ensure_future(self.close(code), loop=self.asyncio_loop)
|
202
|
+
ensure_future(self.aiohttp_close(), loop=self.asyncio_loop)
|
186
203
|
|
187
|
-
def
|
188
|
-
|
189
|
-
self.reject(error)
|
204
|
+
def log(self, *args):
|
205
|
+
print(*args)
|
190
206
|
|
191
|
-
|
192
|
-
|
193
|
-
self.log(iso8601(milliseconds()), 'ping loop')
|
207
|
+
def closed(self):
|
208
|
+
return (self.connection is None) or self.connection.closed
|
194
209
|
|
195
210
|
def receive(self):
|
196
|
-
|
211
|
+
return self.connection.receive()
|
212
|
+
|
213
|
+
# helper method for binary and text messages
|
214
|
+
def handle_text_or_binary_message(self, data):
|
215
|
+
if self.verbose:
|
216
|
+
self.log(iso8601(milliseconds()), 'message', data)
|
217
|
+
if isinstance(data, bytes):
|
218
|
+
data = data.decode()
|
219
|
+
# decoded = json.loads(data) if is_json_encoded_object(data) else data
|
220
|
+
decode = None
|
221
|
+
if is_json_encoded_object(data):
|
222
|
+
if orjson is None:
|
223
|
+
decode = json.loads(data)
|
224
|
+
else:
|
225
|
+
decode = orjson.loads(data)
|
226
|
+
else:
|
227
|
+
decode = data
|
228
|
+
self.on_message_callback(self, decode)
|
197
229
|
|
198
230
|
def handle_message(self, message):
|
199
|
-
|
231
|
+
# self.log(iso8601(milliseconds()), message)
|
232
|
+
if message.type == WSMsgType.TEXT:
|
233
|
+
self.handle_text_or_binary_message(message.data)
|
234
|
+
elif message.type == WSMsgType.BINARY:
|
235
|
+
data = message.data
|
236
|
+
if self.gunzip:
|
237
|
+
data = gunzip(data)
|
238
|
+
elif self.inflate:
|
239
|
+
data = inflate(data)
|
240
|
+
self.handle_text_or_binary_message(data)
|
241
|
+
# autoping is responsible for automatically replying with pong
|
242
|
+
# to a ping incoming from a server, we have to disable autoping
|
243
|
+
# with aiohttp's websockets and respond with pong manually
|
244
|
+
# otherwise aiohttp's websockets client won't trigger WSMsgType.PONG
|
245
|
+
elif message.type == WSMsgType.PING:
|
246
|
+
if self.verbose:
|
247
|
+
self.log(iso8601(milliseconds()), 'ping', message)
|
248
|
+
ensure_future(self.connection.pong(message.data), loop=self.asyncio_loop)
|
249
|
+
elif message.type == WSMsgType.PONG:
|
250
|
+
self.lastPong = milliseconds()
|
251
|
+
if self.verbose:
|
252
|
+
self.log(iso8601(milliseconds()), 'pong', message)
|
253
|
+
pass
|
254
|
+
elif message.type == WSMsgType.CLOSE:
|
255
|
+
if self.verbose:
|
256
|
+
self.log(iso8601(milliseconds()), 'close', self.closed(), message)
|
257
|
+
self.on_close(message.data)
|
258
|
+
elif message.type == WSMsgType.ERROR:
|
259
|
+
if self.verbose:
|
260
|
+
self.log(iso8601(milliseconds()), 'error', message)
|
261
|
+
error = NetworkError(str(message))
|
262
|
+
self.on_error(error)
|
200
263
|
|
201
|
-
def
|
202
|
-
|
264
|
+
def create_connection(self, session):
|
265
|
+
# autoping is responsible for automatically replying with pong
|
266
|
+
# to a ping incoming from a server, we have to disable autoping
|
267
|
+
# with aiohttp's websockets and respond with pong manually
|
268
|
+
# otherwise aiohttp's websockets client won't trigger WSMsgType.PONG
|
269
|
+
# call aenter here to simulate async with otherwise we get the error "await not called with future"
|
270
|
+
# if connecting to a non-existent endpoint
|
271
|
+
if (self.proxy):
|
272
|
+
return session.ws_connect(self.url, autoping=False, autoclose=False, headers=self.options.get('headers'), proxy=self.proxy, max_msg_size=10485760).__aenter__()
|
273
|
+
return session.ws_connect(self.url, autoping=False, autoclose=False, headers=self.options.get('headers'), max_msg_size=10485760).__aenter__()
|
203
274
|
|
204
275
|
async def send(self, message):
|
205
|
-
|
276
|
+
if self.verbose:
|
277
|
+
self.log(iso8601(milliseconds()), 'sending', message)
|
278
|
+
send_msg = None
|
279
|
+
if isinstance(message, str):
|
280
|
+
send_msg = message
|
281
|
+
else:
|
282
|
+
if orjson is None:
|
283
|
+
send_msg = json.dumps(message, separators=(',', ':'))
|
284
|
+
else:
|
285
|
+
send_msg = orjson.dumps(message).decode('utf-8')
|
286
|
+
return await self.connection.send_str(send_msg)
|
206
287
|
|
207
288
|
async def close(self, code=1000):
|
208
|
-
|
289
|
+
if self.verbose:
|
290
|
+
self.log(iso8601(milliseconds()), 'closing', code)
|
291
|
+
for future in self.futures.values():
|
292
|
+
future.cancel()
|
293
|
+
await self.aiohttp_close()
|
209
294
|
|
210
|
-
def
|
211
|
-
|
295
|
+
async def aiohttp_close(self):
|
296
|
+
if not self.closed():
|
297
|
+
await self.connection.close()
|
298
|
+
# these will end automatically once self.closed() = True
|
299
|
+
# so we don't need to cancel them
|
300
|
+
if self.ping_looper:
|
301
|
+
self.ping_looper.cancel()
|
212
302
|
|
213
|
-
def
|
214
|
-
|
303
|
+
async def ping_loop(self):
|
304
|
+
if self.verbose:
|
305
|
+
self.log(iso8601(milliseconds()), 'ping loop')
|
306
|
+
while self.keepAlive and not self.closed():
|
307
|
+
now = milliseconds()
|
308
|
+
self.lastPong = now if self.lastPong is None else self.lastPong
|
309
|
+
if (self.lastPong + self.keepAlive * self.maxPingPongMisses) < now:
|
310
|
+
self.on_error(RequestTimeout('Connection to ' + self.url + ' timed out due to a ping-pong keepalive missing on time'))
|
311
|
+
# the following ping-clause is not necessary with aiohttp's built-in ws
|
312
|
+
# since it has a heartbeat option (see create_connection above)
|
313
|
+
# however some exchanges require a text-type ping message
|
314
|
+
# therefore we need this clause anyway
|
315
|
+
else:
|
316
|
+
if self.ping:
|
317
|
+
try:
|
318
|
+
await self.send(self.ping(self))
|
319
|
+
except Exception as e:
|
320
|
+
self.on_error(e)
|
321
|
+
else:
|
322
|
+
await self.connection.ping()
|
323
|
+
await sleep(self.keepAlive / 1000)
|
@@ -1,69 +1,42 @@
|
|
1
1
|
import asyncio
|
2
|
-
from ccxt import ExchangeClosedByUser
|
3
2
|
|
4
|
-
class Future(asyncio.Future):
|
5
3
|
|
6
|
-
|
4
|
+
class Future(asyncio.Future):
|
7
5
|
|
8
6
|
def resolve(self, result=None):
|
9
7
|
if not self.done():
|
10
|
-
|
11
|
-
self.set_result(result)
|
12
|
-
except BaseException as e:
|
13
|
-
print("Error in Future.resolve")
|
14
|
-
raise e
|
8
|
+
self.set_result(result)
|
15
9
|
|
16
10
|
def reject(self, error=None):
|
17
11
|
if not self.done():
|
18
|
-
|
19
|
-
if not isinstance(error, BaseException):
|
20
|
-
error = Exception(error)
|
21
|
-
try:
|
22
|
-
self.set_exception(error)
|
23
|
-
except BaseException as e:
|
24
|
-
print("Error in Future.reject")
|
25
|
-
raise e
|
12
|
+
self.set_exception(error)
|
26
13
|
|
27
14
|
@classmethod
|
28
15
|
def race(cls, futures):
|
29
16
|
future = Future()
|
30
|
-
for f in futures:
|
31
|
-
f.is_race_future = True
|
32
17
|
coro = asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED)
|
33
18
|
task = asyncio.create_task(coro)
|
34
19
|
|
35
20
|
def callback(done):
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
if are_all_canceled:
|
57
|
-
future.reject(ExchangeClosedByUser('Connection closed by the user'))
|
58
|
-
return
|
59
|
-
|
60
|
-
first = futures_list[0]
|
61
|
-
|
62
|
-
first_result = first.result()
|
63
|
-
future.resolve(first_result)
|
64
|
-
except asyncio.CancelledError as e:
|
65
|
-
future.reject(e)
|
66
|
-
except Exception as e:
|
67
|
-
future.reject(e)
|
21
|
+
complete, _ = done.result()
|
22
|
+
# check for exceptions
|
23
|
+
exceptions = []
|
24
|
+
cancelled = False
|
25
|
+
for f in complete:
|
26
|
+
if f.cancelled():
|
27
|
+
cancelled = True
|
28
|
+
else:
|
29
|
+
err = f.exception()
|
30
|
+
if err:
|
31
|
+
exceptions.append(err)
|
32
|
+
# if any exceptions return with first exception
|
33
|
+
if len(exceptions) > 0:
|
34
|
+
future.set_exception(exceptions[0])
|
35
|
+
# else return first result
|
36
|
+
elif cancelled:
|
37
|
+
future.cancel()
|
38
|
+
else:
|
39
|
+
first_result = list(complete)[0].result()
|
40
|
+
future.set_result(first_result)
|
68
41
|
task.add_done_callback(callback)
|
69
42
|
return future
|
ccxt/async_support/binance.py
CHANGED
@@ -3904,7 +3904,7 @@ class binance(Exchange, ImplicitAPI):
|
|
3904
3904
|
#
|
3905
3905
|
# {
|
3906
3906
|
# "symbol": "BTCUSDT",
|
3907
|
-
# "markPrice": "11793.
|
3907
|
+
# "markPrice": "11793.63104561", # mark price
|
3908
3908
|
# "indexPrice": "11781.80495970", # index price
|
3909
3909
|
# "estimatedSettlePrice": "11781.16138815", # Estimated Settle Price, only useful in the last hour before the settlement starts
|
3910
3910
|
# "lastFundingRate": "0.00038246", # This is the lastest estimated funding rate
|
@@ -9834,7 +9834,7 @@ class binance(Exchange, ImplicitAPI):
|
|
9834
9834
|
response = await self.dapiPrivateV2GetLeverageBracket(query)
|
9835
9835
|
else:
|
9836
9836
|
raise NotSupported(self.id + ' loadLeverageBrackets() supports linear and inverse contracts only')
|
9837
|
-
self.options['leverageBrackets'] =
|
9837
|
+
self.options['leverageBrackets'] = self.create_safe_dictionary()
|
9838
9838
|
for i in range(0, len(response)):
|
9839
9839
|
entry = response[i]
|
9840
9840
|
marketId = self.safe_string(entry, 'symbol')
|
ccxt/async_support/bingx.py
CHANGED
@@ -514,6 +514,8 @@ class bingx(Exchange, ImplicitAPI):
|
|
514
514
|
'100437': BadRequest, # {"code":100437,"msg":"The withdrawal amount is lower than the minimum limit, please re-enter.","timestamp":1689258588845}
|
515
515
|
'101204': InsufficientFunds, # {"code":101204,"msg":"","data":{}}
|
516
516
|
'110425': InvalidOrder, # {"code":110425,"msg":"Please ensure that the minimum nominal value of the order placed must be greater than 2u","data":{}}
|
517
|
+
'Insufficient assets': InsufficientFunds, # {"transferErrorMsg":"Insufficient assets"}
|
518
|
+
'illegal transferType': BadRequest, # {"transferErrorMsg":"illegal transferType"}
|
517
519
|
},
|
518
520
|
'broad': {},
|
519
521
|
},
|
@@ -527,12 +529,14 @@ class bingx(Exchange, ImplicitAPI):
|
|
527
529
|
'options': {
|
528
530
|
'defaultType': 'spot',
|
529
531
|
'accountsByType': {
|
530
|
-
'
|
532
|
+
'funding': 'FUND',
|
533
|
+
'spot': 'SPOT',
|
531
534
|
'swap': 'PFUTURES',
|
532
535
|
'future': 'SFUTURES',
|
533
536
|
},
|
534
537
|
'accountsById': {
|
535
|
-
'FUND': '
|
538
|
+
'FUND': 'funding',
|
539
|
+
'SPOT': 'spot',
|
536
540
|
'PFUTURES': 'swap',
|
537
541
|
'SFUTURES': 'future',
|
538
542
|
},
|
@@ -1530,7 +1534,7 @@ class bingx(Exchange, ImplicitAPI):
|
|
1530
1534
|
# ]
|
1531
1535
|
# }
|
1532
1536
|
#
|
1533
|
-
data = self.
|
1537
|
+
data = self.safe_dict(response, 'data')
|
1534
1538
|
return self.parse_funding_rate(data, market)
|
1535
1539
|
|
1536
1540
|
async def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates:
|
@@ -4638,12 +4642,12 @@ class bingx(Exchange, ImplicitAPI):
|
|
4638
4642
|
"""
|
4639
4643
|
transfer currency internally between wallets on the same account
|
4640
4644
|
|
4641
|
-
https://bingx-api.github.io/docs/#/
|
4645
|
+
https://bingx-api.github.io/docs/#/en-us/common/account-api.html#Asset%20Transfer
|
4642
4646
|
|
4643
4647
|
:param str code: unified currency code
|
4644
4648
|
:param float amount: amount to transfer
|
4645
|
-
:param str fromAccount: account to transfer from
|
4646
|
-
:param str toAccount: account to transfer to
|
4649
|
+
:param str fromAccount: account to transfer from(spot, swap, futures, or funding)
|
4650
|
+
:param str toAccount: account to transfer to(spot, swap, futures, or funding)
|
4647
4651
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
4648
4652
|
:returns dict: a `transfer structure <https://docs.ccxt.com/#/?id=transfer-structure>`
|
4649
4653
|
"""
|
@@ -4659,9 +4663,10 @@ class bingx(Exchange, ImplicitAPI):
|
|
4659
4663
|
}
|
4660
4664
|
response = await self.spotV3PrivateGetGetAssetTransfer(self.extend(request, params))
|
4661
4665
|
#
|
4662
|
-
#
|
4663
|
-
#
|
4664
|
-
#
|
4666
|
+
# {
|
4667
|
+
# "tranId": 1933130865269936128,
|
4668
|
+
# "transferId": "1051450703949464903736"
|
4669
|
+
# }
|
4665
4670
|
#
|
4666
4671
|
return {
|
4667
4672
|
'info': response,
|
@@ -4683,8 +4688,11 @@ class bingx(Exchange, ImplicitAPI):
|
|
4683
4688
|
|
4684
4689
|
:param str [code]: unified currency code of the currency transferred
|
4685
4690
|
:param int [since]: the earliest time in ms to fetch transfers for
|
4686
|
-
:param int [limit]: the maximum number of transfers structures to retrieve
|
4691
|
+
:param int [limit]: the maximum number of transfers structures to retrieve(default 10, max 100)
|
4687
4692
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
4693
|
+
:param str params.fromAccount:(mandatory) transfer from(spot, swap, futures, or funding)
|
4694
|
+
:param str params.toAccount:(mandatory) transfer to(spot, swap, futures, or funding)
|
4695
|
+
:param boolean [params.paginate]: whether to paginate the results(default False)
|
4688
4696
|
:returns dict[]: a list of `transfer structures <https://docs.ccxt.com/#/?id=transfer-structure>`
|
4689
4697
|
"""
|
4690
4698
|
await self.load_markets()
|
@@ -4698,6 +4706,12 @@ class bingx(Exchange, ImplicitAPI):
|
|
4698
4706
|
toId = self.safe_string(accountsByType, toAccount, toAccount)
|
4699
4707
|
if fromId is None or toId is None:
|
4700
4708
|
raise ExchangeError(self.id + ' fromAccount & toAccount parameter are required')
|
4709
|
+
params = self.omit(params, ['fromAccount', 'toAccount'])
|
4710
|
+
maxLimit = 100
|
4711
|
+
paginate = False
|
4712
|
+
paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate', False)
|
4713
|
+
if paginate:
|
4714
|
+
return await self.fetch_paginated_call_dynamic('fetchTransfers', None, since, limit, params, maxLimit)
|
4701
4715
|
request: dict = {
|
4702
4716
|
'type': fromId + '_' + toId,
|
4703
4717
|
}
|
@@ -4705,18 +4719,19 @@ class bingx(Exchange, ImplicitAPI):
|
|
4705
4719
|
request['startTime'] = since
|
4706
4720
|
if limit is not None:
|
4707
4721
|
request['size'] = limit
|
4722
|
+
request, params = self.handle_until_option('endTime', request, params)
|
4708
4723
|
response = await self.spotV3PrivateGetAssetTransfer(self.extend(request, params))
|
4709
4724
|
#
|
4710
4725
|
# {
|
4711
4726
|
# "total": 3,
|
4712
4727
|
# "rows": [
|
4713
4728
|
# {
|
4714
|
-
# "asset":"USDT",
|
4715
|
-
# "amount":"
|
4716
|
-
# "type":"FUND_SFUTURES",
|
4717
|
-
# "status":"CONFIRMED",
|
4718
|
-
# "tranId":1067594500957016069,
|
4719
|
-
# "timestamp":1658388859000
|
4729
|
+
# "asset": "USDT",
|
4730
|
+
# "amount": "100.00000000000000000000",
|
4731
|
+
# "type": "FUND_SFUTURES",
|
4732
|
+
# "status": "CONFIRMED",
|
4733
|
+
# "tranId": 1067594500957016069,
|
4734
|
+
# "timestamp": 1658388859000
|
4720
4735
|
# },
|
4721
4736
|
# ]
|
4722
4737
|
# }
|
@@ -4733,7 +4748,7 @@ class bingx(Exchange, ImplicitAPI):
|
|
4733
4748
|
typeId = self.safe_string(transfer, 'type')
|
4734
4749
|
typeIdSplit = typeId.split('_')
|
4735
4750
|
fromId = self.safe_string(typeIdSplit, 0)
|
4736
|
-
toId = self.safe_string(
|
4751
|
+
toId = self.safe_string(typeIdSplit, 1)
|
4737
4752
|
fromAccount = self.safe_string(accountsById, fromId, fromId)
|
4738
4753
|
toAccount = self.safe_string(accountsById, toId, toId)
|
4739
4754
|
return {
|
@@ -4745,9 +4760,15 @@ class bingx(Exchange, ImplicitAPI):
|
|
4745
4760
|
'amount': self.safe_number(transfer, 'amount'),
|
4746
4761
|
'fromAccount': fromAccount,
|
4747
4762
|
'toAccount': toAccount,
|
4748
|
-
'status': status,
|
4763
|
+
'status': self.parse_transfer_status(status),
|
4749
4764
|
}
|
4750
4765
|
|
4766
|
+
def parse_transfer_status(self, status: Str) -> str:
|
4767
|
+
statuses: dict = {
|
4768
|
+
'CONFIRMED': 'ok',
|
4769
|
+
}
|
4770
|
+
return self.safe_string(statuses, status, status)
|
4771
|
+
|
4751
4772
|
async def fetch_deposit_addresses_by_network(self, code: str, params={}) -> List[DepositAddress]:
|
4752
4773
|
"""
|
4753
4774
|
fetch the deposit addresses for a currency associated with self account
|
@@ -5553,11 +5574,12 @@ class bingx(Exchange, ImplicitAPI):
|
|
5553
5574
|
return self.parse_transaction(data)
|
5554
5575
|
|
5555
5576
|
def parse_params(self, params):
|
5556
|
-
sortedParams = self.keysort(params)
|
5557
|
-
|
5577
|
+
# sortedParams = self.keysort(params)
|
5578
|
+
rawKeys = list(params.keys())
|
5579
|
+
keys = self.sort(rawKeys)
|
5558
5580
|
for i in range(0, len(keys)):
|
5559
5581
|
key = keys[i]
|
5560
|
-
value =
|
5582
|
+
value = params[key]
|
5561
5583
|
if isinstance(value, list):
|
5562
5584
|
arrStr = '['
|
5563
5585
|
for j in range(0, len(value)):
|
@@ -5566,8 +5588,8 @@ class bingx(Exchange, ImplicitAPI):
|
|
5566
5588
|
arrStr += ','
|
5567
5589
|
arrStr += str(arrayElement)
|
5568
5590
|
arrStr += ']'
|
5569
|
-
|
5570
|
-
return
|
5591
|
+
params[key] = arrStr
|
5592
|
+
return params
|
5571
5593
|
|
5572
5594
|
async def fetch_my_liquidations(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
5573
5595
|
"""
|
@@ -6167,13 +6189,14 @@ class bingx(Exchange, ImplicitAPI):
|
|
6167
6189
|
}
|
6168
6190
|
|
6169
6191
|
def custom_encode(self, params):
|
6170
|
-
sortedParams = self.keysort(params)
|
6171
|
-
|
6192
|
+
# sortedParams = self.keysort(params)
|
6193
|
+
rawKeys = list(params.keys())
|
6194
|
+
keys = self.sort(rawKeys)
|
6172
6195
|
adjustedValue = None
|
6173
6196
|
result = None
|
6174
6197
|
for i in range(0, len(keys)):
|
6175
6198
|
key = keys[i]
|
6176
|
-
value =
|
6199
|
+
value = params[key]
|
6177
6200
|
if isinstance(value, list):
|
6178
6201
|
arrStr = None
|
6179
6202
|
for j in range(0, len(value)):
|
@@ -6231,7 +6254,7 @@ class bingx(Exchange, ImplicitAPI):
|
|
6231
6254
|
encodeRequest = self.custom_encode(params)
|
6232
6255
|
else:
|
6233
6256
|
parsedParams = self.parse_params(params)
|
6234
|
-
encodeRequest = self.rawencode(parsedParams)
|
6257
|
+
encodeRequest = self.rawencode(parsedParams, True)
|
6235
6258
|
signature = self.hmac(self.encode(encodeRequest), self.encode(self.secret), hashlib.sha256)
|
6236
6259
|
headers = {
|
6237
6260
|
'X-BX-APIKEY': self.apiKey,
|
@@ -6242,7 +6265,7 @@ class bingx(Exchange, ImplicitAPI):
|
|
6242
6265
|
params['signature'] = signature
|
6243
6266
|
body = self.json(params)
|
6244
6267
|
else:
|
6245
|
-
query = self.urlencode(parsedParams)
|
6268
|
+
query = self.urlencode(parsedParams, True)
|
6246
6269
|
url += '?' + query + '&' + 'signature=' + signature
|
6247
6270
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
6248
6271
|
|
@@ -6266,7 +6289,10 @@ class bingx(Exchange, ImplicitAPI):
|
|
6266
6289
|
#
|
6267
6290
|
code = self.safe_string(response, 'code')
|
6268
6291
|
message = self.safe_string(response, 'msg')
|
6269
|
-
|
6292
|
+
transferErrorMsg = self.safe_string(response, 'transferErrorMsg') # handling with errors from transfer endpoint
|
6293
|
+
if (transferErrorMsg is not None) or (code is not None and code != '0'):
|
6294
|
+
if transferErrorMsg is not None:
|
6295
|
+
message = transferErrorMsg
|
6270
6296
|
feedback = self.id + ' ' + body
|
6271
6297
|
self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
|
6272
6298
|
self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
|