From c620e38c7dbd56e7205305f10bbb2eef988c141c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Jan 2022 16:58:07 +0100 Subject: [PATCH] Informative decorator updates for futures --- freqtrade/strategy/informative_decorator.py | 13 +++-- freqtrade/strategy/interface.py | 16 ++++-- .../strats/informative_decorator_strategy.py | 8 ++- tests/strategy/test_strategy_helpers.py | 54 ++++++++++--------- 4 files changed, 56 insertions(+), 35 deletions(-) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index 986b457a2..98cfabda5 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -15,11 +15,13 @@ class InformativeData(NamedTuple): timeframe: str fmt: Union[str, Callable[[Any], str], None] ffill: bool - candle_type: CandleType + candle_type: Optional[CandleType] def informative(timeframe: str, asset: str = '', fmt: Optional[Union[str, Callable[[Any], str]]] = None, + *, + candle_type: Optional[CandleType] = None, ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: """ A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to @@ -54,12 +56,11 @@ def informative(timeframe: str, asset: str = '', _timeframe = timeframe _fmt = fmt _ffill = ffill + _candle_type = CandleType.from_string(candle_type) if candle_type else None def decorator(fn: PopulateIndicators): informative_pairs = getattr(fn, '_ft_informative', []) - # TODO-lev: Add candle_type to InformativeData - informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill, - CandleType.SPOT)) + informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill, _candle_type)) setattr(fn, '_ft_informative', informative_pairs) return fn return decorator @@ -76,6 +77,8 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: asset = inf_data.asset or '' timeframe = inf_data.timeframe fmt = inf_data.fmt + candle_type = inf_data.candle_type + config = strategy.config if asset: @@ -102,7 +105,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: fmt = '{base}_{quote}_' + fmt # Informatives of other pairs inf_metadata = {'pair': asset, 'timeframe': timeframe} - inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe, candle_type) inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) formatter: Any = None diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 619dc41b1..b782ca6b2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -146,7 +146,8 @@ class IStrategy(ABC, HyperStrategyMixin): cls_method = getattr(self.__class__, attr_name) if not callable(cls_method): continue - informative_data_list = getattr(cls_method, '_ft_informative', None) + informative_data_list = getattr( + cls_method, '_ft_informative', None) if not isinstance(informative_data_list, list): # Type check is required because mocker would return a mock object that evaluates to # True, confusing this code. @@ -156,6 +157,10 @@ class IStrategy(ABC, HyperStrategyMixin): if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes: raise OperationalException('Informative timeframe must be equal or higher than ' 'strategy timeframe!') + if not informative_data.candle_type: + informative_data = InformativeData( + informative_data.asset, informative_data.timeframe, informative_data.fmt, + informative_data.ffill, config['candle_type_def']) self._ft_informative.append((informative_data, cls_method)) @abstractmethod @@ -456,14 +461,17 @@ class IStrategy(ABC, HyperStrategyMixin): # Compatibility code for 2 tuple informative pairs informative_pairs = [ (p[0], p[1], CandleType.from_string(p[2]) if len( - p) > 2 else self.config.get('candle_type_def', CandleType.SPOT)) + p) > 2 and p[2] != '' else self.config.get('candle_type_def', CandleType.SPOT)) for p in informative_pairs] for inf_data, _ in self._ft_informative: + # Get default candle type if not provided explicitly. + candle_type = (inf_data.candle_type if inf_data.candle_type + else self.config.get('candle_type_def', CandleType.SPOT)) if inf_data.asset: pair_tf = ( _format_pair_name(self.config, inf_data.asset), inf_data.timeframe, - inf_data.candle_type + candle_type, ) informative_pairs.append(pair_tf) else: @@ -471,7 +479,7 @@ class IStrategy(ABC, HyperStrategyMixin): raise OperationalException('@informative decorator with unspecified asset ' 'requires DataProvider instance.') for pair in self.dp.current_whitelist(): - informative_pairs.append((pair, inf_data.timeframe, inf_data.candle_type)) + informative_pairs.append((pair, inf_data.timeframe, candle_type)) return list(set(informative_pairs)) def get_strategy_name(self) -> str: diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py index 91c4642fa..8c1466de9 100644 --- a/tests/strategy/strats/informative_decorator_strategy.py +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -20,7 +20,11 @@ class InformativeDecoratorTest(IStrategy): def informative_pairs(self): # Intentionally return 2 tuples, must be converted to 3 in compatibility code - return [('NEO/USDT', '5m')] + return [ + ('NEO/USDT', '5m'), + ('NEO/USDT', '15m', ''), + ('NEO/USDT', '2h', 'futures'), + ] def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['buy'] = 0 @@ -44,7 +48,7 @@ class InformativeDecoratorTest(IStrategy): return dataframe # Quote currency different from stake currency test. - @informative('1h', 'ETH/BTC') + @informative('1h', 'ETH/BTC', candle_type='spot') def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['rsi'] = 14 return dataframe diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index c52a02ab9..732f69918 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -171,24 +171,27 @@ def test_stoploss_from_absolute(): assert pytest.approx(stoploss_from_absolute(100, 1, True)) == 1 -# TODO-lev: @pytest.mark.parametrize('candle_type', ['mark', '']) -def test_informative_decorator(mocker, default_conf): +@pytest.mark.parametrize('trading_mode', ['futures', 'spot']) +def test_informative_decorator(mocker, default_conf, trading_mode): + candle_def = CandleType.get_default(trading_mode) + default_conf['candle_type_def'] = candle_def test_data_5m = generate_test_data('5m', 40) test_data_30m = generate_test_data('30m', 40) test_data_1h = generate_test_data('1h', 40) data = { - ('XRP/USDT', '5m', CandleType.SPOT): test_data_5m, - ('XRP/USDT', '30m', CandleType.SPOT): test_data_30m, - ('XRP/USDT', '1h', CandleType.SPOT): test_data_1h, - ('LTC/USDT', '5m', CandleType.SPOT): test_data_5m, - ('LTC/USDT', '30m', CandleType.SPOT): test_data_30m, - ('LTC/USDT', '1h', CandleType.SPOT): test_data_1h, - ('NEO/USDT', '30m', CandleType.SPOT): test_data_30m, - ('NEO/USDT', '5m', CandleType.SPOT): test_data_5m, - ('NEO/USDT', '1h', CandleType.SPOT): test_data_1h, - ('ETH/USDT', '1h', CandleType.SPOT): test_data_1h, - ('ETH/USDT', '30m', CandleType.SPOT): test_data_30m, - ('ETH/BTC', '1h', CandleType.SPOT): test_data_1h, + ('XRP/USDT', '5m', candle_def): test_data_5m, + ('XRP/USDT', '30m', candle_def): test_data_30m, + ('XRP/USDT', '1h', candle_def): test_data_1h, + ('LTC/USDT', '5m', candle_def): test_data_5m, + ('LTC/USDT', '30m', candle_def): test_data_30m, + ('LTC/USDT', '1h', candle_def): test_data_1h, + ('NEO/USDT', '30m', candle_def): test_data_30m, + ('NEO/USDT', '5m', CandleType.SPOT): test_data_5m, # Explicit request with '' as candletype + ('NEO/USDT', '15m', candle_def): test_data_5m, # Explicit request with '' as candletype + ('NEO/USDT', '1h', candle_def): test_data_1h, + ('ETH/USDT', '1h', candle_def): test_data_1h, + ('ETH/USDT', '30m', candle_def): test_data_30m, + ('ETH/BTC', '1h', CandleType.SPOT): test_data_1h, # Explicitly selected as spot } from .strats.informative_decorator_strategy import InformativeDecoratorTest default_conf['stake_currency'] = 'USDT' @@ -201,26 +204,29 @@ def test_informative_decorator(mocker, default_conf): assert len(strategy._ft_informative) == 6 # Equal to number of decorators used informative_pairs = [ - ('XRP/USDT', '1h', CandleType.SPOT), - ('LTC/USDT', '1h', CandleType.SPOT), - ('XRP/USDT', '30m', CandleType.SPOT), - ('LTC/USDT', '30m', CandleType.SPOT), - ('NEO/USDT', '1h', CandleType.SPOT), - ('NEO/USDT', '30m', CandleType.SPOT), - ('NEO/USDT', '5m', CandleType.SPOT), - ('ETH/BTC', '1h', CandleType.SPOT), - ('ETH/USDT', '30m', CandleType.SPOT)] + ('XRP/USDT', '1h', candle_def), + ('LTC/USDT', '1h', candle_def), + ('XRP/USDT', '30m', candle_def), + ('LTC/USDT', '30m', candle_def), + ('NEO/USDT', '1h', candle_def), + ('NEO/USDT', '30m', candle_def), + ('NEO/USDT', '5m', candle_def), + ('NEO/USDT', '15m', candle_def), + ('NEO/USDT', '2h', CandleType.FUTURES), + ('ETH/BTC', '1h', CandleType.SPOT), # One candle remains as spot + ('ETH/USDT', '30m', candle_def)] for inf_pair in informative_pairs: assert inf_pair in strategy.gather_informative_pairs() def test_historic_ohlcv(pair, timeframe, candle_type): return data[ (pair, timeframe or strategy.timeframe, CandleType.from_string(candle_type))].copy() + mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv', side_effect=test_historic_ohlcv) analyzed = strategy.advise_all_indicators( - {p: data[(p, strategy.timeframe, CandleType.SPOT)] for p in ('XRP/USDT', 'LTC/USDT')}) + {p: data[(p, strategy.timeframe, candle_def)] for p in ('XRP/USDT', 'LTC/USDT')}) expected_columns = [ 'rsi_1h', 'rsi_30m', # Stacked informative decorators 'neo_usdt_rsi_1h', # NEO 1h informative