Repository: freqtrade/freqtrade
Pull request: Hyperopt Max open trades
<!-- Thank you for sending your pull request. But first, have you included
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
-->
## Summary

This pull request is an attempt to add the ability to hyperopt `max_open_trades` and to set `max_open_trades` from the strategy.

Solve the issue: #5963

## Quick changelog

- `max_open_trades` can be set in the strategy class
- added the `trades` space to the hyperopt to optimize `max_open_trades`
- added an ovverridable `trades_space` function to customize the range of `max_open_trades` to optimize

## What's new?

- The hyperopt class has a new space (`trades_space`) used to evaluate the optimal `max_open_trades` parameter.
- Hyperopting will export save `max_open_trades` along with the other parameters.
- Loading a configuration file without `max_open_trades` will fallback to the value set in the strategy (-1 if not set).
- Setting `max_open_trades` = -1 in the strategy or in the config means that `max_open_trades` is unlimited.
- The config schema for BACKTESTING does not require `max_open_trades` anymore
- `use_max_market_positions` is ignored in hyperopt when optimizing `max_open_trades`
- Unlimited `max_open_trades` is ignored during hyperopting if `stake_amount` is set to unlimited

Problem statement:
Add ability to hyperopt max_open_trades
<!-- 
Note: this section will not show up in the issue.
Have you search for this feature before requesting it? It's highly likely that a similar request was already filed.
-->

## Describe your environment
N/A

## Describe the enhancement
Currently it seems the max_open_trades can have significant impact on profit. The only way I have been able to find a good number is to run several backtests one at a time and changing the setting and then comparing the results.
I'm wondering about the likelihood of adding this as a new hyperopt space. I realize HO is all strategy specific, and max_open_trades is bot specific, so this might be a lot of work.

Hints from issue discussion:
it actually isn't (not really, anyway) - as `max_open_trades` is a parameter to backtesting - hyperopting it should be pretty easy.

The only "real" problem i see is that `max_open_trades` is currently only configurable from within the configuration (not from the strategy) - so the configuration will (in backtesting) always overwrite what you assigned.
This can obviously be changed, but as almost everyone will have `max_open_trades` in the config (it's a must, really) - we must be clear in documenting this (and at the same time, enabling it to be set from the strategy).

I'd encourage you to try working on this yourself and creating a PR (which i can then assist  in case of problems in the PR comments).

you can take the changes done in #5360 for protection hyperopt as an example (although you'll need to ignore certain things as it also adds a new parametertype).

Commit message: max_open_trades should be an integer

Max open trades will be always an integer in the strategy (-1 for infinity), but in the config -1 will be parsed as infinity

Patch to review:
diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md
index 0dace998550..0ef4911cd14 100644
--- a/docs/advanced-hyperopt.md
+++ b/docs/advanced-hyperopt.md
@@ -75,7 +75,7 @@ This function needs to return a floating point number (`float`). Smaller numbers
 
 ## Overriding pre-defined spaces
 
-To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`), define a nested class called Hyperopt and define the required spaces as follows:
+To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_space`, `trailing_space`, `trades_space`), define a nested class called Hyperopt and define the required spaces as follows:
 
 ```python
 from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal
@@ -123,6 +123,12 @@ class MyAwesomeStrategy(IStrategy):
 
                 Categorical([True, False], name='trailing_only_offset_is_reached'),
         ]
+
+        # Define a custom max_open_trades space
+        def trades_space(self) -> List[Dimension]:
+            return [
+                Integer(1, 10, name='max_open_trades'),
+            ]
 ```
 
 !!! Note
diff --git a/docs/configuration.md b/docs/configuration.md
index 83b23425c6d..c8c87fcff05 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -134,7 +134,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
 
 |  Parameter | Description |
 |------------|-------------|
-| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade).<br> **Datatype:** Positive integer or -1.
+| `max_open_trades` | **Required.** Number of open trades your bot is allowed to have. Only one open trade per pair is possible, so the length of your pairlist is another limitation that can apply. If -1 then it is ignored (i.e. potentially unlimited open trades, limited by the pairlist). [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Positive integer or -1.
 | `stake_currency` | **Required.** Crypto-currency used for trading. <br> **Datatype:** String
 | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). <br> **Datatype:** Positive float or `"unlimited"`.
 | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade). <br>*Defaults to `0.99` 99%).*<br> **Datatype:** Positive float between `0.1` and `1.0`.
@@ -263,6 +263,7 @@ Values set in the configuration file always overwrite values set in the strategy
 * `minimal_roi`
 * `timeframe`
 * `stoploss`
+* `max_open_trades`
 * `trailing_stop`
 * `trailing_stop_positive`
 * `trailing_stop_positive_offset`
diff --git a/docs/hyperopt.md b/docs/hyperopt.md
index e72b850ca6e..c105dceedcc 100644
--- a/docs/hyperopt.md
+++ b/docs/hyperopt.md
@@ -50,7 +50,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
                           [--eps] [--dmmp] [--enable-protections]
                           [--dry-run-wallet DRY_RUN_WALLET]
                           [--timeframe-detail TIMEFRAME_DETAIL] [-e INT]
-                          [--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]]
+                          [--spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]]
                           [--print-all] [--no-color] [--print-json] [-j JOBS]
                           [--random-state INT] [--min-trades INT]
                           [--hyperopt-loss NAME] [--disable-param-export]
@@ -96,7 +96,7 @@ optional arguments:
                         Specify detail timeframe for backtesting (`1m`, `5m`,
                         `30m`, `1h`, `1d`).
   -e INT, --epochs INT  Specify number of epochs (default: 100).
-  --spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]
+  --spaces {all,buy,sell,roi,stoploss,trailing,protection,trades,default} [{all,buy,sell,roi,stoploss,trailing,protection,trades,default} ...]
                         Specify which parameters to hyperopt. Space-separated
                         list.
   --print-all           Print all results, not only the best ones.
@@ -180,6 +180,7 @@ Rarely you may also need to create a [nested class](advanced-hyperopt.md#overrid
 * `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps)
 * `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
 * `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default)
+* `trades_space` - for custom max_open_trades optimization (if you need the ranges for the max_open_trades parameter in the optimization hyperspace that differ from default)
 
 !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
     You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy.
@@ -643,6 +644,7 @@ Legal values are:
 * `roi`: just optimize the minimal profit table for your strategy
 * `stoploss`: search for the best stoploss value
 * `trailing`: search for the best trailing stop values
+* `trades`: search for the best max open trades values
 * `protection`: search for the best protection parameters (read the [protections section](#optimizing-protections) on how to properly define these)
 * `default`: `all` except `trailing` and `protection`
 * space-separated list of any of the above values for example `--spaces roi stoploss`
@@ -916,5 +918,5 @@ Once the optimized strategy has been implemented into your strategy, you should
 To achieve same the results (number of trades, their durations, profit, etc.) as during Hyperopt, please use the same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
 
 Should results not match, please double-check to make sure you transferred all conditions correctly.
-Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy.
-You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`).
+Pay special care to the stoploss, max_open_trades and trailing stoploss parameters, as these are often set in configuration files, which override changes to the strategy.
+You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss`, `max_open_trades` or `trailing_stop`).
diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py
index 91ac1636590..c7007358209 100644
--- a/freqtrade/commands/cli_options.py
+++ b/freqtrade/commands/cli_options.py
@@ -251,7 +251,8 @@ def __init__(self, *args, **kwargs):
     "spaces": Arg(
         '--spaces',
         help='Specify which parameters to hyperopt. Space-separated list.',
-        choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
+        choices=['all', 'buy', 'sell', 'roi', 'stoploss',
+                 'trailing', 'protection', 'default', 'trades'],
         nargs='+',
         default='default',
     ),
diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index 3973672169a..95efa63b8a0 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -636,7 +636,6 @@
 
 SCHEMA_BACKTEST_REQUIRED = [
     'exchange',
-    'max_open_trades',
     'stake_currency',
     'stake_amount',
     'dry_run_wallet',
@@ -646,6 +645,7 @@
 SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
     'stoploss',
     'minimal_roi',
+    'max_open_trades'
 ]
 
 SCHEMA_MINIMAL_REQUIRED = [
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 258a4500823..779c134bdbb 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -520,7 +520,8 @@ def create_trade(self, pair: str) -> bool:
                 else:
                     self.log_once(f"Pair {pair} is currently locked.", logger.info)
                 return False
-            stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
+            stake_amount = self.wallets.get_trade_stake_amount(
+                pair, self.edge)
 
             bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
             if ((bid_check_dom.get('enabled', False)) and
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index 2b8b96cba9b..181a5b580ca 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -1051,7 +1051,8 @@ def validate_row(
 
     def backtest_loop(
             self, row: Tuple, pair: str, current_time: datetime, end_date: datetime,
-            max_open_trades: int, open_trade_count_start: int, is_first: bool = True) -> int:
+            max_open_trades: int,
+            open_trade_count_start: int, is_first: bool = True) -> int:
         """
         NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
 
@@ -1227,7 +1228,8 @@ def backtest_one_strategy(self, strat: IStrategy, data: Dict[str, DataFrame],
         # Use max_open_trades in backtesting, except --disable-max-market-positions is set
         if self.config.get('use_max_market_positions', True):
             # Must come from strategy config, as the strategy may modify this setting.
-            max_open_trades = self.strategy.config['max_open_trades']
+            max_open_trades = self.strategy.config['max_open_trades'] \
+                if self.strategy.config['max_open_trades'] != float('inf') else -1
         else:
             logger.info(
                 'Ignoring max_open_trades (--disable-max-market-positions was used) ...')
diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index b459d59f27c..4ab9b1a5d39 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -74,6 +74,7 @@ def __init__(self, config: Config) -> None:
         self.roi_space: List[Dimension] = []
         self.stoploss_space: List[Dimension] = []
         self.trailing_space: List[Dimension] = []
+        self.trades_space: List[Dimension] = []
         self.dimensions: List[Dimension] = []
 
         self.config = config
@@ -118,11 +119,14 @@ def __init__(self, config: Config) -> None:
 
         # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
         if self.config.get('use_max_market_positions', True):
-            self.max_open_trades = self.config['max_open_trades']
+            self.max_open_trades = self.config['max_open_trades'] \
+                if self.config['max_open_trades'] != float('inf') else -1
         else:
             logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
             self.max_open_trades = 0
 
+        print("Strategy max open trades", self.max_open_trades)
+
         if HyperoptTools.has_space(self.config, 'sell'):
             # Make sure use_exit_signal is enabled
             self.config['use_exit_signal'] = True
@@ -209,6 +213,8 @@ def _get_params_details(self, params: Dict) -> Dict:
             result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space}
         if HyperoptTools.has_space(self.config, 'trailing'):
             result['trailing'] = self.custom_hyperopt.generate_trailing_params(params)
+        if HyperoptTools.has_space(self.config, 'trades'):
+            result['max_open_trades'] = {p.name: params.get(p.name) for p in self.trades_space}
 
         return result
 
@@ -229,6 +235,8 @@ def _get_no_optimize_details(self) -> Dict[str, Any]:
                 'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
                 'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached,
             }
+        if not HyperoptTools.has_space(self.config, 'trades'):
+            result['max_open_trades'] = {'max_open_trades': strategy.max_open_trades}
         return result
 
     def print_results(self, results) -> None:
@@ -280,8 +288,13 @@ def init_spaces(self):
             logger.debug("Hyperopt has 'trailing' space")
             self.trailing_space = self.custom_hyperopt.trailing_space()
 
+        if HyperoptTools.has_space(self.config, 'trades'):
+            logger.debug("Hyperopt has 'trades' space")
+            self.trades_space = self.custom_hyperopt.trades_space()
+
         self.dimensions = (self.buy_space + self.sell_space + self.protection_space
-                           + self.roi_space + self.stoploss_space + self.trailing_space)
+                           + self.roi_space + self.stoploss_space + self.trailing_space
+                           + self.trades_space)
 
     def assign_params(self, params_dict: Dict, category: str) -> None:
         """
@@ -328,6 +341,9 @@ def generate_optimizer(self, raw_params: List[Any]) -> Dict[str, Any]:
             self.backtesting.strategy.trailing_only_offset_is_reached = \
                 d['trailing_only_offset_is_reached']
 
+        if HyperoptTools.has_space(self.config, 'trades'):
+            self.max_open_trades = params_dict['max_open_trades']
+
         with self.data_pickle_file.open('rb') as f:
             processed = load(f, mmap_mode='r')
             if self.analyze_per_epoch:
diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py
index 5bc0af42b35..0627b52c3d4 100644
--- a/freqtrade/optimize/hyperopt_auto.py
+++ b/freqtrade/optimize/hyperopt_auto.py
@@ -91,5 +91,8 @@ def generate_trailing_params(self, params: Dict) -> Dict:
     def trailing_space(self) -> List['Dimension']:
         return self._get_func('trailing_space')()
 
+    def trades_space(self) -> List['Dimension']:
+        return self._get_func('trades_space')()
+
     def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType:
         return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs)
diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py
index a7c64ffb0af..b692abea4ec 100644
--- a/freqtrade/optimize/hyperopt_interface.py
+++ b/freqtrade/optimize/hyperopt_interface.py
@@ -191,6 +191,16 @@ def trailing_space(self) -> List[Dimension]:
             Categorical([True, False], name='trailing_only_offset_is_reached'),
         ]
 
+    def trades_space(self) -> List[Dimension]:
+        """
+        Create a max open trades space.
+
+        You may override it in your custom Hyperopt class.
+        """
+        return [
+            Integer(1, 10, name='max_open_trades'),
+        ]
+
     # This is needed for proper unpickling the class attribute timeframe
     # which is set to the actual value by the resolver.
     # Why do I still need such shamanic mantras in modern python?
diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py
index 7007ec55e78..6c16100d395 100755
--- a/freqtrade/optimize/hyperopt_tools.py
+++ b/freqtrade/optimize/hyperopt_tools.py
@@ -96,7 +96,7 @@ def has_space(config: Config, space: str) -> bool:
         Tell if the space value is contained in the configuration
         """
         # 'trailing' and 'protection spaces are not included in the 'default' set of spaces
-        if space in ('trailing', 'protection'):
+        if space in ('trailing', 'protection', 'trades'):
             return any(s in config['spaces'] for s in [space, 'all'])
         else:
             return any(s in config['spaces'] for s in [space, 'all', 'default'])
@@ -187,7 +187,8 @@ def show_epoch_details(results, total_epochs: int, print_json: bool,
 
         if print_json:
             result_dict: Dict = {}
-            for s in ['buy', 'sell', 'protection', 'roi', 'stoploss', 'trailing']:
+            for s in ['buy', 'sell', 'protection',
+                      'roi', 'stoploss', 'trailing', 'max_open_trades']:
                 HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
             print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
 
@@ -201,6 +202,8 @@ def show_epoch_details(results, total_epochs: int, print_json: bool,
             HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
             HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", non_optimized)
             HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:", non_optimized)
+            HyperoptTools._params_pretty_print(
+                params, 'max_open_trades', "Max Open Trades:", non_optimized)
 
     @staticmethod
     def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
@@ -239,7 +242,9 @@ def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> N
             if space == "stoploss":
                 stoploss = safe_value_fallback2(space_params, no_params, space, space)
                 result += (f"stoploss = {stoploss}{appendix}")
-
+            elif space == "max_open_trades":
+                max_open_trades = safe_value_fallback2(space_params, no_params, space, space)
+                result += (f"max_open_trades = {max_open_trades}{appendix}")
             elif space == "roi":
                 result = result[:-1] + f'{appendix}\n'
                 minimal_roi_result = rapidjson.dumps({
diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py
index 67df49dcb36..febda782223 100644
--- a/freqtrade/resolvers/strategy_resolver.py
+++ b/freqtrade/resolvers/strategy_resolver.py
@@ -76,6 +76,7 @@ def load_strategy(config: Config = None) -> IStrategy:
                       ("ignore_buying_expired_candle_after",  0),
                       ("position_adjustment_enable",      False),
                       ("max_entry_position_adjustment",      -1),
+                      ("max_open_trades",                    -1)
                       ]
         for attribute, default in attributes:
             StrategyResolver._override_attribute_helper(strategy, config,
@@ -103,14 +104,22 @@ def _override_attribute_helper(strategy, config: Config, attribute: str, default
         if (attribute in config
                 and not isinstance(getattr(type(strategy), attribute, None), property)):
             # Ensure Properties are not overwritten
-            setattr(strategy, attribute, config[attribute])
+            val = config[attribute]
+            # max_open_trades set to float('inf') in the config will be copied as -1 in the strategy
+            if attribute == 'max_open_trades' and val == float('inf'):
+                val = -1
+            setattr(strategy, attribute, val)
             logger.info("Override strategy '%s' with value in config file: %s.",
                         attribute, config[attribute])
         elif hasattr(strategy, attribute):
             val = getattr(strategy, attribute)
             # None's cannot exist in the config, so do not copy them
             if val is not None:
-                config[attribute] = val
+                # max_open_trades set to -1 in the strategy will be copied as infinity in the config
+                if attribute == 'max_open_trades' and val == -1:
+                    config[attribute] = float('inf')
+                else:
+                    config[attribute] = val
         # Explicitly check for None here as other "falsy" values are possible
         elif default is not None:
             setattr(strategy, attribute, default)
diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py
index 6f62c9d3d6a..4dac4154f66 100644
--- a/freqtrade/strategy/hyper.py
+++ b/freqtrade/strategy/hyper.py
@@ -80,6 +80,8 @@ def ft_load_params_from_file(self) -> None:
 
             self.stoploss = params.get('stoploss', {}).get(
                 'stoploss', getattr(self, 'stoploss', -0.1))
+            self.max_open_trades = params.get('max_open_trades', {}).get(
+                'max_open_trades', getattr(self, 'max_open_trades', -1))
             trailing = params.get('trailing', {})
             self.trailing_stop = trailing.get(
                 'trailing_stop', getattr(self, 'trailing_stop', False))
diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index 781ae6c5c67..d1b9a849861 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -54,6 +54,9 @@ class IStrategy(ABC, HyperStrategyMixin):
     # associated stoploss
     stoploss: float
 
+    # max open trades for the strategy
+    max_open_trades:  int
+
     # trailing stoploss
     trailing_stop: bool = False
     trailing_stop_positive: Optional[float] = None
diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
index 5bce9f419e6..8b57883f777 100644
--- a/tests/optimize/test_hyperopt.py
+++ b/tests/optimize/test_hyperopt.py
@@ -292,6 +292,8 @@ def test_params_no_optimize_details(hyperopt) -> None:
     assert res['roi']['0'] == 0.04
     assert "stoploss" in res
     assert res['stoploss']['stoploss'] == -0.1
+    assert "max_open_trades" in res
+    assert res['max_open_trades']['max_open_trades'] == 1
 
 
 def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
@@ -474,6 +476,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
         'trailing_stop_positive': 0.02,
         'trailing_stop_positive_offset_p1': 0.05,
         'trailing_only_offset_is_reached': False,
+        'max_open_trades': 3,
     }
     response_expected = {
         'loss': 1.9147239021396234,
@@ -499,7 +502,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
                            'trailing': {'trailing_only_offset_is_reached': False,
                                         'trailing_stop': True,
                                         'trailing_stop_positive': 0.02,
-                                        'trailing_stop_positive_offset': 0.07}},
+                                        'trailing_stop_positive_offset': 0.07},
+                           'max_open_trades': {'max_open_trades': 3}
+                           },
         'params_dict': optimizer_param,
         'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}},
         'results_metrics': ANY,
@@ -548,7 +553,8 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
                 'buy': {'mfi-value': None},
                 'sell': {'sell-mfi-value': None},
                 'roi': {}, 'stoploss': {'stoploss': None},
-                'trailing': {'trailing_stop': None}
+                'trailing': {'trailing_stop': None},
+                'max_open_trades': {'max_open_trades': None}
             },
             'results_metrics': generate_result_metrics(),
         }])
@@ -571,7 +577,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
     out, err = capsys.readouterr()
     result_str = (
         '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi"'
-        ':{},"stoploss":null,"trailing_stop":null}'
+        ':{},"stoploss":null,"trailing_stop":null,"max_open_trades":null}'
     )
     assert result_str in out  # noqa: E501
     # Should be called for historical candle data
@@ -874,6 +880,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
     assert hyperopt.backtesting.strategy.buy_rsi.value == 35
     assert hyperopt.backtesting.strategy.sell_rsi.value == 74
     assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
+    assert hyperopt.max_open_trades == 1
     buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
     assert isinstance(buy_rsi_range, range)
     # Range from 0 - 50 (inclusive)
@@ -884,6 +891,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
     assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value != 30
     assert hyperopt.backtesting.strategy.buy_rsi.value != 35
     assert hyperopt.backtesting.strategy.sell_rsi.value != 74
+    assert hyperopt.max_open_trades != 1
 
     hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1'
     with pytest.raises(OperationalException, match="Estimator ET1 not supported."):
diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py
index 7d4fef3bd71..eace78eee7e 100644
--- a/tests/optimize/test_hyperopt_tools.py
+++ b/tests/optimize/test_hyperopt_tools.py
@@ -66,52 +66,58 @@ def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
 @pytest.mark.parametrize("spaces, expected_results", [
     (['buy'],
      {'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['sell'],
      {'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['roi'],
      {'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['stoploss'],
      {'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['trailing'],
      {'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['buy', 'sell', 'roi', 'stoploss'],
      {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['buy', 'sell', 'roi', 'stoploss', 'trailing'],
      {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['buy', 'roi'],
      {'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['all'],
      {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
-      'protection': True}),
+      'protection': True, 'trades': True}),
     (['default'],
      {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['default', 'trailing'],
      {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['all', 'buy'],
      {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
-      'protection': True}),
+      'protection': True, 'trades': True}),
     (['default', 'buy'],
      {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
-      'protection': False}),
+      'protection': False, 'trades': False}),
     (['all'],
      {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
-      'protection': True}),
+      'protection': True, 'trades': True}),
     (['protection'],
      {'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
-      'protection': True}),
+      'protection': True, 'trades': False}),
+    (['trades'],
+     {'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
+      'protection': False, 'trades': True}),
+    (['default', 'trades'],
+     {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
+      'protection': False, 'trades': True}),
 ])
 def test_has_space(hyperopt_conf, spaces, expected_results):
-    for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection']:
+    for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'trades']:
         hyperopt_conf.update({'spaces': spaces})
         assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
 
@@ -193,6 +199,9 @@ def test_export_params(tmpdir):
                 "346": 0.08499,
                 "507": 0.049,
                 "1595": 0
+            },
+            "max_open_trades": {
+                "max_open_trades": 5
             }
         },
         "params_not_optimized": {
@@ -219,6 +228,7 @@ def test_export_params(tmpdir):
     assert "roi" in content["params"]
     assert "stoploss" in content["params"]
     assert "trailing" in content["params"]
+    assert "max_open_trades" in content["params"]
 
 
 def test_try_export_params(default_conf, tmpdir, caplog, mocker):
@@ -297,6 +307,9 @@ def test_params_print(capsys):
             "trailing_stop_positive_offset": 0.1,
             "trailing_only_offset_is_reached": True
         },
+        "max_open_trades": {
+            "max_open_trades": 5
+        }
 
     }
     HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized)
@@ -327,6 +340,13 @@ def test_params_print(capsys):
     assert re.search('trailing_stop_positive_offset = 0.1  # value loaded.*\n', captured.out)
     assert re.search('trailing_only_offset_is_reached = True  # value loaded.*\n', captured.out)
 
+    HyperoptTools._params_pretty_print(
+                params, 'max_open_trades', "Max Open Trades:", non_optimized)
+    captured = capsys.readouterr()
+
+    assert re.search("# Max Open Trades:", captured.out)
+    assert re.search('max_open_trades = 5  # value loaded.*\n', captured.out)
+
 
 def test_hyperopt_serializer():
 
diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py
index 088ab21d41b..6f5ff573bb5 100644
--- a/tests/strategy/strats/strategy_test_v3.py
+++ b/tests/strategy/strats/strategy_test_v3.py
@@ -30,6 +30,9 @@ class StrategyTestV3(IStrategy):
         "0": 0.04
     }
 
+    # Optimal max_open_trades for the strategy
+    max_open_trades = -1
+
     # Optimal stoploss designed for the strategy
     stoploss = -0.10
 
diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py
index 5fcc75026e5..2296d4bc63c 100644
--- a/tests/strategy/test_strategy_loading.py
+++ b/tests/strategy/test_strategy_loading.py
@@ -175,6 +175,18 @@ def test_strategy_override_stoploss(caplog, default_conf):
     assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
 
 
+def test_strategy_override_max_open_trades(caplog, default_conf):
+    caplog.set_level(logging.INFO)
+    default_conf.update({
+        'strategy': CURRENT_TEST_STRATEGY,
+        'max_open_trades': 7
+    })
+    strategy = StrategyResolver.load_strategy(default_conf)
+
+    assert strategy.max_open_trades == 7
+    assert log_has("Override strategy 'max_open_trades' with value in config file: 7.", caplog)
+
+
 def test_strategy_override_trailing_stop(caplog, default_conf):
     caplog.set_level(logging.INFO)
     default_conf.update({
@@ -349,6 +361,32 @@ def test_strategy_override_use_exit_profit_only(caplog, default_conf):
     assert log_has("Override strategy 'exit_profit_only' with value in config file: True.", caplog)
 
 
+def test_strategy_max_open_trades_infinity_from_strategy(caplog, default_conf):
+    caplog.set_level(logging.INFO)
+    default_conf.update({
+        'strategy': CURRENT_TEST_STRATEGY,
+    })
+    del default_conf['max_open_trades']
+
+    strategy = StrategyResolver.load_strategy(default_conf)
+
+    # this test assumes -1 set to 'max_open_trades' in CURRENT_TEST_STRATEGY
+    assert strategy.max_open_trades == -1
+    assert default_conf['max_open_trades'] == float('inf')
+
+
+def test_strategy_max_open_trades_infinity_from_config(caplog, default_conf):
+    caplog.set_level(logging.INFO)
+    default_conf.update({
+        'strategy': CURRENT_TEST_STRATEGY,
+        'max_open_trades': float('inf')
+    })
+
+    strategy = StrategyResolver.load_strategy(default_conf)
+
+    assert strategy.max_open_trades == -1
+
+
 @ pytest.mark.filterwarnings("ignore:deprecated")
 def test_missing_implements(default_conf, caplog):
 


Code review comments:
diff_hunk: @@ -328,6 +341,9 @@ def generate_optimizer(self, raw_params: List[Any]) -> Dict[str, Any]:
             self.backtesting.strategy.trailing_only_offset_is_reached = \
                 d['trailing_only_offset_is_reached']
 
+        if HyperoptTools.has_space(self.config, 'trades'):
+            self.max_open_trades = params_dict['max_open_trades']
path: freqtrade/optimize/hyperopt.py
text: @user1:
are you sure this works throughout the bot in all cases?

the strategy is not reloaded on every hyperopt loop - so the sync between parameter and config will not happen implicitly (and is also not necessary for all other attributes).

`config['max_open_trades']` is used throughout the bot - for example in `wallets.py` to calculate dynamic stakes.
So while this will hyperopt max_open_trades - the stake-amount will remain identical 

example with a 1000 USDT wallet, unlimited stake, and an initial value of 3 (from the config).
with that we get 1000/3 (roughly 333$)$ trades.
If the value is changed to 1 - we'd expect the stake size to be 1000$ (all in on every trade), or a 100$ stake if the trade-amount is bumped to 10.

In my tests, the first stake was always 329.73 - independent of the actually selected trade count - so if i'd start with a 1 max_open_trades value - i'd never get different results - as the stake-amount won't change - implicitly also maxing out all trade slots..

@author:
Sorry for replying late. Yes, I've realized that `max_open_trades` is accessed throughout the code only by reading at the config. I need to make sure that for backtesting the `max_open_trades` is only the one passed to the `backtest` function, and there's no path leading to read it from the config.
If it is not the case (as it seems) I will try to see what will be a more elegant solution of the two:
1. Passing `max_open_trades` to all the functions needing it called from the `backtest` function
2. Provide a clone of the config with a different `max_open_trades` for each backtesting epoch.

What do you think about it? 
I will also try to cover those cases with some tests. 
Thanks for the help. :D 

@user1:
> Sorry for replying late

don't think about it that way - that's ... just wrong :)  2 days is NOT late.
it's not our job - so having other priorities in life is perfectly fine. I can't guarantee a review within a day (or whatever), either.

---

i think option 2 will be preferable, at least short-term, otherwise you end up passing `strategy.max_open_trades` around to tons of places.
You'll struggle with option 1 if you attempt to do this "partially" - as there's a ton of "call from a call" scenarios, so even if a function is not directly used in backtesting - it may still be called implicitly (Like the functions in wallets, for example).

in theory, it SHOULD work without leaking to the other epochs in that batch (without copying the config) as hyperopt pickles/unpickles the objects, so it's technically different object id's in each epoch - though i didn't test if the config might leak or not.
it should be easy to test within the strategy with `print(f'{self.max_open_trades}, {self.config["max_open_trades"]}')` or some variation of this ... with the expectation that they both yield the same result every time.

The problem with copying the config is that then you'll need to re-init all objects with the copied config (for example wallets) - which we'd want to avoid if possible.


@user1:
The new changes look to work as expected after a very quick test. :+1: 

i'll try to make some time to test and review properly soon.

diff_hunk: @@ -251,7 +251,8 @@ def __init__(self, *args, **kwargs):
     "spaces": Arg(
         '--spaces',
         help='Specify which parameters to hyperopt. Space-separated list.',
-        choices=['all', 'buy', 'sell', 'roi', 'stoploss', 'trailing', 'protection', 'default'],
+        choices=['all', 'buy', 'sell', 'roi', 'stoploss',
+                 'trailing', 'protection', 'default', 'trades'],
path: freqtrade/commands/cli_options.py
text: @user1:
i'm torn on naming here (default should go last, but that's a small nitpick).

"trades" as space name seems fine at first glance - but the more i think about it makes me feel it's not the right word.

all spaces are optimizing trades to some degree (when to enter, when to exit, when to not enter (protections) ...) - so it can be misunderstood quite easily (and it had me confused while looking through the rest of the changes, too).

We're optimizing trade count (or trade slots, or max_open_trades) - so i find trades almost too generic. 

(before acting/changing - let me know your thoughts on this!)


@author:
Yeah it felt to disruptive to use a multi-word arg name, since they were all one word lol. 
While `trade_slots` or `trade_count` are good names (and better alternatives to `trades`, since they are more explicative and specific), they may be too distant from the parameter that we are optimizing: `max_open_trades`.
So `max_open_trades` or `open_trades` will be the best options in my opinion.
I would also consider `mot`, which also doesn't violate the one word rule :D 

@author:
Sorry @user1 , I didn't explain it well in the last message. I was talking about the `trades` space name, regarding this review. I haven't change it yet, so one would call `--space trades` to hyperopt `max_open_trades`.

@user1:
i'm aware of this (was fully aware of it when we merged) - and lacking any idea on a better term - i don't think we should change it again at this point.

diff_hunk: @@ -118,11 +119,14 @@ def __init__(self, config: Config) -> None:
 
         # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
         if self.config.get('use_max_market_positions', True):
-            self.max_open_trades = self.config['max_open_trades']
+            self.max_open_trades = self.config['max_open_trades'] \
+                if self.config['max_open_trades'] != float('inf') else -1
         else:
             logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
             self.max_open_trades = 0
 
+        print("Strategy max open trades", self.max_open_trades)
path: freqtrade/optimize/hyperopt.py
text: @user1:
don't think we should print here ... seems like a debug remain?

@author:
Yes, I'm removing it :D