From 8990097d6fe359e8de247963add203cfa8daaa5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 31 Oct 2021 18:58:59 +0100 Subject: [PATCH 01/10] Enrich markets mock with "type" and "spot" info --- tests/conftest.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 60d620639..af5468f5b 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,8 @@ def get_markets(): 'symbol': 'ETH/USDT', 'base': 'ETH', 'quote': 'USDT', + 'spot': True, + 'type': 'spot', 'precision': { 'amount': 8, 'price': 8 @@ -785,6 +801,8 @@ def get_markets(): 'base': 'LTC', 'quote': 'USDT', 'active': False, + 'spot': True, + 'type': 'spot', 'precision': { 'amount': 8, 'price': 8 @@ -807,6 +825,8 @@ def get_markets(): 'base': 'XRP', 'quote': 'USDT', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -832,6 +852,8 @@ def get_markets(): 'base': 'NEO', 'quote': 'USDT', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -857,6 +879,8 @@ def get_markets(): 'base': 'TKN', 'quote': 'USDT', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'price': 8, 'amount': 8, @@ -882,6 +906,8 @@ def get_markets(): 'base': 'LTC', 'quote': 'USD', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'amount': 8, 'price': 8 @@ -904,6 +930,8 @@ def get_markets(): 'base': 'LTC', 'quote': 'USDT', 'active': True, + 'spot': False, + 'type': 'SomethingElse', 'precision': { 'amount': 8, 'price': 8 @@ -926,6 +954,8 @@ def get_markets(): 'base': 'LTC', 'quote': 'ETH', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'base': 8, 'quote': 8, @@ -976,6 +1006,8 @@ def shitcoinmarkets(markets_static): 'base': 'HOT', 'quote': 'BTC', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'base': 8, 'quote': 8, @@ -1004,6 +1036,8 @@ def shitcoinmarkets(markets_static): 'base': 'FUEL', 'quote': 'BTC', 'active': True, + 'spot': True, + 'type': 'spot', 'precision': { 'base': 8, 'quote': 8, From 3fac5c5bcd93c1ae55c569b30a79fc71c7b63225 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Nov 2021 08:40:55 +0100 Subject: [PATCH 02/10] Update list-markets to work for futures/margin as well --- freqtrade/commands/list_commands.py | 23 ++++++++++--------- freqtrade/exchange/exchange.py | 35 ++++++++++++++++++++--------- freqtrade/exchange/ftx.py | 9 -------- 3 files changed, 38 insertions(+), 29 deletions(-) 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/exchange/exchange.py b/freqtrade/exchange/exchange.py index a2fbfba02..fed464712 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -280,7 +280,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. @@ -295,8 +297,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 @@ -320,18 +328,25 @@ class Exchange: """ return self.markets.get(pair, {}).get('base', '') + def market_is_future(self, market: Dict[str, Any]) -> bool: + return market.get('future', 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 ( + (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 2acf32ba3..4b3e765bb 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -29,15 +29,6 @@ class Ftx(Exchange): # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported ] - 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: """ From 534b0a5911d131ceb8c85517450ca4174b59f8ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Nov 2021 09:12:39 +0100 Subject: [PATCH 03/10] Some tests for new market checking --- tests/conftest.py | 4 ++ tests/exchange/test_exchange.py | 104 +++++++++++++++++++------------- 2 files changed, 67 insertions(+), 41 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index af5468f5b..75654a83a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -777,6 +777,8 @@ def get_markets(): 'base': 'ETH', 'quote': 'USDT', 'spot': True, + 'future': True, + 'margin': True, 'type': 'spot', 'precision': { 'amount': 8, @@ -802,6 +804,8 @@ def get_markets(): 'quote': 'USDT', 'active': False, 'spot': True, + 'future': True, + 'margin': True, 'type': 'spot', 'precision': { 'amount': 8, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8e3fdfe74..aa07037c1 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) @@ -2926,39 +2946,41 @@ 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 +@pytest.mark.parametrize("market_symbol,base,quote,exchange,spot,futures,add_dict,expected_result", [ + ("BTC/USDT", 'BTC', 'USDT', "binance", True, False, {}, True), + ("USDT/BTC", 'USDT', 'BTC', "binance", True, False, {}, True), + ("USDT/BTC", 'BTC', 'USDT', "binance", True, False, {}, False), # Reversed currencies + ("BTCUSDT", 'BTC', 'USDT', "binance", True, False, {}, False), # No seperating / + ("BTCUSDT", None, "USDT", "binance", True, False, {}, False), # + ("USDT/BTC", "BTC", None, "binance", True, False, {}, False), + ("BTCUSDT", "BTC", None, "binance", True, False, {}, False), + ("BTC/USDT", "BTC", "USDT", "binance", True, False, {}, True), + ("BTC/USDT", "USDT", "BTC", "binance", True, False, {}, False), # reversed currencies + ("BTC/USDT", "BTC", "USD", "binance", True, False, {}, False), # Wrong quote currency + ("BTC/", "BTC", 'UNK', "binance", True, False, {}, False), + ("/USDT", 'UNK', 'USDT', "binance", True, False, {}, False), + ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, {"darkpool": False}, True), + ("EUR/BTC", 'EUR', 'BTC', "kraken", True, False, {"darkpool": False}, True), + ("EUR/BTC", 'BTC', 'EUR', "kraken", True, False, {"darkpool": False}, False), # Reversed currencies + ("BTC/EUR", 'BTC', 'USD', "kraken", True, False, {"darkpool": False}, False), # wrong quote currency + ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, {"darkpool": True}, False), # no darkpools + ("BTC/EUR.d", 'BTC', 'EUR', "kraken", True, False, {"darkpool": True}, False), # no darkpools + ("BTC/USD", 'BTC', 'USD', "ftx", True, False, {'spot': True}, True), + ("USD/BTC", 'USD', 'BTC', "ftx", True, False, {'spot': True}, True), + ("BTC/USD", 'BTC', 'USDT', "ftx", True, False, {'spot': True}, False), # Wrong quote currency + ("BTC/USD", 'USD', 'BTC', "ftx", True, False, {'spot': True}, False), # Reversed currencies + ("BTC/USD", 'BTC', 'USD', "ftx", False, True, {'spot': False}, False), # Can only trade spot markets + ("BTC-PERP", 'BTC', 'USD', "ftx", False, True, {'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: + quote, spot, futures, add_dict, exchange, expected_result) -> None: ex = get_patched_exchange(mocker, default_conf, id=exchange) market = { 'symbol': market_symbol, 'base': base, 'quote': quote, + 'spot': spot, + 'futures': futures, **(add_dict), } assert ex.market_is_tradable(market) == expected_result From 0dd9a277d36b1b4bce430765bd6a30402748644e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Nov 2021 09:24:00 +0100 Subject: [PATCH 04/10] improve market_is_tradable tests --- freqtrade/exchange/exchange.py | 4 +++- tests/exchange/test_exchange.py | 12 ++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fed464712..85b156746 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -343,7 +343,9 @@ class Exchange: Ensures that Configured mode aligns to """ return ( - (self.trading_mode == TradingMode.SPOT and self.market_is_spot(market)) + 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)) ) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index aa07037c1..4f4ba78fd 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2949,26 +2949,18 @@ def test_timeframe_to_next_date(): @pytest.mark.parametrize("market_symbol,base,quote,exchange,spot,futures,add_dict,expected_result", [ ("BTC/USDT", 'BTC', 'USDT', "binance", True, False, {}, True), ("USDT/BTC", 'USDT', 'BTC', "binance", True, False, {}, True), - ("USDT/BTC", 'BTC', 'USDT', "binance", True, False, {}, False), # Reversed currencies - ("BTCUSDT", 'BTC', 'USDT', "binance", True, False, {}, False), # No seperating / + ("BTCUSDT", 'BTC', 'USDT', "binance", True, False, {}, True), # No seperating / ("BTCUSDT", None, "USDT", "binance", True, False, {}, False), # ("USDT/BTC", "BTC", None, "binance", True, False, {}, False), ("BTCUSDT", "BTC", None, "binance", True, False, {}, False), ("BTC/USDT", "BTC", "USDT", "binance", True, False, {}, True), - ("BTC/USDT", "USDT", "BTC", "binance", True, False, {}, False), # reversed currencies - ("BTC/USDT", "BTC", "USD", "binance", True, False, {}, False), # Wrong quote currency - ("BTC/", "BTC", 'UNK', "binance", True, False, {}, False), - ("/USDT", 'UNK', 'USDT', "binance", True, False, {}, False), + ("BTC/UNK", "BTC", 'UNK', "binance", False, True, {}, False), # Futures market ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, {"darkpool": False}, True), ("EUR/BTC", 'EUR', 'BTC', "kraken", True, False, {"darkpool": False}, True), - ("EUR/BTC", 'BTC', 'EUR', "kraken", True, False, {"darkpool": False}, False), # Reversed currencies - ("BTC/EUR", 'BTC', 'USD', "kraken", True, False, {"darkpool": False}, False), # wrong quote currency ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, {"darkpool": True}, False), # no darkpools ("BTC/EUR.d", 'BTC', 'EUR', "kraken", True, False, {"darkpool": True}, False), # no darkpools ("BTC/USD", 'BTC', 'USD', "ftx", True, False, {'spot': True}, True), ("USD/BTC", 'USD', 'BTC', "ftx", True, False, {'spot': True}, True), - ("BTC/USD", 'BTC', 'USDT', "ftx", True, False, {'spot': True}, False), # Wrong quote currency - ("BTC/USD", 'USD', 'BTC', "ftx", True, False, {'spot': True}, False), # Reversed currencies ("BTC/USD", 'BTC', 'USD', "ftx", False, True, {'spot': False}, False), # Can only trade spot markets ("BTC-PERP", 'BTC', 'USD', "ftx", False, True, {'spot': False}, False), # Can only trade spot markets ]) From bfe3760f683fe31b281bc22a3550b0723a3e8ac0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Nov 2021 09:33:55 +0100 Subject: [PATCH 05/10] Add tests for margin mode --- tests/exchange/test_exchange.py | 65 +++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4f4ba78fd..29a566747 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2946,33 +2946,58 @@ 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,spot,futures,add_dict,expected_result", [ - ("BTC/USDT", 'BTC', 'USDT', "binance", True, False, {}, True), - ("USDT/BTC", 'USDT', 'BTC', "binance", True, False, {}, True), - ("BTCUSDT", 'BTC', 'USDT', "binance", True, False, {}, True), # No seperating / - ("BTCUSDT", None, "USDT", "binance", True, False, {}, False), # - ("USDT/BTC", "BTC", None, "binance", True, False, {}, False), - ("BTCUSDT", "BTC", None, "binance", True, False, {}, False), - ("BTC/USDT", "BTC", "USDT", "binance", True, False, {}, True), - ("BTC/UNK", "BTC", 'UNK', "binance", False, True, {}, False), # Futures market - ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, {"darkpool": False}, True), - ("EUR/BTC", 'EUR', 'BTC', "kraken", True, False, {"darkpool": False}, True), - ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, {"darkpool": True}, False), # no darkpools - ("BTC/EUR.d", 'BTC', 'EUR', "kraken", True, False, {"darkpool": True}, False), # no darkpools - ("BTC/USD", 'BTC', 'USD', "ftx", True, False, {'spot': True}, True), - ("USD/BTC", 'USD', 'BTC', "ftx", True, False, {'spot': True}, True), - ("BTC/USD", 'BTC', 'USD', "ftx", False, True, {'spot': False}, False), # Can only trade spot markets - ("BTC-PERP", 'BTC', 'USD', "ftx", False, True, {'spot': False}, False), # Can only trade spot markets +@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), ]) -def test_market_is_tradable(mocker, default_conf, market_symbol, base, - quote, spot, futures, add_dict, exchange, expected_result) -> None: +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, - 'futures': futures, + 'future': futures, + 'margin': margin, **(add_dict), } assert ex.market_is_tradable(market) == expected_result From 11b77cf94c8cc89bc09fa9071313528ff9b0becc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Nov 2021 09:46:35 +0100 Subject: [PATCH 06/10] Update test to new list-pairs format --- freqtrade/exchange/binance.py | 2 ++ freqtrade/exchange/ftx.py | 1 - tests/commands/test_commands.py | 6 +++--- tests/exchange/test_exchange.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d23f84e7b..b71b58151 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -178,6 +178,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/ftx.py b/freqtrade/exchange/ftx.py index 4b3e765bb..066bd7704 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -29,7 +29,6 @@ class Ftx(Exchange): # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported ] - def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) 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/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 29a566747..0d033008a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2983,7 +2983,7 @@ def test_timeframe_to_next_date(): ("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), -]) + ]) def test_market_is_tradable( mocker, default_conf, market_symbol, base, quote, spot, margin, futures, trademode, add_dict, exchange, expected_result From 6cc3f65a833196339c5ab7b57c539149ff94c16f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Nov 2021 10:42:39 +0100 Subject: [PATCH 07/10] Add --trading-mode parameter --- freqtrade/commands/arguments.py | 3 ++- freqtrade/commands/cli_options.py | 6 +++++- freqtrade/configuration/configuration.py | 2 ++ freqtrade/exchange/exchange.py | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) 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/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/exchange.py b/freqtrade/exchange/exchange.py index 85b156746..c55c7ffbb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -329,7 +329,7 @@ class Exchange: return self.markets.get(pair, {}).get('base', '') def market_is_future(self, market: Dict[str, Any]) -> bool: - return market.get('future', False) is True + return market.get('future', False) is True or market.get('futures') is True def market_is_spot(self, market: Dict[str, Any]) -> bool: return market.get('spot', False) is True From 75eccea88df2ad8183b2859cb8d48d278a07db74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Nov 2021 19:43:43 +0100 Subject: [PATCH 08/10] Improve futures detection, add ccxt-compat test --- freqtrade/exchange/binance.py | 6 +++++- freqtrade/exchange/exchange.py | 2 +- freqtrade/exchange/ftx.py | 4 ++++ tests/conftest.py | 2 ++ tests/exchange/test_ccxt_compat.py | 23 ++++++++++++++++++++++- tests/exchange/test_exchange.py | 5 +++++ 6 files changed, 39 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 1cbbffc51..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): """ diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0b47c98b8..0acd10900 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -340,7 +340,7 @@ class Exchange: return self.markets.get(pair, {}).get('base', '') def market_is_future(self, market: Dict[str, Any]) -> bool: - return market.get('future', False) is True or market.get('futures') is True + return market.get('swap', False) is True def market_is_spot(self, market: Dict[str, Any]) -> bool: return market.get('spot', False) is True diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 8347993ee..e798f2c29 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -159,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/tests/conftest.py b/tests/conftest.py index 74dd6f360..7e5dd47e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -778,6 +778,7 @@ def get_markets(): 'quote': 'USDT', 'spot': True, 'future': True, + 'swap': True, 'margin': True, 'type': 'spot', 'precision': { @@ -805,6 +806,7 @@ def get_markets(): 'active': False, 'spot': True, 'future': True, + 'swap': True, 'margin': True, 'type': 'spot', 'precision': { diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index b14df070c..59f144bab 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -26,6 +26,7 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'hasQuoteVolume': True, 'timeframe': '5m', + 'futures': True, }, 'kraken': { 'pair': 'BTC/USDT', @@ -82,13 +83,19 @@ 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['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 +110,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 12c20a7ca..e974cbd43 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2985,6 +2985,10 @@ def test_timeframe_to_next_date(): ("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, @@ -2999,6 +3003,7 @@ def test_market_is_tradable( 'quote': quote, 'spot': spot, 'future': futures, + 'swap': futures, 'margin': margin, **(add_dict), } From 5cfc385d44d017acc23bbd422f02c4223bcaf834 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Nov 2021 06:40:29 +0100 Subject: [PATCH 09/10] Versionbump ccxt --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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', From ce3c5b32f90e6ce5d06a77d1b9c16ccfb8d4bec8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Nov 2021 07:04:56 +0100 Subject: [PATCH 10/10] Fix test leakage in ccxt_compat --- 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 59f144bab..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 @@ -87,6 +88,7 @@ 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'