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
|
@@ -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
|