From be03c22dba4037d371cb26ff43431e10c928f5a0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jun 2020 00:35:58 +0300 Subject: [PATCH 001/123] Minor: Fix exception message --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/kraken.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4279f392c..4d76c7966 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -78,7 +78,7 @@ class Binance(Exchange): return order except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 932d82a27..cac9a945c 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -85,7 +85,7 @@ class Kraken(Exchange): return order except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: From 1bf333d3200ba931215303b6886be1be89065902 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jun 2020 00:57:13 +0300 Subject: [PATCH 002/123] Minor: fix test --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 2b63eee23..48c4956cf 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -705,7 +705,7 @@ def test_validate_order_types(default_conf, mocker): 'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', - 'stoploss_on_exchange': 'false' + 'stoploss_on_exchange': False } with pytest.raises(OperationalException, From 4660909e958eb8da2b79e989435c4e7426f5fbba Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jun 2020 01:07:00 +0300 Subject: [PATCH 003/123] Validate stoploss_on_exchange_limit_ratio at startup time --- freqtrade/exchange/exchange.py | 7 +++++++ tests/exchange/test_exchange.py | 26 +++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bd44f56f2..820526b49 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -386,6 +386,13 @@ class Exchange: f'On exchange stoploss is not supported for {self.name}.' ) + # Limit price threshold: As limit price should always be below stop-price + # Used for limit stoplosses on exchange + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + if limit_price_pct >= 1.0 or limit_price_pct <= 0.0: + raise OperationalException( + "stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0") + def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: """ Checks if order time in force configured in strategy/config are supported diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 48c4956cf..1aaf95379 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -689,13 +689,13 @@ def test_validate_order_types(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') + default_conf['order_types'] = { 'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', 'stoploss_on_exchange': False } - Exchange(default_conf) type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) @@ -707,7 +707,6 @@ def test_validate_order_types(default_conf, mocker): 'stoploss': 'market', 'stoploss_on_exchange': False } - with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): Exchange(default_conf) @@ -718,11 +717,32 @@ def test_validate_order_types(default_conf, mocker): 'stoploss': 'limit', 'stoploss_on_exchange': True } - with pytest.raises(OperationalException, match=r'On exchange stoploss is not supported for .*'): Exchange(default_conf) + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False, + 'stoploss_on_exchange_limit_ratio': 1.05 + } + with pytest.raises(OperationalException, + match=r'stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0'): + Exchange(default_conf) + + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False, + 'stoploss_on_exchange_limit_ratio': -0.1 + } + with pytest.raises(OperationalException, + match=r'stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0'): + Exchange(default_conf) + def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() From de36f3d850b5f17f027ef2ac0fd1ad147d2c4a47 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jun 2020 01:42:45 +0300 Subject: [PATCH 004/123] Cosmetics in freqtradebot --- freqtrade/freqtradebot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8a66957c3..341cd5416 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -795,10 +795,8 @@ class FreqtradeBot: return False # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange - if (not stoploss_order): - + if not stoploss_order: stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss - stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price): From fd97ad9b76973f7ec463591be62e577a9b703b1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Jun 2020 14:02:21 +0200 Subject: [PATCH 005/123] 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 006/123] 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 007/123] 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 008/123] 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 009/123] 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 010/123] 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 011/123] 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 012/123] 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 013/123] 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 014/123] 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 015/123] 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 016/123] 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 017/123] 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 018/123] 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 019/123] 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 020/123] 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 021/123] 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 022/123] 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 023/123] 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 024/123] 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 025/123] 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 026/123] 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 027/123] 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 028/123] 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 029/123] 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 f976905728c45ee21e6daccea25eb25d3ff3ec47 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 20:00:18 +0200 Subject: [PATCH 030/123] Fix more exchange message typos --- freqtrade/exchange/exchange.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 48219096d..4564e671f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -526,13 +526,13 @@ class Exchange: except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to create {ordertype} {side} order on market {pair}.' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise DependencyException( - f'Could not create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate}.' + f'Could not create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( From 0509b9a8fce4eacfa55e01a876b909958ffcf4c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jun 2020 06:43:19 +0200 Subject: [PATCH 031/123] Show winning vs. losing trades --- freqtrade/rpc/rpc.py | 8 ++++++++ freqtrade/rpc/telegram.py | 4 +++- tests/rpc/test_rpc_apiserver.py | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aeaf82662..4cb432aea 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -269,6 +269,8 @@ class RPC: profit_closed_coin = [] profit_closed_ratio = [] durations = [] + winning_trades = 0 + losing_trades = 0 for trade in trades: current_rate: float = 0.0 @@ -282,6 +284,10 @@ class RPC: profit_ratio = trade.close_profit profit_closed_coin.append(trade.close_profit_abs) profit_closed_ratio.append(profit_ratio) + if trade.close_profit > 0: + winning_trades += 1 + else: + losing_trades += 1 else: # Get current rate try: @@ -344,6 +350,8 @@ class RPC: 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_pair': best_pair[0] if best_pair else '', 'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, + 'winning_trades': winning_trades, + 'losing_trades': losing_trades, } def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9b40ee2f6..13cc1afaf 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -366,7 +366,9 @@ class Telegram(RPC): f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" f"*Total Trade Count:* `{trade_count}`\n" f"*First Trade opened:* `{first_trade_date}`\n" - f"*Latest Trade opened:* `{latest_trade_date}`") + f"*Latest Trade opened:* `{latest_trade_date}\n`" + f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" + ) if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8e73eacf8..77a3e5c30 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -444,6 +444,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li 'profit_closed_percent_sum': 6.2, 'trade_count': 1, 'closed_trade_count': 1, + 'winning_trades': 1, + 'losing_trades': 0, } From 6734269bfcb253171a84a51efeb212b18b7cbff1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jun 2020 19:22:50 +0200 Subject: [PATCH 032/123] Use >= to compare for winning trades --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4cb432aea..d6f840aa9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -284,7 +284,7 @@ class RPC: profit_ratio = trade.close_profit profit_closed_coin.append(trade.close_profit_abs) profit_closed_ratio.append(profit_ratio) - if trade.close_profit > 0: + if trade.close_profit >= 0: winning_trades += 1 else: losing_trades += 1 From e813573f270eb8e5579b063d8426954cd87c1451 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 18:35:46 +0200 Subject: [PATCH 033/123] Warning message for open trades when stopping bot --- freqtrade/freqtradebot.py | 13 +++++++++++++ freqtrade/worker.py | 3 +++ 2 files changed, 16 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 289850709..2a1a1492c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -175,6 +175,19 @@ class FreqtradeBot: if self.config['cancel_open_orders_on_exit']: self.cancel_all_open_orders() + def check_for_open_trades(self): + open_trades = Trade.get_trades([Trade.is_open == True, + ]).all() + + if len(open_trades) is not 0: + msg = { + f'type': RPCMessageType.WARNING_NOTIFICATION, + f'status': f'{len(open_trades)} OPEN TRADES ACTIVE\n\n' + f'Handle these trades manually or \'/start\' the bot again ' + f'and use \'/stopbuy\' to handle open trades gracefully.' + } + self.rpc.send_msg(msg) + def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: """ Refresh active whitelist from pairlist or edge and extend it with diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 5bdb166c2..2fc206bd5 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -90,6 +90,9 @@ class Worker: if state == State.RUNNING: self.freqtrade.startup() + if state == State.STOPPED: + self.freqtrade.check_for_open_trades() + # Reset heartbeat timestamp to log the heartbeat message at # first throttling iteration when the state changes self._heartbeat_msg = 0 From 0642ab76bf0f69180a498a3b48e7b5ca25a4b26e Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 18:40:44 +0200 Subject: [PATCH 034/123] Added information to the new function --- freqtrade/freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2a1a1492c..060250b49 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -176,6 +176,10 @@ class FreqtradeBot: self.cancel_all_open_orders() def check_for_open_trades(self): + """ + Notify the user when he stops the bot + and there are still open trades active. + """ open_trades = Trade.get_trades([Trade.is_open == True, ]).all() From 48289e8ca78236f6e2b9dc7eeb67409117f3d0eb Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 20:24:50 +0200 Subject: [PATCH 035/123] Added exchange name, removed capital letters --- freqtrade/freqtradebot.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 060250b49..e3bb5b70b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -177,7 +177,7 @@ class FreqtradeBot: def check_for_open_trades(self): """ - Notify the user when he stops the bot + Notify the user when the bot is stopped and there are still open trades active. """ open_trades = Trade.get_trades([Trade.is_open == True, @@ -186,9 +186,10 @@ class FreqtradeBot: if len(open_trades) is not 0: msg = { f'type': RPCMessageType.WARNING_NOTIFICATION, - f'status': f'{len(open_trades)} OPEN TRADES ACTIVE\n\n' - f'Handle these trades manually or \'/start\' the bot again ' - f'and use \'/stopbuy\' to handle open trades gracefully.' + f'status': f'{len(open_trades)} open trades active.\n\n' + f'Handle these trades manually on {self.exchange.name}, ' + f'or \'/start\' the bot again and use \'/stopbuy\' ' + f'to handle open trades gracefully.' } self.rpc.send_msg(msg) From b938c536fa5cd61f86311e6e83d3901228cc91b9 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 21:46:53 +0200 Subject: [PATCH 036/123] Trying to fix flake8 errors --- freqtrade/freqtradebot.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e3bb5b70b..6d9002c77 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -180,16 +180,16 @@ class FreqtradeBot: Notify the user when the bot is stopped and there are still open trades active. """ - open_trades = Trade.get_trades([Trade.is_open == True, - ]).all() + open_trades = Trade.get_trades([Trade.is_open == 1, + ]).all() - if len(open_trades) is not 0: + if len(open_trades) != 0: msg = { - f'type': RPCMessageType.WARNING_NOTIFICATION, - f'status': f'{len(open_trades)} open trades active.\n\n' + 'type': RPCMessageType.WARNING_NOTIFICATION, + 'status': f'{len(open_trades)} open trades active.\n\n' f'Handle these trades manually on {self.exchange.name}, ' f'or \'/start\' the bot again and use \'/stopbuy\' ' - f'to handle open trades gracefully.' + f'to handle open trades gracefully.', } self.rpc.send_msg(msg) From e5676867a871771dbc887f332464a1b0509e233f Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 21:53:12 +0200 Subject: [PATCH 037/123] Trying to fix flake8 errors --- freqtrade/freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6d9002c77..a0bfe5bc4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -180,8 +180,7 @@ class FreqtradeBot: Notify the user when the bot is stopped and there are still open trades active. """ - open_trades = Trade.get_trades([Trade.is_open == 1, - ]).all() + open_trades = Trade.get_trades([Trade.is_open == 1]).all() if len(open_trades) != 0: msg = { From 118f0511719a9479b83f5010367debc66c5b695d Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sun, 28 Jun 2020 11:02:50 +0200 Subject: [PATCH 038/123] Added message in cleanup and fixes --- freqtrade/freqtradebot.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a0bfe5bc4..e9e65ccac 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -119,6 +119,8 @@ class FreqtradeBot: if self.config['cancel_open_orders_on_exit']: self.cancel_all_open_orders() + self.check_for_open_trades() + self.rpc.cleanup() persistence.cleanup() @@ -185,10 +187,11 @@ class FreqtradeBot: if len(open_trades) != 0: msg = { 'type': RPCMessageType.WARNING_NOTIFICATION, - 'status': f'{len(open_trades)} open trades active.\n\n' - f'Handle these trades manually on {self.exchange.name}, ' - f'or \'/start\' the bot again and use \'/stopbuy\' ' - f'to handle open trades gracefully.', + 'status': f"{len(open_trades)} open trades active.\n\n" + f"Handle these trades manually on {self.exchange.name}, " + f"or '/start' the bot again and use '/stopbuy' " + f"to handle open trades gracefully. \n" + f"{'Trades are simulated.' if self.config['dry_run'] else ''}", } self.rpc.send_msg(msg) From 2c45114a64c31ff1abaed6e6d22bf0fe0561e2f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 11:17:06 +0200 Subject: [PATCH 039/123] Implement DDos backoff (1s) --- freqtrade/exceptions.py | 7 +++++++ freqtrade/exchange/binance.py | 7 +++++-- freqtrade/exchange/common.py | 8 +++++++- freqtrade/exchange/exchange.py | 30 ++++++++++++++++++++++++++---- freqtrade/exchange/ftx.py | 11 +++++++++-- freqtrade/exchange/kraken.py | 6 +++++- tests/exchange/test_exchange.py | 19 +++++++++++++++++-- 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 7cfed87e8..bc84f30b8 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -45,6 +45,13 @@ class TemporaryError(FreqtradeException): """ +class DDosProtection(TemporaryError): + """ + Temporary error caused by DDOS protection. + Bot will wait for a second and then retry. + """ + + class StrategyError(FreqtradeException): """ Errors with custom user-code deteced. diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4279f392c..3a98f161b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) @@ -88,6 +89,8 @@ class Binance(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index a10d41247..1f74412c6 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -1,6 +1,8 @@ +import asyncio import logging +import time -from freqtrade.exceptions import TemporaryError +from freqtrade.exceptions import DDosProtection, TemporaryError logger = logging.getLogger(__name__) @@ -99,6 +101,8 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + await asyncio.sleep(1) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -117,6 +121,8 @@ def retrier(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + time.sleep(1) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b62410c34..09f27e638 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -18,12 +18,13 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision) from pandas import DataFrame +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts, safe_value_fallback -from freqtrade.constants import ListPairsWithTimeframes CcxtModuleType = Any @@ -527,6 +528,8 @@ class Exchange: f'Could not create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e @@ -606,6 +609,8 @@ class Exchange: balances.pop("used", None) return balances + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e @@ -620,6 +625,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e @@ -633,6 +640,8 @@ class Exchange: raise DependencyException(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e @@ -766,6 +775,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical ' f'candle (OHLCV) data. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not fetch historical candle (OHLCV) data ' f'for pair {pair} due to {e.__class__.__name__}. ' @@ -802,6 +813,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical trade data.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. ' f'Message: {e}') from e @@ -948,6 +961,8 @@ class Exchange: except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e @@ -1003,6 +1018,8 @@ class Exchange: except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e @@ -1027,6 +1044,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e @@ -1063,7 +1082,8 @@ class Exchange: matched_trades = [trade for trade in my_trades if trade['order'] == order_id] return matched_trades - + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e @@ -1080,6 +1100,8 @@ class Exchange: return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, price=price, takerOrMaker=taker_or_maker)['rate'] + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f16db96f5..7d0778ae5 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -68,6 +69,8 @@ class Ftx(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e @@ -96,6 +99,8 @@ class Ftx(Exchange): except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e @@ -111,6 +116,8 @@ class Ftx(Exchange): except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 932d82a27..7710b8e9b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, +from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -45,6 +45,8 @@ class Kraken(Exchange): balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] return balances + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e @@ -93,6 +95,8 @@ class Kraken(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 700aff969..f3a3a3789 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4,14 +4,14 @@ import copy import logging from datetime import datetime, timezone from random import randint -from unittest.mock import MagicMock, Mock, PropertyMock +from unittest.mock import MagicMock, Mock, PropertyMock, patch import arrow import ccxt import pytest from pandas import DataFrame -from freqtrade.exceptions import (DependencyException, InvalidOrderException, +from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange.common import API_RETRY_COUNT @@ -38,6 +38,14 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, **kwargs): + + with patch('freqtrade.exchange.common.time.sleep'): + with pytest.raises(DDosProtection): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) @@ -52,6 +60,13 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): + + with patch('freqtrade.exchange.common.asyncio.sleep'): + with pytest.raises(DDosProtection): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + await getattr(exchange, fun)(**kwargs) + with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) From 5bd4798ed0953ab1b94fd5a2cc388cdd82eccf50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 11:56:29 +0200 Subject: [PATCH 040/123] Add retrier to stoploss calls (but without retrying) --- freqtrade/exchange/binance.py | 2 ++ freqtrade/exchange/common.py | 44 +++++++++++++++++++-------------- freqtrade/exchange/ftx.py | 1 + freqtrade/exchange/kraken.py | 1 + tests/exchange/test_binance.py | 15 ++++------- tests/exchange/test_exchange.py | 24 ++++++++++++------ tests/exchange/test_ftx.py | 16 ++++-------- tests/exchange/test_kraken.py | 15 +++-------- 8 files changed, 61 insertions(+), 57 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 3a98f161b..ee9566282 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -8,6 +8,7 @@ from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier logger = logging.getLogger(__name__) @@ -40,6 +41,7 @@ class Binance(Exchange): """ return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss limit order. diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 1f74412c6..d931e1789 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -1,6 +1,7 @@ import asyncio import logging import time +from functools import wraps from freqtrade.exceptions import DDosProtection, TemporaryError @@ -110,21 +111,28 @@ def retrier_async(f): return wrapper -def retrier(f): - def wrapper(*args, **kwargs): - count = kwargs.pop('count', API_RETRY_COUNT) - try: - return f(*args, **kwargs) - except TemporaryError as ex: - logger.warning('%s() returned exception: "%s"', f.__name__, ex) - if count > 0: - count -= 1 - kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection): - time.sleep(1) - return wrapper(*args, **kwargs) - else: - logger.warning('Giving up retrying: %s()', f.__name__) - raise ex - return wrapper +def retrier(_func=None, retries=API_RETRY_COUNT): + def decorator(f): + @wraps(f) + def wrapper(*args, **kwargs): + count = kwargs.pop('count', retries) + try: + return f(*args, **kwargs) + except TemporaryError as ex: + logger.warning('%s() returned exception: "%s"', f.__name__, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + time.sleep(1) + return wrapper(*args, **kwargs) + else: + logger.warning('Giving up retrying: %s()', f.__name__) + raise ex + return wrapper + # Support both @retrier and @retrier() syntax + if _func is None: + return decorator + else: + return decorator(_func) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 7d0778ae5..f1bd23b52 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -27,6 +27,7 @@ class Ftx(Exchange): """ return order['type'] == 'stop' and stop_loss > float(order['price']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss order. diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 7710b8e9b..01aa647b4 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -60,6 +60,7 @@ class Kraken(Exchange): """ return order['type'] == 'stop-loss' and stop_loss > float(order['price']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss market order. diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 52faa284b..72da708b4 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -5,8 +5,9 @@ import ccxt import pytest from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) + OperationalException) from tests.conftest import get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers @pytest.mark.parametrize('limitratio,expected', [ @@ -62,15 +63,9 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_binance(default_conf, mocker): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f3a3a3789..15ab0d3d9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -37,20 +37,20 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - fun, mock_ccxt_fun, **kwargs): + fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): with patch('freqtrade.exchange.common.time.sleep'): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) @@ -59,19 +59,21 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 -async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): +async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, + retries=API_RETRY_COUNT + 1, **kwargs): with patch('freqtrade.exchange.common.asyncio.sleep'): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) @@ -1142,9 +1144,10 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): exchange.get_balance(currency='BTC') -def test_get_balances_dry_run(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_balances_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.get_balances() == {} @@ -2126,6 +2129,13 @@ def test_get_markets(default_conf, mocker, markets, assert sorted(pairs.keys()) == sorted(expected_keys) +def test_get_markets_error(default_conf, mocker): + ex = get_patched_exchange(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=None)) + with pytest.raises(OperationalException, match="Markets were not loaded."): + ex.get_markets('LTC', 'USDT', True, False) + + def test_timeframe_to_minutes(): assert timeframe_to_minutes("5m") == 5 assert timeframe_to_minutes("10m") == 10 diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 75e98740c..1b7d68770 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -6,9 +6,9 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange + from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' @@ -85,15 +85,9 @@ def test_stoploss_order_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_ftx(default_conf, mocker): diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 0950979cf..9451c0b9e 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -6,8 +6,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -206,15 +205,9 @@ def test_stoploss_order_kraken(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_kraken(default_conf, mocker): From e74d2af85788b2e52a4025105e5f6063f29b50e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 15:44:58 +0200 Subject: [PATCH 041/123] Have TemporaryError a subCategory of DependencyException so it's safe to raise out of the exchange --- freqtrade/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index bc84f30b8..1ddb2a396 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,7 +37,7 @@ class InvalidOrderException(FreqtradeException): """ -class TemporaryError(FreqtradeException): +class TemporaryError(DependencyException): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user From bf61bc9d8329aed5ecc7fa034ccb458c1c442434 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:01:40 +0200 Subject: [PATCH 042/123] Introduce ExchangeError --- freqtrade/data/dataprovider.py | 4 ++-- freqtrade/exceptions.py | 9 ++++++++- freqtrade/exchange/binance.py | 4 ++-- freqtrade/exchange/exchange.py | 12 ++++++------ freqtrade/exchange/ftx.py | 4 ++-- freqtrade/exchange/kraken.py | 7 ++++--- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/rpc/rpc.py | 12 ++++++------ 8 files changed, 34 insertions(+), 26 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 058ca42da..b677f374b 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional from pandas import DataFrame from freqtrade.data.history import load_pair_history -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exchange import Exchange from freqtrade.state import RunMode from freqtrade.constants import ListPairsWithTimeframes @@ -105,7 +105,7 @@ class DataProvider: """ try: return self._exchange.fetch_ticker(pair) - except DependencyException: + except ExchangeError: return {} def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 1ddb2a396..995a2cdb7 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,7 +37,14 @@ class InvalidOrderException(FreqtradeException): """ -class TemporaryError(DependencyException): +class ExchangeError(DependencyException): + """ + Error raised out of the exchange. + Has multiple Errors to determine the appropriate error. + """ + + +class TemporaryError(ExchangeError): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index ee9566282..08e84ee34 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, DependencyException, +from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -80,7 +80,7 @@ class Binance(Exchange): 'stop price: %s. limit: %s', pair, stop_price, rate) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 09f27e638..d48e47909 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -20,7 +20,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.exceptions import (DDosProtection, DependencyException, +from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async @@ -352,7 +352,7 @@ class Exchange: for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: if pair in self.markets and self.markets[pair].get('active'): return pair - raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") + raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: Optional[str]) -> None: """ @@ -519,12 +519,12 @@ class Exchange: amount, rate_for_order, params) except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: - raise DependencyException( + raise ExchangeError( f'Could not create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e @@ -637,7 +637,7 @@ class Exchange: def fetch_ticker(self, pair: str) -> dict: try: if pair not in self._api.markets or not self._api.markets[pair].get('active'): - raise DependencyException(f"Pair {pair} not available") + raise ExchangeError(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data except ccxt.DDoSProtection as e: @@ -1151,7 +1151,7 @@ class Exchange: fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask') return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) - except DependencyException: + except ExchangeError: return None def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f1bd23b52..be815d336 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, DependencyException, +from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -61,7 +61,7 @@ class Ftx(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 01aa647b4..2ca4ba167 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, ExchangeError, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -87,7 +88,7 @@ class Kraken(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 289850709..2e59d915d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,14 +11,14 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from requests.exceptions import RequestException from freqtrade import __version__, constants, persistence from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.exceptions import DependencyException, InvalidOrderException, PricingError +from freqtrade.exceptions import (DependencyException, ExchangeError, + InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback from freqtrade.pairlist.pairlistmanager import PairListManager @@ -755,7 +755,7 @@ class FreqtradeBot: logger.warning('Selling the trade forcefully') self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL) - except DependencyException: + except ExchangeError: trade.stoploss_order_id = None logger.exception('Unable to place a stoploss order on exchange.') return False @@ -891,7 +891,7 @@ class FreqtradeBot: if not trade.open_order_id: continue order = self.exchange.get_order(trade.open_order_id, trade.pair) - except (RequestException, DependencyException, InvalidOrderException): + except (ExchangeError, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aeaf82662..aab3da258 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple import arrow from numpy import NAN, mean -from freqtrade.exceptions import DependencyException, TemporaryError +from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -130,7 +130,7 @@ class RPC: # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (ExchangeError, PricingError): current_rate = NAN current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) @@ -174,7 +174,7 @@ class RPC: # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (PricingError, ExchangeError): current_rate = NAN trade_percent = (100 * trade.calc_profit_ratio(current_rate)) trade_profit = trade.calc_profit(current_rate) @@ -286,7 +286,7 @@ class RPC: # Get current rate try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) @@ -352,7 +352,7 @@ class RPC: total = 0.0 try: tickers = self._freqtrade.exchange.get_tickers() - except (TemporaryError, DependencyException): + except (ExchangeError): raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) @@ -373,7 +373,7 @@ class RPC: if pair.startswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total - except (TemporaryError, DependencyException): + except (ExchangeError): logger.warning(f" Could not get rate for pair {coin}.") continue total = total + (est_stake or 0) From 29d3ff1bc922690b2aae5865cc574613ec454631 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:04:04 +0200 Subject: [PATCH 043/123] Adjust tests to work with ExchangeError --- tests/data/test_dataprovider.py | 6 +++--- tests/exchange/test_exchange.py | 2 +- tests/rpc/test_rpc.py | 11 ++++++----- tests/test_freqtradebot.py | 15 +++++++-------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index def3ad535..2f91dcd38 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider +from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode from tests.conftest import get_patched_exchange @@ -164,7 +164,7 @@ def test_ticker(mocker, default_conf, tickers): assert 'symbol' in res assert res['symbol'] == 'ETH/BTC' - ticker_mock = MagicMock(side_effect=DependencyException('Pair not found')) + ticker_mock = MagicMock(side_effect=ExchangeError('Pair not found')) mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, exchange) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 15ab0d3d9..64ea317aa 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -64,7 +64,7 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu with patch('freqtrade.exchange.common.asyncio.sleep'): with pytest.raises(DDosProtection): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DeadBeef")) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("Dooh")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0ffbaa72a..45243e5e6 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,12 +8,13 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo -from freqtrade.exceptions import DependencyException, TemporaryError +from freqtrade.exceptions import ExchangeError, TemporaryError from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from tests.conftest import get_patched_freqtradebot, patch_get_signal, create_mock_trades +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, + patch_get_signal) # Functions for recurrent object patching @@ -106,7 +107,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() assert isnan(results[0]['current_profit']) assert isnan(results[0]['current_rate']) @@ -209,7 +210,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '-0.41% (-0.06)' == result[0][3] mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] @@ -365,7 +366,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Test non-available pair mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5d83c893e..f2967411b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -9,13 +9,12 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -import requests from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, PricingError, - TemporaryError) +from freqtrade.exceptions import (DependencyException, ExchangeError, + InvalidOrderException, OperationalException, + PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType @@ -1172,7 +1171,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, mocker.patch( 'freqtrade.exchange.Exchange.stoploss', - side_effect=DependencyException() + side_effect=ExchangeError() ) trade.is_open = True freqtrade.handle_stoploss_on_exchange(trade) @@ -1216,7 +1215,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), - stoploss=MagicMock(side_effect=DependencyException()), + stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1442,7 +1441,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) - mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException()) + mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -2320,7 +2319,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')), + get_order=MagicMock(side_effect=ExchangeError('Oh snap')), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) From e040c518cac73e6ee1ad551757eef93407fc6fdc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:18:39 +0200 Subject: [PATCH 044/123] Dynamic backoff on DDos errors --- freqtrade/exchange/common.py | 11 +++++++++-- tests/exchange/test_exchange.py | 14 +++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index d931e1789..b0f6b14e3 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -91,6 +91,13 @@ MAP_EXCHANGE_CHILDCLASS = { } +def calculate_backoff(retry, max_retries): + """ + Calculate backoff + """ + return retry ** 2 + 1 + + def retrier_async(f): async def wrapper(*args, **kwargs): count = kwargs.pop('count', API_RETRY_COUNT) @@ -125,13 +132,13 @@ def retrier(_func=None, retries=API_RETRY_COUNT): kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection): - time.sleep(1) + time.sleep(calculate_backoff(count, retries)) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) raise ex return wrapper - # Support both @retrier and @retrier() syntax + # Support both @retrier and @retrier(retries=2) syntax if _func is None: return decorator else: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 64ea317aa..358452caf 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -14,7 +14,7 @@ from pandas import DataFrame from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.common import API_RETRY_COUNT +from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, timeframe_to_minutes, timeframe_to_msecs, @@ -2296,3 +2296,15 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: ex = get_patched_exchange(mocker, default_conf) assert ex.calculate_fee_rate(order) == expected + + +@pytest.mark.parametrize('retry,max_retries,expected', [ + (0, 3, 1), + (1, 3, 2), + (2, 3, 5), + (3, 3, 10), + (0, 1, 1), + (1, 1, 2), +]) +def test_calculate_backoff(retry, max_retries, expected): + assert calculate_backoff(retry, max_retries) == expected From 92c70fb903f59d9651f50efb3061f36a32d13cf3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:27:35 +0200 Subject: [PATCH 045/123] Rename get_order to fetch_order (to align to ccxt naming) --- freqtrade/exchange/exchange.py | 10 +++--- freqtrade/freqtradebot.py | 6 ++-- freqtrade/persistence.py | 2 +- freqtrade/rpc/rpc.py | 4 +-- tests/exchange/test_exchange.py | 12 +++---- tests/rpc/test_rpc.py | 8 ++--- tests/test_freqtradebot.py | 60 ++++++++++++++++----------------- 7 files changed, 51 insertions(+), 51 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d48e47909..5a19b34af 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -946,7 +946,7 @@ class Exchange: def check_order_canceled_empty(self, order: Dict) -> bool: """ Verify if an order has been cancelled without being partially filled - :param order: Order dict as returned from get_order() + :param order: Order dict as returned from fetch_order() :return: True if order has been cancelled without being filled, False otherwise. """ return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0 @@ -983,7 +983,7 @@ class Exchange: """ Cancel order returning a result. Creates a fake result if cancel order returns a non-usable result - and get_order does not work (certain exchanges don't return cancelled orders) + and fetch_order does not work (certain exchanges don't return cancelled orders) :param order_id: Orderid to cancel :param pair: Pair corresponding to order_id :param amount: Amount to use for fake response @@ -996,7 +996,7 @@ class Exchange: except InvalidOrderException: logger.warning(f"Could not cancel order {order_id}.") try: - order = self.get_order(order_id, pair) + order = self.fetch_order(order_id, pair) except InvalidOrderException: logger.warning(f"Could not fetch cancelled order {order_id}.") order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} @@ -1004,7 +1004,7 @@ class Exchange: return order @retrier - def get_order(self, order_id: str, pair: str) -> Dict: + def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: order = self._dry_run_open_orders[order_id] @@ -1027,7 +1027,7 @@ class Exchange: raise OperationalException(e) from e # Assign method to get_stoploss_order to allow easy overriding in other classes - get_stoploss_order = get_order + get_stoploss_order = fetch_order @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2e59d915d..0d9c5e27c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -890,7 +890,7 @@ class FreqtradeBot: try: if not trade.open_order_id: continue - order = self.exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) except (ExchangeError, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue @@ -923,7 +923,7 @@ class FreqtradeBot: for trade in Trade.get_open_order_trades(): try: - order = self.exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) except (DependencyException, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue @@ -1202,7 +1202,7 @@ class FreqtradeBot: # Update trade with order values logger.info('Found open order for %s', trade) try: - order = action_order or self.exchange.get_order(order_id, trade.pair) + order = action_order or self.exchange.fetch_order(order_id, trade.pair) except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 097a2f984..a6c1de402 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -360,7 +360,7 @@ class Trade(_DECL_BASE): def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. - :param order: order retrieved by exchange.get_order() + :param order: order retrieved by exchange.fetch_order() :return: None """ order_type = order['type'] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aab3da258..cc5f35f0d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -126,7 +126,7 @@ class RPC: for trade in trades: order = None if trade.open_order_id: - order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) @@ -442,7 +442,7 @@ class RPC: def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order if trade.open_order_id: - order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # Cancel open LIMIT_BUY orders and close trade if order and order['status'] == 'open' \ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 358452caf..cf38b3cd5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1865,31 +1865,31 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_order(default_conf, mocker, exchange_name): +def test_fetch_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order - assert exchange.get_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_order('Y', 'TKN/BTC') + exchange.fetch_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_order('X', 'TKN/BTC') == 456 + assert exchange.fetch_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_order(order_id='_', pair='TKN/BTC') + exchange.fetch_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_order', 'fetch_order', + 'fetch_order', 'fetch_order', order_id='_', pair='TKN/BTC') diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 45243e5e6..de9327ba9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -607,7 +607,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker, cancel_order=cancel_order_mock, - get_order=MagicMock( + fetch_order=MagicMock( return_value={ 'status': 'closed', 'type': 'limit', @@ -653,7 +653,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: trade = Trade.query.filter(Trade.id == '1').first() filled_amount = trade.amount / 2 mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -672,7 +672,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -689,7 +689,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: freqtradebot.enter_positions() # make an limit-sell open trade mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f2967411b..3b00f3371 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -762,7 +762,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -831,7 +831,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mock 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -858,7 +858,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -1063,7 +1063,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1178,7 +1178,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert log_has('Unable to place a stoploss order on exchange.', caplog) assert trade.stoploss_order_id is None - # Fifth case: get_order returns InvalidOrder + # Fifth case: fetch_order returns InvalidOrder # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() @@ -1193,7 +1193,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = None trade.is_open = False stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_order') + mocker.patch('freqtrade.exchange.Exchange.fetch_order') mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 0 @@ -1248,7 +1248,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=sell_mock, get_fee=fee, - get_order=MagicMock(return_value={'status': 'canceled'}), + fetch_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=InvalidOrderException()), ) freqtrade = FreqtradeBot(default_conf) @@ -1588,7 +1588,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1612,7 +1612,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) trade = MagicMock() trade.open_order_id = None @@ -1633,7 +1633,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1672,8 +1672,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) @@ -1697,8 +1697,8 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ limit_buy_order, mocker, caplog): trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) @@ -1723,7 +1723,7 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ def test_update_trade_state_exception(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) trade = MagicMock() trade.open_order_id = '123' @@ -1740,7 +1740,7 @@ def test_update_trade_state_exception(mocker, default_conf, def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=InvalidOrderException)) trade = MagicMock() @@ -1756,8 +1756,8 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) wallet_mock = MagicMock() mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) @@ -1972,7 +1972,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2021,7 +2021,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order_with_result=cancel_order_mock, get_fee=fee ) @@ -2051,7 +2051,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2078,7 +2078,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), fetch_ticker=ticker, - get_order=MagicMock(side_effect=DependencyException), + fetch_order=MagicMock(side_effect=DependencyException), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2104,7 +2104,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2151,7 +2151,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2182,7 +2182,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order_with_result=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2209,7 +2209,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2237,7 +2237,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), ) @@ -2275,7 +2275,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), ) @@ -2319,7 +2319,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(side_effect=ExchangeError('Oh snap')), + fetch_order=MagicMock(side_effect=ExchangeError('Oh snap')), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -4016,7 +4016,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, @pytest.mark.usefixtures("init_persistence") def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): default_conf['cancel_open_orders_on_exit'] = True - mocker.patch('freqtrade.exchange.Exchange.get_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=[DependencyException(), limit_sell_order, limit_buy_order]) buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') From cbcbb4bdb533247076b338035b640dc0f29f0495 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:30:24 +0200 Subject: [PATCH 046/123] Rename get_stoploss_order to fetch_stoploss_order (align with fetch_order) --- freqtrade/exchange/exchange.py | 6 +++--- freqtrade/exchange/ftx.py | 2 +- freqtrade/freqtradebot.py | 4 ++-- tests/exchange/test_exchange.py | 12 ++++++------ tests/exchange/test_ftx.py | 14 +++++++------- tests/test_freqtradebot.py | 18 +++++++++--------- tests/test_integration.py | 2 +- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5a19b34af..daa73ca35 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -969,7 +969,7 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - # Assign method to get_stoploss_order to allow easy overriding in other classes + # Assign method to fetch_stoploss_order to allow easy overriding in other classes cancel_stoploss_order = cancel_order def is_cancel_order_result_suitable(self, corder) -> bool: @@ -1026,8 +1026,8 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - # Assign method to get_stoploss_order to allow easy overriding in other classes - get_stoploss_order = fetch_order + # Assign method to fetch_stoploss_order to allow easy overriding in other classes + fetch_stoploss_order = fetch_order @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index be815d336..b75f77ca4 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -79,7 +79,7 @@ class Ftx(Exchange): raise OperationalException(e) from e @retrier - def get_stoploss_order(self, order_id: str, pair: str) -> Dict: + def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: order = self._dry_run_open_orders[order_id] diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0d9c5e27c..bd6bba344 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -773,8 +773,8 @@ class FreqtradeBot: try: # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \ - if trade.stoploss_order_id else None + stoploss_order = self.exchange.fetch_stoploss_order( + trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cf38b3cd5..fc4bf490c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1894,7 +1894,7 @@ def test_fetch_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_stoploss_order(default_conf, mocker, exchange_name): +def test_fetch_stoploss_order(default_conf, mocker, exchange_name): # Don't test FTX here - that needs a seperate test if exchange_name == 'ftx': return @@ -1903,25 +1903,25 @@ def test_get_stoploss_order(default_conf, mocker, exchange_name): order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order - assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_stoploss_order('Y', 'TKN/BTC') + exchange.fetch_stoploss_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_stoploss_order('X', 'TKN/BTC') == 456 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_stoploss_order', 'fetch_order', + 'fetch_stoploss_order', 'fetch_order', order_id='_', pair='TKN/BTC') diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 1b7d68770..eb7d83be3 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -124,34 +124,34 @@ def test_stoploss_adjust_ftx(mocker, default_conf): assert not exchange.stoploss_adjust(1501, order) -def test_get_stoploss_order(default_conf, mocker): +def test_fetch_stoploss_order(default_conf, mocker): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id='ftx') exchange._dry_run_open_orders['X'] = order - assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_stoploss_order('Y', 'TKN/BTC') + exchange.fetch_stoploss_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456' + assert exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] == '456' api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): - exchange.get_stoploss_order('X', 'TKN/BTC')['status'] + exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_orders.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', - 'get_stoploss_order', 'fetch_orders', + 'fetch_stoploss_order', 'fetch_orders', order_id='_', pair='TKN/BTC') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3b00f3371..ef48bdc34 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1125,7 +1125,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1138,7 +1138,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1163,7 +1163,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) assert trade.stoploss_order_id is None @@ -1182,7 +1182,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) @@ -1214,7 +1214,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), + fetch_stoploss_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf) @@ -1331,7 +1331,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1431,7 +1431,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c } mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1511,7 +1511,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 20% as edge dictated it. assert freqtrade.handle_trade(trade) is False @@ -2773,7 +2773,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f "fee": None, "trades": None }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_executed) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_executed) freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None diff --git a/tests/test_integration.py b/tests/test_integration.py index 57960503e..168286e6d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -62,7 +62,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, - get_stoploss_order=stoploss_order_mock, + fetch_stoploss_order=stoploss_order_mock, cancel_stoploss_order=cancel_order_mock, ) From 6362bfc36ef843c60046a94de8184b51a09f64e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 19:40:33 +0200 Subject: [PATCH 047/123] Fix calculate_backoff implementation --- freqtrade/exchange/common.py | 4 ++-- tests/exchange/test_exchange.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index b0f6b14e3..d3ba8cef6 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -91,11 +91,11 @@ MAP_EXCHANGE_CHILDCLASS = { } -def calculate_backoff(retry, max_retries): +def calculate_backoff(retrycount, max_retries): """ Calculate backoff """ - return retry ** 2 + 1 + return (max_retries - retrycount) ** 2 + 1 def retrier_async(f): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index fc4bf490c..8c397fe68 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2298,13 +2298,13 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: assert ex.calculate_fee_rate(order) == expected -@pytest.mark.parametrize('retry,max_retries,expected', [ - (0, 3, 1), - (1, 3, 2), - (2, 3, 5), - (3, 3, 10), - (0, 1, 1), - (1, 1, 2), +@pytest.mark.parametrize('retrycount,max_retries,expected', [ + (0, 3, 10), + (1, 3, 5), + (2, 3, 2), + (3, 3, 1), + (0, 1, 2), + (1, 1, 1), ]) -def test_calculate_backoff(retry, max_retries, expected): - assert calculate_backoff(retry, max_retries) == expected +def test_calculate_backoff(retrycount, max_retries, expected): + assert calculate_backoff(retrycount, max_retries) == expected From c6124180fe14bcdf9f78676d021c2e8e6880d1e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 19:45:42 +0200 Subject: [PATCH 048/123] Fix bug when fetching orders fails --- freqtrade/exceptions.py | 7 +++++++ freqtrade/exchange/common.py | 14 ++++++++------ freqtrade/exchange/exchange.py | 5 ++++- tests/exchange/test_exchange.py | 12 ++++++++++++ tests/test_freqtradebot.py | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 995a2cdb7..c85fccc4b 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,6 +37,13 @@ class InvalidOrderException(FreqtradeException): """ +class RetryableOrderError(InvalidOrderException): + """ + This is returned when the order is not found. + This Error will be repeated with increasing backof (in line with DDosError). + """ + + class ExchangeError(DependencyException): """ Error raised out of the exchange. diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index d3ba8cef6..9b7d8dfea 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -3,7 +3,8 @@ import logging import time from functools import wraps -from freqtrade.exceptions import DDosProtection, TemporaryError +from freqtrade.exceptions import (DDosProtection, RetryableOrderError, + TemporaryError) logger = logging.getLogger(__name__) @@ -109,8 +110,8 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection): - await asyncio.sleep(1) + if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + await asyncio.sleep(calculate_backoff(count + 1, API_RETRY_COUNT)) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -125,14 +126,15 @@ def retrier(_func=None, retries=API_RETRY_COUNT): count = kwargs.pop('count', retries) try: return f(*args, **kwargs) - except TemporaryError as ex: + except (TemporaryError, RetryableOrderError) as ex: logger.warning('%s() returned exception: "%s"', f.__name__, ex) if count > 0: count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection): - time.sleep(calculate_backoff(count, retries)) + if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + # increasing backoff + time.sleep(calculate_backoff(count + 1, retries)) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index daa73ca35..a3a548176 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -22,7 +22,7 @@ from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, - TemporaryError) + RetryableOrderError, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts, safe_value_fallback @@ -1015,6 +1015,9 @@ class Exchange: f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e try: return self._api.fetch_order(order_id, pair) + except ccxt.OrderNotFound as e: + raise RetryableOrderError( + f'Order not found (id: {order_id}). Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8c397fe68..66f88d82f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1888,6 +1888,18 @@ def test_fetch_order(default_conf, mocker, exchange_name): exchange.fetch_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 + api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + with patch('freqtrade.exchange.common.time.sleep') as tm: + with pytest.raises(InvalidOrderException): + exchange.fetch_order(order_id='_', pair='TKN/BTC') + # Ensure backoff is called + assert tm.call_args_list[0][0][0] == 1 + assert tm.call_args_list[1][0][0] == 2 + assert tm.call_args_list[2][0][0] == 5 + assert tm.call_args_list[3][0][0] == 10 + assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'fetch_order', 'fetch_order', order_id='_', pair='TKN/BTC') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ef48bdc34..654e6ca4f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2078,7 +2078,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), fetch_ticker=ticker, - fetch_order=MagicMock(side_effect=DependencyException), + fetch_order=MagicMock(side_effect=ExchangeError), cancel_order=cancel_order_mock, get_fee=fee ) From 4d9ecf137b5d2d037e888a56eda4a0bd78925f7c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 20:17:03 +0200 Subject: [PATCH 049/123] Fix failing test in python 3.7 can't use Magicmock in 3.7 (works in 3.8 though). --- freqtrade/exchange/common.py | 2 +- tests/exchange/test_exchange.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 9b7d8dfea..cc70bb875 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -110,7 +110,7 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + if isinstance(ex, DDosProtection): await asyncio.sleep(calculate_backoff(count + 1, API_RETRY_COUNT)) return await wrapper(*args, **kwargs) else: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 66f88d82f..251f257f7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -62,7 +62,7 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): - with patch('freqtrade.exchange.common.asyncio.sleep'): + with patch('freqtrade.exchange.common.asyncio.sleep', get_mock_coro(None)): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("Dooh")) exchange = get_patched_exchange(mocker, default_conf, api_mock) From b95065d70179318f1841708c64c7e333e763afb2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jun 2020 20:00:42 +0200 Subject: [PATCH 050/123] Log backoff --- freqtrade/exchange/common.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index cc70bb875..0610e8447 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -111,7 +111,9 @@ def retrier_async(f): kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection): - await asyncio.sleep(calculate_backoff(count + 1, API_RETRY_COUNT)) + backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT) + logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + await asyncio.sleep(backoff_delay) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -134,7 +136,9 @@ def retrier(_func=None, retries=API_RETRY_COUNT): logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): # increasing backoff - time.sleep(calculate_backoff(count + 1, retries)) + backoff_delay = calculate_backoff(count + 1, retries) + logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + time.sleep(backoff_delay) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) From efd6e4a87535adb95fa4eb7710259dd130a7395e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Jun 2020 07:16:08 +0200 Subject: [PATCH 051/123] Add test for check_for_open_trades --- tests/test_freqtradebot.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5d83c893e..d3014d7a8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4029,3 +4029,19 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi freqtrade.cancel_all_open_orders() assert buy_mock.call_count == 1 assert sell_mock.call_count == 1 + + +@pytest.mark.usefixtures("init_persistence") +def test_check_for_open_trades(mocker, default_conf, fee, limit_buy_order, limit_sell_order): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + freqtrade.check_for_open_trades() + assert freqtrade.rpc.send_msg.call_count == 0 + + create_mock_trades(fee) + trade = Trade.query.first() + trade.is_open = True + + freqtrade.check_for_open_trades() + assert freqtrade.rpc.send_msg.call_count == 1 + assert 'Handle these trades manually' in freqtrade.rpc.send_msg.call_args[0][0]['status'] From 61ae471eef74bd18619ce26710d5650e06d05ccb Mon Sep 17 00:00:00 2001 From: HumanBot Date: Tue, 30 Jun 2020 10:13:27 -0400 Subject: [PATCH 052/123] fixed --export trades command refers to issue 3413 @ https://github.com/freqtrade/freqtrade/issues/3413 --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 51b2e953b..ecd48bdc9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -66,7 +66,7 @@ Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies #### Exporting trades to file ```bash -freqtrade backtesting --export trades +freqtrade backtesting --export trades --config config.json --strategy SampleStrategy ``` The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. From 81850b5fdf5b860db1802fbda0998a2d1662b4dd Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:26:52 +0200 Subject: [PATCH 053/123] AgeFilter add actual amount of days in log message (debug info) --- freqtrade/pairlist/AgeFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index a23682599..b489a59bc 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -69,7 +69,7 @@ class AgeFilter(IPairList): return True else: self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because age is less than " + f"because age {len(daily_candles)} is less than " f"{self._min_days_listed} " f"{plural(self._min_days_listed, 'day')}") return False From 99ac2659f3566526ebbecf95af76a5030871f5fc Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:27:33 +0200 Subject: [PATCH 054/123] Init FIAT converter in api_server.py --- freqtrade/rpc/api_server.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index a2cef9a98..351842e10 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -17,6 +17,7 @@ from werkzeug.serving import make_server from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.rpc.fiat_convert import CryptoToFiatConverter logger = logging.getLogger(__name__) @@ -105,6 +106,9 @@ class ApiServer(RPC): # Register application handling self.register_rest_rpc_urls() + if self._config.get('fiat_display_currency', None): + self._fiat_converter = CryptoToFiatConverter() + thread = threading.Thread(target=self.run, daemon=True) thread.start() From db965332b99622fa93ae1638a94cf49688bd1e81 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:38:38 +0200 Subject: [PATCH 055/123] Update tests for AgeFilter message --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 072e497f3..a2644fe8c 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -389,7 +389,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t for pairlist in pairlists: if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ len(ohlcv_history_list) <= pairlist['min_days_listed']: - assert log_has_re(r'^Removed .* from whitelist, because age is less than ' + assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' r'.* day.*', caplog) if pairlist['method'] == 'PrecisionFilter' and whitelist_result: assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' From 39fa58973527431b5c66040d2b44f8f184abe788 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 13:39:02 +0200 Subject: [PATCH 056/123] Update API test, currently just with 'ANY' --- tests/rpc/test_rpc_apiserver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index cd2b0d311..2935094a5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -431,14 +431,14 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li 'latest_trade_date': 'just now', 'latest_trade_timestamp': ANY, 'profit_all_coin': 6.217e-05, - 'profit_all_fiat': 0, + 'profit_all_fiat': ANY, 'profit_all_percent': 6.2, 'profit_all_percent_mean': 6.2, 'profit_all_ratio_mean': 0.06201058, 'profit_all_percent_sum': 6.2, 'profit_all_ratio_sum': 0.06201058, 'profit_closed_coin': 6.217e-05, - 'profit_closed_fiat': 0, + 'profit_closed_fiat': ANY, 'profit_closed_percent': 6.2, 'profit_closed_ratio_mean': 0.06201058, 'profit_closed_percent_mean': 6.2, From f32e522bd73579d532541a7b444ced723a65e159 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 20:03:15 +0200 Subject: [PATCH 057/123] Update API test, removed 'ANY' --- tests/rpc/test_rpc_apiserver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2935094a5..45aa57588 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -431,14 +431,14 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li 'latest_trade_date': 'just now', 'latest_trade_timestamp': ANY, 'profit_all_coin': 6.217e-05, - 'profit_all_fiat': ANY, + 'profit_all_fiat': 0.76748865, 'profit_all_percent': 6.2, 'profit_all_percent_mean': 6.2, 'profit_all_ratio_mean': 0.06201058, 'profit_all_percent_sum': 6.2, 'profit_all_ratio_sum': 0.06201058, 'profit_closed_coin': 6.217e-05, - 'profit_closed_fiat': ANY, + 'profit_closed_fiat': 0.76748865, 'profit_closed_percent': 6.2, 'profit_closed_ratio_mean': 0.06201058, 'profit_closed_percent_mean': 6.2, From 23c0db925e22652b7e53b758bb21f0e6e1896600 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste LE STANG Date: Thu, 2 Jul 2020 20:55:16 +0200 Subject: [PATCH 058/123] Adding a dataprovider to the strategy before plotting --- freqtrade/plot/plotting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e8b0b4938..ae9f9c409 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -15,6 +15,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_prev_date from freqtrade.misc import pair_to_filename from freqtrade.resolvers import StrategyResolver +from freqtrade.data.dataprovider import DataProvider logger = logging.getLogger(__name__) @@ -474,6 +475,7 @@ def load_and_plot_trades(config: Dict[str, Any]): pair_counter += 1 logger.info("analyse pair %s", pair) + strategy.dp = DataProvider(config,config["exchange"]) df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(df_analyzed, trades_pair) From 20e8a29262e190f5be2d5f792a7bbaf03d6a2c73 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste LE STANG Date: Thu, 2 Jul 2020 20:55:16 +0200 Subject: [PATCH 059/123] Adding a dataprovider to the strategy before plotting Fix flake8 --- freqtrade/plot/plotting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e8b0b4938..db83448c0 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -15,6 +15,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_prev_date from freqtrade.misc import pair_to_filename from freqtrade.resolvers import StrategyResolver +from freqtrade.data.dataprovider import DataProvider logger = logging.getLogger(__name__) @@ -474,6 +475,7 @@ def load_and_plot_trades(config: Dict[str, Any]): pair_counter += 1 logger.info("analyse pair %s", pair) + strategy.dp = DataProvider(config, config["exchange"]) df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(df_analyzed, trades_pair) From c4a9a79be08fdc26865ac23b53879b81076f3c3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jul 2020 09:43:49 +0200 Subject: [PATCH 060/123] 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 061/123] 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 From f63045b0e982940615dbe48d176d2fb9397837f0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2020 09:11:49 +0000 Subject: [PATCH 062/123] Bump ccxt from 1.30.48 to 1.30.64 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.48 to 1.30.64. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.48...1.30.64) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 2948b8f35..2f225c93c 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.30.48 +ccxt==1.30.64 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 From 4c8bee1e5d23c947ae2e3a26157a0a7373249826 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2020 09:12:15 +0000 Subject: [PATCH 063/123] Bump mkdocs-material from 5.3.3 to 5.4.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.3.3 to 5.4.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.3.3...5.4.0) Signed-off-by: dependabot-preview[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index a0505c84b..3a236ee87 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.3.3 +mkdocs-material==5.4.0 mdx_truly_sane_lists==1.2 From 93dd70c77ddc717219b5d9cb9007f28d04469bfd Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2020 09:13:05 +0000 Subject: [PATCH 064/123] Bump joblib from 0.15.1 to 0.16.0 Bumps [joblib](https://github.com/joblib/joblib) from 0.15.1 to 0.16.0. - [Release notes](https://github.com/joblib/joblib/releases) - [Changelog](https://github.com/joblib/joblib/blob/master/CHANGES.rst) - [Commits](https://github.com/joblib/joblib/compare/0.15.1...0.16.0) Signed-off-by: dependabot-preview[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 2784bc156..da34e54b2 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,5 +6,5 @@ scipy==1.5.0 scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 -joblib==0.15.1 +joblib==0.16.0 progressbar2==3.51.4 From deb34d287990c5959afc34b04c270751f8336a21 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2020 19:58:28 +0000 Subject: [PATCH 065/123] Bump scipy from 1.5.0 to 1.5.1 Bumps [scipy](https://github.com/scipy/scipy) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.5.0...v1.5.1) Signed-off-by: dependabot-preview[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index da34e54b2..4773d9877 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.5.0 +scipy==1.5.1 scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 From 2e45859aef515efa5136054835f876fcb4e614f3 Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 8 Jul 2020 18:06:30 +0100 Subject: [PATCH 066/123] Added range checks to min_days_listed in AgeFilter --- freqtrade/exchange/exchange.py | 5 +++++ freqtrade/pairlist/AgeFilter.py | 10 ++++++++++ tests/pairlist/test_pairlist.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a3a548176..8aab225c6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -187,6 +187,11 @@ class Exchange: def timeframes(self) -> List[str]: return list((self._api.timeframes or {}).keys()) + @property + def ohlcv_candle_limit(self) -> int: + """exchange ohlcv candle limit""" + return int(self._ohlcv_candle_limit) + @property def markets(self) -> Dict: """exchange ccxt markets""" diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index b489a59bc..101f19cbe 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -23,6 +23,16 @@ class AgeFilter(IPairList): super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) self._min_days_listed = pairlistconfig.get('min_days_listed', 10) + + if self._min_days_listed < 1: + self.log_on_refresh(logger.info, "min_days_listed must be >= 1, " + "ignoring filter") + if self._min_days_listed > exchange.ohlcv_candle_limit: + self._min_days_listed = min(self._min_days_listed, exchange.ohlcv_candle_limit) + self.log_on_refresh(logger.info, "min_days_listed exceeds " + "exchange max request size " + f"({exchange.ohlcv_candle_limit}), using " + f"min_days_listed={self._min_days_listed}") self._enabled = self._min_days_listed >= 1 @property diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index a2644fe8c..6b96501c9 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -524,6 +524,38 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf +def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog) -> None: + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'AgeFilter', 'min_days_listed': -1}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + get_patched_freqtradebot(mocker, default_conf) + + assert log_has_re(r'min_days_listed must be >= 1, ' + r'ignoring filter', caplog) + + +def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog) -> None: + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'AgeFilter', 'min_days_listed': 99999}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + get_patched_freqtradebot(mocker, default_conf) + + assert log_has_re(r'^min_days_listed exceeds ' + r'exchange max request size', caplog) + + def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): mocker.patch.multiple('freqtrade.exchange.Exchange', From 091285ba43a9b17fd47b434e67f388dbf63f90cd Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 8 Jul 2020 18:32:14 +0100 Subject: [PATCH 067/123] Fix flake8 error in test_pairlist.py --- tests/pairlist/test_pairlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 6b96501c9..f95f001c9 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -524,7 +524,7 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf -def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog) -> None: +def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': -1}] @@ -540,7 +540,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick r'ignoring filter', caplog) -def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog) -> None: +def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': 99999}] From 14eab9be04569a16fea2a56ccb636fb0d205a267 Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 8 Jul 2020 22:02:04 +0100 Subject: [PATCH 068/123] Added min_price, max_price to PriceFilter --- config_full.json.example | 2 +- docs/configuration.md | 15 ++++++++-- freqtrade/pairlist/PriceFilter.py | 49 ++++++++++++++++++++++++++----- tests/pairlist/test_pairlist.py | 17 ++++++++--- 4 files changed, 67 insertions(+), 16 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index e1be01690..d5bfd3fe1 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -66,7 +66,7 @@ }, {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.01}, + {"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010}, {"method": "SpreadFilter", "max_spread_ratio": 0.005} ], "exchange": { diff --git a/docs/configuration.md b/docs/configuration.md index e7a79361a..74bacacc0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -662,16 +662,25 @@ Filters low-value coins which would not allow setting stoplosses. #### PriceFilter -The `PriceFilter` allows filtering of pairs by price. +The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported: +* `min_price` +* `max_price` +* `low_price_ratio` -Currently, only `low_price_ratio` setting is implemented, where a raise of 1 price unit (pip) is below the `low_price_ratio` ratio. +The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. +This option is disabled by default, and will only apply if set to <> 0. + +The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. +This option is disabled by default, and will only apply if set to <> 0. + +The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. This option is disabled by default, and will only apply if set to <> 0. Calculation example: Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. -These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. Here is what the PriceFilters takes over. +These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. #### ShuffleFilter diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 29dd88a76..1afc60999 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -18,7 +18,11 @@ class PriceFilter(IPairList): super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) - self._enabled = self._low_price_ratio != 0 + self._min_price = pairlistconfig.get('min_price', 0) + self._max_price = pairlistconfig.get('max_price', 0) + self._enabled = (self._low_price_ratio != 0) or \ + (self._min_price != 0) or \ + (self._max_price != 0) @property def needstickers(self) -> bool: @@ -33,7 +37,18 @@ class PriceFilter(IPairList): """ Short whitelist method description - used for startup-messages """ - return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%." + active_price_filters = [] + if self._low_price_ratio != 0: + active_price_filters.append(f"below {self._low_price_ratio * 100}%") + if self._min_price != 0: + active_price_filters.append(f"below {self._min_price:.8f}") + if self._max_price != 0: + active_price_filters.append(f"above {self._max_price:.8f}") + + if len(active_price_filters): + return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}." + + return f"{self.name} - No price filters configured." def _validate_pair(self, ticker) -> bool: """ @@ -46,10 +61,28 @@ class PriceFilter(IPairList): f"Removed {ticker['symbol']} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).") return False - compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) - changeperc = compare / ticker['last'] - if changeperc > self._low_price_ratio: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because 1 unit is {changeperc * 100:.3f}%") - return False + + # Perform low_price_ratio check. + if self._low_price_ratio != 0: + compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) + changeperc = compare / ticker['last'] + if changeperc > self._low_price_ratio: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because 1 unit is {changeperc * 100:.3f}%") + return False + + # Perform min_price check. + if self._min_price != 0: + if ticker['last'] < self._min_price: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price < {self._min_price:.8f}") + return False + + # Perform max_price check. + if self._max_price != 0: + if ticker['last'] > self._max_price: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price > {self._max_price:.8f}") + return False + return True diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index f95f001c9..09cbe9d5f 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -275,11 +275,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.03}], "USDT", ['ETH/USDT', 'NANO/USDT']), - # Hot is removed by precision_filter, Fuel by low_price_filter. + # Hot is removed by precision_filter, Fuel by low_price_ratio, Ripple by min_price. ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.02}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), + {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.01}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + # Hot is removed by precision_filter, Fuel by low_price_ratio, Ethereum by max_price. + ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, + {"method": "PrecisionFilter"}, + {"method": "PriceFilter", "low_price_ratio": 0.02, "max_price": 0.05}], + "BTC", ['TKN/BTC', 'LTC/BTC', 'XRP/BTC']), # HOT and XRP are removed because below 1250 quoteVolume ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "min_value": 1250}], @@ -319,7 +324,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", 'filter_at_the_beginning'), # OperationalException expected # PriceFilter after StaticPairList ([{"method": "StaticPairList"}, - {"method": "PriceFilter", "low_price_ratio": 0.02}], + {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.000001, "max_price": 0.1}], "BTC", ['ETH/BTC', 'TKN/BTC']), # PriceFilter only ([{"method": "PriceFilter", "low_price_ratio": 0.02}], @@ -396,6 +401,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t r'would be <= stop limit.*', caplog) if pairlist['method'] == 'PriceFilter' and whitelist_result: assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or + log_has_re(r'^Removed .* from whitelist, ' + r'because last price < .*%$', caplog) or + log_has_re(r'^Removed .* from whitelist, ' + r'because last price > .*%$', caplog) or log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] " r"is empty.*", caplog)) if pairlist['method'] == 'VolumePairList': From 40bdc93653bc97666145f914e5dcc2ea49b49c8a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Jul 2020 20:21:33 +0200 Subject: [PATCH 069/123] Add test for short_desc of priceFilter --- freqtrade/pairlist/PriceFilter.py | 6 +++--- tests/pairlist/test_pairlist.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 1afc60999..5ee1df078 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -20,9 +20,9 @@ class PriceFilter(IPairList): self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) self._min_price = pairlistconfig.get('min_price', 0) self._max_price = pairlistconfig.get('max_price', 0) - self._enabled = (self._low_price_ratio != 0) or \ - (self._min_price != 0) or \ - (self._max_price != 0) + self._enabled = ((self._low_price_ratio != 0) or + (self._min_price != 0) or + (self._max_price != 0)) @property def needstickers(self) -> bool: diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 09cbe9d5f..cf54a09ae 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -588,6 +588,36 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count +@pytest.mark.parametrize("pairlistconfig,expected", [ + ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, + "max_price": 1.0}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below " + "0.1% or below 0.00000010 or above 1.00000000.'}]" + ), + ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or below 0.00000010.'}]" + ), + ({"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or above 1.00010000.'}]" + ), + ({"method": "PriceFilter", "min_price": 0.00002000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]" + ), + ({"method": "PriceFilter"}, + "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]" + ), +]) +def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, expected): + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True) + ) + whitelist_conf['pairlists'] = [pairlistconfig] + + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + short_desc = str(freqtrade.pairlists.short_desc()) + assert short_desc == expected + + def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) From 588043af86d86cce4b90f65e34a8f49e98767db8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jul 2020 07:29:11 +0200 Subject: [PATCH 070/123] Fix documentation brackets, add delete trade hints --- docs/sql_cheatsheet.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 1d396b8ce..3d34d6fe2 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -100,8 +100,8 @@ UPDATE trades SET is_open=0, close_date=, close_rate=, - close_profit=close_rate/open_rate-1, - close_profit_abs = (amount * * (1 - fee_close) - (amount * open_rate * 1 - fee_open)), + close_profit = close_rate / open_rate - 1, + close_profit_abs = (amount * * (1 - fee_close) - (amount * (open_rate * 1 - fee_open))), sell_reason= WHERE id=; ``` @@ -111,24 +111,39 @@ WHERE id=; ```sql UPDATE trades SET is_open=0, - close_date='2017-12-20 03:08:45.103418', + close_date='2020-06-20 03:08:45.103418', close_rate=0.19638016, close_profit=0.0496, - close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open)) + close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))) sell_reason='force_sell' WHERE id=31; ``` -## Insert manually a new trade +## Manually insert a new trade ```sql INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) -VALUES ('bittrex', 'ETH/BTC', 1, 0.0025, 0.0025, , , , '') +VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, , , , '') ``` -##### Example: +### Insert trade example ```sql INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) -VALUES ('bittrex', 'ETH/BTC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000') +VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2020-06-28 12:44:24.000000') ``` + +## Remove trade from the database + +Maybe you'd like to remove a trade from the database, because something went wrong. + +```sql +DELETE FROM trades WHERE id = ; +``` + +```sql +DELETE FROM trades WHERE id = 31; +``` + +!!! Warning + This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the where clause. From ecbca3fab023f7e82661fdd4eb10bb71c2a9e036 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jul 2020 07:29:34 +0200 Subject: [PATCH 071/123] Add sqlite3 to dockerfile --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- docs/sql_cheatsheet.md | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b6333fb13..29808b383 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.8.3-slim-buster RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev \ + && apt-get -y install curl build-essential libssl-dev sqlite3 \ && apt-get clean \ && pip install --upgrade pip diff --git a/Dockerfile.armhf b/Dockerfile.armhf index d6e2aa3a1..45ed2dac9 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,7 +1,7 @@ FROM --platform=linux/arm/v7 python:3.7.7-slim-buster RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev libatlas3-base libgfortran5 \ + && apt-get -y install curl build-essential libssl-dev libatlas3-base libgfortran5 sqlite3 \ && apt-get clean \ && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 3d34d6fe2..56f76b5b7 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -13,6 +13,15 @@ Feel free to use a visual Database editor like SqliteBrowser if you feel more co sudo apt-get install sqlite3 ``` +### Using sqlite3 via docker-compose + +The freqtrade docker image does contain sqlite3, so you can edit the database without having to install anything on the host system. + +``` bash +docker-compose exec freqtrade /bin/bash +sqlite3 .sqlite +``` + ## Open the DB ```bash From f0a1a1720f861ba534be18d144b3ed08751975aa Mon Sep 17 00:00:00 2001 From: HumanBot Date: Sat, 11 Jul 2020 15:21:54 -0400 Subject: [PATCH 072/123] removed duplicate removed duplicate word using using --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index e7a79361a..09a1e76fe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -275,7 +275,7 @@ the static list of pairs) if we should buy. The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using -limit-orders, and create stoplosses using using market orders. It also allows to set the +limit-orders, and create stoplosses using market orders. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. If `stoploss_on_exchange` and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check and update the stoploss on exchange periodically. From 422825ea1b57494716697e550884fd00b8bac80f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 09:50:53 +0200 Subject: [PATCH 073/123] Add ohlcv_get_available_data to find available data --- freqtrade/data/history/idatahandler.py | 9 +++++++++ freqtrade/data/history/jsondatahandler.py | 14 +++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index d5d7c16db..e255710cb 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -13,6 +13,7 @@ from typing import List, Optional, Type from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import (clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe) from freqtrade.exchange import timeframe_to_seconds @@ -28,6 +29,14 @@ class IDataHandler(ABC): def __init__(self, datadir: Path) -> None: self._datadir = datadir + @abstractclassmethod + def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes: + """ + Returns a list of all pairs with ohlcv data available in this datadir + :param datadir: Directory to search for ohlcv files + :return: List of Pair + """ + @abstractclassmethod def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: """ diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 01320f129..79a848d07 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -8,7 +8,8 @@ from pandas import DataFrame, read_json, to_datetime from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS +from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, + ListPairsWithTimeframes) from freqtrade.data.converter import trades_dict_to_list from .idatahandler import IDataHandler, TradeList @@ -21,6 +22,17 @@ class JsonDataHandler(IDataHandler): _use_zip = False _columns = DEFAULT_DATAFRAME_COLUMNS + @classmethod + def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes: + """ + Returns a list of all pairs with ohlcv data available in this datadir + :param datadir: Directory to search for ohlcv files + :return: List of Pair + """ + _tmp = [re.search(r'^([a-zA-Z_]+)\-(\S+)(?=.json)', p.name) + for p in datadir.glob(f"*.{cls._get_file_extension()}")] + return [(match[1].replace('_', '/'), match[2]) for match in _tmp if len(match.groups()) > 1] + @classmethod def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: """ From d4fc52d2d5b6e9bd6a5fe06ce902ea6460375db4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 09:56:46 +0200 Subject: [PATCH 074/123] Add tests for ohlcv_get_available_data --- freqtrade/data/history/jsondatahandler.py | 5 +++-- tests/data/test_history.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 79a848d07..4ef35cf55 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -29,9 +29,10 @@ class JsonDataHandler(IDataHandler): :param datadir: Directory to search for ohlcv files :return: List of Pair """ - _tmp = [re.search(r'^([a-zA-Z_]+)\-(\S+)(?=.json)', p.name) + _tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.json)', p.name) for p in datadir.glob(f"*.{cls._get_file_extension()}")] - return [(match[1].replace('_', '/'), match[2]) for match in _tmp if len(match.groups()) > 1] + return [(match[1].replace('_', '/'), match[2]) for match in _tmp + if match and len(match.groups()) > 1] @classmethod def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c2eb2d715..d84c212b1 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -631,6 +631,20 @@ def test_jsondatahandler_ohlcv_get_pairs(testdatadir): assert set(pairs) == {'UNITTEST/BTC'} +def test_jsondatahandler_ohlcv_get_available_data(testdatadir): + paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir) + # Convert to set to avoid failures due to sorting + assert set(paircombs) == {('UNITTEST/BTC', '5m'), ('ETH/BTC', '5m'), ('XLM/BTC', '5m'), + ('TRX/BTC', '5m'), ('LTC/BTC', '5m'), ('XMR/BTC', '5m'), + ('ZEC/BTC', '5m'), ('UNITTEST/BTC', '1m'), ('ADA/BTC', '5m'), + ('ETC/BTC', '5m'), ('NXT/BTC', '5m'), ('DASH/BTC', '5m'), + ('XRP/ETH', '1m'), ('XRP/ETH', '5m'), ('UNITTEST/BTC', '30m'), + ('UNITTEST/BTC', '8m')} + + paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir) + assert set(paircombs) == {('UNITTEST/BTC', '8m')} + + def test_jsondatahandler_trades_get_pairs(testdatadir): pairs = JsonGzDataHandler.trades_get_pairs(testdatadir) # Convert to set to avoid failures due to sorting From 02afde857d204f91d9170bb9bf22439b07e8c2cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 09:57:00 +0200 Subject: [PATCH 075/123] Add list-data command --- freqtrade/commands/__init__.py | 3 ++- freqtrade/commands/arguments.py | 15 +++++++++++++-- freqtrade/commands/data_commands.py | 23 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 2d0c7733c..4ce3eb421 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -9,7 +9,8 @@ Note: Be careful with file-scoped imports in these subfiles. from freqtrade.commands.arguments import Arguments from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.data_commands import (start_convert_data, - start_download_data) + start_download_data, + start_list_data) from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, start_new_strategy) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 72f2a02f0..a49d917a5 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -54,6 +54,8 @@ ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] +ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv"] + ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] @@ -78,7 +80,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop "print_json", "hyperopt_show_no_header"] NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", - "list-markets", "list-pairs", "list-strategies", + "list-markets", "list-pairs", "list-strategies", "list-data", "list-hyperopts", "hyperopt-list", "hyperopt-show", "plot-dataframe", "plot-profit", "show-trades"] @@ -159,7 +161,7 @@ class Arguments: self._build_args(optionlist=['version'], parser=self.parser) from freqtrade.commands import (start_create_userdir, start_convert_data, - start_download_data, + start_download_data, start_list_data, start_hyperopt_list, start_hyperopt_show, start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, @@ -233,6 +235,15 @@ class Arguments: convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False)) self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd) + # Add list-data subcommand + list_data_cmd = subparsers.add_parser( + 'list-data', + help='List downloaded data.', + parents=[_common_parser], + ) + list_data_cmd.set_defaults(func=start_list_data) + self._build_args(optionlist=ARGS_LIST_DATA, parser=list_data_cmd) + # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.', parents=[_common_parser, _strategy_parser]) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index fc3a49f1d..4a37bdc08 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -1,5 +1,6 @@ import logging import sys +from collections import defaultdict from typing import Any, Dict, List import arrow @@ -11,6 +12,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) from freqtrade.exceptions import OperationalException +from freqtrade.exchange import timeframe_to_minutes from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -88,3 +90,24 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None: convert_trades_format(config, convert_from=args['format_from'], convert_to=args['format_to'], erase=args['erase']) + + +def start_list_data(args: Dict[str, Any]) -> None: + """ + List available backtest data + """ + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + from freqtrade.data.history.idatahandler import get_datahandlerclass + from tabulate import tabulate + dhc = get_datahandlerclass(config['dataformat_ohlcv']) + paircombs = dhc.ohlcv_get_available_data(config['datadir']) + + print(f"Found {len(paircombs)} pair / timeframe combinations.") + groupedpair = defaultdict(list) + for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))): + groupedpair[pair].append(timeframe) + + print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()], + headers=("pairs", "timeframe"))) From 5bb81abce26b4e1bc44f7bc28cc0cf04397fea57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 10:01:37 +0200 Subject: [PATCH 076/123] Add test for start_list_data --- tests/commands/test_commands.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 46350beff..9c741e102 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -6,12 +6,12 @@ import pytest from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, start_hyperopt_list, - start_hyperopt_show, start_list_exchanges, - start_list_hyperopts, start_list_markets, - start_list_strategies, start_list_timeframes, - start_new_hyperopt, start_new_strategy, - start_show_trades, start_test_pairlist, - start_trading) + start_hyperopt_show, start_list_data, + start_list_exchanges, start_list_hyperopts, + start_list_markets, start_list_strategies, + start_list_timeframes, start_new_hyperopt, + start_new_strategy, start_show_trades, + start_test_pairlist, start_trading) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -1043,6 +1043,23 @@ def test_convert_data_trades(mocker, testdatadir): assert trades_mock.call_args[1]['erase'] is False +def test_start_list_data(testdatadir, capsys): + args = [ + "list-data", + "--data-format-ohlcv", + "json", + "--datadir", + str(testdatadir), + ] + pargs = get_args(args) + pargs['config'] = None + start_list_data(pargs) + captured = capsys.readouterr() + assert "Found 16 pair / timeframe combinations." in captured.out + assert "\npairs timeframe\n" in captured.out + assert "\nUNITTEST/BTC 1m, 5m, 8m, 30m\n" in captured.out + + @pytest.mark.usefixtures("init_persistence") def test_show_trades(mocker, fee, capsys, caplog): mocker.patch("freqtrade.persistence.init") From 33c3990972495f1754744b98775f63e99a85aa0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 10:05:47 +0200 Subject: [PATCH 077/123] Add documentation for list-data command --- docs/data-download.md | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/data-download.md b/docs/data-download.md index 3fb775e69..7fbad0b6c 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -158,6 +158,55 @@ It'll also remove original jsongz data files (`--erase` parameter). freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase ``` +### Subcommand list-data + +You can get a list of downloaded data using the `list-data` subcommand. + +``` +usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [--exchange EXCHANGE] + [--data-format-ohlcv {json,jsongz}] + +optional arguments: + -h, --help show this help message and exit + --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no + config is provided. + --data-format-ohlcv {json,jsongz} + Storage format for downloaded candle (OHLCV) data. + (default: `json`). + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +``` + +#### Example list-data + +```bash +> freqtrade list-data --userdir ~/.freqtrade/user_data/ + +Found 33 pair / timeframe combinations. +pairs timeframe +---------- ----------------------------------------- +ADA/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d +ADA/ETH 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d +ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d +ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h +``` + ### Pairs file In alternative to the whitelist from `config.json`, a `pairs.json` file can be used. From b035d9e2671b57cbb09ab340d432349b934182b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 10:23:09 +0200 Subject: [PATCH 078/123] Update return type comment --- freqtrade/commands/data_commands.py | 5 +++-- freqtrade/data/history/idatahandler.py | 2 +- freqtrade/data/history/jsondatahandler.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 4a37bdc08..d3f70b9ec 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -99,9 +99,10 @@ def start_list_data(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - from freqtrade.data.history.idatahandler import get_datahandlerclass + from freqtrade.data.history.idatahandler import get_datahandler from tabulate import tabulate - dhc = get_datahandlerclass(config['dataformat_ohlcv']) + dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv']) + paircombs = dhc.ohlcv_get_available_data(config['datadir']) print(f"Found {len(paircombs)} pair / timeframe combinations.") diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index e255710cb..96d288e01 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -34,7 +34,7 @@ class IDataHandler(ABC): """ Returns a list of all pairs with ohlcv data available in this datadir :param datadir: Directory to search for ohlcv files - :return: List of Pair + :return: List of Tuples of (pair, timeframe) """ @abstractclassmethod diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 4ef35cf55..2e7c0f773 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -27,7 +27,7 @@ class JsonDataHandler(IDataHandler): """ Returns a list of all pairs with ohlcv data available in this datadir :param datadir: Directory to search for ohlcv files - :return: List of Pair + :return: List of Tuples of (pair, timeframe) """ _tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.json)', p.name) for p in datadir.glob(f"*.{cls._get_file_extension()}")] From ed2e35ba5d01fda20ec867d1b28ca020c78752f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 12:36:16 +0200 Subject: [PATCH 079/123] Update docs/sql_cheatsheet.md Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/sql_cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 56f76b5b7..f4cb473ff 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -155,4 +155,4 @@ DELETE FROM trades WHERE id = 31; ``` !!! Warning - This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the where clause. + This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause. From 79af6180bddee3a442c730b24d47f4496980b402 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:00:50 +0000 Subject: [PATCH 080/123] Bump pytest-mock from 3.1.1 to 3.2.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.1.1 to 3.2.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.1.1...v3.2.0) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ed4f8f713..249aa9089 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.782 pytest==5.4.3 pytest-asyncio==0.14.0 pytest-cov==2.10.0 -pytest-mock==3.1.1 +pytest-mock==3.2.0 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From 58eb26d73a44d782a56ae5a64bf5477dbde3f98e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:01:14 +0000 Subject: [PATCH 081/123] Bump pycoingecko from 1.2.0 to 1.3.0 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/1.2.0...1.3.0) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 2f225c93c..34340612d 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -11,7 +11,7 @@ wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.18 tabulate==0.8.7 -pycoingecko==1.2.0 +pycoingecko==1.3.0 jinja2==2.11.2 # find first, C search in arrays From d1e4e463ae7d2c9f7361e68581a1617652869ba6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:01:58 +0000 Subject: [PATCH 082/123] Bump ccxt from 1.30.64 to 1.30.93 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.64 to 1.30.93. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.64...1.30.93) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 2f225c93c..fc38f4ee6 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.30.64 +ccxt==1.30.93 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 From 50573bd3978464d012f5c76408a4699b19c86fc0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:02:07 +0000 Subject: [PATCH 083/123] Bump coveralls from 2.0.0 to 2.1.1 Bumps [coveralls](https://github.com/coveralls-clients/coveralls-python) from 2.0.0 to 2.1.1. - [Release notes](https://github.com/coveralls-clients/coveralls-python/releases) - [Changelog](https://github.com/coveralls-clients/coveralls-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/coveralls-clients/coveralls-python/compare/2.0.0...2.1.1) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ed4f8f713..2b9c4c8f9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==2.0.0 +coveralls==2.1.1 flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 From 0b36693accbf5b0435ebf5daeea0485737d443dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jul 2020 19:48:21 +0200 Subject: [PATCH 084/123] Add filter for stoploss_on_exchange_limit_ratio to constants --- freqtrade/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 8a5332475..1dadc6e16 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -156,7 +156,9 @@ CONF_SCHEMA = { 'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'}, - 'stoploss_on_exchange_interval': {'type': 'number'} + 'stoploss_on_exchange_interval': {'type': 'number'}, + 'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0, + 'maximum': 1.0} }, 'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] }, From 01f325a9e4cd65a0bb117f031f25d0da593002c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jul 2020 21:15:33 +0200 Subject: [PATCH 085/123] Send timeframe min and ms in show_config response --- freqtrade/rpc/rpc.py | 4 ++++ tests/rpc/test_rpc_apiserver.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e0eb12d23..c73fcbf54 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -12,6 +12,8 @@ import arrow from numpy import NAN, mean from freqtrade.exceptions import ExchangeError, PricingError + +from freqtrade.exchange import timeframe_to_msecs, timeframe_to_minutes from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -103,6 +105,8 @@ class RPC: 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), 'ticker_interval': config['timeframe'], # DEPRECATED 'timeframe': config['timeframe'], + 'timeframe_ms': timeframe_to_msecs(config['timeframe']), + 'timeframe_min': timeframe_to_minutes(config['timeframe']), 'exchange': config['exchange']['name'], 'strategy': config['strategy'], 'forcebuy_enabled': config.get('forcebuy_enable', False), diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 45aa57588..355b63f48 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -326,6 +326,8 @@ def test_api_show_config(botclient, mocker): assert rc.json['exchange'] == 'bittrex' assert rc.json['ticker_interval'] == '5m' assert rc.json['timeframe'] == '5m' + assert rc.json['timeframe_ms'] == 300000 + assert rc.json['timeframe_min'] == 5 assert rc.json['state'] == 'running' assert not rc.json['trailing_stop'] assert 'bid_strategy' in rc.json From 62c55b18631398c7447b5eed355fa495f0a299af Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jul 2020 06:55:34 +0200 Subject: [PATCH 086/123] Enhance formatting, Add pair filter --- docs/data-download.md | 5 ++++- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/data_commands.py | 6 +++++- tests/commands/test_commands.py | 21 +++++++++++++++++++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 7fbad0b6c..a2bbec837 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -166,6 +166,7 @@ You can get a list of downloaded data using the `list-data` subcommand. usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [--data-format-ohlcv {json,jsongz}] + [-p PAIRS [PAIRS ...]] optional arguments: -h, --help show this help message and exit @@ -174,6 +175,9 @@ optional arguments: --data-format-ohlcv {json,jsongz} Storage format for downloaded candle (OHLCV) data. (default: `json`). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Show profits for only these pairs. Pairs are space- + separated. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). @@ -190,7 +194,6 @@ Common arguments: Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. - ``` #### Example list-data diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a49d917a5..e6f6f8167 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -54,7 +54,7 @@ ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] -ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv"] +ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index d3f70b9ec..13b796a1e 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -105,10 +105,14 @@ def start_list_data(args: Dict[str, Any]) -> None: paircombs = dhc.ohlcv_get_available_data(config['datadir']) + if args['pairs']: + paircombs = [comb for comb in paircombs if comb[0] in args['pairs']] + print(f"Found {len(paircombs)} pair / timeframe combinations.") groupedpair = defaultdict(list) for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))): groupedpair[pair].append(timeframe) print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()], - headers=("pairs", "timeframe"))) + headers=("Pair", "Timeframe"), + tablefmt='psql', stralign='right')) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 9c741e102..ffced956d 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1056,8 +1056,25 @@ def test_start_list_data(testdatadir, capsys): start_list_data(pargs) captured = capsys.readouterr() assert "Found 16 pair / timeframe combinations." in captured.out - assert "\npairs timeframe\n" in captured.out - assert "\nUNITTEST/BTC 1m, 5m, 8m, 30m\n" in captured.out + assert "\n| Pair | Timeframe |\n" in captured.out + assert "\n| UNITTEST/BTC | 1m, 5m, 8m, 30m |\n" in captured.out + + args = [ + "list-data", + "--data-format-ohlcv", + "json", + "--pairs", "XRP/ETH", + "--datadir", + str(testdatadir), + ] + pargs = get_args(args) + pargs['config'] = None + start_list_data(pargs) + captured = capsys.readouterr() + assert "Found 2 pair / timeframe combinations." in captured.out + assert "\n| Pair | Timeframe |\n" in captured.out + assert "UNITTEST/BTC" not in captured.out + assert "\n| XRP/ETH | 1m, 5m |\n" in captured.out @pytest.mark.usefixtures("init_persistence") From ae55d54967b22c0b9d41d533194ebb96a3b63d82 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2020 06:33:57 +0000 Subject: [PATCH 087/123] Bump python from 3.8.3-slim-buster to 3.8.4-slim-buster Bumps python from 3.8.3-slim-buster to 3.8.4-slim-buster. Signed-off-by: dependabot-preview[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 29808b383..f27167cc5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.3-slim-buster +FROM python:3.8.4-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From 0228b63418bb35d680c9c4bfe8e0c2b492cf3b6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jul 2020 16:42:47 +0200 Subject: [PATCH 088/123] Don't print empty table --- freqtrade/commands/data_commands.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 13b796a1e..aa0b826b5 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -113,6 +113,7 @@ def start_list_data(args: Dict[str, Any]) -> None: for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))): groupedpair[pair].append(timeframe) - print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()], - headers=("Pair", "Timeframe"), - tablefmt='psql', stralign='right')) + if groupedpair: + print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()], + headers=("Pair", "Timeframe"), + tablefmt='psql', stralign='right')) From 1051ab917ad999d09eb2964940a5e3f5381dc22c Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 15 Jul 2020 12:40:54 +0100 Subject: [PATCH 089/123] Replaced logging with OperationalException when AgeFilter given invalid parameters --- freqtrade/pairlist/AgeFilter.py | 12 +++++------- tests/pairlist/test_pairlist.py | 15 +++++++-------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 101f19cbe..7b6b126c3 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -5,6 +5,7 @@ import logging import arrow from typing import Any, Dict +from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.pairlist.IPairList import IPairList @@ -25,14 +26,11 @@ class AgeFilter(IPairList): self._min_days_listed = pairlistconfig.get('min_days_listed', 10) if self._min_days_listed < 1: - self.log_on_refresh(logger.info, "min_days_listed must be >= 1, " - "ignoring filter") + raise OperationalException("AgeFilter requires min_days_listed must be >= 1") if self._min_days_listed > exchange.ohlcv_candle_limit: - self._min_days_listed = min(self._min_days_listed, exchange.ohlcv_candle_limit) - self.log_on_refresh(logger.info, "min_days_listed exceeds " - "exchange max request size " - f"({exchange.ohlcv_candle_limit}), using " - f"min_days_listed={self._min_days_listed}") + raise OperationalException("AgeFilter requires min_days_listed must not exceed " + "exchange max request size " + f"({exchange.ohlcv_candle_limit})") self._enabled = self._min_days_listed >= 1 @property diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index cf54a09ae..e23102162 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -543,10 +543,9 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick get_tickers=tickers ) - get_patched_freqtradebot(mocker, default_conf) - - assert log_has_re(r'min_days_listed must be >= 1, ' - r'ignoring filter', caplog) + with pytest.raises(OperationalException, + match=r'AgeFilter requires min_days_listed must be >= 1'): + get_patched_freqtradebot(mocker, default_conf) def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog): @@ -559,10 +558,10 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick get_tickers=tickers ) - get_patched_freqtradebot(mocker, default_conf) - - assert log_has_re(r'^min_days_listed exceeds ' - r'exchange max request size', caplog) + with pytest.raises(OperationalException, + match=r'AgeFilter requires min_days_listed must not exceed ' + r'exchange max request size \([0-9]+\)'): + get_patched_freqtradebot(mocker, default_conf) def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): From c1191400a4f5c705b394e209c30810dd0d1e669f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 19:20:07 +0200 Subject: [PATCH 090/123] Allow 0 fee value by correctly checking for None --- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e5014dd5a..214c92e0e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -101,7 +101,7 @@ class Backtesting: if len(self.pairlists.whitelist) == 0: raise OperationalException("No pair in whitelist.") - if config.get('fee'): + if config.get('fee', None) is not None: self.fee = config['fee'] else: self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 67da38648..caa40fe84 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -308,6 +308,11 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None: assert backtesting.fee == 0.1234 assert fee_mock.call_count == 0 + default_conf['fee'] = 0.0 + backtesting = Backtesting(default_conf) + assert backtesting.fee == 0.0 + assert fee_mock.call_count == 0 + def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) From 5cebc9f39df700205711776c23949e7fc57ee7eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 19:28:40 +0200 Subject: [PATCH 091/123] Move stoploss_on_exchange_limit_ratio to configuration schema --- freqtrade/exchange/exchange.py | 7 ------- tests/exchange/test_exchange.py | 22 ---------------------- tests/test_configuration.py | 8 ++++++++ 3 files changed, 8 insertions(+), 29 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d91a33926..fd9c83d51 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -389,13 +389,6 @@ class Exchange: f'On exchange stoploss is not supported for {self.name}.' ) - # Limit price threshold: As limit price should always be below stop-price - # Used for limit stoplosses on exchange - limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - if limit_price_pct >= 1.0 or limit_price_pct <= 0.0: - raise OperationalException( - "stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0") - def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: """ Checks if order time in force configured in strategy/config are supported diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1101f3e74..60c4847f6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -746,28 +746,6 @@ def test_validate_order_types(default_conf, mocker): match=r'On exchange stoploss is not supported for .*'): Exchange(default_conf) - default_conf['order_types'] = { - 'buy': 'limit', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False, - 'stoploss_on_exchange_limit_ratio': 1.05 - } - with pytest.raises(OperationalException, - match=r'stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0'): - Exchange(default_conf) - - default_conf['order_types'] = { - 'buy': 'limit', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False, - 'stoploss_on_exchange_limit_ratio': -0.1 - } - with pytest.raises(OperationalException, - match=r'stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0'): - Exchange(default_conf) - def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index cccc87670..ca5d6eadc 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -871,6 +871,14 @@ def test_load_config_default_exchange_name(all_conf) -> None: validate_config_schema(all_conf) +def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None: + all_conf['order_types']['stoploss_on_exchange_limit_ratio'] = 1.15 + + with pytest.raises(ValidationError, + match=r"1.15 is greater than the maximum"): + validate_config_schema(all_conf) + + @pytest.mark.parametrize("keys", [("exchange", "sandbox", False), ("exchange", "key", ""), ("exchange", "secret", ""), From eaf2b53d591001674a9a9ceb24c9e2cefae00387 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 05:10:46 +0000 Subject: [PATCH 092/123] Update Dependabot config file --- .dependabot/config.yml | 17 ----------------- .github/dependabot.yml | 13 +++++++++++++ 2 files changed, 13 insertions(+), 17 deletions(-) delete mode 100644 .dependabot/config.yml create mode 100644 .github/dependabot.yml diff --git a/.dependabot/config.yml b/.dependabot/config.yml deleted file mode 100644 index 66b91e99f..000000000 --- a/.dependabot/config.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 1 - -update_configs: - - package_manager: "python" - directory: "/" - update_schedule: "weekly" - allowed_updates: - - match: - update_type: "all" - target_branch: "develop" - - - package_manager: "docker" - directory: "/" - update_schedule: "daily" - allowed_updates: - - match: - update_type: "all" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..44ff606b4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: +- package-ecosystem: docker + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + target-branch: develop From cd7ba99528576e2961d4e7f38bc47e7c3216c658 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 07:23:16 +0000 Subject: [PATCH 093/123] Bump ccxt from 1.30.93 to 1.31.37 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.93 to 1.31.37. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.93...1.31.37) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 1cfc44ab4..d5c5fd832 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.30.93 +ccxt==1.31.37 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 From 49395601e98cc0ba8824f96aab92ed7aebb430d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jul 2020 10:02:06 +0200 Subject: [PATCH 094/123] Improve informative pair sample --- docs/strategy-customization.md | 47 +++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 50fec79dc..98c71b4b2 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -392,9 +392,9 @@ Imagine you've developed a strategy that trades the `5m` timeframe using signals The strategy might look something like this: -*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day ATR to buy and sell.* +*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.* -Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day ATR. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least! +Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least! Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use. @@ -410,18 +410,49 @@ class SampleStrategy(IStrategy): def informative_pairs(self): - # get access to all pairs available in whitelist. + # get access to all pairs available in whitelist. pairs = self.dp.current_whitelist() # Assign tf to each pair so they can be downloaded and cached for strategy. informative_pairs = [(pair, '1d') for pair in pairs] return informative_pairs - def populate_indicators(self, dataframe, metadata): + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + inf_tf = '1d' # Get the informative pair informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d') - # Get the 14 day ATR. - atr = ta.ATR(informative, timeperiod=14) + # Get the 14 day rsi + informative['rsi'] = ta.RSI(informative, timeperiod=14) + + # Rename columns to be unique + informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] + # Assuming inf_tf = '1d' - then the columns will now be: + # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + + # Combine the 2 dataframes + # all indicators on the informative sample MUST be calculated before this point + dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left') + # FFill to have the 1d value available in every row throughout the day. + # Without this, comparisons would only work once per day. + dataframe = dataframe.ffill() + # Calculate rsi of the original dataframe (5m timeframe) + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + # Do other stuff + # ... + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + dataframe.loc[ + ( + (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + (dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30 + (dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting) + ), + 'buy'] = 1 + ``` #### *get_pair_dataframe(pair, timeframe)* @@ -460,7 +491,7 @@ if self.dp: !!! Warning "Warning in hyperopt" This option cannot currently be used during hyperopt. - + #### *orderbook(pair, maximum)* ``` python @@ -493,6 +524,7 @@ if self.dp: data returned from the exchange and add appropriate error handling / defaults. *** + ### Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. @@ -516,6 +548,7 @@ if self.wallets: - `get_total(asset)` - total available balance - sum of the 2 above *** + ### Additional data (Trades) A history of Trades can be retrieved in the strategy by querying the database. From 3271c773a76d93b361a8816d6e3ee39e20468186 Mon Sep 17 00:00:00 2001 From: Alex Pham <20041501+thopd88@users.noreply.github.com> Date: Sun, 19 Jul 2020 21:30:55 +0700 Subject: [PATCH 095/123] Fix SQL syntax error when compare pair strings First happens in Postgres --- freqtrade/rpc/rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c73fcbf54..69faff533 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -523,7 +523,7 @@ class RPC: # check if valid pair # check if pair already has an open pair - trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() if trade: raise RPCException(f'position for {pair} already open - id: {trade.id}') @@ -532,7 +532,7 @@ class RPC: # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price): - trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade else: return None From dd3a2675b53c30e9f3ff23e9799d2cfd41352fb0 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Sun, 19 Jul 2020 22:02:53 +0700 Subject: [PATCH 096/123] Add telegram trades command to list recent trades --- freqtrade/rpc/telegram.py | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 13cc1afaf..72dbd2ea7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -92,6 +92,7 @@ class Telegram(RPC): CommandHandler('stop', self._stop), CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), + CommandHandler('trades', self._trades), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -496,6 +497,48 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _trades(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /trades + Returns last n recent trades. + :param bot: telegram bot + :param update: message update + :return: None + """ + stake_cur = self._config['stake_currency'] + fiat_disp_cur = self._config.get('fiat_display_currency', '') + try: + nrecent = int(context.args[0]) + except (TypeError, ValueError, IndexError): + nrecent = 10 + try: + trades = self._rpc_trade_history( + nrecent + ) + trades_tab = tabulate( + [[trade['open_date'], + trade['pair'], + f"{trade['open_rate']}", + f"{trade['stake_amount']}", + trade['close_date'], + f"{trade['close_rate']}", + f"{trade['close_profit_abs']}"] for trade in trades['trades']], + headers=[ + 'Open Date', + 'Pair', + 'Open rate', + 'Stake Amount', + 'Close date', + 'Close rate', + 'Profit', + ], + tablefmt='simple') + message = f'{nrecent} recent trades:\n
{trades_tab}
' + self._send_msg(message, parse_mode=ParseMode.HTML) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _performance(self, update: Update, context: CallbackContext) -> None: """ From 08fdd7d86331c5a2c55c69c130350a7b21a104e3 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Sun, 19 Jul 2020 22:10:59 +0700 Subject: [PATCH 097/123] Add telegram /delete command to delete tradeid code inspired from _rpc_forcesell --- freqtrade/rpc/rpc.py | 22 ++++++++++++++++++++++ freqtrade/rpc/telegram.py | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c73fcbf54..2a3feea25 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -537,6 +537,28 @@ class RPC: else: return None + def _rpc_delete(self, trade_id: str) -> Dict[str, str]: + """ + Handler for delete . + Delete the given trade + """ + def _exec_delete(trade: Trade) -> None: + Trade.session.delete(trade) + Trade.session.flush() + + with self._freqtrade._sell_lock: + trade = Trade.get_trades( + trade_filter=[Trade.id == trade_id, ] + ).first() + if not trade: + logger.warning('delete: Invalid argument received') + raise RPCException('invalid argument') + + _exec_delete(trade) + Trade.session.flush() + self._freqtrade.wallets.update() + return {'result': f'Deleted trade {trade_id}.'} + def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 72dbd2ea7..474376938 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -93,6 +93,7 @@ class Telegram(RPC): CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), CommandHandler('trades', self._trades), + CommandHandler('delete', self._delete), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -497,6 +498,24 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _delete(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /delete . + Delete the given trade + :param bot: telegram bot + :param update: message update + :return: None + """ + + trade_id = context.args[0] if len(context.args) > 0 else None + try: + msg = self._rpc_delete(trade_id) + self._send_msg('Delete Result: `{result}`'.format(**msg)) + + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: """ From 37a9edfa35a8321ca38d353ab081a17263ea8f68 Mon Sep 17 00:00:00 2001 From: Pan Long Date: Mon, 20 Jul 2020 00:37:06 +0800 Subject: [PATCH 098/123] Correct a typo in stop loss doc. --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index ed00c1e33..bf7270dff 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -84,7 +84,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai ``` python trailing_stop_positive_offset = 0.011 - trailing_only_offset_is_reached = true + trailing_only_offset_is_reached = True ``` Simplified example: From 28f4a1101e58e38df1fddc22ff573e852a80de33 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Mon, 20 Jul 2020 10:54:17 +0700 Subject: [PATCH 099/123] Revert "Add telegram /delete command to delete tradeid" This reverts commit 08fdd7d86331c5a2c55c69c130350a7b21a104e3. --- freqtrade/rpc/rpc.py | 22 ---------------------- freqtrade/rpc/telegram.py | 19 ------------------- 2 files changed, 41 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2a3feea25..c73fcbf54 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -537,28 +537,6 @@ class RPC: else: return None - def _rpc_delete(self, trade_id: str) -> Dict[str, str]: - """ - Handler for delete . - Delete the given trade - """ - def _exec_delete(trade: Trade) -> None: - Trade.session.delete(trade) - Trade.session.flush() - - with self._freqtrade._sell_lock: - trade = Trade.get_trades( - trade_filter=[Trade.id == trade_id, ] - ).first() - if not trade: - logger.warning('delete: Invalid argument received') - raise RPCException('invalid argument') - - _exec_delete(trade) - Trade.session.flush() - self._freqtrade.wallets.update() - return {'result': f'Deleted trade {trade_id}.'} - def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 474376938..72dbd2ea7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -93,7 +93,6 @@ class Telegram(RPC): CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), CommandHandler('trades', self._trades), - CommandHandler('delete', self._delete), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -498,24 +497,6 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e)) - @authorized_only - def _delete(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /delete . - Delete the given trade - :param bot: telegram bot - :param update: message update - :return: None - """ - - trade_id = context.args[0] if len(context.args) > 0 else None - try: - msg = self._rpc_delete(trade_id) - self._send_msg('Delete Result: `{result}`'.format(**msg)) - - except RPCException as e: - self._send_msg(str(e)) - @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: """ From 811028ae926c47ee60e1904da2a3468e2c753bdd Mon Sep 17 00:00:00 2001 From: gautier pialat Date: Mon, 20 Jul 2020 07:17:34 +0200 Subject: [PATCH 100/123] missing coma in sql request --- docs/sql_cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index f4cb473ff..748b16928 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -123,7 +123,7 @@ SET is_open=0, close_date='2020-06-20 03:08:45.103418', close_rate=0.19638016, close_profit=0.0496, - close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))) + close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))), sell_reason='force_sell' WHERE id=31; ``` From 4c97527b041234d6865d24fb490963b1c17b88ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jul 2020 19:11:15 +0200 Subject: [PATCH 101/123] FIx failing test --- freqtrade/rpc/telegram.py | 2 -- tests/rpc/test_rpc_telegram.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 72dbd2ea7..343b26072 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -506,8 +506,6 @@ class Telegram(RPC): :param update: message update :return: None """ - stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config.get('fiat_display_currency', '') try: nrecent = int(context.args[0]) except (TypeError, ValueError, IndexError): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0a4352f5b..1ea211584 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -60,7 +60,7 @@ def test__init__(default_conf, mocker) -> None: assert telegram._config == default_conf -def test_init(default_conf, mocker, caplog) -> None: +def test_telegram_init(default_conf, mocker, caplog) -> None: start_polling = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) @@ -72,7 +72,7 @@ def test_init(default_conf, mocker, caplog) -> None: assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " - "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " + "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], " "['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], " "['edge'], ['help'], ['version']]") From 21dcef113431e4b42be7acc33d6353f1db33d96e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jul 2020 19:50:29 +0200 Subject: [PATCH 102/123] Add trade_id to webhooks allowing for easier corelation of different messages --- docs/webhook-config.md | 4 ++++ freqtrade/freqtradebot.py | 4 ++++ tests/rpc/test_rpc_telegram.py | 3 +++ tests/test_freqtradebot.py | 4 ++++ 4 files changed, 15 insertions(+) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 70a41dd46..db6d4d1ef 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -47,6 +47,7 @@ Different payloads can be configured for different events. Not all fields are ne The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `limit` @@ -63,6 +64,7 @@ Possible parameters are: The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `limit` @@ -79,6 +81,7 @@ Possible parameters are: The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `gain` @@ -100,6 +103,7 @@ Possible parameters are: The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `gain` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 38afe3230..a6d96ef77 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -598,6 +598,7 @@ class FreqtradeBot: Sends rpc notification when a buy occured. """ msg = { + 'trade_id': trade.id, 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -621,6 +622,7 @@ class FreqtradeBot: current_rate = self.get_buy_rate(trade.pair, False) msg = { + 'trade_id': trade.id, 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -1149,6 +1151,7 @@ class FreqtradeBot: msg = { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, @@ -1191,6 +1194,7 @@ class FreqtradeBot: msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0a4352f5b..631817624 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -725,6 +725,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'profit', @@ -784,6 +785,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', @@ -832,6 +834,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None msg = rpc_mock.call_args_list[0][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ada0d87fd..c7089abfe 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2572,6 +2572,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N assert rpc_mock.call_count == 1 last_msg = rpc_mock.call_args_list[-1][0][0] assert { + 'trade_id': 1, 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', @@ -2622,6 +2623,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', @@ -2678,6 +2680,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', @@ -2883,6 +2886,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'profit', From 7d6708fc6a3fa56381f72882ab09a330e207f792 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jul 2020 20:03:03 +0200 Subject: [PATCH 103/123] Reduce severity of hyperopt "does not provide" messages closes #3371 --- freqtrade/resolvers/hyperopt_resolver.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 633363134..abbfee6ed 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -42,14 +42,14 @@ class HyperOptResolver(IResolver): extra_dir=config.get('hyperopt_path')) if not hasattr(hyperopt, 'populate_indicators'): - logger.warning("Hyperopt class does not provide populate_indicators() method. " - "Using populate_indicators from the strategy.") + logger.info("Hyperopt class does not provide populate_indicators() method. " + "Using populate_indicators from the strategy.") if not hasattr(hyperopt, 'populate_buy_trend'): - logger.warning("Hyperopt class does not provide populate_buy_trend() method. " - "Using populate_buy_trend from the strategy.") + logger.info("Hyperopt class does not provide populate_buy_trend() method. " + "Using populate_buy_trend from the strategy.") if not hasattr(hyperopt, 'populate_sell_trend'): - logger.warning("Hyperopt class does not provide populate_sell_trend() method. " - "Using populate_sell_trend from the strategy.") + logger.info("Hyperopt class does not provide populate_sell_trend() method. " + "Using populate_sell_trend from the strategy.") return hyperopt From 939f91734f2723a72f6545feef2edfef8beaa9c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Jul 2020 20:34:19 +0200 Subject: [PATCH 104/123] Test confirming 0 division ... --- tests/conftest.py | 52 +++++++++++++++++++++++++++++++-- tests/pairlist/test_pairlist.py | 9 ++++-- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 43dc8ca78..e2bdf7da5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -661,7 +661,8 @@ def shitcoinmarkets(markets): Fixture with shitcoin markets - used to test filters in pairlists """ shitmarkets = deepcopy(markets) - shitmarkets.update({'HOT/BTC': { + shitmarkets.update({ + 'HOT/BTC': { 'id': 'HOTBTC', 'symbol': 'HOT/BTC', 'base': 'HOT', @@ -766,7 +767,32 @@ def shitcoinmarkets(markets): "spot": True, "future": False, "active": True - }, + }, + 'ADADOUBLE/USDT': { + "percentage": True, + "tierBased": False, + "taker": 0.001, + "maker": 0.001, + "precision": { + "base": 8, + "quote": 8, + "amount": 2, + "price": 4 + }, + "limits": { + }, + "id": "ADADOUBLEUSDT", + "symbol": "ADADOUBLE/USDT", + "base": "ADADOUBLE", + "quote": "USDT", + "baseId": "ADADOUBLE", + "quoteId": "USDT", + "info": {}, + "type": "spot", + "spot": True, + "future": False, + "active": True + }, }) return shitmarkets @@ -1388,6 +1414,28 @@ def tickers(): "quoteVolume": 0.0, "info": {} }, + "ADADOUBLE/USDT": { + "symbol": "ADADOUBLE/USDT", + "timestamp": 1580469388244, + "datetime": "2020-01-31T11:16:28.244Z", + "high": None, + "low": None, + "bid": 0.7305, + "bidVolume": None, + "ask": 0.7342, + "askVolume": None, + "vwap": None, + "open": None, + "close": None, + "last": 0, + "previousClose": None, + "change": None, + "percentage": 2.628, + "average": None, + "baseVolume": 0.0, + "quoteVolume": 0.0, + "info": {} + }, }) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index e23102162..efe4a784b 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -235,7 +235,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), + "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # No pair for ETH, VolumePairList ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "ETH", []), @@ -303,11 +303,11 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter", "seed": 77}], - "USDT", ['ETH/USDT', 'ADAHALF/USDT', 'NANO/USDT']), + "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), # ShuffleFilter, other seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter", "seed": 42}], - "USDT", ['NANO/USDT', 'ETH/USDT', 'ADAHALF/USDT']), + "USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']), # ShuffleFilter, no seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], @@ -347,6 +347,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, {"method": "StaticPairList"}], "BTC", 'static_in_the_middle'), + ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "low_price_ratio": 0.02}], + "USDT", ['ETH/USDT', 'NANO/USDT']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history_list, pairlists, base_currency, From 6a10c715fae72a466afb040dae8e1a55334c38f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Jul 2020 20:34:29 +0200 Subject: [PATCH 105/123] Fix 0 division (if last = 0, something went wrong!) --- freqtrade/pairlist/PriceFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 5ee1df078..b3b2f43dc 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -56,7 +56,7 @@ class PriceFilter(IPairList): :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ - if ticker['last'] is None: + if ticker['last'] is None or ticker['last'] == 0: self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).") From 2a5f8d889588e258191bc67f524d3b72d6a47d66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2020 06:22:45 +0000 Subject: [PATCH 106/123] Bump python from 3.8.4-slim-buster to 3.8.5-slim-buster Bumps python from 3.8.4-slim-buster to 3.8.5-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f27167cc5..e1220e3b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.4-slim-buster +FROM python:3.8.5-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From f5f529cacedd2fe9cb2807b1e6ce4b0b520158f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jul 2020 15:15:50 +0200 Subject: [PATCH 107/123] Use correct initialization of DataProvider --- freqtrade/plot/plotting.py | 8 +++++--- tests/test_plotting.py | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index db83448c0..a933c6a76 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -10,12 +10,13 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown, create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe +from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_prev_date from freqtrade.misc import pair_to_filename -from freqtrade.resolvers import StrategyResolver -from freqtrade.data.dataprovider import DataProvider +from freqtrade.resolvers import ExchangeResolver, StrategyResolver +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) @@ -468,6 +469,8 @@ def load_and_plot_trades(config: Dict[str, Any]): """ strategy = StrategyResolver.load_strategy(config) + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) + IStrategy.dp = DataProvider(config, exchange) plot_elements = init_plotscript(config) trades = plot_elements['trades'] pair_counter = 0 @@ -475,7 +478,6 @@ def load_and_plot_trades(config: Dict[str, Any]): pair_counter += 1 logger.info("analyse pair %s", pair) - strategy.dp = DataProvider(config, config["exchange"]) df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(df_analyzed, trades_pair) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 05805eb24..8f4512c4b 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -21,7 +21,7 @@ from freqtrade.plot.plotting import (add_indicators, add_profit, load_and_plot_trades, plot_profit, plot_trades, store_plot_file) from freqtrade.resolvers import StrategyResolver -from tests.conftest import get_args, log_has, log_has_re +from tests.conftest import get_args, log_has, log_has_re, patch_exchange def fig_generating_mock(fig, *args, **kwargs): @@ -316,6 +316,8 @@ def test_start_plot_dataframe(mocker): def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir): + patch_exchange(mocker) + default_conf['trade_source'] = 'file' default_conf["datadir"] = testdatadir default_conf['exportfilename'] = testdatadir / "backtest-result_test.json" From 0502fe0496382e6cc2a04161ad3a15f4f9f40e26 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Thu, 23 Jul 2020 09:36:05 +0700 Subject: [PATCH 108/123] New /trades 3 columns and exclude open trades --- freqtrade/rpc/telegram.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 343b26072..87a0cdd62 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,6 +5,7 @@ This module manage Telegram communication """ import json import logging +import arrow from typing import Any, Callable, Dict from tabulate import tabulate @@ -506,6 +507,7 @@ class Telegram(RPC): :param update: message update :return: None """ + stake_cur = self._config['stake_currency'] try: nrecent = int(context.args[0]) except (TypeError, ValueError, IndexError): @@ -515,21 +517,13 @@ class Telegram(RPC): nrecent ) trades_tab = tabulate( - [[trade['open_date'], + [[arrow.get(trade['open_date']).humanize(), trade['pair'], - f"{trade['open_rate']}", - f"{trade['stake_amount']}", - trade['close_date'], - f"{trade['close_rate']}", - f"{trade['close_profit_abs']}"] for trade in trades['trades']], + f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] for trade in trades['trades'] if trade['close_profit'] is not None], headers=[ 'Open Date', 'Pair', - 'Open rate', - 'Stake Amount', - 'Close date', - 'Close rate', - 'Profit', + f'Profit ({stake_cur})', ], tablefmt='simple') message = f'{nrecent} recent trades:\n
{trades_tab}
' From a3daf8e41c209e061a3e50f1b5fcba3802bf6421 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Thu, 23 Jul 2020 09:47:53 +0700 Subject: [PATCH 109/123] Fix line too long --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 87a0cdd62..943d092db 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -519,7 +519,8 @@ class Telegram(RPC): trades_tab = tabulate( [[arrow.get(trade['open_date']).humanize(), trade['pair'], - f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] for trade in trades['trades'] if trade['close_profit'] is not None], + f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] + for trade in trades['trades'] if trade['close_profit'] is not None], headers=[ 'Open Date', 'Pair', From 0bad55637e5e5907d9133df6973c1926056ac6d0 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Thu, 23 Jul 2020 10:12:52 +0700 Subject: [PATCH 110/123] fix flake8 indent error --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 943d092db..66583fa53 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -520,7 +520,7 @@ class Telegram(RPC): [[arrow.get(trade['open_date']).humanize(), trade['pair'], f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] - for trade in trades['trades'] if trade['close_profit'] is not None], + for trade in trades['trades'] if trade['close_profit'] is not None], headers=[ 'Open Date', 'Pair', From 0f18b2a0d45d3056cd52830f3081b359895bb044 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 Jul 2020 07:12:14 +0200 Subject: [PATCH 111/123] Add test and fix case where no trades were closed yet --- freqtrade/rpc/telegram.py | 3 ++- tests/rpc/test_rpc_telegram.py | 35 ++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 66583fa53..153be1e25 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -527,7 +527,8 @@ class Telegram(RPC): f'Profit ({stake_cur})', ], tablefmt='simple') - message = f'{nrecent} recent trades:\n
{trades_tab}
' + message = (f"{min(trades['trades_count'], nrecent)} recent trades:\n" + + (f"
{trades_tab}
" if trades['trades_count'] > 0 else '')) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1ea211584..f011b631d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType -from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange, - patch_get_signal, patch_whitelist) +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, + log_has, patch_exchange, patch_get_signal, + patch_whitelist) class DummyCls(Telegram): @@ -1143,6 +1144,36 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0] +def test_telegram_trades(mocker, update, default_conf, fee): + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + context = MagicMock() + context.args = [] + + telegram._trades(update=update, context=context) + assert "0 recent trades:" in msg_mock.call_args_list[0][0][0] + assert "
" not in msg_mock.call_args_list[0][0][0]
+
+    msg_mock.reset_mock()
+    create_mock_trades(fee)
+
+    context = MagicMock()
+    context.args = [5]
+    telegram._trades(update=update, context=context)
+    msg_mock.call_count == 1
+    assert "3 recent trades:" in msg_mock.call_args_list[0][0][0]
+    assert "Profit (" in msg_mock.call_args_list[0][0][0]
+    assert "Open Date" in msg_mock.call_args_list[0][0][0]
+    assert "
" in msg_mock.call_args_list[0][0][0]
+
+
 def test_help_handle(default_conf, update, mocker) -> None:
     msg_mock = MagicMock()
     mocker.patch.multiple(

From 8300eb59d4b448b4f04da34b80efd603a32a6002 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 23 Jul 2020 07:49:44 +0200
Subject: [PATCH 112/123] Extend create_mock_trades to create 4 trades

2 closed, and 2 open trades
---
 tests/commands/test_commands.py |  2 +-
 tests/conftest.py               | 14 ++++++++++++++
 tests/data/test_btanalysis.py   |  2 +-
 tests/test_freqtradebot.py      |  2 +-
 tests/test_persistence.py       |  6 +++---
 5 files changed, 20 insertions(+), 6 deletions(-)

diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py
index ffced956d..3ec7e4798 100644
--- a/tests/commands/test_commands.py
+++ b/tests/commands/test_commands.py
@@ -1089,7 +1089,7 @@ def test_show_trades(mocker, fee, capsys, caplog):
     pargs = get_args(args)
     pargs['config'] = None
     start_show_trades(pargs)
-    assert log_has("Printing 3 Trades: ", caplog)
+    assert log_has("Printing 4 Trades: ", caplog)
     captured = capsys.readouterr()
     assert "Trade(id=1" in captured.out
     assert "Trade(id=2" in captured.out
diff --git a/tests/conftest.py b/tests/conftest.py
index 43dc8ca78..fe8d54480 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -199,6 +199,20 @@ def create_mock_trades(fee):
     )
     Trade.session.add(trade)
 
+    trade = Trade(
+        pair='XRP/BTC',
+        stake_amount=0.001,
+        amount=123.0,
+        fee_open=fee.return_value,
+        fee_close=fee.return_value,
+        open_rate=0.05,
+        close_rate=0.06,
+        close_profit=0.01,
+        exchange='bittrex',
+        is_open=False,
+    )
+    Trade.session.add(trade)
+
     # Simulate prod entry
     trade = Trade(
         pair='ETC/BTC',
diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py
index b65db7fd8..718c02f05 100644
--- a/tests/data/test_btanalysis.py
+++ b/tests/data/test_btanalysis.py
@@ -43,7 +43,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
 
     trades = load_trades_from_db(db_url=default_conf['db_url'])
     assert init_mock.call_count == 1
-    assert len(trades) == 3
+    assert len(trades) == 4
     assert isinstance(trades, DataFrame)
     assert "pair" in trades.columns
     assert "open_time" in trades.columns
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index ada0d87fd..54e33be4d 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -4090,7 +4090,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
     create_mock_trades(fee)
     trades = Trade.query.all()
-    assert len(trades) == 3
+    assert len(trades) == 4
     freqtrade.cancel_all_open_orders()
     assert buy_mock.call_count == 1
     assert sell_mock.call_count == 1
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
index 8dd27e53a..ab23243a5 100644
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -989,7 +989,7 @@ def test_get_overall_performance(fee):
     create_mock_trades(fee)
     res = Trade.get_overall_performance()
 
-    assert len(res) == 1
+    assert len(res) == 2
     assert 'pair' in res[0]
     assert 'profit' in res[0]
     assert 'count' in res[0]
@@ -1004,5 +1004,5 @@ def test_get_best_pair(fee):
     create_mock_trades(fee)
     res = Trade.get_best_pair()
     assert len(res) == 2
-    assert res[0] == 'ETC/BTC'
-    assert res[1] == 0.005
+    assert res[0] == 'XRP/BTC'
+    assert res[1] == 0.01

From fdc84eef5905d81a69076199990d3e7bf999e938 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 23 Jul 2020 07:50:45 +0200
Subject: [PATCH 113/123] /trades shall only return closed trades

---
 freqtrade/rpc/rpc.py            |  5 +++--
 freqtrade/rpc/telegram.py       |  2 +-
 tests/rpc/test_rpc.py           | 11 +++++------
 tests/rpc/test_rpc_apiserver.py |  8 ++++----
 tests/rpc/test_rpc_telegram.py  |  2 +-
 5 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index c73fcbf54..b39d5aec4 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -252,9 +252,10 @@ class RPC:
     def _rpc_trade_history(self, limit: int) -> Dict:
         """ Returns the X last trades """
         if limit > 0:
-            trades = Trade.get_trades().order_by(Trade.id.desc()).limit(limit)
+            trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
+                Trade.id.desc()).limit(limit)
         else:
-            trades = Trade.get_trades().order_by(Trade.id.desc()).all()
+            trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all()
 
         output = [trade.to_json() for trade in trades]
 
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 153be1e25..17f0e21f9 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -520,7 +520,7 @@ class Telegram(RPC):
                 [[arrow.get(trade['open_date']).humanize(),
                   trade['pair'],
                   f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
-                 for trade in trades['trades'] if trade['close_profit'] is not None],
+                 for trade in trades['trades']],
                 headers=[
                     'Open Date',
                     'Pair',
diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
index de9327ba9..e5859fcd9 100644
--- a/tests/rpc/test_rpc.py
+++ b/tests/rpc/test_rpc.py
@@ -284,12 +284,11 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
     assert isinstance(trades['trades'][1], dict)
 
     trades = rpc._rpc_trade_history(0)
-    assert len(trades['trades']) == 3
-    assert trades['trades_count'] == 3
-    # The first trade is for ETH ... sorting is descending
-    assert trades['trades'][-1]['pair'] == 'ETH/BTC'
-    assert trades['trades'][0]['pair'] == 'ETC/BTC'
-    assert trades['trades'][1]['pair'] == 'ETC/BTC'
+    assert len(trades['trades']) == 2
+    assert trades['trades_count'] == 2
+    # The first closed trade is for ETC ... sorting is descending
+    assert trades['trades'][-1]['pair'] == 'ETC/BTC'
+    assert trades['trades'][0]['pair'] == 'XRP/BTC'
 
 
 def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index 355b63f48..f4d7b8ca3 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -368,12 +368,12 @@ def test_api_trades(botclient, mocker, ticker, fee, markets):
 
     rc = client_get(client, f"{BASE_URI}/trades")
     assert_response(rc)
-    assert len(rc.json['trades']) == 3
-    assert rc.json['trades_count'] == 3
-    rc = client_get(client, f"{BASE_URI}/trades?limit=2")
-    assert_response(rc)
     assert len(rc.json['trades']) == 2
     assert rc.json['trades_count'] == 2
+    rc = client_get(client, f"{BASE_URI}/trades?limit=1")
+    assert_response(rc)
+    assert len(rc.json['trades']) == 1
+    assert rc.json['trades_count'] == 1
 
 
 def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index f011b631d..cfe0ade6f 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -1168,7 +1168,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
     context.args = [5]
     telegram._trades(update=update, context=context)
     msg_mock.call_count == 1
-    assert "3 recent trades:" in msg_mock.call_args_list[0][0][0]
+    assert "2 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "Profit (" in msg_mock.call_args_list[0][0][0]
     assert "Open Date" in msg_mock.call_args_list[0][0][0]
     assert "
" in msg_mock.call_args_list[0][0][0]

From e0c14e6214a79e25d0c6b1d6b189001d89d89e4f Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 23 Jul 2020 07:54:45 +0200
Subject: [PATCH 114/123] Add /trades to help (so users know about it)

---
 docs/telegram-usage.md    | 1 +
 freqtrade/rpc/telegram.py | 1 +
 2 files changed, 2 insertions(+)

diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index f423a9376..250293d25 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -56,6 +56,7 @@ official commands. You can ask at any moment for help with `/help`.
 | `/show_config` | | Shows part of the current configuration with relevant settings to operation
 | `/status` | | Lists all open trades
 | `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
+| `/trades [limit]` | | List all recently closed trades in a table format.
 | `/count` | | Displays number of trades used and available
 | `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
 | `/forcesell ` | | Instantly sells the given trade  (Ignoring `minimum_roi`).
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 17f0e21f9..ab784c962 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -646,6 +646,7 @@ class Telegram(RPC):
                    "         *table :* `will display trades in a table`\n"
                    "                `pending buy orders are marked with an asterisk (*)`\n"
                    "                `pending sell orders are marked with a double asterisk (**)`\n"
+                   "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
                    "*/profit:* `Lists cumulative profit from all finished trades`\n"
                    "*/forcesell |all:* `Instantly sells the given trade or all trades, "
                    "regardless of profit`\n"

From 6ce4fd7aff2c9cd78d86b4830e172526eb26877d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 26 Jul 2020 08:37:10 +0000
Subject: [PATCH 115/123] Bump arrow from 0.15.7 to 0.15.8

Bumps [arrow](https://github.com/arrow-py/arrow) from 0.15.7 to 0.15.8.
- [Release notes](https://github.com/arrow-py/arrow/releases)
- [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/arrow-py/arrow/compare/0.15.7...0.15.8)

Signed-off-by: dependabot[bot] 
---
 requirements-common.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-common.txt b/requirements-common.txt
index d5c5fd832..63b2c0441 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -3,7 +3,7 @@
 ccxt==1.31.37
 SQLAlchemy==1.3.18
 python-telegram-bot==12.8
-arrow==0.15.7
+arrow==0.15.8
 cachetools==4.1.1
 requests==2.24.0
 urllib3==1.25.9

From d1d6f69e43b31491edfc01efb74a12da1349e57e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 26 Jul 2020 08:37:13 +0000
Subject: [PATCH 116/123] Bump scipy from 1.5.1 to 1.5.2

Bumps [scipy](https://github.com/scipy/scipy) from 1.5.1 to 1.5.2.
- [Release notes](https://github.com/scipy/scipy/releases)
- [Commits](https://github.com/scipy/scipy/compare/v1.5.1...v1.5.2)

Signed-off-by: dependabot[bot] 
---
 requirements-hyperopt.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt
index 4773d9877..ce08f08e0 100644
--- a/requirements-hyperopt.txt
+++ b/requirements-hyperopt.txt
@@ -2,7 +2,7 @@
 -r requirements.txt
 
 # Required for hyperopt
-scipy==1.5.1
+scipy==1.5.2
 scikit-learn==0.23.1
 scikit-optimize==0.7.4
 filelock==3.0.12

From 2ff03e173d3634128f47666a3777e0f838ad9374 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 26 Jul 2020 08:37:17 +0000
Subject: [PATCH 117/123] Bump numpy from 1.19.0 to 1.19.1

Bumps [numpy](https://github.com/numpy/numpy) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt)
- [Commits](https://github.com/numpy/numpy/compare/v1.19.0...v1.19.1)

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index 1e61d165f..2392d4cb2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 # Load common requirements
 -r requirements-common.txt
 
-numpy==1.19.0
+numpy==1.19.1
 pandas==1.0.5

From 838743bf01590b885e5bb1ddf5cacf009d02f673 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 26 Jul 2020 08:37:25 +0000
Subject: [PATCH 118/123] Bump mkdocs-material from 5.4.0 to 5.5.0

Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.4.0 to 5.5.0.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.4.0...5.5.0)

Signed-off-by: dependabot[bot] 
---
 docs/requirements-docs.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
index 3a236ee87..2a2405f8e 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1,2 +1,2 @@
-mkdocs-material==5.4.0
+mkdocs-material==5.5.0
 mdx_truly_sane_lists==1.2

From 63e7490a55baae19beed1652c0bb0ba7c15e0cc9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 26 Jul 2020 08:37:45 +0000
Subject: [PATCH 119/123] Bump plotly from 4.8.2 to 4.9.0

Bumps [plotly](https://github.com/plotly/plotly.py) from 4.8.2 to 4.9.0.
- [Release notes](https://github.com/plotly/plotly.py/releases)
- [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.py/compare/v4.8.2...v4.9.0)

Signed-off-by: dependabot[bot] 
---
 requirements-plot.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-plot.txt b/requirements-plot.txt
index ec5af3dbf..51d14d636 100644
--- a/requirements-plot.txt
+++ b/requirements-plot.txt
@@ -1,5 +1,5 @@
 # Include all requirements to run the bot.
 -r requirements.txt
 
-plotly==4.8.2
+plotly==4.9.0
 

From b4d22f10001f8ad2c977bf1e47a79e39fc5094b6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 26 Jul 2020 08:53:36 +0000
Subject: [PATCH 120/123] Bump urllib3 from 1.25.9 to 1.25.10

Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.9 to 1.25.10.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.25.9...1.25.10)

Signed-off-by: dependabot[bot] 
---
 requirements-common.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-common.txt b/requirements-common.txt
index 63b2c0441..e0a9af77e 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -6,7 +6,7 @@ python-telegram-bot==12.8
 arrow==0.15.8
 cachetools==4.1.1
 requests==2.24.0
-urllib3==1.25.9
+urllib3==1.25.10
 wrapt==1.12.1
 jsonschema==3.2.0
 TA-Lib==0.4.18

From dbcccac6cd08bac45cec087f691300b77e962a2b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 26 Jul 2020 08:53:51 +0000
Subject: [PATCH 121/123] Bump ccxt from 1.31.37 to 1.32.3

Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.31.37 to 1.32.3.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst)
- [Commits](https://github.com/ccxt/ccxt/compare/1.31.37...1.32.3)

Signed-off-by: dependabot[bot] 
---
 requirements-common.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-common.txt b/requirements-common.txt
index 63b2c0441..db06fee98 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -1,6 +1,6 @@
 # requirements without requirements installable via conda
 # mainly used for Raspberry pi installs
-ccxt==1.31.37
+ccxt==1.32.3
 SQLAlchemy==1.3.18
 python-telegram-bot==12.8
 arrow==0.15.8

From 7318d02ebc1331adba985d4c2525344c262437b1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 27 Jul 2020 07:05:17 +0000
Subject: [PATCH 122/123] Bump ccxt from 1.32.3 to 1.32.7

Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.32.3 to 1.32.7.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst)
- [Commits](https://github.com/ccxt/ccxt/compare/1.32.3...1.32.7)

Signed-off-by: dependabot[bot] 
---
 requirements-common.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-common.txt b/requirements-common.txt
index ba41d3ac2..942fe3792 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -1,6 +1,6 @@
 # requirements without requirements installable via conda
 # mainly used for Raspberry pi installs
-ccxt==1.32.3
+ccxt==1.32.7
 SQLAlchemy==1.3.18
 python-telegram-bot==12.8
 arrow==0.15.8

From 7263f83f78612d94497b201fae9ce1411e1965ab Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 28 Jul 2020 19:53:05 +0200
Subject: [PATCH 123/123] Version bump 2020.7

---
 freqtrade/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py
index 1eb0f9bec..7d1ef43ad 100644
--- a/freqtrade/__init__.py
+++ b/freqtrade/__init__.py
@@ -1,5 +1,5 @@
 """ Freqtrade bot """
-__version__ = '2020.6'
+__version__ = '2020.7'
 
 if __version__ == 'develop':