Informative decorator updates for futures

This commit is contained in:
Matthias 2022-01-28 16:58:07 +01:00
parent fdea4fcb1b
commit c620e38c7d
4 changed files with 56 additions and 35 deletions

View File

@ -15,11 +15,13 @@ class InformativeData(NamedTuple):
timeframe: str timeframe: str
fmt: Union[str, Callable[[Any], str], None] fmt: Union[str, Callable[[Any], str], None]
ffill: bool ffill: bool
candle_type: CandleType candle_type: Optional[CandleType]
def informative(timeframe: str, asset: str = '', def informative(timeframe: str, asset: str = '',
fmt: Optional[Union[str, Callable[[Any], str]]] = None, fmt: Optional[Union[str, Callable[[Any], str]]] = None,
*,
candle_type: Optional[CandleType] = None,
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
""" """
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to 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 _timeframe = timeframe
_fmt = fmt _fmt = fmt
_ffill = ffill _ffill = ffill
_candle_type = CandleType.from_string(candle_type) if candle_type else None
def decorator(fn: PopulateIndicators): def decorator(fn: PopulateIndicators):
informative_pairs = getattr(fn, '_ft_informative', []) informative_pairs = getattr(fn, '_ft_informative', [])
# TODO-lev: Add candle_type to InformativeData informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill, _candle_type))
informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill,
CandleType.SPOT))
setattr(fn, '_ft_informative', informative_pairs) setattr(fn, '_ft_informative', informative_pairs)
return fn return fn
return decorator return decorator
@ -76,6 +77,8 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata:
asset = inf_data.asset or '' asset = inf_data.asset or ''
timeframe = inf_data.timeframe timeframe = inf_data.timeframe
fmt = inf_data.fmt fmt = inf_data.fmt
candle_type = inf_data.candle_type
config = strategy.config config = strategy.config
if asset: if asset:
@ -102,7 +105,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata:
fmt = '{base}_{quote}_' + fmt # Informatives of other pairs fmt = '{base}_{quote}_' + fmt # Informatives of other pairs
inf_metadata = {'pair': asset, 'timeframe': timeframe} 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) inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata)
formatter: Any = None formatter: Any = None

View File

@ -146,7 +146,8 @@ class IStrategy(ABC, HyperStrategyMixin):
cls_method = getattr(self.__class__, attr_name) cls_method = getattr(self.__class__, attr_name)
if not callable(cls_method): if not callable(cls_method):
continue 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): if not isinstance(informative_data_list, list):
# Type check is required because mocker would return a mock object that evaluates to # Type check is required because mocker would return a mock object that evaluates to
# True, confusing this code. # True, confusing this code.
@ -156,6 +157,10 @@ class IStrategy(ABC, HyperStrategyMixin):
if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes: if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes:
raise OperationalException('Informative timeframe must be equal or higher than ' raise OperationalException('Informative timeframe must be equal or higher than '
'strategy timeframe!') '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)) self._ft_informative.append((informative_data, cls_method))
@abstractmethod @abstractmethod
@ -456,14 +461,17 @@ class IStrategy(ABC, HyperStrategyMixin):
# Compatibility code for 2 tuple informative pairs # Compatibility code for 2 tuple informative pairs
informative_pairs = [ informative_pairs = [
(p[0], p[1], CandleType.from_string(p[2]) if len( (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 p in informative_pairs]
for inf_data, _ in self._ft_informative: 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: if inf_data.asset:
pair_tf = ( pair_tf = (
_format_pair_name(self.config, inf_data.asset), _format_pair_name(self.config, inf_data.asset),
inf_data.timeframe, inf_data.timeframe,
inf_data.candle_type candle_type,
) )
informative_pairs.append(pair_tf) informative_pairs.append(pair_tf)
else: else:
@ -471,7 +479,7 @@ class IStrategy(ABC, HyperStrategyMixin):
raise OperationalException('@informative decorator with unspecified asset ' raise OperationalException('@informative decorator with unspecified asset '
'requires DataProvider instance.') 'requires DataProvider instance.')
for pair in self.dp.current_whitelist(): 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)) return list(set(informative_pairs))
def get_strategy_name(self) -> str: def get_strategy_name(self) -> str:

View File

@ -20,7 +20,11 @@ class InformativeDecoratorTest(IStrategy):
def informative_pairs(self): def informative_pairs(self):
# Intentionally return 2 tuples, must be converted to 3 in compatibility code # 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: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['buy'] = 0 dataframe['buy'] = 0
@ -44,7 +48,7 @@ class InformativeDecoratorTest(IStrategy):
return dataframe return dataframe
# Quote currency different from stake currency test. # 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: def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['rsi'] = 14 dataframe['rsi'] = 14
return dataframe return dataframe

View File

@ -171,24 +171,27 @@ def test_stoploss_from_absolute():
assert pytest.approx(stoploss_from_absolute(100, 1, True)) == 1 assert pytest.approx(stoploss_from_absolute(100, 1, True)) == 1
# TODO-lev: @pytest.mark.parametrize('candle_type', ['mark', '']) @pytest.mark.parametrize('trading_mode', ['futures', 'spot'])
def test_informative_decorator(mocker, default_conf): 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_5m = generate_test_data('5m', 40)
test_data_30m = generate_test_data('30m', 40) test_data_30m = generate_test_data('30m', 40)
test_data_1h = generate_test_data('1h', 40) test_data_1h = generate_test_data('1h', 40)
data = { data = {
('XRP/USDT', '5m', CandleType.SPOT): test_data_5m, ('XRP/USDT', '5m', candle_def): test_data_5m,
('XRP/USDT', '30m', CandleType.SPOT): test_data_30m, ('XRP/USDT', '30m', candle_def): test_data_30m,
('XRP/USDT', '1h', CandleType.SPOT): test_data_1h, ('XRP/USDT', '1h', candle_def): test_data_1h,
('LTC/USDT', '5m', CandleType.SPOT): test_data_5m, ('LTC/USDT', '5m', candle_def): test_data_5m,
('LTC/USDT', '30m', CandleType.SPOT): test_data_30m, ('LTC/USDT', '30m', candle_def): test_data_30m,
('LTC/USDT', '1h', CandleType.SPOT): test_data_1h, ('LTC/USDT', '1h', candle_def): test_data_1h,
('NEO/USDT', '30m', CandleType.SPOT): test_data_30m, ('NEO/USDT', '30m', candle_def): test_data_30m,
('NEO/USDT', '5m', CandleType.SPOT): test_data_5m, ('NEO/USDT', '5m', CandleType.SPOT): test_data_5m, # Explicit request with '' as candletype
('NEO/USDT', '1h', CandleType.SPOT): test_data_1h, ('NEO/USDT', '15m', candle_def): test_data_5m, # Explicit request with '' as candletype
('ETH/USDT', '1h', CandleType.SPOT): test_data_1h, ('NEO/USDT', '1h', candle_def): test_data_1h,
('ETH/USDT', '30m', CandleType.SPOT): test_data_30m, ('ETH/USDT', '1h', candle_def): test_data_1h,
('ETH/BTC', '1h', CandleType.SPOT): 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 from .strats.informative_decorator_strategy import InformativeDecoratorTest
default_conf['stake_currency'] = 'USDT' 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 assert len(strategy._ft_informative) == 6 # Equal to number of decorators used
informative_pairs = [ informative_pairs = [
('XRP/USDT', '1h', CandleType.SPOT), ('XRP/USDT', '1h', candle_def),
('LTC/USDT', '1h', CandleType.SPOT), ('LTC/USDT', '1h', candle_def),
('XRP/USDT', '30m', CandleType.SPOT), ('XRP/USDT', '30m', candle_def),
('LTC/USDT', '30m', CandleType.SPOT), ('LTC/USDT', '30m', candle_def),
('NEO/USDT', '1h', CandleType.SPOT), ('NEO/USDT', '1h', candle_def),
('NEO/USDT', '30m', CandleType.SPOT), ('NEO/USDT', '30m', candle_def),
('NEO/USDT', '5m', CandleType.SPOT), ('NEO/USDT', '5m', candle_def),
('ETH/BTC', '1h', CandleType.SPOT), ('NEO/USDT', '15m', candle_def),
('ETH/USDT', '30m', CandleType.SPOT)] ('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: for inf_pair in informative_pairs:
assert inf_pair in strategy.gather_informative_pairs() assert inf_pair in strategy.gather_informative_pairs()
def test_historic_ohlcv(pair, timeframe, candle_type): def test_historic_ohlcv(pair, timeframe, candle_type):
return data[ return data[
(pair, timeframe or strategy.timeframe, CandleType.from_string(candle_type))].copy() (pair, timeframe or strategy.timeframe, CandleType.from_string(candle_type))].copy()
mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv', mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv',
side_effect=test_historic_ohlcv) side_effect=test_historic_ohlcv)
analyzed = strategy.advise_all_indicators( 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 = [ expected_columns = [
'rsi_1h', 'rsi_30m', # Stacked informative decorators 'rsi_1h', 'rsi_30m', # Stacked informative decorators
'neo_usdt_rsi_1h', # NEO 1h informative 'neo_usdt_rsi_1h', # NEO 1h informative