diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index f0c2dd00b..074dd2b10 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -20,4 +20,7 @@ class Bibox(Exchange): # fetchCurrencies API point requires authentication for Bibox, # so switch it off for Freqtrade load_markets() - _ccxt_config: Dict = {"has": {"fetchCurrencies": False}} + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {"has": {"fetchCurrencies": False}} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 572fa2141..60a1b8019 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -33,9 +33,27 @@ class Binance(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported - (TradingMode.FUTURES, Collateral.ISOLATED) + # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported ] + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + if self.trading_mode == TradingMode.MARGIN: + return { + "options": { + "defaultType": "margin" + } + } + elif self.trading_mode == TradingMode.FUTURES: + return { + "options": { + "defaultType": "future" + } + } + else: + return {} + 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/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8bbc88235..4021e7d02 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -49,9 +49,6 @@ class Exchange: _config: Dict = {} - # Parameters to add directly to ccxt sync/async initialization. - _ccxt_config: Dict = {} - # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) _params: Dict = {} @@ -131,21 +128,6 @@ class Exchange: self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] - # Initialize ccxt objects - ccxt_config = self._ccxt_config.copy() - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) - - self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) - - ccxt_async_config = self._ccxt_config.copy() - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), - ccxt_async_config) - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), - ccxt_async_config) - self._api_async = self._init_ccxt( - exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) - self.trading_mode: TradingMode = ( TradingMode(config.get('trading_mode')) if config.get('trading_mode') @@ -157,6 +139,21 @@ class Exchange: else None ) + # Initialize ccxt objects + ccxt_config = self._ccxt_config + ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) + ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) + + self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) + + ccxt_async_config = self._ccxt_config + ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), + ccxt_async_config) + ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), + ccxt_async_config) + self._api_async = self._init_ccxt( + exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + if self.trading_mode != TradingMode.SPOT: self.fill_leverage_brackets() @@ -210,7 +207,7 @@ class Exchange: 'secret': exchange_config.get('secret'), 'password': exchange_config.get('password'), 'uid': exchange_config.get('uid', ''), - 'options': exchange_config.get('options', {}) + # 'options': exchange_config.get('options', {}) } if ccxt_kwargs: logger.info('Applying additional ccxt config: %s', ccxt_kwargs) @@ -231,6 +228,11 @@ class Exchange: return api + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {} + @property def name(self) -> str: """exchange Name (from ccxt)""" @@ -258,13 +260,6 @@ class Exchange: """exchange ccxt precisionMode""" return self._api.precisionMode - @property - def running_live_mode(self) -> bool: - return ( - self._config['runmode'].value not in ('backtest', 'hyperopt') and - not self._config['dry_run'] - ) - def _log_exchange_response(self, endpoint, response) -> None: """ Log exchange responses """ if self.log_responses: @@ -624,12 +619,12 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return self._divide_stake_amount_by_leverage( + return self._get_stake_amount_considering_leverage( max(min_stake_amounts) * amount_reserve_percent, leverage or 1.0 ) - def _divide_stake_amount_by_leverage(self, stake_amount: float, leverage: float): + def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float): """ Takes the minimum stake amount for a pair with no leverage and returns the minimum stake amount when leverage is considered @@ -1603,7 +1598,7 @@ class Exchange: def fill_leverage_brackets(self): """ - #TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken + # TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ diff --git a/tests/conftest.py b/tests/conftest.py index 3de299752..d2f24fa69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo -from freqtrade.enums import RunMode +from freqtrade.enums import Collateral, RunMode, TradingMode from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db @@ -81,7 +81,13 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: +def patch_exchange( + mocker, + api_mock=None, + id='binance', + mock_markets=True, + mock_supported_modes=True +) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -90,10 +96,22 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No 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)) + if mock_markets: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) + if mock_supported_modes: + mocker.patch( + f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_collateral_pairs', + PropertyMock(return_value=[ + (TradingMode.MARGIN, Collateral.CROSS), + (TradingMode.MARGIN, Collateral.ISOLATED), + (TradingMode.FUTURES, Collateral.CROSS), + (TradingMode.FUTURES, Collateral.ISOLATED) + ]) + ) + if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: @@ -101,8 +119,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No def get_patched_exchange(mocker, config, api_mock=None, id='binance', - mock_markets=True) -> Exchange: - patch_exchange(mocker, api_mock, id, mock_markets) + mock_markets=True, mock_supported_modes=True) -> Exchange: + patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) config['exchange']['name'] = id try: exchange = ExchangeResolver.load_exchange(id, config) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 4999e94af..cbbace1db 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -336,3 +336,15 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): assert exchange._api_async.fetch_ohlcv.call_count == 2 assert res == ohlcv assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) + + +@pytest.mark.parametrize("trading_mode,collateral,config", [ + ("", "", {}), + ("margin", "cross", {"options": {"defaultType": "margin"}}), + ("futures", "isolated", {"options": {"defaultType": "future"}}), +]) +def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = collateral + exchange = get_patched_exchange(mocker, default_conf, id="binance") + assert exchange._ccxt_config == config diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ce09e31e7..8b16a9f12 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -132,10 +132,9 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) assert ex._api.headers == {'hello': 'world'} + assert ex._ccxt_config == {} Exchange._headers = {} - # TODO-lev: Test with options - def test_destroy(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) @@ -1116,6 +1115,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange._set_leverage = MagicMock() + exchange.set_margin_mode = MagicMock() order = exchange.create_order( pair='ETH/BTC', @@ -1134,10 +1135,10 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert api_mock.create_order.call_args[0][2] == side assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] is rate + assert exchange._set_leverage.call_count == 0 + assert exchange.set_margin_mode.call_count == 0 - assert api_mock._set_leverage.call_count == 0 if side == "buy" else 1 - assert api_mock.set_margin_mode.call_count == 0 if side == "buy" else 1 - + exchange.trading_mode = TradingMode.FUTURES order = exchange.create_order( pair='ETH/BTC', ordertype=ordertype, @@ -1147,8 +1148,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, leverage=3.0 ) - assert api_mock._set_leverage.call_count == 1 - assert api_mock.set_margin_mode.call_count == 1 + assert exchange._set_leverage.call_count == 1 + assert exchange.set_margin_mode.call_count == 1 def test_buy_dry_run(default_conf, mocker): @@ -3042,7 +3043,6 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: (3, 5, 5), (4, 5, 2), (5, 5, 1), - ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected @@ -3054,7 +3054,7 @@ def test_calculate_backoff(retrycount, max_retries, expected): (20.0, 5.0, 4.0), (100.0, 100.0, 1.0) ]) -def test_divide_stake_amount_by_leverage( +def test_get_stake_amount_considering_leverage( exchange, stake_amount, leverage, @@ -3063,7 +3063,8 @@ def test_divide_stake_amount_by_leverage( default_conf ): exchange = get_patched_exchange(mocker, default_conf, id=exchange) - assert exchange._divide_stake_amount_by_leverage(stake_amount, leverage) == min_stake_with_lev + assert exchange._get_stake_amount_considering_leverage( + stake_amount, leverage) == min_stake_with_lev @pytest.mark.parametrize("exchange_name,trading_mode", [ @@ -3132,6 +3133,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Remove once implemented ("binance", TradingMode.MARGIN, Collateral.CROSS, True), ("binance", TradingMode.FUTURES, Collateral.CROSS, True), + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), @@ -3140,7 +3142,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Uncomment once implemented # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), - ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), @@ -3154,7 +3156,8 @@ def test_validate_trading_mode_and_collateral( collateral, exception_thrown ): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange( + mocker, default_conf, id=exchange_name, mock_supported_modes=False) if (exception_thrown): with pytest.raises(OperationalException): exchange.validate_trading_mode_and_collateral(trading_mode, collateral)