diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 78e17359d..c7625b53c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -17,7 +17,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU decimal_to_precision) from pandas import DataFrame -from freqtrade.constants import ListPairsWithTimeframes +from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, RetryableOrderError, @@ -490,6 +490,41 @@ class Exchange: else: return 1 / pow(10, precision) + def get_min_pair_stake_amount(self, pair: str, price: float, + stoploss: float) -> Optional[float]: + try: + market = self.markets[pair] + except KeyError: + raise ValueError(f"Can't get market information for symbol {pair}") + + if 'limits' not in market: + return None + + min_stake_amounts = [] + limits = market['limits'] + if ('cost' in limits and 'min' in limits['cost'] + and limits['cost']['min'] is not None): + min_stake_amounts.append(limits['cost']['min']) + + if ('amount' in limits and 'min' in limits['amount'] + and limits['amount']['min'] is not None): + min_stake_amounts.append(limits['amount']['min'] * price) + + if not min_stake_amounts: + return None + + # reserve some percent defined in config (5% default) + stoploss + amount_reserve_percent = 1.0 - self._config.get('amount_reserve_percent', + DEFAULT_AMOUNT_RESERVE_PERCENT) + amount_reserve_percent += stoploss + # it should not be more than 50% + amount_reserve_percent = max(amount_reserve_percent, 0.5) + + # The value returned should satisfy both limits: for amount (base currency) and + # for cost (quote, stake currency), so max() is used here. + # See also #2575 at github. + return max(min_stake_amounts) / amount_reserve_percent + def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d7116834a..2656daab5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -516,40 +516,6 @@ class FreqtradeBot(LoggingMixin): return stake_amount - def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]: - try: - market = self.exchange.markets[pair] - except KeyError: - raise ValueError(f"Can't get market information for symbol {pair}") - - if 'limits' not in market: - return None - - min_stake_amounts = [] - limits = market['limits'] - if ('cost' in limits and 'min' in limits['cost'] - and limits['cost']['min'] is not None): - min_stake_amounts.append(limits['cost']['min']) - - if ('amount' in limits and 'min' in limits['amount'] - and limits['amount']['min'] is not None): - min_stake_amounts.append(limits['amount']['min'] * price) - - if not min_stake_amounts: - return None - - # reserve some percent defined in config (5% default) + stoploss - amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent', - constants.DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent += self.strategy.stoploss - # it should not be more than 50% - amount_reserve_percent = max(amount_reserve_percent, 0.5) - - # The value returned should satisfy both limits: for amount (base currency) and - # for cost (quote, stake currency), so max() is used here. - # See also #2575 at github. - return max(min_stake_amounts) / amount_reserve_percent - def create_trade(self, pair: str) -> bool: """ Check the implemented trading strategy for buy signals. @@ -646,7 +612,8 @@ class FreqtradeBot(LoggingMixin): if not buy_limit_requested: raise PricingError('Could not determine buy price.') - min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested) + min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested, + self.strategy.stoploss) if min_stake_amount is not None and min_stake_amount > stake_amount: logger.warning( f"Can't open a new trade for {pair}: stake amount " diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a35dc9da9..cd24e113e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -305,6 +305,136 @@ def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precisio assert pytest.approx(exchange.price_get_one_pip(pair, price)) == expected +def test_get_min_pair_stake_amount(mocker, default_conf) -> None: + + exchange = get_patched_exchange(mocker, default_conf, id="binance") + stoploss = -0.05 + markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + + # no pair found + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + with pytest.raises(ValueError, match=r'.*get market information.*'): + exchange.get_min_pair_stake_amount('BNB/BTC', 1, stoploss) + + # no 'limits' section + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # empty 'limits' section + markets["ETH/BTC"]["limits"] = {} + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # no cost Min + markets["ETH/BTC"]["limits"] = { + 'cost': {"min": None}, + 'amount': {} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # no amount Min + markets["ETH/BTC"]["limits"] = { + 'cost': {}, + 'amount': {"min": None} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # empty 'cost'/'amount' section + markets["ETH/BTC"]["limits"] = { + 'cost': {}, + 'amount': {} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result is None + + # min cost is set + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 2}, + 'amount': {} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) + assert result == 2 / 0.9 + + # min amount is set + markets["ETH/BTC"]["limits"] = { + 'cost': {}, + 'amount': {'min': 2} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + assert result == 2 * 2 / 0.9 + + # min amount and cost are set (cost is minimal) + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 2}, + 'amount': {'min': 2} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + assert result == max(2, 2 * 2) / 0.9 + + # min amount and cost are set (amount is minial) + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 8}, + 'amount': {'min': 2} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + assert result == max(8, 2 * 2) / 0.9 + + +def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: + exchange = get_patched_exchange(mocker, default_conf, id="binance") + stoploss = -0.05 + markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} + + # Real Binance data + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 0.0001}, + 'amount': {'min': 0.001} + } + mocker.patch( + 'freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=markets) + ) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) + + def test_set_sandbox(default_conf, mocker): """ Test working scenario diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e2b70257a..abb91d66b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -394,139 +394,6 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: assert Trade.total_open_trades_stakes() == 1.97502e-03 -def test_get_min_pair_stake_amount(mocker, default_conf) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss = -0.05 - markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} - - # no pair found - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - with pytest.raises(ValueError, match=r'.*get market information.*'): - freqtrade._get_min_pair_stake_amount('BNB/BTC', 1) - - # no 'limits' section - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # empty 'limits' section - markets["ETH/BTC"]["limits"] = {} - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # no cost Min - markets["ETH/BTC"]["limits"] = { - 'cost': {"min": None}, - 'amount': {} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # no amount Min - markets["ETH/BTC"]["limits"] = { - 'cost': {}, - 'amount': {"min": None} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # empty 'cost'/'amount' section - markets["ETH/BTC"]["limits"] = { - 'cost': {}, - 'amount': {} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result is None - - # min cost is set - markets["ETH/BTC"]["limits"] = { - 'cost': {'min': 2}, - 'amount': {} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) - assert result == 2 / 0.9 - - # min amount is set - markets["ETH/BTC"]["limits"] = { - 'cost': {}, - 'amount': {'min': 2} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == 2 * 2 / 0.9 - - # min amount and cost are set (cost is minimal) - markets["ETH/BTC"]["limits"] = { - 'cost': {'min': 2}, - 'amount': {'min': 2} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == max(2, 2 * 2) / 0.9 - - # min amount and cost are set (amount is minial) - markets["ETH/BTC"]["limits"] = { - 'cost': {'min': 8}, - 'amount': {'min': 2} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 2) - assert result == max(8, 2 * 2) / 0.9 - - -def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - freqtrade = FreqtradeBot(default_conf) - freqtrade.strategy.stoploss = -0.05 - markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} - - # Real Binance data - markets["ETH/BTC"]["limits"] = { - 'cost': {'min': 0.0001}, - 'amount': {'min': 0.001} - } - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock(return_value=markets) - ) - result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 0.020405) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) - - def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -1007,7 +874,6 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', get_buy_rate=buy_rate_mock, - _get_min_pair_stake_amount=MagicMock(return_value=1) ) buy_mm = MagicMock(return_value=limit_buy_order_open) mocker.patch.multiple( @@ -1018,6 +884,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order 'last': 0.00001172 }), buy=buy_mm, + get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, ) pair = 'ETH/BTC' @@ -1112,7 +979,6 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) - mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', get_buy_rate=MagicMock(return_value=0.11), - _get_min_pair_stake_amount=MagicMock(return_value=1) ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1122,6 +988,7 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) - 'last': 0.00001172 }), buy=MagicMock(return_value=limit_buy_order), + get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, ) stake_amount = 2