From fd97ad9b76973f7ec463591be62e577a9b703b1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Jun 2020 14:02:21 +0200 Subject: [PATCH 01/27] Cache analyzed dataframe --- freqtrade/data/dataprovider.py | 34 ++++++++++++++++++++++++++++++--- freqtrade/strategy/interface.py | 10 +++------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 058ca42da..8dc53b034 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,16 +5,16 @@ including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple +from arrow import Arrow from pandas import DataFrame +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.history import load_pair_history from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import Exchange from freqtrade.state import RunMode -from freqtrade.constants import ListPairsWithTimeframes - logger = logging.getLogger(__name__) @@ -25,6 +25,21 @@ class DataProvider: self._config = config self._exchange = exchange self._pairlists = pairlists + self.__cached_pairs: Dict[Tuple(str, str), DataFrame] = {} + + def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None: + """ + Store cached Dataframe. + Using private method as this should never be used by a user + (but the class is exposed via `self.dp` to the strategy) + :param pair: pair to get the data for + :param timeframe: Timeframe to get data for + :param dataframe: analyzed dataframe + """ + self.__cached_pairs[(pair, timeframe)] = { + 'data': dataframe, + 'updated': Arrow.utcnow().datetime, + } def refresh(self, pairlist: ListPairsWithTimeframes, @@ -89,6 +104,19 @@ class DataProvider: logger.warning(f"No data found for ({pair}, {timeframe}).") return data + def get_analyzed_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: + """ + :param pair: pair to get the data for + :param timeframe: timeframe to get data for + :return: Analyzed Dataframe for this pair + """ + # TODO: check updated time and don't return outdated data. + if (pair, timeframe) in self._set_cached_df: + return self._set_cached_df[(pair, timeframe)]['data'] + else: + # TODO: this is most likely wrong... + raise ValueError(f"No analyzed dataframe found for ({pair}, {timeframe})") + def market(self, pair: str) -> Optional[Dict[str, Any]]: """ Return market data for the pair diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f9f3a3678..843197602 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -273,6 +273,7 @@ class IStrategy(ABC): # Defs that only make change on new candle data. dataframe = self.analyze_ticker(dataframe, metadata) self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] + self.dp._set_cached_df(pair, self.timeframe, dataframe) else: logger.debug("Skipping TA Analysis for already analyzed candle") dataframe['buy'] = 0 @@ -348,13 +349,8 @@ class IStrategy(ABC): return False, False (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 - logger.debug( - 'trigger: %s (pair=%s) buy=%s sell=%s', - latest['date'], - pair, - str(buy), - str(sell) - ) + logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', + latest['date'], pair, str(buy), str(sell)) return buy, sell def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, From 9794914838e9f90bcdbf021583b75499db8ad7e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Jun 2020 14:12:33 +0200 Subject: [PATCH 02/27] store dataframe updated as tuple --- freqtrade/constants.py | 3 ++- freqtrade/data/dataprovider.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6741e0605..45df778cd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -336,4 +336,5 @@ CANCEL_REASON = { } # List of pairs with their timeframes -ListPairsWithTimeframes = List[Tuple[str, str]] +PairWithTimeframe = Tuple[str, str] +ListPairsWithTimeframes = List[PairWithTimeframe] diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 8dc53b034..d93b77121 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,12 +5,13 @@ including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging +from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from arrow import Arrow from pandas import DataFrame -from freqtrade.constants import ListPairsWithTimeframes +from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.data.history import load_pair_history from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import Exchange @@ -25,7 +26,7 @@ class DataProvider: self._config = config self._exchange = exchange self._pairlists = pairlists - self.__cached_pairs: Dict[Tuple(str, str), DataFrame] = {} + self.__cached_pairs: Dict[PairWithTimeframe, Tuple(DataFrame, datetime)] = {} def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None: """ @@ -36,10 +37,7 @@ class DataProvider: :param timeframe: Timeframe to get data for :param dataframe: analyzed dataframe """ - self.__cached_pairs[(pair, timeframe)] = { - 'data': dataframe, - 'updated': Arrow.utcnow().datetime, - } + self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime) def refresh(self, pairlist: ListPairsWithTimeframes, @@ -104,15 +102,17 @@ class DataProvider: logger.warning(f"No data found for ({pair}, {timeframe}).") return data - def get_analyzed_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: + def get_analyzed_dataframe(self, pair: str, + timeframe: str = None) -> Tuple[DataFrame, datetime]: """ :param pair: pair to get the data for :param timeframe: timeframe to get data for - :return: Analyzed Dataframe for this pair + :return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe + combination """ # TODO: check updated time and don't return outdated data. - if (pair, timeframe) in self._set_cached_df: - return self._set_cached_df[(pair, timeframe)]['data'] + if (pair, timeframe) in self.__cached_pairs: + return self.__cached_pairs[(pair, timeframe)] else: # TODO: this is most likely wrong... raise ValueError(f"No analyzed dataframe found for ({pair}, {timeframe})") From 95f3ac08d406dd635f74dd68552fd93680c62267 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Jun 2020 07:09:44 +0200 Subject: [PATCH 03/27] Update some comments --- freqtrade/strategy/interface.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 843197602..0ef92b315 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -204,6 +204,10 @@ class IStrategy(ABC): """ return [] +### +# END - Intended to be overridden by strategy +### + def get_strategy_name(self) -> str: """ Returns strategy class name @@ -308,6 +312,7 @@ class IStrategy(ABC): def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ Calculates current signal based several technical analysis indicators + Used by Bot to get the latest signal :param pair: pair in format ANT/BTC :param interval: Interval to use (in min) :param dataframe: Dataframe to analyze @@ -496,7 +501,8 @@ class IStrategy(ABC): def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ - Creates a dataframe and populates indicators for given candle (OHLCV) data + Populates indicators for given candle (OHLCV) data (for multiple pairs) + Does not run advice_buy or advise_sell! Used by optimize operations only, not during dry / live runs. Using .copy() to get a fresh copy of the dataframe for every strategy run. Has positive effects on memory usage for whatever reason - also when From 273aaaff12e273c58adf2ba05170b189e5b24125 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Jun 2020 18:06:52 +0200 Subject: [PATCH 04/27] Introduce .analyze() function for Strategy Fixing a few tests along the way --- freqtrade/freqtradebot.py | 10 +++--- freqtrade/strategy/interface.py | 59 +++++++++++++++++++++++---------- tests/conftest.py | 2 +- tests/test_freqtradebot.py | 1 + 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 289850709..3ad0d061a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -151,6 +151,8 @@ class FreqtradeBot: self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), self.strategy.informative_pairs()) + self.strategy.analyze(self.active_pair_whitelist) + with self._sell_lock: # Check and handle any timed out open orders self.check_handle_timedout() @@ -420,9 +422,7 @@ class FreqtradeBot: return False # running get_signal on historical data fetched - (buy, sell) = self.strategy.get_signal( - pair, self.strategy.timeframe, - self.dataprovider.ohlcv(pair, self.strategy.timeframe)) + (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe) if buy and not sell: stake_amount = self.get_trade_stake_amount(pair) @@ -697,9 +697,7 @@ class FreqtradeBot: if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): - (buy, sell) = self.strategy.get_signal( - trade.pair, self.strategy.timeframe, - self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe)) + (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe) if config_ask_strategy.get('use_order_book', False): order_book_min = config_ask_strategy.get('order_book_min', 1) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0ef92b315..9dcc2d613 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,20 +7,19 @@ import warnings from abc import ABC, abstractmethod from datetime import datetime, timezone from enum import Enum -from typing import Dict, NamedTuple, Optional, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple import arrow from pandas import DataFrame +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import StrategyError from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from freqtrade.constants import ListPairsWithTimeframes from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) @@ -289,6 +288,38 @@ class IStrategy(ABC): return dataframe + def analyze_pair(self, pair: str): + """ + Fetch data for this pair from dataprovider and analyze. + Stores the dataframe into the dataprovider. + The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`. + :param pair: Pair to analyze. + """ + dataframe = self.dp.ohlcv(pair, self.timeframe) + if not isinstance(dataframe, DataFrame) or dataframe.empty: + logger.warning('Empty candle (OHLCV) data for pair %s', pair) + return + + try: + df_len, df_close, df_date = self.preserve_df(dataframe) + + dataframe = strategy_safe_wrapper( + self._analyze_ticker_internal, message="" + )(dataframe, {'pair': pair}) + + self.assert_df(dataframe, df_len, df_close, df_date) + except StrategyError as error: + logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}") + return + + if dataframe.empty: + logger.warning('Empty dataframe for pair %s', pair) + return + + def analyze(self, pairs: List[str]): + for pair in pairs: + self.analyze_pair(pair) + @staticmethod def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]: """ keep some data for dataframes """ @@ -309,30 +340,22 @@ class IStrategy(ABC): else: raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.") - def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: + def get_signal(self, pair: str, timeframe: str) -> Tuple[bool, bool]: """ Calculates current signal based several technical analysis indicators Used by Bot to get the latest signal :param pair: pair in format ANT/BTC - :param interval: Interval to use (in min) + :param timeframe: timeframe to use :param dataframe: Dataframe to analyze :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ + + dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) + if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning('Empty candle (OHLCV) data for pair %s', pair) return False, False - try: - df_len, df_close, df_date = self.preserve_df(dataframe) - dataframe = strategy_safe_wrapper( - self._analyze_ticker_internal, message="" - )(dataframe, {'pair': pair}) - self.assert_df(dataframe, df_len, df_close, df_date) - except StrategyError as error: - logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}") - - return False, False - if dataframe.empty: logger.warning('Empty dataframe for pair %s', pair) return False, False @@ -343,9 +366,9 @@ class IStrategy(ABC): latest_date = arrow.get(latest_date) # Check if dataframe is out of date - interval_minutes = timeframe_to_minutes(interval) + timeframe_minutes = timeframe_to_minutes(timeframe) offset = self.config.get('exchange', {}).get('outdated_offset', 5) - if latest_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): + if latest_date < (arrow.utcnow().shift(minutes=-(timeframe_minutes * 2 + offset))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', pair, diff --git a/tests/conftest.py b/tests/conftest.py index a4106c767..3be7bbd22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,7 +163,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: :param value: which value IStrategy.get_signal() must return :return: None """ - freqtrade.strategy.get_signal = lambda e, s, t: value + freqtrade.strategy.get_signal = lambda e, s: value freqtrade.exchange.refresh_latest_ohlcv = lambda p: None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5d83c893e..e83ac2038 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -912,6 +912,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: refresh_latest_ohlcv=refresh_mock, ) inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")]) + mocker.patch('freqtrade.strategy.interface.IStrategy.get_signal', return_value=(False, False)) mocker.patch('time.sleep', return_value=None) freqtrade = FreqtradeBot(default_conf) From 55fa514ec93a61994c4ee738a5ee4d443c8f15a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Jun 2020 19:40:58 +0200 Subject: [PATCH 05/27] Adapt most tests --- freqtrade/strategy/interface.py | 4 -- tests/strategy/test_interface.py | 72 +++++++++++++++----------------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9dcc2d613..a7d467cb2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -353,10 +353,6 @@ class IStrategy(ABC): dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) if not isinstance(dataframe, DataFrame) or dataframe.empty: - logger.warning('Empty candle (OHLCV) data for pair %s', pair) - return False, False - - if dataframe.empty: logger.warning('Empty dataframe for pair %s', pair) return False, False diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 59b4d5902..da8de947a 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -13,12 +13,14 @@ from freqtrade.exceptions import StrategyError from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.data.dataprovider import DataProvider from tests.conftest import get_patched_exchange, log_has, log_has_re from .strats.default_strategy import DefaultStrategy # Avoid to reinit the same object again and again _STRATEGY = DefaultStrategy(config={}) +_STRATEGY.dp = DataProvider({}, None, None) def test_returns_latest_signal(mocker, default_conf, ohlcv_history): @@ -30,61 +32,65 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): mocked_history.loc[1, 'sell'] = 1 mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=mocked_history + _STRATEGY.dp, 'get_analyzed_dataframe', + return_value=(mocked_history, 0) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True) + assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, True) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 1 mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=mocked_history + _STRATEGY.dp, 'get_analyzed_dataframe', + return_value=(mocked_history, 0) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m') == (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 + _STRATEGY.dp, 'get_analyzed_dataframe', + return_value=(mocked_history, 0) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, False) def test_get_signal_empty(default_conf, mocker, caplog): - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], - DataFrame()) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame(), 0)) + assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe']) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], - []) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(None, 0)) + assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe']) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) + mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', side_effect=ValueError('xyz') ) - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], - ohlcv_history) + _STRATEGY.analyze_pair('foo') + assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog) + caplog.clear() + + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + side_effect=Exception('invalid ticker history ') + ) + _STRATEGY.analyze_pair('foo') assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog) def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([]) - ) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame([]), 0)) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], - ohlcv_history) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe']) assert log_has('Empty dataframe for pair xyz', caplog) @@ -99,13 +105,10 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): mocked_history.loc[1, 'buy'] = 1 caplog.set_level(logging.INFO) - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=mocked_history - ) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(mocked_history, 0)) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], - ohlcv_history) + + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe']) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) @@ -120,12 +123,13 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): mocked_history.loc[1, 'buy'] = 1 caplog.set_level(logging.INFO) + mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(mocked_history, 0)) mocker.patch.object( _STRATEGY, 'assert_df', side_effect=StrategyError('Dataframe returned...') ) - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], - ohlcv_history) + _STRATEGY.analyze_pair('xyz') assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', caplog) @@ -157,15 +161,6 @@ def test_assert_df(default_conf, mocker, ohlcv_history, caplog): _STRATEGY.disable_dataframe_checks = False -def test_get_signal_handles_exceptions(mocker, default_conf): - exchange = get_patched_exchange(mocker, default_conf) - mocker.patch.object( - _STRATEGY, 'analyze_ticker', - side_effect=Exception('invalid ticker history ') - ) - assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False) - - def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) @@ -342,6 +337,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> ) strategy = DefaultStrategy({}) + strategy.dp = DataProvider({}, None, None) strategy.process_only_new_candles = True ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) From 8166b37253e06b1b600e2545fd69fab4cdcb1978 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jun 2020 14:08:57 +0200 Subject: [PATCH 06/27] Explicitly check if dp is available --- freqtrade/data/dataprovider.py | 5 ++--- freqtrade/strategy/interface.py | 9 +++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index d93b77121..adc9ea334 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -26,7 +26,7 @@ class DataProvider: self._config = config self._exchange = exchange self._pairlists = pairlists - self.__cached_pairs: Dict[PairWithTimeframe, Tuple(DataFrame, datetime)] = {} + self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None: """ @@ -102,8 +102,7 @@ class DataProvider: logger.warning(f"No data found for ({pair}, {timeframe}).") return data - def get_analyzed_dataframe(self, pair: str, - timeframe: str = None) -> Tuple[DataFrame, datetime]: + def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]: """ :param pair: pair to get the data for :param timeframe: timeframe to get data for diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a7d467cb2..f7a918624 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -14,7 +14,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import StrategyError, OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper @@ -276,7 +276,8 @@ class IStrategy(ABC): # Defs that only make change on new candle data. dataframe = self.analyze_ticker(dataframe, metadata) self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] - self.dp._set_cached_df(pair, self.timeframe, dataframe) + if self.dp: + self.dp._set_cached_df(pair, self.timeframe, dataframe) else: logger.debug("Skipping TA Analysis for already analyzed candle") dataframe['buy'] = 0 @@ -295,6 +296,8 @@ class IStrategy(ABC): The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`. :param pair: Pair to analyze. """ + if not self.dp: + raise OperationalException("DataProvider not found.") dataframe = self.dp.ohlcv(pair, self.timeframe) if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning('Empty candle (OHLCV) data for pair %s', pair) @@ -349,6 +352,8 @@ class IStrategy(ABC): :param dataframe: Dataframe to analyze :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ + if not self.dp: + raise OperationalException("DataProvider not found.") dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) From 7da955556d18e0cd8d9cebcb8dc2b64436d49e26 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Jun 2020 20:04:15 +0200 Subject: [PATCH 07/27] Add test for empty pair case --- tests/strategy/test_interface.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index da8de947a..94437d373 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -9,12 +9,12 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import StrategyError, OperationalException from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.data.dataprovider import DataProvider -from tests.conftest import get_patched_exchange, log_has, log_has_re +from tests.conftest import log_has, log_has_re from .strats.default_strategy import DefaultStrategy @@ -55,6 +55,28 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, False) +def test_trade_no_dataprovider(default_conf, mocker, caplog): + strategy = DefaultStrategy({}) + with pytest.raises(OperationalException, match="DataProvider not found."): + strategy.get_signal('ETH/BTC', '5m') + + with pytest.raises(OperationalException, match="DataProvider not found."): + strategy.analyze_pair('ETH/BTC') + + +def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): + mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) + mocker.patch.object( + _STRATEGY, '_analyze_ticker_internal', + return_value=DataFrame([]) + ) + mocker.patch.object(_STRATEGY, 'assert_df') + + _STRATEGY.analyze_pair('ETH/BTC') + + assert log_has('Empty dataframe for pair ETH/BTC', caplog) + + def test_get_signal_empty(default_conf, mocker, caplog): mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame(), 0)) assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe']) From 77056a3119e5d7e7f9ab82104c09aa5013f6fd0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 06:52:11 +0200 Subject: [PATCH 08/27] Add bot_loop_start callback --- freqtrade/strategy/interface.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f7a918624..4483e1e8f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -190,6 +190,15 @@ class IStrategy(ABC): """ return False + def bot_loop_start(self, **kwargs) -> None: + """ + Called at the start of the bot iteration (one loop). + Might be used to perform pair-independent tasks + (e.g. like lock pairs with negative profit in the last hour) + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + pass + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. From bc821c7c208f9ebede5b02b2677416f1806fed31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 07:00:55 +0200 Subject: [PATCH 09/27] Add documentation for bot_loop_start --- docs/strategy-advanced.md | 27 +++++++++++++++++++ freqtrade/freqtradebot.py | 2 ++ freqtrade/strategy/interface.py | 2 +- .../subtemplates/strategy_methods_advanced.j2 | 9 +++++++ tests/strategy/test_interface.py | 11 ++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 69e2256a1..aba834c55 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -89,3 +89,30 @@ class Awesomestrategy(IStrategy): return True return False ``` + +## Bot loop start callback + +A simple callback which is called at the start of every bot iteration. +This can be used to perform calculations which are pair independent. + + +``` python +import requests + +class Awesomestrategy(IStrategy): + + # ... populate_* methods + + def bot_loop_start(self, **kwargs) -> None: + """ + Called at the start of the bot iteration (one loop). + Might be used to perform pair-independent tasks + (e.g. gather some remote ressource for comparison) + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + if self.config['runmode'].value in ('live', 'dry_run'): + # Assign this to the class by using self.* + # can then be used by populate_* methods + self.remote_data = requests.get('https://some_remote_source.example.com') + +``` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3ad0d061a..a69178691 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -151,6 +151,8 @@ class FreqtradeBot: self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), self.strategy.informative_pairs()) + strategy_safe_wrapper(self.strategy.bot_loop_start)() + self.strategy.analyze(self.active_pair_whitelist) with self._sell_lock: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4483e1e8f..2ee567c20 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -194,7 +194,7 @@ class IStrategy(ABC): """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks - (e.g. like lock pairs with negative profit in the last hour) + (e.g. gather some remote ressource for comparison) :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 0ca35e117..9af086c77 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -1,4 +1,13 @@ +def bot_loop_start(self, **kwargs) -> None: + """ + Called at the start of the bot iteration (one loop). + Might be used to perform pair-independent tasks + (e.g. gather some remote ressource for comparison) + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + pass + def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: """ Check buy timeout function callback. diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 94437d373..a50b4e1db 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -57,6 +57,9 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): def test_trade_no_dataprovider(default_conf, mocker, caplog): strategy = DefaultStrategy({}) + # Delete DP for sure (suffers from test leakage, as we update this in the base class) + if strategy.dp: + del strategy.dp with pytest.raises(OperationalException, match="DataProvider not found."): strategy.get_signal('ETH/BTC', '5m') @@ -418,6 +421,14 @@ def test_is_pair_locked(default_conf): assert not strategy.is_pair_locked(pair) +def test_is_informative_pairs_callback(default_conf): + default_conf.update({'strategy': 'TestStrategyLegacy'}) + strategy = StrategyResolver.load_strategy(default_conf) + # Should return empty + # Uses fallback to base implementation + assert [] == strategy.informative_pairs() + + @pytest.mark.parametrize('error', [ ValueError, KeyError, Exception, ]) From c047e48a47a7b06bba7525e8750fa006c58f9930 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 07:15:24 +0200 Subject: [PATCH 10/27] Add errorsupression to safe wrapper --- freqtrade/strategy/strategy_wrapper.py | 6 +++--- tests/strategy/test_interface.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/strategy_wrapper.py b/freqtrade/strategy/strategy_wrapper.py index 7b9da9140..8fc548074 100644 --- a/freqtrade/strategy/strategy_wrapper.py +++ b/freqtrade/strategy/strategy_wrapper.py @@ -5,7 +5,7 @@ from freqtrade.exceptions import StrategyError logger = logging.getLogger(__name__) -def strategy_safe_wrapper(f, message: str = "", default_retval=None): +def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_error=False): """ Wrapper around user-provided methods and functions. Caches all exceptions and returns either the default_retval (if it's not None) or raises @@ -20,7 +20,7 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None): f"Strategy caused the following exception: {error}" f"{f}" ) - if default_retval is None: + if default_retval is None and not supress_error: raise StrategyError(str(error)) from error return default_retval except Exception as error: @@ -28,7 +28,7 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None): f"{message}" f"Unexpected error {error} calling {f}" ) - if default_retval is None: + if default_retval is None and not supress_error: raise StrategyError(str(error)) from error return default_retval diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a50b4e1db..63d3b85a1 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -448,6 +448,11 @@ def test_strategy_safe_wrapper_error(caplog, error): assert isinstance(ret, bool) assert ret + caplog.clear() + # Test supressing error + ret = strategy_safe_wrapper(failing_method, message='DeadBeef', supress_error=True)() + assert log_has_re(r'DeadBeef.*', caplog) + @pytest.mark.parametrize('value', [ 1, 22, 55, True, False, {'a': 1, 'b': '112'}, From dea7e3db014f2d8b1888035185b33ac98e37152c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 07:16:56 +0200 Subject: [PATCH 11/27] Use supress_errors in strategy wrapper - ensure it's called once --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a69178691..77ded8355 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -151,7 +151,7 @@ class FreqtradeBot: self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), self.strategy.informative_pairs()) - strategy_safe_wrapper(self.strategy.bot_loop_start)() + strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() self.strategy.analyze(self.active_pair_whitelist) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e83ac2038..381531eba 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1964,6 +1964,18 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, freqtrade.handle_trade(trade) +def test_bot_loop_start_called_once(mocker, default_conf, caplog): + ftbot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(ftbot) + ftbot.strategy.bot_loop_start = MagicMock(side_effect=ValueError) + ftbot.strategy.analyze = MagicMock() + + ftbot.process() + assert log_has_re(r'Strategy caused the following exception.*', caplog) + assert ftbot.strategy.bot_loop_start.call_count == 1 + assert ftbot.strategy.analyze.call_count == 1 + + def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_order_old, open_trade, fee, mocker) -> None: default_conf["unfilledtimeout"] = {"buy": 1400, "sell": 30} From 910100f1c88efa16622acc425a8ea41018482c8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 07:27:13 +0200 Subject: [PATCH 12/27] Improve docstring comment --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2ee567c20..abe5f7dfb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -354,7 +354,7 @@ class IStrategy(ABC): def get_signal(self, pair: str, timeframe: str) -> Tuple[bool, bool]: """ - Calculates current signal based several technical analysis indicators + Calculates current signal based based on the buy / sell columns of the dataframe. Used by Bot to get the latest signal :param pair: pair in format ANT/BTC :param timeframe: timeframe to use From de676bcabac2ca49d5ceea9be5c85dd794b0dbfb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:08:06 +0200 Subject: [PATCH 13/27] Document get_analyzed_dataframe for dataprovider --- docs/strategy-customization.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 08e79d307..1c4c80c47 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -366,6 +366,7 @@ Please always check the mode of operation to select the correct method to get da - [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their intervals (pair, interval). - [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (ie. VolumePairlist) - [`get_pair_dataframe(pair, timeframe)`](#get_pair_dataframepair-timeframe) - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). +- [`get_analyzed_dataframe(pair, timeframe)`](#get_analyzed_dataframepair-timeframe) - Returns the analyzed dataframe (after calling `populate_indicators()`, `populate_buy()`, `populate_sell()`) and the time of the latest analysis. - `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk. - `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on the Market data structure. - `ohlcv(pair, timeframe)` - Currently cached candle (OHLCV) data for the pair, returns DataFrame or empty DataFrame. @@ -431,13 +432,25 @@ if self.dp: ``` !!! Warning "Warning about backtesting" - Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()` + Be careful when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()` for the backtesting runmode) provides the full time-range in one go, so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode). !!! Warning "Warning in hyperopt" This option cannot currently be used during hyperopt. +#### *get_analyzed_dataframe(pair, timeframe)* + +This method is used by freqtrade internally to determine the last signal. +It can also be used in specific callbacks to get the signal that caused the action (see [Advanced Strategy Documentation](strategy-advanced.md) for more details on available callbacks). + +``` python +# fetch current dataframe +if self.dp: + dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'], + timeframe=self.ticker_interval) +``` + #### *orderbook(pair, maximum)* ``` python From 84329ad2ca7297491a11a0fa542fcc5ac729b0a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:08:19 +0200 Subject: [PATCH 14/27] Add confirm_trade* methods to abort buying or selling --- docs/strategy-advanced.md | 83 ++++++++++++++++++- freqtrade/freqtradebot.py | 16 +++- freqtrade/strategy/interface.py | 48 +++++++++++ .../subtemplates/strategy_methods_advanced.j2 | 52 ++++++++++++ 4 files changed, 197 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index aba834c55..7edb0d05d 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -3,6 +3,9 @@ This page explains some advanced concepts available for strategies. If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation first. +!!! Note + All callback methods described below should only be implemented in a strategy if they are also actively used. + ## Custom order timeout rules Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. @@ -95,7 +98,6 @@ class Awesomestrategy(IStrategy): A simple callback which is called at the start of every bot iteration. This can be used to perform calculations which are pair independent. - ``` python import requests @@ -116,3 +118,82 @@ class Awesomestrategy(IStrategy): self.remote_data = requests.get('https://some_remote_source.example.com') ``` + +## Bot order confirmation + +### Trade entry (buy order) confirmation + +`confirm_trade_entry()` an be used to abort a trade entry at the latest second (maybe because the price is not what we expect). + +``` python +class Awesomestrategy(IStrategy): + + # ... populate_* methods + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, **kwargs) -> bool: + """ + Called right before placing a buy order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be bought. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in target (quote) currency that's going to be traded. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the buy-order is placed on the exchange. + False aborts the process + """ + return True + +``` + +### Trade exit (sell order) confirmation + +`confirm_trade_exit()` an be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect). + +``` python +from freqtrade.persistence import Trade + + +class Awesomestrategy(IStrategy): + + # ... populate_* methods + + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, + time_in_force: str, sell_reason: str, ** kwargs) -> bool: + """ + Called right before placing a regular sell order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be sold. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in quote currency. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param sell_reason: Sell reason. + Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', + 'sell_signal', 'force_sell', 'emergency_sell'] + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the sell-order is placed on the exchange. + False aborts the process + """ + if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: + # Reject force-sells with negative profit + # This is just a sample, please adjust to your needs + # (this does not necessarily make sense, assuming you know when you're force-selling) + return False + return True + +``` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 77ded8355..09b794a99 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -497,6 +497,12 @@ class FreqtradeBot: amount = stake_amount / buy_limit_requested order_type = self.strategy.order_types['buy'] + if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( + pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, + time_in_force=time_in_force): + logger.info(f"User requested abortion of buying {pair}") + return False + order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) @@ -1077,12 +1083,20 @@ class FreqtradeBot: order_type = self.strategy.order_types.get("emergencysell", "market") amount = self._safe_sell_amount(trade.pair, trade.amount) + time_in_force = self.strategy.order_time_in_force['sell'] + + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, + time_in_force=time_in_force, + sell_reason=sell_reason.value): + logger.info(f"User requested abortion of selling {trade.pair}") + return False # Execute sell and update trade record order = self.exchange.sell(pair=str(trade.pair), ordertype=order_type, amount=amount, rate=limit, - time_in_force=self.strategy.order_time_in_force['sell'] + time_in_force=time_in_force ) trade.open_order_id = order['id'] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index abe5f7dfb..afa8c192e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -199,6 +199,54 @@ class IStrategy(ABC): """ pass + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, **kwargs) -> bool: + """ + Called right before placing a buy order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be bought. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in target (quote) currency that's going to be traded. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the buy-order is placed on the exchange. + False aborts the process + """ + return True + + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, + time_in_force: str, sell_reason: str, ** kwargs) -> bool: + """ + Called right before placing a regular sell order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be sold. + :param trade: trade object. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in quote currency. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param sell_reason: Sell reason. + Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', + 'sell_signal', 'force_sell', 'emergency_sell'] + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the sell-order is placed on the exchange. + False aborts the process + """ + return True + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 9af086c77..782c5a475 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -4,10 +4,62 @@ def bot_loop_start(self, **kwargs) -> None: Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks (e.g. gather some remote ressource for comparison) + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, this simply does nothing. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass +def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, **kwargs) -> bool: + """ + Called right before placing a buy order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be bought. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in target (quote) currency that's going to be traded. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the buy-order is placed on the exchange. + False aborts the process + """ + return True + +def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, rate: float, + time_in_force: str, sell_reason: str, ** kwargs) -> bool: + """ + Called right before placing a regular sell order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be sold. + :param trade: trade object. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in quote currency. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param sell_reason: Sell reason. + Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', + 'sell_signal', 'force_sell', 'emergency_sell'] + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the sell-order is placed on the exchange. + False aborts the process + """ + return True + def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: """ Check buy timeout function callback. From 6d6e7196f43e97c16208c7685f51d3a39cae3206 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:20:23 +0200 Subject: [PATCH 15/27] Test trade entry / exit is called correctly --- tests/test_integration.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_integration.py b/tests/test_integration.py index 57960503e..fdc3ab1d0 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -79,10 +79,15 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, freqtrade.strategy.order_types['stoploss_on_exchange'] = True # Switch ordertype to market to close trade immediately freqtrade.strategy.order_types['sell'] = 'market' + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) patch_get_signal(freqtrade) # Create some test data freqtrade.enter_positions() + assert freqtrade.strategy.confirm_trade_entry.call_count == 3 + freqtrade.strategy.confirm_trade_entry.reset_mock() + assert freqtrade.strategy.confirm_trade_exit.call_count == 0 wallets_mock.reset_mock() Trade.session = MagicMock() @@ -95,6 +100,9 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, n = freqtrade.exit_positions(trades) assert n == 2 assert should_sell_mock.call_count == 2 + assert freqtrade.strategy.confirm_trade_entry.call_count == 0 + assert freqtrade.strategy.confirm_trade_exit.call_count == 1 + freqtrade.strategy.confirm_trade_exit.reset_mock() # Only order for 3rd trade needs to be cancelled assert cancel_order_mock.call_count == 1 From 7c3fb111f283286a3c7297ab6aef604a9ab4b66d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:23:49 +0200 Subject: [PATCH 16/27] Confirm execute_sell calls confirm_trade_exit --- tests/test_freqtradebot.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 381531eba..fd903fe43 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2502,22 +2502,33 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) + freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False) # Create some test data freqtrade.enter_positions() + rpc_mock.reset_mock() trade = Trade.query.first() assert trade + assert freqtrade.strategy.confirm_trade_exit.call_count == 0 # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_sell_up ) + # Prevented sell ... + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + assert rpc_mock.call_count == 0 + assert freqtrade.strategy.confirm_trade_exit.call_count == 1 + + # Repatch with true + freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + assert freqtrade.strategy.confirm_trade_exit.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, From 1c1a7150ae9906f9d513c86c611bf51d70d728f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:27:29 +0200 Subject: [PATCH 17/27] ensure confirm_trade_entry is called and has the desired effect --- tests/test_freqtradebot.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index fd903fe43..820e69bb0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -975,6 +975,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) stake_amount = 2 bid = 0.11 buy_rate_mock = MagicMock(return_value=bid) @@ -996,6 +997,13 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: ) pair = 'ETH/BTC' + assert not freqtrade.execute_buy(pair, stake_amount) + assert buy_rate_mock.call_count == 1 + assert buy_mm.call_count == 0 + assert freqtrade.strategy.confirm_trade_entry.call_count == 1 + buy_rate_mock.reset_mock() + + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) assert freqtrade.execute_buy(pair, stake_amount) assert buy_rate_mock.call_count == 1 assert buy_mm.call_count == 1 @@ -1003,6 +1011,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: assert call_args['pair'] == pair assert call_args['rate'] == bid assert call_args['amount'] == stake_amount / bid + buy_rate_mock.reset_mock() # Should create an open trade with an open order id # As the order is not fulfilled yet @@ -1015,7 +1024,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: fix_price = 0.06 assert freqtrade.execute_buy(pair, stake_amount, fix_price) # Make sure get_buy_rate wasn't called again - assert buy_rate_mock.call_count == 1 + assert buy_rate_mock.call_count == 0 assert buy_mm.call_count == 2 call_args = buy_mm.call_args_list[1][1] From 8b186dbe0eb3872ba685c7a8c156a1b266c4aa92 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:49:15 +0200 Subject: [PATCH 18/27] Add additional test scenarios --- docs/strategy-advanced.md | 4 +-- freqtrade/strategy/interface.py | 4 +-- .../subtemplates/strategy_methods_advanced.j2 | 4 +-- tests/conftest.py | 1 + tests/strategy/test_interface.py | 6 ++-- tests/test_freqtradebot.py | 33 +++++++++++++++++++ 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7edb0d05d..f1d8c21dc 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -135,7 +135,7 @@ class Awesomestrategy(IStrategy): """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -171,7 +171,7 @@ class Awesomestrategy(IStrategy): """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index afa8c192e..fab438ae6 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -204,7 +204,7 @@ class IStrategy(ABC): """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -226,7 +226,7 @@ class IStrategy(ABC): """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 782c5a475..28d2d1c1b 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -17,7 +17,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -39,7 +39,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ diff --git a/tests/conftest.py b/tests/conftest.py index 3be7bbd22..c64f443bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -787,6 +787,7 @@ def limit_buy_order(): 'price': 0.00001099, 'amount': 90.99181073, 'filled': 90.99181073, + 'cost': 0.0009999, 'remaining': 0.0, 'status': 'closed' } diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 63d3b85a1..176fa43ca 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -57,9 +57,9 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): def test_trade_no_dataprovider(default_conf, mocker, caplog): strategy = DefaultStrategy({}) - # Delete DP for sure (suffers from test leakage, as we update this in the base class) - if strategy.dp: - del strategy.dp + # Delete DP for sure (suffers from test leakage, as this is updated in the base class) + if strategy.dp is not None: + strategy.dp = None with pytest.raises(OperationalException, match="DataProvider not found."): strategy.get_signal('ETH/BTC', '5m') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 820e69bb0..047885942 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1070,6 +1070,39 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: assert not freqtrade.execute_buy(pair, stake_amount) +def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch.multiple( + 'freqtrade.freqtradebot.FreqtradeBot', + get_buy_rate=MagicMock(return_value=0.11), + _get_min_pair_stake_amount=MagicMock(return_value=1) + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value=limit_buy_order), + get_fee=fee, + ) + stake_amount = 2 + pair = 'ETH/BTC' + + freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError) + assert freqtrade.execute_buy(pair, stake_amount) + + freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception) + assert freqtrade.execute_buy(pair, stake_amount) + + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + assert freqtrade.execute_buy(pair, stake_amount) + + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) + assert not freqtrade.execute_buy(pair, stake_amount) + + def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From e5f7610b5d7b1552f6809171812b3937133ccb12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 11:38:56 +0200 Subject: [PATCH 19/27] Add bot basics documentation --- docs/bot-basics.md | 58 ++++++++++++++++++++++++++++++++++ docs/hyperopt.md | 5 --- docs/strategy-advanced.md | 4 ++- docs/strategy-customization.md | 5 ++- mkdocs.yml | 1 + 5 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 docs/bot-basics.md diff --git a/docs/bot-basics.md b/docs/bot-basics.md new file mode 100644 index 000000000..728dcf46e --- /dev/null +++ b/docs/bot-basics.md @@ -0,0 +1,58 @@ +# Freqtrade basics + +This page will try to teach you some basic concepts on how freqtrade works and operates. + +## Freqtrade terminology + +* Trade: Open position +* Open Order: Order which is currently placed on the exchange, and is not yet complete. +* Pair: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT). +* Timeframe: Candle length to use (e.g. `"5m"`, `"1h"`, ...). +* Indicators: Technical indicators (SMA, EMA, RSI, ...). +* Limit order: Limit orders which execute at the defined limit price or better. +* Market order: Guaranteed to fill, may move price depending on the order size. + +## Fee handling + +All profit calculations of Freqtrade include fees. For Backtesting / Hyperopt / Dry-run modes, the exchange default fee is used (lowest tier on the exchange). For live operations, fees are used as applied by the exchange (this includes BNB rebates etc.). + +## Bot execution logic + +Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop. +By default, loop runs every few seconds (`internals.process_throttle_secs`) and does roughly the following in the following sequence: + +* Fetch open trades from persistence. +* Calculate current list of tradable pairs. +* Download ohlcv data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs) + This step is only executed once per Candle to avoid unnecessary network traffic. +* Call `bot_loop_start()` strategy callback. +* Analyze strategy per pair. + * Call `populate_indicators()` + * Call `populate_buy_trend()` + * Call `populate_sell_trend()` +* Check timeouts for open orders. + * Calls `check_buy_timeout()` strategy callback for open buy orders. + * Calls `check_sell_timeout()` strategy callback for open sell orders. +* Verifies existing positions and eventually places sell orders. + * Considers stoploss, ROI and sell-signal. + * Determine sell-price based on `ask_strategy` configuration setting. + * Before a sell order is placed, `confirm_trade_exit()` strategy callback is called. +* Check if trade-slots are still available (if `max_open_trades` is reached). +* Verifies buy signal trying to enter new positions. + * Determine buy-price based on `bid_strategy` configuration setting. + * Before a buy order is placed, `confirm_trade_entry()` strategy callback is called. + +This loop will be repeated again and again until the bot is stopped. + +## Backtesting / Hyperopt execution logic + +[backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated. + +* Load historic data for configured pairlist. +* Calculate indicators (calls `populate_indicators()`). +* Calls `populate_buy_trend()` and `populate_sell_trend()` +* Loops per candle simulating entry and exit points. +* Generate backtest report output + +!!! Note + Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 9acb606c3..efb11e188 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -498,8 +498,3 @@ After you run Hyperopt for the desired amount of epochs, you can later list all Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. - -## Next Step - -Now you have a perfect bot and want to control it from Telegram. Your -next step is to learn the [Telegram usage](telegram-usage.md). diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f1d8c21dc..100e96b81 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -1,7 +1,9 @@ # Advanced Strategies This page explains some advanced concepts available for strategies. -If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation first. +If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first. + +[Freqtrade basics](bot-basics.md) describes in which sequence each method defined below is called, which can be helpful to understand which method to use. !!! Note All callback methods described below should only be implemented in a strategy if they are also actively used. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 1c4c80c47..4a373fb0d 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -1,6 +1,8 @@ # Strategy Customization -This page explains where to customize your strategies, and add new indicators. +This page explains how to customize your strategies, and add new indicators. + +Please familiarize yourself with [Freqtrade basics](bot-basics.md) first. ## Install a custom strategy file @@ -385,6 +387,7 @@ if self.dp: ``` #### *current_whitelist()* + Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume. The strategy might look something like this: diff --git a/mkdocs.yml b/mkdocs.yml index ae24e150c..ebd32b3c1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,7 @@ nav: - Home: index.md - Installation Docker: docker.md - Installation: installation.md + - Freqtrade Basics: bot-basics.md - Configuration: configuration.md - Strategy Customization: strategy-customization.md - Stoploss: stoploss.md From ab9382434fb7945cd882c6905daddf0664918889 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 11:51:20 +0200 Subject: [PATCH 20/27] Add test for get_analyzed_dataframe --- freqtrade/data/dataprovider.py | 10 +++++----- tests/data/test_dataprovider.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index adc9ea334..113a092bc 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,7 +5,7 @@ including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple from arrow import Arrow @@ -107,14 +107,14 @@ class DataProvider: :param pair: pair to get the data for :param timeframe: timeframe to get data for :return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe - combination + combination. + Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached. """ - # TODO: check updated time and don't return outdated data. if (pair, timeframe) in self.__cached_pairs: return self.__cached_pairs[(pair, timeframe)] else: - # TODO: this is most likely wrong... - raise ValueError(f"No analyzed dataframe found for ({pair}, {timeframe})") + + return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) def market(self, pair: str) -> Optional[Dict[str, Any]]: """ diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index def3ad535..c572cd9f3 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -1,11 +1,12 @@ +from datetime import datetime, timezone from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider -from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.state import RunMode from tests.conftest import get_patched_exchange @@ -194,3 +195,29 @@ def test_current_whitelist(mocker, default_conf, tickers): with pytest.raises(OperationalException): dp = DataProvider(default_conf, exchange) dp.current_whitelist() + + +def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history): + + default_conf["runmode"] = RunMode.DRY_RUN + + timeframe = default_conf["timeframe"] + exchange = get_patched_exchange(mocker, default_conf) + + dp = DataProvider(default_conf, exchange) + dp._set_cached_df("XRP/BTC", timeframe, ohlcv_history) + dp._set_cached_df("UNITTEST/BTC", timeframe, ohlcv_history) + + assert dp.runmode == RunMode.DRY_RUN + dataframe, time = dp.get_analyzed_dataframe("UNITTEST/BTC", timeframe) + assert ohlcv_history.equals(dataframe) + assert isinstance(time, datetime) + + dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe) + assert ohlcv_history.equals(dataframe) + assert isinstance(time, datetime) + + dataframe, time = dp.get_analyzed_dataframe("NOTHING/BTC", timeframe) + assert dataframe.empty + assert isinstance(time, datetime) + assert time == datetime(1970, 1, 1, tzinfo=timezone.utc) From 8472fcfff9a6ad93e96ac8800bfe8dbb90277a52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 11:52:42 +0200 Subject: [PATCH 21/27] Add empty to documentation --- docs/strategy-customization.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4a373fb0d..0a1049f3b 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -454,6 +454,13 @@ if self.dp: timeframe=self.ticker_interval) ``` +!!! Note "No data available" + Returns an empty dataframe if the requested pair was not cached. + This should not happen when using whitelisted pairs. + +!!! Warning "Warning in hyperopt" + This option cannot currently be used during hyperopt. + #### *orderbook(pair, maximum)* ``` python From f2a778d294a435c2b775dae1c644eef042dfe1dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 07:03:30 +0200 Subject: [PATCH 22/27] Combine tests for empty dataframe --- freqtrade/strategy/interface.py | 2 +- tests/strategy/test_interface.py | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index fab438ae6..5b5cce268 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -415,7 +415,7 @@ class IStrategy(ABC): dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) if not isinstance(dataframe, DataFrame) or dataframe.empty: - logger.warning('Empty dataframe for pair %s', pair) + logger.warning(f'Empty candle (OHLCV) data for pair {pair}') return False, False latest_date = dataframe['date'].max() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 176fa43ca..835465f38 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -89,6 +89,11 @@ def test_get_signal_empty(default_conf, mocker, caplog): mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(None, 0)) assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe']) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) + caplog.clear() + + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame([]), 0)) + assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe']) + assert log_has('Empty candle (OHLCV) data for pair baz', caplog) def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history): @@ -110,15 +115,6 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_his assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog) -def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history): - caplog.set_level(logging.INFO) - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame([]), 0)) - mocker.patch.object(_STRATEGY, 'assert_df') - - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe']) - assert log_has('Empty dataframe for pair xyz', caplog) - - def test_get_signal_old_dataframe(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 From 48225e0d80669e100cb56518133a95cc8d6adad0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 07:05:06 +0200 Subject: [PATCH 23/27] Improve interface docstrings for analyze functions --- freqtrade/strategy/interface.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5b5cce268..279453920 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -346,7 +346,7 @@ class IStrategy(ABC): return dataframe - def analyze_pair(self, pair: str): + def analyze_pair(self, pair: str) -> None: """ Fetch data for this pair from dataprovider and analyze. Stores the dataframe into the dataprovider. @@ -376,7 +376,11 @@ class IStrategy(ABC): logger.warning('Empty dataframe for pair %s', pair) return - def analyze(self, pairs: List[str]): + def analyze(self, pairs: List[str]) -> None: + """ + Analyze all pairs using analyze_pair(). + :param pairs: List of pairs to analyze + """ for pair in pairs: self.analyze_pair(pair) @@ -386,7 +390,9 @@ class IStrategy(ABC): return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1] def assert_df(self, dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime): - """ make sure data is unmodified """ + """ + Ensure dataframe (length, last candle) was not modified, and has all elements we need. + """ message = "" if df_len != len(dataframe): message = "length" @@ -403,10 +409,9 @@ class IStrategy(ABC): def get_signal(self, pair: str, timeframe: str) -> Tuple[bool, bool]: """ Calculates current signal based based on the buy / sell columns of the dataframe. - Used by Bot to get the latest signal + Used by Bot to get the signal to buy or sell :param pair: pair in format ANT/BTC :param timeframe: timeframe to use - :param dataframe: Dataframe to analyze :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ if not self.dp: @@ -429,8 +434,7 @@ class IStrategy(ABC): if latest_date < (arrow.utcnow().shift(minutes=-(timeframe_minutes * 2 + offset))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', - pair, - (arrow.utcnow() - latest_date).seconds // 60 + pair, (arrow.utcnow() - latest_date).seconds // 60 ) return False, False From f1993fb2f48b986caef0372f1d6e9e0fe39f3c29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 08:01:09 +0200 Subject: [PATCH 24/27] Pass analyzed dataframe to get_signal --- freqtrade/freqtradebot.py | 8 ++++-- freqtrade/strategy/interface.py | 8 ++---- tests/conftest.py | 2 +- tests/strategy/test_interface.py | 43 ++++++-------------------------- 4 files changed, 16 insertions(+), 45 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 09b794a99..59f4447d7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -424,7 +424,8 @@ class FreqtradeBot: return False # running get_signal on historical data fetched - (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe) + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) + (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: stake_amount = self.get_trade_stake_amount(pair) @@ -705,7 +706,10 @@ class FreqtradeBot: if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): - (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe) + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) + + (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) if config_ask_strategy.get('use_order_book', False): order_book_min = config_ask_strategy.get('order_book_min', 1) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 279453920..969259446 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -406,19 +406,15 @@ class IStrategy(ABC): else: raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.") - def get_signal(self, pair: str, timeframe: str) -> Tuple[bool, bool]: + def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ Calculates current signal based based on the buy / sell columns of the dataframe. Used by Bot to get the signal to buy or sell :param pair: pair in format ANT/BTC :param timeframe: timeframe to use + :param dataframe: Analyzed dataframe to get signal from. :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ - if not self.dp: - raise OperationalException("DataProvider not found.") - - dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) - if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning(f'Empty candle (OHLCV) data for pair {pair}') return False, False diff --git a/tests/conftest.py b/tests/conftest.py index c64f443bc..8e8c1bfaa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,7 +163,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: :param value: which value IStrategy.get_signal() must return :return: None """ - freqtrade.strategy.get_signal = lambda e, s: value + freqtrade.strategy.get_signal = lambda e, s, x: value freqtrade.exchange.refresh_latest_ohlcv = lambda p: None diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 835465f38..70ae067d7 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -31,40 +31,15 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): mocked_history['buy'] = 0 mocked_history.loc[1, 'sell'] = 1 - mocker.patch.object( - _STRATEGY.dp, 'get_analyzed_dataframe', - return_value=(mocked_history, 0) - ) - - assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, True) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 1 - mocker.patch.object( - _STRATEGY.dp, 'get_analyzed_dataframe', - return_value=(mocked_history, 0) - ) - assert _STRATEGY.get_signal('ETH/BTC', '5m') == (True, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 0 - mocker.patch.object( - _STRATEGY.dp, 'get_analyzed_dataframe', - return_value=(mocked_history, 0) - ) - assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, False) - - -def test_trade_no_dataprovider(default_conf, mocker, caplog): - strategy = DefaultStrategy({}) - # Delete DP for sure (suffers from test leakage, as this is updated in the base class) - if strategy.dp is not None: - strategy.dp = None - with pytest.raises(OperationalException, match="DataProvider not found."): - strategy.get_signal('ETH/BTC', '5m') - - with pytest.raises(OperationalException, match="DataProvider not found."): - strategy.analyze_pair('ETH/BTC') + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False) def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): @@ -81,18 +56,15 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): def test_get_signal_empty(default_conf, mocker, caplog): - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame(), 0)) - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe']) + assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], DataFrame()) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(None, 0)) - assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe']) + assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) caplog.clear() - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame([]), 0)) - assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe']) + assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe'], DataFrame([])) assert log_has('Empty candle (OHLCV) data for pair baz', caplog) @@ -126,10 +98,9 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): mocked_history.loc[1, 'buy'] = 1 caplog.set_level(logging.INFO) - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(mocked_history, 0)) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe']) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], mocked_history) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) From eef3c01da73008c9f9a4bb03ff5c4106cbbf68fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 19:46:03 +0200 Subject: [PATCH 25/27] Fix function header formatting --- docs/strategy-advanced.md | 4 ++-- freqtrade/strategy/interface.py | 4 ++-- freqtrade/templates/subtemplates/strategy_methods_advanced.j2 | 4 ++-- tests/strategy/test_interface.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 100e96b81..c576cb46e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -168,8 +168,8 @@ class Awesomestrategy(IStrategy): # ... populate_* methods - def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, - time_in_force: str, sell_reason: str, ** kwargs) -> bool: + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 969259446..f8e59ac7b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -221,8 +221,8 @@ class IStrategy(ABC): """ return True - def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, - time_in_force: str, sell_reason: str, ** kwargs) -> bool: + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 28d2d1c1b..c7ce41bb7 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -34,8 +34,8 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f """ return True -def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, rate: float, - time_in_force: str, sell_reason: str, ** kwargs) -> bool: +def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 70ae067d7..381454622 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -9,7 +9,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError, OperationalException +from freqtrade.exceptions import StrategyError from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper From c4a9a79be08fdc26865ac23b53879b81076f3c3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jul 2020 09:43:49 +0200 Subject: [PATCH 26/27] Apply suggested documentation changes from code review Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/bot-basics.md | 4 ++-- docs/strategy-advanced.md | 12 ++++++------ docs/strategy-customization.md | 4 ++-- freqtrade/strategy/interface.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 728dcf46e..44f493456 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -1,10 +1,10 @@ # Freqtrade basics -This page will try to teach you some basic concepts on how freqtrade works and operates. +This page provides you some basic concepts on how Freqtrade works and operates. ## Freqtrade terminology -* Trade: Open position +* Trade: Open position. * Open Order: Order which is currently placed on the exchange, and is not yet complete. * Pair: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT). * Timeframe: Candle length to use (e.g. `"5m"`, `"1h"`, ...). diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index c576cb46e..a5977e5dc 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -3,7 +3,7 @@ This page explains some advanced concepts available for strategies. If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first. -[Freqtrade basics](bot-basics.md) describes in which sequence each method defined below is called, which can be helpful to understand which method to use. +[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs. !!! Note All callback methods described below should only be implemented in a strategy if they are also actively used. @@ -97,8 +97,8 @@ class Awesomestrategy(IStrategy): ## Bot loop start callback -A simple callback which is called at the start of every bot iteration. -This can be used to perform calculations which are pair independent. +A simple callback which is called once at the start of every bot throttling iteration. +This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. ``` python import requests @@ -111,7 +111,7 @@ class Awesomestrategy(IStrategy): """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks - (e.g. gather some remote ressource for comparison) + (e.g. gather some remote resource for comparison) :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ if self.config['runmode'].value in ('live', 'dry_run'): @@ -125,7 +125,7 @@ class Awesomestrategy(IStrategy): ### Trade entry (buy order) confirmation -`confirm_trade_entry()` an be used to abort a trade entry at the latest second (maybe because the price is not what we expect). +`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). ``` python class Awesomestrategy(IStrategy): @@ -158,7 +158,7 @@ class Awesomestrategy(IStrategy): ### Trade exit (sell order) confirmation -`confirm_trade_exit()` an be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect). +`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect). ``` python from freqtrade.persistence import Trade diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 0a1049f3b..50fec79dc 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -1,8 +1,8 @@ # Strategy Customization -This page explains how to customize your strategies, and add new indicators. +This page explains how to customize your strategies, add new indicators and set up trading rules. -Please familiarize yourself with [Freqtrade basics](bot-basics.md) first. +Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates. ## Install a custom strategy file diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f8e59ac7b..f3c5e154d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -194,7 +194,7 @@ class IStrategy(ABC): """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks - (e.g. gather some remote ressource for comparison) + (e.g. gather some remote resource for comparison) :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass From 75318525a96b018fe138aaaf3a51773a6b437e88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jul 2020 16:41:19 +0200 Subject: [PATCH 27/27] Update docs/strategy-advanced.md Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index a5977e5dc..e4bab303e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -6,7 +6,7 @@ If you're just getting started, please be familiar with the methods described in [Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs. !!! Note - All callback methods described below should only be implemented in a strategy if they are also actively used. + All callback methods described below should only be implemented in a strategy if they are actually used. ## Custom order timeout rules