diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 032f7dd51..025fee66c 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -48,7 +48,8 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", - "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] + "print_csv", "base_currencies", "quote_currencies", "list_pairs_all", + "trading_mode"] ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column", "list_pairs_print_json"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 6aa4ed363..7d1d2edd1 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -179,7 +179,6 @@ AVAILABLE_CLI_OPTIONS = { '--export', help='Export backtest results (default: trades).', choices=constants.EXPORT_OPTIONS, - ), "exportfilename": Arg( '--export-filename', @@ -349,6 +348,11 @@ AVAILABLE_CLI_OPTIONS = { nargs='+', metavar='BASE_CURRENCY', ), + "trading_mode": Arg( + '--trading-mode', + help='Select Trading mode', + choices=constants.TRADING_MODES, + ), # Script options "pairs": Arg( '-p', '--pairs', diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 464b38967..c4bd0bf4d 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -129,10 +129,9 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: quote_currencies = args.get('quote_currencies', []) try: - # TODO-lev: Add leverage amount to get markets that support a certain leverage pairs = exchange.get_markets(base_currencies=base_currencies, quote_currencies=quote_currencies, - pairs_only=pairs_only, + tradable_only=pairs_only, active_only=active_only) # Sort the pairs/markets by symbol pairs = dict(sorted(pairs.items())) @@ -152,15 +151,19 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: if quote_currencies else "")) headers = ["Id", "Symbol", "Base", "Quote", "Active", - *(['Is pair'] if not pairs_only else [])] + "Spot", "Margin", "Future", "Leverage"] - tabular_data = [] - for _, v in pairs.items(): - tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'], - 'Base': v['base'], 'Quote': v['quote'], - 'Active': market_is_active(v), - **({'Is pair': exchange.market_is_tradable(v)} - if not pairs_only else {})}) + tabular_data = [{ + 'Id': v['id'], + 'Symbol': v['symbol'], + 'Base': v['base'], + 'Quote': v['quote'], + 'Active': market_is_active(v), + 'Spot': 'Spot' if exchange.market_is_spot(v) else '', + 'Margin': 'Margin' if exchange.market_is_margin(v) else '', + 'Future': 'Future' if exchange.market_is_future(v) else '', + 'Leverage': exchange.get_max_leverage(v['symbol'], 20) + } for _, v in pairs.items()] if (args.get('print_one_column', False) or args.get('list_pairs_print_json', False) or diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index f5a674878..67617d84f 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -431,6 +431,8 @@ class Configuration: self._args_to_config(config, argname='new_pairs_days', logstring='Detected --new-pairs-days: {}') + self._args_to_config(config, argname='trading_mode', + logstring='Detected --trading-mode: {}') def _process_runmode(self, config: Dict[str, Any]) -> None: diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f4aeacae3..232e2cb55 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -3,7 +3,7 @@ import json import logging from datetime import datetime from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt @@ -119,6 +119,10 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e + def market_is_future(self, market: Dict[str, Any]) -> bool: + # TODO-lev: This should be unified in ccxt to "swap"... + return market.get('future', False) is True + @retrier def fill_leverage_brackets(self): """ @@ -161,6 +165,8 @@ class Binance(Exchange): :param pair: The base/quote currency pair being traded :nominal_value: The total value of the trade in quote currency (collateral + debt) """ + if pair not in self._leverage_brackets: + return 1.0 pair_brackets = self._leverage_brackets[pair] max_lev = 1.0 for [min_amount, margin_req] in pair_brackets: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fe19230a9..0acd10900 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -291,7 +291,9 @@ class Exchange: timeframe, self._ft_has.get('ohlcv_candle_limit'))) def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None, - pairs_only: bool = False, active_only: bool = False) -> Dict[str, Any]: + spot_only: bool = False, margin_only: bool = False, futures_only: bool = False, + tradable_only: bool = True, + active_only: bool = False) -> Dict[str, Any]: """ Return exchange ccxt markets, filtered out by base currency and quote currency if this was requested in parameters. @@ -306,8 +308,14 @@ class Exchange: markets = {k: v for k, v in markets.items() if v['base'] in base_currencies} if quote_currencies: markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies} - if pairs_only: + if tradable_only: markets = {k: v for k, v in markets.items() if self.market_is_tradable(v)} + if spot_only: + markets = {k: v for k, v in markets.items() if self.market_is_spot(v)} + if margin_only: + markets = {k: v for k, v in markets.items() if self.market_is_margin(v)} + if futures_only: + markets = {k: v for k, v in markets.items() if self.market_is_future(v)} if active_only: markets = {k: v for k, v in markets.items() if market_is_active(v)} return markets @@ -331,18 +339,27 @@ class Exchange: """ return self.markets.get(pair, {}).get('base', '') + def market_is_future(self, market: Dict[str, Any]) -> bool: + return market.get('swap', False) is True + + def market_is_spot(self, market: Dict[str, Any]) -> bool: + return market.get('spot', False) is True + + def market_is_margin(self, market: Dict[str, Any]) -> bool: + return market.get('margin', False) is True + def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. - By default, checks if it's splittable by `/` and both sides correspond to base / quote + Ensures that Configured mode aligns to """ - symbol_parts = market['symbol'].split('/') - return (len(symbol_parts) == 2 and - len(symbol_parts[0]) > 0 and - len(symbol_parts[1]) > 0 and - symbol_parts[0] == market.get('base') and - symbol_parts[1] == market.get('quote') - ) + return ( + market.get('quote', None) is not None + and market.get('base', None) is not None + and (self.trading_mode == TradingMode.SPOT and self.market_is_spot(market)) + or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market)) + or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market)) + ) def klines(self, pair_interval: Tuple[str, str], copy: bool = True) -> DataFrame: if pair_interval in self._klines: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 1d06be821..e798f2c29 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -30,16 +30,6 @@ class Ftx(Exchange): # (TradingMode.FUTURES, Collateral.CROSS) ] - def market_is_tradable(self, market: Dict[str, Any]) -> bool: - """ - Check if the market symbol is tradable by Freqtrade. - Default checks + check if pair is spot pair (no futures trading yet). - """ - parent_check = super().market_is_tradable(market) - - return (parent_check and - market.get('spot', False) is True) - def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) @@ -169,3 +159,7 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] + + def market_is_future(self, market: Dict[str, Any]) -> bool: + # TODO-lev: This should be unified in ccxt to "swap"... + return market.get('future', False) is True diff --git a/requirements.txt b/requirements.txt index 9d8015129..c6044cf7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.3 pandas==1.3.4 pandas-ta==0.3.14b -ccxt==1.60.11 +ccxt==1.61.24 # Pin cryptography for now due to rust build errors with piwheels cryptography==35.0.0 aiohttp==3.7.4.post0 diff --git a/setup.py b/setup.py index 4695680d9..a5e91dfac 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ setup( ], install_requires=[ # from requirements.txt - 'ccxt>=1.60.11', + 'ccxt>=1.61.24', 'SQLAlchemy', 'python-telegram-bot>=13.4', 'arrow>=0.17.0', diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 6cd009f96..55fc4463d 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -434,9 +434,9 @@ def test_list_markets(mocker, markets_static, capsys): ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert ("Id,Symbol,Base,Quote,Active,Is pair" in captured.out) - assert ("blkbtc,BLK/BTC,BLK,BTC,True,True" in captured.out) - assert ("USD-LTC,LTC/USD,LTC,USD,True,True" in captured.out) + assert ("Id,Symbol,Base,Quote,Active,Spot,Margin,Future,Leverage" in captured.out) + assert ("blkbtc,BLK/BTC,BLK,BTC,True,Spot" in captured.out) + assert ("USD-LTC,LTC/USD,LTC,USD,True,Spot" in captured.out) # Test --one-column args = [ diff --git a/tests/conftest.py b/tests/conftest.py index 9d04e994b..7e5dd47e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -575,6 +575,8 @@ def get_markets(): 'base': 'ETH', 'quote': 'BTC', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -604,6 +606,8 @@ def get_markets(): 'quote': 'BTC', # According to ccxt, markets without active item set are also active # 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -632,6 +636,8 @@ def get_markets(): 'base': 'BLK', 'quote': 'BTC', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -660,6 +666,8 @@ def get_markets(): 'base': 'LTC', 'quote': 'BTC', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -685,6 +693,8 @@ def get_markets(): 'base': 'XRP', 'quote': 'BTC', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -710,6 +720,8 @@ def get_markets(): 'base': 'NEO', 'quote': 'BTC', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -735,6 +747,8 @@ def get_markets(): 'base': 'BTT', 'quote': 'BTC', 'active': False, + 'spot': True, + 'type': 'spot', 'precision': { 'base': 8, 'quote': 8, @@ -762,6 +776,11 @@ def get_markets(): 'symbol': 'ETH/USDT', 'base': 'ETH', 'quote': 'USDT', + 'spot': True, + 'future': True, + 'swap': True, + 'margin': True, + 'type': 'spot', 'precision': { 'amount': 8, 'price': 8 @@ -785,6 +804,11 @@ def get_markets(): 'base': 'LTC', 'quote': 'USDT', 'active': False, + 'spot': True, + 'future': True, + 'swap': True, + 'margin': True, + 'type': 'spot', 'precision': { 'amount': 8, 'price': 8 @@ -807,6 +831,8 @@ def get_markets(): 'base': 'XRP', 'quote': 'USDT', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -832,6 +858,8 @@ def get_markets(): 'base': 'NEO', 'quote': 'USDT', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -857,6 +885,8 @@ def get_markets(): 'base': 'TKN', 'quote': 'USDT', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -882,6 +912,8 @@ def get_markets(): 'base': 'LTC', 'quote': 'USD', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'amount': 8, 'price': 8 @@ -904,6 +936,8 @@ def get_markets(): 'base': 'LTC', 'quote': 'USDT', 'active': True, + 'spot': False, + 'type': 'SomethingElse', 'precision': { 'amount': 8, 'price': 8 @@ -926,6 +960,8 @@ def get_markets(): 'base': 'LTC', 'quote': 'ETH', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'base': 8, 'quote': 8, @@ -976,6 +1012,8 @@ def shitcoinmarkets(markets_static): 'base': 'HOT', 'quote': 'BTC', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'base': 8, 'quote': 8, @@ -1004,6 +1042,8 @@ def shitcoinmarkets(markets_static): 'base': 'FUEL', 'quote': 'BTC', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'base': 8, 'quote': 8, diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index b14df070c..ea0dc0fa4 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -5,6 +5,7 @@ However, these tests should give a good idea to determine if a new exchange is suitable to run with freqtrade. """ +from copy import deepcopy from datetime import datetime, timedelta, timezone from pathlib import Path @@ -26,6 +27,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'hasQuoteVolume': True, 'timeframe': '5m', + 'futures': True, }, 'kraken': { 'pair': 'BTC/USDT', @@ -82,13 +84,20 @@ def exchange(request, exchange_conf): @pytest.fixture(params=EXCHANGES, scope="class") -def exchange_futures(request, exchange_conf): +def exchange_futures(request, exchange_conf, class_mocker): if not EXCHANGES[request.param].get('futures') is True: yield None, request.param else: + exchange_conf = deepcopy(exchange_conf) exchange_conf['exchange']['name'] = request.param exchange_conf['trading_mode'] = 'futures' exchange_conf['collateral'] = 'cross' + # TODO-lev This mock should no longer be necessary once futures are enabled. + class_mocker.patch( + 'freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_collateral') + class_mocker.patch( + 'freqtrade.exchange.binance.Binance.fill_leverage_brackets') + exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True) yield exchange, request.param @@ -103,6 +112,20 @@ class TestCCXTExchange(): markets = exchange.markets assert pair in markets assert isinstance(markets[pair], dict) + assert exchange.market_is_spot(markets[pair]) + + def test_load_markets_futures(self, exchange_futures): + exchange, exchangename = exchange_futures + if not exchange: + # exchange_futures only returns values for supported exchanges + return + pair = EXCHANGES[exchangename]['pair'] + pair = EXCHANGES[exchangename].get('futures_pair', pair) + markets = exchange.markets + assert pair in markets + assert isinstance(markets[pair], dict) + + assert exchange.market_is_future(markets[pair]) def test_ccxt_fetch_tickers(self, exchange): exchange, exchangename = exchange diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0e2824b6c..e974cbd43 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2760,7 +2760,8 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): @pytest.mark.parametrize( - "base_currencies, quote_currencies, pairs_only, active_only, expected_keys", [ + "base_currencies,quote_currencies,tradable_only,active_only,spot_only," + "futures_only,expected_keys", [ # Testing markets (in conftest.py): # 'BLK/BTC': 'active': True # 'BTT/BTC': 'active': True @@ -2775,48 +2776,62 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): # 'XLTCUSDT': 'active': True, not a pair # 'XRP/BTC': 'active': False # all markets - ([], [], False, False, + ([], [], False, False, False, False, ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), + # all markets, only spot pairs + ([], [], False, False, True, False, + ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', + 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), # active markets - ([], [], False, True, + ([], [], False, True, False, False, ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), # all pairs - ([], [], True, False, + ([], [], True, False, False, False, ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), # active pairs - ([], [], True, True, + ([], [], True, True, False, False, ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), # all markets, base=ETH, LTC - (['ETH', 'LTC'], [], False, False, + (['ETH', 'LTC'], [], False, False, False, False, ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), # all markets, base=LTC - (['LTC'], [], False, False, + (['LTC'], [], False, False, False, False, ['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), + # spot markets, base=LTC + (['LTC'], [], False, False, True, False, + ['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT']), # all markets, quote=USDT - ([], ['USDT'], False, False, + ([], ['USDT'], False, False, False, False, ['ETH/USDT', 'LTC/USDT', 'XLTCUSDT']), + # Futures markets, quote=USDT + ([], ['USDT'], False, False, False, True, + ['ETH/USDT', 'LTC/USDT']), # all markets, quote=USDT, USD - ([], ['USDT', 'USD'], False, False, + ([], ['USDT', 'USD'], False, False, False, False, ['ETH/USDT', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), + # spot markets, quote=USDT, USD + ([], ['USDT', 'USD'], False, False, True, False, + ['ETH/USDT', 'LTC/USD', 'LTC/USDT']), # all markets, base=LTC, quote=USDT - (['LTC'], ['USDT'], False, False, + (['LTC'], ['USDT'], False, False, False, False, ['LTC/USDT', 'XLTCUSDT']), # all pairs, base=LTC, quote=USDT - (['LTC'], ['USDT'], True, False, + (['LTC'], ['USDT'], True, False, False, False, ['LTC/USDT']), # all markets, base=LTC, quote=USDT, NONEXISTENT - (['LTC'], ['USDT', 'NONEXISTENT'], False, False, + (['LTC'], ['USDT', 'NONEXISTENT'], False, False, False, False, ['LTC/USDT', 'XLTCUSDT']), # all markets, base=LTC, quote=NONEXISTENT - (['LTC'], ['NONEXISTENT'], False, False, + (['LTC'], ['NONEXISTENT'], False, False, False, False, []), ]) def test_get_markets(default_conf, mocker, markets_static, - base_currencies, quote_currencies, pairs_only, active_only, + base_currencies, quote_currencies, tradable_only, active_only, + spot_only, futures_only, expected_keys): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), @@ -2825,7 +2840,12 @@ def test_get_markets(default_conf, mocker, markets_static, validate_timeframes=MagicMock(), markets=PropertyMock(return_value=markets_static)) ex = Exchange(default_conf) - pairs = ex.get_markets(base_currencies, quote_currencies, pairs_only, active_only) + pairs = ex.get_markets(base_currencies, + quote_currencies, + tradable_only=tradable_only, + spot_only=spot_only, + futures_only=futures_only, + active_only=active_only) assert sorted(pairs.keys()) == sorted(expected_keys) @@ -2928,39 +2948,63 @@ def test_timeframe_to_next_date(): assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5) -@pytest.mark.parametrize("market_symbol,base,quote,exchange,add_dict,expected_result", [ - ("BTC/USDT", 'BTC', 'USDT', "binance", {}, True), - ("USDT/BTC", 'USDT', 'BTC', "binance", {}, True), - ("USDT/BTC", 'BTC', 'USDT', "binance", {}, False), # Reversed currencies - ("BTCUSDT", 'BTC', 'USDT', "binance", {}, False), # No seperating / - ("BTCUSDT", None, "USDT", "binance", {}, False), # - ("USDT/BTC", "BTC", None, "binance", {}, False), - ("BTCUSDT", "BTC", None, "binance", {}, False), - ("BTC/USDT", "BTC", "USDT", "binance", {}, True), - ("BTC/USDT", "USDT", "BTC", "binance", {}, False), # reversed currencies - ("BTC/USDT", "BTC", "USD", "binance", {}, False), # Wrong quote currency - ("BTC/", "BTC", 'UNK', "binance", {}, False), - ("/USDT", 'UNK', 'USDT', "binance", {}, False), - ("BTC/EUR", 'BTC', 'EUR', "kraken", {"darkpool": False}, True), - ("EUR/BTC", 'EUR', 'BTC', "kraken", {"darkpool": False}, True), - ("EUR/BTC", 'BTC', 'EUR', "kraken", {"darkpool": False}, False), # Reversed currencies - ("BTC/EUR", 'BTC', 'USD', "kraken", {"darkpool": False}, False), # wrong quote currency - ("BTC/EUR", 'BTC', 'EUR', "kraken", {"darkpool": True}, False), # no darkpools - ("BTC/EUR.d", 'BTC', 'EUR', "kraken", {"darkpool": True}, False), # no darkpools - ("BTC/USD", 'BTC', 'USD', "ftx", {'spot': True}, True), - ("USD/BTC", 'USD', 'BTC', "ftx", {'spot': True}, True), - ("BTC/USD", 'BTC', 'USDT', "ftx", {'spot': True}, False), # Wrong quote currency - ("BTC/USD", 'USD', 'BTC', "ftx", {'spot': True}, False), # Reversed currencies - ("BTC/USD", 'BTC', 'USD', "ftx", {'spot': False}, False), # Can only trade spot markets - ("BTC-PERP", 'BTC', 'USD', "ftx", {'spot': False}, False), # Can only trade spot markets -]) -def test_market_is_tradable(mocker, default_conf, market_symbol, base, - quote, add_dict, exchange, expected_result) -> None: +@pytest.mark.parametrize( + "market_symbol,base,quote,exchange,spot,margin,futures,trademode,add_dict,expected_result", + [ + ("BTC/USDT", 'BTC', 'USDT', "binance", True, False, False, 'spot', {}, True), + ("USDT/BTC", 'USDT', 'BTC', "binance", True, False, False, 'spot', {}, True), + # No seperating / + ("BTCUSDT", 'BTC', 'USDT', "binance", True, False, False, 'spot', {}, True), + ("BTCUSDT", None, "USDT", "binance", True, False, False, 'spot', {}, False), + ("USDT/BTC", "BTC", None, "binance", True, False, False, 'spot', {}, False), + ("BTCUSDT", "BTC", None, "binance", True, False, False, 'spot', {}, False), + ("BTC/USDT", "BTC", "USDT", "binance", True, False, False, 'spot', {}, True), + # Futures mode, spot pair + ("BTC/USDT", "BTC", "USDT", "binance", True, False, False, 'futures', {}, False), + ("BTC/USDT", "BTC", "USDT", "binance", True, False, False, 'margin', {}, False), + ("BTC/USDT", "BTC", "USDT", "binance", True, True, True, 'margin', {}, True), + ("BTC/USDT", "BTC", "USDT", "binance", False, True, False, 'margin', {}, True), + # Futures mode, futures pair + ("BTC/USDT", "BTC", "USDT", "binance", False, False, True, 'futures', {}, True), + # Futures market + ("BTC/UNK", "BTC", 'UNK', "binance", False, False, True, 'spot', {}, False), + ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, False, 'spot', {"darkpool": False}, True), + ("EUR/BTC", 'EUR', 'BTC', "kraken", True, False, False, 'spot', {"darkpool": False}, True), + # no darkpools + ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, False, 'spot', + {"darkpool": True}, False), + # no darkpools + ("BTC/EUR.d", 'BTC', 'EUR', "kraken", True, False, False, 'spot', + {"darkpool": True}, False), + ("BTC/USD", 'BTC', 'USD', "ftx", True, False, False, 'spot', {}, True), + ("USD/BTC", 'USD', 'BTC', "ftx", True, False, False, 'spot', {}, True), + # Can only trade spot markets + ("BTC/USD", 'BTC', 'USD', "ftx", False, False, True, 'spot', {}, False), + ("BTC/USD", 'BTC', 'USD', "ftx", False, False, True, 'futures', {}, True), + # Can only trade spot markets + ("BTC-PERP", 'BTC', 'USD', "ftx", False, False, True, 'spot', {}, False), + ("BTC-PERP", 'BTC', 'USD', "ftx", False, False, True, 'margin', {}, False), + ("BTC-PERP", 'BTC', 'USD', "ftx", False, False, True, 'futures', {}, True), + + ("BTC/USDT:USDT", 'BTC', 'USD', "okex", False, False, True, 'spot', {}, False), + ("BTC/USDT:USDT", 'BTC', 'USD', "okex", False, False, True, 'margin', {}, False), + ("BTC/USDT:USDT", 'BTC', 'USD', "okex", False, False, True, 'futures', {}, True), + ]) +def test_market_is_tradable( + mocker, default_conf, market_symbol, base, + quote, spot, margin, futures, trademode, add_dict, exchange, expected_result + ) -> None: + default_conf['trading_mode'] = trademode + mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_collateral') ex = get_patched_exchange(mocker, default_conf, id=exchange) market = { 'symbol': market_symbol, 'base': base, 'quote': quote, + 'spot': spot, + 'future': futures, + 'swap': futures, + 'margin': margin, **(add_dict), } assert ex.market_is_tradable(market) == expected_result