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:
commit
cd41d11b85
@ -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()}'
|
||||
|
@ -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 "
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user