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.
@@ -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,9 @@
1
+ class KucoinErrors(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}"
@@ -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