diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 63039e69a..e2819bb59 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -30,9 +30,6 @@ BAD_EXCHANGES = { "bitmex": "Various reasons", "bitstamp": "Does not provide history. " "Details in https://github.com/freqtrade/freqtrade/issues/1983", - "kraken": "TEMPORARY: Balance does not report free balance, so freqtrade will not know " - "if enough balance is available." - "Details in https://github.com/freqtrade/freqtrade/issues/1687#issuecomment-528509266" } diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 91b41a159..6d3e82eca 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -2,7 +2,11 @@ import logging from typing import Dict +import ccxt + +from freqtrade import OperationalException, TemporaryError from freqtrade.exchange import Exchange +from freqtrade.exchange.exchange import retrier logger = logging.getLogger(__name__) @@ -10,3 +14,33 @@ logger = logging.getLogger(__name__) class Kraken(Exchange): _params: Dict = {"trading_agreement": "agree"} + + @retrier + def get_balances(self) -> dict: + if self._config['dry_run']: + return {} + + try: + balances = self._api.fetch_balance() + # Remove additional info from ccxt results + balances.pop("info", None) + balances.pop("free", None) + balances.pop("total", None) + balances.pop("used", None) + + orders = self._api.fetch_open_orders() + order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1], + x["remaining"], + # Don't remove the below comment, this can be important for debuggung + # x["side"], x["amount"], + ) for x in orders] + for bal in balances: + balances[bal]['used'] = sum(order[1] for order in order_list if order[0] == bal) + balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] + + return balances + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 694a2228e..f8c556aeb 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -867,7 +867,7 @@ def test_get_balance_dry_run(default_conf, mocker): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_balance_prod(default_conf, mocker, exchange_name): api_mock = MagicMock() - api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4}}) + api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4, 'total': 123.4}}) default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) @@ -883,6 +883,7 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'): exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={})) + mocker.patch('freqtrade.exchange.Kraken.get_balances', MagicMock(return_value={})) exchange.get_balance(currency='BTC') diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index ba94f8b45..3ad62d85a 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -4,6 +4,7 @@ from random import randint from unittest.mock import MagicMock from tests.conftest import get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers def test_buy_kraken_trading_agreement(default_conf, mocker): @@ -67,3 +68,84 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] is None assert api_mock.create_order.call_args[0][5] == {'trading_agreement': 'agree'} + + +def test_get_balances_prod(default_conf, mocker): + balance_item = { + 'free': None, + 'total': 10.0, + 'used': 0.0 + } + + api_mock = MagicMock() + api_mock.fetch_balance = MagicMock(return_value={ + '1ST': balance_item.copy(), + '2ST': balance_item.copy(), + '3ST': balance_item.copy(), + '4ST': balance_item.copy(), + }) + kraken_open_orders = [{'symbol': '1ST/EUR', + 'type': 'limit', + 'side': 'sell', + 'price': 20, + 'cost': 0.0, + 'amount': 1.0, + 'filled': 0.0, + 'average': 0.0, + 'remaining': 1.0, + }, + {'status': 'open', + 'symbol': '2ST/EUR', + 'type': 'limit', + 'side': 'sell', + 'price': 20.0, + 'cost': 0.0, + 'amount': 2.0, + 'filled': 0.0, + 'average': 0.0, + 'remaining': 2.0, + }, + {'status': 'open', + 'symbol': '2ST/USD', + 'type': 'limit', + 'side': 'sell', + 'price': 20.0, + 'cost': 0.0, + 'amount': 2.0, + 'filled': 0.0, + 'average': 0.0, + 'remaining': 2.0, + }, + {'status': 'open', + 'symbol': 'BTC/3ST', + 'type': 'limit', + 'side': 'buy', + 'price': 20, + 'cost': 0.0, + 'amount': 3.0, + 'filled': 0.0, + 'average': 0.0, + 'remaining': 3.0, + }] + api_mock.fetch_open_orders = MagicMock(return_value=kraken_open_orders) + default_conf['dry_run'] = False + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") + balances = exchange.get_balances() + assert len(balances) == 4 + assert balances['1ST']['free'] == 9.0 + assert balances['1ST']['total'] == 10.0 + assert balances['1ST']['used'] == 1.0 + + assert balances['2ST']['free'] == 6.0 + assert balances['2ST']['total'] == 10.0 + assert balances['2ST']['used'] == 4.0 + + assert balances['3ST']['free'] == 7.0 + assert balances['3ST']['total'] == 10.0 + assert balances['3ST']['used'] == 3.0 + + assert balances['4ST']['free'] == 10.0 + assert balances['4ST']['total'] == 10.0 + assert balances['4ST']['used'] == 0.0 + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", + "get_balances", "fetch_balance")