Skip to content

Commit 5f60ce1

Browse files
committed
Implement Bybit reduce_only and batch cancel
1 parent 882ce3c commit 5f60ce1

File tree

5 files changed

+96
-59
lines changed

5 files changed

+96
-59
lines changed

nautilus_trader/adapters/bybit/endpoints/trade/place_order.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class BybitPlaceOrderPostParams(msgspec.Struct, omit_defaults=True, frozen=True)
4040
triggerBy: BybitTriggerType | None = None
4141
timeInForce: BybitTimeInForce | None = None
4242
orderLinkId: str | None = None
43+
reduceOnly: bool | None = None
4344

4445

4546
class BybitPlaceOrderEndpoint(BybitHttpEndpoint):

nautilus_trader/adapters/bybit/execution.py

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -532,53 +532,58 @@ async def _cancel_order(self, command: CancelOrder) -> None:
532532

533533
async def _cancel_all_orders(self, command: CancelAllOrders) -> None:
534534
bybit_symbol = BybitSymbol(command.instrument_id.symbol.value)
535-
await self._http_account.cancel_all_orders(
536-
bybit_symbol.product_type,
537-
bybit_symbol.raw_symbol,
535+
536+
if bybit_symbol.product_type == BybitProductType.INVERSE:
537+
# Batch cancel not implemented for INVERSE
538+
self._log.warning(
539+
f"Batch cancel not implemented for INVERSE, "
540+
f"canceling all for symbol {command.instrument_id.symbol.value}",
541+
)
542+
await self._http_account.cancel_all_orders(
543+
bybit_symbol.product_type,
544+
bybit_symbol.raw_symbol,
545+
)
546+
return
547+
548+
open_orders_strategy: list[Order] = self._cache.orders_open(
549+
instrument_id=command.instrument_id,
550+
strategy_id=command.strategy_id,
538551
)
539552

540-
# TODO: Determine signing issue for batch requests
541-
# async def _cancel_all_orders(self, command: CancelAllOrders) -> None:
542-
# open_orders_strategy: list[Order] = self._cache.orders_open(
543-
# instrument_id=command.instrument_id,
544-
# strategy_id=command.strategy_id,
545-
# )
546-
#
547-
# bybit_symbol = BybitSymbol(command.instrument_id.symbol.value)
548-
#
549-
# # Check total orders for instrument
550-
# open_orders_total_count = self._cache.orders_open_count(
551-
# instrument_id=command.instrument_id,
552-
# )
553-
# if open_orders_total_count > 10:
554-
# # This could be reimplemented later to group requests into batches of 10
555-
# self._log.warning(
556-
# f"Total {command.instrument_id.symbol.value} orders open exceeds 10, "
557-
# f"is {open_orders_total_count}: canceling all for symbol",
558-
# )
559-
# await self._http_account.cancel_all_orders(
560-
# bybit_symbol.product_type,
561-
# bybit_symbol.raw_symbol,
562-
# )
563-
# return
564-
#
565-
# cancel_batch: list[Order] = []
566-
# for order in open_orders_strategy:
567-
# cancel_batch.append(order)
568-
#
569-
# await self._http_account.batch_cancel_orders(
570-
# product_type=bybit_symbol.product_type,
571-
# symbol=bybit_symbol.raw_symbol,
572-
# orders=cancel_batch,
573-
# )
553+
# Check total orders for instrument
554+
open_orders_total_count = self._cache.orders_open_count(
555+
instrument_id=command.instrument_id,
556+
)
557+
if open_orders_total_count > 10:
558+
# This could be reimplemented later to group requests into batches of 10
559+
self._log.warning(
560+
f"Total {command.instrument_id.symbol.value} orders open exceeds 10, "
561+
f"is {open_orders_total_count}: canceling all for symbol",
562+
)
563+
await self._http_account.cancel_all_orders(
564+
bybit_symbol.product_type,
565+
bybit_symbol.raw_symbol,
566+
)
567+
return
568+
569+
cancel_batch: list[Order] = []
570+
for order in open_orders_strategy:
571+
cancel_batch.append(order)
572+
573+
await self._http_account.batch_cancel_orders(
574+
product_type=bybit_symbol.product_type,
575+
symbol=bybit_symbol.raw_symbol,
576+
orders=cancel_batch,
577+
)
574578

575579
async def _submit_order(self, command: SubmitOrder) -> None:
576580
order = command.order
577581
if order.is_closed:
578582
self._log.warning(f"Order {order} is already closed")
579583
return
580584

581-
if not self._check_order_validity(order):
585+
bybit_symbol = BybitSymbol(command.instrument_id.symbol.value)
586+
if not self._check_order_validity(order, bybit_symbol.product_type):
582587
return
583588

584589
self._log.debug(f"Submitting order {order}")
@@ -600,20 +605,28 @@ async def _submit_order(self, command: SubmitOrder) -> None:
600605
except BybitError as e:
601606
self._log.error(repr(e))
602607

603-
def _check_order_validity(self, order: Order) -> bool:
608+
def _check_order_validity(self, order: Order, product_type: BybitProductType) -> bool:
604609
# Check order type valid
605610
if order.order_type not in self._enum_parser.valid_order_types:
606611
self._log.error(
607612
f"Cannot submit {order} has invalid order type {order.order_type}, unsupported on Bybit",
608613
)
609614
return False
615+
610616
# Check post only
611617
if order.is_post_only and order.order_type != OrderType.LIMIT:
612618
self._log.error(
613619
f"Cannot submit {order} has invalid post only {order.is_post_only}, unsupported on Bybit",
614620
)
615621
return False
616622

623+
# Check reduce only
624+
if order.is_reduce_only and product_type == BybitProductType.SPOT:
625+
self._log.error(
626+
f"Cannot submit {order} is reduce_only, unsupported on Bybit SPOT",
627+
)
628+
return False
629+
617630
return True
618631

619632
async def _submit_market_order(self, order: MarketOrder) -> None:
@@ -630,6 +643,7 @@ async def _submit_market_order(self, order: MarketOrder) -> None:
630643
quote_quantity=order.is_quote_quantity,
631644
time_in_force=time_in_force,
632645
client_order_id=str(order.client_order_id),
646+
reduce_only=order.is_reduce_only if order.is_reduce_only else None,
633647
)
634648

635649
async def _submit_limit_order(self, order: LimitOrder) -> None:
@@ -647,6 +661,7 @@ async def _submit_limit_order(self, order: LimitOrder) -> None:
647661
price=str(order.price),
648662
time_in_force=time_in_force,
649663
client_order_id=str(order.client_order_id),
664+
reduce_only=order.is_reduce_only if order.is_reduce_only else None,
650665
)
651666

652667
def _handle_ws_message(self, raw: bytes) -> None:

nautilus_trader/adapters/bybit/http/account.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ async def place_order(
219219
price: str | None = None,
220220
time_in_force: BybitTimeInForce | None = None,
221221
client_order_id: str | None = None,
222+
reduce_only: bool | None = None,
222223
) -> BybitPlaceOrderResponse:
223224
market_unit = "baseCoin" if not quote_quantity else "quoteCoin"
224225
result = await self._endpoint_place_order.post(
@@ -232,6 +233,7 @@ async def place_order(
232233
price=price,
233234
timeInForce=time_in_force,
234235
orderLinkId=client_order_id,
236+
reduceOnly=reduce_only,
235237
),
236238
)
237239
return result
@@ -313,4 +315,4 @@ async def batch_cancel_orders(
313315
request=request,
314316
),
315317
)
316-
return response.result
318+
return response.result.list

nautilus_trader/adapters/bybit/http/client.py

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,6 @@
3131
from nautilus_trader.core.nautilus_pyo3 import Quota
3232

3333

34-
def create_string_from_dict(data):
35-
property_strings = []
36-
37-
for key, value in data.items():
38-
property_string = f'"{key}":"{value}"'
39-
property_strings.append(property_string)
40-
41-
result_string = "{" + ",".join(property_strings) + "}"
42-
return result_string
43-
44-
4534
class ResponseCode(msgspec.Struct):
4635
retCode: int
4736

@@ -80,7 +69,7 @@ def __init__(
8069
self._log: Logger = Logger(name=type(self).__name__)
8170
self._api_key: str = api_key
8271
self._api_secret: str = api_secret
83-
self._recv_window: int = 8000
72+
self._recv_window: int = 5000
8473

8574
self._base_url: str = base_url
8675
self._headers: dict[str, Any] = {
@@ -178,7 +167,7 @@ async def sign_request(
178167

179168
def _sign_post_request(self, payload: dict[str, Any]) -> list[str]:
180169
timestamp = str(self._clock.timestamp_ms())
181-
payload_str = create_string_from_dict(payload)
170+
payload_str = msgspec.json.encode(payload).decode()
182171
result = timestamp + self._api_key + str(self._recv_window) + payload_str
183172
signature = hmac.new(
184173
self._api_secret.encode(),

nautilus_trader/adapters/bybit/schemas/order.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,15 @@ class BybitAmendOrderResponse(msgspec.Struct):
216216
################################################################################
217217

218218

219+
class BybitPlaceResult(msgspec.Struct):
220+
code: int # Success/error code
221+
msg: str # Success/error message
222+
223+
224+
class BybitBatchPlaceOrderExtInfo(msgspec.Struct):
225+
list: list[BybitPlaceResult]
226+
227+
219228
class BybitBatchPlaceOrder(msgspec.Struct):
220229
category: BybitProductType
221230
symbol: str
@@ -224,10 +233,15 @@ class BybitBatchPlaceOrder(msgspec.Struct):
224233
createAt: str
225234

226235

236+
class BybitBatchPlaceOrderResult(msgspec.Struct):
237+
list: list[BybitBatchPlaceOrder]
238+
239+
227240
class BybitBatchPlaceOrderResponse(msgspec.Struct):
228241
retCode: int
229242
retMsg: str
230-
result: list[BybitBatchPlaceOrder]
243+
result: BybitBatchPlaceOrderResult
244+
retExtInfo: BybitBatchPlaceOrderExtInfo
231245
time: int
232246

233247

@@ -241,18 +255,26 @@ class BybitCancelResult(msgspec.Struct):
241255
msg: str # Success/error message
242256

243257

258+
class BybitBatchCancelOrderExtInfo(msgspec.Struct):
259+
list: list[BybitCancelResult]
260+
261+
244262
class BybitBatchCancelOrder(msgspec.Struct):
245263
category: BybitProductType
246264
symbol: str
247265
orderId: str
248266
orderLinkId: str
249267

250268

269+
class BybitBatchCancelOrderResult(msgspec.Struct):
270+
list: list[BybitBatchCancelOrder]
271+
272+
251273
class BybitBatchCancelOrderResponse(msgspec.Struct):
252274
retCode: int
253275
retMsg: str
254-
result: list[BybitBatchCancelOrder]
255-
retExtInfo: list[BybitCancelResult]
276+
result: BybitBatchCancelOrderResult
277+
retExtInfo: BybitBatchCancelOrderExtInfo
256278
time: int
257279

258280

@@ -266,16 +288,24 @@ class BybitAmendResult(msgspec.Struct):
266288
msg: str # Success/error message
267289

268290

269-
class BybitBatchAmend(msgspec.Struct):
291+
class BybitBatchAmendOrderExtInfo(msgspec.Struct):
292+
list: list[BybitAmendResult]
293+
294+
295+
class BybitBatchAmendOrder(msgspec.Struct):
270296
category: BybitProductType
271297
symbol: str
272298
orderId: str
273299
orderLinkId: str
274300

275301

302+
class BybitBatchAmendOrderResult(msgspec.Struct):
303+
list: list[BybitBatchAmendOrder]
304+
305+
276306
class BybitBatchAmendOrderResponse(msgspec.Struct):
277307
retCode: int
278308
retMsg: str
279-
result: list[BybitBatchAmend]
280-
retExtInfo: list[BybitAmendResult]
309+
result: BybitBatchAmendOrderResult
310+
retExtInfo: BybitBatchAmendOrderExtInfo
281311
time: int

0 commit comments

Comments
 (0)