Merge pull request #4300 from freqtrade/extract_get_min_stake_amount

Extract min stake amount from bot to exchange class
This commit is contained in:
Matthias 2021-02-02 20:28:35 +01:00 committed by GitHub
commit cd41d11b85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 170 additions and 171 deletions

View File

@ -17,7 +17,7 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRU
decimal_to_precision) decimal_to_precision)
from pandas import DataFrame 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.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, RetryableOrderError, InvalidOrderException, OperationalException, RetryableOrderError,
@ -490,6 +490,41 @@ class Exchange:
else: else:
return 1 / pow(10, precision) 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, def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, params: Dict = {}) -> Dict[str, Any]: rate: float, params: Dict = {}) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{datetime.now().timestamp()}' order_id = f'dry_run_{side}_{datetime.now().timestamp()}'

View File

@ -516,40 +516,6 @@ class FreqtradeBot(LoggingMixin):
return stake_amount 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: def create_trade(self, pair: str) -> bool:
""" """
Check the implemented trading strategy for buy signals. Check the implemented trading strategy for buy signals.
@ -646,7 +612,8 @@ class FreqtradeBot(LoggingMixin):
if not buy_limit_requested: if not buy_limit_requested:
raise PricingError('Could not determine buy price.') 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: if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning( logger.warning(
f"Can't open a new trade for {pair}: stake amount " f"Can't open a new trade for {pair}: stake amount "

View File

@ -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 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): def test_set_sandbox(default_conf, mocker):
""" """
Test working scenario Test working scenario

View File

@ -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 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: def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(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( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
get_buy_rate=buy_rate_mock, get_buy_rate=buy_rate_mock,
_get_min_pair_stake_amount=MagicMock(return_value=1)
) )
buy_mm = MagicMock(return_value=limit_buy_order_open) buy_mm = MagicMock(return_value=limit_buy_order_open)
mocker.patch.multiple( mocker.patch.multiple(
@ -1018,6 +884,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
'last': 0.00001172 'last': 0.00001172
}), }),
buy=buy_mm, buy=buy_mm,
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee, get_fee=fee,
) )
pair = 'ETH/BTC' pair = 'ETH/BTC'
@ -1112,7 +979,6 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
get_buy_rate=MagicMock(return_value=0.11), get_buy_rate=MagicMock(return_value=0.11),
_get_min_pair_stake_amount=MagicMock(return_value=1)
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -1122,6 +988,7 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -
'last': 0.00001172 'last': 0.00001172
}), }),
buy=MagicMock(return_value=limit_buy_order), buy=MagicMock(return_value=limit_buy_order),
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee, get_fee=fee,
) )
stake_amount = 2 stake_amount = 2