Repository: freqtrade/freqtrade
Pull request: Implement strategy-controlled stake sizes
Submitting initial draft of strategy-controlled stake sizes so you can get the feel of it.

What should be max stake size? Should it be `stake_amount` specified in config or entire available balance? Both have their merits i would say.

Otherwise tests pass, but i totally did not test the code so treat it as a rough sketch, a fuel for further discussion.

closes #5076

Problem statement:
Implementing Dynamic stake amount that responds to market status(momentum, volatility, volume.. etc) supporting indicator with TA
I want to implement controlling bet ratio dynamically on market status (for example bet high[90%] on high trend, bet low on minus trend[10%], or use current market volatility for betting ratio) function for freqtrade. It's basically managing dynamic staking amount on market situations.
As you guys know, **money management** is extremely important for successful system trading. 
I actually have backtested/used this logic on own my stock trading system, (trend following / (bet ratio)money management with momentum indicator) and this helped me getting profit and _not losing much_ so greatly.
This technic is so useful for **high volatile** assets such as crypto.

My idea (plus an idea from someone from discord) is def custom_stake_amount(...) in interface.py and use that instead of getting stake_amount from config, and make custom dynamic staking options for config file, and code dynamic staking logic(with TA) in dynamic_stake_logic.py. 
Another way is adding an option to get stake_ratio(0%~100%) from strategies. Maybe this can be an easier workaround for dynamic staking. Because we need to use TA for regulating trading bet ratio.

Example dynamic stake logic is this : Ticker's n-day SMA Scoring(momentum) * Volatile Ratio

Hints from issue discussion:
You may look at #3886 for a reference to this - which was an attempt to implement it.
I've now closed this as there was no progress - and too much has changed (so it would no longer apply, and would require almost everything to change) - however it might still serve as a sample ...

Based on your description - i fear you're mixing 2 things (maybe it's just unfortunate picking of words). 
One thing is defining a custom stake-amount per trade (first trade for 50$ - 2nd trade for 30$, 3rd trade for 100$), but another thing is staking (#1519 - multiple trades / entries / exits per pair/trade) - which is a whole different thing - and should clearly not be mixed into this (and i'll ignore staking for the rest of this post - and focus on position sizing).


---

one important thing about this - it must be optional - without removing / breaking the simplicity of configuration options we have at the moment (for dynamic stakes, ...) for users / strategies that don't care about this, so it either must return the standard calculation - or be explicitly enabled (like `custom_stoploss`).

As such, there can be multiple approaches to such a function in what should be provided to it - which will need good consideration.
* Run the "default" methods first and allow the function to override / change the "proposed" stake (obviously the proposed stake must be a parameter)
* completely override the stake generation option - without running the default calculations

I tend to lean towards the first option here - as i hope it'll be "more dynamic" - so you can use the freqtrade proposals for 90% of your trades, and only override for certain pairs (say lower stakes for leveraged pairs, as they have higher risk).

I think the return-value should be absolute (50$) - not a percentage - 
first of all, because you can't rely on "stake_amount" to be numeric (it can be configured as "unlimited") -  and i'm not sure how behaviour in that case should be - which would need to be considered and documented clearly (and i think that will lead to confusions)
and (maybe more importantly) because the strategy / user then won't know what the "basis" for the calculation will be (if you say 50% - will it be `stake_amount * 0.5` - or `available_amount * 0.5` - or `total stake_amount * 0.5`.
As such - if you need it as ratio - you can calculate this in the function and still return the "desired resulting value" - giving the strategy however complete control to eventually also go "all in" - which (depending on which of the 3 above approaches you use) - might not even be possible.

More importantly however is the point on what parameters are necessary / required.
Based on the above, i guess the parameters might be (probably non complete):
* pair
* current_time (for backtesting support)
* current rate (?) (not sure this is necessary / even available at this point)
* proposed stake amount (simply returning this should be the default
* available stake amount (free capital) (this is the maximum you can use)


What you use within the strategy then (momentum, volatility, gut feeling, Elon's tweets) is up to the strategy ... and not really relevant for defining the interface (obviously it shouldn't prevent it - but i think it'll be similar to `custom_stoploss` or `custom_sell` - which allow access to the dataframe through dataprovider (should that be necessary).
I believe it is clear that it should be able to a) Replace/imitate/augment anything the config file currently does (unlimited or not unlimited) b) Use TA since that's what most algorithms that need it would need it for to begin with c) Have enough inputs to accomplish that.

For that reason it should provide (additionally to the usual pair/time/rate):
* The Dataframe (obviously since it would need to do TA-based staking)
* The total slots available / and the number of slots currently in use
* The stake-currency balance remaining / and the balance in use

Having those the user can replace in a custom_stake()
* The current unlimited and non-unlimited methods (as examples or starting points)
* Build on those to unlimited+TA-based stake or non-unlimited+TA-based
* Return either a constant/absolute or a factor of the available balance

Advantages
* It can be basic (e.g. only imitate what the current config is doing) or it can be advanced (fully TA-based)
* It's relatively simple (to the user) since it fits everything in the same function
* It doesn't matter if it returns an absolute or not since all the info is input

PS I gather it may be complex in terms of backtesting performance (or hyperopt etc implementations) but it might be relatively fast since it's only needed on entries and exits and some people may not need it beyond live anyway.
We will not provide dataframe to callback methods. It's easily available through dataprovider should the user need it - but will not slow down users who want to use callbacks without needing the dataframe (and it's then aligned with the other custom methods).
I think just returning custom_stake_ratio(percentage of bet stake in the total wallet stake when buying pairs) would be enough for using money-management methods. And advantages for this will be _not hurting_ freqtrade's back-testing or other performances. And it seems to be much simpler than other solutions.

`Total wallet stake = If unlimited stake, (just wallet's total stake) / (trades that can be opened). If limited, (numeric limit value)`

`And each trade's stake becomes (total wallet stake) * (bet_ratio)`

For example this is effective method for money management. 
Score = 0
Add 0.1 to score when close price is above moving average. Repeat it for 10~19days moving average and the result score becomes the betting ratio. As you can see 0 means do not bet, and 1 means bet fully(100%), 0.5 means bet only half(50%)
This is simple and effective method which is momentum-based money managing. **This kind of so much trading strategies can be implemented for freqtrade with merely returning bet ratio from strategies.** I think heavy-complex approach is not a essential for it. 
(But also, if it is possible to implement, making strat can return staking amount would be great too.)
But maybe with percentage ratio method, we should decide whether freqtrade will give current stake ratio informations. (current bet money of full charge stake) Maybe it can effect a _little_ for freq's performance, but it gives more freedom to strats. I think it's NOT a necessary thing so if it hurts performance, we can just make it simple.

(I think complex multi enters and multi exits are not a _core_ element for strategies, and I am aware of it's a fully different thing from dynamic staking ratio. It can lessen slippages and slightly decrease risks but it would be a lot of work to implement.)
Returning the ratio will suit your particular usecase (what you have in mind), but i don't think it will generalize well.

You're using `total wallet stake` - which is pretty much undefined what this means.

Is this the total wallet the bot can use (open trades + available stake)?
is it the Available amount in stake that's currently usable?

Does it include / exclude the bot's "usable" ratio?

As said - the current rules and logics **can not be violated** (e.g. The bot will never use more than `tradable_balance_ratio` of the wallet) - and as such - your ratio approach makes it very complex (mainly complex to understand from a user's perspective), as it's unclear what the ratio will refer to - and for every usecase, a different base for the ratio might be desired.

A simple (and flexible) approach is to return the absolute stake.
Nothing prevents you from using the ratio in your strategy - and then calculate it against whatever base you see fit in your usecase - returning the amount (in stake currency) that the bot should buy.
Side note: it's true what you mentioned regarding the dataframe; I guess what I said about it is implicitly provided by default nowadays anyway; dp has it available even in backtesting too now so there's no point talking about it since **it's already there**.

Main note: returning an absolute stake is fine since the user could have as input the **remaining stake balance and the used stake balance**: though of course since the software also knows those it could be a ratio too (of either the remaining or their total).

I think it's generally important for the method to be able to both replace the current methods and build on them; hence having enough input data is important; **total/used_num_slots & remaining/used_stake & pair/market data appear enough**.
I think my disagreement with xmatthias boils down to whether the method should be a complement to the current config or a more powerful alternative since xmatthias suggested the method itself to have an input of proposed_stake and I believe that's unnecessary if the user can derive that themselves. Giving the user enough input to have all the power to both fully replace the current config methods and build on them is a more complete solution. I don't see a reason it would be a big performance impact or more complex to implement either (I wouldn't be surprised if it's simpler)
the problem with your approach is the "simple" user, who only wants to override a specific stake (say for all up/down pairs).
You're forcing the user to reimplement the logic that already exists - sure, all inputs can be given - but WHY?

The calculations for stake are not performance relevant - so providing the "proposed" stake will make it very simple - for example for the following usecase - where we're using the regular methods - but 50% stake-size.

```
def custom_stake(pair, proposed_stake, ...):
    if 'UP/' in pair or 'DOWN/' in pair:
        return proposed_stake / 2
    return proposed_stake
```

This method will not prevent ANY usecase - however give you an idea what the "regular" method would've proposed.

I find the first point (difficulty to the user) unimportant: the first implementation of the method could just come with two example functions: the first one would implement the current non-unlimited method of the current config and the second the unlimited method of the current config so their starting point is already done for them.

The issue I see with proposed_stake is that it gives me the impression that you imply you don't want to provide the slots_available/used or the balance_available/used to the user. If yes: it makes the method less powerful since it would rely on the methods of the current config to get a proposed_stake as a starting point.

Commit message: Implement strategy-controlled stake sizes. Expose `self.wallet` to a strategy.

Patch to review:
diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index b06cf3ecb9e..e2fd30575b7 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -521,6 +521,37 @@ class AwesomeStrategy(IStrategy):
 
 ```
 
+### Stake size management
+
+It is possible to manage your risk by reducing or increasing or reducing 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
+```
+
+!!! Tip
+    You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed, as 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
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 7a7371357ab..efbf15fcac6 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -424,16 +424,10 @@ def create_trade(self, pair: str) -> bool:
 
         if buy and not sell:
             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', {})
             if ((bid_check_dom.get('enabled', False)) and
-                    (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
+               (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
                 if self._check_depth_of_market_buy(pair, bid_check_dom):
                     return self.execute_buy(pair, stake_amount)
                 else:
@@ -488,13 +482,22 @@ def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = N
 
         min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested,
                                                                    self.strategy.stoploss)
-        if min_stake_amount is not None and min_stake_amount > stake_amount:
-            logger.warning(
-                f"Can't open a new trade for {pair}: stake amount "
-                f"is too small ({stake_amount} < {min_stake_amount})"
-            )
+
+        if not self.edge:
+            max_stake_amount = self.wallets.get_available_stake_amount()
+            stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
+                                                 default_retval=None)(
+                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
 
+        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
         order_type = self.strategy.order_types['buy']
         if forcebuy:
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index 8f818047d7f..885e67e0bd4 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -129,6 +129,8 @@ def _set_strategy(self, strategy: IStrategy):
         """
         self.strategy: IStrategy = strategy
         strategy.dp = self.dataprovider
+        # Attach Wallets to Strategy baseclass
+        IStrategy.wallets = self.wallets
         # Set stoploss_on_exchange to false for backtesting,
         # since a "perfect" stoploss-sell is assumed anyway
         # And the regular "stoploss" function would not apply to that case
@@ -312,7 +314,18 @@ def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
             stake_amount = self.wallets.get_trade_stake_amount(pair, None)
         except DependencyException:
             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=None)(
+            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']
         time_in_force = self.strategy.order_time_in_force['sell']
diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index 26bcb036975..f9772e1df8f 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -304,6 +304,23 @@ def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_r
         """
         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:
         """
         Define additional, informative pair/interval combinations to be cached from the exchange.
diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py
index 1b2ec4550de..42144dad73f 100644
--- a/freqtrade/wallets.py
+++ b/freqtrade/wallets.py
@@ -131,7 +131,22 @@ def update(self, require_update: bool = True) -> None:
     def get_all_balances(self) -> Dict[str, Any]:
         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,
         respecting tradable_balance_ratio.
@@ -142,9 +157,7 @@ def _get_available_stake_amount(self, val_tied_up: float) -> float:
         # 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
+        return self.get_total_stake_amount() - Trade.total_open_trades_stakes()
 
     def _calculate_unlimited_stake_amount(self, available_amount: float,
                                           val_tied_up: float) -> float:
@@ -193,7 +206,7 @@ def get_trade_stake_amount(self, pair: str, edge=None) -> float:
         # Ensure wallets are uptodate.
         self.update()
         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:
             stake_amount = edge.stake_amount(
@@ -209,3 +222,23 @@ def get_trade_stake_amount(self, pair: str, edge=None) -> float:
                     available_amount, val_tied_up)
 
         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
diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
index cc29dc1571c..0049e59bbbf 100644
--- a/tests/rpc/test_rpc.py
+++ b/tests/rpc/test_rpc.py
@@ -886,7 +886,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
 
     # Test not buying
     freqtradebot = get_patched_freqtradebot(mocker, default_conf)
-    freqtradebot.config['stake_amount'] = 0.0000001
+    freqtradebot.config['stake_amount'] = 0
     patch_get_signal(freqtradebot, (True, False))
     rpc = RPC(freqtradebot)
     pair = 'TKN/BTC'
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 99e11e89331..117589b12c6 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -413,6 +413,26 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord
 
     patch_get_signal(freqtrade)
 
+    assert freqtrade.create_trade('ETH/BTC')
+
+
+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')
 
 


Code review comments:
diff_hunk: @@ -312,7 +314,18 @@ def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]:
             stake_amount = self.wallets.get_trade_stake_amount(pair, None)
         except DependencyException:
             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=None)(
path: freqtrade/optimize/backtesting.py
text: @user1:
I would think that we'd want the `default_retval` to be `stake_amount` - not None (i've changed this in the last commit - but would want your opinion on this too).

It'd apply whenever the method called raises an error.
Current behaviour would cause it to assume min_stake_amount - which is kinda surprising behaviour.

@author:
Good catch. Yeah that would be consistent with how other parts behave.