Merge pull request #5189 from rokups/rk/custom-stake
Implement strategy-controlled stake sizes
This commit is contained in:
commit
38296e8689
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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']
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user