fix: Fire reject report if order ack has errors
Rejects are ONLY observable in the order ack, not the execution report stream from which all other updates are observed. While the pybit sdk does expose ack callbacks, it doesn't invoke them on rejects/errors.
This commit is contained in:
1 parent
02626ce35c
commit
27cbd51b1c
8 files changed
+231
-50
No files matched your search
@@ -2,7 +2,7 @@ import json
|
||||
import time
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from pybit.unified_trading import WebSocket, WebSocketTrading
|
||||
from pybit_unified_trading import WebSocket, WebSocketTrading
|
||||
from spark_core import (
|
||||
JsonElement,
|
||||
as_bool,
|
||||
@@ -24,14 +24,32 @@ from python_interop_module import (
|
||||
PySecurity,
|
||||
PySecurityList,
|
||||
)
|
||||
from spark_enums import OrderStatus, ExecType
|
||||
from spark_enums import OrderStatus, ExecType, OrderType, SecurityType, TimeInForce
|
||||
import threading
|
||||
|
||||
|
||||
class OrderData:
|
||||
def __init__(self, bts_id: int, exch_id: str, price: float, quantity: float):
|
||||
def __init__(
|
||||
self,
|
||||
account: str,
|
||||
bts_id: int,
|
||||
exch_id: str,
|
||||
exch_sec_id: str,
|
||||
sec_type: SecurityType,
|
||||
is_buy: bool,
|
||||
order_type: OrderType,
|
||||
time_in_force: TimeInForce,
|
||||
price: float,
|
||||
quantity: float,
|
||||
):
|
||||
self.account = account
|
||||
self.bts_id = bts_id
|
||||
self.exch_id = exch_id
|
||||
self.exch_sec_id = exch_sec_id
|
||||
self.sec_type = sec_type
|
||||
self.is_buy = is_buy
|
||||
self.order_type = order_type
|
||||
self.time_in_force = time_in_force
|
||||
self.price = price
|
||||
self.quantity = quantity
|
||||
self.filled_quantity: float = 0
|
||||
@@ -41,6 +59,53 @@ class OrderData:
|
||||
self.exec_type: ExecType = ExecType.PENDING_NEW
|
||||
self.status: OrderStatus = OrderStatus.PENDING_NEW
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"account": self.account,
|
||||
"bts_id": self.bts_id,
|
||||
"exch_id": self.exch_id,
|
||||
"exch_sec_id": self.exch_sec_id,
|
||||
"sec_type": self.sec_type.name,
|
||||
"is_buy": self.is_buy,
|
||||
"order_type": self.order_type.name,
|
||||
"time_in_force": self.time_in_force.name,
|
||||
"price": self.price,
|
||||
"quantity": self.quantity,
|
||||
"filled_quantity": self.filled_quantity,
|
||||
"last_filled_quantity": self.last_filled_quantity,
|
||||
"avg_filled_price": self.avg_filled_price,
|
||||
"last_filled_price": self.last_filled_price,
|
||||
"exec_type": self.exec_type.name,
|
||||
"status": self.status.name,
|
||||
}
|
||||
|
||||
def to_execution_report(self):
|
||||
transact_time = time.time_ns()
|
||||
er = PyExecutionReport()
|
||||
er.set_data(
|
||||
key=self.bts_id,
|
||||
exchange_order_id=self.exch_id,
|
||||
venue="BYBIT",
|
||||
security_type=self.sec_type.name,
|
||||
exchange_security_id=self.exch_sec_id,
|
||||
account=self.account,
|
||||
order_type=self.order_type.name,
|
||||
is_buy=self.is_buy,
|
||||
price=self.price,
|
||||
quantity=self.quantity,
|
||||
time_in_force=self.time_in_force.name,
|
||||
exec_type=self.exec_type.name,
|
||||
order_status=self.status.name,
|
||||
last_filled_price=self.last_filled_price,
|
||||
last_filled_quantity=self.last_filled_quantity,
|
||||
filled_quantity=self.filled_quantity,
|
||||
avg_filled_price=self.avg_filled_price,
|
||||
# There is no transact_id in Bybit, so we use updatedTime
|
||||
transact_id=str(transact_time),
|
||||
transact_time=transact_time,
|
||||
)
|
||||
return er
|
||||
|
||||
def on_update(
|
||||
self,
|
||||
price: float,
|
||||
@@ -83,6 +148,7 @@ class OrderData:
|
||||
self.status = OrderStatus.REJECTED
|
||||
case _:
|
||||
raise ValueError(f"Unexpected order status '{status}'")
|
||||
return self.to_execution_report()
|
||||
|
||||
|
||||
class BybitExecutionGateway(ExecutionGateway):
|
||||
@@ -113,10 +179,53 @@ class BybitExecutionGateway(ExecutionGateway):
|
||||
return v
|
||||
return None
|
||||
|
||||
def _on_order_ack(self, typ, msg: object):
|
||||
self.logger.info(
|
||||
'[bybit_gw_ord_ack.json]{ "typ": "%s", "msg": %s}' % (typ, msg)
|
||||
)
|
||||
def _order_handlers(self, typ: str, exch_id: str):
|
||||
def get_error_desc(msg: dict):
|
||||
return "%s (code %s)" % (
|
||||
(msg.get("retMsg") or "Unknown", msg.get("retCode") or "Unknown")
|
||||
)
|
||||
|
||||
def on_err(msg: dict):
|
||||
self.logger.error(
|
||||
'[bybit_gw_ord_ack_err.json]{ "typ": "%s", "exch_id": "%s", "err": %s, "known_orders": %s }'
|
||||
% (
|
||||
typ,
|
||||
exch_id,
|
||||
JsonElement(msg),
|
||||
JsonElement({k: v.to_json() for k, v in self.orders.items()}),
|
||||
)
|
||||
)
|
||||
req_id = msg.get("reqId")
|
||||
if not req_id:
|
||||
self.logger.error(
|
||||
'[bybit_gw_ord_ack_err.json]{ "typ": "%s", "exch_id": "%s", "err": "%s" }'
|
||||
% (typ, exch_id, msg)
|
||||
)
|
||||
ord_data = self._get_order_data(exch_id)
|
||||
if ord_data:
|
||||
er = ord_data.to_execution_report()
|
||||
match er.order_status():
|
||||
case OrderStatus.PENDING_NEW.name:
|
||||
er.set_order_status(OrderStatus.REJECTED.name)
|
||||
er.set_exec_type(ExecType.REJECTED.name)
|
||||
er.set_error_msg(get_error_desc(msg))
|
||||
self.bridge.on_execution_report(er)
|
||||
case _:
|
||||
pass
|
||||
else:
|
||||
self.logger.error(
|
||||
'[bybit_gw_ord_ack_err.json]{"typ": "%s", "exch_id": "%s", "err": "Unknown exch id" }'
|
||||
)
|
||||
|
||||
def on_ack(msg: dict):
|
||||
ord_data = self._get_order_data(exch_id)
|
||||
if ord_data is None:
|
||||
self.logger.error(
|
||||
'[bybit_gw_ord_ack_err.json]{ "typ": "%s", "exch_id": "%s", "err": "No order data found with exch id", "msg": "%s" }'
|
||||
% (typ, exch_id, msg)
|
||||
)
|
||||
|
||||
return (on_ack, on_err)
|
||||
|
||||
def _on_abort(
|
||||
self,
|
||||
@@ -210,9 +319,6 @@ class BybitExecutionGateway(ExecutionGateway):
|
||||
}
|
||||
self.logger.info("[bybit_gw_start_trd_cli.json]%s" % JsonElement(params))
|
||||
self.trade_cli = WebSocketTrading(**params)
|
||||
params = {
|
||||
"category": "linear",
|
||||
}
|
||||
self.bridge.on_is_ready(True)
|
||||
# Sleep loop to keep thread alive and detect connection status changes
|
||||
conn_statuses = {
|
||||
@@ -269,13 +375,22 @@ class BybitExecutionGateway(ExecutionGateway):
|
||||
bts_id = req.key()
|
||||
px = req.price()
|
||||
qty = req.quantity()
|
||||
self.orders[req.key()] = OrderData(
|
||||
bts_id=bts_id, exch_id=exch_ord_id, price=px, quantity=qty
|
||||
)
|
||||
sec_name = req.exchange_security_id()
|
||||
self.orders[req.key()] = OrderData(
|
||||
account=self.account,
|
||||
bts_id=bts_id,
|
||||
exch_id=exch_ord_id,
|
||||
exch_sec_id=req.exchange_security_id(),
|
||||
sec_type=SecurityType.FUTURE,
|
||||
is_buy=req.is_buy(),
|
||||
order_type=OrderType.parse(req.order_type()),
|
||||
time_in_force=TimeInForce.parse(req.time_in_force()),
|
||||
price=px,
|
||||
quantity=qty,
|
||||
)
|
||||
sec = self.securities.get(sec_name)
|
||||
if not sec:
|
||||
return
|
||||
raise ValueError(f"Unknown security '{sec_name}'")
|
||||
params = {
|
||||
"orderLinkId": exch_ord_id,
|
||||
"category": "linear",
|
||||
@@ -285,8 +400,9 @@ class BybitExecutionGateway(ExecutionGateway):
|
||||
"price": format_price(sec, px),
|
||||
"qty": format_quantity(sec, qty),
|
||||
}
|
||||
on_ack, on_err = self._order_handlers("crt", exch_ord_id)
|
||||
self.logger.info("[bybit_gw_crt_ord_send.json]%s" % SLog(params=params))
|
||||
self.trade_cli.place_order(lambda msg: self._on_order_ack("crt", msg), **params)
|
||||
self.trade_cli.place_order(on_ack, **params, error_callback=on_err)
|
||||
|
||||
def cancel_replace_order(self, req: PyCancelReplaceOrderRequest):
|
||||
self.logger.info("[bybit_gw_cxlrep_ord.json]%s" % req)
|
||||
@@ -309,10 +425,9 @@ class BybitExecutionGateway(ExecutionGateway):
|
||||
"price": format_price(sec, req.price()),
|
||||
"qty": format_quantity(sec, req.quantity()),
|
||||
}
|
||||
on_ack, on_err = self._order_handlers("cxlrep", ord_data.exch_id)
|
||||
self.logger.info("[bybit_gw_cxlrep_ord_send.json]%s" % SLog(params=params))
|
||||
self.trade_cli.amend_order(
|
||||
lambda msg: self._on_order_ack("cxlrep", msg), **params
|
||||
)
|
||||
self.trade_cli.amend_order(on_ack, **params, error_callback=on_err)
|
||||
|
||||
def cancel_order(self, req: PyCancelOrderRequest):
|
||||
self.logger.info("[bybit_gw_cxl_ord.json]%s" % req)
|
||||
@@ -329,10 +444,9 @@ class BybitExecutionGateway(ExecutionGateway):
|
||||
"category": "linear",
|
||||
"symbol": req.exchange_security_id(),
|
||||
}
|
||||
on_ack, on_err = self._order_handlers("cxl", ord_data.exch_id)
|
||||
self.logger.info("[bybit_gw_cxl_ord_send.json]%s" % SLog(params=params))
|
||||
self.trade_cli.cancel_order(
|
||||
lambda msg: self._on_order_ack("cxl", msg), **params
|
||||
)
|
||||
self.trade_cli.cancel_order(on_ack, **params, error_callback=on_err)
|
||||
|
||||
def on_securities(self, securities: PySecurityList):
|
||||
self.logger.info("[bybit_gw_sec.json]%s" % securities)
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
from pybit.exceptions import (
|
||||
InvalidChannelTypeError,
|
||||
TopicMismatchError,
|
||||
UnauthorizedExceptionError,
|
||||
)
|
||||
from ._v5_misc import MiscHTTP
|
||||
from ._v5_market import MarketHTTP
|
||||
from ._v5_trade import TradeHTTP
|
||||
from ._v5_account import AccountHTTP
|
||||
from ._v5_asset import AssetHTTP
|
||||
from ._v5_position import PositionHTTP
|
||||
from ._v5_pre_upgrade import PreUpgradeHTTP
|
||||
from ._v5_spot_leverage_token import SpotLeverageHTTP
|
||||
from ._v5_spot_margin_trade import SpotMarginTradeHTTP
|
||||
from ._v5_user import UserHTTP
|
||||
from ._v5_broker import BrokerHTTP
|
||||
from ._v5_institutional_loan import InstitutionalLoanHTTP
|
||||
from ._v5_crypto_loan import CryptoLoanHTTP
|
||||
from ._v5_earn import EarnHTTP
|
||||
from ._websocket_stream import _V5WebSocketManager
|
||||
from ._websocket_trading import _V5TradeWebSocketManager
|
||||
from ._v5_spread import (
|
||||
from pybit._v5_misc import MiscHTTP
|
||||
from pybit._v5_market import MarketHTTP
|
||||
from pybit._v5_trade import TradeHTTP
|
||||
from pybit._v5_account import AccountHTTP
|
||||
from pybit._v5_asset import AssetHTTP
|
||||
from pybit._v5_position import PositionHTTP
|
||||
from pybit._v5_pre_upgrade import PreUpgradeHTTP
|
||||
from pybit._v5_spot_leverage_token import SpotLeverageHTTP
|
||||
from pybit._v5_spot_margin_trade import SpotMarginTradeHTTP
|
||||
from pybit._v5_user import UserHTTP
|
||||
from pybit._v5_broker import BrokerHTTP
|
||||
from pybit._v5_institutional_loan import InstitutionalLoanHTTP
|
||||
from pybit._v5_crypto_loan import CryptoLoanHTTP
|
||||
from pybit._v5_earn import EarnHTTP
|
||||
from pybit_websocket_stream import _V5WebSocketManager
|
||||
from pybit_websocket_trading import _V5TradeWebSocketManager
|
||||
from pybit._v5_spread import (
|
||||
SpreadHTTP,
|
||||
_V5WebSocketSpreadTrading,
|
||||
)
|
||||
@@ -370,29 +371,46 @@ class WebSocketTrading(_V5TradeWebSocketManager):
|
||||
def __init__(self, recv_window=0, referral_id="", **kwargs):
|
||||
super().__init__(recv_window, referral_id, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _extract_error_callback(req_params: dict[str, Any]):
|
||||
err_cb = req_params.pop("error_callback") if "error_callback" in req_params else None
|
||||
return (req_params, err_cb)
|
||||
|
||||
def place_order(self, callback, **kwargs):
|
||||
operation = "order.create"
|
||||
self._send_order_operation(operation, callback, kwargs)
|
||||
self._send_order_operation(
|
||||
operation, callback, *self._extract_error_callback(kwargs)
|
||||
)
|
||||
|
||||
def amend_order(self, callback, **kwargs):
|
||||
operation = "order.amend"
|
||||
self._send_order_operation(operation, callback, kwargs)
|
||||
self._send_order_operation(
|
||||
operation, callback, *self._extract_error_callback(kwargs)
|
||||
)
|
||||
|
||||
def cancel_order(self, callback, **kwargs):
|
||||
operation = "order.cancel"
|
||||
self._send_order_operation(operation, callback, kwargs)
|
||||
self._send_order_operation(
|
||||
operation, callback, *self._extract_error_callback(kwargs)
|
||||
)
|
||||
|
||||
def place_batch_order(self, callback, **kwargs):
|
||||
operation = "order.create-batch"
|
||||
self._send_order_operation(operation, callback, kwargs)
|
||||
self._send_order_operation(
|
||||
operation, callback, *self._extract_error_callback(kwargs)
|
||||
)
|
||||
|
||||
def amend_batch_order(self, callback, **kwargs):
|
||||
operation = "order.amend-batch"
|
||||
self._send_order_operation(operation, callback, kwargs)
|
||||
self._send_order_operation(
|
||||
operation, callback, *self._extract_error_callback(kwargs)
|
||||
)
|
||||
|
||||
def cancel_batch_order(self, callback, **kwargs):
|
||||
operation = "order.cancel-batch"
|
||||
self._send_order_operation(operation, callback, kwargs)
|
||||
self._send_order_operation(
|
||||
operation, callback, *self._extract_error_callback(kwargs)
|
||||
)
|
||||
|
||||
|
||||
class WebsocketSpreadTrading(_V5WebSocketSpreadTrading):
|
||||
|
||||
@@ -2,11 +2,11 @@ import websocket
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
from ._http_manager import generate_signature
|
||||
from pybit._http_manager import generate_signature
|
||||
import logging
|
||||
import copy
|
||||
from uuid import uuid4
|
||||
from . import _helpers
|
||||
from pybit import _helpers
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -62,6 +62,7 @@ class _WebSocketManager:
|
||||
# "topic_name": function
|
||||
# }
|
||||
self.callback_directory = {}
|
||||
self.error_callback_directory = {}
|
||||
|
||||
# Record the subscriptions made so that we can resubscribe if the WSS
|
||||
# connection is broken.
|
||||
|
||||
@@ -2,8 +2,8 @@ from dataclasses import dataclass, field
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
from ._websocket_stream import _WebSocketManager
|
||||
from . import _helpers
|
||||
from pybit_websocket_stream import _WebSocketManager
|
||||
from pybit import _helpers
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,7 +37,10 @@ class _V5TradeWebSocketManager(_WebSocketManager):
|
||||
f"WebSocket request {message['reqId']} hit an error. Enabling "
|
||||
f"traceLogging to reproduce the issue. Raw error: {message}"
|
||||
)
|
||||
self._pop_callback(message["reqId"])
|
||||
req_id = message["reqId"]
|
||||
self._pop_callback(req_id)
|
||||
if req_id in self.error_callback_directory:
|
||||
self.error_callback_directory.pop(req_id)(message)
|
||||
|
||||
def _handle_incoming_message(self, message):
|
||||
def is_auth_message():
|
||||
@@ -63,10 +66,16 @@ class _V5TradeWebSocketManager(_WebSocketManager):
|
||||
def _set_callback(self, topic, callback_function):
|
||||
self.callback_directory[topic] = callback_function
|
||||
|
||||
def _set_error_callback(self, topic, callback_function):
|
||||
self.error_callback_directory[topic] = callback_function
|
||||
|
||||
def _pop_callback(self, topic):
|
||||
return self.callback_directory.pop(topic)
|
||||
|
||||
def _send_order_operation(self, operation, callback, request):
|
||||
def _pop_error_callback(self, topic):
|
||||
return self.error_callback_directory.pop(topic)
|
||||
|
||||
def _send_order_operation(self, operation, callback, request, error_callback=None):
|
||||
request_id = str(uuid.uuid4())
|
||||
|
||||
message = {
|
||||
@@ -87,3 +96,5 @@ class _V5TradeWebSocketManager(_WebSocketManager):
|
||||
|
||||
self.ws.send(json.dumps(message))
|
||||
self._set_callback(request_id, callback)
|
||||
if error_callback:
|
||||
self._set_error_callback(request_id, error_callback)
|
||||
@@ -17,3 +17,27 @@ class ExecType(Enum):
|
||||
FILL = 4
|
||||
CANCELED = 5
|
||||
REJECTED = 6
|
||||
|
||||
class OrderType(Enum):
|
||||
LIMIT = 1
|
||||
MARKET = 2
|
||||
ICEBERG = 3
|
||||
SPREADER = 4
|
||||
|
||||
@staticmethod
|
||||
def parse(s: str):
|
||||
return OrderType.__members__[s]
|
||||
|
||||
|
||||
class TimeInForce(Enum):
|
||||
FOK = 1
|
||||
GTC = 2
|
||||
|
||||
@staticmethod
|
||||
def parse(s: str):
|
||||
return TimeInForce.__members__[s]
|
||||
|
||||
class SecurityType(Enum):
|
||||
SPOT = 1
|
||||
FUTURE = 2
|
||||
OPTION = 3
|
||||
Reference in new issue
Block a user