Merge pull request #6312 from freqtrade/short_informative_decorator
Short informative decorator
This commit is contained in:
commit
cf7edace2b
@ -78,8 +78,9 @@ class DataProvider:
|
|||||||
:param timeframe: timeframe to get data for
|
:param timeframe: timeframe to get data for
|
||||||
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
:param candle_type: '', mark, index, premiumIndex, or funding_rate
|
||||||
"""
|
"""
|
||||||
candleType = CandleType.from_string(candle_type)
|
_candle_type = CandleType.from_string(
|
||||||
saved_pair = (pair, str(timeframe), candleType)
|
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:
|
if saved_pair not in self.__cached_pairs_backtesting:
|
||||||
timerange = TimeRange.parse_timerange(None if self._config.get(
|
timerange = TimeRange.parse_timerange(None if self._config.get(
|
||||||
'timerange') is None else str(self._config.get('timerange')))
|
'timerange') is None else str(self._config.get('timerange')))
|
||||||
@ -93,7 +94,7 @@ class DataProvider:
|
|||||||
datadir=self._config['datadir'],
|
datadir=self._config['datadir'],
|
||||||
timerange=timerange,
|
timerange=timerange,
|
||||||
data_format=self._config.get('dataformat_ohlcv', 'json'),
|
data_format=self._config.get('dataformat_ohlcv', 'json'),
|
||||||
candle_type=candleType,
|
candle_type=_candle_type,
|
||||||
|
|
||||||
)
|
)
|
||||||
return self.__cached_pairs_backtesting[saved_pair].copy()
|
return self.__cached_pairs_backtesting[saved_pair].copy()
|
||||||
@ -221,8 +222,10 @@ class DataProvider:
|
|||||||
if self._exchange is None:
|
if self._exchange is None:
|
||||||
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
||||||
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
|
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(
|
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
|
copy=copy
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -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
|
from pandas import DataFrame
|
||||||
|
|
||||||
@ -10,16 +11,19 @@ from freqtrade.strategy.strategy_helper import merge_informative_pair
|
|||||||
PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame]
|
PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame]
|
||||||
|
|
||||||
|
|
||||||
class InformativeData(NamedTuple):
|
@dataclass
|
||||||
|
class InformativeData:
|
||||||
asset: Optional[str]
|
asset: Optional[str]
|
||||||
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 +58,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 +79,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 +107,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
|
||||||
|
@ -147,7 +147,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.
|
||||||
@ -157,6 +158,8 @@ 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.candle_type = config['candle_type_def']
|
||||||
self._ft_informative.append((informative_data, cls_method))
|
self._ft_informative.append((informative_data, cls_method))
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -465,14 +468,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:
|
||||||
@ -480,7 +486,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:
|
||||||
|
@ -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
|
||||||
|
@ -6,6 +6,7 @@ import pytest
|
|||||||
|
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.enums import CandleType
|
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,
|
from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open,
|
||||||
timeframe_to_minutes)
|
timeframe_to_minutes)
|
||||||
from tests.conftest import get_patched_exchange
|
from tests.conftest import get_patched_exchange
|
||||||
@ -171,29 +172,31 @@ 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_usdt, trading_mode):
|
||||||
|
candle_def = CandleType.get_default(trading_mode)
|
||||||
|
default_conf_usdt['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
|
default_conf_usdt['strategy'] = 'InformativeDecoratorTest'
|
||||||
default_conf['stake_currency'] = 'USDT'
|
strategy = StrategyResolver.load_strategy(default_conf_usdt)
|
||||||
strategy = InformativeDecoratorTest(config=default_conf)
|
exchange = get_patched_exchange(mocker, default_conf_usdt)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
|
||||||
strategy.dp = DataProvider({}, exchange, None)
|
strategy.dp = DataProvider({}, exchange, None)
|
||||||
mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[
|
mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[
|
||||||
'XRP/USDT', 'LTC/USDT', 'NEO/USDT'
|
'XRP/USDT', 'LTC/USDT', 'NEO/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
|
||||||
|
Loading…
Reference in New Issue
Block a user