From 1936dd1ee8a0c3187dfec1216909c71572ae1205 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 15:45:07 +0200 Subject: [PATCH 1/6] Add test-case verifying "changing" wallet with unlimited amount --- tests/test_wallets.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 562957790..86f49698b 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -121,13 +121,14 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) -@pytest.mark.parametrize("balance_ratio,result1", [ - (1, 50), - (0.99, 49.5), - (0.50, 25), +@pytest.mark.parametrize("balance_ratio,result1,result2", [ + (1, 50, 66.66666), + (0.99, 49.5, 66.0), + (0.50, 25, 33.3333), ]) 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( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -150,7 +151,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' 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', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None @@ -159,6 +160,12 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) 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', freqtrade.get_free_open_trades()) + assert round(result, 4) == round(result2, 4) + # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) From 06d6f9ac41b1425a19062074ba6a0f945a98788f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 15:55:48 +0200 Subject: [PATCH 2/6] Fix calculation of unlimited_stake in case of modified wallet --- freqtrade/wallets.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f4432e932..889fe6fa8 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -130,14 +130,13 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: 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, respecting tradable_balance_ratio. Calculated as - ( + free amount ) * tradable_balance_ratio - + ( + free amount) * tradable_balance_ratio - """ - val_tied_up = Trade.total_open_trades_stakes() # Ensure % is used from the overall balance # Otherwise we'd risk lowering stakes with each open trade. @@ -151,12 +150,13 @@ class Wallets: Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. """ - if not free_open_trades: + if not free_open_trades or self._config['max_open_trades'] == 0: return 0 - available_amount = self._get_available_stake_amount() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) - return available_amount / free_open_trades + return (available_amount + val_tied_up) / self._config['max_open_trades'] def _check_available_stake_amount(self, stake_amount: float) -> float: """ @@ -165,7 +165,8 @@ class Wallets: :return: float: Stake amount :raise: DependencyException if balance is lower than stake-amount """ - available_amount = self._get_available_stake_amount() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) if self._config['amend_last_stake_amount']: # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio From 0233aa248e6fc9edfaa9567e7dd50941616d4d0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 17:22:16 +0200 Subject: [PATCH 3/6] Limit stake_amount to max available amount --- freqtrade/wallets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 889fe6fa8..4415e4d53 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -156,7 +156,9 @@ class Wallets: val_tied_up = Trade.total_open_trades_stakes() available_amount = self._get_available_stake_amount(val_tied_up) - return (available_amount + val_tied_up) / self._config['max_open_trades'] + # Theoretical amount can be above available amount - therefore limit to available amount! + return min((available_amount + val_tied_up) / self._config['max_open_trades'], + available_amount) def _check_available_stake_amount(self, stake_amount: float) -> float: """ From ba2d4d4656d2ecc929bd517786a2c0e325ea7c45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 19:27:21 +0200 Subject: [PATCH 4/6] Reduce number of calls to `Trade.total_open_traes_stakes()` --- freqtrade/wallets.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 4415e4d53..dba16cc35 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -145,7 +145,8 @@ class Wallets: self._config['tradable_balance_ratio']) - val_tied_up return available_amount - def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> float: + def _calculate_unlimited_stake_amount(self, free_open_trades: int, available_amount: float, + val_tied_up: float) -> float: """ Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. @@ -153,22 +154,17 @@ class Wallets: if not free_open_trades or self._config['max_open_trades'] == 0: return 0 - val_tied_up = Trade.total_open_trades_stakes() - available_amount = self._get_available_stake_amount(val_tied_up) - + 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((available_amount + val_tied_up) / self._config['max_open_trades'], - available_amount) + return min(possible_stake, available_amount) - def _check_available_stake_amount(self, stake_amount: float) -> float: + def _check_available_stake_amount(self, stake_amount: float, available_amount: float) -> float: """ Check if stake amount can be fulfilled with the available balance for the stake currency :return: float: Stake amount :raise: DependencyException if balance is lower than stake-amount """ - val_tied_up = Trade.total_open_trades_stakes() - available_amount = self._get_available_stake_amount(val_tied_up) if self._config['amend_last_stake_amount']: # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio @@ -195,17 +191,20 @@ class Wallets: stake_amount: float # Ensure wallets are uptodate. self.update() + val_tied_up = Trade.total_open_trades_stakes() + available_amount = self._get_available_stake_amount(val_tied_up) if edge: stake_amount = edge.stake_amount( pair, self.get_free(self._config['stake_currency']), self.get_total(self._config['stake_currency']), - Trade.total_open_trades_stakes() + val_tied_up ) else: stake_amount = self._config['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( + free_open_trades, available_amount, val_tied_up) - return self._check_available_stake_amount(stake_amount) + return self._check_available_stake_amount(stake_amount, available_amount) From d8c8a8d8c22923c301f8db7e1099a5422c5fe026 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 20:01:10 +0200 Subject: [PATCH 5/6] Remvoe pointless arguments from get_trade_stake_amount --- freqtrade/freqtradebot.py | 3 +-- freqtrade/optimize/backtesting.py | 8 +++----- freqtrade/rpc/rpc.py | 3 +-- freqtrade/wallets.py | 8 ++++---- tests/optimize/test_backtesting.py | 10 +++++----- tests/test_freqtradebot.py | 16 ++++++---------- tests/test_integration.py | 6 ++---- tests/test_wallets.py | 12 ++++++------ 8 files changed, 28 insertions(+), 38 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ebf28ebd..f370ff34f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -472,8 +472,7 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: - stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(), - self.edge) + stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) if not stake_amount: logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff1dd934c..71110b914 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -273,11 +273,9 @@ class Backtesting: return None - def _enter_trade(self, pair: str, row: List, max_open_trades: int, - open_trade_count: int) -> Optional[LocalTrade]: + def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: try: - stake_amount = self.wallets.get_trade_stake_amount( - pair, max_open_trades - open_trade_count, None) + stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: return None 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 row[BUY_IDX] == 1 and row[SELL_IDX] != 1 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: # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behaviour - not sure if this is correct diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b86562e80..eedb1c510 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -603,8 +603,7 @@ class RPC: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount - stakeamount = self._freqtrade.wallets.get_trade_stake_amount( - pair, self._freqtrade.get_free_open_trades()) + stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price, forcebuy=True): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index dba16cc35..bbbe5ba5e 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -145,13 +145,13 @@ class Wallets: self._config['tradable_balance_ratio']) - val_tied_up return available_amount - def _calculate_unlimited_stake_amount(self, free_open_trades: int, available_amount: float, + def _calculate_unlimited_stake_amount(self, available_amount: float, val_tied_up: float) -> float: """ Calculate stake amount for "unlimited" stake amount :return: 0 if max number of trades reached, else stake_amount to use. """ - if not free_open_trades or self._config['max_open_trades'] == 0: + if self._config['max_open_trades'] == 0: return 0 possible_stake = (available_amount + val_tied_up) / self._config['max_open_trades'] @@ -182,7 +182,7 @@ class Wallets: 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 :return: float: Stake amount @@ -205,6 +205,6 @@ class Wallets: stake_amount = self._config['stake_amount'] if stake_amount == UNLIMITED_STAKE_AMOUNT: stake_amount = self._calculate_unlimited_stake_amount( - free_open_trades, available_amount, val_tied_up) + available_amount, val_tied_up) return self._check_available_stake_amount(stake_amount, available_amount) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4bbfe8a78..41d4207c3 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -457,7 +457,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti 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 mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) @@ -474,24 +474,24 @@ def test_backtest__enter_trade(default_conf, fee, mocker, testdatadir) -> None: 0.00099, # Low 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 trade.stake_amount == 495 - trade = backtesting._enter_trade(pair, row=row, max_open_trades=2, open_trade_count=2) + trade = backtesting._enter_trade(pair, row=row) assert trade is None # Stake-amount too high! 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 # Stake-amount too high! mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", 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 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 433cce170..b0fa2d6c2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -160,8 +160,7 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: freqtrade = FreqtradeBot(default_conf) - result = freqtrade.wallets.get_trade_stake_amount( - 'ETH/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') 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: limit_buy_order_open['id'] = str(i) - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', - freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC') assert pytest.approx(result) == expected[i] freqtrade.execute_buy('ETH/BTC', result) else: with pytest.raises(DependencyException): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', - freqtrade.get_free_open_trades()) + freqtrade.wallets.get_trade_stake_amount('ETH/BTC') 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) 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( - '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: @@ -448,8 +445,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade) assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades(), - freqtrade.edge) == 0 + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, diff --git a/tests/test_integration.py b/tests/test_integration.py index 1c60faa7b..be0dd1137 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -177,8 +177,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc trades = Trade.query.all() assert len(trades) == 4 - assert freqtrade.wallets.get_trade_stake_amount( - 'XRP/BTC', freqtrade.get_free_open_trades()) == result1 + assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1 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 assert len(trades) == 4 # stake-amount should now be reduced, since one trade was sold at a loss. - assert freqtrade.wallets.get_trade_stake_amount( - 'XRP/BTC', freqtrade.get_free_open_trades()) < result1 + assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') < result1 # Validate that balance of sold trade is not in dry-run balances anymore. bals2 = freqtrade.wallets.get_all_balances() assert bals != bals2 diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 86f49698b..ff303e2ec 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -118,7 +118,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) 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,result2", [ @@ -145,28 +145,28 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r freqtrade = get_patched_freqtradebot(mocker, conf) # 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 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDT') assert result == result1 # create 2 trades, order amount should be None 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 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', freqtrade.get_free_open_trades()) + 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 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 From 92a2e254af2c0182840830f3c0f2b9ba10d566cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Apr 2021 20:17:30 +0200 Subject: [PATCH 6/6] Fix backtesting test --- tests/optimize/test_backtesting.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 41d4207c3..00114be5b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -463,6 +463,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) patch_exchange(mocker) default_conf['stake_amount'] = 'unlimited' + default_conf['max_open_trades'] = 2 backtesting = Backtesting(default_conf) pair = 'UNITTEST/BTC' row = [ @@ -478,8 +479,14 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: assert isinstance(trade, LocalTrade) assert trade.stake_amount == 495 + # 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 + LocalTrade.trades_open.pop() + trade = backtesting._enter_trade(pair, row=row) + assert trade is not None # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) @@ -487,7 +494,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: trade = backtesting._enter_trade(pair, row=row) assert trade is None - # Stake-amount too high! + # Stake-amount throwing error mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", side_effect=DependencyException)