From c620e38c7dbd56e7205305f10bbb2eef988c141c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Jan 2022 16:58:07 +0100 Subject: [PATCH 1/3] 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 From ab932d83980e26320c56f5460c711e9cc4370880 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Jan 2022 19:18:03 +0100 Subject: [PATCH 2/3] Properly detect default candle type --- freqtrade/data/dataprovider.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 3d0ca45d5..b9b118c00 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -78,8 +78,9 @@ class DataProvider: :param timeframe: timeframe to get data for :param candle_type: '', mark, index, premiumIndex, or funding_rate """ - candleType = CandleType.from_string(candle_type) - saved_pair = (pair, str(timeframe), candleType) + _candle_type = CandleType.from_string( + candle_type) if candle_type != '' else self._config['candle_type_def'] + saved_pair = (pair, str(timeframe), _candle_type) if saved_pair not in self.__cached_pairs_backtesting: timerange = TimeRange.parse_timerange(None if self._config.get( 'timerange') is None else str(self._config.get('timerange'))) @@ -93,7 +94,7 @@ class DataProvider: datadir=self._config['datadir'], timerange=timerange, data_format=self._config.get('dataformat_ohlcv', 'json'), - candle_type=candleType, + candle_type=_candle_type, ) return self.__cached_pairs_backtesting[saved_pair].copy() @@ -221,8 +222,10 @@ class DataProvider: if self._exchange is None: raise OperationalException(NO_EXCHANGE_EXCEPTION) if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): + _candle_type = CandleType.from_string( + candle_type) if candle_type != '' else self._config['candle_type_def'] return self._exchange.klines( - (pair, timeframe or self._config['timeframe'], CandleType.from_string(candle_type)), + (pair, timeframe or self._config['timeframe'], _candle_type), copy=copy ) else: From 8a6823deb15185221903090b10cd4149db4e8686 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jan 2022 19:59:54 +0100 Subject: [PATCH 3/3] Convert InformativeData to dataclass --- freqtrade/strategy/informative_decorator.py | 6 ++++-- freqtrade/strategy/interface.py | 4 +--- tests/strategy/test_strategy_helpers.py | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index 98cfabda5..0dd5320cd 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -1,4 +1,5 @@ -from typing import Any, Callable, NamedTuple, Optional, Union +from dataclasses import dataclass +from typing import Any, Callable, Optional, Union from pandas import DataFrame @@ -10,7 +11,8 @@ from freqtrade.strategy.strategy_helper import merge_informative_pair PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] -class InformativeData(NamedTuple): +@dataclass +class InformativeData: asset: Optional[str] timeframe: str fmt: Union[str, Callable[[Any], str], None] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b782ca6b2..9aa730f98 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -158,9 +158,7 @@ class IStrategy(ABC, HyperStrategyMixin): 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']) + informative_data.candle_type = config['candle_type_def'] self._ft_informative.append((informative_data, cls_method)) @abstractmethod diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 732f69918..205fb4dac 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -6,6 +6,7 @@ import pytest from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import CandleType +from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open, timeframe_to_minutes) from tests.conftest import get_patched_exchange @@ -172,9 +173,9 @@ def test_stoploss_from_absolute(): @pytest.mark.parametrize('trading_mode', ['futures', 'spot']) -def test_informative_decorator(mocker, default_conf, trading_mode): +def test_informative_decorator(mocker, default_conf_usdt, trading_mode): candle_def = CandleType.get_default(trading_mode) - default_conf['candle_type_def'] = candle_def + default_conf_usdt['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) @@ -193,10 +194,9 @@ def test_informative_decorator(mocker, default_conf, trading_mode): ('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' - strategy = InformativeDecoratorTest(config=default_conf) - exchange = get_patched_exchange(mocker, default_conf) + default_conf_usdt['strategy'] = 'InformativeDecoratorTest' + strategy = StrategyResolver.load_strategy(default_conf_usdt) + exchange = get_patched_exchange(mocker, default_conf_usdt) strategy.dp = DataProvider({}, exchange, None) mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[ 'XRP/USDT', 'LTC/USDT', 'NEO/USDT'