pybinbot 0.4.0__py3-none-any.whl → 0.4.15__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.
- pybinbot/__init__.py +4 -2
- pybinbot/apis/binance/base.py +588 -0
- pybinbot/apis/binance/exceptions.py +17 -0
- pybinbot/apis/binbot/base.py +327 -0
- pybinbot/apis/binbot/exceptions.py +56 -0
- pybinbot/apis/kucoin/base.py +208 -0
- pybinbot/apis/kucoin/exceptions.py +9 -0
- pybinbot/apis/kucoin/market.py +92 -0
- pybinbot/apis/kucoin/orders.py +663 -0
- pybinbot/apis/kucoin/rest.py +33 -0
- pybinbot/shared/types.py +5 -4
- {pybinbot-0.4.0.dist-info → pybinbot-0.4.15.dist-info}/METADATA +1 -1
- pybinbot-0.4.15.dist-info/RECORD +32 -0
- pybinbot-0.4.0.dist-info/RECORD +0 -23
- {pybinbot-0.4.0.dist-info → pybinbot-0.4.15.dist-info}/WHEEL +0 -0
- {pybinbot-0.4.0.dist-info → pybinbot-0.4.15.dist-info}/licenses/LICENSE +0 -0
- {pybinbot-0.4.0.dist-info → pybinbot-0.4.15.dist-info}/top_level.txt +0 -0
pybinbot/__init__.py
CHANGED
|
@@ -47,7 +47,7 @@ from pybinbot.shared.enums import (
|
|
|
47
47
|
from pybinbot.shared.indicators import Indicators
|
|
48
48
|
from pybinbot.shared.heikin_ashi import HeikinAshi
|
|
49
49
|
from pybinbot.shared.logging_config import configure_logging
|
|
50
|
-
from pybinbot.shared.types import Amount
|
|
50
|
+
from pybinbot.shared.types import Amount, CombinedApis
|
|
51
51
|
from pybinbot.shared.cache import cache
|
|
52
52
|
from pybinbot.shared.handlers import handle_binance_errors, aio_response_handler
|
|
53
53
|
from pybinbot.models.bot_base import BotBase
|
|
@@ -78,12 +78,13 @@ from pybinbot.apis.binance.exceptions import (
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
from . import models, shared
|
|
81
|
+
from . import models, shared, apis
|
|
82
82
|
|
|
83
83
|
__all__ = [
|
|
84
84
|
# subpackages
|
|
85
85
|
"shared",
|
|
86
86
|
"models",
|
|
87
|
+
"apis",
|
|
87
88
|
# models
|
|
88
89
|
"BotBase",
|
|
89
90
|
"OrderBase",
|
|
@@ -93,6 +94,7 @@ __all__ = [
|
|
|
93
94
|
"SignalsConsumer",
|
|
94
95
|
# misc
|
|
95
96
|
"Amount",
|
|
97
|
+
"CombinedApis",
|
|
96
98
|
"configure_logging",
|
|
97
99
|
"cache",
|
|
98
100
|
"handle_binance_errors",
|
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from random import randrange
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
from requests import Session, request, HTTPError
|
|
7
|
+
from pybinbot.shared.handlers import handle_binance_errors
|
|
8
|
+
from pybinbot.shared.cache import cache
|
|
9
|
+
from pybinbot.apis.binbot.exceptions import IsolateBalanceError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BinanceApi:
|
|
13
|
+
"""
|
|
14
|
+
Binance API URLs
|
|
15
|
+
https://binance.github.io/binance-api-swagger/
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
api_servers = [
|
|
19
|
+
"https://api.binance.com",
|
|
20
|
+
"https://api1.binance.com",
|
|
21
|
+
"https://api3.binance.com",
|
|
22
|
+
"https://api-gcp.binance.com",
|
|
23
|
+
]
|
|
24
|
+
market_api_servers = ["https://data-api.binance.vision", "https://api3.binance.com"]
|
|
25
|
+
BASE = api_servers[randrange(2) - 1]
|
|
26
|
+
MARKET_DATA_BASE = market_api_servers[randrange(3) - 1]
|
|
27
|
+
WAPI = f"{BASE}/api/v3/depth"
|
|
28
|
+
WS_BASE = "wss://stream.binance.com:9443/stream?streams="
|
|
29
|
+
tags_url = "https://www.binance.com/bapi/asset/v2/public/asset-service/product/get-product-by-symbol"
|
|
30
|
+
|
|
31
|
+
recvWindow = 9000
|
|
32
|
+
server_time_url = f"{MARKET_DATA_BASE}/api/v3/time"
|
|
33
|
+
# Binance always returning forbidden for other APIs
|
|
34
|
+
account_url = f"{api_servers[1]}/api/v3/account"
|
|
35
|
+
exchangeinfo_url = f"{MARKET_DATA_BASE}/api/v3/exchangeInfo"
|
|
36
|
+
ticker_price_url = f"{MARKET_DATA_BASE}/api/v3/ticker/price"
|
|
37
|
+
ticker24_url = f"{MARKET_DATA_BASE}/api/v3/ticker/24hr"
|
|
38
|
+
candlestick_url = "https://api3.binance.com/api/v3/uiKlines"
|
|
39
|
+
order_url = f"{BASE}/api/v3/order"
|
|
40
|
+
order_book_url = f"{BASE}/api/v3/depth"
|
|
41
|
+
avg_price = f"{BASE}/api/v3/avgPrice"
|
|
42
|
+
open_orders = f"{BASE}/api/v3/openOrders"
|
|
43
|
+
all_orders_url = f"{BASE}/api/v3/allOrders"
|
|
44
|
+
cancel_replace_url = f"{BASE}/api/v3/order/cancelReplace"
|
|
45
|
+
trade_fee = f"{BASE}/sapi/v1/asset/tradeFee"
|
|
46
|
+
wallet_balance_url = f"{BASE}/sapi/v1/asset/wallet/balance"
|
|
47
|
+
user_asset_url = f"{BASE}/sapi/v3/asset/getUserAsset"
|
|
48
|
+
|
|
49
|
+
# order, user data, only works with api.binance host
|
|
50
|
+
user_data_stream = "https://api.binance.com/api/v3/userDataStream"
|
|
51
|
+
|
|
52
|
+
withdraw_url = f"{BASE}/wapi/v3/withdraw.html"
|
|
53
|
+
withdraw_history_url = f"{BASE}/wapi/v3/withdrawHistory.html"
|
|
54
|
+
deposit_history_url = f"{BASE}/wapi/v3/depositHistory.html"
|
|
55
|
+
deposit_address_url = f"{BASE}/wapi/v3/depositAddress.html"
|
|
56
|
+
|
|
57
|
+
dust_transfer_url = f"{BASE}/sapi/v1/asset/dust"
|
|
58
|
+
account_snapshot_url = f"{BASE}/sapi/v1/accountSnapshot"
|
|
59
|
+
|
|
60
|
+
# Margin
|
|
61
|
+
isolated_fee_url = f"{BASE}/sapi/v1/margin/isolatedMarginData"
|
|
62
|
+
isolated_account_url = f"{BASE}/sapi/v1/margin/isolated/account"
|
|
63
|
+
margin_isolated_transfer_url = f"{BASE}/sapi/v1/margin/isolated/transfer"
|
|
64
|
+
loan_record_url = f"{BASE}/sapi/v1/margin/borrow-repay"
|
|
65
|
+
isolated_hourly_interest = f"{BASE}/sapi/v1/margin/next-hourly-interest-rate"
|
|
66
|
+
margin_order = f"{BASE}/sapi/v1/margin/order"
|
|
67
|
+
max_borrow_url = f"{BASE}/sapi/v1/margin/maxBorrowable"
|
|
68
|
+
interest_history_url = f"{BASE}/sapi/v1/margin/interestHistory"
|
|
69
|
+
manual_liquidation_url = f"{BASE}/sapi/v1/margin/manual-liquidation"
|
|
70
|
+
|
|
71
|
+
def __init__(self, key, secret) -> None:
|
|
72
|
+
self.secret: str = secret
|
|
73
|
+
self.key: str = key
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def request(
|
|
77
|
+
self,
|
|
78
|
+
url,
|
|
79
|
+
method="GET",
|
|
80
|
+
session: Session | None = None,
|
|
81
|
+
payload: dict | None = None,
|
|
82
|
+
**kwargs,
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Standard request
|
|
86
|
+
- No signed
|
|
87
|
+
- No authorization
|
|
88
|
+
"""
|
|
89
|
+
if session:
|
|
90
|
+
res = session.request(method=method, url=url, **kwargs)
|
|
91
|
+
else:
|
|
92
|
+
res = request(method=method, url=url, json=payload, **kwargs)
|
|
93
|
+
data = handle_binance_errors(res)
|
|
94
|
+
return data
|
|
95
|
+
|
|
96
|
+
def get_server_time(self):
|
|
97
|
+
response = self.request(url=self.server_time_url)
|
|
98
|
+
data = handle_binance_errors(response)
|
|
99
|
+
return data["serverTime"]
|
|
100
|
+
|
|
101
|
+
def signed_request(self, url, method="GET", payload: dict = {}) -> dict:
|
|
102
|
+
"""
|
|
103
|
+
USER_DATA, TRADE signed requests
|
|
104
|
+
|
|
105
|
+
Arguments are all the same as requests
|
|
106
|
+
except payload, which is centrally formatted
|
|
107
|
+
here to become a JSON
|
|
108
|
+
"""
|
|
109
|
+
session = Session()
|
|
110
|
+
query_string = urlencode(payload, True)
|
|
111
|
+
timestamp = self.get_server_time()
|
|
112
|
+
session.headers.update(
|
|
113
|
+
{"Content-Type": "application/json", "X-MBX-APIKEY": self.key}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if query_string:
|
|
117
|
+
query_string = (
|
|
118
|
+
f"{query_string}&recvWindow={self.recvWindow}×tamp={timestamp}"
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
query_string = f"recvWindow={self.recvWindow}×tamp={timestamp}"
|
|
122
|
+
|
|
123
|
+
signature = hmac.new(
|
|
124
|
+
self.secret.encode("utf-8"),
|
|
125
|
+
query_string.encode("utf-8"),
|
|
126
|
+
hashlib.sha256,
|
|
127
|
+
).hexdigest()
|
|
128
|
+
url = f"{url}?{query_string}&signature={signature}"
|
|
129
|
+
data = self.request(url, method, session)
|
|
130
|
+
return data
|
|
131
|
+
|
|
132
|
+
def get_listen_key(self):
|
|
133
|
+
"""
|
|
134
|
+
Get user data websocket stream
|
|
135
|
+
"""
|
|
136
|
+
headers = {"Content-Type": "application/json", "X-MBX-APIKEY": self.key}
|
|
137
|
+
res = request(method="POST", url=self.user_data_stream, headers=headers)
|
|
138
|
+
response = handle_binance_errors(res)
|
|
139
|
+
listen_key = response["listenKey"]
|
|
140
|
+
return listen_key
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
No security endpoints
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def exchange_info(self, symbol=None):
|
|
147
|
+
"""
|
|
148
|
+
This must be a separate method because classes use it with inheritance
|
|
149
|
+
|
|
150
|
+
This request is used in many places to retrieve data about symbols, precisions etc.
|
|
151
|
+
It is a high weight endpoint, thus Binance could ban our IP
|
|
152
|
+
However it is not real-time updated data, so cache is used to avoid hitting endpoint
|
|
153
|
+
too many times and still be able to re-request data everywhere.
|
|
154
|
+
|
|
155
|
+
In addition, it uses MongoDB, with a separate database called "mongo_cache"
|
|
156
|
+
"""
|
|
157
|
+
params = {}
|
|
158
|
+
if symbol:
|
|
159
|
+
params["symbol"] = symbol
|
|
160
|
+
|
|
161
|
+
# mongo_cache = self.setup_mongocache()
|
|
162
|
+
# set up a cache that expires in 1440'' (24hrs)
|
|
163
|
+
# session = CachedSession("http_cache", backend=mongo_cache, expire_after=1440)
|
|
164
|
+
exchange_info_res = self.request(url=f"{self.exchangeinfo_url}", params=params)
|
|
165
|
+
return exchange_info_res
|
|
166
|
+
|
|
167
|
+
def price_filter_by_symbol(self, symbol, filter_limit):
|
|
168
|
+
"""
|
|
169
|
+
PRICE_FILTER restrictions from /exchangeinfo
|
|
170
|
+
@params:
|
|
171
|
+
- symbol: string - pair/market e.g. BNBBTC
|
|
172
|
+
- filter_limit: string - minPrice or maxPrice
|
|
173
|
+
"""
|
|
174
|
+
symbols = self.exchange_info(symbol)
|
|
175
|
+
market = symbols["symbols"][0]
|
|
176
|
+
price_filter = next(
|
|
177
|
+
(m for m in market["filters"] if m["filterType"] == "PRICE_FILTER")
|
|
178
|
+
)
|
|
179
|
+
return price_filter[filter_limit].rstrip(".0")
|
|
180
|
+
|
|
181
|
+
def lot_size_by_symbol(self, symbol, lot_size_limit):
|
|
182
|
+
"""
|
|
183
|
+
LOT_SIZE (quantity) restrictions from /exchangeinfo
|
|
184
|
+
@params:
|
|
185
|
+
- symbol: string - pair/market e.g. BNBBTC
|
|
186
|
+
- lot_size_limit: string - minQty, maxQty, stepSize
|
|
187
|
+
"""
|
|
188
|
+
symbols = self.exchange_info(symbol)
|
|
189
|
+
market = symbols["symbols"][0]
|
|
190
|
+
quantity_filter = next(
|
|
191
|
+
(m for m in market["filters"] if m["filterType"] == "LOT_SIZE")
|
|
192
|
+
)
|
|
193
|
+
return quantity_filter[lot_size_limit].rstrip(".0")
|
|
194
|
+
|
|
195
|
+
def min_notional_by_symbol(self, symbol, min_notional_limit="minNotional"):
|
|
196
|
+
"""
|
|
197
|
+
MIN_NOTIONAL (price x quantity) restrictions
|
|
198
|
+
from Binance /exchangeinfo
|
|
199
|
+
@deprecated
|
|
200
|
+
@params:
|
|
201
|
+
- symbol: string - pair/market e.g. BNBBTC
|
|
202
|
+
- min_notional_limit: string - minNotional
|
|
203
|
+
"""
|
|
204
|
+
symbols = self.exchange_info(symbol)
|
|
205
|
+
market = symbols["symbols"][0]
|
|
206
|
+
min_notional_filter = next(
|
|
207
|
+
m for m in market["filters"] if m["filterType"] == "NOTIONAL"
|
|
208
|
+
)
|
|
209
|
+
return min_notional_filter[min_notional_limit]
|
|
210
|
+
|
|
211
|
+
def _calculate_price_precision(self, symbol) -> int:
|
|
212
|
+
"""
|
|
213
|
+
Decimals needed for Binance price
|
|
214
|
+
@deprecated - use calculate_price_precision
|
|
215
|
+
"""
|
|
216
|
+
precision = -1 * (
|
|
217
|
+
Decimal(str(self.price_filter_by_symbol(symbol, "tickSize")))
|
|
218
|
+
.as_tuple()
|
|
219
|
+
.exponent
|
|
220
|
+
)
|
|
221
|
+
price_precision = int(precision)
|
|
222
|
+
return price_precision
|
|
223
|
+
|
|
224
|
+
def _calculate_qty_precision(self, symbol) -> int:
|
|
225
|
+
"""
|
|
226
|
+
Decimals needed for Binance quantity
|
|
227
|
+
@deprecated - use calculate_qty_precision
|
|
228
|
+
"""
|
|
229
|
+
precision = -1 * (
|
|
230
|
+
Decimal(str(self.lot_size_by_symbol(symbol, "stepSize")))
|
|
231
|
+
.as_tuple()
|
|
232
|
+
.exponent
|
|
233
|
+
)
|
|
234
|
+
qty_precision = int(precision)
|
|
235
|
+
return qty_precision
|
|
236
|
+
|
|
237
|
+
def ticker_24(self, type: str = "FULL", symbol: str | None = None):
|
|
238
|
+
"""
|
|
239
|
+
Weight 40 without symbol
|
|
240
|
+
https://github.com/carkod/binbot/issues/438
|
|
241
|
+
|
|
242
|
+
Cannot use cache, because data would be stale
|
|
243
|
+
"""
|
|
244
|
+
params = {"type": type}
|
|
245
|
+
if symbol:
|
|
246
|
+
params["symbol"] = symbol
|
|
247
|
+
|
|
248
|
+
data = self.request(url=self.ticker24_url, params=params)
|
|
249
|
+
return data
|
|
250
|
+
|
|
251
|
+
def get_ticker_price(self, symbol: str) -> float:
|
|
252
|
+
data = self.request(url=f"{self.ticker_price_url}", params={"symbol": symbol})
|
|
253
|
+
return float(data["price"])
|
|
254
|
+
|
|
255
|
+
def get_ui_klines(
|
|
256
|
+
self, symbol, interval, limit=500, start_time=None, end_time=None
|
|
257
|
+
):
|
|
258
|
+
"""
|
|
259
|
+
Get raw klines
|
|
260
|
+
"""
|
|
261
|
+
params = {
|
|
262
|
+
"symbol": symbol,
|
|
263
|
+
"interval": interval,
|
|
264
|
+
"limit": limit,
|
|
265
|
+
}
|
|
266
|
+
if start_time:
|
|
267
|
+
params["startTime"] = start_time
|
|
268
|
+
if end_time:
|
|
269
|
+
params["endTime"] = end_time
|
|
270
|
+
|
|
271
|
+
data = self.request(url=self.candlestick_url, params=params)
|
|
272
|
+
return data
|
|
273
|
+
|
|
274
|
+
"""
|
|
275
|
+
USER_DATA endpoints
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def get_account_balance(self):
|
|
279
|
+
"""
|
|
280
|
+
Get account balance
|
|
281
|
+
"""
|
|
282
|
+
payload = {"omitZeroBalances": "true"}
|
|
283
|
+
data = self.signed_request(self.account_url, payload=payload)
|
|
284
|
+
return data
|
|
285
|
+
|
|
286
|
+
def get_wallet_balance(self):
|
|
287
|
+
"""
|
|
288
|
+
Balance by wallet (SPOT, FUNDING, CROSS MARGIN...)
|
|
289
|
+
https://binance-docs.github.io/apidocs/spot/en/#query-user-wallet-balance-user_data
|
|
290
|
+
|
|
291
|
+
This is a consolidated balance across all account
|
|
292
|
+
so it doesn't require us to retrieve isolated margin, cross margin, etc, separately.
|
|
293
|
+
"""
|
|
294
|
+
data = self.signed_request(self.wallet_balance_url)
|
|
295
|
+
return data
|
|
296
|
+
|
|
297
|
+
def cancel_margin_order(self, symbol: str, order_id: int):
|
|
298
|
+
return self.signed_request(
|
|
299
|
+
self.margin_order,
|
|
300
|
+
method="DELETE",
|
|
301
|
+
payload={"symbol": symbol, "orderId": str(order_id)},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def enable_isolated_margin_account(self, symbol):
|
|
305
|
+
return self.signed_request(
|
|
306
|
+
self.isolated_account_url, method="POST", payload={"symbol": symbol}
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def disable_isolated_margin_account(self, symbol):
|
|
310
|
+
"""
|
|
311
|
+
Very high weight, use as little as possible
|
|
312
|
+
|
|
313
|
+
There is a cronjob that disables all margin isolated accounts everyday
|
|
314
|
+
check market_updates
|
|
315
|
+
"""
|
|
316
|
+
return self.signed_request(
|
|
317
|
+
self.isolated_account_url, method="DELETE", payload={"symbol": symbol}
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def get_isolated_account(self, symbol):
|
|
321
|
+
"""
|
|
322
|
+
https://developers.binance.com/docs/margin_trading/account/Query-Isolated-Margin-Account-Info
|
|
323
|
+
Request weight: 10(IP)
|
|
324
|
+
"""
|
|
325
|
+
return self.signed_request(
|
|
326
|
+
self.isolated_account_url, payload={"symbol": symbol}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def transfer_isolated_margin_to_spot(self, asset, symbol, amount):
|
|
330
|
+
return self.signed_request(
|
|
331
|
+
self.margin_isolated_transfer_url,
|
|
332
|
+
method="POST",
|
|
333
|
+
payload={
|
|
334
|
+
"transFrom": "ISOLATED_MARGIN",
|
|
335
|
+
"transTo": "SPOT",
|
|
336
|
+
"asset": asset,
|
|
337
|
+
"symbol": symbol,
|
|
338
|
+
"amount": amount,
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def transfer_spot_to_isolated_margin(self, asset: str, symbol: str, amount: float):
|
|
343
|
+
return self.signed_request(
|
|
344
|
+
self.margin_isolated_transfer_url,
|
|
345
|
+
method="POST",
|
|
346
|
+
payload={
|
|
347
|
+
"transFrom": "SPOT",
|
|
348
|
+
"transTo": "ISOLATED_MARGIN",
|
|
349
|
+
"asset": asset,
|
|
350
|
+
"symbol": symbol,
|
|
351
|
+
"amount": str(amount),
|
|
352
|
+
},
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def create_margin_loan(self, asset, symbol, amount, isIsolated=True):
|
|
356
|
+
if not isIsolated:
|
|
357
|
+
isIsolated = "FALSE"
|
|
358
|
+
else:
|
|
359
|
+
isIsolated = "TRUE"
|
|
360
|
+
|
|
361
|
+
return self.signed_request(
|
|
362
|
+
self.loan_record_url,
|
|
363
|
+
method="POST",
|
|
364
|
+
payload={
|
|
365
|
+
"asset": asset,
|
|
366
|
+
"symbol": symbol,
|
|
367
|
+
"amount": amount,
|
|
368
|
+
"isIsolated": isIsolated,
|
|
369
|
+
"type": "BORROW",
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def get_max_borrow(self, asset, isolated_symbol: str | None = None):
|
|
374
|
+
return self.signed_request(
|
|
375
|
+
self.max_borrow_url,
|
|
376
|
+
payload={"asset": asset, "isolatedSymbol": isolated_symbol},
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def get_margin_loan_details(self, loan_id: int, symbol: str):
|
|
380
|
+
return self.signed_request(
|
|
381
|
+
self.loan_record_url,
|
|
382
|
+
payload={
|
|
383
|
+
"txId": loan_id,
|
|
384
|
+
"type": "BORROW",
|
|
385
|
+
"isolatedSymbol": symbol,
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def get_repay_details(self, loan_id: int, symbol: str):
|
|
390
|
+
return self.signed_request(
|
|
391
|
+
self.loan_record_url,
|
|
392
|
+
payload={
|
|
393
|
+
"txId": loan_id,
|
|
394
|
+
"type": "REPAY",
|
|
395
|
+
"isolatedSymbol": symbol,
|
|
396
|
+
},
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
def repay_margin_loan(
|
|
400
|
+
self, asset: str, symbol: str, amount: float | int, isIsolated: str = "TRUE"
|
|
401
|
+
):
|
|
402
|
+
return self.signed_request(
|
|
403
|
+
self.loan_record_url,
|
|
404
|
+
method="POST",
|
|
405
|
+
payload={
|
|
406
|
+
"asset": asset,
|
|
407
|
+
"isIsolated": isIsolated,
|
|
408
|
+
"symbol": symbol,
|
|
409
|
+
"amount": amount,
|
|
410
|
+
"type": "REPAY",
|
|
411
|
+
},
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def manual_liquidation(self, symbol: str):
|
|
415
|
+
"""
|
|
416
|
+
Not supported in region
|
|
417
|
+
"""
|
|
418
|
+
return self.signed_request(
|
|
419
|
+
self.manual_liquidation_url,
|
|
420
|
+
method="POST",
|
|
421
|
+
payload={
|
|
422
|
+
"symbol": symbol,
|
|
423
|
+
"type": "ISOLATED",
|
|
424
|
+
},
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
def get_interest_history(self, asset: str, symbol: str):
|
|
428
|
+
return self.signed_request(
|
|
429
|
+
self.interest_history_url,
|
|
430
|
+
payload={"asset": asset, "isolatedSymbol": symbol},
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def get_isolated_balance(self, symbol=None) -> list:
|
|
434
|
+
"""
|
|
435
|
+
Get balance of Isolated Margin account
|
|
436
|
+
|
|
437
|
+
Use isolated margin account is preferrable,
|
|
438
|
+
because this is the one that supports the most assets
|
|
439
|
+
"""
|
|
440
|
+
payload = {}
|
|
441
|
+
if symbol:
|
|
442
|
+
payload["symbols"] = [symbol]
|
|
443
|
+
info = self.signed_request(url=self.isolated_account_url, payload=payload)
|
|
444
|
+
assets = info["assets"]
|
|
445
|
+
return assets
|
|
446
|
+
|
|
447
|
+
def get_isolated_balance_total(self):
|
|
448
|
+
"""
|
|
449
|
+
Get balance of Isolated Margin account
|
|
450
|
+
|
|
451
|
+
Use isolated margin account is preferrable,
|
|
452
|
+
because this is the one that supports the most assets
|
|
453
|
+
"""
|
|
454
|
+
info = self.signed_request(url=self.isolated_account_url, payload={})
|
|
455
|
+
assets = info["totalNetAssetOfBtc"]
|
|
456
|
+
if len(assets) == 0:
|
|
457
|
+
raise IsolateBalanceError(
|
|
458
|
+
"Hit symbol 24hr restriction or not available (requires transfer in)"
|
|
459
|
+
)
|
|
460
|
+
return assets
|
|
461
|
+
|
|
462
|
+
def transfer_dust(self, assets: list[str]):
|
|
463
|
+
"""
|
|
464
|
+
Transform small balances to BNB
|
|
465
|
+
"""
|
|
466
|
+
list_assets = ",".join(assets)
|
|
467
|
+
response = self.signed_request(
|
|
468
|
+
url=self.dust_transfer_url, method="POST", payload={"asset": list_assets}
|
|
469
|
+
)
|
|
470
|
+
return response
|
|
471
|
+
|
|
472
|
+
def query_open_orders(self, symbol):
|
|
473
|
+
"""
|
|
474
|
+
Get current open orders
|
|
475
|
+
|
|
476
|
+
This is a high weight endpoint IP Weight: 20
|
|
477
|
+
https://binance-docs.github.io/apidocs/spot/en/#current-open-orders-user_data
|
|
478
|
+
"""
|
|
479
|
+
open_orders = self.signed_request(self.open_orders, payload={"symbol": symbol})
|
|
480
|
+
return open_orders
|
|
481
|
+
|
|
482
|
+
def get_all_orders(self, symbol, order_id: str | None = None, start_time=None):
|
|
483
|
+
"""
|
|
484
|
+
Get all orders given symbol and order_id
|
|
485
|
+
|
|
486
|
+
This is a high weight endpoint IP Weight: 20
|
|
487
|
+
https://binance-docs.github.io/apidocs/spot/en/#all-orders-user_data
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
- symbol: str
|
|
491
|
+
- order_id: int
|
|
492
|
+
- start_time
|
|
493
|
+
- end_time
|
|
494
|
+
|
|
495
|
+
At least one of order_id or (start_time and end_time) must be sent
|
|
496
|
+
"""
|
|
497
|
+
if order_id:
|
|
498
|
+
return self.signed_request(
|
|
499
|
+
self.all_orders_url, payload={"symbol": symbol, "orderId": order_id}
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
elif start_time:
|
|
503
|
+
return self.signed_request(
|
|
504
|
+
self.all_orders_url, payload={"symbol": symbol, "startTime": start_time}
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
else:
|
|
508
|
+
raise ValueError(
|
|
509
|
+
"At least one of order_id or (start_time and end_time) must be sent"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def delete_opened_order(self, symbol, order_id):
|
|
513
|
+
"""
|
|
514
|
+
Cancel single order
|
|
515
|
+
"""
|
|
516
|
+
return self.signed_request(
|
|
517
|
+
self.order_url,
|
|
518
|
+
method="DELETE",
|
|
519
|
+
payload={"symbol": symbol, "orderId": order_id},
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def get_book_depth(self, symbol: str) -> dict:
|
|
523
|
+
"""
|
|
524
|
+
Get order book for a given symbol
|
|
525
|
+
"""
|
|
526
|
+
data = self.request(url=f"{self.order_book_url}?symbol={symbol}")
|
|
527
|
+
return data
|
|
528
|
+
|
|
529
|
+
def get_user_asset(self, asset: str, need_btc_valuation: bool = False):
|
|
530
|
+
"""
|
|
531
|
+
Get user asset
|
|
532
|
+
|
|
533
|
+
https://developers.binance.com/docs/wallet/asset/user-assets
|
|
534
|
+
response:
|
|
535
|
+
{
|
|
536
|
+
"asset": "AVAX",
|
|
537
|
+
"free": "1",
|
|
538
|
+
"locked": "0",
|
|
539
|
+
"freeze": "0",
|
|
540
|
+
"withdrawing": "0",
|
|
541
|
+
"ipoable": "0",
|
|
542
|
+
"btcValuation": "0"
|
|
543
|
+
},
|
|
544
|
+
"""
|
|
545
|
+
data = self.signed_request(
|
|
546
|
+
url=self.user_asset_url,
|
|
547
|
+
method="POST",
|
|
548
|
+
payload={"asset": asset, "needBtcValuation": need_btc_valuation},
|
|
549
|
+
)
|
|
550
|
+
return data
|
|
551
|
+
|
|
552
|
+
def ticker_24_pct_change(
|
|
553
|
+
self, symbol: str = "BTCUSDC", type: str = "FULL"
|
|
554
|
+
) -> float:
|
|
555
|
+
"""Return only the last price from Binance 24h ticker for a symbol.
|
|
556
|
+
Default symbol is BTCUSDC to match project usage elsewhere.
|
|
557
|
+
"""
|
|
558
|
+
data = self.ticker_24(type=type, symbol=symbol)
|
|
559
|
+
try:
|
|
560
|
+
last_price = float(
|
|
561
|
+
data["priceChangePercent"]
|
|
562
|
+
) # Binance returns string numbers
|
|
563
|
+
return last_price
|
|
564
|
+
except Exception as e:
|
|
565
|
+
raise RuntimeError(f"Failed to get last price for {symbol}: {e}")
|
|
566
|
+
|
|
567
|
+
def ticker_24_last_price_cached(
|
|
568
|
+
self,
|
|
569
|
+
ttl_seconds: int = 3600,
|
|
570
|
+
) -> float:
|
|
571
|
+
"""Cached version of ticker_24_last_price with TTL (per-process cache).
|
|
572
|
+
Adjust ttl_seconds as needed (e.g., 30*3600 for 30 hours).
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
@cache(ttl_seconds=ttl_seconds)
|
|
576
|
+
def _cached() -> float:
|
|
577
|
+
return self.ticker_24_pct_change(symbol="BTCUSDC", type="FULL")
|
|
578
|
+
|
|
579
|
+
return _cached()
|
|
580
|
+
|
|
581
|
+
def get_tags(self, symbol: str) -> dict:
|
|
582
|
+
"""
|
|
583
|
+
Get tags for a specific symbol.
|
|
584
|
+
"""
|
|
585
|
+
response = self.request(self.tags_url, params={"symbol": symbol})
|
|
586
|
+
if response["success"]:
|
|
587
|
+
return response["data"]
|
|
588
|
+
raise HTTPError(response["message"], response=response)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class BinanceErrors(Exception):
|
|
2
|
+
def __init__(self, msg, code):
|
|
3
|
+
self.code = code
|
|
4
|
+
self.message = msg
|
|
5
|
+
super().__init__(self.code, self.message)
|
|
6
|
+
return None
|
|
7
|
+
|
|
8
|
+
def __str__(self) -> str:
|
|
9
|
+
return f"{self.code} {self.message}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidSymbol(BinanceErrors):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotEnoughFunds(BinanceErrors):
|
|
17
|
+
pass
|