Merge pull request #4309 from freqtrade/extract_stake_amount
Move get_trade_stake_amount to wallets
This commit is contained in:
commit
2c71b3b118
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user