Added support for max_open_trades hyperopting

This commit is contained in:
Antonio Della Fortuna 2023-01-04 10:34:44 +01:00
parent cd4faa9c59
commit 5fd85368a9
20 changed files with 155 additions and 36 deletions

View File

@ -123,6 +123,12 @@ class MyAwesomeStrategy(IStrategy):
Categorical([True, False], name='trailing_only_offset_is_reached'), 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 !!! Note

View File

@ -134,7 +134,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| Parameter | Description | | 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_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"`. | `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`. | `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` * `minimal_roi`
* `timeframe` * `timeframe`
* `stoploss` * `stoploss`
* `max_open_trades`
* `trailing_stop` * `trailing_stop`
* `trailing_stop_positive` * `trailing_stop_positive`
* `trailing_stop_positive_offset` * `trailing_stop_positive_offset`

View File

@ -50,7 +50,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--eps] [--dmmp] [--enable-protections] [--eps] [--dmmp] [--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [--dry-run-wallet DRY_RUN_WALLET]
[--timeframe-detail TIMEFRAME_DETAIL] [-e INT] [--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] [--print-all] [--no-color] [--print-json] [-j JOBS]
[--random-state INT] [--min-trades INT] [--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME] [--disable-param-export] [--hyperopt-loss NAME] [--disable-param-export]
@ -96,7 +96,7 @@ optional arguments:
Specify detail timeframe for backtesting (`1m`, `5m`, Specify detail timeframe for backtesting (`1m`, `5m`,
`30m`, `1h`, `1d`). `30m`, `1h`, `1d`).
-e INT, --epochs INT Specify number of epochs (default: 100). -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 Specify which parameters to hyperopt. Space-separated
list. list.
--print-all Print all results, not only the best ones. --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) * `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) * `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) * `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" !!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything in your strategy. 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 * `roi`: just optimize the minimal profit table for your strategy
* `stoploss`: search for the best stoploss value * `stoploss`: search for the best stoploss value
* `trailing`: search for the best trailing stop values * `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) * `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` * `default`: `all` except `trailing` and `protection`
* space-separated list of any of the above values for example `--spaces roi stoploss` * 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. 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. 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. 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` or `trailing_stop`). 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`).

View File

@ -251,7 +251,8 @@ AVAILABLE_CLI_OPTIONS = {
"spaces": Arg( "spaces": Arg(
'--spaces', '--spaces',
help='Specify which parameters to hyperopt. Space-separated list.', 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='+', nargs='+',
default='default', default='default',
), ),

View File

@ -636,7 +636,6 @@ SCHEMA_TRADE_REQUIRED = [
SCHEMA_BACKTEST_REQUIRED = [ SCHEMA_BACKTEST_REQUIRED = [
'exchange', 'exchange',
'max_open_trades',
'stake_currency', 'stake_currency',
'stake_amount', 'stake_amount',
'dry_run_wallet', 'dry_run_wallet',
@ -646,6 +645,7 @@ SCHEMA_BACKTEST_REQUIRED = [
SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [ SCHEMA_BACKTEST_REQUIRED_FINAL = SCHEMA_BACKTEST_REQUIRED + [
'stoploss', 'stoploss',
'minimal_roi', 'minimal_roi',
'max_open_trades'
] ]
SCHEMA_MINIMAL_REQUIRED = [ SCHEMA_MINIMAL_REQUIRED = [

View File

@ -332,7 +332,7 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF
def evaluate_result_multi(results: pd.DataFrame, timeframe: str, def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
max_open_trades: int) -> pd.DataFrame: max_open_trades: int | float) -> pd.DataFrame:
""" """
Find overlapping trades by expanding each trade once per period it was open Find overlapping trades by expanding each trade once per period it was open
and then counting overlaps and then counting overlaps

View File

@ -520,7 +520,8 @@ class FreqtradeBot(LoggingMixin):
else: else:
self.log_once(f"Pair {pair} is currently locked.", logger.info) self.log_once(f"Pair {pair} is currently locked.", logger.info)
return False return False
stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) stake_amount = self.wallets.get_trade_stake_amount(
pair, self.edge, self.config['max_open_trades'])
bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {}) bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
if ((bid_check_dom.get('enabled', False)) and if ((bid_check_dom.get('enabled', False)) and

View File

@ -920,7 +920,7 @@ class Backtesting:
trade.close(exit_row[OPEN_IDX], show_msg=False) trade.close(exit_row[OPEN_IDX], show_msg=False)
LocalTrade.close_bt_trade(trade) LocalTrade.close_bt_trade(trade)
def trade_slot_available(self, max_open_trades: int, open_trade_count: int) -> bool: def trade_slot_available(self, max_open_trades: int | float, open_trade_count: int) -> bool:
# Always allow trades when max_open_trades is enabled. # Always allow trades when max_open_trades is enabled.
if max_open_trades <= 0 or open_trade_count < max_open_trades: if max_open_trades <= 0 or open_trade_count < max_open_trades:
return True return True
@ -1051,7 +1051,8 @@ class Backtesting:
def backtest_loop( def backtest_loop(
self, row: Tuple, pair: str, current_time: datetime, end_date: datetime, 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 | float,
open_trade_count_start: int, is_first: bool = True) -> int:
""" """
NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized. NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized.
@ -1122,7 +1123,7 @@ class Backtesting:
def backtest(self, processed: Dict, def backtest(self, processed: Dict,
start_date: datetime, end_date: datetime, start_date: datetime, end_date: datetime,
max_open_trades: int = 0) -> Dict[str, Any]: max_open_trades: int | float = 0) -> Dict[str, Any]:
""" """
Implement backtesting functionality Implement backtesting functionality

View File

@ -74,6 +74,7 @@ class Hyperopt:
self.roi_space: List[Dimension] = [] self.roi_space: List[Dimension] = []
self.stoploss_space: List[Dimension] = [] self.stoploss_space: List[Dimension] = []
self.trailing_space: List[Dimension] = [] self.trailing_space: List[Dimension] = []
self.trades_space: List[Dimension] = []
self.dimensions: List[Dimension] = [] self.dimensions: List[Dimension] = []
self.config = config self.config = config
@ -209,6 +210,8 @@ class Hyperopt:
result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space} result['stoploss'] = {p.name: params.get(p.name) for p in self.stoploss_space}
if HyperoptTools.has_space(self.config, 'trailing'): if HyperoptTools.has_space(self.config, 'trailing'):
result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) 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 return result
@ -229,6 +232,8 @@ class Hyperopt:
'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset, 'trailing_stop_positive_offset': strategy.trailing_stop_positive_offset,
'trailing_only_offset_is_reached': strategy.trailing_only_offset_is_reached, '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 return result
def print_results(self, results) -> None: def print_results(self, results) -> None:
@ -280,8 +285,13 @@ class Hyperopt:
logger.debug("Hyperopt has 'trailing' space") logger.debug("Hyperopt has 'trailing' space")
self.trailing_space = self.custom_hyperopt.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.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: def assign_params(self, params_dict: Dict, category: str) -> None:
""" """
@ -328,6 +338,9 @@ class Hyperopt:
self.backtesting.strategy.trailing_only_offset_is_reached = \ self.backtesting.strategy.trailing_only_offset_is_reached = \
d['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: with self.data_pickle_file.open('rb') as f:
processed = load(f, mmap_mode='r') processed = load(f, mmap_mode='r')
if self.analyze_per_epoch: if self.analyze_per_epoch:

View File

@ -91,5 +91,8 @@ class HyperOptAuto(IHyperOpt):
def trailing_space(self) -> List['Dimension']: def trailing_space(self) -> List['Dimension']:
return self._get_func('trailing_space')() 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: def generate_estimator(self, dimensions: List['Dimension'], **kwargs) -> EstimatorType:
return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs) return self._get_func('generate_estimator')(dimensions=dimensions, **kwargs)

View File

@ -191,6 +191,16 @@ class IHyperOpt(ABC):
Categorical([True, False], name='trailing_only_offset_is_reached'), 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 # This is needed for proper unpickling the class attribute timeframe
# which is set to the actual value by the resolver. # which is set to the actual value by the resolver.
# Why do I still need such shamanic mantras in modern python? # Why do I still need such shamanic mantras in modern python?

View File

@ -96,7 +96,7 @@ class HyperoptTools():
Tell if the space value is contained in the configuration Tell if the space value is contained in the configuration
""" """
# 'trailing' and 'protection spaces are not included in the 'default' set of spaces # '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']) return any(s in config['spaces'] for s in [space, 'all'])
else: else:
return any(s in config['spaces'] for s in [space, 'all', 'default']) return any(s in config['spaces'] for s in [space, 'all', 'default'])
@ -187,7 +187,8 @@ class HyperoptTools():
if print_json: if print_json:
result_dict: Dict = {} 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) HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE)) print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
@ -201,6 +202,8 @@ class HyperoptTools():
HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized) HyperoptTools._params_pretty_print(params, 'roi', "ROI table:", non_optimized)
HyperoptTools._params_pretty_print(params, 'stoploss', "Stoploss:", 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, 'trailing', "Trailing stop:", non_optimized)
HyperoptTools._params_pretty_print(
params, 'max_open_trades', "Max Open Trades:", non_optimized)
@staticmethod @staticmethod
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None: def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
@ -239,7 +242,9 @@ class HyperoptTools():
if space == "stoploss": if space == "stoploss":
stoploss = safe_value_fallback2(space_params, no_params, space, space) stoploss = safe_value_fallback2(space_params, no_params, space, space)
result += (f"stoploss = {stoploss}{appendix}") 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": elif space == "roi":
result = result[:-1] + f'{appendix}\n' result = result[:-1] + f'{appendix}\n'
minimal_roi_result = rapidjson.dumps({ minimal_roi_result = rapidjson.dumps({

View File

@ -190,7 +190,7 @@ def generate_tag_metrics(tag_type: str,
return [] return []
def generate_exit_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: def generate_exit_reason_stats(max_open_trades: int | float, results: DataFrame) -> List[Dict]:
""" """
Generate small table outlining Backtest results Generate small table outlining Backtest results
:param max_open_trades: Max_open_trades parameter :param max_open_trades: Max_open_trades parameter

View File

@ -76,6 +76,7 @@ class StrategyResolver(IResolver):
("ignore_buying_expired_candle_after", 0), ("ignore_buying_expired_candle_after", 0),
("position_adjustment_enable", False), ("position_adjustment_enable", False),
("max_entry_position_adjustment", -1), ("max_entry_position_adjustment", -1),
("max_open_trades", -1)
] ]
for attribute, default in attributes: for attribute, default in attributes:
StrategyResolver._override_attribute_helper(strategy, config, StrategyResolver._override_attribute_helper(strategy, config,
@ -128,6 +129,8 @@ class StrategyResolver(IResolver):
key=lambda t: t[0])) key=lambda t: t[0]))
if hasattr(strategy, 'stoploss'): if hasattr(strategy, 'stoploss'):
strategy.stoploss = float(strategy.stoploss) strategy.stoploss = float(strategy.stoploss)
if hasattr(strategy, 'max_open_trades') and strategy.max_open_trades == -1:
strategy.max_open_trades = float('inf')
return strategy return strategy
@staticmethod @staticmethod

View File

@ -80,6 +80,8 @@ class HyperStrategyMixin:
self.stoploss = params.get('stoploss', {}).get( self.stoploss = params.get('stoploss', {}).get(
'stoploss', getattr(self, 'stoploss', -0.1)) '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', {}) trailing = params.get('trailing', {})
self.trailing_stop = trailing.get( self.trailing_stop = trailing.get(
'trailing_stop', getattr(self, 'trailing_stop', False)) 'trailing_stop', getattr(self, 'trailing_stop', False))

View File

@ -54,6 +54,9 @@ class IStrategy(ABC, HyperStrategyMixin):
# associated stoploss # associated stoploss
stoploss: float stoploss: float
# max open trades for the strategy
max_open_trades: int | float
# trailing stoploss # trailing stoploss
trailing_stop: bool = False trailing_stop: bool = False
trailing_stop_positive: Optional[float] = None trailing_stop_positive: Optional[float] = None

View File

@ -292,6 +292,8 @@ def test_params_no_optimize_details(hyperopt) -> None:
assert res['roi']['0'] == 0.04 assert res['roi']['0'] == 0.04
assert "stoploss" in res assert "stoploss" in res
assert res['stoploss']['stoploss'] == -0.1 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: 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': 0.02,
'trailing_stop_positive_offset_p1': 0.05, 'trailing_stop_positive_offset_p1': 0.05,
'trailing_only_offset_is_reached': False, 'trailing_only_offset_is_reached': False,
'max_open_trades': 3,
} }
response_expected = { response_expected = {
'loss': 1.9147239021396234, 'loss': 1.9147239021396234,
@ -499,7 +502,9 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
'trailing': {'trailing_only_offset_is_reached': False, 'trailing': {'trailing_only_offset_is_reached': False,
'trailing_stop': True, 'trailing_stop': True,
'trailing_stop_positive': 0.02, '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_dict': optimizer_param,
'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}}, 'params_not_optimized': {'buy': {}, 'protection': {}, 'sell': {}},
'results_metrics': ANY, 'results_metrics': ANY,
@ -548,7 +553,8 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
'buy': {'mfi-value': None}, 'buy': {'mfi-value': None},
'sell': {'sell-mfi-value': None}, 'sell': {'sell-mfi-value': None},
'roi': {}, 'stoploss': {'stoploss': 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(), 'results_metrics': generate_result_metrics(),
}]) }])
@ -571,7 +577,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
out, err = capsys.readouterr() out, err = capsys.readouterr()
result_str = ( result_str = (
'{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi"' '{"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 assert result_str in out # noqa: E501
# Should be called for historical candle data # 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.buy_rsi.value == 35
assert hyperopt.backtesting.strategy.sell_rsi.value == 74 assert hyperopt.backtesting.strategy.sell_rsi.value == 74
assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30 assert hyperopt.backtesting.strategy.protection_cooldown_lookback.value == 30
assert hyperopt.max_open_trades == 1
buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range
assert isinstance(buy_rsi_range, range) assert isinstance(buy_rsi_range, range)
# Range from 0 - 50 (inclusive) # 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.protection_cooldown_lookback.value != 30
assert hyperopt.backtesting.strategy.buy_rsi.value != 35 assert hyperopt.backtesting.strategy.buy_rsi.value != 35
assert hyperopt.backtesting.strategy.sell_rsi.value != 74 assert hyperopt.backtesting.strategy.sell_rsi.value != 74
assert hyperopt.max_open_trades != 1
hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1' hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1'
with pytest.raises(OperationalException, match="Estimator ET1 not supported."): with pytest.raises(OperationalException, match="Estimator ET1 not supported."):

View File

@ -66,52 +66,58 @@ def test_load_previous_results2(mocker, testdatadir, caplog) -> None:
@pytest.mark.parametrize("spaces, expected_results", [ @pytest.mark.parametrize("spaces, expected_results", [
(['buy'], (['buy'],
{'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False, {'buy': True, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False,
'protection': False}), 'protection': False, 'trades': False}),
(['sell'], (['sell'],
{'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False, {'buy': False, 'sell': True, 'roi': False, 'stoploss': False, 'trailing': False,
'protection': False}), 'protection': False, 'trades': False}),
(['roi'], (['roi'],
{'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False, {'buy': False, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
'protection': False}), 'protection': False, 'trades': False}),
(['stoploss'], (['stoploss'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False, {'buy': False, 'sell': False, 'roi': False, 'stoploss': True, 'trailing': False,
'protection': False}), 'protection': False, 'trades': False}),
(['trailing'], (['trailing'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True, {'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': True,
'protection': False}), 'protection': False, 'trades': False}),
(['buy', 'sell', 'roi', 'stoploss'], (['buy', 'sell', 'roi', 'stoploss'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False, {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False}), 'protection': False, 'trades': False}),
(['buy', 'sell', 'roi', 'stoploss', 'trailing'], (['buy', 'sell', 'roi', 'stoploss', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': False}), 'protection': False, 'trades': False}),
(['buy', 'roi'], (['buy', 'roi'],
{'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False, {'buy': True, 'sell': False, 'roi': True, 'stoploss': False, 'trailing': False,
'protection': False}), 'protection': False, 'trades': False}),
(['all'], (['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': True}), 'protection': True, 'trades': True}),
(['default'], (['default'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False, {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False}), 'protection': False, 'trades': False}),
(['default', 'trailing'], (['default', 'trailing'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': False}), 'protection': False, 'trades': False}),
(['all', 'buy'], (['all', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': True}), 'protection': True, 'trades': True}),
(['default', 'buy'], (['default', 'buy'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False, {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': False,
'protection': False}), 'protection': False, 'trades': False}),
(['all'], (['all'],
{'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True, {'buy': True, 'sell': True, 'roi': True, 'stoploss': True, 'trailing': True,
'protection': True}), 'protection': True, 'trades': True}),
(['protection'], (['protection'],
{'buy': False, 'sell': False, 'roi': False, 'stoploss': False, 'trailing': False, {'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): 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}) hyperopt_conf.update({'spaces': spaces})
assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s] assert HyperoptTools.has_space(hyperopt_conf, s) == expected_results[s]
@ -193,6 +199,9 @@ def test_export_params(tmpdir):
"346": 0.08499, "346": 0.08499,
"507": 0.049, "507": 0.049,
"1595": 0 "1595": 0
},
"max_open_trades": {
"max_open_trades": 5
} }
}, },
"params_not_optimized": { "params_not_optimized": {
@ -219,6 +228,7 @@ def test_export_params(tmpdir):
assert "roi" in content["params"] assert "roi" in content["params"]
assert "stoploss" in content["params"] assert "stoploss" in content["params"]
assert "trailing" 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): 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_stop_positive_offset": 0.1,
"trailing_only_offset_is_reached": True "trailing_only_offset_is_reached": True
}, },
"max_open_trades": {
"max_open_trades": 5
}
} }
HyperoptTools._params_pretty_print(params, 'buy', 'No header', non_optimized) 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_stop_positive_offset = 0.1 # value loaded.*\n', captured.out)
assert re.search('trailing_only_offset_is_reached = True # 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(): def test_hyperopt_serializer():

View File

@ -30,6 +30,9 @@ class StrategyTestV3(IStrategy):
"0": 0.04 "0": 0.04
} }
# Optimal max_open_trades for the strategy
max_open_trades = -1
# Optimal stoploss designed for the strategy # Optimal stoploss designed for the strategy
stoploss = -0.10 stoploss = -0.10

View File

@ -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) 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): def test_strategy_override_trailing_stop(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
@ -349,6 +361,31 @@ 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) 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 == 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': -1
})
strategy = StrategyResolver.load_strategy(default_conf)
assert strategy.max_open_trades == float('inf')
@ pytest.mark.filterwarnings("ignore:deprecated") @ pytest.mark.filterwarnings("ignore:deprecated")
def test_missing_implements(default_conf, caplog): def test_missing_implements(default_conf, caplog):