From 0419f08bf2822d097079b75adec456d13abd2555 Mon Sep 17 00:00:00 2001 From: "Benjamin T. Schwertfeger" Date: Sat, 9 Mar 2024 07:00:25 +0100 Subject: [PATCH 1/4] Resolve "Add `processBefore` parameter to `kraken.futures.Trade.`{`cancel_order`,`edit_order`,`create_order`,`create_batch_order`}" (#192) --- kraken/futures/trade.py | 24 +++++++++++++++++++++++- pyproject.toml | 2 ++ tests/futures/test_futures_trade.py | 6 +++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/kraken/futures/trade.py b/kraken/futures/trade.py index 2018af3b..aff45415 100644 --- a/kraken/futures/trade.py +++ b/kraken/futures/trade.py @@ -119,6 +119,7 @@ def get_fills( def create_batch_order( self: Trade, batchorder_list: list[dict], + processBefore: Optional[str] = None, *, extra_params: Optional[dict] = None, ) -> dict: @@ -133,6 +134,8 @@ def create_batch_order( :param batchorder_list: List of order instructions (see example below - or the linked official Kraken documentation) :type batchorder_list: list[dict] + :param processBefore: Process before timestamp otherwise reject + :type processBefore: str, optional :return: Information about the submitted request :rtype: dict @@ -223,10 +226,14 @@ def create_batch_order( } """ batchorder: dict = {"batchOrder": batchorder_list} + params = {"json": f"{batchorder}"} + if processBefore: + params["processBefore"] = processBefore + return self._request( # type: ignore[return-value] method="POST", uri="/derivatives/api/v3/batchorder", - post_params={"json": f"{batchorder}"}, + post_params=params, auth=True, extra_params=extra_params, ) @@ -335,6 +342,7 @@ def cancel_order( self: Trade, order_id: Optional[str] = None, cliOrdId: Optional[str] = None, + processBefore: Optional[str] = None, *, extra_params: Optional[dict] = None, ) -> dict: @@ -351,6 +359,8 @@ def cancel_order( :type order_id: str, optional :param cliOrdId: The client defined order id :type cliOrdId: str, optional + :param processBefore: Process before timestamp otherwise reject + :type processBefore: str, optional :raises ValueError: If both ``order_id`` and ``cliOrdId`` are not set :return: Success or failure :rtype: dict @@ -377,6 +387,8 @@ def cancel_order( params["order_id"] = order_id elif defined(cliOrdId): params["cliOrdId"] = cliOrdId + elif defined(processBefore): + params["processBefore"] = processBefore else: raise ValueError("Either order_id or cliOrdId must be set!") @@ -395,6 +407,7 @@ def edit_order( limitPrice: Optional[str | float] = None, size: Optional[str | float] = None, stopPrice: Optional[str | float] = None, + processBefore: Optional[str] = None, *, extra_params: Optional[dict] = None, ) -> dict: @@ -416,6 +429,8 @@ def edit_order( :type size: str | float, optional :param stopPrice: The stop price :type stopPrice: str | float, optional + :param processBefore: Process before timestamp otherwise reject + :type processBefore: str, optional :raises ValueError: If both ``orderId`` and ``cliOrdId`` are not set :return: Success or failure :rtype: dict @@ -453,6 +468,8 @@ def edit_order( params["size"] = size if defined(stopPrice): params["stopPrice"] = stopPrice + if defined(processBefore): + params["processBefore"] = processBefore return self._request( # type: ignore[return-value] method="POST", @@ -524,6 +541,7 @@ def create_order( # pylint: disable=too-many-arguments # noqa: PLR0913, PLR0917 triggerSignal: Optional[str] = None, trailingStopDeviationUnit: Optional[str] = None, trailingStopMaxDeviation: Optional[str] = None, + processBefore: Optional[str] = None, *, extra_params: Optional[dict] = None, ) -> dict: @@ -561,6 +579,8 @@ def create_order( # pylint: disable=too-many-arguments # noqa: PLR0913, PLR0917 :type trailingStopDeviationUnit: str, optional :param trailingStopMaxDeviation: See referenced Kraken documentation :type trailingStopMaxDeviation: str, optional + :param processBefore: Process before timestamp otherwise reject + :type processBefore: str, optional :return: Success or failure :rtype: dict @@ -721,6 +741,8 @@ def create_order( # pylint: disable=too-many-arguments # noqa: PLR0913, PLR0917 params["trailingStopDeviationUnit"] = trailingStopDeviationUnit if defined(trailingStopMaxDeviation): params["trailingStopMaxDeviation"] = trailingStopMaxDeviation + if defined(processBefore): + params["processBefore"] = processBefore return self._request( # type: ignore[return-value] method="POST", diff --git a/pyproject.toml b/pyproject.toml index e82bed20..e97c1c48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -339,6 +339,7 @@ ignore-names = [ "TIMEOUT", "URL", "HEADERS", + "processBefore", ] [tool.ruff.lint.pylint] @@ -547,6 +548,7 @@ good-names = [ "TIMEOUT", "URL", "HEADERS", + "processBefore", ] # Good variable names regexes, separated by a comma. If names match any regex, diff --git a/tests/futures/test_futures_trade.py b/tests/futures/test_futures_trade.py index 803937ae..5dc91215 100644 --- a/tests/futures/test_futures_trade.py +++ b/tests/futures/test_futures_trade.py @@ -99,6 +99,7 @@ def test_create_order(futures_demo_trade) -> None: limitPrice=1, stopPrice=10, reduceOnly=True, + processBefore="3033-11-08T19:56:35.441899Z", ) # FIXME: why are these commented out? @@ -204,6 +205,7 @@ def test_create_batch_order(futures_demo_trade) -> None: "cliOrdId": "my_client_id", }, ], + processBefore="3033-11-08T19:56:35.441899Z", ), ) @@ -227,6 +229,7 @@ def test_edit_order(futures_demo_trade) -> None: cliOrdId="685d5a1a-23eb-450c-bf17-1e4ab5c6fe8a", size=111.0, stopPrice=1000, + processBefore="3033-11-08T19:56:35.441899Z", ), ) @@ -251,7 +254,8 @@ def test_cancel_order(futures_demo_trade) -> None: """ assert is_success( futures_demo_trade.cancel_order( - cliOrdId="685d5a1a-23eb-450c-bf17-1e4ab5c6fe8a", + cliOrdId="my_another_client_id", + processBefore="3033-11-08T19:56:35.441899Z", ), ) assert is_success( From 87f039683c77f24f3db0c5123b6a68176c60f2d3 Mon Sep 17 00:00:00 2001 From: "Benjamin T. Schwertfeger" Date: Sat, 9 Mar 2024 09:18:39 +0100 Subject: [PATCH 2/4] Resolve "Add `kraken.futures.Trade.get_max_order_size`" (#193) --- kraken/futures/trade.py | 38 +++++++++++++++++++++++++++++ tests/futures/test_futures_trade.py | 22 +++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/kraken/futures/trade.py b/kraken/futures/trade.py index aff45415..1e859cf3 100644 --- a/kraken/futures/trade.py +++ b/kraken/futures/trade.py @@ -752,5 +752,43 @@ def create_order( # pylint: disable=too-many-arguments # noqa: PLR0913, PLR0917 extra_params=extra_params, ) + def get_max_order_size( + self: Trade, + orderType: str, + symbol: str, + limitPrice: Optional[float] = None, + *, + extra_params: Optional[dict] = None, + ) -> dict: + """ + Retrieve the maximum order price for a specific symbol. Can be adjusted + by ``limitPrice``. This endpoint only supports multi-collateral futures. + + Requires at least the ``General API - Read Access`` permission in the + API key settings. + + - https://docs.futures.kraken.com/#http-api-trading-v3-api-order-management-get-maximum-order-size + + :param orderType: ``lmt`` or ``mkt`` + :type orderType: str + :param symbol: The symbol to filter for + :type symbol: str + :param limitPrice: Limit price if ``orderType == lmt`` , defaults to + None + :type limitPrice: Optional[float], optional + """ + params: dict = {"orderType": orderType, "symbol": symbol} + + if defined(limitPrice) and orderType == "lmt": + params["limitPrice"] = limitPrice + + return self._request( # type: ignore[return-value] + method="GET", + uri="/derivatives/api/v3/initialmargin/maxordersize", + query_params=params, + auth=True, + extra_params=extra_params, + ) + __all__ = ["Trade"] diff --git a/tests/futures/test_futures_trade.py b/tests/futures/test_futures_trade.py index 5dc91215..6ba86e45 100644 --- a/tests/futures/test_futures_trade.py +++ b/tests/futures/test_futures_trade.py @@ -286,3 +286,25 @@ def test_cancel_all_orders(futures_demo_trade) -> None: """ assert is_success(futures_demo_trade.cancel_all_orders(symbol="pi_xbtusd")) assert is_success(futures_demo_trade.cancel_all_orders()) + + +@pytest.mark.futures() +@pytest.mark.futures_auth() +@pytest.mark.futures_trade() +def test_get_max_order_size(futures_auth_trade) -> None: + """ + Checks the ``cancel_all_orders`` endpoint. + """ + assert is_success( + futures_auth_trade.get_max_order_size( + orderType="lmt", + symbol="PF_XBTUSD", + limitPrice=10000, + ), + ) + assert is_success( + futures_auth_trade.get_max_order_size( + orderType="mkt", + symbol="PF_XBTUSD", + ), + ) From c1e6f782460fa15d7bd2be872b82015834e18aa1 Mon Sep 17 00:00:00 2001 From: "Benjamin T. Schwertfeger" Date: Sat, 9 Mar 2024 10:55:51 +0100 Subject: [PATCH 3/4] Resolve "Mark `kraken.spot.Staking` as deprecated and add `kraken.spot.Earn`" (#199) --- .gitignore | 2 +- README.md | 1 - doc/src/spot/rest.rst | 5 + kraken/base_api/__init__.py | 1 - kraken/exceptions/__init__.py | 48 ++++ kraken/spot/__init__.py | 4 +- kraken/spot/earn.py | 386 +++++++++++++++++++++++++++++++ kraken/spot/funding.py | 5 +- kraken/spot/market.py | 5 +- kraken/spot/staking.py | 21 +- kraken/spot/trade.py | 6 +- kraken/spot/user.py | 6 +- kraken/utils/__init__.py | 10 + kraken/utils/utils.py | 32 +++ pyproject.toml | 3 + tests/spot/conftest.py | 19 +- tests/spot/test_spot_base_api.py | 4 + tests/spot/test_spot_earn.py | 105 +++++++++ 18 files changed, 645 insertions(+), 18 deletions(-) create mode 100644 kraken/spot/earn.py create mode 100644 kraken/utils/__init__.py create mode 100644 kraken/utils/utils.py create mode 100644 tests/spot/test_spot_earn.py diff --git a/.gitignore b/.gitignore index 74412e73..ec70b914 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,7 @@ pytest.xml *.pot # Sphinx documentation -docs/_build/ +doc/_build/ # PyBuilder target/ diff --git a/README.md b/README.md index 0e14e85b..c795f28e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Typing](https://img.shields.io/badge/typing-mypy-informational)](https://mypy-lang.org/) -[![CodeQL](https://github.com/btschwertfeger/python-kraken-sdk/actions/workflows/codeql.yaml/badge.svg?branch=master)](https://github.com/btschwertfeger/python-kraken-sdk/actions/workflows/codeql.yaml) [![CI/CD](https://github.com/btschwertfeger/python-kraken-sdk/actions/workflows/cicd.yaml/badge.svg?branch=master)](https://github.com/btschwertfeger/python-kraken-sdk/actions/workflows/cicd.yaml) [![codecov](https://codecov.io/gh/btschwertfeger/python-kraken-sdk/branch/master/badge.svg)](https://app.codecov.io/gh/btschwertfeger/python-kraken-sdk) diff --git a/doc/src/spot/rest.rst b/doc/src/spot/rest.rst index 275317e7..562defbf 100644 --- a/doc/src/spot/rest.rst +++ b/doc/src/spot/rest.rst @@ -25,6 +25,11 @@ Spot REST :show-inheritance: :inherited-members: +.. autoclass:: kraken.spot.Earn + :members: + :show-inheritance: + :inherited-members: + .. autoclass:: kraken.spot.Staking :members: :show-inheritance: diff --git a/kraken/base_api/__init__.py b/kraken/base_api/__init__.py index d5159b07..60c8f725 100644 --- a/kraken/base_api/__init__.py +++ b/kraken/base_api/__init__.py @@ -261,7 +261,6 @@ def _request( # noqa: PLR0913 # pylint: disable=too-many-arguments if isinstance(extra_params, str) else extra_params ) - query_params: str = ( urlencode(params, doseq=True) if METHOD in {"GET", "DELETE"} and params diff --git a/kraken/exceptions/__init__.py b/kraken/exceptions/__init__.py index a3d782f8..b47b28ac 100644 --- a/kraken/exceptions/__init__.py +++ b/kraken/exceptions/__init__.py @@ -272,6 +272,46 @@ class KrakenTemporaryLockoutError(Exception): """The account was temporary locked out.""" +@docstring_message +class KrakenEarnMinimumAllocationError(Exception): + """(De)allocation operation amount less than minimum""" + + +@docstring_message +class KrakenEarnAllocationInProgressError(Exception): + """Another allocation is already in progress""" + + +@docstring_message +class KrakenEarnTemporaryUnavailableError(Exception): + """The Earn service is temporary unavailable, try again in a few minutes""" + + +@docstring_message +class KrakenEarnTierVerificationError(Exception): + """The user's tier is not high enough""" + + +@docstring_message +class KrakenEarnStrategyNotFoundError(Exception): + """Strategy not found""" + + +@docstring_message +class KrakenEarnInsufficientFundsError(Exception): + """Insufficient funds to complete the transaction""" + + +@docstring_message +class KrakenEarnAllocationExceededError(Exception): + """The allocation exceeds user limit for the strategy""" + + +@docstring_message +class KrakenEarnDeallocationExceededError(Exception): + """The deallocation exceeds user limit for the strategy""" + + @docstring_message class KrakenMaxFeeExceededError(Exception): """The fee was higher than the defined maximum.""" @@ -330,6 +370,14 @@ class MaxReconnectError(Exception): # "WDatabase:No change": , # Futures Trading Errors # + "EEarnings:Below min:(De)allocation operation amount less than minimum": KrakenEarnMinimumAllocationError, + "EEarnings:Busy:Another (de)allocation for the same strategy is in progress": KrakenEarnAllocationInProgressError, + "EEarnings:Busy": KrakenEarnTemporaryUnavailableError, + "EEarnings:Permission denied:The user's tier is not high enough": KrakenEarnTierVerificationError, + "EGeneral:Invalid arguments:Invalid strategy ID": KrakenEarnStrategyNotFoundError, + "EEarnings:Insufficient funds:Insufficient funds to complete the (de)allocation request": KrakenEarnInsufficientFundsError, + "EEarnings:Above max:The allocation exceeds user limit for the strategy": KrakenEarnAllocationExceededError, + "EEarnings:Above max:The allocation exceeds the total strategy limit": KrakenEarnDeallocationExceededError, "authenticationError": KrakenAuthenticationError, "insufficientAvailableFunds": KrakenInsufficientAvailableFundsError, "requiredArgumentMissing": KrakenRequiredArgumentMissingError, diff --git a/kraken/spot/__init__.py b/kraken/spot/__init__.py index 20fad592..9ce743f9 100644 --- a/kraken/spot/__init__.py +++ b/kraken/spot/__init__.py @@ -4,8 +4,9 @@ # GitHub: https://github.com/btschwertfeger # pylint: disable=unused-import,cyclic-import -"""Module that provides the Spot REST clients and utility functions.""" +"""Module that provides the Spot REST clients.""" +from kraken.spot.earn import Earn from kraken.spot.funding import Funding from kraken.spot.market import Market from kraken.spot.orderbook_v1 import OrderbookClientV1 @@ -17,6 +18,7 @@ from kraken.spot.websocket_v2 import KrakenSpotWSClientV2 __all__ = [ + "Earn", "Funding", "KrakenSpotWSClientV1", "KrakenSpotWSClientV2", diff --git a/kraken/spot/earn.py b/kraken/spot/earn.py new file mode 100644 index 00000000..20a79786 --- /dev/null +++ b/kraken/spot/earn.py @@ -0,0 +1,386 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2024 Benjamin Thomas Schwertfeger +# GitHub: https://github.com/btschwertfeger + + +"""Module that implements the Kraken Spot Earn client""" + +from __future__ import annotations + +from typing import Optional, TypeVar + +from kraken.base_api import KrakenSpotBaseAPI, defined + +Self = TypeVar("Self") + + +class Earn(KrakenSpotBaseAPI): + """ + + Class that implements the Kraken Spot Earn client. Currently there are no + earn endpoints that could be accesses without authentication. The earn + endpoints replace the past staking endpoints. + + - https://docs.kraken.com/rest/#tag/Earn + + :param key: Spot API public key (default: ``""``) + :type key: str, optional + :param secret: Spot API secret key (default: ``""``) + :type secret: str, optional + :param url: Alternative URL to access the Kraken API (default: + https://api.kraken.com) + :type url: str, optional + + .. code-block:: python + :linenos: + :caption: Spot Earn: Create the Earn client + + >>> from kraken.spot import Earn + >>> earn = Earn() # unauthenticated + >>> auth_earn = Earn(key="api-key", secret="secret-key") # authenticated + + .. code-block:: python + :linenos: + :caption: Spot Earn: Create the earn client as context manager + + >>> from kraken.spot import Earn + >>> with Earn(key="api-key", secret="secret-key") as earn: + ... print(earn.stake_asset(asset="XLM", amount=200, method="Lumen Staked")) + """ + + def __init__( + self, + key: str = "", + secret: str = "", + url: str = "", + ) -> None: + super().__init__(key=key, secret=secret, url=url) + + def __enter__(self: Self) -> Self: + super().__enter__() + return self + + def allocate_earn_funds( + self: Earn, + amount: str | float, + strategy_id: str, + *, + extra_params: Optional[dict] = None, + ) -> bool: + """ + Allocate funds according to the defined strategy. + + Requires the ``Earn Funds`` API key permission + + - https://docs.kraken.com/rest/#tag/Earn/operation/allocateStrategy + + :param amount: The amount to allocate + :type amount: str | float + :param strategy_id: Identifier of th chosen earn strategy (see + :func:`kraken.spot.Earn.list_earn_strategies`) + :type strategy_id: str + + .. code-block:: python + :linenos: + :caption: Spot Earn: Allocate funds + + >>> from kraken.earn import Earn + >>> earn = Earn(key="api-key", secret="secret-key") + >>> earn.allocate_earn_funds( + ... amount=2000, + ... strategy_id="ESRFUO3-Q62XD-WIOIL7" + ... ) + True + + """ + + return self._request( # type: ignore[return-value] + method="POST", + uri="/0/private/Earn/Allocate", + params={"amount": amount, "strategy_id": strategy_id}, + auth=True, + extra_params=extra_params, + ) + + def deallocate_earn_funds( + self: Earn, + amount: str | float, + strategy_id: str, + *, + extra_params: Optional[dict] = None, + ) -> bool: + """ + Deallocate funds according to the defined strategy. + + Requires the ``Earn Funds`` API key permission + + - https://docs.kraken.com/rest/#tag/Earn/operation/deallocateStrategy + + :param amount: The amount to deallocate + :type amount: str | float + :param strategy_id: Identifier of th chosen earn strategy (see + :func:`kraken.spot.Earn.list_earn_strategies`) + :type strategy_id: str + + .. code-block:: python + :linenos: + :caption: Spot Earn: Deallocate funds + + >>> from kraken.earn import Earn + >>> earn = Earn(key="api-key", secret="secret-key") + >>> earn.deallocate_earn_funds( + ... amount=2000, + ... strategy_id="ESRFUO3-Q62XD-WIOIL7" + ... ) + True + + """ + + return self._request( # type: ignore[return-value] + method="POST", + uri="/0/private/Earn/Deallocate", + params={"amount": amount, "strategy_id": strategy_id}, + auth=True, + extra_params=extra_params, + ) + + def get_allocation_status( + self: Earn, + strategy_id: str, + *, + extra_params: Optional[dict] = None, + ) -> dict: + """ + Retrieve the status of the last allocation request. + + Requires the ``Earn Funds`` or ``Query Funds`` API key permission. + + - https://docs.kraken.com/rest/#tag/Earn/operation/getAllocateStrategyStatus + + :param strategy_id: Identifier of th chosen earn strategy (see + :func:`kraken.spot.Earn.list_earn_strategies`) + :type strategy_id: str + + .. code-block:: python + :linenos: + :caption: Spot Earn: Allocation Status + + >>> from kraken.earn import Earn + >>> earn = Earn(key="api-key", secret="secret-key") + >>> earn.get_allocation_status( + ... strategy_id="ESRFUO3-Q62XD-WIOIL7" + ... ) + {'pending': False} + """ + + return self._request( # type: ignore[return-value] + method="POST", + uri="/0/private/Earn/AllocateStatus", + params={"strategy_id": strategy_id}, + auth=True, + extra_params=extra_params, + ) + + def get_deallocation_status( + self: Earn, + strategy_id: str, + *, + extra_params: Optional[dict] = None, + ) -> dict: + """ + Retrieve the status of the last deallocation request. + + Requires the ``Earn Funds`` or ``Query Funds`` API key permission. + + - https://docs.kraken.com/rest/#tag/Earn/operation/getDeallocateStrategyStatus + + :param strategy_id: Identifier of th chosen earn strategy (see + :func:`kraken.spot.Earn.list_earn_strategies`) + :type strategy_id: str + + .. code-block:: python + :linenos: + :caption: Spot Earn: Deallocation Status + + >>> from kraken.earn import Earn + >>> earn = Earn(key="api-key", secret="secret-key") + >>> earn.get_deallocation_status( + ... strategy_id="ESRFUO3-Q62XD-WIOIL7" + ... ) + {'pending': False} + """ + + return self._request( # type: ignore[return-value] + method="POST", + uri="/0/private/Earn/DeallocateStatus", + params={"strategy_id": strategy_id}, + auth=True, + extra_params=extra_params, + ) + + def list_earn_strategies( + self: Earn, + asset: Optional[str] = None, + limit: Optional[int] = None, + lock_type: Optional[list[str]] = None, + cursor: Optional[bool] = None, # noqa: FBT001 + ascending: Optional[bool] = None, # noqa: FBT001 + *, + extra_params: Optional[dict] = None, + ) -> dict: + """ + List the available earn strategies as well as additional information. + + Requires an API key but no special permission set. + + - https://docs.kraken.com/rest/#tag/Earn/operation/listStrategies + + (March 9, 2024): The endpoint is not fully implemented on the side of + Kraken. Some errors may happen. + + :param asset: Asset to filter for, defaults to None + :type asset: Optional[str], optional + :param limit: Items per page, defaults to None + :type limit: Optional[int], optional + :param lock_type: Filter strategies by lock type (``flex``, ``bounded``, + ``timed``, ``instant``), defaults to None + :type lock_type: Optional[list[str]], optional + :param cursor: Page ID, defaults to None + :type cursor: Optional[bool], optional + :param ascending: Sort ascending, defaults to False + :type ascending: bool, optional + + .. code-block:: python + :linenos: + :caption: Spot Earn: List Earn Strategies + + >>> from kraken.earn import Earn + >>> earn = Earn(key="api-key", secret="secret-key") + >>> earn.list_earn_strategies(asset="DOT") + { + "next_cursor": None, + "items": [ + { + "id": "ESMWVX6-JAPVY-23L3CV", + "asset": "DOT", + "lock_type": { + "type": "bonded", + "payout_frequency": 604800, + "bonding_period": 0, + "bonding_period_variable": False, + "bonding_rewards": False, + "unbonding_period": 2419200, + "unbonding_period_variable": False, + "unbonding_rewards": False, + "exit_queue_period": 0, + }, + "apr_estimate": {"low": "15.0000", "high": "21.0000"}, + "user_min_allocation": "0.01", + "allocation_fee": "0.0000", + "deallocation_fee": "0.0000", + "auto_compound": {"type": "enabled"}, + "yield_source": {"type": "staking"}, + "can_allocate": True, + "can_deallocate": True, + "allocation_restriction_info": [], + }, + { + "id": "ESRFUO3-Q62XD-WIOIL7", + "asset": "DOT", + "lock_type": {"type": "instant", "payout_frequency": 604800}, + "apr_estimate": {"low": "7.0000", "high": "11.0000"}, + "user_min_allocation": "0.01", + "allocation_fee": "0.0000", + "deallocation_fee": "0.0000", + "auto_compound": {"type": "enabled"}, + "yield_source": {"type": "staking"}, + "can_allocate": True, + "can_deallocate": True, + "allocation_restriction_info": [], + }, + ], + } + """ + params: dict = {} + if defined(ascending): + params["ascending"] = ascending + if defined(asset): + params["asset"] = asset + if defined(limit): + params["limit"] = limit + if defined(lock_type): + params["lock_type"] = lock_type + if defined(cursor): + params["cursor"] = cursor + + return self._request( # type: ignore[return-value] + method="POST", + uri="/0/private/Earn/Strategies", + params=params, + auth=True, + extra_params=extra_params, + ) + + def list_earn_allocations( + self: Earn, + ascending: Optional[str] = None, + hide_zero_allocations: Optional[str] = None, + converted_asset: Optional[str] = None, + *, + extra_params: Optional[dict] = None, + ) -> dict: + """ + List the user's allocations. + + Requires the ``Query Funds`` API key permission. + + - https://docs.kraken.com/rest/#tag/Earn/operation/listAllocations + + (March 9, 2024): The endpoint is not fully implemented on the side of + Kraken. Some errors may happen. + + :param ascending: Sort ascending, defaults to False + :type ascending: bool, optional + :param hide_zero_allocations: Hide past allocations without balance, + defaults to False + :type hide_zero_allocations: bool, optional + :param coverted_asset: Currency to express the value of the allocated + asset, defaults to None + :type coverted_asset: str, optional + + .. code-block:: python + :linenos: + :caption: Spot Earn: List Earn Allocations + + >>> from kraken.earn import Earn + >>> earn = Earn(key="api-key", secret="secret-key") + >>> earn.list_earn_allocations(asset="DOT") + { + "converted_asset": "USD", + "total_allocated": "49.2398", + "total_rewarded": "0.0675", + "next_cursor": "2", + "items": [{ + "strategy_id": "ESDQCOL-WTZEU-NU55QF", + "native_asset": "ETH", + "amount_allocated": {}, + "total_rewarded": {} + }] + } + """ + params: dict = {} + if defined(ascending): + params["ascending"] = ascending + if defined(hide_zero_allocations): + params["hide_zero_allocations"] = hide_zero_allocations + if defined(converted_asset): + params["converted_asset"] = converted_asset + + return self._request( # type: ignore[return-value] + method="POST", + uri="/0/private/Earn/Allocations", + params=params, + auth=True, + extra_params=extra_params, + ) diff --git a/kraken/spot/funding.py b/kraken/spot/funding.py index 87fa1e13..ce031adf 100644 --- a/kraken/spot/funding.py +++ b/kraken/spot/funding.py @@ -20,6 +20,8 @@ class Funding(KrakenSpotBaseAPI): Class that implements the Spot Funding client. Currently there are no funding endpoints that could be accesses without authentication. + - https://docs.kraken.com/rest/#tag/Funding + :param key: Spot API public key (default: ``""``) :type key: str, optional :param secret: Spot API secret key (default: ``""``) @@ -27,9 +29,6 @@ class Funding(KrakenSpotBaseAPI): :param url: Alternative URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional - :param sandbox: Use the sandbox (not supported for Spot trading so far, - default: ``False``) - :type sandbox: bool, optional .. code-block:: python :linenos: diff --git a/kraken/spot/market.py b/kraken/spot/market.py index 54047666..5cb60f0b 100644 --- a/kraken/spot/market.py +++ b/kraken/spot/market.py @@ -21,6 +21,8 @@ class Market(KrakenSpotBaseAPI): Class that implements the Kraken Spot Market client. Can be used to access the Kraken Spot market data. + - https://docs.kraken.com/rest/#tag/Spot-Market-Data + :param key: Spot API public key (default: ``""``) :type key: str, optional :param secret: Spot API secret key (default: ``""``) @@ -28,9 +30,6 @@ class Market(KrakenSpotBaseAPI): :param url: Alternative URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional - :param sandbox: Use the sandbox (not supported for Spot trading so far, - default: ``False``) - :type sandbox: bool, optional .. code-block:: python :linenos: diff --git a/kraken/spot/staking.py b/kraken/spot/staking.py index b6f91b6a..fa3a14a7 100644 --- a/kraken/spot/staking.py +++ b/kraken/spot/staking.py @@ -11,12 +11,15 @@ from typing import Optional, TypeVar from kraken.base_api import KrakenSpotBaseAPI, defined +from kraken.utils import deprecated Self = TypeVar("Self") class Staking(KrakenSpotBaseAPI): """ + .. deprecated:: v2.2.0 + Class that implements the Kraken Spot Staking client. Currently there are no staking endpoints that could be accesses without authentication. @@ -56,10 +59,12 @@ def __init__( ) -> None: super().__init__(key=key, secret=secret, url=url) + @deprecated def __enter__(self: Self) -> Self: super().__enter__() return self + @deprecated def stake_asset( self: Staking, asset: str, @@ -69,12 +74,14 @@ def stake_asset( extra_params: Optional[dict] = None, ) -> dict: """ + .. deprecated:: v2.2.0 + Stake the specified asset from the Spot wallet. Requires the ``Withdraw funds`` permission in the API key settings. Have a look at :func:`kraken.spot.Staking.list_stakeable_assets` to get - information about the stakeable assets and methods. + information about the stakable assets and methods. - https://docs.kraken.com/rest/#operation/stake @@ -108,6 +115,7 @@ def stake_asset( extra_params=extra_params, ) + @deprecated def unstake_asset( self: Staking, asset: str, @@ -117,6 +125,8 @@ def unstake_asset( extra_params: Optional[dict] = None, ) -> dict: """ + .. deprecated:: v2.2.0 + Unstake an asset and transfer the amount to the Spot wallet. Requires the ``Withdraw funds`` permission in the API key settings. @@ -160,12 +170,15 @@ def unstake_asset( extra_params=extra_params, ) + @deprecated def list_stakeable_assets( self: Staking, *, extra_params: Optional[dict] = None, ) -> list[dict]: """ + .. deprecated:: v2.2.0 + Get a list of stakeable assets. Only assets that the user is able to stake will be shown. @@ -224,12 +237,15 @@ def list_stakeable_assets( extra_params=extra_params, ) + @deprecated def get_pending_staking_transactions( self: Staking, *, extra_params: Optional[dict] = None, ) -> list[dict]: """ + .. deprecated:: v2.2.0 + Get the list of pending staking transactions of the user. Requires the ``Withdraw funds`` and ``Query funds`` API key permissions. @@ -267,12 +283,15 @@ def get_pending_staking_transactions( extra_params=extra_params, ) + @deprecated def list_staking_transactions( self: Staking, *, extra_params: Optional[dict] = None, ) -> list[dict]: """ + .. deprecated:: v2.2.0 + List the last 1000 staking transactions of the past 90 days. Requires the ``Query funds`` API key permission. diff --git a/kraken/spot/trade.py b/kraken/spot/trade.py index 5cf0df74..8202f819 100644 --- a/kraken/spot/trade.py +++ b/kraken/spot/trade.py @@ -21,7 +21,9 @@ class Trade(KrakenSpotBaseAPI): """ - Class that implements the Kraken Trade Spot client + Class that implements the Kraken Trade Spot client. + + - https://docs.kraken.com/rest/#tag/Spot-Trading :param key: Spot API public key (default: ``""``) :type key: str, optional @@ -29,8 +31,6 @@ class Trade(KrakenSpotBaseAPI): :type secret: str, optional :param url: The URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional - :param sandbox: Use the sandbox (not supported for Spot trading so far, default: ``False``) - :type sandbox: bool, optional .. code-block:: python :linenos: diff --git a/kraken/spot/user.py b/kraken/spot/user.py index 120357c7..82dca9b0 100644 --- a/kraken/spot/user.py +++ b/kraken/spot/user.py @@ -24,6 +24,9 @@ class User(KrakenSpotBaseAPI): Requires the ``Query funds`` permission in the API key settings. + - https://docs.kraken.com/rest/#tag/Account-Data + - https://docs.kraken.com/rest/#tag/Subaccounts + :param key: Spot API public key (default: ``""``) :type key: str, optional :param secret: Spot API secret key (default: ``""``) @@ -31,9 +34,6 @@ class User(KrakenSpotBaseAPI): :param url: The URL to access the Kraken API (default: https://api.kraken.com) :type url: str, optional - :param sandbox: Use the sandbox (not supported for Spot trading so far, - default: ``False``) - :type sandbox: bool, optional .. code-block:: python :linenos: diff --git a/kraken/utils/__init__.py b/kraken/utils/__init__.py new file mode 100644 index 00000000..b22a473d --- /dev/null +++ b/kraken/utils/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2024 Benjamin Thomas Schwertfeger +# GitHub: https://github.com/btschwertfeger + +"""Module providing utility functions.""" + +from kraken.utils.utils import deprecated + +__all__ = ["deprecated"] diff --git a/kraken/utils/utils.py b/kraken/utils/utils.py new file mode 100644 index 00000000..cc37d7d4 --- /dev/null +++ b/kraken/utils/utils.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2024 Benjamin Thomas Schwertfeger +# GitHub: https://github.com/btschwertfeger + +"""Module implementing utility functions used across the package""" + +from __future__ import annotations + +import warnings +from functools import wraps +from typing import Any, Callable + + +def deprecated(func: Callable) -> Callable: + """ + Function used as decorator to mark decorated functions as deprecated. + """ + + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + warnings.warn( + f"Call to deprecated function {func.__name__}.", + category=DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + +__all__ = ["deprecated"] diff --git a/pyproject.toml b/pyproject.toml index e97c1c48..1c76ed6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,7 @@ markers = [ "flaky: Flaky tests", "spot: … Spot endpoint.", "spot_auth: … authenticated Spot endpoint.", + "spot_earn: … Spot Earn endpoint.", "spot_trade: … Spot Trade endpoint.", "spot_user: … Spot User endpoint.", "spot_market: … Spot Market endpoint.", @@ -247,6 +248,8 @@ fixable = [ "PT", "ICN", "COM", + "E261", + "E231", # Missing whitespace after ':' "RSE", "PT", "FA", diff --git a/tests/spot/conftest.py b/tests/spot/conftest.py index 343abcde..3c878854 100644 --- a/tests/spot/conftest.py +++ b/tests/spot/conftest.py @@ -12,7 +12,7 @@ import pytest -from kraken.spot import Funding, Market, Staking, Trade, User +from kraken.spot import Earn, Funding, Market, Staking, Trade, User SPOT_API_KEY: str = os.getenv("SPOT_API_KEY") SPOT_SECRET_KEY: str = os.getenv("SPOT_SECRET_KEY") @@ -70,6 +70,23 @@ def spot_auth_trade() -> Trade: return Trade(key=SPOT_API_KEY, secret=SPOT_SECRET_KEY) +@pytest.fixture() +def spot_earn() -> Earn: + """ + Fixture providing an unauthenticated Spot earn client. + """ + return Earn() + + +@pytest.fixture() +def spot_auth_earn() -> Earn: # noqa: PT004 + """ + Fixture providing an authenticated Spot earn client. + """ + raise ValueError("Do not use the authenticated Spot earn client for testing!") + # return Earn(key=SPOT_API_KEY, secret=SPOT_SECRET_KEY) + + @pytest.fixture() def spot_auth_funding() -> Funding: """ diff --git a/tests/spot/test_spot_base_api.py b/tests/spot/test_spot_base_api.py index 1b13a012..34318c51 100644 --- a/tests/spot/test_spot_base_api.py +++ b/tests/spot/test_spot_base_api.py @@ -64,5 +64,9 @@ def test_spot_rest_contextmanager( # with spot_auth_staking as staking: # assert isinstance(staking.get_pending_staking_transactions(), list) + # Disabled since there is no Earn support in CI + # with spot_auth_earn as earn: + # assert isinstance(earn.list_earn_allocations(), dict) + with spot_auth_trade as trade, pytest.raises(KrakenPermissionDeniedError): trade.cancel_order(txid="OB6JJR-7NZ5P-N5SKCB") diff --git a/tests/spot/test_spot_earn.py b/tests/spot/test_spot_earn.py new file mode 100644 index 00000000..6fc46941 --- /dev/null +++ b/tests/spot/test_spot_earn.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2024 Benjamin Thomas Schwertfeger +# GitHub: https://github.com/btschwertfeger +# + +"""Module that implements the unit tests for the Spot Earn client.""" + +import pytest + +from kraken.spot import Earn + +from .helper import is_not_error + + +@pytest.mark.spot() +@pytest.mark.spot_auth() +@pytest.mark.spot_earn() +@pytest.mark.skip(reason="CI does not have earn permission") +def test_allocate_earn_funds(spot_auth_earn: Earn) -> None: + """ + Checks if the response of the ``allocate_earn_funds`` is of + type bool which mean that the request was successful. + """ + assert isinstance( + spot_auth_earn.allocate_earn_funds( + amount="1", + strategy_id="ESRFUO3-Q62XD-WIOIL7", + ), + bool, + ) + + +@pytest.mark.spot() +@pytest.mark.spot_auth() +@pytest.mark.spot_earn() +@pytest.mark.skip(reason="CI does not have earn permission") +def test_deallocate_earn_funds(spot_auth_earn: Earn) -> None: + """ + Checks if the response of the ``deallocate_earn_funds`` is of + type bool which mean that the request was successful. + """ + assert isinstance( + spot_auth_earn.deallocate_earn_funds( + amount="1", + strategy_id="ESRFUO3-Q62XD-WIOIL7", + ), + bool, + ) + + +@pytest.mark.spot() +@pytest.mark.spot_auth() +@pytest.mark.spot_earn() +@pytest.mark.skip(reason="CI does not have earn permission") +def test_get_allocation_status(spot_auth_earn: Earn) -> None: + """ + Checks if the response of the ``get_allocation_status`` does not contain a + named error which mean that the request was successful. + """ + assert is_not_error( + spot_auth_earn.get_allocation_status( + strategy_id="ESRFUO3-Q62XD-WIOIL7", + ), + ) + + +@pytest.mark.spot() +@pytest.mark.spot_auth() +@pytest.mark.spot_earn() +@pytest.mark.skip(reason="CI does not have earn permission") +def test_get_deallocation_status(spot_auth_earn: Earn) -> None: + """ + Checks if the response of the ``get_deallocation_status`` does not contain a + named error which mean that the request was successful. + """ + assert is_not_error( + spot_auth_earn.get_deallocation_status( + strategy_id="ESRFUO3-Q62XD-WIOIL7", + ), + ) + + +@pytest.mark.spot() +@pytest.mark.spot_auth() +@pytest.mark.spot_earn() +@pytest.mark.skip(reason="CI does not have earn permission") +def test_list_earn_strategies(spot_auth_earn: Earn) -> None: + """ + Checks if the response of the ``list_earn_strategies`` does not contain a + named error which mean that the request was successful. + """ + assert is_not_error(spot_auth_earn.list_earn_strategies(asset="DOT")) + + +@pytest.mark.spot() +@pytest.mark.spot_auth() +@pytest.mark.spot_earn() +@pytest.mark.skip(reason="CI does not have earn permission") +def test_list_earn_allocations(spot_auth_earn: Earn) -> None: + """ + Checks if the response of the ``list_earn_allocations`` does not contain a + named error which mean that the request was successful. + """ + assert is_not_error(spot_auth_earn.list_earn_allocations(asset="DOT")) From 83873279e8522495cb1af0705bca562b8aa82016 Mon Sep 17 00:00:00 2001 From: "Benjamin T. Schwertfeger" Date: Sat, 9 Mar 2024 12:26:35 +0100 Subject: [PATCH 4/4] Resolve "Add `ledger` parameter to `kraken.spot.User.get_trades_history`" (#195) --- kraken/spot/user.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/kraken/spot/user.py b/kraken/spot/user.py index 82dca9b0..55f2d8e6 100644 --- a/kraken/spot/user.py +++ b/kraken/spot/user.py @@ -518,7 +518,7 @@ def get_orders_info( extra_params=extra_params, ) - def get_trades_history( + def get_trades_history( # noqa: PLR0913 # pylint: disable=too-many-arguments self: User, type_: Optional[str] = "all", start: Optional[int] = None, @@ -527,6 +527,7 @@ def get_trades_history( *, trades: Optional[bool] = False, consolidate_taker: bool = True, + ledgers: bool = False, extra_params: Optional[dict] = None, ) -> dict: """ @@ -551,6 +552,9 @@ def get_trades_history( :param consolidate_taker: Consolidate trades by individual taker trades (default: ``True``) :type consolidate_taker: bool + :param ledgers: Include related leger entries for filtered trade + (default: ``False``) + :type ledgers: bool .. code-block:: python :linenos: @@ -586,6 +590,7 @@ def get_trades_history( params: dict = { "type": type_, "trades": trades, + "ledgers": ledgers, "consolidate_taker": consolidate_taker, } if defined(start):