diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index ebd8cbe8c..0214ce5c5 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -39,7 +39,6 @@ A strategy file contains all the information needed to build a good strategy: - Sell strategy rules - Minimal ROI recommended - Stoploss recommended -- Hyperopt parameter 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` @@ -61,17 +60,16 @@ file as reference.** ### Buy strategy -Edit the method `populate_buy_trend()` into your strategy file to -update your buy strategy. +Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy. Sample from `user_data/strategies/test_strategy.py`: ```python -def populate_buy_trend(self, dataframe: DataFrame, pair: str) -> DataFrame: +def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame populated with indicators - :param pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ @@ -93,11 +91,11 @@ Please note that the sell-signal is only used if `use_sell_signal` is set to tru Sample from `user_data/strategies/test_strategy.py`: ```python -def populate_sell_trend(self, dataframe: DataFrame, pair: str) -> DataFrame: +def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame populated with indicators - :param pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ @@ -110,7 +108,7 @@ def populate_sell_trend(self, dataframe: DataFrame, pair: str) -> DataFrame: return dataframe ``` -## Add more Indicator +## Add more Indicators 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. @@ -119,9 +117,16 @@ You should only add the indicators used in either `populate_buy_trend()`, `popul Sample: ```python -def populate_indicators(self, dataframe: DataFrame, pair: str) -> DataFrame: +def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> 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['adx'] = ta.ADX(dataframe) @@ -152,6 +157,11 @@ def populate_indicators(self, dataframe: DataFrame, pair: str) -> 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 Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py). diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e3c3974be..593af619c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -230,7 +230,7 @@ class Backtesting(object): pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run ticker_data = self.advise_sell( - self.advise_buy(pair_data, pair), pair)[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 ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index f0d81e3ff..086cad5aa 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -75,7 +75,7 @@ class Hyperopt(Backtesting): return arg_dict @staticmethod - def populate_indicators(dataframe: DataFrame, pair: str) -> DataFrame: + def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] @@ -228,7 +228,7 @@ class Hyperopt(Backtesting): """ Define the buy strategy parameters to be used by hyperopt """ - def populate_buy_trend(dataframe: DataFrame, pair: str) -> DataFrame: + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: """ Buy strategy Hyperopt will build and use """ diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index 9eaf093ae..f1646779b 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -28,7 +28,7 @@ class DefaultStrategy(IStrategy): # Optimal ticker interval for the strategy ticker_interval = '5m' - def populate_indicators(self, dataframe: DataFrame, pair: str) -> DataFrame: + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame @@ -36,7 +36,7 @@ class DefaultStrategy(IStrategy): 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 pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ @@ -199,11 +199,11 @@ class DefaultStrategy(IStrategy): return dataframe - def populate_buy_trend(self, dataframe: DataFrame, pair: str) -> DataFrame: + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame - :param pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ @@ -221,11 +221,11 @@ class DefaultStrategy(IStrategy): return dataframe - def populate_sell_trend(self, dataframe: DataFrame, pair: str) -> DataFrame: + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame - :param pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 887c1d583..dfd624393 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -74,29 +74,29 @@ class IStrategy(ABC): self.config = config @abstractmethod - def populate_indicators(self, dataframe: DataFrame, pair: str) -> DataFrame: + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ 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 pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ @abstractmethod - def populate_buy_trend(self, dataframe: DataFrame, pair: str) -> DataFrame: + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame - :param pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ @abstractmethod - def populate_sell_trend(self, dataframe: DataFrame, pair: str) -> DataFrame: + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame - :param pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: DataFrame with sell column """ @@ -106,16 +106,16 @@ class IStrategy(ABC): """ return self.__class__.__name__ - def analyze_ticker(self, ticker_history: List[Dict], pair: str) -> DataFrame: + def analyze_ticker(self, ticker_history: List[Dict], metadata: dict) -> DataFrame: """ Parses the given ticker history and returns a populated DataFrame add several TA indicators and buy signal to it :return DataFrame with ticker data and indicator data """ dataframe = parse_ticker_dataframe(ticker_history) - dataframe = self.advise_indicators(dataframe, pair) - dataframe = self.advise_buy(dataframe, pair) - dataframe = self.advise_sell(dataframe, pair) + dataframe = self.advise_indicators(dataframe, metadata) + dataframe = self.advise_buy(dataframe, metadata) + dataframe = self.advise_sell(dataframe, metadata) return dataframe def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]: @@ -130,7 +130,7 @@ class IStrategy(ABC): return False, False try: - dataframe = self.analyze_ticker(ticker_hist, pair) + dataframe = self.analyze_ticker(ticker_hist, {'pair': pair}) except ValueError as error: logger.warning( 'Unable to analyze ticker for pair %s: %s', @@ -275,15 +275,15 @@ class IStrategy(ABC): """ Creates a dataframe and populates indicators for given ticker data """ - return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), pair) + return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), {'pair': pair}) for pair, pair_data in tickerdata.items()} - def advise_indicators(self, dataframe: DataFrame, pair: str) -> DataFrame: + 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 pair: The currently traded pair + :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: @@ -291,14 +291,14 @@ class IStrategy(ABC): "the current function headers!", DeprecationWarning) return self.populate_indicators(dataframe) # type: ignore else: - return self.populate_indicators(dataframe, pair) + return self.populate_indicators(dataframe, metadata) - def advise_buy(self, dataframe: DataFrame, pair: str) -> DataFrame: + 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: The currently traded pair + :param pair: Additional information, like the currently traded pair :return: DataFrame with buy column """ if self._buy_fun_len == 2: @@ -306,14 +306,14 @@ class IStrategy(ABC): "the current function headers!", DeprecationWarning) return self.populate_buy_trend(dataframe) # type: ignore else: - return self.populate_buy_trend(dataframe, pair) + return self.populate_buy_trend(dataframe, metadata) - def advise_sell(self, dataframe: DataFrame, pair: str) -> DataFrame: + 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: The currently traded pair + :param pair: Additional information, like the currently traded pair :return: DataFrame with sell column """ if self._sell_fun_len == 2: @@ -321,4 +321,4 @@ class IStrategy(ABC): "the current function headers!", DeprecationWarning) return self.populate_sell_trend(dataframe) # type: ignore else: - return self.populate_sell_trend(dataframe, pair) + return self.populate_sell_trend(dataframe, metadata) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 02eea312f..1c8f80ca1 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -60,10 +60,7 @@ def test_search_strategy(): def test_load_strategy(result): resolver = StrategyResolver({'strategy': 'TestStrategy'}) pair = 'ETH/BTC' - assert len(resolver.strategy.populate_indicators.__annotations__) == 3 - assert 'dataframe' in resolver.strategy.populate_indicators.__annotations__ - assert 'pair' in resolver.strategy.populate_indicators.__annotations__ - assert 'adx' in resolver.strategy.advise_indicators(result, pair=pair) + assert 'adx' in resolver.strategy.advise_indicators(result, metadata=pair) def test_load_strategy_invalid_directory(result, caplog): @@ -92,7 +89,7 @@ def test_strategy(result): config = {'strategy': 'DefaultStrategy'} resolver = StrategyResolver(config) - pair = 'ETH/BTC' + metadata = {'pair': 'ETH/BTC'} assert resolver.strategy.minimal_roi[0] == 0.04 assert config["minimal_roi"]['0'] == 0.04 @@ -102,13 +99,13 @@ def test_strategy(result): assert resolver.strategy.ticker_interval == '5m' assert config['ticker_interval'] == '5m' - df_indicators = resolver.strategy.advise_indicators(result, pair=pair) + df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata) assert 'adx' in df_indicators - dataframe = resolver.strategy.advise_buy(df_indicators, pair=pair) + dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata) assert 'buy' in dataframe.columns - dataframe = resolver.strategy.advise_sell(df_indicators, pair='ETH/BTC') + dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata) assert 'sell' in dataframe.columns @@ -196,21 +193,21 @@ def test_call_deprecated_function(result, monkeypatch): default_location = path.join(path.dirname(path.realpath(__file__))) resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', 'strategy_path': default_location}) - pair = 'ETH/BTC' + 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, pair=pair) + 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, pair=pair) + buydf = resolver.strategy.advise_buy(result, metadata=metadata) assert type(buydf) is DataFrame assert 'buy' in buydf.columns - selldf = resolver.strategy.advise_sell(result, pair=pair) + selldf = resolver.strategy.advise_sell(result, metadata=metadata) assert type(selldf) is DataFrame assert 'sell' in selldf diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 65e06558c..80c238d92 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -45,7 +45,7 @@ class TestStrategy(IStrategy): # Optimal ticker interval for the strategy ticker_interval = '5m' - def populate_indicators(self, dataframe: DataFrame, pair: str) -> DataFrame: + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame @@ -53,7 +53,7 @@ class TestStrategy(IStrategy): 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 pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ @@ -215,11 +215,11 @@ class TestStrategy(IStrategy): return dataframe - def populate_buy_trend(self, dataframe: DataFrame, pair: str) -> DataFrame: + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame populated with indicators - :param pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ @@ -232,11 +232,11 @@ class TestStrategy(IStrategy): return dataframe - def populate_sell_trend(self, dataframe: DataFrame, pair: str) -> DataFrame: + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame populated with indicators - :param pair: Pair currently analyzed + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[