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.
Files changed (36) hide show
  1. pybinbot/__init__.py +162 -0
  2. pybinbot/apis/binance/base.py +588 -0
  3. pybinbot/apis/binance/exceptions.py +17 -0
  4. pybinbot/apis/binbot/base.py +327 -0
  5. pybinbot/apis/binbot/exceptions.py +56 -0
  6. pybinbot/apis/kucoin/base.py +208 -0
  7. pybinbot/apis/kucoin/exceptions.py +9 -0
  8. pybinbot/apis/kucoin/market.py +92 -0
  9. pybinbot/apis/kucoin/orders.py +663 -0
  10. pybinbot/apis/kucoin/rest.py +33 -0
  11. pybinbot/models/__init__.py +0 -0
  12. {models → pybinbot/models}/bot_base.py +5 -5
  13. {models → pybinbot/models}/deal.py +24 -16
  14. {models → pybinbot/models}/order.py +41 -33
  15. pybinbot/models/routes.py +6 -0
  16. {models → pybinbot/models}/signals.py +5 -10
  17. pybinbot/py.typed +0 -0
  18. pybinbot/shared/__init__.py +0 -0
  19. pybinbot/shared/cache.py +32 -0
  20. {shared → pybinbot/shared}/enums.py +33 -22
  21. pybinbot/shared/handlers.py +89 -0
  22. pybinbot/shared/heikin_ashi.py +198 -0
  23. pybinbot/shared/indicators.py +271 -0
  24. {shared → pybinbot/shared}/logging_config.py +1 -3
  25. {shared → pybinbot/shared}/timestamps.py +5 -4
  26. pybinbot/shared/types.py +12 -0
  27. {pybinbot-0.1.6.dist-info → pybinbot-0.4.15.dist-info}/METADATA +22 -2
  28. pybinbot-0.4.15.dist-info/RECORD +32 -0
  29. pybinbot-0.4.15.dist-info/top_level.txt +1 -0
  30. pybinbot-0.1.6.dist-info/RECORD +0 -15
  31. pybinbot-0.1.6.dist-info/top_level.txt +0 -3
  32. pybinbot.py +0 -93
  33. shared/types.py +0 -8
  34. {shared → pybinbot/shared}/maths.py +0 -0
  35. {pybinbot-0.1.6.dist-info → pybinbot-0.4.15.dist-info}/WHEEL +0 -0
  36. {pybinbot-0.1.6.dist-info → pybinbot-0.4.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,663 @@
1
+ import random
2
+ import uuid
3
+ import logging
4
+ from time import sleep, time
5
+ from pybinbot.apis.kucoin.market import KucoinMarket
6
+ from kucoin_universal_sdk.generate.spot.order.model_add_order_sync_resp import (
7
+ AddOrderSyncResp,
8
+ )
9
+ from kucoin_universal_sdk.generate.spot.order.model_add_order_sync_req import (
10
+ AddOrderSyncReq,
11
+ AddOrderSyncReqBuilder,
12
+ )
13
+ from kucoin_universal_sdk.generate.spot.order.model_batch_add_orders_sync_req import (
14
+ BatchAddOrdersSyncReqBuilder,
15
+ )
16
+ from kucoin_universal_sdk.generate.spot.order.model_batch_add_orders_sync_order_list import (
17
+ BatchAddOrdersSyncOrderList,
18
+ )
19
+ from kucoin_universal_sdk.generate.spot.order.model_cancel_order_by_order_id_sync_req import (
20
+ CancelOrderByOrderIdSyncReqBuilder,
21
+ )
22
+ from kucoin_universal_sdk.generate.spot.order.model_get_order_by_order_id_req import (
23
+ GetOrderByOrderIdReqBuilder,
24
+ )
25
+ from kucoin_universal_sdk.generate.spot.order.model_get_open_orders_req import (
26
+ GetOpenOrdersReqBuilder,
27
+ )
28
+ from kucoin_universal_sdk.generate.margin.order.model_add_order_req import (
29
+ AddOrderReq,
30
+ AddOrderReqBuilder,
31
+ )
32
+ from kucoin_universal_sdk.generate.margin.order.model_cancel_order_by_order_id_req import (
33
+ CancelOrderByOrderIdReqBuilder,
34
+ )
35
+ from kucoin_universal_sdk.generate.margin.order.model_get_order_by_order_id_resp import (
36
+ GetOrderByOrderIdResp,
37
+ )
38
+ from kucoin_universal_sdk.generate.margin.debit.model_repay_req import (
39
+ RepayReqBuilder,
40
+ )
41
+ from kucoin_universal_sdk.generate.margin.debit.model_repay_resp import (
42
+ RepayResp,
43
+ )
44
+ from kucoin_universal_sdk.generate.margin.debit.model_borrow_req import (
45
+ BorrowReqBuilder,
46
+ )
47
+ from kucoin_universal_sdk.generate.margin.debit.model_borrow_resp import (
48
+ BorrowResp,
49
+ )
50
+ from kucoin_universal_sdk.generate.account.transfer.model_flex_transfer_req import (
51
+ FlexTransferReqBuilder,
52
+ FlexTransferReq,
53
+ )
54
+ from kucoin_universal_sdk.generate.account.transfer.model_flex_transfer_resp import (
55
+ FlexTransferResp,
56
+ )
57
+ from kucoin_universal_sdk.generate.spot.market import (
58
+ GetPartOrderBookReqBuilder,
59
+ GetFullOrderBookReqBuilder,
60
+ )
61
+
62
+ from pybinbot.shared.enums import KucoinKlineIntervals
63
+
64
+
65
+ class KucoinOrders(KucoinMarket):
66
+ """
67
+ Convienience wrapper for Kucoin order operations.
68
+
69
+ - Kucoin transactions don't immediately return all order details so we need cooldown slee
70
+ """
71
+
72
+ TRANSACTION_COOLDOWN_SECONDS = 1
73
+
74
+ def __init__(self, key: str, secret: str, passphrase: str):
75
+ super().__init__(key=key, secret=secret, passphrase=passphrase)
76
+ self.client = self.setup_client()
77
+ self.spot_api = self.client.rest_service().get_spot_service().get_market_api()
78
+ self.order_api = self.client.rest_service().get_spot_service().get_order_api()
79
+ self.margin_order_api = (
80
+ self.client.rest_service().get_margin_service().get_order_api()
81
+ )
82
+ self.debit_api = self.client.rest_service().get_margin_service().get_debit_api()
83
+ self.transfer_api = (
84
+ self.client.rest_service().get_account_service().get_transfer_api()
85
+ )
86
+
87
+ def _get_order_with_retry(
88
+ self, symbol: str, order_id: str, max_retries: int = 10
89
+ ) -> GetOrderByOrderIdResp:
90
+ """
91
+ Get order by ID with exponential backoff retry.
92
+ KuCoin's order data is not immediately available after placement.
93
+
94
+ We only consider the order "ready" when:
95
+ - it is no longer active (order.active is False), and
96
+ - id, price and size are all populated.
97
+
98
+ Exponential backoff: 2 ** attempt number / 10 = 100ms, 200ms, ...
99
+ """
100
+ for attempt in range(max_retries):
101
+ logging.info(f"Attempt {attempt + 1} to get order {order_id}")
102
+ order = self.get_order_by_order_id(symbol=symbol, order_id=order_id)
103
+
104
+ if order:
105
+ # We require a minimum set of fields before downstream code can
106
+ # safely update deals:
107
+ # - id (order_id)
108
+ # - price
109
+ # - size (quantity)
110
+ # and the order must no longer be active.
111
+
112
+ is_active = getattr(order, "active", None)
113
+
114
+ if is_active:
115
+ logging.info(
116
+ "KuCoin order %s is still active on attempt %d; waiting for completion",
117
+ order_id,
118
+ attempt + 1,
119
+ )
120
+ else:
121
+ missing: list[str] = []
122
+ if not getattr(order, "id", None):
123
+ missing.append("id")
124
+ if getattr(order, "price", None) is None:
125
+ missing.append("price")
126
+ if getattr(order, "size", None) is None:
127
+ missing.append("size")
128
+
129
+ if not missing:
130
+ return order
131
+
132
+ logging.info(
133
+ "KuCoin order %s inactive but missing required fields %s on attempt %d; retrying",
134
+ order_id,
135
+ ",".join(missing),
136
+ attempt + 1,
137
+ )
138
+
139
+ sleep((2**attempt) / 10)
140
+
141
+ raise TimeoutError(f"Order {order_id} not ready after {max_retries} attempts")
142
+
143
+ def _get_margin_order_with_retry(
144
+ self, symbol: str, order_id: str, max_retries: int = 10
145
+ ) -> GetOrderByOrderIdResp:
146
+ """
147
+ Get margin order by ID with exponential backoff retry.
148
+ KuCoin's order data is not immediately available after placement.
149
+ """
150
+ for attempt in range(max_retries):
151
+ order = self.get_margin_order_by_order_id(symbol=symbol, order_id=order_id)
152
+ if order and order.order_id:
153
+ return order
154
+ # Exponential backoff: 100ms, 200ms, 400ms, 800ms...
155
+ sleep((2**attempt) / 10)
156
+
157
+ raise TimeoutError(
158
+ f"Margin order {order_id} not ready after {max_retries} attempts"
159
+ )
160
+
161
+ def get_part_order_book(self, symbol: str, size: int):
162
+ request = (
163
+ GetPartOrderBookReqBuilder().set_symbol(symbol).set_size(str(size)).build()
164
+ )
165
+ response = self.spot_api.get_part_order_book(request)
166
+ return response
167
+
168
+ def get_full_order_book(self, symbol: str, size: int):
169
+ request = GetFullOrderBookReqBuilder().set_symbol(symbol).build()
170
+ response = self.spot_api.get_full_order_book(request)
171
+ return response
172
+
173
+ def simulate_order(
174
+ self,
175
+ symbol: str,
176
+ side: AddOrderSyncReq.SideEnum,
177
+ order_type: AddOrderSyncReq.TypeEnum = AddOrderSyncReq.TypeEnum.LIMIT,
178
+ qty: float = 1,
179
+ ) -> GetOrderByOrderIdResp:
180
+ """
181
+ Fake synchronous order response shaped similarly to add_order_sync.
182
+ Returns a dict echoing inputs and a computed price when missing.
183
+ """
184
+ book_price = self.matching_engine(
185
+ symbol, order_side=(side == AddOrderSyncReq.SideEnum.SELL), qty=qty
186
+ )
187
+ # fake data
188
+ ts = int(time() * 1000)
189
+ order_id = str(random.randint(1000000000, 9999999999))
190
+
191
+ order = GetOrderByOrderIdResp.model_validate(
192
+ {
193
+ "id": order_id,
194
+ "symbol": symbol,
195
+ "op_type": "DEAL",
196
+ "type": order_type.value,
197
+ "side": side.value.lower(),
198
+ "price": str(book_price),
199
+ "size": str(qty),
200
+ "funds": str(float(book_price) * qty),
201
+ "deal_funds": str(float(book_price) * qty),
202
+ "deal_size": str(qty),
203
+ "fee": "0",
204
+ "fee_currency": symbol.split("-")[1],
205
+ "stp": "CN",
206
+ "stop": "",
207
+ "stop_price": "0",
208
+ "time_in_force": AddOrderSyncReq.TimeInForceEnum.GTC.value,
209
+ "post_only": False,
210
+ "hidden": False,
211
+ "iceberg": False,
212
+ "visible_size": "0",
213
+ "cancel_after": 0,
214
+ "channel": "API",
215
+ "client_oid": "",
216
+ "remark": "",
217
+ "tags": "",
218
+ "is_active": False,
219
+ "cancel_exist": False,
220
+ "created_at": ts,
221
+ }
222
+ )
223
+ return order
224
+
225
+ def simple_matching_engine(self, symbol: str, order_side: bool) -> float:
226
+ """
227
+ Get top of book price for immediate buy/sell
228
+ this is good for paper trading
229
+ or initial price estimates
230
+
231
+ @param: order_side -
232
+ Buy order = get bid prices = False
233
+ Sell order = get ask prices = True
234
+ """
235
+ # Part order book only returns top 1 level at time of writing
236
+ data = self.get_part_order_book(symbol, size=1)
237
+ price = data.bids[0][0] if order_side else data.asks[0][0]
238
+ return price
239
+
240
+ def matching_engine(self, symbol: str, order_side: bool, qty: float = 0) -> float:
241
+ """
242
+ Match quantity with available 100% fill order price,
243
+ so that order can immediately buy/sell
244
+
245
+ Only use this if we need to find optimal price for given qty
246
+
247
+ @param: order_side -
248
+ Buy order = get bid prices = False
249
+ Sell order = get ask prices = True
250
+ """
251
+ # --- Step 1: Get order book ---
252
+ data = self.get_full_order_book(symbol, size=10)
253
+
254
+ # top-of-book price and available qty
255
+ top_price = float(data.asks[0][0]) if order_side else float(data.bids[0][0])
256
+ top_qty = float(data.asks[0][1]) if order_side else float(data.bids[0][1])
257
+
258
+ # --- Step 2: Compute VWAP for last 5 candles ---
259
+ candles = self.get_ui_klines(
260
+ symbol, interval=KucoinKlineIntervals.FIFTEEN_MINUTES.value, limit=10
261
+ )
262
+ total_volume = sum(float(c[5]) for c in candles)
263
+ vwap = (
264
+ sum(float(c[4]) * float(c[5]) for c in candles) / total_volume
265
+ ) # c[4] = close
266
+
267
+ # --- Step 3: Determine safe price based on VWAP and top-of-book ---
268
+ safe_offset = 0.002 # 0.2%
269
+ if order_side: # sell
270
+ # Never above top ask, slightly above VWAP
271
+ safe_price = min(top_price, vwap * (1 + safe_offset))
272
+ else: # buy
273
+ # Never below top bid, slightly below VWAP
274
+ safe_price = max(top_price, vwap * (1 - safe_offset))
275
+
276
+ # --- Step 4: Ensure top-of-book has enough liquidity for qty ---
277
+ # qty here is in quote currency, convert to base qty
278
+ base_qty_needed = float(qty) / safe_price
279
+ if base_qty_needed > top_qty:
280
+ # Not enough at top level; fallback to top-of-book price to guarantee execution
281
+ safe_price = top_price
282
+
283
+ return safe_price
284
+
285
+ def buy_order(
286
+ self,
287
+ symbol: str,
288
+ qty: float,
289
+ order_type: AddOrderSyncReq.TypeEnum = AddOrderSyncReq.TypeEnum.LIMIT,
290
+ ) -> GetOrderByOrderIdResp:
291
+ """
292
+ Wrapper for Kucoin add order for convenience and consistency with other exchanges.
293
+
294
+ Price is not provided so LIMIT orders can be filled immediately using matching engine.
295
+
296
+ Because add_order_sync doesn't return enough info for our orders,
297
+ we need to retrieve the order by order id after placing it.
298
+ And because retrieving it is not immediate, we need to sleep delay
299
+ """
300
+ book_price = self.matching_engine(symbol, order_side=False, qty=qty)
301
+ builder = (
302
+ AddOrderSyncReqBuilder()
303
+ .set_symbol(symbol)
304
+ .set_side(AddOrderSyncReq.SideEnum.BUY)
305
+ .set_type(order_type)
306
+ .set_size(str(qty))
307
+ .set_price(str(book_price))
308
+ )
309
+
310
+ req = builder.build()
311
+ order_response = self.order_api.add_order_sync(req)
312
+ # order_response returns incomplete info, retry with backoff
313
+ order = self._get_order_with_retry(
314
+ symbol=symbol, order_id=order_response.order_id
315
+ )
316
+ logging.error(f"Buy order status active? {order.active}")
317
+ return order
318
+
319
+ def sell_order(
320
+ self,
321
+ symbol: str,
322
+ qty: float,
323
+ order_type: AddOrderSyncReq.TypeEnum = AddOrderSyncReq.TypeEnum.LIMIT,
324
+ ) -> GetOrderByOrderIdResp:
325
+ """
326
+ Wrapper for Kucoin add order for convenience and consistent interface with other exchanges.
327
+
328
+ Price is not provided so LIMIT orders can be filled immediately using matching engine.
329
+
330
+ Because add_order_sync doesn't return enough info for our orders,
331
+ we need to retrieve the order by order id after placing it.
332
+ And because retrieving it is not immediate, we need to sleep delay
333
+ """
334
+ book_price = self.matching_engine(symbol, order_side=True, qty=qty)
335
+ builder = (
336
+ AddOrderSyncReqBuilder()
337
+ .set_symbol(symbol)
338
+ .set_side(AddOrderSyncReq.SideEnum.SELL)
339
+ .set_type(order_type)
340
+ .set_size(str(qty))
341
+ .set_price(str(book_price))
342
+ )
343
+
344
+ req = builder.build()
345
+ order_response = self.order_api.add_order_sync(req)
346
+ # order_response returns incomplete info, retry with backoff
347
+ order = self._get_order_with_retry(
348
+ symbol=symbol, order_id=order_response.order_id
349
+ )
350
+ return order
351
+
352
+ def batch_add_orders_sync(self, orders: list[dict]) -> AddOrderSyncResp:
353
+ """
354
+ Batch place up to 5 limit orders for the same symbol.
355
+ Each dict in `orders` should contain: symbol, side, type, size, price (for limit), optional fields as per SDK.
356
+
357
+ Not usable at the time of writing due to inconsistency with other exchange's interfaces (other exchanges might not support batch orders).
358
+ """
359
+ order_list: list[BatchAddOrdersSyncOrderList] = []
360
+ for o in orders:
361
+ item = BatchAddOrdersSyncOrderList(
362
+ client_oid=o.get("clientOid"),
363
+ symbol=o["symbol"],
364
+ side=(
365
+ BatchAddOrdersSyncOrderList.SideEnum.BUY
366
+ if str(o["side"]).lower() == "buy"
367
+ else BatchAddOrdersSyncOrderList.SideEnum.SELL
368
+ ),
369
+ type=BatchAddOrdersSyncOrderList.TypeEnum.LIMIT,
370
+ size=str(o["size"]),
371
+ price=str(o["price"]) if "price" in o else None,
372
+ time_in_force=BatchAddOrdersSyncOrderList.TimeInForceEnum.GTC,
373
+ )
374
+ order_list.append(item)
375
+
376
+ req = BatchAddOrdersSyncReqBuilder().set_order_list(order_list).build()
377
+ return self.order_api.batch_add_orders_sync(req)
378
+
379
+ def cancel_order_by_order_id_sync(self, symbol: str, order_id: str):
380
+ req = (
381
+ CancelOrderByOrderIdSyncReqBuilder()
382
+ .set_symbol(symbol)
383
+ .set_order_id(order_id)
384
+ .build()
385
+ )
386
+ return self.order_api.cancel_order_by_order_id_sync(req)
387
+
388
+ def get_order_by_order_id(
389
+ self, symbol: str, order_id: str
390
+ ) -> GetOrderByOrderIdResp:
391
+ req = (
392
+ GetOrderByOrderIdReqBuilder()
393
+ .set_symbol(symbol)
394
+ .set_order_id(order_id)
395
+ .build()
396
+ )
397
+ return self.order_api.get_order_by_order_id(req)
398
+
399
+ def get_open_orders(self, symbol: str):
400
+ req = GetOpenOrdersReqBuilder().set_symbol(symbol).build()
401
+ return self.order_api.get_open_orders(req)
402
+
403
+ # --- Margin (Isolated) operations ---
404
+ def buy_margin_order(
405
+ self,
406
+ symbol: str,
407
+ qty: float,
408
+ order_type: AddOrderReq.TypeEnum = AddOrderReq.TypeEnum.LIMIT,
409
+ price: float = 0,
410
+ time_in_force: AddOrderReq.TimeInForceEnum = AddOrderReq.TimeInForceEnum.GTC,
411
+ client_oid: str | None = None,
412
+ auto_borrow: bool = False,
413
+ auto_repay: bool = False,
414
+ ) -> GetOrderByOrderIdResp:
415
+ builder = (
416
+ AddOrderReqBuilder()
417
+ .set_symbol(symbol)
418
+ .set_side(AddOrderReq.SideEnum.BUY)
419
+ .set_type(order_type)
420
+ .set_size(str(qty))
421
+ .set_time_in_force(time_in_force)
422
+ .set_is_isolated(True)
423
+ )
424
+ if client_oid:
425
+ builder = builder.set_client_oid(client_oid)
426
+ if order_type == AddOrderReq.TypeEnum.LIMIT and price > 0:
427
+ builder = builder.set_price(str(price))
428
+ if auto_borrow:
429
+ builder = builder.set_auto_borrow(True)
430
+ if auto_repay:
431
+ builder = builder.set_auto_repay(True)
432
+
433
+ req = builder.build()
434
+ order_response = self.margin_order_api.add_order(req)
435
+ # order_response returns incomplete info, retry with backoff
436
+ order = self._get_margin_order_with_retry(
437
+ symbol=symbol, order_id=order_response.order_id
438
+ )
439
+ return order
440
+
441
+ def sell_margin_order(
442
+ self,
443
+ symbol: str,
444
+ qty: float,
445
+ order_type: AddOrderReq.TypeEnum = AddOrderReq.TypeEnum.LIMIT,
446
+ price: float = 0,
447
+ time_in_force: AddOrderReq.TimeInForceEnum = AddOrderReq.TimeInForceEnum.GTC,
448
+ client_oid: str | None = None,
449
+ auto_borrow: bool = False,
450
+ auto_repay: bool = False,
451
+ ) -> GetOrderByOrderIdResp:
452
+ builder = (
453
+ AddOrderReqBuilder()
454
+ .set_symbol(symbol)
455
+ .set_side(AddOrderReq.SideEnum.SELL)
456
+ .set_type(order_type)
457
+ .set_size(str(qty))
458
+ .set_time_in_force(time_in_force)
459
+ .set_is_isolated(True)
460
+ )
461
+ if client_oid:
462
+ builder = builder.set_client_oid(client_oid)
463
+ if order_type == AddOrderReq.TypeEnum.LIMIT and price > 0:
464
+ builder = builder.set_price(str(price))
465
+ if auto_borrow:
466
+ builder = builder.set_auto_borrow(True)
467
+ if auto_repay:
468
+ builder = builder.set_auto_repay(True)
469
+
470
+ req = builder.build()
471
+ order_response = self.margin_order_api.add_order(req)
472
+ # order_response returns incomplete info, retry with backoff
473
+ order = self._get_margin_order_with_retry(
474
+ symbol=symbol, order_id=order_response.order_id
475
+ )
476
+ return order
477
+
478
+ def cancel_margin_order_by_order_id(self, symbol: str, order_id: str):
479
+ # Margin API uses cancel by order id req builder from margin.order
480
+ req_cancel = (
481
+ CancelOrderByOrderIdReqBuilder()
482
+ .set_symbol(symbol)
483
+ .set_order_id(order_id)
484
+ .build()
485
+ )
486
+ return self.margin_order_api.cancel_order_by_order_id(req_cancel)
487
+
488
+ def get_margin_order_by_order_id(
489
+ self, symbol: str, order_id: str
490
+ ) -> GetOrderByOrderIdResp:
491
+ req = (
492
+ GetOrderByOrderIdReqBuilder()
493
+ .set_symbol(symbol)
494
+ .set_order_id(order_id)
495
+ .build()
496
+ )
497
+ return self.margin_order_api.get_order_by_order_id(req)
498
+
499
+ def get_margin_open_orders(self, symbol: str):
500
+ req = GetOpenOrdersReqBuilder().set_symbol(symbol).build()
501
+ return self.margin_order_api.get_open_orders(req)
502
+
503
+ def simulate_margin_order(
504
+ self,
505
+ symbol: str,
506
+ side: AddOrderReq.SideEnum,
507
+ order_type: AddOrderReq.TypeEnum = AddOrderReq.TypeEnum.LIMIT,
508
+ qty: float = 1,
509
+ ) -> GetOrderByOrderIdResp:
510
+ """
511
+ Fake isolated margin order response echoing inputs.
512
+ """
513
+ book_price = self.matching_engine(
514
+ symbol, order_side=(side == AddOrderReq.SideEnum.SELL), qty=qty
515
+ )
516
+ ts = int(time() * 1000)
517
+ order_id = str(random.randint(1000000000, 9999999999))
518
+ order = GetOrderByOrderIdResp.model_validate(
519
+ {
520
+ "id": order_id,
521
+ "symbol": symbol,
522
+ "op_type": "DEAL",
523
+ "type": order_type.value,
524
+ "side": side.value.lower(),
525
+ "price": str(book_price),
526
+ "size": str(qty),
527
+ "funds": str(float(book_price) * qty),
528
+ "deal_funds": str(float(book_price) * qty),
529
+ "deal_size": str(qty),
530
+ "fee": "0",
531
+ "fee_currency": symbol.split("-")[1],
532
+ "stp": "CN",
533
+ "stop": "",
534
+ "stop_price": "0",
535
+ "time_in_force": AddOrderSyncReq.TimeInForceEnum.GTC.value,
536
+ "post_only": False,
537
+ "hidden": False,
538
+ "iceberg": False,
539
+ "visible_size": "0",
540
+ "cancel_after": 0,
541
+ "channel": "API",
542
+ "client_oid": "",
543
+ "remark": "",
544
+ "tags": "",
545
+ "is_active": False,
546
+ "cancel_exist": False,
547
+ "created_at": ts,
548
+ }
549
+ )
550
+ return order
551
+
552
+ def repay_margin_loan(
553
+ self,
554
+ asset: str,
555
+ symbol: str,
556
+ amount: float,
557
+ ) -> RepayResp:
558
+ req = (
559
+ RepayReqBuilder()
560
+ .set_currency(asset)
561
+ .set_symbol(symbol)
562
+ .set_size(str(amount))
563
+ .set_is_isolated(True)
564
+ .build()
565
+ )
566
+ return self.debit_api.repay(req)
567
+
568
+ def transfer_isolated_margin_to_spot(
569
+ self, asset: str, symbol: str, amount: float
570
+ ) -> FlexTransferResp:
571
+ """
572
+ Transfer funds from isolated margin to spot (main) account.
573
+ `symbol` is the isolated pair like "BTC-USDT".
574
+ """
575
+ client_oid = str(uuid.uuid4())
576
+ req = (
577
+ FlexTransferReqBuilder()
578
+ .set_client_oid(client_oid)
579
+ .set_currency(asset)
580
+ .set_amount(str(amount))
581
+ .set_type(FlexTransferReq.TypeEnum.INTERNAL)
582
+ .set_from_account_type(FlexTransferReq.FromAccountTypeEnum.ISOLATED)
583
+ .set_from_account_tag(symbol)
584
+ .set_to_account_type(FlexTransferReq.ToAccountTypeEnum.MAIN)
585
+ .build()
586
+ )
587
+ return self.transfer_api.flex_transfer(req)
588
+
589
+ def transfer_spot_to_isolated_margin(
590
+ self, asset: str, symbol: str, amount: float
591
+ ) -> FlexTransferResp:
592
+ """
593
+ Transfer funds from spot (main) account to isolated margin account.
594
+ `symbol` must be the isolated pair like "BTC-USDT".
595
+ """
596
+ client_oid = str(uuid.uuid4())
597
+ req = (
598
+ FlexTransferReqBuilder()
599
+ .set_client_oid(client_oid)
600
+ .set_currency(asset)
601
+ .set_amount(str(amount))
602
+ .set_type(FlexTransferReq.TypeEnum.INTERNAL)
603
+ .set_from_account_type(FlexTransferReq.FromAccountTypeEnum.MAIN)
604
+ .set_to_account_type(FlexTransferReq.ToAccountTypeEnum.ISOLATED)
605
+ .set_to_account_tag(symbol)
606
+ .build()
607
+ )
608
+ return self.transfer_api.flex_transfer(req)
609
+
610
+ def transfer_main_to_trade(self, asset: str, amount: float) -> FlexTransferResp:
611
+ """
612
+ Transfer funds from main to trade (spot) account.
613
+ """
614
+ client_oid = str(uuid.uuid4())
615
+ req = (
616
+ FlexTransferReqBuilder()
617
+ .set_client_oid(client_oid)
618
+ .set_currency(asset)
619
+ .set_amount(str(amount))
620
+ .set_type(FlexTransferReq.TypeEnum.INTERNAL)
621
+ .set_from_account_type(FlexTransferReq.FromAccountTypeEnum.MAIN)
622
+ .set_to_account_type(FlexTransferReq.ToAccountTypeEnum.TRADE)
623
+ .build()
624
+ )
625
+ return self.transfer_api.flex_transfer(req)
626
+
627
+ def transfer_trade_to_main(self, asset: str, amount: float) -> FlexTransferResp:
628
+ """
629
+ Transfer funds from trade (spot) account to main.
630
+ """
631
+ client_oid = str(uuid.uuid4())
632
+ req = (
633
+ FlexTransferReqBuilder()
634
+ .set_client_oid(client_oid)
635
+ .set_currency(asset)
636
+ .set_amount(str(amount))
637
+ .set_type(FlexTransferReq.TypeEnum.INTERNAL)
638
+ .set_from_account_type(FlexTransferReq.FromAccountTypeEnum.TRADE)
639
+ .set_to_account_type(FlexTransferReq.ToAccountTypeEnum.MAIN)
640
+ .build()
641
+ )
642
+ return self.transfer_api.flex_transfer(req)
643
+
644
+ def create_margin_loan(
645
+ self,
646
+ asset: str,
647
+ symbol: str,
648
+ amount: float,
649
+ is_isolated: bool = True,
650
+ ) -> BorrowResp:
651
+ """
652
+ Create a margin loan (borrow) on KuCoin.
653
+ For isolated margin, pass the trading pair in `symbol` (e.g., "BTC-USDT") and set `is_isolated=True`.
654
+ """
655
+ req = (
656
+ BorrowReqBuilder()
657
+ .set_currency(asset)
658
+ .set_symbol(symbol)
659
+ .set_size(str(amount))
660
+ .set_is_isolated(is_isolated)
661
+ .build()
662
+ )
663
+ return self.debit_api.borrow(req)
@@ -0,0 +1,33 @@
1
+ from kucoin_universal_sdk.api import DefaultClient
2
+ from kucoin_universal_sdk.model import TransportOptionBuilder
3
+ from kucoin_universal_sdk.model import ClientOptionBuilder
4
+ from kucoin_universal_sdk.model import (
5
+ GLOBAL_API_ENDPOINT,
6
+ )
7
+
8
+
9
+ class KucoinRest:
10
+ def __init__(self, key: str, secret: str, passphrase: str):
11
+ self.key = key
12
+ self.secret = secret
13
+ self.passphrase = passphrase
14
+
15
+ def setup_client(self) -> DefaultClient:
16
+ http_transport_option = (
17
+ TransportOptionBuilder()
18
+ .set_keep_alive(True)
19
+ .set_max_pool_size(10)
20
+ .set_max_connection_per_pool(10)
21
+ .build()
22
+ )
23
+ client_option = (
24
+ ClientOptionBuilder()
25
+ .set_key(self.key)
26
+ .set_secret(self.secret)
27
+ .set_passphrase(self.passphrase)
28
+ .set_spot_endpoint(GLOBAL_API_ENDPOINT)
29
+ .set_transport_option(http_transport_option)
30
+ .build()
31
+ )
32
+ self.client = DefaultClient(client_option)
33
+ return self.client
File without changes