diff --git a/docs/hyperopt.md b/docs/hyperopt.md index c1bf56a3d..95bec8715 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -389,18 +389,20 @@ minimal_roi = { } ``` -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps) with the values that can vary in the following ranges: +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values can vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point): -| # | minutes | ROI percentage | -|---|---|---| -| 1 | always 0 | 0.03...0.31 | -| 2 | 10...40 | 0.02...0.11 | -| 3 | 20...100 | 0.01...0.04 | -| 4 | 30...220 | always 0 | +| # step 1m 5m 1h 1d | +|---|---|---|---|---|---|---|---|---| +| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | +| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | +| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | +| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | -This structure of the ROI table is sufficient in most cases. Override the `roi_space()` method defining the ranges desired if you need components of the ROI tables to vary in other ranges. +These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the ticker interval used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the ticker interval used. -Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization in these methods if you need a different structure of the ROI table or other amount of rows (steps) in the ROI tables. +If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. + +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). ### Understand Hyperopt Stoploss results @@ -422,7 +424,9 @@ Stoploss: -0.37996664668703606 If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.5...-0.02, which is sufficient in most cases. -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. +If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. + +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py). ### Validate backtesting results diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 12a90a14d..c9fbda17e 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -114,3 +114,10 @@ def deep_merge_dicts(source, destination): destination[key] = value return destination + + +def round_dict(d, n): + """ + Rounds float values in the dict to n digits after the decimal point. + """ + return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()} diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 9c3f085b6..eaa9ced7d 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -24,8 +24,10 @@ from skopt.space import Dimension from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data, get_timeframe +from freqtrade.misc import round_dict from freqtrade.optimize.backtesting import Backtesting -# Import IHyperOptLoss to allow users import from this file +# Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules +from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver @@ -178,9 +180,11 @@ class Hyperopt: indent=4) if self.has_space('roi'): print("ROI table:") - pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) + # Round printed values to 5 digits after the decimal point + pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4) if self.has_space('stoploss'): - print(f"Stoploss: {params.get('stoploss')}") + # Also round to 5 digits after the decimal point + print(f"Stoploss: {round(params.get('stoploss'), 5)}") def log_results(self, results) -> None: """ diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index f1f123653..2b86e018e 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -2,6 +2,8 @@ IHyperOpt interface This module defines the interface to apply for hyperopts """ +import logging +import math from abc import ABC, abstractmethod from typing import Dict, Any, Callable, List @@ -9,15 +11,19 @@ from typing import Dict, Any, Callable, List from pandas import DataFrame from skopt.space import Dimension, Integer, Real +from freqtrade.exchange import timeframe_to_minutes +from freqtrade.misc import round_dict + + +logger = logging.getLogger(__name__) + class IHyperOpt(ABC): """ Interface for freqtrade hyperopts - Defines the mandatory structure must follow any custom strategies + Defines the mandatory structure must follow any custom hyperopts - Attributes you can use: - minimal_roi -> Dict: Minimal ROI designed for the strategy - stoploss -> float: optimal stoploss designed for the strategy + Class attributes you can use: ticker_interval -> int: value of the ticker interval to use for the strategy """ ticker_interval: str @@ -75,6 +81,83 @@ class IHyperOpt(ABC): return roi_table + @staticmethod + def roi_space() -> List[Dimension]: + """ + Create a ROI space. + + Defines values to search for each ROI steps. + + This method implements adaptive roi hyperspace with varied + ranges for parameters which automatically adapts to the + ticker interval used. + + It's used by Freqtrade by default, if no custom roi_space method is defined. + """ + + # Default scaling coefficients for the roi hyperspace. Can be changed + # to adjust resulting ranges of the ROI tables. + # Increase if you need wider ranges in the roi hyperspace, decrease if shorter + # ranges are needed. + roi_t_alpha = 1.0 + roi_p_alpha = 1.0 + + ticker_interval_mins = timeframe_to_minutes(IHyperOpt.ticker_interval) + + # We define here limits for the ROI space parameters automagically adapted to the + # ticker_interval used by the bot: + # + # * 'roi_t' (limits for the time intervals in the ROI tables) components + # are scaled linearly. + # * 'roi_p' (limits for the ROI value steps) components are scaled logarithmically. + # + # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() + # method for the 5m ticker interval. + roi_t_scale = ticker_interval_mins / 5 + roi_p_scale = math.log1p(ticker_interval_mins) / math.log1p(5) + roi_limits = { + 'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha), + 'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha), + 'roi_t2_min': int(10 * roi_t_scale * roi_t_alpha), + 'roi_t2_max': int(60 * roi_t_scale * roi_t_alpha), + 'roi_t3_min': int(10 * roi_t_scale * roi_t_alpha), + 'roi_t3_max': int(40 * roi_t_scale * roi_t_alpha), + 'roi_p1_min': 0.01 * roi_p_scale * roi_p_alpha, + 'roi_p1_max': 0.04 * roi_p_scale * roi_p_alpha, + 'roi_p2_min': 0.01 * roi_p_scale * roi_p_alpha, + 'roi_p2_max': 0.07 * roi_p_scale * roi_p_alpha, + 'roi_p3_min': 0.01 * roi_p_scale * roi_p_alpha, + 'roi_p3_max': 0.20 * roi_p_scale * roi_p_alpha, + } + logger.debug(f"Using roi space limits: {roi_limits}") + p = { + 'roi_t1': roi_limits['roi_t1_min'], + 'roi_t2': roi_limits['roi_t2_min'], + 'roi_t3': roi_limits['roi_t3_min'], + 'roi_p1': roi_limits['roi_p1_min'], + 'roi_p2': roi_limits['roi_p2_min'], + 'roi_p3': roi_limits['roi_p3_min'], + } + logger.info(f"Min roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}") + p = { + 'roi_t1': roi_limits['roi_t1_max'], + 'roi_t2': roi_limits['roi_t2_max'], + 'roi_t3': roi_limits['roi_t3_max'], + 'roi_p1': roi_limits['roi_p1_max'], + 'roi_p2': roi_limits['roi_p2_max'], + 'roi_p3': roi_limits['roi_p3_max'], + } + logger.info(f"Max roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}") + + return [ + Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), + Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), + Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), + Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'), + Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'), + Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'), + ] + @staticmethod def stoploss_space() -> List[Dimension]: """ @@ -87,19 +170,14 @@ class IHyperOpt(ABC): Real(-0.5, -0.02, name='stoploss'), ] - @staticmethod - def roi_space() -> List[Dimension]: - """ - Create a ROI space. + # This is needed for proper unpickling the class attribute ticker_interval + # which is set to the actual value by the resolver. + # Why do I still need such shamanic mantras in modern python? + def __getstate__(self): + state = self.__dict__.copy() + state['ticker_interval'] = self.ticker_interval + return state - Defines values to search for each ROI steps. - You may override it in your custom Hyperopt class. - """ - return [ - Integer(10, 120, name='roi_t1'), - Integer(10, 60, name='roi_t2'), - Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), - ] + def __setstate__(self, state): + self.__dict__.update(state) + IHyperOpt.ticker_interval = state['ticker_interval'] diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 15d1997ef..f808ca0d9 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -35,7 +35,7 @@ class HyperOptResolver(IResolver): extra_dir=config.get('hyperopt_path')) # Assign ticker_interval to be used in hyperopt - self.hyperopt.__class__.ticker_interval = str(config['ticker_interval']) + IHyperOpt.ticker_interval = str(config['ticker_interval']) if not hasattr(self.hyperopt, 'populate_buy_trend'): logger.warning("Custom Hyperopt does not provide populate_buy_trend. " diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 9583de510..63d2c4604 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -418,7 +418,8 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: parallel = mocker.patch( 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', - MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', + 'params': {'buy': {}, 'sell': {}, 'roi': {}, 'stoploss': 0.0}}]) ) patch_exchange(mocker)