Merge pull request #1044 from freqtrade/pair_to_strat

pair to strategy enhancement
This commit is contained in:
Janne Sinivirta 2018-07-30 20:18:46 +03:00 committed by GitHub
commit 81cf7229be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 459 additions and 74 deletions

View File

@ -39,7 +39,6 @@ A strategy file contains all the information needed to build a good strategy:
- Sell strategy rules - Sell strategy rules
- Minimal ROI recommended - Minimal ROI recommended
- Stoploss recommended - Stoploss recommended
- Hyperopt parameter
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`. The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
You can test it with the parameter: `--strategy TestStrategy` You can test it with the parameter: `--strategy TestStrategy`
@ -61,22 +60,22 @@ file as reference.**
### Buy strategy ### Buy strategy
Edit the method `populate_buy_trend()` into your strategy file to Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy.
update your buy strategy.
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/test_strategy.py`:
```python ```python
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 30) & (dataframe['adx'] > 30) &
(dataframe['tema'] <= dataframe['blower']) & (dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1)) (dataframe['tema'] > dataframe['tema'].shift(1))
), ),
'buy'] = 1 'buy'] = 1
@ -87,38 +86,47 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
### Sell strategy ### Sell strategy
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy. Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration.
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/test_strategy.py`:
```python ```python
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 70) & (dataframe['adx'] > 70) &
(dataframe['tema'] > dataframe['blower']) & (dataframe['tema'] > dataframe['bb_middleband']) &
(dataframe['tema'] < dataframe['tema'].shift(1)) (dataframe['tema'] < dataframe['tema'].shift(1))
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
``` ```
## Add more Indicator ## Add more Indicators
As you have seen, buy and sell strategies need indicators. You can add As you have seen, buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
more indicators by extending the list contained in
the method `populate_indicators()` from your strategy file. You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer.
Sample: Sample:
```python ```python
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given 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: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
""" """
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
@ -149,6 +157,11 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
return dataframe return dataframe
``` ```
### Metadata dict
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
### Want more indicator examples ### Want more indicator examples
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py). Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).

View File

@ -57,8 +57,8 @@ class Backtesting(object):
self.strategy: IStrategy = StrategyResolver(self.config).strategy self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.ticker_interval = self.strategy.ticker_interval self.ticker_interval = self.strategy.ticker_interval
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
self.populate_buy_trend = self.strategy.populate_buy_trend self.advise_buy = self.strategy.advise_buy
self.populate_sell_trend = self.strategy.populate_sell_trend self.advise_sell = self.strategy.advise_sell
# Reset keys for backtesting # Reset keys for backtesting
self.config['exchange']['key'] = '' self.config['exchange']['key'] = ''
@ -229,8 +229,8 @@ class Backtesting(object):
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = self.populate_sell_trend( ticker_data = self.advise_sell(
self.populate_buy_trend(pair_data))[headers].copy() self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
# to avoid using data from future, we buy/sell with signal from previous candle # to avoid using data from future, we buy/sell with signal from previous candle
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)

View File

@ -75,7 +75,7 @@ class Hyperopt(Backtesting):
return arg_dict return arg_dict
@staticmethod @staticmethod
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'] dataframe['macd'] = macd['macd']
@ -228,7 +228,7 @@ class Hyperopt(Backtesting):
""" """
Define the buy strategy parameters to be used by hyperopt Define the buy strategy parameters to be used by hyperopt
""" """
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Buy strategy Hyperopt will build and use Buy strategy Hyperopt will build and use
""" """
@ -270,7 +270,7 @@ class Hyperopt(Backtesting):
self.strategy.minimal_roi = self.generate_roi_table(params) self.strategy.minimal_roi = self.generate_roi_table(params)
if self.has_space('buy'): if self.has_space('buy'):
self.populate_buy_trend = self.buy_strategy_generator(params) self.advise_buy = self.buy_strategy_generator(params)
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.strategy.stoploss = params['stoploss'] self.strategy.stoploss = params['stoploss']
@ -351,7 +351,7 @@ class Hyperopt(Backtesting):
) )
if self.has_space('buy'): if self.has_space('buy'):
self.strategy.populate_indicators = Hyperopt.populate_indicators # type: ignore self.strategy.advise_indicators = Hyperopt.populate_indicators # type: ignore
dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
self.exchange = None # type: ignore self.exchange = None # type: ignore
self.load_previous_results() self.load_previous_results()

View File

@ -28,13 +28,16 @@ class DefaultStrategy(IStrategy):
# Optimal ticker interval for the strategy # Optimal ticker interval for the strategy
ticker_interval = '5m' ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators 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 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. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
""" """
# Momentum Indicator # Momentum Indicator
@ -196,10 +199,11 @@ class DefaultStrategy(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
@ -217,10 +221,11 @@ class DefaultStrategy(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[

View File

@ -7,6 +7,7 @@ from abc import ABC, abstractmethod
from datetime import datetime from datetime import datetime
from enum import Enum from enum import Enum
from typing import Dict, List, NamedTuple, Tuple from typing import Dict, List, NamedTuple, Tuple
import warnings
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
@ -57,34 +58,45 @@ class IStrategy(ABC):
ticker_interval -> str: value of the ticker interval to use for the strategy ticker_interval -> str: value of the ticker interval to use for the strategy
""" """
_populate_fun_len: int = 0
_buy_fun_len: int = 0
_sell_fun_len: int = 0
# associated minimal roi
minimal_roi: Dict minimal_roi: Dict
# associated stoploss
stoploss: float stoploss: float
# associated ticker interval
ticker_interval: str ticker_interval: str
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
self.config = config self.config = config
@abstractmethod @abstractmethod
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Populate indicators that will be used in the Buy and Sell strategy Populate indicators that will be used in the Buy and Sell strategy
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """
@abstractmethod @abstractmethod
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
@abstractmethod @abstractmethod
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with sell column :return: DataFrame with sell column
""" """
@ -94,16 +106,16 @@ class IStrategy(ABC):
""" """
return self.__class__.__name__ return self.__class__.__name__
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame: def analyze_ticker(self, ticker_history: List[Dict], metadata: dict) -> DataFrame:
""" """
Parses the given ticker history and returns a populated DataFrame Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data :return DataFrame with ticker data and indicator data
""" """
dataframe = parse_ticker_dataframe(ticker_history) dataframe = parse_ticker_dataframe(ticker_history)
dataframe = self.populate_indicators(dataframe) dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.populate_buy_trend(dataframe) dataframe = self.advise_buy(dataframe, metadata)
dataframe = self.populate_sell_trend(dataframe) dataframe = self.advise_sell(dataframe, metadata)
return dataframe return dataframe
def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]: def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]:
@ -118,7 +130,7 @@ class IStrategy(ABC):
return False, False return False, False
try: try:
dataframe = self.analyze_ticker(ticker_hist) dataframe = self.analyze_ticker(ticker_hist, {'pair': pair})
except ValueError as error: except ValueError as error:
logger.warning( logger.warning(
'Unable to analyze ticker for pair %s: %s', 'Unable to analyze ticker for pair %s: %s',
@ -263,5 +275,50 @@ class IStrategy(ABC):
""" """
Creates a dataframe and populates indicators for given ticker data Creates a dataframe and populates indicators for given ticker data
""" """
return {pair: self.populate_indicators(parse_ticker_dataframe(pair_data)) return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), {'pair': pair})
for pair, pair_data in tickerdata.items()} for pair, pair_data in tickerdata.items()}
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Populate indicators that will be used in the Buy and Sell strategy
This method should not be overridden.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
if self._populate_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_indicators(dataframe) # type: ignore
else:
return self.populate_indicators(dataframe, metadata)
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
if self._buy_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_buy_trend(dataframe) # type: ignore
else:
return self.populate_buy_trend(dataframe, metadata)
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:return: DataFrame with sell column
"""
if self._sell_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_sell_trend(dataframe) # type: ignore
else:
return self.populate_sell_trend(dataframe, metadata)

View File

@ -92,6 +92,13 @@ class StrategyResolver(object):
strategy = self._search_strategy(path, strategy_name=strategy_name, config=config) strategy = self._search_strategy(path, strategy_name=strategy_name, config=config)
if strategy: if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
strategy._populate_fun_len = len(
inspect.getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(
inspect.getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(
inspect.getfullargspec(strategy.populate_sell_trend).args)
return import_strategy(strategy, config=config) return import_strategy(strategy, config=config)
except FileNotFoundError: except FileNotFoundError:
logger.warning('Path "%s" does not exist', path) logger.warning('Path "%s" does not exist', path)

View File

@ -146,7 +146,7 @@ def _trend(signals, buy_value, sell_value):
return signals return signals
def _trend_alternate(dataframe=None): def _trend_alternate(dataframe=None, metadata=None):
signals = dataframe signals = dataframe
low = signals['low'] low = signals['low']
n = len(low) n = len(low)
@ -332,8 +332,8 @@ def test_backtesting_init(mocker, default_conf) -> None:
assert backtesting.config == default_conf assert backtesting.config == default_conf
assert backtesting.ticker_interval == '5m' assert backtesting.ticker_interval == '5m'
assert callable(backtesting.tickerdata_to_dataframe) assert callable(backtesting.tickerdata_to_dataframe)
assert callable(backtesting.populate_buy_trend) assert callable(backtesting.advise_buy)
assert callable(backtesting.populate_sell_trend) assert callable(backtesting.advise_sell)
get_fee.assert_called() get_fee.assert_called()
assert backtesting.fee == 0.5 assert backtesting.fee == 0.5
@ -611,42 +611,42 @@ def test_backtest_ticks(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
ticks = [1, 5] ticks = [1, 5]
fun = Backtesting(default_conf).populate_buy_trend fun = Backtesting(default_conf).advise_buy
for _ in ticks: for _ in ticks:
backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override backtesting.advise_buy = fun # Override
backtesting.populate_sell_trend = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert not results.empty assert not results.empty
def test_backtest_clash_buy_sell(mocker, default_conf): def test_backtest_clash_buy_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None, pair=None):
buy_value = 1 buy_value = 1
sell_value = 1 sell_value = 1
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override backtesting.advise_buy = fun # Override
backtesting.populate_sell_trend = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert results.empty assert results.empty
def test_backtest_only_sell(mocker, default_conf): def test_backtest_only_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None, pair=None):
buy_value = 0 buy_value = 0
sell_value = 1 sell_value = 1
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override backtesting.advise_buy = fun # Override
backtesting.populate_sell_trend = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert results.empty assert results.empty
@ -655,8 +655,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC') backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override backtesting.advise_buy = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override backtesting.advise_sell = _trend_alternate # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
backtesting._store_backtest_result("test_.json", results) backtesting._store_backtest_result("test_.json", results)
assert len(results) == 4 assert len(results) == 4

View File

@ -247,7 +247,7 @@ def test_populate_indicators(init_hyperopt) -> None:
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick} tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
# Check if some indicators are generated. We will not test all of them # Check if some indicators are generated. We will not test all of them
assert 'adx' in dataframe assert 'adx' in dataframe
@ -259,7 +259,7 @@ def test_buy_strategy_generator(init_hyperopt) -> None:
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick} tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
populate_buy_trend = _HYPEROPT.buy_strategy_generator( populate_buy_trend = _HYPEROPT.buy_strategy_generator(
{ {
@ -274,7 +274,7 @@ def test_buy_strategy_generator(init_hyperopt) -> None:
'trigger': 'bb_lower' 'trigger': 'bb_lower'
} }
) )
result = populate_buy_trend(dataframe) result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'})
# Check if some indicators are generated. We will not test all of them # Check if some indicators are generated. We will not test all of them
assert 'buy' in result assert 'buy' in result
assert 1 in result['buy'] assert 1 in result['buy']

View File

@ -0,0 +1,235 @@
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy # noqa
# This class is a sample. Feel free to customize it.
class TestStrategyLegacy(IStrategy):
"""
This is a test strategy using the legacy function headers, which will be
removed in a future update.
Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py
for a uptodate version of this template.
"""
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.10
# Optimal ticker interval for the strategy
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> 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.
"""
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
"""
# Awesome oscillator
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
dataframe['cci'] = ta.CCI(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# Minus Directional Indicator / Movement
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# ROC
dataframe['roc'] = ta.ROC(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
"""
# Overlap Studies
# ------------------------------------
# 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['ema3'] = ta.EMA(dataframe, timeperiod=3)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
"""
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
"""
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
"""
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
"""
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 30) &
(dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1))
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 70) &
(dataframe['tema'] > dataframe['bb_middleband']) &
(dataframe['tema'] < dataframe['tema'].shift(1))
),
'sell'] = 1
return dataframe

View File

@ -25,10 +25,11 @@ def test_default_strategy_structure():
def test_default_strategy(result): def test_default_strategy(result):
strategy = DefaultStrategy({}) strategy = DefaultStrategy({})
metadata = {'pair': 'ETH/BTC'}
assert type(strategy.minimal_roi) is dict assert type(strategy.minimal_roi) is dict
assert type(strategy.stoploss) is float assert type(strategy.stoploss) is float
assert type(strategy.ticker_interval) is str assert type(strategy.ticker_interval) is str
indicators = strategy.populate_indicators(result) indicators = strategy.populate_indicators(result, metadata)
assert type(indicators) is DataFrame assert type(indicators) is DataFrame
assert type(strategy.populate_buy_trend(indicators)) is DataFrame assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
assert type(strategy.populate_sell_trend(indicators)) is DataFrame assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame

View File

@ -1,8 +1,10 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103 # pragma pylint: disable=missing-docstring, protected-access, C0103
import logging import logging
import os from os import path
import warnings
import pytest import pytest
from pandas import DataFrame
from freqtrade.strategy import import_strategy from freqtrade.strategy import import_strategy
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
@ -37,8 +39,8 @@ def test_import_strategy(caplog):
def test_search_strategy(): def test_search_strategy():
default_config = {} default_config = {}
default_location = os.path.join(os.path.dirname( default_location = path.join(path.dirname(
os.path.realpath(__file__)), '..', '..', 'strategy' path.realpath(__file__)), '..', '..', 'strategy'
) )
assert isinstance( assert isinstance(
StrategyResolver._search_strategy( StrategyResolver._search_strategy(
@ -57,12 +59,13 @@ def test_search_strategy():
def test_load_strategy(result): def test_load_strategy(result):
resolver = StrategyResolver({'strategy': 'TestStrategy'}) resolver = StrategyResolver({'strategy': 'TestStrategy'})
assert 'adx' in resolver.strategy.populate_indicators(result) metadata = {'pair': 'ETH/BTC'}
assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata)
def test_load_strategy_invalid_directory(result, caplog): def test_load_strategy_invalid_directory(result, caplog):
resolver = StrategyResolver() resolver = StrategyResolver()
extra_dir = os.path.join('some', 'path') extra_dir = path.join('some', 'path')
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
assert ( assert (
@ -70,7 +73,8 @@ def test_load_strategy_invalid_directory(result, caplog):
logging.WARNING, logging.WARNING,
'Path "{}" does not exist'.format(extra_dir), 'Path "{}" does not exist'.format(extra_dir),
) in caplog.record_tuples ) in caplog.record_tuples
assert 'adx' in resolver.strategy.populate_indicators(result)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_not_found_strategy(): def test_load_not_found_strategy():
@ -85,7 +89,7 @@ def test_strategy(result):
config = {'strategy': 'DefaultStrategy'} config = {'strategy': 'DefaultStrategy'}
resolver = StrategyResolver(config) resolver = StrategyResolver(config)
metadata = {'pair': 'ETH/BTC'}
assert resolver.strategy.minimal_roi[0] == 0.04 assert resolver.strategy.minimal_roi[0] == 0.04
assert config["minimal_roi"]['0'] == 0.04 assert config["minimal_roi"]['0'] == 0.04
@ -95,12 +99,13 @@ def test_strategy(result):
assert resolver.strategy.ticker_interval == '5m' assert resolver.strategy.ticker_interval == '5m'
assert config['ticker_interval'] == '5m' assert config['ticker_interval'] == '5m'
assert 'adx' in resolver.strategy.populate_indicators(result) df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata)
assert 'adx' in df_indicators
dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result)) dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata)
assert 'buy' in dataframe.columns assert 'buy' in dataframe.columns
dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result)) dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata)
assert 'sell' in dataframe.columns assert 'sell' in dataframe.columns
@ -150,3 +155,59 @@ def test_strategy_override_ticker_interval(caplog):
logging.INFO, logging.INFO,
'Override strategy \'ticker_interval\' with value in config file: 60.' 'Override strategy \'ticker_interval\' with value in config file: 60.'
) in caplog.record_tuples ) in caplog.record_tuples
def test_deprecate_populate_indicators(result):
default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location})
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC')
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \
in str(w[-1].message)
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
resolver.strategy.advise_buy(indicators, 'ETH/BTC')
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \
in str(w[-1].message)
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
resolver.strategy.advise_sell(indicators, 'ETH_BTC')
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \
in str(w[-1].message)
def test_call_deprecated_function(result, monkeypatch):
default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location})
metadata = {'pair': 'ETH/BTC'}
# Make sure we are using a legacy function
assert resolver.strategy._populate_fun_len == 2
assert resolver.strategy._buy_fun_len == 2
assert resolver.strategy._sell_fun_len == 2
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
assert type(indicator_df) is DataFrame
assert 'adx' in indicator_df.columns
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
assert type(buydf) is DataFrame
assert 'buy' in buydf.columns
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
assert type(selldf) is DataFrame
assert 'sell' in selldf

View File

@ -14,7 +14,7 @@ def load_dataframe_pair(pairs, strategy):
assert isinstance(pairs[0], str) assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]] dataframe = ld[pairs[0]]
dataframe = strategy.analyze_ticker(dataframe) dataframe = strategy.analyze_ticker(dataframe, pairs[0])
return dataframe return dataframe

View File

@ -159,8 +159,8 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
dataframes = strategy.tickerdata_to_dataframe(tickers) dataframes = strategy.tickerdata_to_dataframe(tickers)
dataframe = dataframes[pair] dataframe = dataframes[pair]
dataframe = strategy.populate_buy_trend(dataframe) dataframe = strategy.advise_buy(dataframe, {'pair': pair})
dataframe = strategy.populate_sell_trend(dataframe) dataframe = strategy.advise_sell(dataframe, {'pair': pair})
if len(dataframe.index) > args.plot_limit: if len(dataframe.index) > args.plot_limit:
logger.warning('Ticker contained more than %s candles as defined ' logger.warning('Ticker contained more than %s candles as defined '

View File

@ -18,6 +18,7 @@ class TestStrategy(IStrategy):
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
You can: You can:
:return: a Dataframe with all mandatory indicators for the strategies
- Rename the class name (Do not forget to update class_name) - Rename the class name (Do not forget to update class_name)
- Add any methods you want to build your strategy - Add any methods you want to build your strategy
- Add any lib you need to build your strategy - Add any lib you need to build your strategy
@ -44,13 +45,16 @@ class TestStrategy(IStrategy):
# Optimal ticker interval for the strategy # Optimal ticker interval for the strategy
ticker_interval = '5m' ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators 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 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. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
""" """
# Momentum Indicator # Momentum Indicator
@ -211,10 +215,11 @@ class TestStrategy(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
@ -227,10 +232,11 @@ class TestStrategy(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[