Skip to content

Commit 0bbe8fa

Browse files
authored
Add WebSocket batch order operations for Bybit (#2521)
1 parent 46429b8 commit 0bbe8fa

File tree

12 files changed

+253
-134
lines changed

12 files changed

+253
-134
lines changed

RELEASES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ None
1010

1111
### Internal Improvements
1212
- Implemented exponential backoff and jitter for the `RetryManager` (#2518), thanks @davidsblom
13-
- Improved reconnection robustness for Bybit private/trading channels (#2520), thanks @davidsblom
13+
- Improved reconnection robustness for Bybit private/trading channels (#2520), thanks @sunlei
1414
- Fixed some clippy lints (#2517), thanks @twitu
1515
- Upgraded `databento` crate to v0.23.0
1616
- Upgraded `sqlx` crate to v0.8.5

nautilus_trader/adapters/bybit/common/enums.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ class BybitWsOrderRequestMsgOP(Enum):
137137
CREATE = "order.create"
138138
AMEND = "order.amend"
139139
CANCEL = "order.cancel"
140+
CREATE_BATCH = "order.create-batch"
141+
AMEND_BATCH = "order.amend-batch"
142+
CANCEL_BATCH = "order.cancel-batch"
140143

141144

142145
@unique

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

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121

2222
from nautilus_trader.adapters.bybit.common.enums import BybitEndpointType
2323
from nautilus_trader.adapters.bybit.common.enums import BybitProductType
24-
from nautilus_trader.adapters.bybit.common.enums import BybitTriggerType
2524
from nautilus_trader.adapters.bybit.endpoints.endpoint import BybitHttpEndpoint
25+
from nautilus_trader.adapters.bybit.endpoints.trade.batch_amend_order import BybitBatchAmendOrder
2626
from nautilus_trader.adapters.bybit.schemas.order import BybitAmendOrderResponse
2727
from nautilus_trader.core.nautilus_pyo3 import HttpMethod
2828

@@ -31,23 +31,13 @@
3131
from nautilus_trader.adapters.bybit.http.client import BybitHttpClient
3232

3333

34-
class BybitAmendOrderPostParams(msgspec.Struct, omit_defaults=True, frozen=True):
34+
class BybitAmendOrderPostParams(
35+
BybitBatchAmendOrder,
36+
omit_defaults=True,
37+
frozen=True,
38+
kw_only=True,
39+
):
3540
category: BybitProductType
36-
symbol: str
37-
orderId: str | None = None
38-
orderLinkId: str | None = None
39-
orderIv: str | None = None
40-
triggerPrice: str | None = None
41-
qty: str | None = None
42-
price: str | None = None
43-
tpslMode: str | None = None
44-
takeProfit: str | None = None
45-
stopLoss: str | None = None
46-
tpTriggerBy: str | None = None
47-
slTriggerBy: str | None = None
48-
triggerBy: BybitTriggerType | None = None
49-
tpLimitPrice: str | None = None
50-
slLimitPrice: str | None = None
5141

5242

5343
class BybitAmendOrderEndpoint(BybitHttpEndpoint):

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from nautilus_trader.adapters.bybit.common.enums import BybitEndpointType
2323
from nautilus_trader.adapters.bybit.common.enums import BybitProductType
24+
from nautilus_trader.adapters.bybit.common.enums import BybitTriggerType
2425
from nautilus_trader.adapters.bybit.endpoints.endpoint import BybitHttpEndpoint
2526
from nautilus_trader.adapters.bybit.schemas.order import BybitBatchAmendOrderResponse
2627
from nautilus_trader.core.nautilus_pyo3 import HttpMethod
@@ -43,6 +44,7 @@ class BybitBatchAmendOrder(msgspec.Struct, omit_defaults=True, frozen=True):
4344
stopLoss: str | None = None
4445
tpTriggerBy: str | None = None
4546
slTriggerBy: str | None = None
47+
triggerBy: BybitTriggerType | None = None
4648
tpLimitPrice: str | None = None
4749
slLimitPrice: str | None = None
4850

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class BybitBatchCancelOrder(msgspec.Struct, omit_defaults=True, frozen=True):
3434
symbol: str
3535
orderId: str | None = None
3636
orderLinkId: str | None = None
37+
orderFilter: str | None = None # Spot only
3738

3839

3940
class BybitBatchCancelOrderPostParams(msgspec.Struct, omit_defaults=True, frozen=True):

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from nautilus_trader.adapters.bybit.common.enums import BybitEndpointType
2323
from nautilus_trader.adapters.bybit.common.enums import BybitProductType
2424
from nautilus_trader.adapters.bybit.endpoints.endpoint import BybitHttpEndpoint
25+
from nautilus_trader.adapters.bybit.endpoints.trade.batch_cancel_order import BybitBatchCancelOrder
2526
from nautilus_trader.adapters.bybit.schemas.order import BybitCancelOrderResponse
2627
from nautilus_trader.core.nautilus_pyo3 import HttpMethod
2728

@@ -30,12 +31,13 @@
3031
from nautilus_trader.adapters.bybit.http.client import BybitHttpClient
3132

3233

33-
class BybitCancelOrderPostParams(msgspec.Struct, omit_defaults=True, frozen=True):
34+
class BybitCancelOrderPostParams(
35+
BybitBatchCancelOrder,
36+
omit_defaults=True,
37+
frozen=True,
38+
kw_only=True,
39+
):
3440
category: BybitProductType
35-
symbol: str
36-
orderId: str | None = None
37-
orderLinkId: str | None = None
38-
orderFilter: str | None = None # Spot only
3941

4042

4143
class BybitCancelOrderEndpoint(BybitHttpEndpoint):

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class BybitPlaceOrderPostParams(
3838
kw_only=True,
3939
):
4040
category: BybitProductType
41+
slippageToleranceType: str | None = None
42+
slippageTolerance: str | None = None
4143

4244

4345
class BybitPlaceOrderEndpoint(BybitHttpEndpoint):

nautilus_trader/adapters/bybit/execution.py

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ async def _cancel_order(self, command: CancelOrder) -> None:
685685

686686
retry_manager = await self._retry_manager_pool.acquire()
687687
try:
688-
await retry_manager.run(
688+
response = await retry_manager.run(
689689
"cancel_order",
690690
[client_order_id, venue_order_id],
691691
self._order_single_client.cancel_order,
@@ -703,10 +703,35 @@ async def _cancel_order(self, command: CancelOrder) -> None:
703703
retry_manager.message,
704704
self._clock.timestamp_ns(),
705705
)
706+
707+
if response:
708+
ret_code = response.retCode
709+
if ret_code != 0:
710+
if ret_code == 110001: # order not exists or too late to cancel
711+
if not order.is_closed:
712+
self.generate_order_canceled(
713+
strategy_id=order.strategy_id,
714+
instrument_id=order.instrument_id,
715+
client_order_id=order.client_order_id,
716+
venue_order_id=order.venue_order_id,
717+
ts_event=self._clock.timestamp_ns(),
718+
)
719+
else:
720+
self.generate_order_cancel_rejected(
721+
strategy_id=order.strategy_id,
722+
instrument_id=order.instrument_id,
723+
client_order_id=order.client_order_id,
724+
venue_order_id=order.venue_order_id,
725+
reason=response.retMsg,
726+
ts_event=self._clock.timestamp_ns(),
727+
)
706728
finally:
707729
await self._retry_manager_pool.release(retry_manager)
708730

709-
async def _batch_cancel_orders(self, command: BatchCancelOrders) -> None:
731+
async def _batch_cancel_orders( # noqa: C901 (too complex)
732+
self,
733+
command: BatchCancelOrders,
734+
) -> None:
710735
# https://bybit-exchange.github.io/docs/v5/order/batch-cancel
711736

712737
bybit_symbol = BybitSymbol(command.instrument_id.symbol.value)
@@ -742,7 +767,7 @@ async def _batch_cancel_orders(self, command: BatchCancelOrders) -> None:
742767

743768
retry_manager = await self._retry_manager_pool.acquire()
744769
try:
745-
await retry_manager.run(
770+
response = await retry_manager.run(
746771
"batch_cancel_orders",
747772
None,
748773
self._order_batch_client.batch_cancel_orders,
@@ -755,13 +780,40 @@ async def _batch_cancel_orders(self, command: BatchCancelOrders) -> None:
755780
if order is None or order.is_closed:
756781
continue
757782
self.generate_order_cancel_rejected(
758-
order.strategy_id,
759-
order.instrument_id,
760-
order.client_order_id,
761-
order.venue_order_id,
762-
retry_manager.message,
763-
self._clock.timestamp_ns(),
783+
strategy_id=order.strategy_id,
784+
instrument_id=order.instrument_id,
785+
client_order_id=order.client_order_id,
786+
venue_order_id=order.venue_order_id,
787+
reason=retry_manager.message,
788+
ts_event=self._clock.timestamp_ns(),
764789
)
790+
791+
if response:
792+
for idx, ret_info in enumerate(response.retExtInfo.list):
793+
ret_code = ret_info.code
794+
if ret_code != 0:
795+
order = self._cache.order(
796+
ClientOrderId(response.data.list[idx].orderLinkId),
797+
)
798+
if order is None or order.is_closed:
799+
continue
800+
if ret_code == 110001: # order not exists or too late to cancel
801+
self.generate_order_canceled(
802+
strategy_id=order.strategy_id,
803+
instrument_id=order.instrument_id,
804+
client_order_id=order.client_order_id,
805+
venue_order_id=order.venue_order_id,
806+
ts_event=self._clock.timestamp_ns(),
807+
)
808+
else:
809+
self.generate_order_cancel_rejected(
810+
strategy_id=order.strategy_id,
811+
instrument_id=order.instrument_id,
812+
client_order_id=order.client_order_id,
813+
venue_order_id=order.venue_order_id,
814+
reason=ret_info.msg,
815+
ts_event=self._clock.timestamp_ns(),
816+
)
765817
finally:
766818
await self._retry_manager_pool.release(retry_manager)
767819

@@ -876,7 +928,10 @@ async def _submit_order(self, command: SubmitOrder) -> None:
876928
finally:
877929
await self._retry_manager_pool.release(retry_manager)
878930

879-
async def _submit_order_list(self, command: SubmitOrderList) -> None:
931+
async def _submit_order_list( # noqa: C901 (too complex)
932+
self,
933+
command: SubmitOrderList,
934+
) -> None:
880935
bybit_symbol = BybitSymbol(command.instrument_id.symbol.value)
881936
product_type = bybit_symbol.product_type
882937
command_orders = command.order_list.orders
@@ -912,13 +967,39 @@ async def _submit_order_list(self, command: SubmitOrderList) -> None:
912967

913968
retry_manager = await self._retry_manager_pool.acquire()
914969
try:
915-
await retry_manager.run(
970+
response = await retry_manager.run(
916971
"submit_order_list",
917972
None,
918973
self._order_batch_client.batch_place_orders,
919974
product_type=product_type,
920975
submit_orders=submit_orders,
921976
)
977+
978+
if not retry_manager.result:
979+
for order in batch_submits:
980+
self.generate_order_rejected(
981+
strategy_id=order.strategy_id,
982+
instrument_id=order.instrument_id,
983+
client_order_id=order.client_order_id,
984+
reason=retry_manager.message,
985+
ts_event=self._clock.timestamp_ns(),
986+
)
987+
988+
if response:
989+
for idx, ret_info in enumerate(response.retExtInfo.list):
990+
if ret_info.code != 0:
991+
order = self._cache.order(
992+
ClientOrderId(response.data.list[idx].orderLinkId),
993+
)
994+
if order is None:
995+
continue
996+
self.generate_order_rejected(
997+
strategy_id=order.strategy_id,
998+
instrument_id=order.instrument_id,
999+
client_order_id=order.client_order_id,
1000+
reason=ret_info.msg,
1001+
ts_event=self._clock.timestamp_ns(),
1002+
)
9221003
finally:
9231004
await self._retry_manager_pool.release(retry_manager)
9241005

@@ -1148,8 +1229,7 @@ def _process_execution(
11481229

11491230
if client_order_id is None:
11501231
self._log.debug(
1151-
f"Cannot process order execution for {venue_order_id!r}: "
1152-
"no `ClientOrderId` found (most likely due to being an external order)",
1232+
f"Cannot process order execution for {venue_order_id!r}: no `ClientOrderId` found (most likely due to being an external order)",
11531233
)
11541234
return
11551235

@@ -1172,8 +1252,7 @@ def _process_execution(
11721252
)
11731253
if strategy_id is None:
11741254
self._log.warning(
1175-
f"Cannot process order execution for {client_order_id!r}: "
1176-
"no strategy ID found (most likely due to being an external order)",
1255+
f"Cannot process order execution for {client_order_id!r}: no strategy ID found (most likely due to being an external order)",
11771256
)
11781257
return
11791258
else:

nautilus_trader/adapters/bybit/http/account.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
from nautilus_trader.adapters.bybit.endpoints.trade.trade_history import BybitTradeHistoryEndpoint
6565
from nautilus_trader.adapters.bybit.endpoints.trade.trade_history import BybitTradeHistoryGetParams
6666
from nautilus_trader.adapters.bybit.http.client import BybitHttpClient
67+
from nautilus_trader.adapters.bybit.schemas.order import BybitBatchCancelOrderResponse
68+
from nautilus_trader.adapters.bybit.schemas.order import BybitBatchPlaceOrderResponse
6769
from nautilus_trader.common.component import LiveClock
6870
from nautilus_trader.core.correctness import PyCondition
6971

@@ -79,7 +81,7 @@
7981
from nautilus_trader.adapters.bybit.schemas.account.set_margin_mode import BybitSetMarginModeResponse
8082
from nautilus_trader.adapters.bybit.schemas.account.switch_mode import BybitSwitchModeResponse
8183
from nautilus_trader.adapters.bybit.schemas.order import BybitAmendOrder
82-
from nautilus_trader.adapters.bybit.schemas.order import BybitCancelOrder
84+
from nautilus_trader.adapters.bybit.schemas.order import BybitCancelOrderResponse
8385
from nautilus_trader.adapters.bybit.schemas.order import BybitOrder
8486
from nautilus_trader.adapters.bybit.schemas.order import BybitPlaceOrderResponse
8587
from nautilus_trader.adapters.bybit.schemas.order import BybitSetTradingStopResponse
@@ -409,7 +411,7 @@ async def cancel_order(
409411
client_order_id: str | None = None,
410412
venue_order_id: str | None = None,
411413
order_filter: str | None = None,
412-
) -> BybitCancelOrder:
414+
) -> BybitCancelOrderResponse:
413415
response = await self._endpoint_cancel_order.post(
414416
BybitCancelOrderPostParams(
415417
category=product_type,
@@ -419,7 +421,7 @@ async def cancel_order(
419421
orderFilter=order_filter,
420422
),
421423
)
422-
return response.result
424+
return response
423425

424426
async def cancel_all_orders(
425427
self,
@@ -438,24 +440,24 @@ async def batch_place_orders(
438440
self,
439441
product_type: BybitProductType,
440442
submit_orders: list[BybitBatchPlaceOrder],
441-
) -> list[Any]:
443+
) -> BybitBatchPlaceOrderResponse:
442444
response = await self._endpoint_batch_place_order.post(
443445
BybitBatchPlaceOrderPostParams(
444446
category=product_type,
445447
request=submit_orders,
446448
),
447449
)
448-
return response.result.list
450+
return response
449451

450452
async def batch_cancel_orders(
451453
self,
452454
product_type: BybitProductType,
453455
cancel_orders: list[BybitBatchCancelOrder],
454-
) -> list[Any]:
456+
) -> BybitBatchCancelOrderResponse:
455457
response = await self._endpoint_batch_cancel_order.post(
456458
BybitBatchCancelOrderPostParams(
457459
category=product_type,
458460
request=cancel_orders,
459461
),
460462
)
461-
return response.result.list
463+
return response

0 commit comments

Comments
 (0)