From 0f53e646fd7d3e707f470244d785539211a17793 Mon Sep 17 00:00:00 2001 From: orehunt Date: Tue, 24 Mar 2020 13:54:46 +0100 Subject: [PATCH 1/6] check that the strategy dataframe matches the one given by the bot --- freqtrade/strategy/interface.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 696d2b2d2..530cd0af4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -241,8 +241,18 @@ class IStrategy(ABC): return dataframe - def get_signal(self, pair: str, interval: str, - dataframe: DataFrame) -> Tuple[bool, bool]: + @staticmethod + def preserve_df(d: DataFrame) -> Tuple[int, float, datetime]: + """ keep some data for dataframes """ + return len(d), d["close"].iloc[-1], d["date"].iloc[-1] + + @staticmethod + def assert_df(d: DataFrame, df_len: int, df_close: float, df_date: datetime): + """ make sure data is unmodified """ + if df_len != len(d) or df_close != d["close"].iloc[-1] or df_date != d["date"].iloc[-1]: + raise Exception("Dataframe returned from strategy does not match original") + + def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ Calculates current signal based several technical analysis indicators :param pair: pair in format ANT/BTC @@ -254,8 +264,11 @@ class IStrategy(ABC): logger.warning('Empty candle (OHLCV) data for pair %s', pair) return False, False + latest_date = dataframe['date'].max() try: + df_len, df_close, df_date = self.preserve_df(dataframe) dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) + self.assert_df(dataframe, df_len, df_close, df_date) except ValueError as error: logger.warning( 'Unable to analyze candle (OHLCV) data for pair %s: %s', @@ -275,7 +288,7 @@ class IStrategy(ABC): logger.warning('Empty dataframe for pair %s', pair) return False, False - latest = dataframe.iloc[-1] + latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] # Check if dataframe is out of date signal_date = arrow.get(latest['date']) From 3ef568029f48a55dc22b64d4ea904da75d50b2d3 Mon Sep 17 00:00:00 2001 From: orehunt Date: Thu, 26 Mar 2020 07:05:30 +0100 Subject: [PATCH 2/6] different exception messages --- freqtrade/strategy/interface.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 530cd0af4..4f833be23 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -16,6 +16,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.wallets import Wallets +from freqtrade.exceptions import DependencyException logger = logging.getLogger(__name__) @@ -242,15 +243,23 @@ class IStrategy(ABC): return dataframe @staticmethod - def preserve_df(d: DataFrame) -> Tuple[int, float, datetime]: + def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]: """ keep some data for dataframes """ - return len(d), d["close"].iloc[-1], d["date"].iloc[-1] + return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1] @staticmethod - def assert_df(d: DataFrame, df_len: int, df_close: float, df_date: datetime): + def assert_df(dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime): """ make sure data is unmodified """ - if df_len != len(d) or df_close != d["close"].iloc[-1] or df_date != d["date"].iloc[-1]: - raise Exception("Dataframe returned from strategy does not match original") + message = "" + if df_len != len(dataframe): + message = "length" + elif df_close != dataframe["close"].iloc[-1]: + message = "last close price" + elif df_date != dataframe["date"].iloc[-1]: + message = "last date" + if message: + raise DependencyException("Dataframe returned from strategy has mismatching " + f"{message}.") def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ From 78aa65825550726142ca154b9b0c4427fac283c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Mar 2020 11:27:40 +0200 Subject: [PATCH 3/6] Remove unnecessary test (it's a copy of the remaining test) --- tests/strategy/test_interface.py | 38 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 949dda4a0..4e8d8f708 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -17,33 +17,36 @@ from tests.conftest import get_patched_exchange, log_has _STRATEGY = DefaultStrategy(config={}) -def test_returns_latest_buy_signal(mocker, default_conf, ohlcv_history): - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) - ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False) +def test_returns_latest_signal(mocker, default_conf, ohlcv_history): + ohlcv_history.loc[1, 'date'] = arrow.utcnow() + # Take a copy to correctly modify the call + mocked_history = ohlcv_history.copy() + mocked_history['sell'] = 0 + mocked_history['buy'] = 0 + mocked_history.loc[1, 'sell'] = 1 mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) - ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True) - - -def test_returns_latest_sell_signal(mocker, default_conf, ohlcv_history): - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) + return_value=mocked_history ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True) + mocked_history.loc[1, 'sell'] = 0 + mocked_history.loc[1, 'buy'] = 1 mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) + return_value=mocked_history ) assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False) + mocked_history.loc[1, 'sell'] = 0 + mocked_history.loc[1, 'buy'] = 0 + + mocker.patch.object( + _STRATEGY, '_analyze_ticker_internal', + return_value=mocked_history + ) + assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, False) def test_get_signal_empty(default_conf, mocker, caplog): @@ -74,6 +77,8 @@ def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history) _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame([]) ) + mocker.patch.object(_STRATEGY, 'assert_df') + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], ohlcv_history) assert log_has('Empty dataframe for pair xyz', caplog) @@ -89,6 +94,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): _STRATEGY, '_analyze_ticker_internal', return_value=DataFrame(ticks) ) + mocker.patch.object(_STRATEGY, 'assert_df') assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], ohlcv_history) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) From 0887a0212c73816e3ae68c7ca7f7a4cb09a3f192 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Mar 2020 11:29:31 +0200 Subject: [PATCH 4/6] Adjust tests to pass validation --- tests/strategy/test_interface.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4e8d8f708..be8750c3c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -85,14 +85,19 @@ def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history) def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): - caplog.set_level(logging.INFO) # default_conf defines a 5m interval. we check interval * 2 + 5m # this is necessary as the last candle is removed (partial candles) by default - oldtime = arrow.utcnow().shift(minutes=-16) - ticks = DataFrame([{'buy': 1, 'date': oldtime}]) + ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) + # Take a copy to correctly modify the call + mocked_history = ohlcv_history.copy() + mocked_history['sell'] = 0 + mocked_history['buy'] = 0 + mocked_history.loc[1, 'buy'] = 1 + + caplog.set_level(logging.INFO) mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame(ticks) + return_value=mocked_history ) mocker.patch.object(_STRATEGY, 'assert_df') assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], From cd2e738e351ae2e84d144009291c5346848f029b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Mar 2020 11:40:13 +0200 Subject: [PATCH 5/6] Add test for assert error --- freqtrade/strategy/interface.py | 11 ++++++----- tests/strategy/test_interface.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4f833be23..89a38bf54 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -279,11 +279,12 @@ class IStrategy(ABC): dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair}) self.assert_df(dataframe, df_len, df_close, df_date) except ValueError as error: - logger.warning( - 'Unable to analyze candle (OHLCV) data for pair %s: %s', - pair, - str(error) - ) + logger.warning('Unable to analyze candle (OHLCV) data for pair %s: %s', + pair, str(error)) + return False, False + except DependencyException as error: + logger.warning("Unable to analyze candle (OHLCV) data for pair %s: %s", + pair, str(error)) return False, False except Exception as error: logger.exception( diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index be8750c3c..1f496a01b 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock import arrow from pandas import DataFrame +from freqtrade.exceptions import DependencyException from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data from freqtrade.persistence import Trade @@ -105,6 +106,26 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) +def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): + # default_conf defines a 5m interval. we check interval * 2 + 5m + # this is necessary as the last candle is removed (partial candles) by default + ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) + # Take a copy to correctly modify the call + mocked_history = ohlcv_history.copy() + mocked_history['sell'] = 0 + mocked_history['buy'] = 0 + mocked_history.loc[1, 'buy'] = 1 + + caplog.set_level(logging.INFO) + mocker.patch.object( + _STRATEGY, 'assert_df', + side_effect=DependencyException('Dataframe returned...') + ) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], + ohlcv_history) + assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', caplog) + + def test_get_signal_handles_exceptions(mocker, default_conf): exchange = get_patched_exchange(mocker, default_conf) mocker.patch.object( From 83cc121b706efe6e364885f24778255e9c0eb60a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Mar 2020 11:44:36 +0200 Subject: [PATCH 6/6] Add tsts for assert_df (ensuring it raises when it should) --- tests/strategy/test_interface.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 1f496a01b..8bc399f42 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -4,16 +4,18 @@ import logging from unittest.mock import MagicMock import arrow +import pytest from pandas import DataFrame -from freqtrade.exceptions import DependencyException from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data +from freqtrade.exceptions import DependencyException from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver -from .strats.default_strategy import DefaultStrategy from tests.conftest import get_patched_exchange, log_has +from .strats.default_strategy import DefaultStrategy + # Avoid to reinit the same object again and again _STRATEGY = DefaultStrategy(config={}) @@ -123,7 +125,27 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): ) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], ohlcv_history) - assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', caplog) + assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', + caplog) + + +def test_assert_df(default_conf, mocker, ohlcv_history): + # Ensure it's running when passed correctly + _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), + ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) + + with pytest.raises(DependencyException, match=r"Dataframe returned from strategy.*length\."): + _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1, + ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) + + with pytest.raises(DependencyException, + match=r"Dataframe returned from strategy.*last close price\."): + _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), + ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date']) + with pytest.raises(DependencyException, + match=r"Dataframe returned from strategy.*last date\."): + _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), + ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date']) def test_get_signal_handles_exceptions(mocker, default_conf):