Merge pull request #4775 from freqtrade/fix_wallet_unlimited

Fix wallet unlimited
This commit is contained in:
Matthias 2021-04-24 15:54:06 +02:00 committed by GitHub
commit f12e002686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 59 additions and 53 deletions

View File

@ -473,8 +473,7 @@ class FreqtradeBot(LoggingMixin):
(buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
if buy and not sell: if buy and not sell:
stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(), stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
self.edge)
if not stake_amount: if not stake_amount:
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
return False return False

View File

@ -273,11 +273,9 @@ class Backtesting:
return None return None
def _enter_trade(self, pair: str, row: List, max_open_trades: int, def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
open_trade_count: int) -> Optional[LocalTrade]:
try: try:
stake_amount = self.wallets.get_trade_stake_amount( stake_amount = self.wallets.get_trade_stake_amount(pair, None)
pair, max_open_trades - open_trade_count, None)
except DependencyException: except DependencyException:
return None return None
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05) min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, row[OPEN_IDX], -0.05)
@ -388,7 +386,7 @@ class Backtesting:
and tmp != end_date and tmp != end_date
and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and row[BUY_IDX] == 1 and row[SELL_IDX] != 1
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): and not PairLocks.is_pair_locked(pair, row[DATE_IDX])):
trade = self._enter_trade(pair, row, max_open_trades, open_trade_count_start) trade = self._enter_trade(pair, row)
if trade: if trade:
# TODO: hacky workaround to avoid opening > max_open_trades # TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behaviour - not sure if this is correct # This emulates previous behaviour - not sure if this is correct

View File

@ -607,8 +607,7 @@ class RPC:
raise RPCException(f'position for {pair} already open - id: {trade.id}') raise RPCException(f'position for {pair} already open - id: {trade.id}')
# gen stake amount # gen stake amount
stakeamount = self._freqtrade.wallets.get_trade_stake_amount( stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
pair, self._freqtrade.get_free_open_trades())
# execute buy # execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True):

View File

@ -130,14 +130,13 @@ class Wallets:
def get_all_balances(self) -> Dict[str, Any]: def get_all_balances(self) -> Dict[str, Any]:
return self._wallets return self._wallets
def _get_available_stake_amount(self) -> float: def _get_available_stake_amount(self, val_tied_up: float) -> float:
""" """
Return the total currently available balance in stake currency, Return the total currently available balance in stake currency,
respecting tradable_balance_ratio. respecting tradable_balance_ratio.
Calculated as Calculated as
(<open_trade stakes> + free amount ) * tradable_balance_ratio - <open_trade stakes> (<open_trade stakes> + free amount) * tradable_balance_ratio - <open_trade stakes>
""" """
val_tied_up = Trade.total_open_trades_stakes()
# Ensure <tradable_balance_ratio>% is used from the overall balance # Ensure <tradable_balance_ratio>% is used from the overall balance
# Otherwise we'd risk lowering stakes with each open trade. # Otherwise we'd risk lowering stakes with each open trade.
@ -146,26 +145,26 @@ class Wallets:
self._config['tradable_balance_ratio']) - val_tied_up self._config['tradable_balance_ratio']) - val_tied_up
return available_amount return available_amount
def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> float: def _calculate_unlimited_stake_amount(self, available_amount: float,
val_tied_up: float) -> float:
""" """
Calculate stake amount for "unlimited" stake amount Calculate stake amount for "unlimited" stake amount
:return: 0 if max number of trades reached, else stake_amount to use. :return: 0 if max number of trades reached, else stake_amount to use.
""" """
if not free_open_trades: if self._config['max_open_trades'] == 0:
return 0 return 0
available_amount = self._get_available_stake_amount() possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades']
# Theoretical amount can be above available amount - therefore limit to available amount!
return min(possible_stake, available_amount)
return available_amount / free_open_trades def _check_available_stake_amount(self, stake_amount: float, available_amount: float) -> float:
def _check_available_stake_amount(self, stake_amount: float) -> float:
""" """
Check if stake amount can be fulfilled with the available balance Check if stake amount can be fulfilled with the available balance
for the stake currency for the stake currency
:return: float: Stake amount :return: float: Stake amount
:raise: DependencyException if balance is lower than stake-amount :raise: DependencyException if balance is lower than stake-amount
""" """
available_amount = self._get_available_stake_amount()
if self._config['amend_last_stake_amount']: if self._config['amend_last_stake_amount']:
# Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio
@ -183,7 +182,7 @@ class Wallets:
return stake_amount return stake_amount
def get_trade_stake_amount(self, pair: str, free_open_trades: int, edge=None) -> float: def get_trade_stake_amount(self, pair: str, edge=None) -> float:
""" """
Calculate stake amount for the trade Calculate stake amount for the trade
:return: float: Stake amount :return: float: Stake amount
@ -192,17 +191,20 @@ class Wallets:
stake_amount: float stake_amount: float
# Ensure wallets are uptodate. # Ensure wallets are uptodate.
self.update() self.update()
val_tied_up = Trade.total_open_trades_stakes()
available_amount = self._get_available_stake_amount(val_tied_up)
if edge: if edge:
stake_amount = edge.stake_amount( stake_amount = edge.stake_amount(
pair, pair,
self.get_free(self._config['stake_currency']), self.get_free(self._config['stake_currency']),
self.get_total(self._config['stake_currency']), self.get_total(self._config['stake_currency']),
Trade.total_open_trades_stakes() val_tied_up
) )
else: else:
stake_amount = self._config['stake_amount'] stake_amount = self._config['stake_amount']
if stake_amount == UNLIMITED_STAKE_AMOUNT: if stake_amount == UNLIMITED_STAKE_AMOUNT:
stake_amount = self._calculate_unlimited_stake_amount(free_open_trades) stake_amount = self._calculate_unlimited_stake_amount(
available_amount, val_tied_up)
return self._check_available_stake_amount(stake_amount) return self._check_available_stake_amount(stake_amount, available_amount)

View File

@ -457,12 +457,13 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti
Backtesting(default_conf) Backtesting(default_conf)
def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
default_conf['ask_strategy']['use_sell_signal'] = False default_conf['ask_strategy']['use_sell_signal'] = False
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001)
patch_exchange(mocker) patch_exchange(mocker)
default_conf['stake_amount'] = 'unlimited' default_conf['stake_amount'] = 'unlimited'
default_conf['max_open_trades'] = 2
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
pair = 'UNITTEST/BTC' pair = 'UNITTEST/BTC'
row = [ row = [
@ -474,24 +475,30 @@ def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None:
0.00099, # Low 0.00099, # Low
0.0012, # High 0.0012, # High
] ]
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) trade = backtesting._enter_trade(pair, row=row)
assert isinstance(trade, LocalTrade) assert isinstance(trade, LocalTrade)
assert trade.stake_amount == 495 assert trade.stake_amount == 495
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2) # Fake 2 trades, so there's not enough amount for the next trade left.
LocalTrade.trades_open.append(trade)
LocalTrade.trades_open.append(trade)
trade = backtesting._enter_trade(pair, row=row)
assert trade is None assert trade is None
LocalTrade.trades_open.pop()
trade = backtesting._enter_trade(pair, row=row)
assert trade is not None
# Stake-amount too high! # Stake-amount too high!
mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0)
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) trade = backtesting._enter_trade(pair, row=row)
assert trade is None assert trade is None
# Stake-amount too high! # Stake-amount throwing error
mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount",
side_effect=DependencyException) side_effect=DependencyException)
trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=0) trade = backtesting._enter_trade(pair, row=row)
assert trade is None assert trade is None

View File

@ -160,8 +160,7 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None:
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
result = freqtrade.wallets.get_trade_stake_amount( result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
'ETH/BTC', freqtrade.get_free_open_trades())
assert result == default_conf['stake_amount'] assert result == default_conf['stake_amount']
@ -197,14 +196,12 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b
if expected[i] is not None: if expected[i] is not None:
limit_buy_order_open['id'] = str(i) limit_buy_order_open['id'] = str(i)
result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
freqtrade.get_free_open_trades())
assert pytest.approx(result) == expected[i] assert pytest.approx(result) == expected[i]
freqtrade.execute_buy('ETH/BTC', result) freqtrade.execute_buy('ETH/BTC', result)
else: else:
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
freqtrade.get_free_open_trades())
def test_edge_called_in_process(mocker, edge_conf) -> None: def test_edge_called_in_process(mocker, edge_conf) -> None:
@ -230,9 +227,9 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
freqtrade = FreqtradeBot(edge_conf) freqtrade = FreqtradeBot(edge_conf)
assert freqtrade.wallets.get_trade_stake_amount( assert freqtrade.wallets.get_trade_stake_amount(
'NEO/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 'NEO/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20
assert freqtrade.wallets.get_trade_stake_amount( assert freqtrade.wallets.get_trade_stake_amount(
'LTC/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None:
@ -448,8 +445,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open,
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
assert not freqtrade.create_trade('ETH/BTC') assert not freqtrade.create_trade('ETH/BTC')
assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades(), assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0
freqtrade.edge) == 0
def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee,

View File

@ -177,8 +177,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
trades = Trade.query.all() trades = Trade.query.all()
assert len(trades) == 4 assert len(trades) == 4
assert freqtrade.wallets.get_trade_stake_amount( assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1
'XRP/BTC', freqtrade.get_free_open_trades()) == result1
rpc._rpc_forcebuy('TKN/BTC', None) rpc._rpc_forcebuy('TKN/BTC', None)
@ -199,8 +198,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
# One trade sold # One trade sold
assert len(trades) == 4 assert len(trades) == 4
# stake-amount should now be reduced, since one trade was sold at a loss. # stake-amount should now be reduced, since one trade was sold at a loss.
assert freqtrade.wallets.get_trade_stake_amount( assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') < result1
'XRP/BTC', freqtrade.get_free_open_trades()) < result1
# Validate that balance of sold trade is not in dry-run balances anymore. # Validate that balance of sold trade is not in dry-run balances anymore.
bals2 = freqtrade.wallets.get_all_balances() bals2 = freqtrade.wallets.get_all_balances()
assert bals != bals2 assert bals != bals2

View File

@ -118,16 +118,17 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
with pytest.raises(DependencyException, match=r'.*stake amount.*'): with pytest.raises(DependencyException, match=r'.*stake amount.*'):
freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) freqtrade.wallets.get_trade_stake_amount('ETH/BTC')
@pytest.mark.parametrize("balance_ratio,result1", [ @pytest.mark.parametrize("balance_ratio,result1,result2", [
(1, 50), (1, 50, 66.66666),
(0.99, 49.5), (0.99, 49.5, 66.0),
(0.50, 25), (0.50, 25, 33.3333),
]) ])
def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1,
limit_buy_order_open, fee, mocker) -> None: result2, limit_buy_order_open,
fee, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
@ -144,22 +145,28 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r
freqtrade = get_patched_freqtradebot(mocker, conf) freqtrade = get_patched_freqtradebot(mocker, conf)
# no open trades, order amount should be 'balance / max_open_trades' # no open trades, order amount should be 'balance / max_open_trades'
result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.get_free_open_trades()) result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT')
assert result == result1 assert result == result1
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
freqtrade.execute_buy('ETH/USDT', result) freqtrade.execute_buy('ETH/USDT', result)
result = freqtrade.wallets.get_trade_stake_amount('LTC/USDDT', freqtrade.get_free_open_trades()) result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT')
assert result == result1 assert result == result1
# create 2 trades, order amount should be None # create 2 trades, order amount should be None
freqtrade.execute_buy('LTC/BTC', result) freqtrade.execute_buy('LTC/BTC', result)
result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT')
assert result == 0 assert result == 0
freqtrade.config['max_open_trades'] = 3
freqtrade.config['dry_run_wallet'] = 200
freqtrade.wallets.start_cap = 200
result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT')
assert round(result, 4) == round(result2, 4)
# set max_open_trades = None, so do not trade # set max_open_trades = None, so do not trade
freqtrade.config['max_open_trades'] = 0 freqtrade.config['max_open_trades'] = 0
result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT')
assert result == 0 assert result == 0