From b6a6aa48c952dfec28059bb9fa71c97a67efdec6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Mar 2022 20:05:05 +0100 Subject: [PATCH 01/11] Create separate _ft_has_futures dict --- freqtrade/exchange/binance.py | 4 +++- freqtrade/exchange/exchange.py | 25 ++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 6607c15b7..bfc363c5e 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -22,7 +22,6 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, "stoploss_order_types": {"limit": "stop_loss_limit"}, - "stoploss_order_types_futures": {"limit": "stop"}, "order_time_in_force": ['gtc', 'fok', 'ioc'], "time_in_force_parameter": "timeInForce", "ohlcv_candle_limit": 1000, @@ -31,6 +30,9 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], "ccxt_futures_name": "future" } + _ft_has_futures: Dict = { + "stoploss_order_types": {"limit": "stop"}, + } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ # TradingMode.SPOT always supported and not required in this list diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9e6a19de9..03d29233e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -76,6 +76,7 @@ class Exchange: "ccxt_futures_name": "swap", } _ft_has: Dict = {} + _ft_has_futures: Dict = {} _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ # TradingMode.SPOT always supported and not required in this list @@ -122,8 +123,19 @@ class Exchange: exchange_config = config['exchange'] self.log_responses = exchange_config.get('log_responses', False) + # Leverage properties + self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) + self.margin_mode: Optional[MarginMode] = ( + MarginMode(config.get('margin_mode')) + if config.get('margin_mode') + else None + ) + self.liquidation_buffer = config.get('liquidation_buffer', 0.05) + # Deep merge ft_has with default ft_has options self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default)) + if self.trading_mode == TradingMode.FUTURES: + self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has) if exchange_config.get('_ft_has_params'): self._ft_has = deep_merge_dicts(exchange_config.get('_ft_has_params'), self._ft_has) @@ -135,15 +147,6 @@ class Exchange: self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] - # Leverage properties - self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) - self.margin_mode: Optional[MarginMode] = ( - MarginMode(config.get('margin_mode')) - if config.get('margin_mode') - else None - ) - self.liquidation_buffer = config.get('liquidation_buffer', 0.05) - # Initialize ccxt objects ccxt_config = self._ccxt_config ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) @@ -1011,10 +1014,6 @@ class Exchange: def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]: available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"] - if self.trading_mode == TradingMode.FUTURES: - # Optionally use different order type for stop order - available_order_Types = self._ft_has.get('stoploss_order_types_futures', - self._ft_has["stoploss_order_types"]) if user_order_type in available_order_Types.keys(): ordertype = available_order_Types[user_order_type] From a13b633c5623fc16b1ed997a8195ca82dc27c001 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Mar 2022 20:11:56 +0100 Subject: [PATCH 02/11] update VOlumepairlist to also work without tickers --- freqtrade/plugins/pairlist/VolumePairList.py | 36 ++++++++++++-------- tests/plugins/test_pairlist.py | 4 +-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 961c461f2..6adca7369 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -71,10 +71,11 @@ class VolumePairList(IPairList): f'to at least {self._tf_in_sec} and restart the bot.' ) - if not self._exchange.exchange_has('fetchTickers'): + if not self._use_range and not self._exchange.exchange_has('fetchTickers'): raise OperationalException( - 'Exchange does not support dynamic whitelist. ' - 'Please edit your config and restart the bot.' + "Exchange does not support dynamic whitelist in this configuration. " + "Please edit your config and either remove Volumepairlist, " + "or switch to using candles. and restart the bot." ) if not self._validate_keys(self._sort_key): @@ -95,7 +96,7 @@ class VolumePairList(IPairList): If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ - return True + return not self._use_range def _validate_keys(self, key): return key in SORT_VALUES @@ -126,13 +127,15 @@ class VolumePairList(IPairList): tradable_only=True, active_only=True).keys()] # No point in testing for blacklisted pairs... _pairlist = self.verify_blacklist(_pairlist, logger.info) - - filtered_tickers = [ - v for k, v in tickers.items() - if (self._exchange.get_pair_quote_currency(k) == self._stake_currency - and (self._use_range or v[self._sort_key] is not None) - and v['symbol'] in _pairlist)] - pairlist = [s['symbol'] for s in filtered_tickers] + if not self._use_range: + filtered_tickers = [ + v for k, v in tickers.items() + if (self._exchange.get_pair_quote_currency(k) == self._stake_currency + and (self._use_range or v[self._sort_key] is not None) + and v['symbol'] in _pairlist)] + pairlist = [s['symbol'] for s in filtered_tickers] + else: + pairlist = _pairlist pairlist = self.filter_pairlist(pairlist, tickers) self._pair_cache['pairlist'] = pairlist.copy() @@ -147,11 +150,11 @@ class VolumePairList(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new whitelist """ - # Use the incoming pairlist. - filtered_tickers = [v for k, v in tickers.items() if k in pairlist] - - # get lookback period in ms, for exchange ohlcv fetch if self._use_range: + # Create bare minimum from tickers structure. + filtered_tickers = [{'symbol': k} for k in pairlist] + + # get lookback period in ms, for exchange ohlcv fetch since_ms = int(arrow.utcnow() .floor('minute') .shift(minutes=-(self._lookback_period * self._tf_in_min) @@ -208,6 +211,9 @@ class VolumePairList(IPairList): filtered_tickers[i]['quoteVolume'] = quoteVolume else: filtered_tickers[i]['quoteVolume'] = 0 + else: + # Tickers mode - filter based on incomming pairlist. + filtered_tickers = [v for k, v in tickers.items() if k in pairlist] if self._min_value > 0: filtered_tickers = [ diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 8d7e2b5c1..9b7865e82 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -587,10 +587,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], "BTC", "binance", ['LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC', 'HOT/BTC']), - # expecting pairs from default tickers, because 1h candles are not available + # expecting pairs as input, because 1h candles are not available ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "lookback_timeframe": "1h", "lookback_period": 2, "refresh_period": 3600}], - "BTC", "binance", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), + "BTC", "binance", ['ETH/BTC', 'LTC/BTC', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), # ftx data is already in Quote currency, therefore won't require conversion ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], From 1299a703e279ad61b99625b601bc72380e344049 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Mar 2022 20:15:51 +0100 Subject: [PATCH 03/11] Implement fix for okx futures not having quoteVolume --- freqtrade/exchange/exchange.py | 1 + freqtrade/exchange/okx.py | 3 +++ freqtrade/plugins/pairlist/VolumePairList.py | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 03d29233e..a0b1177ad 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -67,6 +67,7 @@ class Exchange: "ohlcv_partial_candle": True, # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency "ohlcv_volume_currency": "base", # "base" or "quote" + "tickers_has_quoteVolume": True, "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", "l2_limit_range": None, diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 08c29c7b2..aa4390b31 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -23,6 +23,9 @@ class Okx(Exchange): "mark_ohlcv_timeframe": "4h", "funding_fee_timeframe": "8h", } + _ft_has_futures: Dict = { + "tickers_has_quoteVolume": False, + } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ # TradingMode.SPOT always supported and not required in this list diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 6adca7369..f18dce735 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -71,7 +71,9 @@ class VolumePairList(IPairList): f'to at least {self._tf_in_sec} and restart the bot.' ) - if not self._use_range and not self._exchange.exchange_has('fetchTickers'): + if (not self._use_range and not ( + self._exchange.exchange_has('fetchTickers') + and self._exchange._ft_has["tickers_has_quoteVolume"])): raise OperationalException( "Exchange does not support dynamic whitelist in this configuration. " "Please edit your config and either remove Volumepairlist, " From b56aab0bdfa6527902d36940a7a1ec2402dca3a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 06:34:35 +0100 Subject: [PATCH 04/11] Update Volumepairlist type --- freqtrade/plugins/pairlist/VolumePairList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index f18dce735..688305b5a 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -154,7 +154,7 @@ class VolumePairList(IPairList): """ if self._use_range: # Create bare minimum from tickers structure. - filtered_tickers = [{'symbol': k} for k in pairlist] + filtered_tickers: List[Dict[str, Any]] = [{'symbol': k} for k in pairlist] # get lookback period in ms, for exchange ohlcv fetch since_ms = int(arrow.utcnow() From fdce055061324c4f29f29093feb33813db840755 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 06:58:22 +0100 Subject: [PATCH 05/11] Update deep_merge_dicts to disallow null-overrides --- freqtrade/misc.py | 6 +++--- tests/test_misc.py | 22 ++++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 534844036..acc7fc2e4 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -129,7 +129,7 @@ def format_ms_time(date: int) -> str: return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S') -def deep_merge_dicts(source, destination): +def deep_merge_dicts(source, destination, allow_null_overrides: bool = True): """ Values from Source override destination, destination is returned (and modified!!) Sample: @@ -142,8 +142,8 @@ def deep_merge_dicts(source, destination): if isinstance(value, dict): # get node or create one node = destination.setdefault(key, {}) - deep_merge_dicts(value, node) - else: + deep_merge_dicts(value, node, allow_null_overrides) + elif value is not None or allow_null_overrides: destination[key] = value return destination diff --git a/tests/test_misc.py b/tests/test_misc.py index aef7e576b..d28050dfb 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,15 +1,16 @@ # pragma pylint: disable=missing-docstring,C0103 import datetime +from copy import deepcopy from pathlib import Path from unittest.mock import MagicMock import pytest -from freqtrade.misc import (decimals_per_coin, file_dump_json, file_load_json, format_ms_time, - pair_to_filename, parse_db_uri_for_logging, plural, render_template, - render_template_with_fallback, round_coin_value, safe_value_fallback, - safe_value_fallback2, shorten_date) +from freqtrade.misc import (decimals_per_coin, deep_merge_dicts, file_dump_json, file_load_json, + format_ms_time, pair_to_filename, parse_db_uri_for_logging, plural, + render_template, render_template_with_fallback, round_coin_value, + safe_value_fallback, safe_value_fallback2, shorten_date) def test_decimals_per_coin(): @@ -203,3 +204,16 @@ def test_render_template_fallback(mocker): def test_parse_db_uri_for_logging(conn_url, expected) -> None: assert parse_db_uri_for_logging(conn_url) == expected + + +def test_deep_merge_dicts(): + a = {'first': {'rows': {'pass': 'dog', 'number': '1', 'test': None}}} + b = {'first': {'rows': {'fail': 'cat', 'number': '5', 'test': 'asdf'}}} + res = {'first': {'rows': {'pass': 'dog', 'fail': 'cat', 'number': '5', 'test': 'asdf'}}} + res2 = {'first': {'rows': {'pass': 'dog', 'fail': 'cat', 'number': '1', 'test': None}}} + assert deep_merge_dicts(b, deepcopy(a)) == res + + assert deep_merge_dicts(a, deepcopy(b)) == res2 + + res2['first']['rows']['test'] = 'asdf' + assert deep_merge_dicts(a, deepcopy(b), allow_null_overrides=False) == res2 From 208a139d2b2119cff7eda4da8fe89375d5ce0ffa Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 07:08:16 +0100 Subject: [PATCH 06/11] Incorporate fetch_bids_asks to allow binance spread filter to work closes #6474 --- freqtrade/exchange/binance.py | 10 +++++ freqtrade/exchange/exchange.py | 30 ++++++++++++++- tests/exchange/test_exchange.py | 68 +++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index bfc363c5e..314337c3e 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -12,6 +12,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier +from freqtrade.misc import deep_merge_dicts logger = logging.getLogger(__name__) @@ -55,6 +56,15 @@ class Binance(Exchange): (side == "buy" and stop_loss < float(order['info']['stopPrice'])) ) + def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict: + tickers = super().get_tickers(symbols=symbols, cached=cached) + if self.trading_mode == TradingMode.FUTURES: + # Binance's future result has no bid/ask values. + # Therefore we must fetch that from fetch_bids_asks and combine the two results. + bidsasks = self.fetch_bids_asks(symbols, cached) + tickers = deep_merge_dicts(bidsasks, tickers, allow_null_overrides=False) + return tickers + @retrier def _set_leverage( self, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a0b1177ad..25c6cbd69 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -104,7 +104,7 @@ class Exchange: self._last_markets_refresh: int = 0 # Cache for 10 minutes ... - self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) + self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10) # Cache values for 1800 to avoid frequent polling of the exchange for prices # Caching only applies to RPC methods, so prices for open trades are still # refreshed once every iteration. @@ -1289,6 +1289,34 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + @retrier + def fetch_bids_asks(self, symbols: List[str] = None, cached: bool = False) -> Dict: + """ + :param cached: Allow cached result + :return: fetch_tickers result + """ + if not self.exchange_has('fetchBidsAsks'): + return {} + if cached: + tickers = self._fetch_tickers_cache.get('fetch_bids_asks') + if tickers: + return tickers + try: + tickers = self._api.fetch_bids_asks(symbols) + self._fetch_tickers_cache['fetch_bids_asks'] = tickers + return tickers + except ccxt.NotSupported as e: + raise OperationalException( + f'Exchange {self._api.name} does not support fetching bids/asks 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 bids/asks due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + @retrier def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict: """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5c0b55efe..7438d900d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1572,6 +1572,59 @@ def test_fetch_positions(default_conf, mocker, exchange_name): "fetch_positions", "fetch_positions") +def test_fetch_bids_asks(default_conf, mocker): + api_mock = MagicMock() + tick = {'ETH/BTC': { + 'symbol': 'ETH/BTC', + 'bid': 0.5, + 'ask': 1, + 'last': 42, + }, 'BCH/BTC': { + 'symbol': 'BCH/BTC', + 'bid': 0.6, + 'ask': 0.5, + 'last': 41, + } + } + exchange_name = 'binance' + api_mock.fetch_bids_asks = MagicMock(return_value=tick) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + # retrieve original ticker + bidsasks = exchange.fetch_bids_asks() + + assert 'ETH/BTC' in bidsasks + assert 'BCH/BTC' in bidsasks + assert bidsasks['ETH/BTC']['bid'] == 0.5 + assert bidsasks['ETH/BTC']['ask'] == 1 + assert bidsasks['BCH/BTC']['bid'] == 0.6 + assert bidsasks['BCH/BTC']['ask'] == 0.5 + assert api_mock.fetch_bids_asks.call_count == 1 + + api_mock.fetch_bids_asks.reset_mock() + + # Cached ticker should not call api again + tickers2 = exchange.fetch_bids_asks(cached=True) + assert tickers2 == bidsasks + assert api_mock.fetch_bids_asks.call_count == 0 + tickers2 = exchange.fetch_bids_asks(cached=False) + assert api_mock.fetch_bids_asks.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + "fetch_bids_asks", "fetch_bids_asks") + + with pytest.raises(OperationalException): + api_mock.fetch_bids_asks = MagicMock(side_effect=ccxt.NotSupported("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.fetch_bids_asks() + + api_mock.fetch_bids_asks = MagicMock(return_value={}) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.fetch_bids_asks() + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + assert exchange.fetch_bids_asks() == {} + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_tickers(default_conf, mocker, exchange_name): api_mock = MagicMock() @@ -1588,6 +1641,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): } } api_mock.fetch_tickers = MagicMock(return_value=tick) + api_mock.fetch_bids_asks = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker tickers = exchange.get_tickers() @@ -1599,6 +1653,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['ask'] == 0.5 assert api_mock.fetch_tickers.call_count == 1 + assert api_mock.fetch_bids_asks.call_count == 0 api_mock.fetch_tickers.reset_mock() @@ -1606,8 +1661,10 @@ def test_get_tickers(default_conf, mocker, exchange_name): tickers2 = exchange.get_tickers(cached=True) assert tickers2 == tickers assert api_mock.fetch_tickers.call_count == 0 + assert api_mock.fetch_bids_asks.call_count == 0 tickers2 = exchange.get_tickers(cached=False) assert api_mock.fetch_tickers.call_count == 1 + assert api_mock.fetch_bids_asks.call_count == 0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") @@ -1621,6 +1678,17 @@ def test_get_tickers(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange.get_tickers() + api_mock.fetch_tickers.reset_mock() + api_mock.fetch_bids_asks.reset_mock() + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + exchange.get_tickers() + assert api_mock.fetch_tickers.call_count == 1 + assert api_mock.fetch_bids_asks.call_count == (1 if exchange_name == 'binance' else 0) + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_ticker(default_conf, mocker, exchange_name): From 1de5d2fb94d9df67c5e18f1a3528d86e64c44667 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 16:44:27 +0100 Subject: [PATCH 07/11] Remove unnecessary condition --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 25c6cbd69..a84a63edb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1426,7 +1426,7 @@ class Exchange: conf_strategy = self._config.get(strat_name, {}) - if conf_strategy.get('use_order_book', False) and ('use_order_book' in conf_strategy): + if conf_strategy.get('use_order_book', False): order_book_top = conf_strategy.get('order_book_top', 1) order_book = self.fetch_l2_order_book(pair, order_book_top) From 2791e799ee163e8360e8092a4e6caa75a786f848 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 16:49:37 +0100 Subject: [PATCH 08/11] Rename tickers_has_quoteVolume --- freqtrade/exchange/binance.py | 1 + freqtrade/exchange/exchange.py | 3 ++- freqtrade/exchange/okx.py | 2 +- freqtrade/plugins/pairlist/VolumePairList.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 314337c3e..8c442cd26 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -33,6 +33,7 @@ class Binance(Exchange): } _ft_has_futures: Dict = { "stoploss_order_types": {"limit": "stop"}, + "tickers_have_price": False, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a84a63edb..5d6786476 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -67,7 +67,8 @@ class Exchange: "ohlcv_partial_candle": True, # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency "ohlcv_volume_currency": "base", # "base" or "quote" - "tickers_has_quoteVolume": True, + "tickers_have_quoteVolume": True, + "tickers_have_price": True, "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", "l2_limit_range": None, diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index aa4390b31..a21da2344 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -24,7 +24,7 @@ class Okx(Exchange): "funding_fee_timeframe": "8h", } _ft_has_futures: Dict = { - "tickers_has_quoteVolume": False, + "tickers_have_quoteVolume": False, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 688305b5a..26e7d45be 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -73,7 +73,7 @@ class VolumePairList(IPairList): if (not self._use_range and not ( self._exchange.exchange_has('fetchTickers') - and self._exchange._ft_has["tickers_has_quoteVolume"])): + and self._exchange._ft_has["tickers_have_quoteVolume"])): raise OperationalException( "Exchange does not support dynamic whitelist in this configuration. " "Please edit your config and either remove Volumepairlist, " From d32153c8d352d7ada8e4af8b70c1000b32f93a1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 17:07:12 +0100 Subject: [PATCH 09/11] Validate pricing configuration --- freqtrade/exchange/exchange.py | 10 +++++++++ tests/exchange/test_exchange.py | 40 ++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5d6786476..fd4fd627f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -182,6 +182,8 @@ class Exchange: self.required_candle_call_count = self.validate_required_startup_candles( config.get('startup_candle_count', 0), config.get('timeframe', '')) self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode) + self.validate_pricing(config['ask_strategy']) + self.validate_pricing(config['bid_strategy']) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( @@ -575,6 +577,14 @@ class Exchange: f'On exchange stoploss is not supported for {self.name}.' ) + def validate_pricing(self, pricing: Dict) -> None: + if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'): + raise OperationalException(f'Orderbook not available for {self.name}.') + if (not pricing.get('use_order_book', False) and not ( + self.exchange_has('fetchTicker') and self._ft_has['tickers_have_price'] + )): + raise OperationalException(f'Ticker pricing not available for {self.name}.') + 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 7438d900d..c375f37fe 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -939,7 +939,45 @@ def test_validate_timeframes_not_in_config(default_conf, mocker): Exchange(default_conf) -def test_validate_order_types(default_conf, mocker): +def test_validate_pricing(default_conf, mocker): + api_mock = MagicMock() + has = { + 'fetchL2OrderBook': True, + 'fetchTicker': True, + } + type(api_mock).has = PropertyMock(return_value=has) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) + mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode') + mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.name', 'Binance') + ExchangeResolver.load_exchange('binance', default_conf) + has.update({'fetchTicker': False}) + with pytest.raises(OperationalException, match="Ticker pricing not available for .*"): + ExchangeResolver.load_exchange('binance', default_conf) + + has.update({'fetchTicker': True}) + + default_conf['ask_strategy']['use_order_book'] = True + ExchangeResolver.load_exchange('binance', default_conf) + has.update({'fetchL2OrderBook': False}) + + with pytest.raises(OperationalException, match="Orderbook not available for .*"): + ExchangeResolver.load_exchange('binance', default_conf) + + has.update({'fetchL2OrderBook': True}) + + # Binance has no tickers on futures + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + + with pytest.raises(OperationalException, match="Ticker pricing not available for .*"): + ExchangeResolver.load_exchange('binance', default_conf) + + +def test_validate_ordertypes(default_conf, mocker): api_mock = MagicMock() type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True}) From 97c1316bf1c37697304b265d77d65ab2304177a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 18 Mar 2022 19:26:54 +0100 Subject: [PATCH 10/11] Add new validation to validate excludes --- freqtrade/exchange/exchange.py | 6 +++--- tests/conftest.py | 1 + tests/exchange/test_exchange.py | 19 ++++++++++++++++++- tests/exchange/test_gateio.py | 1 + 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fd4fd627f..108b074f3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -580,9 +580,9 @@ class Exchange: def validate_pricing(self, pricing: Dict) -> None: if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'): raise OperationalException(f'Orderbook not available for {self.name}.') - if (not pricing.get('use_order_book', False) and not ( - self.exchange_has('fetchTicker') and self._ft_has['tickers_have_price'] - )): + if (not pricing.get('use_order_book', False) and ( + not self.exchange_has('fetchTicker') + or not self._ft_has['tickers_have_price'])): raise OperationalException(f'Ticker pricing not available for {self.name}.') def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: diff --git a/tests/conftest.py b/tests/conftest.py index 698cdc7b4..05ff39358 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,6 +104,7 @@ def patch_exchange( mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c375f37fe..27750637e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -167,6 +167,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') exchange = ExchangeResolver.load_exchange('zaif', default_conf) assert isinstance(exchange, Exchange) @@ -570,6 +571,7 @@ def test__load_async_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') exchange = Exchange(default_conf) exchange._api_async.load_markets = get_mock_coro(None) exchange._load_async_markets() @@ -591,6 +593,7 @@ def test__load_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') Exchange(default_conf) assert log_has('Unable to initialize markets.', caplog) @@ -659,6 +662,7 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') Exchange(default_conf) @@ -731,6 +735,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') Exchange(default_conf) @@ -757,6 +762,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): @@ -777,6 +783,7 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_async_markets') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -796,6 +803,7 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') Exchange(default_conf) @@ -812,6 +820,7 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') Exchange(default_conf) assert type(api_mock).load_markets.call_count == 1 @@ -852,6 +861,7 @@ def test_validate_timeframes(default_conf, mocker, timeframe): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') Exchange(default_conf) @@ -936,6 +946,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') Exchange(default_conf) @@ -986,6 +997,7 @@ def test_validate_ordertypes(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') default_conf['order_types'] = { @@ -1026,6 +1038,7 @@ def test_validate_order_types_not_in_config(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') conf = copy.deepcopy(default_conf) @@ -1040,6 +1053,7 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') default_conf['startup_candle_count'] = 20 @@ -3084,7 +3098,8 @@ def test_merge_ft_has_dict(default_conf, mocker): _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), - validate_stakecurrency=MagicMock() + validate_stakecurrency=MagicMock(), + validate_pricing=MagicMock(), ) ex = Exchange(default_conf) assert ex._ft_has == Exchange._ft_has_default @@ -3118,6 +3133,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), + validate_pricing=MagicMock(), markets=PropertyMock(return_value=markets)) ex = Exchange(default_conf) @@ -3209,6 +3225,7 @@ def test_get_markets(default_conf, mocker, markets_static, _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), + validate_pricing=MagicMock(), markets=PropertyMock(return_value=markets_static)) ex = Exchange(default_conf) pairs = ex.get_markets(base_currencies, diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 3ecce96aa..2de6080ef 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -12,6 +12,7 @@ def test_validate_order_types_gateio(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.validate_pricing') mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') exch = ExchangeResolver.load_exchange('gateio', default_conf, True) assert isinstance(exch, Gateio) From f44601d0cc4f9559dfafda60272f9cf0bf503161 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Mar 2022 14:45:26 +0100 Subject: [PATCH 11/11] Update ccxt-compat test config --- tests/exchange/test_ccxt_compat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index a9b399461..5eb7e68d4 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -107,6 +107,8 @@ def exchange_conf(): config['exchange']['key'] = '' config['exchange']['secret'] = '' config['dry_run'] = False + config['bid_strategy']['use_order_book'] = True + config['ask_strategy']['use_order_book'] = True return config