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