From 86f7d4f7e3f13caed35c37b14152e0bf2cd81452 Mon Sep 17 00:00:00 2001 From: Claudia Pacini <44412060+claudiapacini@users.noreply.github.com> Date: Wed, 18 Feb 2026 05:56:23 +0100 Subject: [PATCH] Adds cl_ord_id spot endpoints --- src/kraken/spot/trade.py | 97 +++++++++++++++++++++++++++++++---- src/kraken/spot/user.py | 15 ++++++ src/kraken/spot/ws_client.py | 19 ++++--- tests/spot/test_spot_trade.py | 29 +++++++++-- tests/spot/test_spot_user.py | 13 ++++- 5 files changed, 149 insertions(+), 24 deletions(-) diff --git a/src/kraken/spot/trade.py b/src/kraken/spot/trade.py index f614d82..24b3995 100644 --- a/src/kraken/spot/trade.py +++ b/src/kraken/spot/trade.py @@ -96,6 +96,7 @@ def create_order( # pylint: disable=too-many-branches,too-many-arguments # noqa close_price2: str | float | None = None, deadline: str | None = None, userref: int | None = None, + cl_ord_id: str | None = None, *, truncate: bool = False, reduce_only: bool | None = False, @@ -190,6 +191,8 @@ def create_order( # pylint: disable=too-many-branches,too-many-arguments # noqa :type validate: bool, optional :param userref: User reference id for example to group orders :type userref: int, optional + :param cl_ord_id: Client order id (optional) + :type cl_ord_id: str, optional :raises ValueError: If input is not correct :return: The transaction id :rtype: dict @@ -400,6 +403,8 @@ def create_order( # pylint: disable=too-many-branches,too-many-arguments # noqa params["deadline"] = deadline if defined(userref): params["userref"] = userref + if defined(cl_ord_id): + params["cl_ord_id"] = cl_ord_id if defined(displayvol): params["displayvol"] = str(displayvol) @@ -456,6 +461,7 @@ def create_order_batch( ... "timeinforce": "GTC", ... "type": "buy", ... "userref": 16861348843, + ... "cl_ord_id": "my-client-order-id-1", ... "volume": 1, ... }, ... { @@ -464,6 +470,7 @@ def create_order_batch( ... "timeinforce": "GTC", ... "type": "sell", ... "userref": 16861348843, + ... "cl_ord_id": "my-client-order-id-2", ... "volume": 2, ... }, ... ], @@ -491,8 +498,19 @@ def create_order_batch( extra_params=extra_params, ) - def amend_order( + def amend_order( # pylint: disable=too-many-arguments # noqa: PLR0913, PLR0917 self: Trade, + txid: str | None = None, + cl_ord_id: str | None = None, + order_qty: str | float | None = None, + display_qty: str | float | None = None, + limit_price: str | float | None = None, + trigger_price: str | float | None = None, + pair: str | None = None, + post_only: bool | None = None, + deadline: str | None = None, + nonce: int | None = None, + validate: bool = False, *, extra_params: dict | None = None, ) -> dict: @@ -504,6 +522,32 @@ def amend_order( - https://docs.kraken.com/api/docs/rest-api/amend-order + :param txid: The txid of the order to edit + :type txid: str, optional + :param cl_ord_id: Client order id (optional) + :type cl_ord_id: str, optional + :param order_qty: Set a new order quantity + :type order_qty: str | float, optional + :param display_qty: Set a new display quantity + :type display_qty: str | float, optional + :param limit_price: Set a new limit price + :type limit_price: str | float, optional + :param trigger_price: Set a new trigger price + :type trigger_price: str | float, optional + :param pair: Pair of the order, required on amends for non-crypto orders + :type pair: str, optional + :param post_only: Set post-only flag + :type post_only: bool, optional + :param deadline: RFC3339 timestamp + :type deadline: str, optional + :param nonce: Nonce used in construction of API-Sign header + :type nonce: int, optional + :param validate: Validate the order without placing on the market (default: ``False``) + :type validate: bool, optional + :raises ValueError: If both ``txid`` and ``cl_ord_id`` are not set + :return: Success or failure + :rtype: dict + .. code-block:: python :linenos: :caption: Spot Trade: Amend order @@ -511,15 +555,36 @@ def amend_order( >>> from kraken.spot import Trade >>> trade = Trade(key="api-key", secret="secret-key") >>> trade.amend_order( - ... extra_params={ - ... "txid": "OVM3PT-56ACO-53SM2T", - ... "limit_price": "105636.9", - ... } + ... txid="OVM3PT-56ACO-53SM2T", + ... limit_price="105636.9" ... ) """ + params: dict = {"validate": validate} + if defined(txid): + params["txid"] = txid + if defined(cl_ord_id): + params["cl_ord_id"] = cl_ord_id + if defined(order_qty): + params["order_qty"] = str(order_qty) + if defined(display_qty): + params["display_qty"] = str(display_qty) + if defined(limit_price): + params["limit_price"] = str(limit_price) + if defined(trigger_price): + params["trigger_price"] = str(trigger_price) + if defined(pair): + params["pair"] = pair + if defined(post_only): + params["post_only"] = post_only + if defined(deadline): + params["deadline"] = deadline + if defined(nonce): + params["nonce"] = nonce + return self.request( # type: ignore[return-value] "POST", uri="/0/private/AmendOrder", + params=params, extra_params=extra_params, ) @@ -632,21 +697,24 @@ def edit_order( # pylint: disable=too-many-arguments # noqa: PLR0913, PLR0917 @ensure_string("txid") def cancel_order( self: Trade, - txid: str, + txid: str | None = None, + cl_ord_id: str | None = None, *, extra_params: dict | None = None, ) -> dict: """ Cancel a specific order by ``txid``. Instead of a transaction id - a user reference id can be passed. + a user reference id or client order id can be passed. Requires the ``Cancel/close orders`` permission in the API key settings. - https://docs.kraken.com/api/docs/rest-api/cancel-order - :param txid: Transaction id or comma delimited list of user reference ids to cancel. - :type txid: str + :param txid: Transaction id, client order id, or comma delimited list of user reference ids to cancel. + :type txid: str, optional + :param cl_ord_id: Client order id (optional) + :type cl_ord_id: str, optional :return: Success or failure - Number of closed orders :rtype: dict @@ -659,10 +727,17 @@ def cancel_order( >>> trade.cancel_order(txid="OAUHYR-YCVK6-P22G6P") { 'count': 1 } """ + params: dict = {} + if defined(txid): + params["txid"] = txid + if defined(cl_ord_id): + params["cl_ord_id"] = cl_ord_id + + return self.request( # type: ignore[return-value] method="POST", uri="/0/private/CancelOrder", - params={"txid": txid}, + params=params, extra_params=extra_params, ) @@ -742,7 +817,7 @@ def cancel_order_batch( extra_params: dict | None = None, ) -> dict: """ - Cancel a a list of orders by ``txid`` or ``userref`` + Cancel a list of orders by ``txid``, ``userref`` or ``cl_ord_id``. Requires the ``Cancel/close orders`` permission in the API key settings. diff --git a/src/kraken/spot/user.py b/src/kraken/spot/user.py index d7223b2..f316cce 100644 --- a/src/kraken/spot/user.py +++ b/src/kraken/spot/user.py @@ -252,6 +252,7 @@ def get_trade_balance( def get_open_orders( self: User, userref: int | None = None, + cl_ord_id: str | None = None, *, trades: bool | None = False, extra_params: dict | None = None, @@ -266,6 +267,8 @@ def get_open_orders( :param userref: Filter the results by user reference id :type userref: int, optional + :param cl_ord_id: Filter the results by client order id + :type cl_ord_id: str, optional :param trades: Include trades related to position or not into the response (default: ``False``) :type trades: bool @@ -316,6 +319,8 @@ def get_open_orders( params: dict = {"trades": trades} if defined(userref): params["userref"] = userref + if defined(cl_ord_id): + params["cl_ord_id"] = cl_ord_id return self.request( # type: ignore[return-value] method="POST", uri="/0/private/OpenOrders", @@ -326,6 +331,7 @@ def get_open_orders( def get_closed_orders( self: User, userref: int | None = None, + cl_ord_id: str | None = None, start: int | None = None, end: int | None = None, ofs: int | None = None, @@ -344,6 +350,8 @@ def get_closed_orders( :param userref: Filter the results by user reference id :type userref: int, optional + :param cl_ord_id: Filter the results by client order id + :type cl_ord_id: str, optional :param start: Unix timestamp to start the search from :type start: int, optional :param end: Unix timestamp to define the last result to include @@ -404,6 +412,8 @@ def get_closed_orders( params: dict = {"trades": trades, "closetime": closetime} if defined(userref): params["userref"] = userref + if defined(cl_ord_id): + params["cl_ord_id"] = cl_ord_id if defined(start): params["start"] = start if defined(end): @@ -423,6 +433,7 @@ def get_orders_info( self: User, txid: list[str] | str, userref: int | None = None, + cl_ord_id: str | None = None, *, trades: bool | None = False, consolidate_taker: bool | None = True, @@ -441,6 +452,8 @@ def get_orders_info( :type txid: str | list[str] :param userref: Filter results by user reference id :type userref: int, optional + :param cl_ord_id: Filter results by client order id + :type cl_ord_id: str, optional :param trades: Include trades in the result or not (default: ``False``) :type trades: bool, optional :param consolidate_taker: Consolidate trades by individual taker trades @@ -524,6 +537,8 @@ def get_orders_info( } if defined(userref): params["userref"] = userref + if defined(cl_ord_id): + params["cl_ord_id"] = cl_ord_id return self.request( # type: ignore[return-value] method="POST", uri="/0/private/QueryOrders", diff --git a/src/kraken/spot/ws_client.py b/src/kraken/spot/ws_client.py index b8d76e3..cffc1c9 100644 --- a/src/kraken/spot/ws_client.py +++ b/src/kraken/spot/ws_client.py @@ -253,6 +253,7 @@ async def send_message( # noqa: C901 # pylint: disable=arguments-differ ... "order_qty": 1.0, ... "side": "buy", ... "symbol": "BTC/USD", + ... "cl_ord_id": "my-client-order-id" ... }, ... } ... ) @@ -275,6 +276,7 @@ async def send_message( # noqa: C901 # pylint: disable=arguments-differ ... "order_qty": 1, ... "order_type": "limit", ... "order_userref": 123456789, + ... "cl_ord_id": "my-client-order-id-1", ... "side": "buy", ... }, ... { @@ -282,6 +284,7 @@ async def send_message( # noqa: C901 # pylint: disable=arguments-differ ... "order_qty": 2.12345, ... "order_type": "limit", ... "order_userref": 212345679, + ... "cl_ord_id": "my-client-order-id-2", ... "side": "sell", ... "stp_type": "cancel_both", ... }, @@ -307,7 +310,8 @@ async def send_message( # noqa: C901 # pylint: disable=arguments-differ ... "orders": [ ... "123456789", ... "212345679", - ... "ORDER-ID123-4567890" + ... "ORDER-ID123-4567890", + ... "my-client-order-id" ... ], ... }, ... } @@ -356,25 +360,26 @@ async def send_message( # noqa: C901 # pylint: disable=arguments-differ ... message={ ... "method": "cancel_order", ... "params": { - ... "order_id": ["ORDER-ID123-456789", "ORDER-ID123-987654"], + ... "order_id": ["ORDER-ID123-456789", "my-client-order-id"], ... }, ... } ... ) - **Editing orders** can be done as shown in the example below. See - https://docs.kraken.com/api/docs/websocket-v2/edit_order for more information. + **Amending orders** can be done as shown in the example below. See + https://docs.kraken.com/api/docs/websocket-v2/amend_order for more information. .. code-block:: python :linenos: - :caption: Spot Websocket: Cancel order(s) + :caption: Spot Websocket: Amend order >>> await client_auth.send_message( ... message={ - ... "method": "edit_order", + ... "method": "amend_order", ... "params": { ... "order_id": "ORDER-ID123-456789", ... "order_qty": 2.5, ... "symbol": "BTC/USD", + ... "cl_ord_id": "my-client-order-id" ... }, ... } ... ) @@ -586,7 +591,7 @@ def private_methods(self: SpotWSClient) -> list[str]: June 2023): - `add_order `_ - - `amend_order` `_ + - `amend_order `_ - `cancel_order `_ - `cancel_all `_ - `cancel_all_orders_after `_ diff --git a/tests/spot/test_spot_trade.py b/tests/spot/test_spot_trade.py index 6a68e4f..448cefc 100644 --- a/tests/spot/test_spot_trade.py +++ b/tests/spot/test_spot_trade.py @@ -27,6 +27,7 @@ class TestSpotTrade: TEST_PAIR_DOTUSD = "DOTUSD" TEST_TXID = "OHYO67-6LP66-HMQ437" TEST_USERREF = "12345678" + TEST_CL_ORD_ID = "12345678" @pytest.mark.spot_auth def test_create_order(self: Self, spot_auth_trade: Trade) -> None: @@ -45,6 +46,7 @@ def test_create_order(self: Self, spot_auth_trade: Trade) -> None: pair=self.TEST_PAIR_BTCEUR, price=1.001, # this also checks the truncate option timeinforce="GTC", + userref=self.TEST_USERREF, truncate=True, validate=True, # important to just test this endpoint without risking money ) @@ -62,6 +64,7 @@ def test_create_order(self: Self, spot_auth_trade: Trade) -> None: price=100, expiretm="0", displayvol=1000, + cl_ord_id=self.TEST_CL_ORD_ID, validate=True, # important to just test this endpoint without risking money ) @@ -151,6 +154,7 @@ def test_create_order_batch(self: Self, spot_auth_trade: Trade) -> None: "timeinforce": "GTC", "type": "buy", "userref": 1680953421, + "cl_ord_id": self.TEST_CL_ORD_ID, "volume": 1000, }, { @@ -191,6 +195,7 @@ def test_edit_order(self: Self, spot_auth_trade: Trade) -> None: validate=True, # important ) + @pytest.mark.spot_auth def test_amend_order(self: Self, spot_auth_trade: Trade) -> None: """ @@ -204,10 +209,17 @@ def test_amend_order(self: Self, spot_auth_trade: Trade) -> None: match=r"API key doesn't have permission to make this request.", ): spot_auth_trade.amend_order( - extra_params={ - "txid": "OVM3PT-56ACO-53SM2T", - "limit_price": "105636.9", - }, + txid="OVM3PT-56ACO-53SM2T", + limit_price="105636.9", + ) + + with pytest.raises( + KrakenPermissionDeniedError, + match=r"API key doesn't have permission to make this request.", + ): + spot_auth_trade.amend_order( + cl_ord_id=self.TEST_CL_ORD_ID, + limit_price="105636.9", ) @pytest.mark.spot_auth @@ -224,6 +236,15 @@ def test_cancel_order(self: Self, spot_auth_trade: Trade) -> None: ): spot_auth_trade.cancel_order(txid="OB6JJR-7NZ5P-N5SKCB") + with pytest.raises( + KrakenPermissionDeniedError, + match=r"API key doesn't have permission to make this request.", + ): + spot_auth_trade.cancel_order(cl_ord_id="my-client-order-id") + + with pytest.raises(ValueError, match=r"Either txid or cl_ord_id must be set!"): + spot_auth_trade.cancel_order() + @pytest.mark.spot_auth @pytest.mark.skip(reason="Test do not have trade/cancel permission") def test_cancel_all_orders(self: Self, spot_auth_trade: Trade) -> None: diff --git a/tests/spot/test_spot_user.py b/tests/spot/test_spot_user.py index ebf9afa..594f37e 100644 --- a/tests/spot/test_spot_user.py +++ b/tests/spot/test_spot_user.py @@ -93,7 +93,11 @@ def test_get_open_orders(self: Self, spot_auth_user: User) -> None: """ assert is_not_error(spot_auth_user.get_open_orders(trades=True)) assert is_not_error( - spot_auth_user.get_open_orders(trades=False, userref="1234567"), + spot_auth_user.get_open_orders( + trades=False, + userref="1234567", + cl_ord_id="1234567", + ), ) def test_get_closed_orders(self: Self, spot_auth_user: User) -> None: @@ -103,7 +107,11 @@ def test_get_closed_orders(self: Self, spot_auth_user: User) -> None: """ assert is_not_error(spot_auth_user.get_closed_orders()) assert is_not_error( - spot_auth_user.get_closed_orders(trades=True, userref="1234"), + spot_auth_user.get_closed_orders( + trades=True, + userref="1234", + cl_ord_id="1234", + ), ) assert is_not_error( spot_auth_user.get_closed_orders(trades=True, start="1668431675.4778206"), @@ -162,6 +170,7 @@ def test_get_orders_info(self: Self, spot_auth_user: User) -> None: ( {"txid": "OXBBSK-EUGDR-TDNIEQ"}, {"txid": "OXBBSK-EUGDR-TDNIEQ", "trades": True}, + {"txid": "OQQYNL-FXCFA-FBFVD7", "cl_ord_id": "1234567"}, {"txid": "OQQYNL-FXCFA-FBFVD7", "consolidate_taker": True}, {"txid": ["OE3B4A-NSIEQ-5L6HW3", "O23GOI-WZDVD-XWGC3R"]}, ),