diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2703c99bf..2eac0d05a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -74,7 +74,6 @@ class Exchange: "mark_ohlcv_price": "mark", "mark_ohlcv_timeframe": "8h", "ccxt_futures_name": "swap", - "can_fetch_multiple_tiers": True, } _ft_has: Dict = {} @@ -1874,19 +1873,63 @@ class Exchange: raise OperationalException(e) from e @retrier + def get_leverage_tiers(self) -> Dict[str, List[Dict]]: + try: + return self._api.fetch_leverage_tiers() + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not load leverage tiers due to {e.__class__.__name__}. Message: {e}' + ) from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + @retrier + def get_market_leverage_tiers(self, symbol) -> List[Dict]: + try: + return self._api.fetch_market_leverage_tiers(symbol) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not load leverage tiers for {symbol}' + f' due to {e.__class__.__name__}. Message: {e}' + ) from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + def load_leverage_tiers(self) -> Dict[str, List[Dict]]: - if self.trading_mode == TradingMode.FUTURES and self.exchange_has('fetchLeverageTiers'): - try: - return self._api.fetch_leverage_tiers() - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not load leverage tiers due to {e.__class__.__name__}.' - f'Message: {e}' - ) from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + if self.trading_mode == TradingMode.FUTURES: + if self.exchange_has('fetchLeverageTiers'): + # Fetch all leverage tiers at once + return self.get_leverage_tiers() + elif self.exchange_has('fetchMarketLeverageTiers'): + # Must fetch the leverage tiers for each market separately + # * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets + markets = self.markets + symbols = [] + + for symbol, market in markets.items(): + if (self.market_is_future(market) + and market['quote'] == self._config['stake_currency']): + symbols.append(symbol) + + tiers: Dict[str, List[Dict]] = {} + + # Be verbose here, as this delays startup by ~1 minute. + logger.info( + f"Initializing leverage_tiers for {len(symbols)} markets. " + "This will take about a minute.") + + for symbol in sorted(symbols): + tiers[symbol] = self.get_market_leverage_tiers(symbol) + + logger.info(f"Done initializing {len(symbols)} markets.") + + return tiers + else: + return {} else: return {} diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 305bf1547..1c5b43cb7 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -1,6 +1,6 @@ """ Gate.io exchange subclass """ import logging -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Tuple from freqtrade.enums import MarginMode, TradingMode from freqtrade.exceptions import OperationalException @@ -40,26 +40,3 @@ class Gateio(Exchange): if any(v == 'market' for k, v in order_types.items()): raise OperationalException( f'Exchange {self.name} does not support market orders.') - - def get_maintenance_ratio_and_amt( - self, - pair: str, - nominal_value: Optional[float] = 0.0, - ) -> Tuple[float, Optional[float]]: - """ - :return: The maintenance margin ratio and maintenance amount - """ - info = self.markets[pair]['info'] - return (float(info['maintenance_rate']), None) - - def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float: - """ - Returns the maximum leverage that a pair can be traded at - :param pair: The base/quote currency pair being traded - :param nominal_value: The total value of the trade in quote currency (margin_mode + debt) - """ - market = self.markets[pair] - if market['limits']['leverage']['max'] is not None: - return market['limits']['leverage']['max'] - else: - return 1.0 diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 8bdd81b14..08c29c7b2 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -22,7 +22,6 @@ class Okx(Exchange): "ohlcv_candle_limit": 300, "mark_ohlcv_timeframe": "4h", "funding_fee_timeframe": "8h", - "can_fetch_multiple_tiers": False, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ @@ -93,31 +92,3 @@ class Okx(Exchange): pair_tiers = self._leverage_tiers[pair] return pair_tiers[-1]['max'] / leverage - - @retrier - def load_leverage_tiers(self) -> Dict[str, List[Dict]]: - # * This is slow(~45s) on Okex, must make 90-some api calls to load all linear swap markets - if self.trading_mode == TradingMode.FUTURES: - markets = self.markets - symbols = [] - - for symbol, market in markets.items(): - if (self.market_is_future(market) - and market['quote'] == self._config['stake_currency']): - symbols.append(symbol) - - tiers: Dict[str, List[Dict]] = {} - - # Be verbose here, as this delays startup by ~1 minute. - logger.info( - f"Initializing leverage_tiers for {len(symbols)} markets. " - "This will take about a minute.") - - for symbol in sorted(symbols): - res = self._api.fetch_leverage_tiers(symbol) - tiers[symbol] = res[symbol] - logger.info(f"Done initializing {len(symbols)} markets.") - - return tiers - else: - return {} diff --git a/requirements.txt b/requirements.txt index 28f31c3a6..2392a05ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.2 pandas==1.4.1 pandas-ta==0.3.14b -ccxt==1.73.70 +ccxt==1.74.22 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 6bf555867..52f39a274 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -50,7 +50,7 @@ EXCHANGES = { 'hasQuoteVolume': True, 'timeframe': '5m', 'futures_pair': 'BTC/USD:USD', - 'futures': True, + 'futures': False, 'leverage_tiers_public': False, # TODO: Set to True once implemented on CCXT 'leverage_in_spot_market': True, }, @@ -69,7 +69,7 @@ EXCHANGES = { 'timeframe': '5m', 'futures': True, 'futures_pair': 'BTC/USDT:USDT', - 'leverage_tiers_public': False, # TODO-lev: Set to True once implemented on CCXT + 'leverage_tiers_public': True, 'leverage_in_spot_market': True, }, 'okx': { @@ -123,9 +123,6 @@ def exchange_futures(request, exchange_conf, class_mocker): exchange_conf['margin_mode'] = 'isolated' exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency'] - # TODO-lev: This mock should no longer be necessary once futures are enabled. - class_mocker.patch( - 'freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode') class_mocker.patch( 'freqtrade.exchange.binance.Binance.fill_leverage_tiers') diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py index 9f65560a5..6f7862909 100644 --- a/tests/exchange/test_gateio.py +++ b/tests/exchange/test_gateio.py @@ -1,11 +1,8 @@ -from unittest.mock import MagicMock, PropertyMock - import pytest from freqtrade.exceptions import OperationalException from freqtrade.exchange import Gateio from freqtrade.resolvers.exchange_resolver import ExchangeResolver -from tests.conftest import get_patched_exchange def test_validate_order_types_gateio(default_conf, mocker): @@ -29,51 +26,3 @@ def test_validate_order_types_gateio(default_conf, mocker): with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): ExchangeResolver.load_exchange('gateio', default_conf, True) - - -@pytest.mark.parametrize('pair,mm_ratio', [ - ("ETH/USDT:USDT", 0.005), - ("ADA/USDT:USDT", 0.003), -]) -def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio): - api_mock = MagicMock() - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio") - mocker.patch( - 'freqtrade.exchange.Exchange.markets', - PropertyMock( - return_value={ - 'ETH/USDT:USDT': { - 'taker': 0.0000075, - 'maker': -0.0000025, - 'info': { - 'maintenance_rate': '0.005', - }, - 'id': 'ETH_USDT', - 'symbol': 'ETH/USDT:USDT', - }, - 'ADA/USDT:USDT': { - 'taker': 0.0000075, - 'maker': -0.0000025, - 'info': { - 'maintenance_rate': '0.003', - }, - 'id': 'ADA_USDT', - 'symbol': 'ADA/USDT:USDT', - }, - } - ) - ) - assert exchange.get_maintenance_ratio_and_amt(pair) == (mm_ratio, None) - - -@pytest.mark.parametrize('pair,nominal_value,max_lev', [ - ("ETH/BTC", 0.0, 2.0), - ("TKN/BTC", 100.0, 5.0), - ("BLK/BTC", 173.31, 3.0), - ("LTC/BTC", 0.0, 1.0), - ("TKN/USDT", 210.30, 1.0), -]) -def test_get_max_leverage_gateio(default_conf, mocker, pair, nominal_value, max_lev): - # Binance has a different method of getting the max leverage - exchange = get_patched_exchange(mocker, default_conf, id="gateio") - assert exchange.get_max_leverage(pair, nominal_value) == max_lev diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index 035e08f26..8ecdf6904 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock # , PropertyMock +from unittest.mock import MagicMock, PropertyMock from freqtrade.enums import MarginMode, TradingMode from tests.conftest import get_patched_exchange @@ -172,135 +172,135 @@ def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers): def test_load_leverage_tiers_okx(default_conf, mocker, markets): api_mock = MagicMock() - api_mock.fetch_leverage_tiers = MagicMock(side_effect=[ - { - 'ADA/USDT:USDT': [ - { - 'tier': 1, - 'notionalFloor': 0, - 'notionalCap': 500, - 'maintenanceMarginRate': 0.02, - 'maxLeverage': 75, - 'info': { - 'baseMaxLoan': '', - 'imr': '0.013', - 'instId': '', - 'maxLever': '75', - 'maxSz': '500', - 'minSz': '0', - 'mmr': '0.01', - 'optMgnFactor': '0', - 'quoteMaxLoan': '', - 'tier': '1', - 'uly': 'ADA-USDT' - } - }, - { - 'tier': 2, - 'notionalFloor': 501, - 'notionalCap': 1000, - 'maintenanceMarginRate': 0.025, - 'maxLeverage': 50, - 'info': { - 'baseMaxLoan': '', - 'imr': '0.02', - 'instId': '', - 'maxLever': '50', - 'maxSz': '1000', - 'minSz': '501', - 'mmr': '0.015', - 'optMgnFactor': '0', - 'quoteMaxLoan': '', - 'tier': '2', - 'uly': 'ADA-USDT' - } - }, - { - 'tier': 3, - 'notionalFloor': 1001, - 'notionalCap': 2000, - 'maintenanceMarginRate': 0.03, - 'maxLeverage': 20, - 'info': { - 'baseMaxLoan': '', - 'imr': '0.05', - 'instId': '', - 'maxLever': '20', - 'maxSz': '2000', - 'minSz': '1001', - 'mmr': '0.02', - 'optMgnFactor': '0', - 'quoteMaxLoan': '', - 'tier': '3', - 'uly': 'ADA-USDT' - } - }, - ] - }, - { - 'ETH/USDT:USDT': [ - { - 'tier': 1, - 'notionalFloor': 0, - 'notionalCap': 2000, - 'maintenanceMarginRate': 0.01, - 'maxLeverage': 75, - 'info': { - 'baseMaxLoan': '', - 'imr': '0.013', - 'instId': '', - 'maxLever': '75', - 'maxSz': '2000', - 'minSz': '0', - 'mmr': '0.01', - 'optMgnFactor': '0', - 'quoteMaxLoan': '', - 'tier': '1', - 'uly': 'ETH-USDT' - } - }, - { - 'tier': 2, - 'notionalFloor': 2001, - 'notionalCap': 4000, - 'maintenanceMarginRate': 0.015, - 'maxLeverage': 50, - 'info': { - 'baseMaxLoan': '', - 'imr': '0.02', - 'instId': '', - 'maxLever': '50', - 'maxSz': '4000', - 'minSz': '2001', - 'mmr': '0.015', - 'optMgnFactor': '0', - 'quoteMaxLoan': '', - 'tier': '2', - 'uly': 'ETH-USDT' - } - }, - { - 'tier': 3, - 'notionalFloor': 4001, - 'notionalCap': 8000, - 'maintenanceMarginRate': 0.02, - 'maxLeverage': 20, - 'info': { - 'baseMaxLoan': '', - 'imr': '0.05', - 'instId': '', - 'maxLever': '20', - 'maxSz': '8000', - 'minSz': '4001', - 'mmr': '0.02', - 'optMgnFactor': '0', - 'quoteMaxLoan': '', - 'tier': '3', - 'uly': 'ETH-USDT' - } - }, - ] - }, + type(api_mock).has = PropertyMock(return_value={ + 'fetchLeverageTiers': False, + 'fetchMarketLeverageTiers': True, + }) + api_mock.fetch_market_leverage_tiers = MagicMock(side_effect=[ + [ + { + 'tier': 1, + 'notionalFloor': 0, + 'notionalCap': 500, + 'maintenanceMarginRate': 0.02, + 'maxLeverage': 75, + 'info': { + 'baseMaxLoan': '', + 'imr': '0.013', + 'instId': '', + 'maxLever': '75', + 'maxSz': '500', + 'minSz': '0', + 'mmr': '0.01', + 'optMgnFactor': '0', + 'quoteMaxLoan': '', + 'tier': '1', + 'uly': 'ADA-USDT' + } + }, + { + 'tier': 2, + 'notionalFloor': 501, + 'notionalCap': 1000, + 'maintenanceMarginRate': 0.025, + 'maxLeverage': 50, + 'info': { + 'baseMaxLoan': '', + 'imr': '0.02', + 'instId': '', + 'maxLever': '50', + 'maxSz': '1000', + 'minSz': '501', + 'mmr': '0.015', + 'optMgnFactor': '0', + 'quoteMaxLoan': '', + 'tier': '2', + 'uly': 'ADA-USDT' + } + }, + { + 'tier': 3, + 'notionalFloor': 1001, + 'notionalCap': 2000, + 'maintenanceMarginRate': 0.03, + 'maxLeverage': 20, + 'info': { + 'baseMaxLoan': '', + 'imr': '0.05', + 'instId': '', + 'maxLever': '20', + 'maxSz': '2000', + 'minSz': '1001', + 'mmr': '0.02', + 'optMgnFactor': '0', + 'quoteMaxLoan': '', + 'tier': '3', + 'uly': 'ADA-USDT' + } + }, + ], + [ + { + 'tier': 1, + 'notionalFloor': 0, + 'notionalCap': 2000, + 'maintenanceMarginRate': 0.01, + 'maxLeverage': 75, + 'info': { + 'baseMaxLoan': '', + 'imr': '0.013', + 'instId': '', + 'maxLever': '75', + 'maxSz': '2000', + 'minSz': '0', + 'mmr': '0.01', + 'optMgnFactor': '0', + 'quoteMaxLoan': '', + 'tier': '1', + 'uly': 'ETH-USDT' + } + }, + { + 'tier': 2, + 'notionalFloor': 2001, + 'notionalCap': 4000, + 'maintenanceMarginRate': 0.015, + 'maxLeverage': 50, + 'info': { + 'baseMaxLoan': '', + 'imr': '0.02', + 'instId': '', + 'maxLever': '50', + 'maxSz': '4000', + 'minSz': '2001', + 'mmr': '0.015', + 'optMgnFactor': '0', + 'quoteMaxLoan': '', + 'tier': '2', + 'uly': 'ETH-USDT' + } + }, + { + 'tier': 3, + 'notionalFloor': 4001, + 'notionalCap': 8000, + 'maintenanceMarginRate': 0.02, + 'maxLeverage': 20, + 'info': { + 'baseMaxLoan': '', + 'imr': '0.05', + 'instId': '', + 'maxLever': '20', + 'maxSz': '8000', + 'minSz': '4001', + 'mmr': '0.02', + 'optMgnFactor': '0', + 'quoteMaxLoan': '', + 'tier': '3', + 'uly': 'ETH-USDT' + } + }, + ] ]) default_conf['trading_mode'] = 'futures' default_conf['margin_mode'] = 'isolated'