diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 2b86e018e..0e2e75d2e 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -11,6 +11,7 @@ from typing import Dict, Any, Callable, List from pandas import DataFrame from skopt.space import Dimension, Integer, Real +from freqtrade import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict @@ -18,6 +19,13 @@ from freqtrade.misc import round_dict logger = logging.getLogger(__name__) +def _format_exception_message(method: str, space: str) -> str: + return (f"The '{space}' space is included into the hyperoptimization " + f"but {method}() method is not found in your " + f"custom Hyperopt class. You should either implement this " + f"method or remove the '{space}' space from hyperoptimization.") + + class IHyperOpt(ABC): """ Interface for freqtrade hyperopts @@ -38,32 +46,32 @@ class IHyperOpt(ABC): """ @staticmethod - @abstractmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: """ Create a buy strategy generator. """ + raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy')) @staticmethod - @abstractmethod def sell_strategy_generator(params: Dict[str, Any]) -> Callable: """ Create a sell strategy generator. """ + raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell')) @staticmethod - @abstractmethod def indicator_space() -> List[Dimension]: """ Create an indicator space. """ + raise OperationalException(_format_exception_message('indicator_space', 'buy')) @staticmethod - @abstractmethod def sell_indicator_space() -> List[Dimension]: """ Create a sell indicator space. """ + raise OperationalException(_format_exception_message('sell_indicator_space', 'sell')) @staticmethod def generate_roi_table(params: Dict) -> Dict[int, float]: diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 63d2c4604..976cb3d53 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -694,3 +694,200 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> assert dumper.called # Should be called twice, once for tickerdata, once to save evaluations assert dumper.call_count == 2 + + +def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) -> None: + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + parallel = mocker.patch( + 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', + MagicMock(return_value=[{ + 'loss': 1, 'results_explanation': 'foo result', 'params': {'stoploss': 0.0}}]) + ) + patch_exchange(mocker) + + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'roi stoploss', + 'hyperopt_jobs': 1, }) + + hyperopt = Hyperopt(default_conf) + hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + del hyperopt.custom_hyperopt.__class__.buy_strategy_generator + del hyperopt.custom_hyperopt.__class__.sell_strategy_generator + del hyperopt.custom_hyperopt.__class__.indicator_space + del hyperopt.custom_hyperopt.__class__.sell_indicator_space + + hyperopt.start() + + parallel.assert_called_once() + + out, err = capsys.readouterr() + assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out + assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 + assert hasattr(hyperopt.backtesting, "advise_sell") + assert hasattr(hyperopt.backtesting, "advise_buy") + assert hasattr(hyperopt, "max_open_trades") + assert hyperopt.max_open_trades == default_conf['max_open_trades'] + assert hasattr(hyperopt, "position_stacking") + + +def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -> None: + mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + patch_exchange(mocker) + + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'all', + 'hyperopt_jobs': 1, }) + + hyperopt = Hyperopt(default_conf) + hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + del hyperopt.custom_hyperopt.__class__.buy_strategy_generator + del hyperopt.custom_hyperopt.__class__.sell_strategy_generator + del hyperopt.custom_hyperopt.__class__.indicator_space + del hyperopt.custom_hyperopt.__class__.sell_indicator_space + + with pytest.raises(OperationalException, match=r"The 'buy' space is included into *"): + hyperopt.start() + + +def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + parallel = mocker.patch( + 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) + ) + patch_exchange(mocker) + + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'buy', + 'hyperopt_jobs': 1, }) + + hyperopt = Hyperopt(default_conf) + hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + # TODO: sell_strategy_generator() is actually not called because + # run_optimizer_parallel() is mocked + del hyperopt.custom_hyperopt.__class__.sell_strategy_generator + del hyperopt.custom_hyperopt.__class__.sell_indicator_space + + hyperopt.start() + + parallel.assert_called_once() + + out, err = capsys.readouterr() + assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out + assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 + assert hasattr(hyperopt.backtesting, "advise_sell") + assert hasattr(hyperopt.backtesting, "advise_buy") + assert hasattr(hyperopt, "max_open_trades") + assert hyperopt.max_open_trades == default_conf['max_open_trades'] + assert hasattr(hyperopt, "position_stacking") + + +def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None: + dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + parallel = mocker.patch( + 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', + MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) + ) + patch_exchange(mocker) + + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': 'sell', + 'hyperopt_jobs': 1, }) + + hyperopt = Hyperopt(default_conf) + hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + # TODO: buy_strategy_generator() is actually not called because + # run_optimizer_parallel() is mocked + del hyperopt.custom_hyperopt.__class__.buy_strategy_generator + del hyperopt.custom_hyperopt.__class__.indicator_space + + hyperopt.start() + + parallel.assert_called_once() + + out, err = capsys.readouterr() + assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out + assert dumper.called + # Should be called twice, once for tickerdata, once to save evaluations + assert dumper.call_count == 2 + assert hasattr(hyperopt.backtesting, "advise_sell") + assert hasattr(hyperopt.backtesting, "advise_buy") + assert hasattr(hyperopt, "max_open_trades") + assert hyperopt.max_open_trades == default_conf['max_open_trades'] + assert hasattr(hyperopt, "position_stacking") + + +@pytest.mark.parametrize("method,space", [ + ('buy_strategy_generator', 'buy'), + ('indicator_space', 'buy'), + ('sell_strategy_generator', 'sell'), + ('sell_indicator_space', 'sell'), +]) +def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, method, space) -> None: + mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) + mocker.patch( + 'freqtrade.optimize.hyperopt.get_timeframe', + MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) + ) + + patch_exchange(mocker) + + default_conf.update({'config': 'config.json.example', + 'epochs': 1, + 'timerange': None, + 'spaces': space, + 'hyperopt_jobs': 1, }) + + hyperopt = Hyperopt(default_conf) + hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock() + hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) + + delattr(hyperopt.custom_hyperopt.__class__, method) + + with pytest.raises(OperationalException, match=f"The '{space}' space is included into *"): + hyperopt.start()