Merge pull request #4309 from freqtrade/extract_stake_amount
Move get_trade_stake_amount to wallets
This commit is contained in:
		| @@ -233,7 +233,7 @@ class FreqtradeBot(LoggingMixin): | ||||
|             _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) | ||||
|         return _whitelist | ||||
|  | ||||
|     def get_free_open_trades(self): | ||||
|     def get_free_open_trades(self) -> int: | ||||
|         """ | ||||
|         Return the number of free open trades slots or 0 if | ||||
|         max number of open trades reached | ||||
| @@ -439,83 +439,6 @@ class FreqtradeBot(LoggingMixin): | ||||
|  | ||||
|         return used_rate | ||||
|  | ||||
|     def get_trade_stake_amount(self, pair: str) -> float: | ||||
|         """ | ||||
|         Calculate stake amount for the trade | ||||
|         :return: float: Stake amount | ||||
|         :raise: DependencyException if the available stake amount is too low | ||||
|         """ | ||||
|         stake_amount: float | ||||
|         # Ensure wallets are uptodate. | ||||
|         self.wallets.update() | ||||
|  | ||||
|         if self.edge: | ||||
|             stake_amount = self.edge.stake_amount( | ||||
|                 pair, | ||||
|                 self.wallets.get_free(self.config['stake_currency']), | ||||
|                 self.wallets.get_total(self.config['stake_currency']), | ||||
|                 Trade.total_open_trades_stakes() | ||||
|             ) | ||||
|         else: | ||||
|             stake_amount = self.config['stake_amount'] | ||||
|             if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: | ||||
|                 stake_amount = self._calculate_unlimited_stake_amount() | ||||
|  | ||||
|         return self._check_available_stake_amount(stake_amount) | ||||
|  | ||||
|     def _get_available_stake_amount(self) -> float: | ||||
|         """ | ||||
|         Return the total currently available balance in stake currency, | ||||
|         respecting tradable_balance_ratio. | ||||
|         Calculated as | ||||
|         <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 | ||||
|         # Otherwise we'd risk lowering stakes with each open trade. | ||||
|         # (tied up + current free) * ratio) - tied up | ||||
|         available_amount = ((val_tied_up + self.wallets.get_free(self.config['stake_currency'])) * | ||||
|                             self.config['tradable_balance_ratio']) - val_tied_up | ||||
|         return available_amount | ||||
|  | ||||
|     def _calculate_unlimited_stake_amount(self) -> float: | ||||
|         """ | ||||
|         Calculate stake amount for "unlimited" stake amount | ||||
|         :return: 0 if max number of trades reached, else stake_amount to use. | ||||
|         """ | ||||
|         free_open_trades = self.get_free_open_trades() | ||||
|         if not free_open_trades: | ||||
|             return 0 | ||||
|  | ||||
|         available_amount = self._get_available_stake_amount() | ||||
|  | ||||
|         return available_amount / free_open_trades | ||||
|  | ||||
|     def _check_available_stake_amount(self, stake_amount: float) -> float: | ||||
|         """ | ||||
|         Check if stake amount can be fulfilled with the available balance | ||||
|         for the stake currency | ||||
|         :return: float: Stake amount | ||||
|         """ | ||||
|         available_amount = self._get_available_stake_amount() | ||||
|  | ||||
|         if self.config['amend_last_stake_amount']: | ||||
|             # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio | ||||
|             # Otherwise the remaining amount is too low to trade. | ||||
|             if available_amount > (stake_amount * self.config['last_stake_amount_min_ratio']): | ||||
|                 stake_amount = min(stake_amount, available_amount) | ||||
|             else: | ||||
|                 stake_amount = 0 | ||||
|  | ||||
|         if available_amount < stake_amount: | ||||
|             raise DependencyException( | ||||
|                 f"Available balance ({available_amount} {self.config['stake_currency']}) is " | ||||
|                 f"lower than stake amount ({stake_amount} {self.config['stake_currency']})" | ||||
|             ) | ||||
|  | ||||
|         return stake_amount | ||||
|  | ||||
|     def create_trade(self, pair: str) -> bool: | ||||
|         """ | ||||
|         Check the implemented trading strategy for buy signals. | ||||
| @@ -549,7 +472,8 @@ class FreqtradeBot(LoggingMixin): | ||||
|         (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) | ||||
|  | ||||
|         if buy and not sell: | ||||
|             stake_amount = self.get_trade_stake_amount(pair) | ||||
|             stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(), | ||||
|                                                                self.edge) | ||||
|             if not stake_amount: | ||||
|                 logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") | ||||
|                 return False | ||||
|   | ||||
| @@ -590,7 +590,8 @@ class RPC: | ||||
|             raise RPCException(f'position for {pair} already open - id: {trade.id}') | ||||
|  | ||||
|         # gen stake amount | ||||
|         stakeamount = self._freqtrade.get_trade_stake_amount(pair) | ||||
|         stakeamount = self._freqtrade.wallets.get_trade_stake_amount( | ||||
|             pair, self._freqtrade.get_free_open_trades()) | ||||
|  | ||||
|         # execute buy | ||||
|         if self._freqtrade.execute_buy(pair, stakeamount, price): | ||||
|   | ||||
| @@ -7,6 +7,8 @@ from typing import Any, Dict, NamedTuple | ||||
|  | ||||
| import arrow | ||||
|  | ||||
| from freqtrade.constants import UNLIMITED_STAKE_AMOUNT | ||||
| from freqtrade.exceptions import DependencyException | ||||
| from freqtrade.exchange import Exchange | ||||
| from freqtrade.persistence import Trade | ||||
|  | ||||
| @@ -118,3 +120,79 @@ class Wallets: | ||||
|  | ||||
|     def get_all_balances(self) -> Dict[str, Any]: | ||||
|         return self._wallets | ||||
|  | ||||
|     def _get_available_stake_amount(self) -> float: | ||||
|         """ | ||||
|         Return the total currently available balance in stake currency, | ||||
|         respecting tradable_balance_ratio. | ||||
|         Calculated as | ||||
|         (<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 | ||||
|         # Otherwise we'd risk lowering stakes with each open trade. | ||||
|         # (tied up + current free) * ratio) - tied up | ||||
|         available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) * | ||||
|                             self._config['tradable_balance_ratio']) - val_tied_up | ||||
|         return available_amount | ||||
|  | ||||
|     def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> 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: | ||||
|             return 0 | ||||
|  | ||||
|         available_amount = self._get_available_stake_amount() | ||||
|  | ||||
|         return available_amount / free_open_trades | ||||
|  | ||||
|     def _check_available_stake_amount(self, stake_amount: float) -> float: | ||||
|         """ | ||||
|         Check if stake amount can be fulfilled with the available balance | ||||
|         for the stake currency | ||||
|         :return: float: Stake amount | ||||
|         """ | ||||
|         available_amount = self._get_available_stake_amount() | ||||
|  | ||||
|         if self._config['amend_last_stake_amount']: | ||||
|             # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio | ||||
|             # Otherwise the remaining amount is too low to trade. | ||||
|             if available_amount > (stake_amount * self._config['last_stake_amount_min_ratio']): | ||||
|                 stake_amount = min(stake_amount, available_amount) | ||||
|             else: | ||||
|                 stake_amount = 0 | ||||
|  | ||||
|         if available_amount < stake_amount: | ||||
|             raise DependencyException( | ||||
|                 f"Available balance ({available_amount} {self._config['stake_currency']}) is " | ||||
|                 f"lower than stake amount ({stake_amount} {self._config['stake_currency']})" | ||||
|             ) | ||||
|  | ||||
|         return stake_amount | ||||
|  | ||||
|     def get_trade_stake_amount(self, pair: str, free_open_trades: int, edge=None) -> float: | ||||
|         """ | ||||
|         Calculate stake amount for the trade | ||||
|         :return: float: Stake amount | ||||
|         :raise: DependencyException if the available stake amount is too low | ||||
|         """ | ||||
|         stake_amount: float | ||||
|         # Ensure wallets are uptodate. | ||||
|         self.update() | ||||
|  | ||||
|         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() | ||||
|             ) | ||||
|         else: | ||||
|             stake_amount = self._config['stake_amount'] | ||||
|             if stake_amount == UNLIMITED_STAKE_AMOUNT: | ||||
|                 stake_amount = self._calculate_unlimited_stake_amount(free_open_trades) | ||||
|  | ||||
|         return self._check_available_stake_amount(stake_amount) | ||||
|   | ||||
| @@ -158,7 +158,8 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: | ||||
|  | ||||
|     freqtrade = FreqtradeBot(default_conf) | ||||
|  | ||||
|     result = freqtrade.get_trade_stake_amount('ETH/BTC') | ||||
|     result = freqtrade.wallets.get_trade_stake_amount( | ||||
|         'ETH/BTC', freqtrade.get_free_open_trades()) | ||||
|     assert result == default_conf['stake_amount'] | ||||
|  | ||||
|  | ||||
| @@ -194,12 +195,14 @@ 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.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] | ||||
|             freqtrade.execute_buy('ETH/BTC', result) | ||||
|         else: | ||||
|             with pytest.raises(DependencyException): | ||||
|                 freqtrade.get_trade_stake_amount('ETH/BTC') | ||||
|                 freqtrade.wallets.get_trade_stake_amount('ETH/BTC', | ||||
|                                                          freqtrade.get_free_open_trades()) | ||||
|  | ||||
|  | ||||
| def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: | ||||
| @@ -210,7 +213,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: | ||||
|     patch_get_signal(freqtrade) | ||||
|  | ||||
|     with pytest.raises(DependencyException, match=r'.*stake amount.*'): | ||||
|         freqtrade.get_trade_stake_amount('ETH/BTC') | ||||
|         freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize("balance_ratio,result1", [ | ||||
| @@ -239,25 +242,25 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r | ||||
|     patch_get_signal(freqtrade) | ||||
|  | ||||
|     # no open trades, order amount should be 'balance / max_open_trades' | ||||
|     result = freqtrade.get_trade_stake_amount('ETH/BTC') | ||||
|     result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) | ||||
|     assert result == result1 | ||||
|  | ||||
|     # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' | ||||
|     freqtrade.execute_buy('ETH/BTC', result) | ||||
|  | ||||
|     result = freqtrade.get_trade_stake_amount('LTC/BTC') | ||||
|     result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) | ||||
|     assert result == result1 | ||||
|  | ||||
|     # create 2 trades, order amount should be None | ||||
|     freqtrade.execute_buy('LTC/BTC', result) | ||||
|  | ||||
|     result = freqtrade.get_trade_stake_amount('XRP/BTC') | ||||
|     result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) | ||||
|     assert result == 0 | ||||
|  | ||||
|     # set max_open_trades = None, so do not trade | ||||
|     conf['max_open_trades'] = 0 | ||||
|     freqtrade = FreqtradeBot(conf) | ||||
|     result = freqtrade.get_trade_stake_amount('NEO/BTC') | ||||
|     result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) | ||||
|     assert result == 0 | ||||
|  | ||||
|  | ||||
| @@ -283,8 +286,10 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: | ||||
|     edge_conf['dry_run_wallet'] = 999.9 | ||||
|     freqtrade = FreqtradeBot(edge_conf) | ||||
|  | ||||
|     assert freqtrade.get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 | ||||
|     assert freqtrade.get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 | ||||
|     assert freqtrade.wallets.get_trade_stake_amount( | ||||
|         'NEO/BTC', freqtrade.get_free_open_trades(), 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 | ||||
|  | ||||
|  | ||||
| def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: | ||||
| @@ -500,7 +505,8 @@ 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.get_trade_stake_amount('ETH/BTC') == 0 | ||||
|     assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades(), | ||||
|                                                     freqtrade.edge) == 0 | ||||
|  | ||||
|  | ||||
| def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, | ||||
|   | ||||
| @@ -178,7 +178,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc | ||||
|  | ||||
|     trades = Trade.query.all() | ||||
|     assert len(trades) == 4 | ||||
|     assert freqtrade.get_trade_stake_amount('XRP/BTC') == result1 | ||||
|     assert freqtrade.wallets.get_trade_stake_amount( | ||||
|         'XRP/BTC', freqtrade.get_free_open_trades()) == result1 | ||||
|  | ||||
|     rpc._rpc_forcebuy('TKN/BTC', None) | ||||
|  | ||||
| @@ -199,7 +200,8 @@ 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.get_trade_stake_amount('XRP/BTC') < result1 | ||||
|     assert freqtrade.wallets.get_trade_stake_amount( | ||||
|         'XRP/BTC', freqtrade.get_free_open_trades()) < result1 | ||||
|     # Validate that balance of sold trade is not in dry-run balances anymore. | ||||
|     bals2 = freqtrade.wallets.get_all_balances() | ||||
|     assert bals != bals2 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user