From 71e2134694c88815d0c4b39e704abdbfbcadfa30 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Mar 2021 11:26:26 +0100 Subject: [PATCH] Add some simple tests for hyperoptParameters --- freqtrade/strategy/hyper.py | 14 +- tests/rpc/test_rpc_apiserver.py | 6 +- .../strategy/strats/hyperoptable_strategy.py | 170 ++++++++++++++++++ tests/strategy/test_interface.py | 38 +++- tests/strategy/test_strategy_loading.py | 6 +- 5 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 tests/strategy/strats/hyperoptable_strategy.py diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 32b03d57e..b8bfef767 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -67,8 +67,10 @@ class IntParameter(BaseParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Integer. """ - if high is None: - if len(low) != 2: + if high is not None and isinstance(low, Sequence): + raise OperationalException('IntParameter space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: raise OperationalException('IntParameter space must be [low, high]') opt_range = low else: @@ -101,9 +103,11 @@ class FloatParameter(BaseParameter): parameter fieldname is prefixed with 'buy_' or 'sell_'. :param kwargs: Extra parameters to skopt.space.Real. """ - if high is None: - if len(low) != 2: - raise OperationalException('IntParameter space must be [low, high]') + if high is not None and isinstance(low, Sequence): + raise OperationalException('FloatParameter space invalid.') + if high is None or isinstance(low, Sequence): + if not isinstance(low, Sequence) or len(low) != 2: + raise OperationalException('FloatParameter space must be [low, high]') opt_range = low else: opt_range = [low, high] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5a0a04943..bef70a5dd 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1149,7 +1149,11 @@ def test_api_strategies(botclient): rc = client_get(client, f"{BASE_URI}/strategies") assert_response(rc) - assert rc.json() == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} + assert rc.json() == {'strategies': [ + 'DefaultStrategy', + 'HyperoptableStrategy', + 'TestStrategyLegacy' + ]} def test_api_strategy(botclient): diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py new file mode 100644 index 000000000..8cde28321 --- /dev/null +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -0,0 +1,170 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +import talib.abstract as ta +from pandas import DataFrame + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.strategy import FloatParameter, IntParameter, IStrategy + + +class HyperoptableStrategy(IStrategy): + """ + Default Strategy provided by freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + # Optimal ticker interval for the strategy + timeframe = '5m' + + # Optional order type mapping + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False + } + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional time in force for orders + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc', + } + + buy_params = { + 'buy_rsi': 35, + # Intentionally not specified, so "default" is tested + # 'buy_plusdi': 0.4 + } + + sell_params = { + 'sell_rsi': 74 + } + + buy_rsi = IntParameter([0, 50], default=30, space='buy') + buy_plusdi = FloatParameter(low=0, high=1, default=0.5, space='buy') + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Dataframe with data from the exchange + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # Minus Directional Indicator / Movement + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # EMA - Exponential Moving Average + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['rsi'] < self.buy_rsi.value) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + ( + (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > 0.5) + ), + 'sell'] = 1 + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f158a1518..9c831d194 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 +from freqtrade.strategy.hyper import BaseParameter, FloatParameter, IntParameter import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -10,7 +11,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -552,3 +553,38 @@ def test_strategy_safe_wrapper(value): assert type(ret) == type(value) assert ret == value + + +def test_hyperopt_parameters(): + with pytest.raises(OperationalException, match=r"Name is determined.*"): + IntParameter(low=0, high=5, default=1, name='hello') + + with pytest.raises(OperationalException, match=r"IntParameter space must be.*"): + IntParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"FloatParameter space must be.*"): + FloatParameter(low=0, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"IntParameter space invalid\."): + IntParameter([0, 10], high=7, default=5, space='buy') + + with pytest.raises(OperationalException, match=r"FloatParameter space invalid\."): + FloatParameter([0, 10], high=7, default=5, space='buy') + + x = BaseParameter(opt_range=[0, 1], default=1, space='buy') + with pytest.raises(NotImplementedError): + x.get_space('space') + + fltpar = IntParameter(low=0, high=5, default=1, space='buy') + assert fltpar.value == 1 + + +def test_auto_hyperopt_interface(default_conf): + default_conf.update({'strategy': 'HyperoptableStrategy'}) + PairLocks.timeframe = default_conf['timeframe'] + strategy = StrategyResolver.load_strategy(default_conf) + + assert strategy.buy_rsi.value == strategy.buy_params['buy_rsi'] + # PlusDI is NOT in the buy-params, so default should be used + assert strategy.buy_plusdi.value == 0.5 + assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi'] diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 1c692d2da..965c3d37b 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 2 + assert len(strategies) == 3 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 3 + assert len(strategies) == 4 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 2 + assert len([x for x in strategies if x['class'] is not None]) == 3 assert len([x for x in strategies if x['class'] is None]) == 1