From 0c810868de467e8a9a5f5f3941992117abadb31a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Sep 2022 14:59:16 +0200 Subject: [PATCH 01/32] Add Dataprovider to pairlist --- freqtrade/freqtradebot.py | 5 ++++- freqtrade/optimize/backtesting.py | 2 +- freqtrade/plugins/pairlistmanager.py | 6 ++++-- tests/plugins/test_pairlist.py | 10 +++++----- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 72b88a82f..169af2ab6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -82,7 +82,10 @@ class FreqtradeBot(LoggingMixin): # Keep this at the end of this initialization method. self.rpc: RPCManager = RPCManager(self) - self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists, self.rpc) + self.dataprovider = DataProvider(self.config, self.exchange, rpc=self.rpc) + self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider) + + self.dataprovider.add_pairlisthandler(self.pairlists) # Attach Dataprovider to strategy instance self.strategy.dp = self.dataprovider diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2a1c44f7f..aa25e049a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -110,7 +110,7 @@ class Backtesting: self.timeframe = str(self.config.get('timeframe')) self.timeframe_min = timeframe_to_minutes(self.timeframe) self.init_backtest_detail() - self.pairlists = PairListManager(self.exchange, self.config) + self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider) if 'VolumePairList' in self.pairlists.name_list: raise OperationalException("VolumePairList not allowed for backtesting. " "Please use StaticPairlist instead.") diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index e01abb297..763307d3f 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -3,11 +3,12 @@ PairList manager class """ import logging from functools import partial -from typing import Dict, List +from typing import Dict, List, Optional from cachetools import TTLCache, cached from freqtrade.constants import Config, ListPairsWithTimeframes +from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import CandleType from freqtrade.exceptions import OperationalException from freqtrade.mixins import LoggingMixin @@ -21,13 +22,14 @@ logger = logging.getLogger(__name__) class PairListManager(LoggingMixin): - def __init__(self, exchange, config: Config) -> None: + def __init__(self, exchange, config: Config, dataprovider: DataProvider = None) -> None: self._exchange = exchange self._config = config self._whitelist = self._config['exchange'].get('pair_whitelist') self._blacklist = self._config['exchange'].get('pair_blacklist', []) self._pairlist_handlers: List[IPairList] = [] self._tickers_needed = False + self._dataprovider: Optional[DataProvider] = dataprovider for pairlist_handler_config in self._config.get('pairlists', []): pairlist_handler = PairListResolver.load_pairlist( pairlist_handler_config['method'], diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 538751251..26b7ebbe2 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -126,7 +126,7 @@ def test_log_cached(mocker, static_pl_conf, markets, tickers): def test_load_pairlist_noexist(mocker, markets, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) - plm = PairListManager(freqtrade.exchange, default_conf) + plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) with pytest.raises(OperationalException, match=r"Impossible to load Pairlist 'NonexistingPairList'. " r"This class does not exist or contains Python code errors."): @@ -137,7 +137,7 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): def test_load_pairlist_verify_multi(mocker, markets_static, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets_static)) - plm = PairListManager(freqtrade.exchange, default_conf) + plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) # Call different versions one after the other, should always consider what was passed in # and have no side-effects (therefore the same check multiple times) assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC'] @@ -269,7 +269,7 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co with pytest.raises(OperationalException, match=r'`number_assets` not specified. Please check your configuration ' r'for "pairlist.config.number_assets"'): - PairListManager(freqtrade.exchange, whitelist_conf) + PairListManager(freqtrade.exchange, whitelist_conf, MagicMock()) def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_conf_2): @@ -694,7 +694,7 @@ def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: with pytest.raises(OperationalException, match=r"PrecisionFilter can only work with stoploss defined\..*"): - PairListManager(MagicMock, whitelist_conf) + PairListManager(MagicMock, whitelist_conf, MagicMock()) def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: @@ -703,7 +703,7 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: del Trade.query mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, whitelist_conf) - pm = PairListManager(exchange, whitelist_conf) + pm = PairListManager(exchange, whitelist_conf, MagicMock()) pm.refresh_pairlist() assert log_has("PerformanceFilter is not available in this mode.", caplog) From 4940fa7be3520e6096ccbd6f7231d0eb6b9b128b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Sep 2022 15:29:57 +0200 Subject: [PATCH 02/32] Add Producer Pairlist --- freqtrade/constants.py | 2 +- .../plugins/pairlist/ProducerPairList.py | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 freqtrade/plugins/pairlist/ProducerPairList.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 4c2bd6e18..e0e42c821 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -31,7 +31,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'CalmarHyperOptLoss', 'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss', 'ProfitDrawDownHyperOptLoss'] -AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', +AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py new file mode 100644 index 000000000..81320f713 --- /dev/null +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -0,0 +1,87 @@ +""" +External Pair List provider + +Provides pair list from Leader data +""" +import logging +from typing import Any, Dict, List, Optional + +from freqtrade.plugins.pairlist.IPairList import IPairList + + +logger = logging.getLogger(__name__) + + +class ProducerPairList(IPairList): + """ + PairList plugin for use with external_message_consumer. + Will use pairs given from leader data. + + Usage: + "pairlists": [ + { + "method": "ProducerPairList", + "number_assets": 5, + "producer_name": "default", + } + ], + """ + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._num_assets = self._pairlistconfig.get('number_assets') + self._producer_name = self._pairlistconfig.get('producer_name', 'default') + if config.get('external_message_consumer').get('enabled') is False: + raise ValueError("ProducerPairList requires external_message_consumer to be enabled.") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty Dict is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + -> Please overwrite in subclasses + """ + return f"{self.name} - {self._producer_name}" + + def _filter_pairlist(self, pairlist: Optional[List[str]]): + upstream_pairlist = self._pairlistmanager._dataprovider.get_producer_pairs( + self._producer_name) + + if pairlist is None: + pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(self._producer_name) + + pairs = list(dict.fromkeys(upstream_pairlist + pairlist))[:self._num_assets] + + return pairs + + def gen_pairlist(self, tickers: Dict) -> List[str]: + """ + Generate the pairlist + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: List of pairs + """ + pairs = self._filter_pairlist(None) + self.log_once(f"Received pairs: {pairs}", logger.debug) + pairs = self._whitelist_for_active_markets(self.verify_whitelist(pairs, logger.info)) + self.log_once(f"New Pairlist: {pairs}", logger.info) + return pairs + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist + """ + return self._filter_pairlist(pairlist) From 527fd36134f16995ebf960a42fff1080100e8af7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 09:38:20 +0200 Subject: [PATCH 03/32] num_assets should be optional --- freqtrade/plugins/pairlist/ProducerPairList.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index 81320f713..0dc90ac0f 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -32,7 +32,7 @@ class ProducerPairList(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - self._num_assets = self._pairlistconfig.get('number_assets') + self._num_assets: int = self._pairlistconfig.get('number_assets', 0) self._producer_name = self._pairlistconfig.get('producer_name', 'default') if config.get('external_message_consumer').get('enabled') is False: raise ValueError("ProducerPairList requires external_message_consumer to be enabled.") @@ -60,7 +60,9 @@ class ProducerPairList(IPairList): if pairlist is None: pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(self._producer_name) - pairs = list(dict.fromkeys(upstream_pairlist + pairlist))[:self._num_assets] + pairs = list(dict.fromkeys(upstream_pairlist + pairlist)) + if self._num_assets: + pairs = pairs[:self._num_assets] return pairs From 1c089dcd51fb192ed41beb98573cd915c2be380e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 09:40:44 +0200 Subject: [PATCH 04/32] Add docs for Producer/consumer pairlist --- docs/includes/pairlists.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 0f55c1b79..7dff75a02 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -22,6 +22,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged * [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`VolumePairList`](#volume-pair-list) +* [`ProducerPairList`](#producerpairlist) * [`AgeFilter`](#agefilter) * [`OffsetFilter`](#offsetfilter) * [`PerformanceFilter`](#performancefilter) @@ -84,7 +85,7 @@ Filtering instances (not the first position in the list) will not apply any cach You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange. -### VolumePairList Advanced mode +##### VolumePairList Advanced mode `VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles. @@ -146,6 +147,32 @@ More sophisticated approach can be used, by using `lookback_timeframe` for candl !!! Note `VolumePairList` does not support backtesting mode. +#### ProducerPairList + +With `ProducerPairList`, you can reuse the pairlist from a [Producer](producer-consumer.md) without explicitly defining the pairlist on each consumer. + +[Consumer mode](producer-consumer.md) is required for this pairlist to work. + +The pairlist will perform a check on active pairs against the current exchange configuration to avoid attempting to trade on invalid markets. + +You can limit the length of the pairlist with the optional parameter `number_assets`. Using `"number_assets"=0` or omitting this key will result in the reuse of all producer pairs valid for the current setup. + +```json +"pairlists": [ + { + "method": "ProducerPairList", + "number_assets": 5, + "producer_name": "default", + } +], +``` + + +!!! Tip "Combining pairlists" + This pairlist can be combined with all other pairlists and filters for further pairlist reduction, and can also act as an "additional" pairlist, on top of already defined pairs. + `ProducerPairList` can also be used multiple times in sequence, combining the pairs from multiple producers. + Obviously in complex such configurations, the Producer may not provide data for all pairs, so the strategy must be fit for this. + #### AgeFilter Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity). From 30d51b6939819b541b9582284b93d17bc6927783 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 09:43:39 +0200 Subject: [PATCH 05/32] Move "pairlist" logging to manager --- freqtrade/plugins/pairlist/ProducerPairList.py | 1 - freqtrade/plugins/pairlist/VolumePairList.py | 2 -- freqtrade/plugins/pairlistmanager.py | 2 ++ 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index 0dc90ac0f..d0fb4ada2 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -75,7 +75,6 @@ class ProducerPairList(IPairList): pairs = self._filter_pairlist(None) self.log_once(f"Received pairs: {pairs}", logger.debug) pairs = self._whitelist_for_active_markets(self.verify_whitelist(pairs, logger.info)) - self.log_once(f"New Pairlist: {pairs}", logger.info) return pairs def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 9dcada291..b290f76aa 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -232,6 +232,4 @@ class VolumePairList(IPairList): # Limit pairlist to the requested number of pairs pairs = pairs[:self._number_pairs] - self.log_once(f"Searching {self._number_pairs} pairs: {pairs}", logger.info) - return pairs diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 763307d3f..5ed319e93 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -98,6 +98,8 @@ class PairListManager(LoggingMixin): # to ensure blacklist is respected. pairlist = self.verify_blacklist(pairlist, logger.warning) + self.log_once(f"Whitelist with {len(pairlist)} pairs: {pairlist}", logger.info) + self._whitelist = pairlist def verify_blacklist(self, pairlist: List[str], logmethod) -> List[str]: From 1bb45a2650df119a5dad27b37a0390584254655e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 09:47:57 +0200 Subject: [PATCH 06/32] Fix crash due to insufficient check --- freqtrade/plugins/pairlist/ProducerPairList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index d0fb4ada2..dc575f29b 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -34,7 +34,7 @@ class ProducerPairList(IPairList): self._num_assets: int = self._pairlistconfig.get('number_assets', 0) self._producer_name = self._pairlistconfig.get('producer_name', 'default') - if config.get('external_message_consumer').get('enabled') is False: + if config.get('external_message_consumer', {}).get('enabled') is False: raise ValueError("ProducerPairList requires external_message_consumer to be enabled.") @property From bd106b4b8eaf581b0a77a31e1d1f8133ba067b9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 10:13:00 +0200 Subject: [PATCH 07/32] Add tests for Producerpairlist --- .../plugins/pairlist/ProducerPairList.py | 6 +- tests/conftest.py | 2 + tests/plugins/test_pairlist.py | 72 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index dc575f29b..fa351c9cc 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -6,6 +6,7 @@ Provides pair list from Leader data import logging from typing import Any, Dict, List, Optional +from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList @@ -34,8 +35,9 @@ class ProducerPairList(IPairList): self._num_assets: int = self._pairlistconfig.get('number_assets', 0) self._producer_name = self._pairlistconfig.get('producer_name', 'default') - if config.get('external_message_consumer', {}).get('enabled') is False: - raise ValueError("ProducerPairList requires external_message_consumer to be enabled.") + if not config.get('external_message_consumer', {}).get('enabled'): + raise OperationalException( + "ProducerPairList requires external_message_consumer to be enabled.") @property def needstickers(self) -> bool: diff --git a/tests/conftest.py b/tests/conftest.py index 51b1b03e3..a9eeb481e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -200,6 +200,8 @@ def patch_freqtradebot(mocker, config) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) patch_whitelist(mocker, config) + mocker.patch('freqtrade.freqtradebot.ExternalMessageConsumer') + mocker.patch('freqtrade.configuration.config_validation._validate_consumers') def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 26b7ebbe2..a6b5813da 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -9,6 +9,7 @@ import pytest import time_machine from freqtrade.constants import AVAILABLE_PAIRLISTS +from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import CandleType, RunMode from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade @@ -40,6 +41,12 @@ def whitelist_conf(default_conf): "sort_key": "quoteVolume", }, ] + default_conf.update({ + "external_message_consumer": { + "enabled": True, + "producers": [], + } + }) return default_conf @@ -1167,6 +1174,10 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo "[{'OffsetFilter': 'OffsetFilter - Taking 10 Pairs, starting from 5.'}]", None ), + ({"method": "ProducerPairList"}, + "[{'ProducerPairList': 'ProducerPairList - default'}]", + None + ), ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected): @@ -1341,3 +1352,64 @@ def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected): expand_pairlist(wildcardlist, pairs, keep_invalid=True) else: assert sorted(expand_pairlist(wildcardlist, pairs, keep_invalid=True)) == sorted(expected) + + +def test_ProducerPairlist_no_emc(mocker, whitelist_conf): + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + + whitelist_conf['pairlists'] = [ + { + "method": "ProducerPairList", + "number_assets": 10, + "producer_name": "hello_world", + } + ] + del whitelist_conf['external_message_consumer'] + + with pytest.raises(OperationalException, + match=r"ProducerPairList requires external_message_consumer to be enabled."): + get_patched_freqtradebot(mocker, whitelist_conf) + + +def test_ProducerPairlist(mocker, whitelist_conf, markets): + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + ) + whitelist_conf['pairlists'] = [ + { + "method": "ProducerPairList", + "number_assets": 2, + "producer_name": "hello_world", + } + ] + whitelist_conf.update({ + "external_message_consumer": { + "enabled": True, + "producers": [ + { + "name": "hello_world", + "host": "null", + "port": 9891, + "ws_token": "dummy", + } + ] + } + }) + + exchange = get_patched_exchange(mocker, whitelist_conf) + dp = DataProvider(whitelist_conf, exchange, None) + pairs = ['ETH/BTC', 'LTC/BTC', 'XRP/BTC'] + # different producer + dp._set_producer_pairs(pairs + ['MEEP/USDT'], 'default') + pm = PairListManager(exchange, whitelist_conf, dp) + pm.refresh_pairlist() + assert pm.whitelist == [] + # proper producer + dp._set_producer_pairs(pairs, 'hello_world') + pm.refresh_pairlist() + + # Pairlist reduced to 2 + assert pm.whitelist == pairs[:2] + assert len(pm.whitelist) == 2 From af59572cb9a7abd5286540de3dc3a38fb355cc64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Sep 2022 19:32:39 +0200 Subject: [PATCH 08/32] prior pairlists should go first --- freqtrade/plugins/pairlist/ProducerPairList.py | 2 +- tests/plugins/test_pairlist.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/ProducerPairList.py b/freqtrade/plugins/pairlist/ProducerPairList.py index fa351c9cc..50b674e60 100644 --- a/freqtrade/plugins/pairlist/ProducerPairList.py +++ b/freqtrade/plugins/pairlist/ProducerPairList.py @@ -62,7 +62,7 @@ class ProducerPairList(IPairList): if pairlist is None: pairlist = self._pairlistmanager._dataprovider.get_producer_pairs(self._producer_name) - pairs = list(dict.fromkeys(upstream_pairlist + pairlist)) + pairs = list(dict.fromkeys(pairlist + upstream_pairlist)) if self._num_assets: pairs = pairs[:self._num_assets] diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index a6b5813da..82fc99d7a 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -1413,3 +1413,16 @@ def test_ProducerPairlist(mocker, whitelist_conf, markets): # Pairlist reduced to 2 assert pm.whitelist == pairs[:2] assert len(pm.whitelist) == 2 + whitelist_conf['exchange']['pair_whitelist'] = ['TKN/BTC'] + + whitelist_conf['pairlists'] = [ + {"method": "StaticPairList"}, + { + "method": "ProducerPairList", + "producer_name": "hello_world", + } + ] + pm = PairListManager(exchange, whitelist_conf, dp) + pm.refresh_pairlist() + assert len(pm.whitelist) == 4 + assert pm.whitelist == ['TKN/BTC'] + pairs From 8eda3a45a3e91a1dc2720ca5d4b5f9a4301836e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Sep 2022 19:47:49 +0200 Subject: [PATCH 09/32] Test backest detail with leverage --- .../test_backtesting_adjust_position.py | 21 ++++++++++++------- tests/test_integration.py | 17 +++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 71f8cdcea..99c160a40 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -93,11 +93,16 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> t["close_rate"], 6) < round(ln.iloc[0]["high"], 6)) -def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> None: +@pytest.mark.parametrize('leverage', [ + 1, 2 +]) +def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, leverage) -> None: default_conf['use_exit_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=10) mocker.patch("freqtrade.exchange.Exchange.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10) + patch_exchange(mocker) default_conf.update({ "stake_amount": 100.0, @@ -105,6 +110,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non "strategy": "StrategyTestV3" }) backtesting = Backtesting(default_conf) + backtesting._can_short = True backtesting._set_strategy(backtesting.strategylist[0]) pair = 'XRP/USDT' row = [ @@ -120,18 +126,19 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non '', # enter_tag '', # exit_tag ] + backtesting.strategy.leverage = MagicMock(return_value=leverage) trade = backtesting._enter_trade(pair, row=row, direction='long') trade.orders[0].close_bt_order(row[0], trade) assert trade assert pytest.approx(trade.stake_amount) == 100.0 - assert pytest.approx(trade.amount) == 47.61904762 + assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 1 backtesting.strategy.adjust_trade_position = MagicMock(return_value=None) trade = backtesting._get_adjust_trade_entry_for_candle(trade, row) assert trade assert pytest.approx(trade.stake_amount) == 100.0 - assert pytest.approx(trade.amount) == 47.61904762 + assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 1 # Increase position by 100 backtesting.strategy.adjust_trade_position = MagicMock(return_value=100) @@ -140,7 +147,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non assert trade assert pytest.approx(trade.stake_amount) == 200.0 - assert pytest.approx(trade.amount) == 95.23809524 + assert pytest.approx(trade.amount) == 95.23809524 * leverage assert len(trade.orders) == 2 # Reduce by more than amount - no change to trade. @@ -150,7 +157,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non assert trade assert pytest.approx(trade.stake_amount) == 200.0 - assert pytest.approx(trade.amount) == 95.23809524 + assert pytest.approx(trade.amount) == 95.23809524 * leverage assert len(trade.orders) == 2 assert trade.nr_of_successful_entries == 2 @@ -160,7 +167,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non assert trade assert pytest.approx(trade.stake_amount) == 100.0 - assert pytest.approx(trade.amount) == 47.61904762 + assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 3 assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_exits == 1 @@ -171,7 +178,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker) -> Non assert trade assert pytest.approx(trade.stake_amount) == 100.0 - assert pytest.approx(trade.amount) == 47.61904762 + assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 3 assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_exits == 1 diff --git a/tests/test_integration.py b/tests/test_integration.py index a7b4fbdd3..a848de5d3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock import pytest -from freqtrade.enums import ExitCheckTuple, ExitType +from freqtrade.enums import ExitCheckTuple, ExitType, TradingMode from freqtrade.persistence import Trade from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC @@ -455,10 +455,12 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert pytest.approx(trade.orders[-1].amount) == 61.538461232 -def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> None: +@pytest.mark.parametrize('leverage', [1, 2]) +def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, leverage) -> None: default_conf_usdt['position_adjustment_enable'] = True freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + freqtrade.trading_mode = TradingMode.FUTURES mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, @@ -467,15 +469,17 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non price_to_precision=lambda s, x, y: y, get_min_pair_stake_amount=MagicMock(return_value=10), ) + mocker.patch("freqtrade.exchange.Exchange.get_max_leverage", return_value=10) patch_get_signal(freqtrade) + freqtrade.strategy.leverage = MagicMock(return_value=leverage) freqtrade.enter_positions() assert len(Trade.get_trades().all()) == 1 trade = Trade.get_trades().first() assert len(trade.orders) == 1 assert pytest.approx(trade.stake_amount) == 60 - assert pytest.approx(trade.amount) == 30.0 + assert pytest.approx(trade.amount) == 30.0 * leverage assert trade.open_rate == 2.0 # Too small size @@ -484,8 +488,9 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non trade = Trade.get_trades().first() assert len(trade.orders) == 1 assert pytest.approx(trade.stake_amount) == 60 - assert pytest.approx(trade.amount) == 30.0 - assert log_has_re("Remaining amount of 1.6.* would be smaller than the minimum of 10.", caplog) + assert pytest.approx(trade.amount) == 30.0 * leverage + assert log_has_re( + r"Remaining amount of \d\.\d+.* would be smaller than the minimum of 10.", caplog) freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-20) @@ -494,7 +499,7 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog) -> Non assert len(trade.orders) == 2 assert trade.orders[-1].ft_order_side == 'sell' assert pytest.approx(trade.stake_amount) == 40.198 - assert pytest.approx(trade.amount) == 20.099 + assert pytest.approx(trade.amount) == 20.099 * leverage assert trade.open_rate == 2.0 assert trade.is_open caplog.clear() From 30a5bb08ddcc0708a3a50217243608a294174f20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Sep 2022 19:53:55 +0200 Subject: [PATCH 10/32] partial exits should account for leverage --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 72b88a82f..b1c95a721 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -597,7 +597,7 @@ class FreqtradeBot(LoggingMixin): # We should decrease our position amount = self.exchange.amount_to_contract_precision( trade.pair, - abs(float(FtPrecise(stake_amount) / FtPrecise(current_exit_rate)))) + abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate)))) if amount > trade.amount: # This is currently ineffective as remaining would become < min tradable # Fixing this would require checking for 0.0 there - diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e942bdfeb..efe199bdf 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -540,7 +540,7 @@ class Backtesting: if stake_amount is not None and stake_amount < 0.0: amount = amount_to_contract_precision( - abs(stake_amount) / current_rate, trade.amount_precision, + abs(stake_amount * trade.leverage) / current_rate, trade.amount_precision, self.precision_mode, trade.contract_size) if amount == 0.0: return trade From 255c748ca2ac6c4ee58452c8f0e17f077afd2a11 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Sep 2022 19:55:17 +0200 Subject: [PATCH 11/32] Update docs for new trade_position behavior --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 0b8403414..ea10fc472 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -643,7 +643,7 @@ This callback is **not** called when there is an open order (either buy or sell) Additional Buys are ignored once you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`, but the callback is called anyway looking for partial exits. -Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible. +Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. Modifications to leverage are not possible, and the stake-amount is assumed to be before applying leverage. !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. From 4e920e9c5381dd7d54e9e5c0d400cb1a058b4cf4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Sep 2022 06:41:16 +0200 Subject: [PATCH 12/32] Reduce verbosity of sending-message --- freqtrade/rpc/rpc_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index e286487ff..e3b31d225 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -67,7 +67,7 @@ class RPCManager: 'status': 'stopping bot' } """ - if msg.get('type') is not RPCMessageType.ANALYZED_DF: + if msg.get('type') not in (RPCMessageType.ANALYZED_DF, RPCMessageType.WHITELIST): logger.info('Sending rpc message: %s', msg) if 'pair' in msg: msg.update({ From ac229b7a429c185c1b52ca01d21c03414cd0dcc5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Sep 2022 07:10:00 +0200 Subject: [PATCH 13/32] Reduce message consumer verbosity --- freqtrade/rpc/external_message_consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/external_message_consumer.py b/freqtrade/rpc/external_message_consumer.py index dcfe1d109..f5ba4b490 100644 --- a/freqtrade/rpc/external_message_consumer.py +++ b/freqtrade/rpc/external_message_consumer.py @@ -284,7 +284,7 @@ class ExternalMessageConsumer: logger.error(f"Empty message received from `{producer_name}`") return - logger.info(f"Received message of type `{producer_message.type}` from `{producer_name}`") + logger.debug(f"Received message of type `{producer_message.type}` from `{producer_name}`") message_handler = self._message_handlers.get(producer_message.type) From 388a572cb38791f48c0f85cff396ee8c1e1df3bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Sep 2022 07:17:38 +0200 Subject: [PATCH 14/32] Version bump develop version --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 77c305c66..1e62266a8 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2022.9.dev' +__version__ = '2022.10.dev' if 'dev' in __version__: try: From 80d0e66b48a2aa35d1ca50cc44a73da5fd6bf4ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Sep 2022 07:19:16 +0200 Subject: [PATCH 15/32] Update log level in test --- tests/rpc/test_rpc_emc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/rpc/test_rpc_emc.py b/tests/rpc/test_rpc_emc.py index 2649c5460..28adc66b9 100644 --- a/tests/rpc/test_rpc_emc.py +++ b/tests/rpc/test_rpc_emc.py @@ -276,6 +276,8 @@ async def test_emc_create_connection_error(default_conf, caplog, mocker): async def test_emc_receive_messages_valid(default_conf, caplog, mocker): + caplog.set_level(logging.DEBUG) + default_conf.update({ "external_message_consumer": { "enabled": True, From 00965d8c069eaf82c5d42c19d3b4b9901dae2183 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Sep 2022 20:20:22 +0200 Subject: [PATCH 16/32] Default to assume stored data only contains complete candles closes #7468 --- freqtrade/data/converter.py | 3 +-- freqtrade/data/history/history_utils.py | 2 +- freqtrade/data/history/idatahandler.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 67461973f..98ed15489 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -47,8 +47,7 @@ def ohlcv_to_dataframe(ohlcv: list, timeframe: str, pair: str, *, def clean_ohlcv_dataframe(data: DataFrame, timeframe: str, pair: str, *, - fill_missing: bool = True, - drop_incomplete: bool = True) -> DataFrame: + fill_missing: bool, drop_incomplete: bool) -> DataFrame: """ Cleanse a OHLCV dataframe by * Grouping it by date (removes duplicate tics) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 6a6e29429..93534e919 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -26,7 +26,7 @@ def load_pair_history(pair: str, datadir: Path, *, timerange: Optional[TimeRange] = None, fill_up_missing: bool = True, - drop_incomplete: bool = True, + drop_incomplete: bool = False, startup_candles: int = 0, data_format: str = None, data_handler: IDataHandler = None, diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index c2d92fc4f..80e29f4c0 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -275,7 +275,7 @@ class IDataHandler(ABC): candle_type: CandleType, *, timerange: Optional[TimeRange] = None, fill_missing: bool = True, - drop_incomplete: bool = True, + drop_incomplete: bool = False, startup_candles: int = 0, warn_no_data: bool = True, ) -> DataFrame: From b4fb28e4ef992f9ec44f2814f83dc5fbf7eb10de Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Sep 2022 20:23:56 +0200 Subject: [PATCH 17/32] Update tests for new dataload strategy --- tests/data/test_datahandler.py | 4 ++-- tests/data/test_entryexitanalysis.py | 4 ++-- tests/data/test_history.py | 8 +++---- tests/optimize/test_backtesting.py | 32 ++++++++++++++-------------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index 8e1b0050a..5d6d60f84 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -139,10 +139,10 @@ def test_jsondatahandler_ohlcv_purge(mocker, testdatadir): def test_jsondatahandler_ohlcv_load(testdatadir, caplog): dh = JsonDataHandler(testdatadir) df = dh.ohlcv_load('XRP/ETH', '5m', 'spot') - assert len(df) == 711 + assert len(df) == 712 df_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', candle_type="mark") - assert len(df_mark) == 99 + assert len(df_mark) == 100 df_no_mark = dh.ohlcv_load('UNITTEST/USDT', '1h', 'spot') assert len(df_no_mark) == 0 diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py index 09fbe9957..588220465 100755 --- a/tests/data/test_entryexitanalysis.py +++ b/tests/data/test_entryexitanalysis.py @@ -124,8 +124,8 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, tmp assert '0' in captured.out assert '0.01616' in captured.out assert '34.049' in captured.out - assert '0.104104' in captured.out - assert '47.0996' in captured.out + assert '0.104411' in captured.out + assert '52.8292' in captured.out # test group 1 args = get_args(base_args + ['--analysis-groups', "1"]) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 5642442b2..e7e3d4063 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -377,8 +377,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None: td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(data['UNITTEST/BTC']) - # Shift endtime with +5 - as last candle is dropped (partial candle) - end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5) + # Shift endtime with +5 + end_real = arrow.get(data['UNITTEST/BTC'].iloc[-1, 0]) assert log_has(f'UNITTEST/BTC, spot, 5m, ' f'data ends at {end_real.strftime(DATETIME_PRINT_FORMAT)}', caplog) @@ -447,7 +447,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None: ) min_date, max_date = get_timerange(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' - assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' + assert max_date.isoformat() == '2017-11-14T22:59:00+00:00' def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None: @@ -470,7 +470,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) min_date, max_date, timeframe_to_minutes('1m')) assert len(caplog.record_tuples) == 1 assert log_has( - "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", + "UNITTEST/BTC has missing frames: expected 14397, got 13681, that's 716 missing values", caplog) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index bd87b2b42..907e97fb7 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -80,7 +80,7 @@ def load_data_test(what, testdatadir): data.loc[:, 'close'] = np.sin(data.index * hz) / 1000 + base return {'UNITTEST/BTC': clean_ohlcv_dataframe(data, timeframe='1m', pair='UNITTEST/BTC', - fill_missing=True)} + fill_missing=True, drop_incomplete=True)} # FIX: fixturize this? @@ -323,7 +323,7 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) processed = backtesting.strategy.advise_all_indicators(data) - assert len(processed['UNITTEST/BTC']) == 102 + assert len(processed['UNITTEST/BTC']) == 103 # Load strategy to compare the result between Backtesting function and strategy are the same strategy = StrategyResolver.load_strategy(default_conf) @@ -1165,9 +1165,9 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Parameter --enable-position-stacking detected ...' ] @@ -1244,9 +1244,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Parameter --enable-position-stacking detected ...', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy StrategyTestV2', @@ -1355,9 +1355,9 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Backtesting with data from 2017-11-14 21:17:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Parameter --enable-position-stacking detected ...', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy StrategyTestV2', @@ -1371,7 +1371,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat assert 'EXIT REASON STATS' in captured.out assert 'DAY BREAKDOWN' in captured.out assert 'LEFT OPEN TRADES REPORT' in captured.out - assert '2017-11-14 21:17:00 -> 2017-11-14 22:58:00 | Max open trades : 1' in captured.out + assert '2017-11-14 21:17:00 -> 2017-11-14 22:59:00 | Max open trades : 1' in captured.out assert 'STRATEGY SUMMARY' in captured.out @@ -1503,9 +1503,9 @@ def test_backtest_start_nomock_futures(default_conf_usdt, mocker, 'Parameter -i/--timeframe detected ... Using timeframe: 1h ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2021-11-17 01:00:00 ' - 'up to 2021-11-21 03:00:00 (4 days).', + 'up to 2021-11-21 04:00:00 (4 days).', 'Backtesting with data from 2021-11-17 21:00:00 ' - 'up to 2021-11-21 03:00:00 (3 days).', + 'up to 2021-11-21 04:00:00 (3 days).', 'XRP/USDT, funding_rate, 8h, data starts at 2021-11-18 00:00:00', 'XRP/USDT, mark, 8h, data starts at 2021-11-18 00:00:00', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', @@ -1616,9 +1616,9 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, 'Parameter --timeframe-detail detected, using 1m for intra-candle backtesting ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2019-10-11 00:00:00 ' - 'up to 2019-10-13 11:10:00 (2 days).', + 'up to 2019-10-13 11:15:00 (2 days).', 'Backtesting with data from 2019-10-11 01:40:00 ' - 'up to 2019-10-13 11:10:00 (2 days).', + 'up to 2019-10-13 11:15:00 (2 days).', f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', ] @@ -1719,7 +1719,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', 'Loading data from 2017-11-14 20:57:00 ' - 'up to 2017-11-14 22:58:00 (0 days).', + 'up to 2017-11-14 22:59:00 (0 days).', 'Parameter --enable-position-stacking detected ...', ] @@ -1732,7 +1732,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda 'Running backtesting for Strategy StrategyTestV2', 'Running backtesting for Strategy StrategyTestV3', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', - 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).', + 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:59:00 (0 days).', ] elif run_id == '2' and min_backtest_date < start_time: assert backtestmock.call_count == 0 @@ -1745,7 +1745,7 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda 'Reusing result of previous backtest for StrategyTestV2', 'Running backtesting for Strategy StrategyTestV3', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', - 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:58:00 (0 days).', + 'Backtesting with data from 2017-11-14 21:17:00 up to 2017-11-14 22:59:00 (0 days).', ] assert backtestmock.call_count == 1 From 34951f59d2c54aaf6bcb02d8bf6810f544dca877 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 06:44:19 +0200 Subject: [PATCH 18/32] Update failing tests --- tests/data/test_btanalysis.py | 2 +- tests/strategy/test_interface.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index dab76d0cb..ec7b457ea 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -235,7 +235,7 @@ def test_calculate_market_change(testdatadir): data = load_data(datadir=testdatadir, pairs=pairs, timeframe='5m') result = calculate_market_change(data) assert isinstance(result, float) - assert pytest.approx(result) == 0.00955514 + assert pytest.approx(result) == 0.01100002 def test_combine_dataframes_with_mean(testdatadir): diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 070e78b1d..294021c83 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -288,7 +288,7 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None: data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, fill_up_missing=True) processed = strategy.advise_all_indicators(data) - assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed + assert len(processed['UNITTEST/BTC']) == 103 def test_populate_any_indicators(default_conf, testdatadir) -> None: @@ -300,7 +300,7 @@ def test_populate_any_indicators(default_conf, testdatadir) -> None: processed = strategy.populate_any_indicators('UNITTEST/BTC', data, '5m') assert processed == data assert id(processed) == id(data) - assert len(processed['UNITTEST/BTC']) == 102 # partial candle was removed + assert len(processed['UNITTEST/BTC']) == 103 def test_freqai_not_initialized(default_conf) -> None: From 2d2ff2fff6c45c366e58289ea668c71ccc57da53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 07:02:18 +0200 Subject: [PATCH 19/32] remove unnecessary assignments and comments --- freqtrade/freqtradebot.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e912fa832..175f6f148 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1393,16 +1393,10 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = None logger.info(f'{side} Order timeout for {trade}.') else: - # if trade is partially complete, edit the stake details for the trade - # and close the order - # cancel_order may not contain the full order dict, so we need to fallback - # to the order dict acquired before cancelling. - # we need to fall back to the values from order if corder does not contain these keys. + # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates + # to the trade object trade.amount = filled_amount - # * Check edge cases, we don't want to make leverage > 1.0 if we don't have to - # * (for leverage modes which aren't isolated futures) - trade.stake_amount = trade.amount * trade.open_rate / trade.leverage self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None @@ -1442,8 +1436,6 @@ class FreqtradeBot(LoggingMixin): trade.close_rate_requested = None trade.close_profit = None trade.close_profit_abs = None - trade.close_date = None - trade.is_open = True trade.open_order_id = None trade.exit_reason = None cancelled = True From 561600e98ba85da1992bc69dbccda844d77580ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 07:05:21 +0200 Subject: [PATCH 20/32] Remove false test statements a trade is ONLY closed on `.close()` - which will only happen once the last order has been filled. --- tests/test_freqtradebot.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5fe4d4011..02a3b7cf6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2673,7 +2673,6 @@ def test_manage_open_orders_exit_usercustom( open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade_usdt.close_profit_abs = 0.001 - open_trade_usdt.is_open = False Trade.query.session.add(open_trade_usdt) Trade.commit() @@ -2687,7 +2686,6 @@ def test_manage_open_orders_exit_usercustom( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 1 - assert open_trade_usdt.is_open is False assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 @@ -2697,7 +2695,6 @@ def test_manage_open_orders_exit_usercustom( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 1 - assert open_trade_usdt.is_open is False assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 @@ -2707,7 +2704,6 @@ def test_manage_open_orders_exit_usercustom( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 2 - assert open_trade_usdt.is_open is True assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 @@ -2755,7 +2751,6 @@ def test_manage_open_orders_exit( open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime open_trade_usdt.close_profit_abs = 0.001 - open_trade_usdt.is_open = False open_trade_usdt.is_short = is_short Trade.query.session.add(open_trade_usdt) @@ -2796,7 +2791,6 @@ def test_check_handle_cancelled_exit( open_trade_usdt.open_date = arrow.utcnow().shift(hours=-5).datetime open_trade_usdt.close_date = arrow.utcnow().shift(minutes=-601).datetime - open_trade_usdt.is_open = False open_trade_usdt.is_short = is_short Trade.query.session.add(open_trade_usdt) From 7dd984e25e5472f0a2fa069b81bc6f520e3ac1cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 07:13:57 +0200 Subject: [PATCH 21/32] Simplify cancel_entry --- freqtrade/freqtradebot.py | 7 ++----- tests/test_freqtradebot.py | 2 ++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 175f6f148..83089152a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1343,11 +1343,12 @@ class FreqtradeBot(LoggingMixin): replacing: Optional[bool] = False ) -> bool: """ - Buy cancel - cancel order + entry cancel - cancel order :param replacing: Replacing order - prevent trade deletion. :return: True if order was fully cancelled """ was_trade_fully_canceled = False + side = trade.entry_side.capitalize() # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: @@ -1374,7 +1375,6 @@ class FreqtradeBot(LoggingMixin): corder = order reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - side = trade.entry_side.capitalize() logger.info('%s order %s for %s.', side, reason, trade) # Using filled to determine the filled amount @@ -1388,15 +1388,12 @@ class FreqtradeBot(LoggingMixin): was_trade_fully_canceled = True reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" else: - # FIXME TODO: This could possibly reworked to not duplicate the code 15 lines below. self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None logger.info(f'{side} Order timeout for {trade}.') else: # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates # to the trade object - trade.amount = filled_amount - self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 02a3b7cf6..7c7132bdd 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2998,6 +2998,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ trade.open_rate = 200 trade.is_short = False trade.entry_side = "buy" + trade.amount = 100 l_order['filled'] = 0.0 l_order['status'] = 'open' trade.nr_of_successful_entries = 0 @@ -3086,6 +3087,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order trade.entry_side = "buy" trade.open_order_id = "open_order_noop" trade.nr_of_successful_entries = 0 + trade.amount = 100 l_order['filled'] = 0.0 l_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] From f6a0d677d2050dba3e805c4a69c5cbb65941e695 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 07:23:50 +0200 Subject: [PATCH 22/32] Remove pointless notification assignment --- freqtrade/freqtradebot.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 83089152a..532d5d3d8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1692,11 +1692,6 @@ class FreqtradeBot(LoggingMixin): 'stake_amount': trade.stake_amount, } - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - # Send the message self.rpc.send_msg(msg) From 0d8dfc1a922a8fb6550f94a449f1c56c7e63ef5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 13:47:26 +0200 Subject: [PATCH 23/32] Force joblib update via setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0581081fa..d3f9ea7c0 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ setup( 'pandas', 'tables', 'blosc', - 'joblib', + 'joblib>=1.2.0', 'pyarrow; platform_machine != "armv7l"', 'fastapi', 'uvicorn', From cc06c60fd8f8d45cc9b4643d005dcef209351d57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 15:43:05 +0200 Subject: [PATCH 24/32] Fix pandas deprecation warnings from freqAI --- freqtrade/freqai/data_kitchen.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/data_kitchen.py b/freqtrade/freqai/data_kitchen.py index f4fa4e5fd..400e70fc8 100644 --- a/freqtrade/freqai/data_kitchen.py +++ b/freqtrade/freqai/data_kitchen.py @@ -210,7 +210,7 @@ class FreqaiDataKitchen: filtered_df = unfiltered_df.filter(training_feature_list, axis=1) filtered_df = filtered_df.replace([np.inf, -np.inf], np.nan) - drop_index = pd.isnull(filtered_df).any(1) # get the rows that have NaNs, + drop_index = pd.isnull(filtered_df).any(axis=1) # get the rows that have NaNs, drop_index = drop_index.replace(True, 1).replace(False, 0) # pep8 requirement. if (training_filter): const_cols = list((filtered_df.nunique() == 1).loc[lambda x: x].index) @@ -221,7 +221,7 @@ class FreqaiDataKitchen: # about removing any row with NaNs # if labels has multiple columns (user wants to train multiple modelEs), we detect here labels = unfiltered_df.filter(label_list, axis=1) - drop_index_labels = pd.isnull(labels).any(1) + drop_index_labels = pd.isnull(labels).any(axis=1) drop_index_labels = drop_index_labels.replace(True, 1).replace(False, 0) dates = unfiltered_df['date'] filtered_df = filtered_df[ @@ -249,7 +249,7 @@ class FreqaiDataKitchen: else: # we are backtesting so we need to preserve row number to send back to strategy, # so now we use do_predict to avoid any prediction based on a NaN - drop_index = pd.isnull(filtered_df).any(1) + drop_index = pd.isnull(filtered_df).any(axis=1) self.data["filter_drop_index_prediction"] = drop_index filtered_df.fillna(0, inplace=True) # replacing all NaNs with zeros to avoid issues in 'prediction', but any prediction @@ -808,7 +808,7 @@ class FreqaiDataKitchen: :, :no_prev_pts ] distances = distances.replace([np.inf, -np.inf], np.nan) - drop_index = pd.isnull(distances).any(1) + drop_index = pd.isnull(distances).any(axis=1) distances = distances[drop_index == 0] inliers = pd.DataFrame(index=distances.index) From bd664580fbd3459884d61c6db46cff37e366bfb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 15:07:18 +0200 Subject: [PATCH 25/32] Don't unnecessarily reset order_id --- freqtrade/freqtradebot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 532d5d3d8..387bae534 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1345,7 +1345,7 @@ class FreqtradeBot(LoggingMixin): """ entry cancel - cancel order :param replacing: Replacing order - prevent trade deletion. - :return: True if order was fully cancelled + :return: True if trade was fully cancelled """ was_trade_fully_canceled = False side = trade.entry_side.capitalize() @@ -1389,14 +1389,12 @@ class FreqtradeBot(LoggingMixin): reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" else: self.update_trade_state(trade, trade.open_order_id, corder) - trade.open_order_id = None logger.info(f'{side} Order timeout for {trade}.') else: # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates # to the trade object self.update_trade_state(trade, trade.open_order_id, corder) - trade.open_order_id = None logger.info(f'Partial {trade.entry_side} order timeout for {trade}.') reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" From d462f4029998a8ff22d98aa7763851248baecb2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Sep 2022 15:26:34 +0200 Subject: [PATCH 26/32] Simple test improvements --- tests/test_freqtradebot.py | 40 +++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7c7132bdd..0f1a05ab4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2661,6 +2661,7 @@ def test_manage_open_orders_exit_usercustom( rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) + mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.0) et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2744,7 +2745,8 @@ def test_manage_open_orders_exit( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=limit_sell_order_old), - cancel_order=cancel_order_mock + cancel_order=cancel_order_mock, + get_min_pair_stake_amount=MagicMock(return_value=0), ) freqtrade = FreqtradeBot(default_conf_usdt) @@ -3117,20 +3119,21 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: amount=2, exchange='binance', open_rate=0.245441, - open_order_id="123456", + open_order_id="sell_123456", open_date=arrow.utcnow().shift(days=-2).datetime, fee_open=fee.return_value, fee_close=fee.return_value, close_rate=0.555, close_date=arrow.utcnow().datetime, exit_reason="sell_reason_whatever", + stake_amount=0.245441 * 2, ) trade.orders = [ - Order( + Order( ft_order_side='buy', ft_pair=trade.pair, - ft_is_open=True, - order_id='123456', + ft_is_open=False, + order_id='buy_123456', status="closed", symbol=trade.pair, order_type="market", @@ -3143,15 +3146,33 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: order_date=trade.open_date, order_filled_date=trade.open_date, ), + Order( + ft_order_side='sell', + ft_pair=trade.pair, + ft_is_open=True, + order_id='sell_123456', + status="open", + symbol=trade.pair, + order_type="limit", + side="sell", + price=trade.open_rate, + average=trade.open_rate, + filled=0.0, + remaining=trade.amount, + cost=trade.open_rate * trade.amount, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ), ] - order = {'id': "123456", + order = {'id': "sell_123456", 'remaining': 1, 'amount': 1, 'status': "open"} reason = CANCEL_REASON['TIMEOUT'] + send_msg_mock.reset_mock() assert freqtrade.handle_cancel_exit(trade, order, reason) assert cancel_order_mock.call_count == 1 - assert send_msg_mock.call_count == 2 + assert send_msg_mock.call_count == 1 assert trade.close_rate is None assert trade.exit_reason is None @@ -3177,8 +3198,9 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch( - 'freqtrade.exchange.Exchange.cancel_order_with_result', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=0.0) + mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', + side_effect=InvalidOrderException()) freqtrade = FreqtradeBot(default_conf_usdt) From fad90269391fb35a92c3fc03fa24d3b51720bc28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 08:35:51 +0200 Subject: [PATCH 27/32] Update updating docs closes #7507 --- docs/updating.md | 9 +++++++++ docs/windows_installation.md | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/updating.md b/docs/updating.md index 8dc7279a4..893bc846e 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -37,3 +37,12 @@ pip install -e . # Ensure freqUI is at the latest version freqtrade install-ui ``` + +### Problems updating + +Update-problems usually come missing dependencies (you didn't follow the above instructions) - or from updated dependencies, which fail to install (for example TA-lib). +Please refer to the corresponding installation sections (common problems linked below) + +Common problems and their solutions: + +* [ta-lib update on windows](windows_installation.md#2-install-ta-lib) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 9fbbf8250..5cfae8c10 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -34,7 +34,7 @@ python -m venv .env .env\Scripts\activate.ps1 # optionally install ta-lib from wheel # Eventually adjust the below filename to match the downloaded wheel -pip install --find-links build_helpers\ TA-Lib +pip install --find-links build_helpers\ TA-Lib -U pip install -r requirements.txt pip install -e . freqtrade From 545d65235261563dbc4b482dea82e2d76f0c5440 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:02:05 +0200 Subject: [PATCH 28/32] Update okx exception wording --- freqtrade/exchange/okx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 2db5fb6a9..fe1c94017 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -78,7 +78,7 @@ class Okx(Exchange): raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e From a96aa568bfa81c7c78ba60cc676483f3903a2d74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:21:40 +0200 Subject: [PATCH 29/32] Add binance futures mode checks closes #7505 --- docs/assets/binance_futures_settings.png | Bin 0 -> 81500 bytes docs/exchanges.md | 19 ++++++++++---- freqtrade/exchange/binance.py | 31 +++++++++++++++++++++++ tests/exchange/test_binance.py | 18 +++++++++++++ tests/exchange/test_ccxt_compat.py | 1 + 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 docs/assets/binance_futures_settings.png diff --git a/docs/assets/binance_futures_settings.png b/docs/assets/binance_futures_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..a3f7a2c7032d54c80b2206bad0a38cd5c7729d2a GIT binary patch literal 81500 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ℜPfmEGeOXY2CBrYzI?vOku=IV<o9Ii1%>ViNqZ-!6eiN?(LKZ2 z?4LgeI{Y(IV|I2nIILz+uwl3fQw^{BG#6D+(bv6D5xpcUZD*>gPEJ^$d@4qC^vI+0 zV=J@#%03>maCPHd1vuYdL*J&RTsd7XL2C5#r~YKAb^z(v(GrD}_;Y0BjTd|gWv-c7 z!ZUKZNE`!x9m{vC_f8TLm|O%h;Fg3VU$ z9*FJM*Rw_#5Wq?7b4Tqbw6wchYq=d#%xWq=!ewAq{c6A{O`~q9d?`vUz4G+S(@+*1 zx*b*UKG9vS*KywmCh3$d-tBi8!{P^%PQPiDayc%kS|A=LvF5R`;+`x2o85n4827OxCWm_GoQ()@?;0>XLDyn$zc0YRAc%Lmy(>%^i)2XV~ zZt7lzYUJD)Poj-FF2%=hSZVsWIxc*{qD?o$=CE358eVaBFHxynZwwD5){JMiX{pLG z98A|u4v>ayPp*z9b$m3-MmN^i)A;P}>Ot*rKT)*7NPw)#jsk&0l2)ZSmB;BO#inWG zQG{f7Yiszd>3jnZrF$@AkcN)VcOf&LK|4L3Lwkah*ZIuWe4GAM^?AUDqRqJ>xyRjy z=A@(-Nm3>gNvz@F_Y@_HuaP?=_BB~qHU=|VkB*j~Icx<#+W*z*umP(`di9Hm*+Za~U8Ks8!Dp_ufNrVuLFPh4Ossn|-7CG_NJ;6t z)pW)5cjEU4IDG@{p8LEpH~M%8=+DrIcWebtkg#9zG&B_2yca>JKkQQtx>;qSPcU{{ zBD!P#Jo5W*83IM@JIj2yt^JRG@`>*~D#HI8BeEu-5)%ggAz_XLXh_}Wc;UwvY=H0@ zg!2zfd9y9>u%>HK-D{Ah@b8;1@hmPyF*|*CO$Mu8xp7#ge(`2wK_-}BPGNH{%{7nkZNVj9}RgBo}&hut?_4_sQ&Iwy z+{Y5Pu3uo6+#Y%D6a@JA99L^G=w2>sjEVy5gH?YgVker{Ph=k!bv>$Nwv{Ge`abDr!Xo6e;nC#M&E@MpC|{eB@Zaxy$MJnA1#R8&Ya zkOr&Zv#srWQKgLjx8rx{aIHEe7%yO^yZnLA^V*g48`cMX2e3+DO0yu|Xg3-4fLd*0 zV&dldnqKP+_x&pz&o9sZn+q^$Uk2X>e5?%&4AJizT61!y1xfTC?g9vS2ZC3-A-&l_ zL7cTsdW|N_`vxB~Hm}_pzGUXd#(sQhS)@&%fQpIlQAv{F&ZGYUIjD0R0p_Jmwdm8D zQ{dvk`$Ru%+|q4#-^KZGL0#9>nDIAKzhnA=HIb~~ZK~XqV_s<3rjhPz3AYPVmtGfU0iSAMRPof_Pa_N{HDh+(*_5QaHSf-5_RRVX`o?SNB8>r1GFaq9RUi z^$H-lg{WK+rhO@ouz`K0>~T6v&{{@-w}F;k@F^-#T3>B-OiZXi7|1C=Z&&y6#$INi zlfgJaC2!oUN(fjo&M0Xp8v}W4A8}jp^P!0s{r%0L^0K5k`fbN&<$kkH%MFUhc)(Z^ znx3YP`$Vr;@^g60nSd8Iwrs#7TL5&hwb(4qws_H+?8EF>1NF6#tX&f9B*oHXTuweN z270x6C_wE($Hw+>Y%{miJ&G2quQ_hpXJLzkN1(|h0NC*(8oSd+*oa76OXgXJFn4^bt z%f(-4q+QG*VZn5mxIN}K`}Jq-2~Q{4VCnQ4iehFwJ)e9CT)hH${`CfanOyq;363VvqXxVE{j1x{ z{bsk(sh?&Q?){B!lVxDcuCCEzY)t8Js)gO59oT1oV=$fske|XP#L+$#Ly&3aRs3?@ zf6WYmFv9f1+&0W)_(mz<`)eG^ylf1_l{P;wnre5j4}rrYXQ}RNqYI}*4AWM|!l2+| zRJuUQ?oyNk_3hEmC(tj)=@4)W&@a2uc={Ahtc2IDxM+e zL<&BP4W!ND;qQovMYt^ow3ce$GMvBJURmi~=Xuy{wCS{Zix_*nxv5j<$jy7%Ypm*0 z8x|&|9u!8Se|-`M3X#u`$1W71)hEGurduZId~T^cuL&_v!5)28fB>n`>8i^`)n3oq z+&zJ(kWB)RF%X(KOApBTu@ohp(h>tR0+?*$%5mJ|%^l`x77>wWy_CuMI(*9y?YHIP zmNqu0OZW*)H9H0u*C5Z5A7<_p35f4T9NKjqT~14SQZXqx;waSqyA*P zxm$K`wU6|82TtaaqOxi}K3}&^jgm)Zz}8v8Y`L1uL;R8`K*mo_-p9tl!$nENNXNJG zCpZiGWqG~IC3Cws8VNLeW8T3`2T~pGu3qx;He%6c+*zNB%1eH>)bjkWq85t2C(BW+ zd%ea=-~YSPRQ59)9-wpZu>I}a02^3c*$#yHUq?sEMGp$f>cb@!1Zig{$N#WB+tiDl zBAdH&n=~*CAA9$g@tsCwDXOo32I|_Z?Ntc=~J9|_N``_?fY<{tF_h0XEAE!cfkNPiX;~N zMEFUZz~f+Vn?r&Z%9cq5noK)Ii~@%{JLw?M<)uH2=GqL&QLt3}XvB+-erM&b0fO_F zJH7&?WUu27z}m^)p2jI305)4ge#yx@492RCN?}^0yq;S^DcL2(RaHMO73Vsq-_g-| z4yLDl%2AkE@V*-I`yL!C6+kTD_@gNkP#w_#Lq^9~*|@f_vFdDM!=|+yk0VJ*S^2Lq z3k1$yLJSO{tn=Hh-v;H5?#*r?&tp5s@afRE&v`gftfI{T!8eu9jg5>9vm@{f$Nl*C zQ%1%^w^M8k1QY@C%i088AOj^m)(RG3e2_;pIRItENve5DWIS%yEu^qM7d@o8@&$h! zp)imAH0yF-v05Xau+0Yw?z)y1hRYqS{}+75773~Jy-j{e!M2jKPLu1yyQx*+%6SmA{rHOHxxq;VX!SmgY5bePo$Ut12*E(qEQNQ_0)j?ip+(P3ZZPsRDuh z*$soGbQ<{WpTx#0GH$MEwZ_w0*|d66=T(nS`QZ^~0E&_0`X``12q+OfJuQjJEW>-K ze&oS6Ef1O5*=hw!8=Lb(=GE|in{jvgp z*OSATJCHq`IsZ&9L9=!evyvILBnOz4fD+}$76pj8B!6dX4BvY%fb<)b!sZ^g;rNpP z9+VEMU`%JEq5`!7T-#aI*?gl*NaxDILFxVN#n*tq`i6${xGyAR5J(0^vHjYLKw_&B zB0fiRbe2ib+7Hya=|<8zGr6rj(Q(!oL8I%MI*#?yZ`ma3AKa~U}TNXDjTXJ=~U z8g;J6D`V!P-Qkif8Hf)D9*-E$!m5v#XD(;lO@EqKCs&$Q5_vf;fJ%UoeajFA&IfjO zc7@2ewB84#!}J0Jt8pJY{bf)#>$vUrC+`%FjA*itD|<241oFtnkn{5L7BEn?s$9S zD1b@_;b(r_(2Knd2yZaZyp5S9n2j4Zva`l zv!5{YpIm0;XA&_xMC|N;UqBXdy~8~(Ejjtmzi-_eWGNKw?Ug8PSj`Sc?{fLH|6W?c z08pe6OMtX=u5j*RLc3mFT|>*s4t}m6{C~qxiY5rx{p#N2B!6vS_Bd(~E?W$4esl9Z z0I@;C(e~s|c}&d8x1(=~iBoU3StrYGmr5`F6RK*tdASX7tu;p5KayjMd6((V617M7rCylS9P&mv`CtP!C;@SL*npTuM2Bx5Fr7r%V^^WUY9&+)@Mc1 zf*)(Gx;rE3$MI2!?@xupMOi`Kb9Ic_7n!VCYrX$hofhFeBHoA)B4FsnPnfg_>ABM^ zd4zoQVjYzxkMSPOPLx_z;9OCO4h;=$_J49d7QMT*tnbBv<=+E#gt2kjLftWK42f4s z_&Zd2FYS_rAHYU{IVUVQMCbaCA|$8g%1TZS>;py}smR#?<+0AGXzIHGton03Vl$}# zL;H6Sem-M!$UjPHqkT^&I?t8XMGw2P2EzSz^9fwP|cxjM?!^y*4A<~W|S9iPP8ci z4IC7FVRWXI6$*-!on4)cb#*eSY}{O2S10QPFewO#_VZCLWW70^pXdMGL1tjEl7EJ# zE+((P94jl(f#Z7eG+kEUKJR%C^}Q}K)V6F3I_C}9+0O~kD9O403@o%X&)Xy44Q-#_ zWQn2E>mILteO@3SAOL!wWu7YqAt&|_MyLkkx>4)dq$ec;@MKA5X6x?X-a+@rv}P{^tcZL9i$~mOplPftc#& zUxEPVh&b=5!EgKxXI8sX`Qd-X!D4N~V5g@xQ?s+CeK1Em=jUk8wa@QD+wXVP$t|25 z931TJ;eEe;{YVkpV^Yq9QVFnFO;oFD^B+FD=m90U#0c+SaiMP7a&bk>*AQxIY65do zI3T0)=g%J?pLpKO9qVMV{H6Pg@CUbnffv|xy;{HivLFElPqB}UP}E|G_*jM2t}JMK z*EKY7yhv%CZyNRtr^PIhxm|p;)M@bStTG>!{Pf8h8yn#YNg>F2Z>qs)JEV3|Cu@8Q zv{1P1uk;=h;Gz8So&8zwfy9h@kiM~~cY|d1HS#WP1lsVmgDEPa@J(ORzyQOmV57m( zG){NbgsDo?JT1};UKgUsP4c);kpu!}c)f0S!Ib%Ny8x$=v{-jn^!pAv;Nhs}DwMeM zjK~27-4`VQ#n~Lxb-g-*J5?qEJ~a;~lL?`rA^G%h-VfV8|5CWL*47M{RZ-)d{*;a4 ziO2s3xD9GsJ&uW01Vy+F)#)YNQbO7+k!HnSN$ukB_PJ`AmVny{E~KQYY@*9mYrn1- zp(Up8FbYTr8S60Ulcc0du^PA2&Kc*TYbT%Af4x(0Vc~mF`J0$97_lU&n+~vX;F->Y z<=nq$t<~V@rZMBF9t3K_j+`8TB>=%Y%1C0L{qtI2B&DOX0F2WzG|yLNZJI5LgdA${ zW-LbAd?$#JL7e-RyptRjcAeq%upWrIYcm@GNC{pm!Fd4WTrAkTb9!9JOZVm=Gi+~Z zZltSNH_~-z^-Y%xPX2wFVS%M(*h7 zq+DQ3^M$09eUwKh9w{n1yvOJ{z6JCvPKJmpP#?m@#R+ML>tJeg-{d!d5~u5mJNrHq zJvp2hIY5>cf~EQf+iX>)-0-(NVYBSjN_L1{mJ{HfN?Ruz?uy{8^TLJZ=ED6C^Sc29 zbL}#?V|j^7I;*~)zUtXX_wb9z$hN0iZBRpwkx0SM%np{pp?c9_)2Gq(-6-P?w_5hZ zc=0?b8%jljDl?PjY!&J0gpM_`c^q?ixtToNFyLAori8o>5@=EkVd+@q`SKXq`Nx~-Ob&ozq{7`zI)GN@elBF&g^+-o_VHt zp_GIMujqNdbBFuS#TPF%iuu%) zExPk4V;e6r@C}lLxLovjzu;5?PZd}IpcuY7J2)qwiv3JY8s~jK?|D!v<)wb2jEEj? z?%Glh_Q$IEB!<8s-^0{&A5%-JBPnr8K_?U&_GD8xKJ5o)@CyK(uwjU5msc~F z`z(<~hwoyg{fvr$dHwEsR4o^8P)!34;q{kEUiNXrBg~)WH}6#S!c-8Re56}v=r5#S z9V)vGF_gX%Vd{%SOnk~ail=n??bs&+k^?4e|I$FuM4jC_&ul(0L`^Me$4nX z5~FN<^mg_CVJQLp?qQ;aLE>YZE=}i{dsB^KCYvRq)O83p8CxWYS=5By;ByUpymQAd zXIQP(8?iw@a*b9_5Doe=#^DwP+dg+9Frv-Tt`r2|S-Z-k~r)rLR$* zW%^pPurHmnuy6hCq13H=aHv{s$24$;C(?i0r`2?j5{bc!I6UH)L~er8;-aoslF)U+ zCj|WNo>vB*(fvOxnW=?@yc!ajQQAmWz%(CeR7-nHDhbo-l9%2^^8L-H)tF6es$e1> z*Y=*Cts+l1QL=-p?Osn+#icg~>1H9(@iwDCxD(WP4 zP$IOAvVo>g$3d#2W)+tPy`RnpT1BM*-9?4PN|AS6zg-OXx(LUiB*pz857i&%#GnY_ zzdd=86kawqyeIW&E_Y#7W6gWigi+H`XCw0>tZG> zybNyId=74nF9-2iFmd$i=H}%1D}KAH(NkCru(+{*KMviOyR6WVk#Xq1yRCyl891j- z4i9A{378I!z!hT^nUr9dm3qK|;&yi(P_$-yS&KdqcgAb<8lN=(I!7@Tf8Fu2zZ!8d zUAP!or&L{d+Z2BRCEdJR40H2wnzjTA>07Ci7e-!}2zr{|g(9T3A;^!n{Yd6rDVV#MY;d3w3HqMI=CTa}L3NU9Ixxo2 z+!aL+4jbY`If8hRb)%2p5rYk(R+8K8U`F4r?Hz2D<@u>FQ5U#-bZ2?lbx(6V?1eAT zc5{0}>ar@It5oZGZ}kO#C)(c8@%#6n%y|R=zlz# zhfrs>M`EQ3ApwRSg^++$1aEy0#L(~G6Z91x0SakCLIKEWKK7eXQTGtsr+uj1%TbMJ zwDFWPdnabJY^!i2YXK;xDG^xFC>FEjlW#{W7r$#?aFL{%k|XVd6U)H6%}a#;^bHVG z`w1*Q`22cS6dmIqL3X8XMRR3t$u0>RxxHEsLi)w|~~!Gu(JiD-Xr)(2jx zu8^q6C^#?imp=;%tjx_>i8C2rf;6#?&VI8)LM*@AL?Q8Lst*bt9gaVr+g0rzEgc>B z1(snN88c2-S%=WLr_9cR&*XO&91~I3NLIE^;}2{iBK_EXr~HEPP-XL2WB`%Uh2foN zKZWwU^~A9aa0#0L#}1L4-{}w3)yuE0BW0Dsx0S7pZ^tgbnHgZrW}keSUsfpiJtm+O z0H5BB^4vz>V1;$Pe}kKclbvI4szH$&=he%X{I`8}f5=3GE;ODuZ!)vqrzI)84f_9h z&4p^A)9Q@FkUp2up&kipiz~1QaNT&RZr{Cm;}}9_puG-KZKz!~W;h`|hK09_$>u6e zQzPk!02Rr8!})lj$2Dq$w!p!stKYbb4d()@$CqGE229cET=mjEZ8PqVW3?GJM4Krr z&o`X-O`Q1aJ)j1b1qOX9Q_H`&XqV;JX2Lo>Edgs4{~u;GH8=tM6u;f=lgi2higQ1J z#VtWWsQX1)Rc;$-BgQjDO^0VwWhUSPZ}7@+?q8i&d&7fSYLHkWo~bfsRKp>`9Ln68 zt~~7mSz}s(pl=UoA1v)KGTBJts{_GZ&dFv>49NY|*3>-lX$4C+$0sHfw_Z6qI`*pO z6X7z0-77jZu3HDGI!ODf#YWqs<=i2>KlkMK7wR_F`#)_8tZx6$N<~bw#U4$u&YQR*P~meEB3+ta3sLIvw?$wL0fk6C&Rm}|E(1D`38z?Jcq${ z`3L%E(Dbd#!|eNX*WNz$0!}mWk{oap9k&nJcaTX@Xv%#&Fm*|~Uj%Kqa`?#^-C=Bf zaZy&Wf16kvO081#T(JlHM|iCCI*5=T$h$S2hV?l@&ccq4PPYRD{V-G5IBC0nuRP(g z-n+a=L=LlsJPyMNNs)XdGHV|N(N=uu=Nkdd-G22uL^#5X0|K1~l1VmTW$ zLf3Xz!^&RTqCID5%_tKju)%DRva<4B2^`aE3s{iOQVRYilXm@T7wJ6}4u_a%SN+9s zFO1*0DTS=*b_A8F>1aA`%zFD2@#$9%8)$ou{Qdduv&8pO{|9->J_dn6zr^S-Ds`SeiY0O^f4iocfxDg@tJ!5oqt|h@P*_ zoPZT1g}7{tQxT2}oc1LXY{LeU1yBGQ8gP+2cGLa-vrGkUu(U;SfcF|K?aZ)MLEP=|HM-WMq6cB@40+-<}dX4)C4dZOM9Cd}uu2_VM!a;s*EJPhYWD zD&l_FN@uLQ@dxReg#53$TU*lgU`%JAe?`pSJ|6dHJKq&|@=IVD|pn7aVF> zw12l-AQ5hrz_ZWUdv`;W7akFF6XIz#Uwhgc)a>?g>QSAV zQBBQE<1&WV|7}!pvENTxOPkhrSKi;ieaCCP(OH4g zu})oymt7%=)}xkZ{jq#ZJlBg?4tWME%bnT+{B$rk-?uX=;_pdf`L|0W>9p;+60xmg z*qjdr6L#oji%?IE2B)X5y4#v`j&_n4T&{z{MGNmKe1#r=ohWo)lh&FWjnC?j<+1MQ z^@Mn(wtbU{d2Q3-j|YPd^i{8oGaGc4Ry_Y64SPmH;=y}A8O4x{PRwDk)Ve^Bv=v)h z3$h{^88}%yJrX^XqM|~Hd9nQ z&gkKscbK@r<0aKNvLBk&)|)aLp(0JLRDKyG?Xpq?o0-;tEKZbd#7VD5Yik-;T=ge# zyB)3OLaRJdQSHbb_pXe8C%EWgF4fdbfpRHhMYPJz%|$~+WsPS*^L0Pj9D2TBVRA?V z&ai2?!j0d>A-}OStYcvj3u)XYy`RiyA2+C~+MT@H?+Lf&bv)_LkO&vJ-(kIL8ATp; zSp-S?!j5Vi?8ZA+sK;GNR#w)d>+>mE+Tr2h#P?L!dG(o@9ZSXFDq7#RJ^MFM4UL5N zZ_Gy5*48o?JmRNDN9~tI^w#&ZChqi~0-8>9)rmZ;9^L%~Jc`#vmQd9q4G9`cOY_Xp zy^oVx=)P;~hS^zJW%jPqISC0w2d_yhl+B@eZ9=l|Iz9R`va(2fcxI=6B^z5=y@#4ZrR?m+TlNP=#}X@* zf##%!SZ3y1*OSebZ6ZF`msC6;A5Hth;JU=%9Vo{qAlyj^jjf^Mf$7S~^zUi$prD`> z)vRl%=wJ~IXK$|deEat8sC4)grsC9G3oL53)qrv^`(`0T#Ao(4O_7PpUx+PV2qI@ZluImsaz-K(BGehemYW{Q`rIpPb-%U9b% z_`xfbhC`;qRqVN7UR1C|@W6mTtgUx9p$GDPy8u z)}a}iTvVk0lJ?CT8k)<@Xw{PZ9z$A2S~#!nZtLW*B`4=~CygS*Gbu06 z=H|~fzQ^N&JR1fT8f~p@vNIR2g4>pIe*X0L4>oplDe$+>)%3W;UFllDAi@%s&QO&L zO+NjS+ztWcl$F)(k5iz9kpNG}r<1T2zbH0R4FruG}d2&eFXv^4!EM-?6x*>q`X28L0GyYqdQD0@Kx zAxv*vSU5SeONXn2)=>Hio}-!!Tp^kI7UI`Ay4x+JE!;Y9`YxxX<$qlu9B=dJ>u)&h z%thzFG1U|D(4Jpv{qpJHX4(`F&1Q>$knl0{>B+v!-E7V9@DEs*EOLk+m!fKP6#q{( ziwVhkP0m+O0<${BCQRbO-4C+jv()LI=~KKAd+Bl1^X}V|ByCo+0rm=Z_ZL^c@;^c3 zBw^dzS7*CWO~YA2Bj;LEtD<5miuCkt7;J5N_XAXPAnDFsSlA3!evg{C`10mk44g3a z7)Nf%>KbuRFF3@0Yb&}xj=iq-r-qg5&)2wC4q@vIIv$|9vRY1`7#o|_?Kc?d>TuhnVE{>P9PNaf^Y^~87)JBK;?PIU00YC63f z7ua|*oH_7{lCrA{-Bo{kS(#3?+s+`dyRxW~=*`K>fnJ-U;)qoYo6FYmz^_wYfNSQX zM;JG>A#84UK(fv%1!bU(K0E}s>BM1YF=%z|6d?rxDzHm9(G_sRTooD>S3>1ly5w!+nQLn_K( zX=%yd{Y%iNyK%B0S?u7TY=}cT3eEu%5t+2;q8Cgm6VMrfrOvbA%Xrs4s*Iq zCrk`a%aU^QX2NUS+OGFWc%5G})AY!tDi&X_u`^#3>7@tg(I#{YjCh zbL;)d7I!D%5uor_7DTF z=LMgXPe}6zZV1q zNdn|YfY;95g97}QCyV57Z*OlEhlfF}>5oG7_xJakVzat&p3s@ZGwdErrJ?x%qNtV5 zAX0vd_Ro*50o|^%-3e_sEsd+3!NFqYi69wq@yR+)Y`{w{yl1v7TL7Tfu)hz4TEXUc zxOgcj7#Wj@$)rePS&V+M%>j6yo0{%fWd#IWPsZ-%l1`U_?Z=lV{cnN z2pQdm?y}!%dwWKL#Y%E|^n|pa?jkBSm9LL4h;L*Q*j!}St){lO^E{bN<%A8DPIkDR zM#snf+{MgFVo>X5k{IJ==<=5{-iJ@E?(bW(gw?WE)YNd>ZOlK%#bpO?tW9PZtNdPrzQAf-IT;&J1RMCxpK;nxo8+=F;zn|7}E z%hw8v|GcWJtHD!I2_g=PEN{d}jEpi(wqaJ)dbUvLOaK$(b?=ctQ}NDa;sQAB2!!_e zg@uev6Ag4z*g-+lCM#s(8^gh#+Aq2(7e{lCQ1_ecjss`b`)bnb^X+f1*tenMbNW>? z6Gc!~^YNX>*ssRN#w{<8j_d2&068V4a+i?>&&ysN1SH>xsHl9Mrnm-XTBiNM1a6S- zioe(-C1JBy*3b|}R*H!36`J8m=5upA-ySo+YZblYy9puUyQ!*LU>hG)R8gt4gpK=> zy3EdxyB;jISn60NHXSr}k-Fu8Kscp%v1TKh@AxDf%+Q;~vD0@8G`g8|*1I^J?Tyvt z*Pk>vE&Om_H#X7YG2Q`D<1fDlFZEtpvi7*Jj_@}}{qb461#XuyPsP%$lXcB$B`CO~ z(jy1hL~(!0@bYkTb2Am*{nnNMUyaSmb=3mDd5@h`e0==*?p%0cA}UZ)QUYr2uIr0< zJkVP3Pae#lK&@_1cWN9QD!>q^rMUa#bmG4DbXHUQLJ5 ziVxIjlVm6&P+(a=_2h}R`emu>7dtTQS@5{d2eHpwiGiE?hsT+(br+itSExoOCae{# z_I$|h8N>xcU3+uAnwih67!n|Hv(&iiDymC4bh#+WhvL-v#(9)_R$0 z$J|a3J3F;qI+TZ)nE^SQ?a|NsXOTE=P`5p^i_24LI!1W1<1g27$m9V0MFE4fATosF z_YfwKM&$LHh|%7SrB&<+lra?9a#&C8e7@ zGL169bm|t^(l)N$;5s&@y=Yn1XuyIAc3CgIz~Z#Lt1EeW81!R#IXUVB`5Y1s6a1xm z$qf_TuV0dOc6CG3ot{2mRKT?|t^?91{rZ()AhCC_^j6!dcS?JC1)IeAAdD}zj%h91 z{)HAck{mXpyaV43f0M)%?yQ-}8wmIf1Aa?nRb}NA(o*8VXOmZwXOmXp(pz)JVB1?D z_6eMb45lFLEC`~YJW>+|y8p(nQA zn+wD;%$Z+voRxGXDZRKp_spADA^ekXt4(nx*Fwt<=dUv?>Eoz!u1l2&A>q3_TE6GM zXtoYQct|G=p9&rd*HRo|KV}jyn%@rEe3$}|A!Sf`b^5v!M_9Ox7|oa2pUhRbjl#@4 zFK`GAlL*VUpWQ%HK;31Y*r~I<9S3Lik)U`oKU0zOT3%6QAc4cwTXa4%me{O$d%KV% zz7UcgJ7|zX+>ak;0#0*au<-2m>@I439b`yiVq)CZ7ap7)T~YPXY|Sw)ZZ<$hMz+Z5 zG|z7GoA@Diz{iR3yrG^KL=p;>3YoIthU#QDK*NfWKB=zA#&|W8MK$9xtx`#YXmjmI zK;X!?i_~*RukIG^UKkk_6*iW44l63)(fR;lXO#WhIaO#?eWizty?x)@IX$nUj#FB( z?$IiZcPOP)(l#*>i~AJ=*l4Y+9N8KCM-b_ML>T@(-_@v~uTbMsL0&#gC4bTW%>%iR z(gEXs-C1LY3U+PFMl~M&%KOzT)pW}NVz&h9zS#!Yv`!$myc))TN3x*R0V7%1^oxfK zmFQP64C79CUwh-TVKjbztUdq1(lT-@XLBMb^8yxBqo?D%R3^dNgb9=Tl*c={; zQptkFkxD8_Q8Ewj4$O)16DmwSJ$0!Y*=?lRSK6IM&>|;a+@nTzz#Bc7765>yWQ~H9 z{T~XnO+iL*=kgGLykXpuDT_`ylQPfy#vsyqYh)50NBakC{o}{V3KAc)!{N_JI!Q@? zREGx*r0WYRFJG3$!yu5?B2_pIz)17Mguo$|4XXl{uZSdbd2E)yWsyET;oHK{dxAp^ zUiBYvjo{CK=7y=91W25ybc^wvL%K=7RUIV4Fc!w4hi!MCBUYv-F~J2L-f_1E(@lx zG?uVmPXlrHx90-HAtQ z#F~e~9t00~|K-!zCSCcJ%j5fV&wC=fIkjg+;I{WET5VUCv2_{Gf$qt@+&Dv6uDp4B;VBurmmT1hBmas%jqkOKypdSiX)fJM?}KzDZ)-|_;93C&!IUzf@A7}T zQatxIOnyX=X3dVu;ySE&tq@HZ?#t-4til-?PoNb3__XWNc>wub!XMXi94P-_P2T>s z{-Yk30SniXxa#8NtDa91$-lFai;&{dFqkJ4{ZKVCr4aG^UVJAiW8>xTB~6Ye#g87j50w-ACvX4;LGGSm>lO4Qe|4qw$5&H_5K0Z*L>3=#JnQHWkkxpW&>j z;3Y&yR|HP>UOJ+#nNpB*VAGhVSPIkHje5Euss5S50>AYsN4t?47%KOMi!_&}XE-~( z;3rK5G=02wN#KeVgv+sX{a?;W^&dg`4usnIU*=JMdz}xJ=+Uq67T>zUlmFHEEQBAl zFAu43zX3TQZQ@mb5j6r?z4#br!B4nz(j*m6jIiMw$*(NYSib8i(kb-Dy8| zWD?7zq>t$l_vJQYF0kwj9o!NBc^xLXl-4XSxTZt62@FPd_zN0mvKwa}YZ}*&heU9* zCipnWsiQqnWqvr6GWE#hH!au^3Wv=+ue=p5#l*gnVcJ;f_=qYU>Z2d^*y0-aXRDkN zgTDE}Q`M(CF+~}hwYLtZB2z|qjS_81C_yP!Ana{2!GJ0eC&$}Mq!auV(7?zk`<%!i z12dm8r0eSKGaV?vJu;aNho)|GDF1n6g0X#amc60Kbc#QVtCx+jl-qe{Q0T~-XHjrG z6(tS#wb}f>a6HY+qE9I~^R7uAN06e!)YGb`Vrp{%8Igy#6N(}{c1jnP^-GTlwSpF% znk7qw31?8azG2&8w&En{jjk||VxE20Oeqye`KbQ=yH4T1TDK|?BpbZk5l0OGJVlg5 z$UyZ{O};#{--g~}46?#X(e^J<-x!OoqDs<2K)3RsD$9=|NR=nDJ8VO_+TX8!ZSOaW zCRvi}^nX(&Z;b{4>Lagyi?3K^F{TN`>}xz1+{TlF9Tf(zPfv5o{Z*7hR0nhU7!*7L z%YNiQYy6WT`Ptl1xS3T}v7o|CWeT#5PRSGY)3~lD4s>Q3VX%}=y&2F4Jm3?ud3hpH zJEmXDt<~izLXv?nwcVED)iu;~GG>J1$!^hNL4mkHEwTmefBz{A<8txQurP9OcgO2w zw}U5kRKZdq)3mr=ROCNs*W{ZS*{T23rwsQGRnEOBq(a=?CR$1rVWpM4+(3YAM&->C zs%EG53HB4V{YNwB2qzSGi3JyWIxeHA46}cpA>-Qlc0uQ0R>+8H-20MEE7S`j%@YHR z!}WjPL_(+8Rp#Hrc&I`qrPwo;sgte!5s^QBvz(M(>h~MPOYPyR8gRBh@ z>Rz_SpdMD>%sf+zfBN^?hFT#*TZV>rvhhd{Az3ttc|)bcv4-4w=zH%X&!4O~rbAV* z-)F^OqMJJK_J1`IDzFeq%&goN^@78og%6BP8fpl1;rl0`+d*ic3FHtHPS^;UnJc)j zIQb33AqS%(Y0U~U5XT@v^=NTqLMo0UgX2qpvkO520FLFlc#4F z5v7AH?SJPLEFPuF=0Rjwc%OR*m;+uPp9}jD9x9wjL1VGVOVX*Ig7b|xjh%(nG&vs) zRndO9lKD0jY(?6?Mi|&eR1EV_1uI*cSoJC2|mMFC) z+h0aDrvAK)njIeS1`s5ve`5HOX|Y&|^N3 zd^DI(EE)J!*JY>w zgTr%>EmfBOK3`L`v?4fQkG?gih+5&Lyd*NY|0YJNd>R}z)f&W$2A!NtC7PO4F_8QU zo@X#`hn^xh`CyNV@XAV#ys;<$L@o2tfI!+*Fvp)! zd{X|EymFg+gacWVNGKB9hha8J6*wT%%l+BQ%w~a;Ff6!Q8ChX3h1L+Qt=c6@-nMto zxP_5P#iRrJ9|0Go&*ES5{AN;2sdM-6<#yV$kdw2{8}t8bvF8X9#WCd!#Ucq%&*f2D zam03RFz3JI;iXzgNHS?}6SdnubD~aTu&VjW^fA{gL;a<7HXrGD`j2On)Ap025J8yo zZsXS{>%}v$fl)ZG9<@K$y?X$iX%7wnexb!V=13q7IklF?6?MNvNZI-H%uUHi)UjZu zctxT@`a>(}NjMh!1vkCc^8lFKt33_XkQj=Yy9`MYv`*s}8yUt-qdZpZ6rcqWck(J_ z;AQBYuu=1a1B!2X0F3y+Pd1X~X{}`xl2hEyuV}oBFuz-?wyTi84JVra1}FE88Xf~X zBVSW^E!9;!T*fD}eB+&(GC;c!K&`T>Mrb+B+dF;8fBDn}f~!M6hxYH<^Q6S#AvAt{ zM_p(6XeKe;)+t1;P**{S?Sp}fRuoIPzStvS6}8-T-O}$U3ia(N^b(?v;}I~+A{hXn zeLjWO+M5td&t!KBJ9|2cGM?uA^$8NcacDeh9BeBN%yPEClF2$-E>n8qX4< zL6LuAAp&+&89H|536U7`O!Y&!p(&=u!H23GO{(2|q+o2~cQL zz#3tm&-OA z(8oN?Kj`QEa)QIi`Ra5@fjm3_`W8%mm;D26-HxvNa9g#t>NRmGhR1%SJrC}9>Fxu~qL zp<5`FXO??d3!nF837NnE{=+E7H{QuQAtSg(pnvhYLgnpLxPq&4rh9uwr$#WqNv)m* z-MtsF{Uh-=Az7Wr^r>&6=_4;Fbi||K2L7TcP@H$9"` to your blacklist to avoid issues, unless you are willing to maintain enough extra `BNB` on the account or unless you're willing to disable using `BNB` for fees. Binance accounts may use `BNB` for fees, and if a trade happens to be on `BNB`, further trades may consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore. +### Binance sites + +Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. + +* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. +* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. + ### Binance Futures Binance has specific (unfortunately complex) [Futures Trading Quantitative Rules](https://www.binance.com/en/support/faq/4f462ebe6ff445d4a170be7d9e897272) which need to be followed, and which prohibit a too low stake-amount (among others) for too many orders. @@ -87,12 +94,14 @@ When trading on Binance Futures market, orderbook must be used because there is }, ``` -### Binance sites +#### Binance futures settings -Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized. +Users will also have to have the futures-setting "Position Mode" set to "One-way Mode", and "Asset Mode" set to "Single-Asset Mode". +These settings will be checked on startup, and freqtrade will show an error if this setting is wrong. -* [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. -* [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. +![Binance futures settings](assets/binance_futures_settings.png) + +Freqtrade will not attempt to change these settings. ## Kraken diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f9fb4a8b1..a0d4b2d82 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -68,6 +68,37 @@ class Binance(Exchange): tickers = deep_merge_dicts(bidsasks, tickers, allow_null_overrides=False) return tickers + @retrier + def additional_exchange_init(self) -> None: + """ + Additional exchange initialization logic. + .api will be available at this point. + Must be overridden in child methods if required. + """ + try: + if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: + position_side = self._api.fapiPrivateGetPositionsideDual() + self._log_exchange_response('position_side_setting', position_side) + assets_margin = self._api.fapiPrivateGetMultiAssetsMargin() + self._log_exchange_response('multi_asset_margin', assets_margin) + msg = "" + if position_side.get('dualSidePosition') is True: + msg += ( + "\nHedge Mode is not supported by freqtrade. " + "Please change 'Position Mode' on your binance futures account.") + if assets_margin.get('multiAssetsMargin') is True: + msg += ("\nMulti-Asset Mode is not supported by freqtrade. " + "Please change 'Asset Mode' on your binance futures account.") + if msg: + raise OperationalException(msg) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + @retrier def _set_leverage( self, diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index e9f4dfa8a..ef5cb1240 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -501,6 +501,24 @@ def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers assert len(v) == len(value) +def test_additional_exchange_init_binance(default_conf, mocker): + api_mock = MagicMock() + api_mock.fapiPrivateGetPositionsideDual = MagicMock(return_value={"dualSidePosition": True}) + api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": True}) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + with pytest.raises(OperationalException, + match=r"Hedge Mode is not supported.*\nMulti-Asset Mode is not supported.*"): + get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) + api_mock.fapiPrivateGetPositionsideDual = MagicMock(return_value={"dualSidePosition": False}) + api_mock.fapiPrivateGetMultiAssetsMargin = MagicMock(return_value={"multiAssetsMargin": False}) + exchange = get_patched_exchange(mocker, default_conf, id="binance", api_mock=api_mock) + assert exchange + ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'binance', + "additional_exchange_init", "fapiPrivateGetPositionsideDual") + + def test__set_leverage_binance(mocker, default_conf): api_mock = MagicMock() diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 82be6196a..6798cd2f7 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -137,6 +137,7 @@ def exchange_futures(request, exchange_conf, class_mocker): 'freqtrade.exchange.binance.Binance.fill_leverage_tiers') class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees') class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') + class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init') class_mocker.patch('freqtrade.exchange.exchange.Exchange.load_cached_leverage_tiers', return_value=None) class_mocker.patch('freqtrade.exchange.exchange.Exchange.cache_leverage_tiers') From 201bbbcee67d33a70dc81e1a04743345fedacd35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:32:16 +0200 Subject: [PATCH 30/32] Okx formatting --- freqtrade/exchange/okx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index fe1c94017..6792c2cba 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -78,7 +78,8 @@ class Okx(Exchange): raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}') from e + f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e From 8f8b5cc28ef82ddeed11c216058cd0c1c47ee710 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:35:21 +0200 Subject: [PATCH 31/32] Disable log spam from analyze_df in webhook/discord --- freqtrade/rpc/discord.py | 2 +- freqtrade/rpc/webhook.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/discord.py b/freqtrade/rpc/discord.py index 9efe6f427..c48508300 100644 --- a/freqtrade/rpc/discord.py +++ b/freqtrade/rpc/discord.py @@ -30,9 +30,9 @@ class Discord(Webhook): pass def send_msg(self, msg) -> None: - logger.info(f"Sending discord message: {msg}") if msg['type'].value in self.config['discord']: + logger.info(f"Sending discord message: {msg}") msg['strategy'] = self.strategy msg['timeframe'] = self.timeframe diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 6109e80bc..bb3b3922f 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -61,6 +61,14 @@ class Webhook(RPCHandler): RPCMessageType.STARTUP, RPCMessageType.WARNING): valuedict = whconfig.get('webhookstatus') + elif msg['type'] in ( + RPCMessageType.PROTECTION_TRIGGER, + RPCMessageType.PROTECTION_TRIGGER_GLOBAL, + RPCMessageType.WHITELIST, + RPCMessageType.ANALYZED_DF, + RPCMessageType.STRATEGY_MSG): + # Don't fail for non-implemented types + return else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) if not valuedict: From 6702a1b21905be3a6a8b00222c20e2c5e09bc449 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Oct 2022 09:45:58 +0200 Subject: [PATCH 32/32] Update test to verify webhook won't log-spam on new messagetypes --- tests/rpc/test_rpc_webhook.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 4d65b4966..3bbb85d54 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -365,6 +365,14 @@ def test_exception_send_msg(default_conf, mocker, caplog): with pytest.raises(NotImplementedError): webhook.send_msg(msg) + # Test no failure for not implemented but known messagetypes + for e in RPCMessageType: + msg = { + 'type': e, + 'status': 'whatever' + } + webhook.send_msg(msg) + def test__send_msg(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict()