pybinbot 0.1.6__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 +162 -0
- 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/models/__init__.py +0 -0
- {models → pybinbot/models}/bot_base.py +5 -5
- {models → pybinbot/models}/deal.py +24 -16
- {models → pybinbot/models}/order.py +41 -33
- pybinbot/models/routes.py +6 -0
- {models → pybinbot/models}/signals.py +5 -10
- pybinbot/py.typed +0 -0
- pybinbot/shared/__init__.py +0 -0
- pybinbot/shared/cache.py +32 -0
- {shared → pybinbot/shared}/enums.py +33 -22
- pybinbot/shared/handlers.py +89 -0
- pybinbot/shared/heikin_ashi.py +198 -0
- pybinbot/shared/indicators.py +271 -0
- {shared → pybinbot/shared}/logging_config.py +1 -3
- {shared → pybinbot/shared}/timestamps.py +5 -4
- pybinbot/shared/types.py +12 -0
- {pybinbot-0.1.6.dist-info → pybinbot-0.4.15.dist-info}/METADATA +22 -2
- pybinbot-0.4.15.dist-info/RECORD +32 -0
- pybinbot-0.4.15.dist-info/top_level.txt +1 -0
- pybinbot-0.1.6.dist-info/RECORD +0 -15
- pybinbot-0.1.6.dist-info/top_level.txt +0 -3
- pybinbot.py +0 -93
- shared/types.py +0 -8
- {shared → pybinbot/shared}/maths.py +0 -0
- {pybinbot-0.1.6.dist-info → pybinbot-0.4.15.dist-info}/WHEEL +0 -0
- {pybinbot-0.1.6.dist-info → pybinbot-0.4.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from aiohttp import ClientSession
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
from pybinbot import ExchangeId, Status
|
|
6
|
+
from requests import Session
|
|
7
|
+
from pybinbot import BinanceApi, handle_binance_errors, aio_response_handler
|
|
8
|
+
|
|
9
|
+
load_dotenv()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BinbotApi:
|
|
13
|
+
"""
|
|
14
|
+
API endpoints on this project itself
|
|
15
|
+
includes Binance Api
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
bb_base_url = os.getenv("BACKEND_DOMAIN", "https://api.terminal.binbot.in")
|
|
19
|
+
bb_symbols_raw = f"{bb_base_url}/account/symbols"
|
|
20
|
+
bb_bot_url = f"{bb_base_url}/bot"
|
|
21
|
+
bb_activate_bot_url = f"{bb_base_url}/bot/activate"
|
|
22
|
+
bb_gainers_losers = f"{bb_base_url}/account/gainers-losers"
|
|
23
|
+
bb_market_domination = f"{bb_base_url}/charts/market-domination"
|
|
24
|
+
bb_top_gainers = f"{bb_base_url}/charts/top-gainers"
|
|
25
|
+
bb_top_losers = f"{bb_base_url}/charts/top-losers"
|
|
26
|
+
bb_btc_correlation_url = f"{bb_base_url}/charts/btc-correlation"
|
|
27
|
+
bb_timeseries_url = f"{bb_base_url}/charts/timeseries"
|
|
28
|
+
bb_adr_series_url = f"{bb_base_url}/charts/adr-series"
|
|
29
|
+
|
|
30
|
+
# Trade operations
|
|
31
|
+
bb_buy_order_url = f"{bb_base_url}/order/buy"
|
|
32
|
+
bb_tp_buy_order_url = f"{bb_base_url}/order/buy/take-profit"
|
|
33
|
+
bb_buy_market_order_url = f"{bb_base_url}/order/buy/market"
|
|
34
|
+
bb_sell_order_url = f"{bb_base_url}/order/sell"
|
|
35
|
+
bb_tp_sell_order_url = f"{bb_base_url}/order/sell/take-profit"
|
|
36
|
+
bb_sell_market_order_url = f"{bb_base_url}/order/sell/market"
|
|
37
|
+
bb_opened_orders_url = f"{bb_base_url}/order/open"
|
|
38
|
+
bb_close_order_url = f"{bb_base_url}/order/close"
|
|
39
|
+
bb_stop_buy_order_url = f"{bb_base_url}/order/buy/stop-limit"
|
|
40
|
+
bb_stop_sell_order_url = f"{bb_base_url}/order/sell/stop-limit"
|
|
41
|
+
bb_submit_errors = f"{bb_base_url}/bot/errors"
|
|
42
|
+
bb_pt_submit_errors_url = f"{bb_base_url}/paper-trading/errors"
|
|
43
|
+
bb_liquidation_url = f"{bb_base_url}/account/one-click-liquidation"
|
|
44
|
+
|
|
45
|
+
# balances
|
|
46
|
+
bb_balance_url = f"{bb_base_url}/account/balance"
|
|
47
|
+
bb_balance_series_url = f"{bb_base_url}/account/balance/series"
|
|
48
|
+
bb_kucoin_balance_url = f"{bb_base_url}/account/kucoin-balance"
|
|
49
|
+
|
|
50
|
+
# research
|
|
51
|
+
bb_autotrade_settings_url = f"{bb_base_url}/autotrade-settings/bots"
|
|
52
|
+
bb_blacklist_url = f"{bb_base_url}/research/blacklist"
|
|
53
|
+
bb_symbols = f"{bb_base_url}/symbols"
|
|
54
|
+
bb_one_symbol_url = f"{bb_base_url}/symbol"
|
|
55
|
+
|
|
56
|
+
# bots
|
|
57
|
+
bb_active_pairs = f"{bb_base_url}/bot/active-pairs"
|
|
58
|
+
|
|
59
|
+
# paper trading
|
|
60
|
+
bb_test_bot_url = f"{bb_base_url}/paper-trading"
|
|
61
|
+
bb_paper_trading_url = f"{bb_base_url}/paper-trading"
|
|
62
|
+
bb_activate_test_bot_url = f"{bb_base_url}/paper-trading/activate"
|
|
63
|
+
bb_paper_trading_activate_url = f"{bb_base_url}/paper-trading/activate"
|
|
64
|
+
bb_paper_trading_deactivate_url = f"{bb_base_url}/paper-trading/deactivate"
|
|
65
|
+
bb_test_bot_active_list = f"{bb_base_url}/paper-trading/active-list"
|
|
66
|
+
bb_test_autotrade_url = f"{bb_base_url}/autotrade-settings/paper-trading"
|
|
67
|
+
bb_test_active_pairs = f"{bb_base_url}/paper-trading/active-pairs"
|
|
68
|
+
|
|
69
|
+
def request(self, url, method="GET", session: Session = Session(), **kwargs):
|
|
70
|
+
res = session.request(url=url, method=method, **kwargs)
|
|
71
|
+
data = handle_binance_errors(res)
|
|
72
|
+
return data
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
Async HTTP client/server for asyncio
|
|
76
|
+
that replaces requests library
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
async def fetch(self, url, method="GET", **kwargs):
|
|
80
|
+
async with ClientSession() as session:
|
|
81
|
+
async with session.request(method=method, url=url, **kwargs) as response:
|
|
82
|
+
data = await aio_response_handler(response)
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
def get_symbols(self) -> list[dict]:
|
|
86
|
+
response = self.request(url=self.bb_symbols)
|
|
87
|
+
return response["data"]
|
|
88
|
+
|
|
89
|
+
def get_single_symbol(self, symbol: str) -> dict:
|
|
90
|
+
response = self.request(url=f"{self.bb_one_symbol_url}/{symbol}")
|
|
91
|
+
return response["data"]
|
|
92
|
+
|
|
93
|
+
async def get_market_breadth(self, size=400):
|
|
94
|
+
"""
|
|
95
|
+
Get market breadth data
|
|
96
|
+
"""
|
|
97
|
+
response = await self.fetch(url=self.bb_adr_series_url, params={"size": size})
|
|
98
|
+
if "data" in response:
|
|
99
|
+
return response["data"]
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def get_latest_btc_price(self):
|
|
103
|
+
binance_api = BinanceApi()
|
|
104
|
+
# Get 24hr last BTCUSDC
|
|
105
|
+
btc_ticker_24 = binance_api.get_ticker_price("BTCUSDC")
|
|
106
|
+
self.btc_change_perc = float(btc_ticker_24["priceChangePercent"])
|
|
107
|
+
return self.btc_change_perc
|
|
108
|
+
|
|
109
|
+
def post_error(self, msg):
|
|
110
|
+
data = self.request(
|
|
111
|
+
method="PUT", url=self.bb_autotrade_settings_url, json={"system_logs": msg}
|
|
112
|
+
)
|
|
113
|
+
return data
|
|
114
|
+
|
|
115
|
+
def get_test_autotrade_settings(self):
|
|
116
|
+
data = self.request(url=self.bb_test_autotrade_url)
|
|
117
|
+
return data["data"]
|
|
118
|
+
|
|
119
|
+
def get_autotrade_settings(self) -> dict:
|
|
120
|
+
data = self.request(url=self.bb_autotrade_settings_url)
|
|
121
|
+
return data["data"]
|
|
122
|
+
|
|
123
|
+
def get_bots_by_status(
|
|
124
|
+
self,
|
|
125
|
+
start_date,
|
|
126
|
+
end_date,
|
|
127
|
+
collection_name="bots",
|
|
128
|
+
status=Status.active,
|
|
129
|
+
):
|
|
130
|
+
url = self.bb_bot_url
|
|
131
|
+
if collection_name == "paper_trading":
|
|
132
|
+
url = self.bb_test_bot_url
|
|
133
|
+
|
|
134
|
+
data = self.request(
|
|
135
|
+
url=url,
|
|
136
|
+
params={
|
|
137
|
+
"status": status.value,
|
|
138
|
+
"start_date": start_date,
|
|
139
|
+
"end_date": end_date,
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
return data["data"]
|
|
143
|
+
|
|
144
|
+
def submit_bot_event_logs(self, bot_id, message):
|
|
145
|
+
data = self.request(
|
|
146
|
+
url=f"{self.bb_submit_errors}/{bot_id}",
|
|
147
|
+
method="POST",
|
|
148
|
+
json={"errors": message},
|
|
149
|
+
)
|
|
150
|
+
return data
|
|
151
|
+
|
|
152
|
+
def submit_paper_trading_event_logs(self, bot_id, message):
|
|
153
|
+
data = self.request(
|
|
154
|
+
url=f"{self.bb_pt_submit_errors_url}/{bot_id}",
|
|
155
|
+
method="POST",
|
|
156
|
+
json={"errors": message},
|
|
157
|
+
)
|
|
158
|
+
return data
|
|
159
|
+
|
|
160
|
+
def add_to_blacklist(self, symbol, reason=None):
|
|
161
|
+
payload = {"symbol": symbol, "reason": reason}
|
|
162
|
+
data = self.request(url=self.bb_blacklist_url, method="POST", json=payload)
|
|
163
|
+
return data
|
|
164
|
+
|
|
165
|
+
def clean_margin_short(self, pair):
|
|
166
|
+
"""
|
|
167
|
+
Liquidate and disable margin_short trades
|
|
168
|
+
"""
|
|
169
|
+
data = self.request(url=f"{self.bb_liquidation_url}/{pair}", method="DELETE")
|
|
170
|
+
return data
|
|
171
|
+
|
|
172
|
+
def delete_bot(self, bot_id: str | list[str]):
|
|
173
|
+
bot_ids = []
|
|
174
|
+
if isinstance(bot_id, str):
|
|
175
|
+
bot_ids.append(bot_id)
|
|
176
|
+
|
|
177
|
+
data = self.request(
|
|
178
|
+
url=f"{self.bb_bot_url}", method="DELETE", params={"id": bot_ids}
|
|
179
|
+
)
|
|
180
|
+
return data
|
|
181
|
+
|
|
182
|
+
def get_balances(self):
|
|
183
|
+
data = self.request(url=self.bb_balance_url)
|
|
184
|
+
return data
|
|
185
|
+
|
|
186
|
+
def get_balances_by_type(self):
|
|
187
|
+
data = self.request(url=self.bb_kucoin_balance_url)
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
def get_available_fiat(
|
|
191
|
+
self, exchange: str, fiat: str = "USDT", is_margin=False
|
|
192
|
+
) -> float:
|
|
193
|
+
if exchange == ExchangeId.KUCOIN.value:
|
|
194
|
+
all_balances = self.get_balances_by_type()
|
|
195
|
+
available_fiat = 0.0
|
|
196
|
+
|
|
197
|
+
for item in all_balances["data"]["balances"]:
|
|
198
|
+
if is_margin:
|
|
199
|
+
if item == "margin":
|
|
200
|
+
for key in all_balances["data"]["balances"]["margin"]:
|
|
201
|
+
if key == fiat:
|
|
202
|
+
available_fiat += float(
|
|
203
|
+
all_balances["data"]["balances"]["margin"][key]
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
if item == "trade":
|
|
207
|
+
for key in all_balances["data"]["balances"]["trade"]:
|
|
208
|
+
if key == fiat:
|
|
209
|
+
available_fiat += float(
|
|
210
|
+
all_balances["data"]["balances"]["trade"][key]
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if item == "main":
|
|
214
|
+
for key in all_balances["data"]["balances"]["main"]:
|
|
215
|
+
if key == fiat:
|
|
216
|
+
available_fiat += float(
|
|
217
|
+
all_balances["data"]["balances"]["main"][key]
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return float(all_balances["data"]["fiat_available"])
|
|
221
|
+
else:
|
|
222
|
+
all_balances = self.get_balances()
|
|
223
|
+
return float(all_balances["data"]["fiat_available"])
|
|
224
|
+
|
|
225
|
+
def create_bot(self, data):
|
|
226
|
+
data = self.request(url=self.bb_bot_url, method="POST", data=data)
|
|
227
|
+
return data
|
|
228
|
+
|
|
229
|
+
def activate_bot(self, bot_id):
|
|
230
|
+
data = self.request(url=f"{self.bb_activate_bot_url}/{bot_id}")
|
|
231
|
+
return data
|
|
232
|
+
|
|
233
|
+
def create_paper_bot(self, data):
|
|
234
|
+
data = self.request(url=self.bb_test_bot_url, method="POST", data=data)
|
|
235
|
+
return data
|
|
236
|
+
|
|
237
|
+
def activate_paper_bot(self, bot_id):
|
|
238
|
+
data = self.request(url=f"{self.bb_activate_test_bot_url}/{bot_id}")
|
|
239
|
+
return data
|
|
240
|
+
|
|
241
|
+
def delete_paper_bot(self, bot_id):
|
|
242
|
+
bot_ids = []
|
|
243
|
+
if isinstance(bot_id, str):
|
|
244
|
+
bot_ids.append(bot_id)
|
|
245
|
+
|
|
246
|
+
data = self.request(
|
|
247
|
+
url=f"{self.bb_test_bot_url}", method="DELETE", data={"id": bot_ids}
|
|
248
|
+
)
|
|
249
|
+
return data
|
|
250
|
+
|
|
251
|
+
def get_active_pairs(self, collection_name="bots"):
|
|
252
|
+
"""
|
|
253
|
+
Get distinct (non-repeating) bots by status active
|
|
254
|
+
"""
|
|
255
|
+
url = self.bb_active_pairs
|
|
256
|
+
if collection_name == "paper_trading":
|
|
257
|
+
url = self.bb_test_active_pairs
|
|
258
|
+
|
|
259
|
+
res = self.request(
|
|
260
|
+
url=url,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if res["data"] is None:
|
|
264
|
+
return []
|
|
265
|
+
|
|
266
|
+
return res["data"]
|
|
267
|
+
|
|
268
|
+
def filter_excluded_symbols(self) -> list[str]:
|
|
269
|
+
"""
|
|
270
|
+
all symbols that are active, not blacklisted
|
|
271
|
+
minus active bots
|
|
272
|
+
minus all symbols that match base asset of these active bots
|
|
273
|
+
i.e. BTC in BTCUSDC
|
|
274
|
+
"""
|
|
275
|
+
active_pairs = self.get_active_pairs()
|
|
276
|
+
all_symbols = self.get_symbols()
|
|
277
|
+
exclusion_list = []
|
|
278
|
+
exclusion_list.extend(active_pairs)
|
|
279
|
+
|
|
280
|
+
for s in all_symbols:
|
|
281
|
+
for ap in active_pairs:
|
|
282
|
+
if (
|
|
283
|
+
ap.startswith(s["base_asset"]) and s["id"] not in exclusion_list
|
|
284
|
+
) or (not s["active"]):
|
|
285
|
+
exclusion_list.append(s["id"])
|
|
286
|
+
|
|
287
|
+
return exclusion_list
|
|
288
|
+
|
|
289
|
+
async def get_top_gainers(self):
|
|
290
|
+
"""
|
|
291
|
+
Top crypto/token/coin gainers of the day
|
|
292
|
+
"""
|
|
293
|
+
response = await self.fetch(url=self.bb_top_gainers)
|
|
294
|
+
return response["data"]
|
|
295
|
+
|
|
296
|
+
async def get_top_losers(self):
|
|
297
|
+
"""
|
|
298
|
+
Top crypto/token/coin losers of the day
|
|
299
|
+
"""
|
|
300
|
+
response = await self.fetch(url=self.bb_top_losers)
|
|
301
|
+
return response["data"]
|
|
302
|
+
|
|
303
|
+
def get_btc_correlation(self, symbol) -> tuple[float, float]:
|
|
304
|
+
"""
|
|
305
|
+
Get BTC correlation and 24hr price change
|
|
306
|
+
"""
|
|
307
|
+
response = self.request(
|
|
308
|
+
url=self.bb_btc_correlation_url, params={"symbol": symbol}
|
|
309
|
+
)
|
|
310
|
+
data = response["data"]
|
|
311
|
+
correlation = float(data["correlation"])
|
|
312
|
+
price_change_24hr = float(data["24hr_price_change"])
|
|
313
|
+
return correlation, price_change_24hr
|
|
314
|
+
|
|
315
|
+
def price_precision(self, symbol) -> int:
|
|
316
|
+
"""
|
|
317
|
+
Get price decimals from API db
|
|
318
|
+
"""
|
|
319
|
+
symbol_info = self.get_single_symbol(symbol)
|
|
320
|
+
return symbol_info["price_precision"]
|
|
321
|
+
|
|
322
|
+
def qty_precision(self, symbol) -> int:
|
|
323
|
+
"""
|
|
324
|
+
Get qty decimals from API db
|
|
325
|
+
"""
|
|
326
|
+
symbol_info = self.get_single_symbol(symbol)
|
|
327
|
+
return symbol_info["qty_precision"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class BinbotErrors(Exception):
|
|
2
|
+
def __init__(self, msg, code=None):
|
|
3
|
+
self.message = msg
|
|
4
|
+
self.code = code
|
|
5
|
+
super().__init__(self.message)
|
|
6
|
+
return None
|
|
7
|
+
|
|
8
|
+
def __str__(self) -> str:
|
|
9
|
+
return f"{self.message}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class IsolateBalanceError(BinbotErrors):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QuantityTooLow(BinbotErrors):
|
|
17
|
+
"""
|
|
18
|
+
Raised when LOT_SIZE filter error triggers
|
|
19
|
+
This error should happen in the least cases,
|
|
20
|
+
unless purposedly triggered to check quantity
|
|
21
|
+
e.g. BTC = 0.0001 amounts are usually so small that it's hard to see if it's nothing or a considerable amount compared to others
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MarginShortError(BinbotErrors):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MarginLoanNotFound(BinbotErrors):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DeleteOrderError(BinbotErrors):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LowBalanceCleanupError(BinbotErrors):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DealCreationError(BinbotErrors):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SaveBotError(BinbotErrors):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class InsufficientBalance(BinbotErrors):
|
|
52
|
+
"""
|
|
53
|
+
Insufficient total_buy_qty to deactivate
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
pass
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from kucoin_universal_sdk.generate.spot.market import (
|
|
2
|
+
GetPartOrderBookReqBuilder,
|
|
3
|
+
GetAllSymbolsReqBuilder,
|
|
4
|
+
GetSymbolReqBuilder,
|
|
5
|
+
)
|
|
6
|
+
from kucoin_universal_sdk.generate.account.account import (
|
|
7
|
+
GetSpotAccountListReqBuilder,
|
|
8
|
+
GetIsolatedMarginAccountReqBuilder,
|
|
9
|
+
)
|
|
10
|
+
from kucoin_universal_sdk.generate.account.account.model_get_isolated_margin_account_resp import (
|
|
11
|
+
GetIsolatedMarginAccountResp,
|
|
12
|
+
)
|
|
13
|
+
from kucoin_universal_sdk.generate.account.transfer.model_flex_transfer_req import (
|
|
14
|
+
FlexTransferReq,
|
|
15
|
+
FlexTransferReqBuilder,
|
|
16
|
+
)
|
|
17
|
+
from kucoin_universal_sdk.generate.account.transfer.model_flex_transfer_resp import (
|
|
18
|
+
FlexTransferResp,
|
|
19
|
+
)
|
|
20
|
+
from uuid import uuid4
|
|
21
|
+
from pybinbot.apis.kucoin.orders import KucoinOrders
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class KucoinApi(KucoinOrders):
|
|
25
|
+
def __init__(self, key: str, secret: str, passphrase: str):
|
|
26
|
+
super().__init__(key=key, secret=secret, passphrase=passphrase)
|
|
27
|
+
self.account_api = (
|
|
28
|
+
self.client.rest_service().get_account_service().get_account_api()
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def get_all_symbols(self):
|
|
32
|
+
request = GetAllSymbolsReqBuilder().build()
|
|
33
|
+
response = self.spot_api.get_all_symbols(request)
|
|
34
|
+
return response
|
|
35
|
+
|
|
36
|
+
def get_symbol(self, symbol: str):
|
|
37
|
+
"""
|
|
38
|
+
Get single symbol data
|
|
39
|
+
"""
|
|
40
|
+
request = GetSymbolReqBuilder().set_symbol(symbol).build()
|
|
41
|
+
response = self.spot_api.get_symbol(request)
|
|
42
|
+
return response
|
|
43
|
+
|
|
44
|
+
def get_ticker_price(self, symbol: str) -> float:
|
|
45
|
+
request = GetPartOrderBookReqBuilder().set_symbol(symbol).set_size("1").build()
|
|
46
|
+
response = self.spot_api.get_ticker(request)
|
|
47
|
+
return float(response.price)
|
|
48
|
+
|
|
49
|
+
def get_account_balance(self):
|
|
50
|
+
"""
|
|
51
|
+
Aggregate all balances from all account types (spot, main, trade, margin, futures).
|
|
52
|
+
|
|
53
|
+
The right data shape for Kucion should be provided by
|
|
54
|
+
get_account_balance_by_type method.
|
|
55
|
+
|
|
56
|
+
However, this method provides a normalized version for backwards compatibility (Binance) and consistency with current balances table.
|
|
57
|
+
|
|
58
|
+
Returns a dict:
|
|
59
|
+
{
|
|
60
|
+
asset:
|
|
61
|
+
{
|
|
62
|
+
total: float,
|
|
63
|
+
breakdown:
|
|
64
|
+
{
|
|
65
|
+
account_type: float, ...
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
"""
|
|
70
|
+
spot_request = GetSpotAccountListReqBuilder().build()
|
|
71
|
+
all_accounts = self.account_api.get_spot_account_list(spot_request)
|
|
72
|
+
balance_items = dict()
|
|
73
|
+
for item in all_accounts.data:
|
|
74
|
+
if float(item.balance) > 0:
|
|
75
|
+
balance_items[item.currency] = {
|
|
76
|
+
"balance": float(item.balance),
|
|
77
|
+
"free": float(item.available),
|
|
78
|
+
"locked": float(item.holds),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
margin_request = GetIsolatedMarginAccountReqBuilder().build()
|
|
82
|
+
margin_accounts = self.account_api.get_isolated_margin_account(margin_request)
|
|
83
|
+
if float(margin_accounts.total_asset_of_quote_currency) > 0:
|
|
84
|
+
balance_items["USDT"]["balance"] += float(
|
|
85
|
+
margin_accounts.total_asset_of_quote_currency
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return balance_items
|
|
89
|
+
|
|
90
|
+
def get_account_balance_by_type(self) -> dict[str, dict[str, dict[str, float]]]:
|
|
91
|
+
"""
|
|
92
|
+
Get balances grouped by account type.
|
|
93
|
+
Returns:
|
|
94
|
+
{
|
|
95
|
+
'MAIN': {'USDT': {...}, 'BTC': {...}, ...},
|
|
96
|
+
'TRADE': {'USDT': {...}, ...},
|
|
97
|
+
'MARGIN': {...},
|
|
98
|
+
...
|
|
99
|
+
}
|
|
100
|
+
Each currency has: balance (total), available, holds
|
|
101
|
+
"""
|
|
102
|
+
spot_request = GetSpotAccountListReqBuilder().build()
|
|
103
|
+
all_accounts = self.account_api.get_spot_account_list(spot_request)
|
|
104
|
+
|
|
105
|
+
balance_by_type: dict[str, dict[str, dict[str, float]]] = {}
|
|
106
|
+
for item in all_accounts.data:
|
|
107
|
+
if float(item.balance) > 0:
|
|
108
|
+
account_type = item.type # MAIN, TRADE, MARGIN, etc.
|
|
109
|
+
if account_type not in balance_by_type:
|
|
110
|
+
balance_by_type[account_type] = {}
|
|
111
|
+
balance_by_type[account_type][item.currency] = {
|
|
112
|
+
"balance": float(item.balance),
|
|
113
|
+
"available": float(item.available),
|
|
114
|
+
"holds": float(item.holds),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return balance_by_type
|
|
118
|
+
|
|
119
|
+
def get_single_spot_balance(self, asset: str) -> float:
|
|
120
|
+
spot_request = GetSpotAccountListReqBuilder().build()
|
|
121
|
+
all_accounts = self.account_api.get_spot_account_list(spot_request)
|
|
122
|
+
total_balance = 0.0
|
|
123
|
+
for item in all_accounts.data:
|
|
124
|
+
if item.currency == asset:
|
|
125
|
+
return float(item.balance)
|
|
126
|
+
|
|
127
|
+
return total_balance
|
|
128
|
+
|
|
129
|
+
def get_isolated_balance(self, symbol: str) -> GetIsolatedMarginAccountResp:
|
|
130
|
+
request = GetIsolatedMarginAccountReqBuilder().set_symbol(symbol).build()
|
|
131
|
+
response = self.account_api.get_isolated_margin_account(request)
|
|
132
|
+
return response
|
|
133
|
+
|
|
134
|
+
def transfer_isolated_margin_to_spot(
|
|
135
|
+
self, asset: str, symbol: str, amount: float
|
|
136
|
+
) -> FlexTransferResp:
|
|
137
|
+
"""
|
|
138
|
+
Transfer funds from isolated margin to spot (main) account.
|
|
139
|
+
`symbol` is the isolated pair like "BTC-USDT".
|
|
140
|
+
"""
|
|
141
|
+
client_oid = str(uuid4())
|
|
142
|
+
req = (
|
|
143
|
+
FlexTransferReqBuilder()
|
|
144
|
+
.set_client_oid(client_oid)
|
|
145
|
+
.set_currency(asset)
|
|
146
|
+
.set_amount(str(amount))
|
|
147
|
+
.set_type(FlexTransferReq.TypeEnum.INTERNAL)
|
|
148
|
+
.set_from_account_type(FlexTransferReq.FromAccountTypeEnum.ISOLATED)
|
|
149
|
+
.set_from_account_tag(symbol)
|
|
150
|
+
.set_to_account_type(FlexTransferReq.ToAccountTypeEnum.MAIN)
|
|
151
|
+
.build()
|
|
152
|
+
)
|
|
153
|
+
return self.transfer_api.flex_transfer(req)
|
|
154
|
+
|
|
155
|
+
def transfer_spot_to_isolated_margin(
|
|
156
|
+
self, asset: str, symbol: str, amount: float
|
|
157
|
+
) -> FlexTransferResp:
|
|
158
|
+
"""
|
|
159
|
+
Transfer funds from spot (main) account to isolated margin account.
|
|
160
|
+
`symbol` must be the isolated pair like "BTC-USDT".
|
|
161
|
+
"""
|
|
162
|
+
client_oid = str(uuid4())
|
|
163
|
+
req = (
|
|
164
|
+
FlexTransferReqBuilder()
|
|
165
|
+
.set_client_oid(client_oid)
|
|
166
|
+
.set_currency(asset)
|
|
167
|
+
.set_amount(str(amount))
|
|
168
|
+
.set_type(FlexTransferReq.TypeEnum.INTERNAL)
|
|
169
|
+
.set_from_account_type(FlexTransferReq.FromAccountTypeEnum.MAIN)
|
|
170
|
+
.set_to_account_type(FlexTransferReq.ToAccountTypeEnum.ISOLATED)
|
|
171
|
+
.set_to_account_tag(symbol)
|
|
172
|
+
.build()
|
|
173
|
+
)
|
|
174
|
+
return self.transfer_api.flex_transfer(req)
|
|
175
|
+
|
|
176
|
+
def transfer_main_to_trade(self, asset: str, amount: float) -> FlexTransferResp:
|
|
177
|
+
"""
|
|
178
|
+
Transfer funds from main to trade (spot) account.
|
|
179
|
+
"""
|
|
180
|
+
client_oid = str(uuid4())
|
|
181
|
+
req = (
|
|
182
|
+
FlexTransferReqBuilder()
|
|
183
|
+
.set_client_oid(client_oid)
|
|
184
|
+
.set_currency(asset)
|
|
185
|
+
.set_amount(str(amount))
|
|
186
|
+
.set_type(FlexTransferReq.TypeEnum.INTERNAL)
|
|
187
|
+
.set_from_account_type(FlexTransferReq.FromAccountTypeEnum.MAIN)
|
|
188
|
+
.set_to_account_type(FlexTransferReq.ToAccountTypeEnum.TRADE)
|
|
189
|
+
.build()
|
|
190
|
+
)
|
|
191
|
+
return self.transfer_api.flex_transfer(req)
|
|
192
|
+
|
|
193
|
+
def transfer_trade_to_main(self, asset: str, amount: float) -> FlexTransferResp:
|
|
194
|
+
"""
|
|
195
|
+
Transfer funds from trade (spot) account to main.
|
|
196
|
+
"""
|
|
197
|
+
client_oid = str(uuid4())
|
|
198
|
+
req = (
|
|
199
|
+
FlexTransferReqBuilder()
|
|
200
|
+
.set_client_oid(client_oid)
|
|
201
|
+
.set_currency(asset)
|
|
202
|
+
.set_amount(str(amount))
|
|
203
|
+
.set_type(FlexTransferReq.TypeEnum.INTERNAL)
|
|
204
|
+
.set_from_account_type(FlexTransferReq.FromAccountTypeEnum.TRADE)
|
|
205
|
+
.set_to_account_type(FlexTransferReq.ToAccountTypeEnum.MAIN)
|
|
206
|
+
.build()
|
|
207
|
+
)
|
|
208
|
+
return self.transfer_api.flex_transfer(req)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from pybinbot.apis.kucoin.rest import KucoinRest
|
|
2
|
+
from pybinbot import KucoinKlineIntervals
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from kucoin_universal_sdk.generate.spot.market import GetKlinesReqBuilder
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class KucoinMarket(KucoinRest):
|
|
8
|
+
"""
|
|
9
|
+
Convienience wrapper for Kucoin order operations.
|
|
10
|
+
|
|
11
|
+
- Kucoin transactions don't immediately return all order details so we need cooldown slee
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
TRANSACTION_COOLDOWN_SECONDS = 1
|
|
15
|
+
|
|
16
|
+
def __init__(self, key: str, secret: str, passphrase: str):
|
|
17
|
+
super().__init__(key=key, secret=secret, passphrase=passphrase)
|
|
18
|
+
self.client = self.setup_client()
|
|
19
|
+
self.spot_api = self.client.rest_service().get_spot_service().get_market_api()
|
|
20
|
+
self.order_api = self.client.rest_service().get_spot_service().get_order_api()
|
|
21
|
+
self.margin_order_api = (
|
|
22
|
+
self.client.rest_service().get_margin_service().get_order_api()
|
|
23
|
+
)
|
|
24
|
+
self.debit_api = self.client.rest_service().get_margin_service().get_debit_api()
|
|
25
|
+
self.transfer_api = (
|
|
26
|
+
self.client.rest_service().get_account_service().get_transfer_api()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def get_ui_klines(
|
|
30
|
+
self,
|
|
31
|
+
symbol: str,
|
|
32
|
+
interval: str,
|
|
33
|
+
limit: int = 500,
|
|
34
|
+
start_time=None,
|
|
35
|
+
end_time=None,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Get raw klines/candlestick data from Kucoin.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
symbol: Trading pair symbol (e.g., "BTC-USDT")
|
|
42
|
+
interval: Kline interval (e.g., "15min", "1hour", "1day")
|
|
43
|
+
limit: Number of klines to retrieve (max 1500, default 500)
|
|
44
|
+
start_time: Start time in milliseconds (optional)
|
|
45
|
+
end_time: End time in milliseconds (optional)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of klines in format compatible with Binance format:
|
|
49
|
+
[timestamp, open, high, low, close, volume, close_time, ...]
|
|
50
|
+
"""
|
|
51
|
+
# Compute time window based on limit and interval
|
|
52
|
+
interval_ms = KucoinKlineIntervals.get_interval_ms(interval)
|
|
53
|
+
now_ms = int(datetime.now().timestamp() * 1000)
|
|
54
|
+
# Align end_time to interval boundary
|
|
55
|
+
end_time = now_ms - (now_ms % interval_ms)
|
|
56
|
+
start_time = end_time - (limit * interval_ms)
|
|
57
|
+
|
|
58
|
+
builder = (
|
|
59
|
+
GetKlinesReqBuilder()
|
|
60
|
+
.set_symbol(symbol)
|
|
61
|
+
.set_type(interval)
|
|
62
|
+
.set_start_at(start_time // 1000)
|
|
63
|
+
.set_end_at(end_time // 1000)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
request = builder.build()
|
|
67
|
+
response = self.spot_api.get_klines(request)
|
|
68
|
+
|
|
69
|
+
# Convert Kucoin format to Binance-compatible format
|
|
70
|
+
# Kucoin returns: [time, open, close, high, low, volume, turnover]
|
|
71
|
+
# Binance format: [open_time, open, high, low, close, volume, close_time, ...]
|
|
72
|
+
klines = []
|
|
73
|
+
if response.data:
|
|
74
|
+
for k in response.data:
|
|
75
|
+
# k format: [timestamp(seconds), open, close, high, low, volume, turnover]
|
|
76
|
+
open_time = int(k[0]) * 1000 # Convert to milliseconds
|
|
77
|
+
close_time = open_time + interval_ms # Calculate proper close time
|
|
78
|
+
klines.append(
|
|
79
|
+
[
|
|
80
|
+
open_time, # open_time in milliseconds
|
|
81
|
+
k[1], # open
|
|
82
|
+
k[3], # high
|
|
83
|
+
k[4], # low
|
|
84
|
+
k[2], # close
|
|
85
|
+
k[5], # volume base asset
|
|
86
|
+
close_time, # close_time properly calculated
|
|
87
|
+
k[6], # volume quote asset
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
klines.reverse()
|
|
91
|
+
|
|
92
|
+
return klines
|