Merge pull request #5189 from rokups/rk/custom-stake

Implement strategy-controlled stake sizes
This commit is contained in:
Matthias 2021-07-11 19:45:43 +02:00 committed by GitHub
commit 38296e8689
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 169 additions and 20 deletions

View File

@ -521,6 +521,39 @@ class AwesomeStrategy(IStrategy):
``` ```
### Stake size management
It is possible to manage your risk by reducing or increasing stake amount when placing a new trade.
```python
class AwesomeStrategy(IStrategy):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze()
if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']:
if self.config['stake_amount'] == 'unlimited':
# Use entire available wallet during favorable conditions when in compounding mode.
return max_stake
else:
# Compound profits during favorable conditions instead of using a static stake.
return self.wallets.get_total_stake_amount() / self.config['max_open_trades']
# Use default stake amount.
return proposed_stake
```
Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged.
!!! Tip
You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged.
!!! Tip
Returning `0` or `None` will prevent trades from being placed.
--- ---
## Derived strategies ## Derived strategies

View File

@ -424,12 +424,6 @@ class FreqtradeBot(LoggingMixin):
if buy and not sell: if buy and not sell:
stake_amount = self.wallets.get_trade_stake_amount(pair, 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
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ...")
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
if ((bid_check_dom.get('enabled', False)) and if ((bid_check_dom.get('enabled', False)) and
@ -488,13 +482,22 @@ class FreqtradeBot(LoggingMixin):
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested, min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
self.strategy.stoploss) self.strategy.stoploss)
if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning( if not self.edge:
f"Can't open a new trade for {pair}: stake amount " max_stake_amount = self.wallets.get_available_stake_amount()
f"is too small ({stake_amount} < {min_stake_amount})" stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
) default_retval=stake_amount)(
pair=pair, current_time=datetime.now(timezone.utc),
current_rate=buy_limit_requested, proposed_stake=stake_amount,
min_stake=min_stake_amount, max_stake=max_stake_amount)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount:
return False return False
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
f"{stake_amount} ...")
amount = stake_amount / buy_limit_requested amount = stake_amount / buy_limit_requested
order_type = self.strategy.order_types['buy'] order_type = self.strategy.order_types['buy']
if forcebuy: if forcebuy:

View File

@ -129,6 +129,8 @@ class Backtesting:
""" """
self.strategy: IStrategy = strategy self.strategy: IStrategy = strategy
strategy.dp = self.dataprovider strategy.dp = self.dataprovider
# Attach Wallets to Strategy baseclass
IStrategy.wallets = self.wallets
# Set stoploss_on_exchange to false for backtesting, # Set stoploss_on_exchange to false for backtesting,
# since a "perfect" stoploss-sell is assumed anyway # since a "perfect" stoploss-sell is assumed anyway
# And the regular "stoploss" function would not apply to that case # And the regular "stoploss" function would not apply to that case
@ -312,7 +314,18 @@ class Backtesting:
stake_amount = self.wallets.get_trade_stake_amount(pair, None) stake_amount = self.wallets.get_trade_stake_amount(pair, 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) or 0
max_stake_amount = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount)
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
if not stake_amount:
return None
order_type = self.strategy.order_types['buy'] order_type = self.strategy.order_types['buy']
time_in_force = self.strategy.order_time_in_force['sell'] time_in_force = self.strategy.order_time_in_force['sell']

View File

@ -304,6 +304,23 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return None return None
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
**kwargs) -> float:
"""
Customize stake size for each new trade. This method is not called when edge module is
enabled.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:return: A stake size, which is between min_stake and max_stake.
"""
return proposed_stake
def informative_pairs(self) -> ListPairsWithTimeframes: def informative_pairs(self) -> ListPairsWithTimeframes:
""" """
Define additional, informative pair/interval combinations to be cached from the exchange. Define additional, informative pair/interval combinations to be cached from the exchange.

View File

@ -131,7 +131,22 @@ 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, val_tied_up: float) -> float: def get_total_stake_amount(self):
"""
Return the total currently available balance in stake currency, including tied up stake and
respecting tradable_balance_ratio.
Calculated as
(<open_trade stakes> + free amount) * tradable_balance_ratio
"""
# 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
val_tied_up = Trade.total_open_trades_stakes()
available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) *
self._config['tradable_balance_ratio'])
return available_amount
def get_available_stake_amount(self) -> 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.
@ -142,9 +157,7 @@ class Wallets:
# 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.
# (tied up + current free) * ratio) - tied up # (tied up + current free) * ratio) - tied up
available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) * return self.get_total_stake_amount() - Trade.total_open_trades_stakes()
self._config['tradable_balance_ratio']) - val_tied_up
return available_amount
def _calculate_unlimited_stake_amount(self, available_amount: float, def _calculate_unlimited_stake_amount(self, available_amount: float,
val_tied_up: float) -> float: val_tied_up: float) -> float:
@ -193,7 +206,7 @@ class Wallets:
# Ensure wallets are uptodate. # Ensure wallets are uptodate.
self.update() self.update()
val_tied_up = Trade.total_open_trades_stakes() val_tied_up = Trade.total_open_trades_stakes()
available_amount = self._get_available_stake_amount(val_tied_up) available_amount = self.get_available_stake_amount()
if edge: if edge:
stake_amount = edge.stake_amount( stake_amount = edge.stake_amount(
@ -209,3 +222,23 @@ class Wallets:
available_amount, val_tied_up) available_amount, val_tied_up)
return self._check_available_stake_amount(stake_amount, available_amount) return self._check_available_stake_amount(stake_amount, available_amount)
def _validate_stake_amount(self, pair, stake_amount, min_stake_amount):
if not stake_amount:
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
return 0
max_stake_amount = self.get_available_stake_amount()
if min_stake_amount is not None and stake_amount < min_stake_amount:
stake_amount = min_stake_amount
logger.info(
f"Stake amount for pair {pair} is too small ({stake_amount} < {min_stake_amount}), "
f"adjusting to {min_stake_amount}."
)
if stake_amount > max_stake_amount:
stake_amount = max_stake_amount
logger.info(
f"Stake amount for pair {pair} is too big ({stake_amount} > {max_stake_amount}), "
f"adjusting to {max_stake_amount}."
)
return stake_amount

View File

@ -496,6 +496,17 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
trade = backtesting._enter_trade(pair, row=row) trade = backtesting._enter_trade(pair, row=row)
assert trade is not None assert trade is not None
backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5
trade = backtesting._enter_trade(pair, row=row)
assert trade
assert trade.stake_amount == 123.5
# In case of error - use proposed stake
backtesting.strategy.custom_stake_amount = lambda **kwargs: 20 / 0
trade = backtesting._enter_trade(pair, row=row)
assert trade
assert trade.stake_amount == 495
# 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)

View File

@ -886,7 +886,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
# Test not buying # Test not buying
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
freqtradebot.config['stake_amount'] = 0.0000001 freqtradebot.config['stake_amount'] = 0
patch_get_signal(freqtradebot, (True, False)) patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
pair = 'TKN/BTC' pair = 'TKN/BTC'

View File

@ -397,7 +397,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open,
def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open, def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open,
fee, mocker) -> None: fee, mocker, caplog) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
buy_mock = MagicMock(return_value=limit_buy_order_open) buy_mock = MagicMock(return_value=limit_buy_order_open)
@ -413,6 +413,27 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
assert freqtrade.create_trade('ETH/BTC')
assert log_has_re(r"Stake amount for pair .* is too small.*", caplog)
def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open,
fee, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
buy_mock = MagicMock(return_value=limit_buy_order_open)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
buy=buy_mock,
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf)
freqtrade.config['stake_amount'] = 0
patch_get_signal(freqtrade)
assert not freqtrade.create_trade('ETH/BTC') assert not freqtrade.create_trade('ETH/BTC')
@ -842,6 +863,24 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
assert trade.open_rate == 0.5 assert trade.open_rate == 0.5
assert trade.stake_amount == 40.495905365 assert trade.stake_amount == 40.495905365
# Test with custom stake
limit_buy_order['status'] = 'open'
limit_buy_order['id'] = '556'
freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0
assert freqtrade.execute_buy(pair, stake_amount)
trade = Trade.query.all()[4]
assert trade
assert trade.stake_amount == 150
# Exception case
limit_buy_order['id'] = '557'
freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0
assert freqtrade.execute_buy(pair, stake_amount)
trade = Trade.query.all()[5]
assert trade
assert trade.stake_amount == 2.0
# In case of the order is rejected and not filled at all # In case of the order is rejected and not filled at all
limit_buy_order['status'] = 'rejected' limit_buy_order['status'] = 'rejected'
limit_buy_order['amount'] = 90.99181073 limit_buy_order['amount'] = 90.99181073