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]) |             _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) | ||||||
|         return _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 |         Return the number of free open trades slots or 0 if | ||||||
|         max number of open trades reached |         max number of open trades reached | ||||||
| @@ -439,83 +439,6 @@ class FreqtradeBot(LoggingMixin): | |||||||
|  |  | ||||||
|         return used_rate |         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: |     def create_trade(self, pair: str) -> bool: | ||||||
|         """ |         """ | ||||||
|         Check the implemented trading strategy for buy signals. |         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) |         (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.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: |             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 | ||||||
|   | |||||||
| @@ -590,7 +590,8 @@ 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.get_trade_stake_amount(pair) |         stakeamount = self._freqtrade.wallets.get_trade_stake_amount( | ||||||
|  |             pair, self._freqtrade.get_free_open_trades()) | ||||||
|  |  | ||||||
|         # execute buy |         # execute buy | ||||||
|         if self._freqtrade.execute_buy(pair, stakeamount, price): |         if self._freqtrade.execute_buy(pair, stakeamount, price): | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ from typing import Any, Dict, NamedTuple | |||||||
|  |  | ||||||
| import arrow | import arrow | ||||||
|  |  | ||||||
|  | from freqtrade.constants import UNLIMITED_STAKE_AMOUNT | ||||||
|  | from freqtrade.exceptions import DependencyException | ||||||
| from freqtrade.exchange import Exchange | from freqtrade.exchange import Exchange | ||||||
| from freqtrade.persistence import Trade | from freqtrade.persistence import Trade | ||||||
|  |  | ||||||
| @@ -118,3 +120,79 @@ 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: | ||||||
|  |         """ | ||||||
|  |         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) |     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'] |     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: |         if expected[i] is not None: | ||||||
|             limit_buy_order_open['id'] = str(i) |             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] |             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.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: | 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) |     patch_get_signal(freqtrade) | ||||||
|  |  | ||||||
|     with pytest.raises(DependencyException, match=r'.*stake amount.*'): |     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", [ | @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) |     patch_get_signal(freqtrade) | ||||||
|  |  | ||||||
|     # no open trades, order amount should be 'balance / max_open_trades' |     # 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 |     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/BTC', result) |     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 |     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.get_trade_stake_amount('XRP/BTC') |     result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) | ||||||
|     assert result == 0 |     assert result == 0 | ||||||
|  |  | ||||||
|     # set max_open_trades = None, so do not trade |     # set max_open_trades = None, so do not trade | ||||||
|     conf['max_open_trades'] = 0 |     conf['max_open_trades'] = 0 | ||||||
|     freqtrade = FreqtradeBot(conf) |     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 |     assert result == 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -283,8 +286,10 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: | |||||||
|     edge_conf['dry_run_wallet'] = 999.9 |     edge_conf['dry_run_wallet'] = 999.9 | ||||||
|     freqtrade = FreqtradeBot(edge_conf) |     freqtrade = FreqtradeBot(edge_conf) | ||||||
|  |  | ||||||
|     assert freqtrade.get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 |     assert freqtrade.wallets.get_trade_stake_amount( | ||||||
|     assert freqtrade.get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 |         '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: | 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) |     patch_get_signal(freqtrade) | ||||||
|  |  | ||||||
|     assert not freqtrade.create_trade('ETH/BTC') |     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, | 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() |     trades = Trade.query.all() | ||||||
|     assert len(trades) == 4 |     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) |     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 |     # 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.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. |     # 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 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user