From e53e53087491081b4b0f7e6f2f6c3afcd5ddefec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 11:32:43 +0200 Subject: [PATCH 1/4] Add test showing broken inheritance hyperopt --- tests/optimize/test_hyperopt.py | 36 ++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 1ad8b33cf..fb5593ed2 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -855,7 +855,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: 'strategy': 'HyperoptableStrategy', 'user_data_dir': Path(tmpdir), 'hyperopt_random_state': 42, - 'spaces': ['all'] + 'spaces': ['all'], }) hyperopt = Hyperopt(hyperopt_conf) hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0) @@ -883,6 +883,40 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: hyperopt.get_optimizer([], 2) +def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None: + # patch_exchange(mocker) + # TODO: This reaches out to the exchange! + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) + # No hyperopt needed + hyperopt_conf.update({ + 'strategy': 'HyperoptableStrategy', + 'user_data_dir': Path(tmpdir), + 'hyperopt_random_state': 42, + 'spaces': ['all'], + # Enforce parallelity + 'epochs': 2, + 'hyperopt_jobs': 2, + 'fee': fee.return_value, + }) + hyperopt = Hyperopt(hyperopt_conf) + hyperopt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0 + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) + assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) + assert hyperopt.backtesting.strategy.bot_loop_started is True + + assert hyperopt.backtesting.strategy.buy_rsi.in_space is True + 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 + buy_rsi_range = hyperopt.backtesting.strategy.buy_rsi.range + assert isinstance(buy_rsi_range, range) + # Range from 0 - 50 (inclusive) + assert len(list(buy_rsi_range)) == 51 + + hyperopt.start() + + def test_SKDecimal(): space = SKDecimal(1, 2, decimals=2) assert 1.5 in space From 40e2da10f35f7b2769df9278d050f912d2650a39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 11:05:58 +0200 Subject: [PATCH 2/4] Add hypeorpt cloudpickle magic closes #7078 --- freqtrade/optimize/hyperopt.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7c7493590..566412f29 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -6,6 +6,7 @@ This module contains the hyperopt logic import logging import random +import sys import warnings from datetime import datetime, timezone from math import ceil @@ -17,6 +18,7 @@ import rapidjson from colorama import Fore, Style from colorama import init as colorama_init from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects +from joblib.externals import cloudpickle from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN @@ -87,6 +89,7 @@ class Hyperopt: self.backtesting._set_strategy(self.backtesting.strategylist[0]) self.custom_hyperopt.strategy = self.backtesting.strategy + self.hyperopt_pickle_magic(self.backtesting.strategy.__class__.__bases__) self.custom_hyperoptloss: IHyperOptLoss = HyperOptLossResolver.load_hyperoptloss( self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function @@ -137,6 +140,17 @@ class Hyperopt: logger.info(f"Removing `{p}`.") p.unlink() + def hyperopt_pickle_magic(self, bases) -> None: + """ + Hyperopt magic to allow strategy inheritance across files. + For this to properly work, we need to register the module of the imported class + to pickle as value. + """ + for modules in bases: + if modules.__name__ != 'IStrategy': + cloudpickle.register_pickle_by_value(sys.modules[modules.__module__]) + self.hyperopt_pickle_magic(modules.__bases__) + def _get_params_dict(self, dimensions: List[Dimension], raw_params: List[Any]) -> Dict: # Ensure the number of dimensions match From 357000c478a078a22d09345887ee9955d41b9abe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 13:27:13 +0200 Subject: [PATCH 3/4] Extract exchange validation to separate method --- freqtrade/exchange/exchange.py | 28 +++++++++++++++------------- tests/conftest.py | 5 +---- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cd13964c4..505d02e8c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -175,23 +175,11 @@ class Exchange: logger.info(f'Using Exchange "{self.name}"') if validate: - # Check if timeframe is available - self.validate_timeframes(config.get('timeframe')) - # Initial markets load self._load_markets() - - # Check if all pairs are available - self.validate_stakecurrency(config['stake_currency']) - if not exchange_config.get('skip_pair_validation'): - self.validate_pairs(config['exchange']['pair_whitelist']) - self.validate_ordertypes(config.get('order_types', {})) - self.validate_order_time_in_force(config.get('order_time_in_force', {})) + self.validate_config(config) self.required_candle_call_count = self.validate_required_startup_candles( config.get('startup_candle_count', 0), config.get('timeframe', '')) - self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode) - self.validate_pricing(config['exit_pricing']) - self.validate_pricing(config['entry_pricing']) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -214,6 +202,20 @@ class Exchange: logger.info("Closing async ccxt session.") self.loop.run_until_complete(self._api_async.close()) + def validate_config(self, config): + # Check if timeframe is available + self.validate_timeframes(config.get('timeframe')) + + # Check if all pairs are available + self.validate_stakecurrency(config['stake_currency']) + if not config['exchange'].get('skip_pair_validation'): + self.validate_pairs(config['exchange']['pair_whitelist']) + self.validate_ordertypes(config.get('order_types', {})) + self.validate_order_time_in_force(config.get('order_time_in_force', {})) + self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode) + self.validate_pricing(config['exit_pricing']) + self.validate_pricing(config['entry_pricing']) + def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt, ccxt_kwargs: Dict = {}) -> ccxt.Exchange: """ diff --git a/tests/conftest.py b/tests/conftest.py index 0cf32545f..3158e9ede 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -112,11 +112,8 @@ def patch_exchange( mock_supported_modes=True ) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) From bf07d8fe879ca592db74b1bf61bb59cd8441bee4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 Jul 2022 13:57:04 +0200 Subject: [PATCH 4/4] Update test to properly patch/mock exchange --- tests/optimize/test_hyperopt.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index fb5593ed2..0f615b7a3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,7 +1,7 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 from datetime import datetime, timedelta from pathlib import Path -from unittest.mock import ANY, MagicMock +from unittest.mock import ANY, MagicMock, PropertyMock import pandas as pd import pytest @@ -18,8 +18,8 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IntParameter -from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file) +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, get_markets, log_has, log_has_re, + patch_exchange, patched_configuration_load_config_file) def generate_result_metrics(): @@ -884,9 +884,11 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, fee) -> None: - # patch_exchange(mocker) - # TODO: This reaches out to the exchange! + mocker.patch('freqtrade.exchange.Exchange.validate_config', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch('freqtrade.exchange.Exchange._load_markets') + mocker.patch('freqtrade.exchange.Exchange.markets', + PropertyMock(return_value=get_markets())) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed hyperopt_conf.update({ @@ -901,6 +903,9 @@ def test_in_strategy_auto_hyperopt_with_parallel(mocker, hyperopt_conf, tmpdir, }) hyperopt = Hyperopt(hyperopt_conf) hyperopt.backtesting.exchange.get_max_leverage = lambda *x, **xx: 1.0 + hyperopt.backtesting.exchange.get_min_pair_stake_amount = lambda *x, **xx: 1.0 + hyperopt.backtesting.exchange.get_max_pair_stake_amount = lambda *x, **xx: 100.0 + assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) assert hyperopt.backtesting.strategy.bot_loop_started is True