From 63c732a5604409a42f4333bfb25fe77873a72c2a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 30 Dec 2022 07:47:00 +0100 Subject: [PATCH 01/24] Bybit futures data download --- freqtrade/exchange/bybit.py | 28 +++++++++++++++++++++++- tests/exchange/test_bybit.py | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 tests/exchange/test_bybit.py diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index d14c7c192..dd3d74e84 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,9 +1,10 @@ """ Bybit exchange subclass """ import logging -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple from freqtrade.enums import MarginMode, TradingMode from freqtrade.exchange import Exchange +from freqtrade.exchange.exchange_utils import timeframe_to_msecs logger = logging.getLogger(__name__) @@ -25,7 +26,9 @@ class Bybit(Exchange): "ohlcv_has_history": False, } _ft_has_futures: Dict = { + "ohlcv_candle_limit": 200, "ohlcv_has_history": True, + "mark_ohlcv_timeframe": "4h", } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ @@ -47,3 +50,26 @@ class Bybit(Exchange): }) config.update(super()._ccxt_config) return config + + async def _fetch_funding_rate_history( + self, + pair: str, + timeframe: str, + limit: int, + since_ms: Optional[int] = None, + ) -> List[List]: + """ + Fetch funding rate history + Necessary workaround until https://github.com/ccxt/ccxt/issues/15990 is fixed. + """ + params = {} + if since_ms: + until = since_ms + (timeframe_to_msecs(timeframe) * self._ft_has['ohlcv_candle_limit']) + params.update({'until': until}) + # Funding rate + data = await self._api_async.fetch_funding_rate_history( + pair, since=since_ms, + params=params) + # Convert funding rate to candle pattern + data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] + return data diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py new file mode 100644 index 000000000..5eeb5ca24 --- /dev/null +++ b/tests/exchange/test_bybit.py @@ -0,0 +1,42 @@ +from unittest.mock import MagicMock + +from freqtrade.exchange.exchange_utils import timeframe_to_msecs +from tests.conftest import get_mock_coro, get_patched_exchange + + +async def test_bybit_fetch_funding_rate(default_conf, mocker): + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + api_mock = MagicMock() + api_mock.fetch_funding_rate_history = get_mock_coro(return_value=[]) + exchange = get_patched_exchange(mocker, default_conf, id='bybit', api_mock=api_mock) + limit = 200 + # Test fetch_funding_rate_history (current data) + await exchange._fetch_funding_rate_history( + pair='BTC/USDT:USDT', + timeframe='4h', + limit=limit, + ) + + assert api_mock.fetch_funding_rate_history.call_count == 1 + assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' + kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] + assert kwargs['params'] == {} + assert kwargs['since'] is None + + api_mock.fetch_funding_rate_history.reset_mock() + since_ms = 1610000000000 + since_ms_end = since_ms + (timeframe_to_msecs('4h') * limit) + # Test fetch_funding_rate_history (current data) + await exchange._fetch_funding_rate_history( + pair='BTC/USDT:USDT', + timeframe='4h', + limit=limit, + since_ms=since_ms, + ) + + assert api_mock.fetch_funding_rate_history.call_count == 1 + assert api_mock.fetch_funding_rate_history.call_args_list[0][0][0] == 'BTC/USDT:USDT' + kwargs = api_mock.fetch_funding_rate_history.call_args_list[0][1] + assert kwargs['params'] == {'until': since_ms_end} + assert kwargs['since'] == since_ms From 3192af8df8df069e9a0e582ee2a1b4ebf867757a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Dec 2022 09:07:07 +0100 Subject: [PATCH 02/24] Limit bybit futures markets to USDT --- freqtrade/exchange/bybit.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index dd3d74e84..8a949fbbb 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,6 +1,6 @@ """ Bybit exchange subclass """ import logging -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple from freqtrade.enums import MarginMode, TradingMode from freqtrade.exchange import Exchange @@ -51,6 +51,13 @@ class Bybit(Exchange): config.update(super()._ccxt_config) return config + def market_is_future(self, market: Dict[str, Any]) -> bool: + main = super().market_is_future(market) + # For ByBit, we'll only support USDT markets for now. + return ( + main and market['settle'] == 'USDT' + ) + async def _fetch_funding_rate_history( self, pair: str, From a7b030fff9088e8076ec5a7c0b345551970de7dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Dec 2022 09:07:50 +0100 Subject: [PATCH 03/24] Add note about bybit futures --- docs/exchanges.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 48b14c470..d2a2862e3 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -255,6 +255,11 @@ OKX requires a passphrase for each api key, you will therefore need to add this Gate.io allows the use of `POINT` to pay for fees. As this is not a tradable currency (no regular market available), automatic fee calculations will fail (and default to a fee of 0). The configuration parameter `exchange.unknown_fee_rate` can be used to specify the exchange rate between Point and the stake currency. Obviously, changing the stake-currency will also require changes to this value. +## Bybit + +Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode. +Users with unified accounts (there's no way back) can create a subaccount which will start as "non-unified", and can therefore use isolated futures. + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. From 34e7433844e36b68e8c9e03b232986d8b7780fcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Dec 2022 10:41:03 +0100 Subject: [PATCH 04/24] Add leverage to dry-run liquidation price calculation --- freqtrade/exchange/binance.py | 4 +++- freqtrade/exchange/exchange.py | 4 ++++ freqtrade/freqtradebot.py | 1 + freqtrade/optimize/backtesting.py | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d85b2fb28..75d72c6e9 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -150,6 +150,7 @@ class Binance(Exchange): is_short: bool, amount: float, stake_amount: float, + leverage: float, wallet_balance: float, # Or margin balance mm_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only @@ -159,11 +160,12 @@ class Binance(Exchange): MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93 - :param exchange_name: + :param pair: Pair to calculate liquidation price for :param open_rate: Entry price of position :param is_short: True if the trade is a short, false otherwise :param amount: Absolute value of position size incl. leverage (in base currency) :param stake_amount: Stake amount - Collateral in settle currency. + :param leverage: Leverage used for this position. :param trading_mode: SPOT, MARGIN, FUTURES, etc. :param margin_mode: Either ISOLATED or CROSS :param wallet_balance: Amount of margin_mode in the wallet being used to trade diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f0bcee702..2e172c652 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2687,6 +2687,7 @@ class Exchange: is_short: bool, amount: float, # Absolute value of position size stake_amount: float, + leverage: float, wallet_balance: float, mm_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only @@ -2708,6 +2709,7 @@ class Exchange: open_rate=open_rate, is_short=is_short, amount=amount, + leverage=leverage, stake_amount=stake_amount, wallet_balance=wallet_balance, mm_ex_1=mm_ex_1, @@ -2737,6 +2739,7 @@ class Exchange: is_short: bool, amount: float, stake_amount: float, + leverage: float, wallet_balance: float, # Or margin balance mm_ex_1: float = 0.0, # (Binance) Cross only upnl_ex_1: float = 0.0, # (Binance) Cross only @@ -2758,6 +2761,7 @@ class Exchange: :param is_short: True if the trade is a short, false otherwise :param amount: Absolute value of position size incl. leverage (in base currency) :param stake_amount: Stake amount - Collateral in settle currency. + :param leverage: Leverage used for this position. :param trading_mode: SPOT, MARGIN, FUTURES, etc. :param margin_mode: Either ISOLATED or CROSS :param wallet_balance: Amount of margin_mode in the wallet being used to trade diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 25ae5002a..a558f7bf4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1789,6 +1789,7 @@ class FreqtradeBot(LoggingMixin): is_short=trade.is_short, amount=trade.amount, stake_amount=trade.stake_amount, + leverage=trade.leverage, wallet_balance=trade.stake_amount, )) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 01138d79c..5e17eb45d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -863,6 +863,7 @@ class Backtesting: open_rate=propose_rate, amount=amount, stake_amount=trade.stake_amount, + leverage=trade.leverage, wallet_balance=trade.stake_amount, is_short=is_short, )) From d05ecd630f819cb6b4a63907ad805139fc750c76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Dec 2022 10:52:02 +0100 Subject: [PATCH 05/24] Update tests for new liquidation parameter --- tests/exchange/test_ccxt_compat.py | 26 ++++++++++++++------------ tests/exchange/test_exchange.py | 11 +++++++++-- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 4d7216860..8aee106a1 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -553,23 +553,25 @@ class TestCCXTExchange(): ) liquidation_price = futures.dry_run_liquidation_price( - futures_pair, - 40000, - False, - 100, - 100, - 100, + pair=futures_pair, + open_rate=40000, + is_short=False, + amount=100, + stake_amount=100, + leverage=5, + wallet_balance=100, ) assert (isinstance(liquidation_price, float)) assert liquidation_price >= 0.0 liquidation_price = futures.dry_run_liquidation_price( - futures_pair, - 40000, - False, - 100, - 100, - 100, + pair=futures_pair, + open_rate=40000, + is_short=False, + amount=100, + stake_amount=100, + leverage=5, + wallet_balance=100, ) assert (isinstance(liquidation_price, float)) assert liquidation_price >= 0.0 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0fa7f90ec..1b6f6f825 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4566,6 +4566,7 @@ def test_liquidation_price_is_none( is_short=is_short, amount=71200.81144, stake_amount=open_rate * 71200.81144, + leverage=5, wallet_balance=-56354.57, mm_ex_1=0.10, upnl_ex_1=0.0 @@ -4604,6 +4605,7 @@ def test_liquidation_price( upnl_ex_1=upnl_ex_1, amount=amount, stake_amount=open_rate * amount, + leverage=5, ), 2)) == expected @@ -5025,6 +5027,7 @@ def test__get_params(mocker, default_conf, exchange_name): def test_get_liquidation_price1(mocker, default_conf): api_mock = MagicMock() + leverage = 9.97 positions = [ { 'info': {}, @@ -5037,7 +5040,7 @@ def test_get_liquidation_price1(mocker, default_conf): 'maintenanceMarginPercentage': 0.025, 'entryPrice': 18.884, 'notional': 15.1072, - 'leverage': 9.97, + 'leverage': leverage, 'unrealizedPnl': 0.0048, 'contracts': 8, 'contractSize': 0.1, @@ -5067,6 +5070,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) assert liq_price == 17.47 @@ -5079,6 +5083,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) assert liq_price == 17.540699999999998 @@ -5091,6 +5096,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) assert liq_price is None @@ -5104,6 +5110,7 @@ def test_get_liquidation_price1(mocker, default_conf): is_short=False, amount=0.8, stake_amount=18.884 * 0.8, + leverage=leverage, wallet_balance=18.884 * 0.8, ) @@ -5222,7 +5229,7 @@ def test_get_liquidation_price( amount=amount, stake_amount=amount * open_rate / leverage, wallet_balance=amount * open_rate / leverage, - # leverage=leverage, + leverage=leverage, is_short=is_short, ) if expected_liq is None: From 752110a2686ee613cc15180c39578e2bf17f3b29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Dec 2022 11:06:52 +0100 Subject: [PATCH 06/24] Add online tests for bybit --- freqtrade/exchange/bybit.py | 2 +- tests/exchange/test_ccxt_compat.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 8a949fbbb..35d5ef1d2 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -34,7 +34,7 @@ class Bybit(Exchange): _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ # TradingMode.SPOT always supported and not required in this list # (TradingMode.FUTURES, MarginMode.CROSS), - # (TradingMode.FUTURES, MarginMode.ISOLATED) + (TradingMode.FUTURES, MarginMode.ISOLATED) ] @property diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 8aee106a1..87d9f52fd 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -100,6 +100,16 @@ EXCHANGES = { 'leverage_tiers_public': True, 'leverage_in_spot_market': True, }, + 'bybit': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + 'futures_pair': 'BTC/USDT:USDT', + 'futures': True, + 'leverage_tiers_public': True, + 'leverage_in_spot_market': True, + }, 'huobi': { 'pair': 'ETH/BTC', 'stake_currency': 'BTC', From 93ce963e9bff22c7cb8b306a63b54b5fdea07ba2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Dec 2022 11:07:58 +0100 Subject: [PATCH 07/24] Update test name --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1b6f6f825..36990afc4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4587,7 +4587,7 @@ def test_liquidation_price_is_none( ("binance", False, 'futures', 'cross', 1535443.01, 356512.508, -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) ]) -def test_liquidation_price( +def test_liquidation_price_binance( mocker, default_conf, exchange_name, open_rate, is_short, trading_mode, margin_mode, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, amount, mm_ratio, expected ): From 31745a9dc2214cdc2760be6955fc2fb487baa6cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Dec 2022 11:19:27 +0100 Subject: [PATCH 08/24] bybit: Initial implementation liquidation calculation --- freqtrade/exchange/bybit.py | 63 +++++++++++++++++++++++++++++++++ tests/exchange/test_exchange.py | 12 ++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 35d5ef1d2..cb8faa7a8 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -3,6 +3,7 @@ import logging from typing import Any, Dict, List, Optional, Tuple from freqtrade.enums import MarginMode, TradingMode +from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange from freqtrade.exchange.exchange_utils import timeframe_to_msecs @@ -80,3 +81,65 @@ class Bybit(Exchange): # Convert funding rate to candle pattern data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] return data + + def dry_run_liquidation_price( + self, + pair: str, + open_rate: float, # Entry price of position + is_short: bool, + amount: float, + stake_amount: float, + leverage: float, + wallet_balance: float, # Or margin balance + mm_ex_1: float = 0.0, # (Binance) Cross only + upnl_ex_1: float = 0.0, # (Binance) Cross only + ) -> Optional[float]: + """ + Important: Must be fetching data from cached values as this is used by backtesting! + PERPETUAL: + bybit: + https://www.bybithelp.com/HelpCenterKnowledge/bybitHC_Article?language=en_US&id=000001067 + + Long: + Liquidation Price = ( + Entry Price * (1 - Initial Margin Rate + Maintenance Margin Rate) + - Extra Margin Added/ Contract) + Short: + Liquidation Price = ( + Entry Price * (1 + Initial Margin Rate - Maintenance Margin Rate) + + Extra Margin Added/ Contract) + + Implementation Note: Extra margin is currently not used. + + :param pair: Pair to calculate liquidation price for + :param open_rate: Entry price of position + :param is_short: True if the trade is a short, false otherwise + :param amount: Absolute value of position size incl. leverage (in base currency) + :param stake_amount: Stake amount - Collateral in settle currency. + :param leverage: Leverage used for this position. + :param trading_mode: SPOT, MARGIN, FUTURES, etc. + :param margin_mode: Either ISOLATED or CROSS + :param wallet_balance: Amount of margin_mode in the wallet being used to trade + Cross-Margin Mode: crossWalletBalance + Isolated-Margin Mode: isolatedWalletBalance + """ + + market = self.markets[pair] + mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount) + + if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED: + + if market['inverse']: + raise OperationalException( + "Freqtrade does not yet support inverse contracts") + initial_margin_rate = 1 / leverage + + # See docstring - ignores extra margin! + if is_short: + return open_rate * (1 + initial_margin_rate - mm_ratio) + else: + return open_rate * (1 - initial_margin_rate + mm_ratio) + + else: + raise OperationalException( + "Freqtrade only supports isolated futures for leverage trading") diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 36990afc4..61e99a5ea 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -5115,7 +5115,7 @@ def test_get_liquidation_price1(mocker, default_conf): ) -@pytest.mark.parametrize('liquidation_buffer', [0.0, 0.05]) +@pytest.mark.parametrize('liquidation_buffer', [0.0]) @pytest.mark.parametrize( "is_short,trading_mode,exchange_name,margin_mode,leverage,open_rate,amount,expected_liq", [ (False, 'spot', 'binance', '', 5.0, 10.0, 1.0, None), @@ -5144,6 +5144,16 @@ def test_get_liquidation_price1(mocker, default_conf): (False, 'futures', 'gateio', 'isolated', 5.0, 10.0, 1.0, 8.085708510208207), (False, 'futures', 'gateio', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), (False, 'futures', 'okx', 'isolated', 3.0, 10.0, 1.0, 6.738090425173506), + # bybit, long + (False, 'futures', 'bybit', 'isolated', 1.0, 10.0, 1.0, 0.1), + (False, 'futures', 'bybit', 'isolated', 3.0, 10.0, 1.0, 6.7666666), + (False, 'futures', 'bybit', 'isolated', 5.0, 10.0, 1.0, 8.1), + (False, 'futures', 'bybit', 'isolated', 10.0, 10.0, 1.0, 9.1), + # bybit, short + (True, 'futures', 'bybit', 'isolated', 1.0, 10.0, 1.0, 19.9), + (True, 'futures', 'bybit', 'isolated', 3.0, 10.0, 1.0, 13.233333), + (True, 'futures', 'bybit', 'isolated', 5.0, 10.0, 1.0, 11.9), + (True, 'futures', 'bybit', 'isolated', 10.0, 10.0, 1.0, 10.9), ] ) def test_get_liquidation_price( From f681ce9139896225c7ea50b8af6900eae81301b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 31 Dec 2022 15:53:43 +0100 Subject: [PATCH 09/24] Allow margin and leverage setting failures (this is important when an exchange "fails" a request if the setting didn't change). --- freqtrade/exchange/bybit.py | 26 ++++++++++++++++++++++++++ freqtrade/exchange/exchange.py | 14 ++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index cb8faa7a8..fc79abd6a 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -2,6 +2,7 @@ import logging from typing import Any, Dict, List, Optional, Tuple +from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange @@ -82,6 +83,31 @@ class Bybit(Exchange): data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] return data + def _lev_prep(self, pair: str, leverage: float, side: BuySell): + if self.trading_mode != TradingMode.SPOT: + params = {'leverage': leverage} + self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params) + self._set_leverage(leverage, pair, accept_fail=True) + + def _get_params( + self, + side: BuySell, + ordertype: str, + leverage: float, + reduceOnly: bool, + time_in_force: str = 'GTC', + ) -> Dict: + params = super()._get_params( + side=side, + ordertype=ordertype, + leverage=leverage, + reduceOnly=reduceOnly, + time_in_force=time_in_force, + ) + if self.trading_mode == TradingMode.FUTURES and self.margin_mode: + params['position_idx'] = 0 + return params + def dry_run_liquidation_price( self, pair: str, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2e172c652..9b0419cd6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2484,7 +2484,8 @@ class Exchange: self, leverage: float, pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None + trading_mode: Optional[TradingMode] = None, + accept_fail: bool = False, ): """ Set's the leverage before making a trade, in order to not @@ -2499,6 +2500,10 @@ class Exchange: self._log_exchange_response('set_leverage', res) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e + except ccxt.BadRequest as e: + if not accept_fail: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e @@ -2520,7 +2525,8 @@ class Exchange: return open_date.minute > 0 or open_date.second > 0 @retrier - def set_margin_mode(self, pair: str, margin_mode: MarginMode, params: dict = {}): + def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False, + params: dict = {}): """ Set's the margin mode on the exchange to cross or isolated for a specific pair :param pair: base/quote currency pair (e.g. "ADA/USDT") @@ -2534,6 +2540,10 @@ class Exchange: self._log_exchange_response('set_margin_mode', res) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e + except ccxt.BadRequest as e: + if not accept_fail: + raise TemporaryError( + f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e From 7a18e96042e5175eef99e165926190b8b9ef0bba Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 4 Jan 2023 18:23:56 +0100 Subject: [PATCH 10/24] bybit: hot-fix funding fees (temporary - must be changed) --- freqtrade/exchange/bybit.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index fc79abd6a..e9e75b8af 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,6 +1,7 @@ """ Bybit exchange subclass """ import logging -from typing import Any, Dict, List, Optional, Tuple +from datetime import datetime +from typing import Any, Dict, List, Optional, Tuple, Union from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, TradingMode @@ -169,3 +170,14 @@ class Bybit(Exchange): else: raise OperationalException( "Freqtrade only supports isolated futures for leverage trading") + + def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: + """ + Returns the sum of all funding fees that were exchanged for a pair within a timeframe + Dry-run handling happens as part of _calculate_funding_fees. + :param pair: (e.g. ADA/USDT) + :param since: The earliest time of consideration for calculating funding fees, + in unix time or as a datetime + """ + # TODO: Workaround for bybit, which has no funding-fees + return 0 From c2b33a0f58da559ad867550147b9273695bfc995 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Jan 2023 11:04:47 +0100 Subject: [PATCH 11/24] Fix set-leverage function sig --- freqtrade/exchange/binance.py | 3 ++- freqtrade/exchange/kraken.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 75d72c6e9..41225b523 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -87,7 +87,8 @@ class Binance(Exchange): self, leverage: float, pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None + trading_mode: Optional[TradingMode] = None, + accept_fail: bool = False, ): """ Set's the leverage before making a trade, in order to not diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 5d8c1ad29..2b37c45bd 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -158,7 +158,8 @@ class Kraken(Exchange): self, leverage: float, pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None + trading_mode: Optional[TradingMode] = None, + accept_fail: bool = False, ): """ Kraken set's the leverage as an option in the order object, so we need to From c14553bacbfd0595a143f790ddc00a1c28ee552e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Jan 2023 11:40:18 +0100 Subject: [PATCH 12/24] Add bybit to supported Futures exchanges --- README.md | 1 + docs/index.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 2ab62793d..232326ba1 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even - [X] [Binance](https://www.binance.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [OKX](https://okx.com/) +- [X] [Bybit](https://bybit.com/) Please make sure to read the [exchange specific notes](docs/exchanges.md), as well as the [trading with leverage](docs/leverage.md) documentation before diving in. diff --git a/docs/index.md b/docs/index.md index 40b9e98ad..c24d1f36b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,6 +52,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Binance](https://www.binance.com/) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [OKX](https://okx.com/) +- [X] [Bybit](https://bybit.com/) Please make sure to read the [exchange specific notes](exchanges.md), as well as the [trading with leverage](leverage.md) documentation before diving in. From 3a83427f92430f35e1cca74f8c4f4b1a2a6127eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Jan 2023 11:40:24 +0100 Subject: [PATCH 13/24] Add Bybit stoploss support --- freqtrade/exchange/bybit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index e9e75b8af..9cef27591 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -32,6 +32,8 @@ class Bybit(Exchange): "ohlcv_candle_limit": 200, "ohlcv_has_history": True, "mark_ohlcv_timeframe": "4h", + "stoploss_on_exchange": True, + "stoploss_order_types": {"limit": "limit", "market": "market"}, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ From 051c3be99e6e440edb22a818cffcd6e9f40f5f9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Jan 2023 07:20:42 +0100 Subject: [PATCH 14/24] add test case for bybit --- tests/test_freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7efd0393d..91fc25a83 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -751,6 +751,8 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker) (False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796), (True, 'futures', 'okx', 'isolated', 0.0, 11.87413417771621), (False, 'futures', 'okx', 'isolated', 0.0, 8.085708510208207), + (True, 'futures', 'bybit', 'isolated', 0.0, 11.9), + (False, 'futures', 'bybit', 'isolated', 0.0, 8.1), ]) def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, limit_order_open, is_short, trading_mode, From 25fa6bee74fe8025bed8b4e2a9ef8b5184a9c0df Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Jan 2023 07:21:16 +0100 Subject: [PATCH 15/24] Override get_funding_fees for bybit --- docs/exchanges.md | 2 ++ freqtrade/exchange/bybit.py | 23 +++++++++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index d2a2862e3..bbed3cef0 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -260,6 +260,8 @@ The configuration parameter `exchange.unknown_fee_rate` can be used to specify t Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode. Users with unified accounts (there's no way back) can create a subaccount which will start as "non-unified", and can therefore use isolated futures. +As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well. + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 9cef27591..a99f3656c 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -173,13 +173,20 @@ class Bybit(Exchange): raise OperationalException( "Freqtrade only supports isolated futures for leverage trading") - def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: + def get_funding_fees( + self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float: """ - Returns the sum of all funding fees that were exchanged for a pair within a timeframe - Dry-run handling happens as part of _calculate_funding_fees. - :param pair: (e.g. ADA/USDT) - :param since: The earliest time of consideration for calculating funding fees, - in unix time or as a datetime + Fetch funding fees, either from the exchange (live) or calculates them + based on funding rate/mark price history + :param pair: The quote/base pair of the trade + :param is_short: trade direction + :param amount: Trade amount + :param open_date: Open date of the trade + :return: funding fee since open_date + :raises: ExchangeError if something goes wrong. """ - # TODO: Workaround for bybit, which has no funding-fees - return 0 + # Bybit does not provide "applied" funding fees per position. + if self.trading_mode == TradingMode.FUTURES: + return self._fetch_and_calculate_funding_fees( + pair, amount, is_short, open_date) + return 0.0 From c12fb1a49c0e454bf92020a0a8adb0b36b57205a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Jan 2023 20:12:50 +0100 Subject: [PATCH 16/24] bybit: Some final cleanup --- docs/exchanges.md | 2 +- freqtrade/exchange/bybit.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index bbed3cef0..54fd7eae3 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -258,7 +258,7 @@ The configuration parameter `exchange.unknown_fee_rate` can be used to specify t ## Bybit Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode. -Users with unified accounts (there's no way back) can create a subaccount which will start as "non-unified", and can therefore use isolated futures. +Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures. As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well. diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index a99f3656c..e08380213 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,7 +1,7 @@ """ Bybit exchange subclass """ import logging from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, TradingMode From 73ef1d5191406779262c46172e938415cf96e290 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 Jan 2023 19:53:14 +0100 Subject: [PATCH 17/24] Improve exception wording on binance --- freqtrade/exchange/binance.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 41225b523..f50b5f2e1 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -78,7 +78,9 @@ class Binance(Exchange): raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' + ) from e + except ccxt.BaseError as e: raise OperationalException(e) from e From 1431f7cc3e0a5c01c7ea0baa4976f739f2ba317c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 Jan 2023 19:53:24 +0100 Subject: [PATCH 18/24] Set position mode to one-way on startup --- docs/exchanges.md | 1 + freqtrade/exchange/bybit.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 54fd7eae3..12b6c874d 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -259,6 +259,7 @@ The configuration parameter `exchange.unknown_fee_rate` can be used to specify t Futures trading on bybit is currently supported for USDT markets, and will use isolated futures mode. Users with unified accounts (there's no way back) can create a Sub-account which will start as "non-unified", and can therefore use isolated futures. +On startup, freqtrade will set the position mode to "One-way Mode" for the whole (sub)account. This avoids making this call over and over again (slowing down bot operations), but means that changes to this setting may result in exceptions and errors. As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well. diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index e08380213..dc9b5f621 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -3,10 +3,13 @@ import logging from datetime import datetime from typing import Any, Dict, List, Optional, Tuple +import ccxt + from freqtrade.constants import BuySell from freqtrade.enums import MarginMode, TradingMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier from freqtrade.exchange.exchange_utils import timeframe_to_msecs @@ -63,6 +66,26 @@ class Bybit(Exchange): main and market['settle'] == 'USDT' ) + @retrier + def additional_exchange_init(self) -> None: + """ + Additional exchange initialization logic. + .api will be available at this point. + Must be overridden in child methods if required. + """ + try: + if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: + position_mode = self._api.set_position_mode(False) + self._log_exchange_response('set_position_mode', position_mode) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' + ) from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + async def _fetch_funding_rate_history( self, pair: str, From 8665d0866d8c24dd907d8a40cda142a86d75c47a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 Jan 2023 19:58:42 +0100 Subject: [PATCH 19/24] Add test for bybit startup magic --- tests/exchange/test_bybit.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index 5eeb5ca24..7c8324bf6 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -1,7 +1,22 @@ from unittest.mock import MagicMock +from freqtrade.enums.marginmode import MarginMode +from freqtrade.enums.tradingmode import TradingMode from freqtrade.exchange.exchange_utils import timeframe_to_msecs from tests.conftest import get_mock_coro, get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers + + +def test_additional_exchange_init_bybit(default_conf, mocker): + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + api_mock = MagicMock() + api_mock.set_position_mode = MagicMock(return_value={"dualSidePosition": False}) + get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + assert api_mock.set_position_mode.call_count == 1 + ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'bybit', + "additional_exchange_init", "set_position_mode") async def test_bybit_fetch_funding_rate(default_conf, mocker): From 08ede377951e3ad4c9cbe073dfc1bf85759baa3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 Jan 2023 19:58:58 +0100 Subject: [PATCH 20/24] Add documentation note about stoploss on exchange --- docs/exchanges.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 12b6c874d..5ceeccb19 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -263,6 +263,10 @@ On startup, freqtrade will set the position mode to "One-way Mode" for the whole As bybit doesn't provide funding rate history, the dry-run calculation is used for live trades as well. +!!! Tip "Stoploss on Exchange" + Bybit (futures only) supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange. + On futures, Bybit supports both `stop-limit` as well as `stop-market` orders. You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use. + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. From fa033965c8b032d5cbc56821b42e968b8987da47 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Jan 2023 19:34:29 +0100 Subject: [PATCH 21/24] use "swap" for bybit --- freqtrade/exchange/binance.py | 1 - freqtrade/exchange/bybit.py | 1 - 2 files changed, 2 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f50b5f2e1..22dfdc1d1 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -28,7 +28,6 @@ class Binance(Exchange): "trades_pagination": "id", "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], - "ccxt_futures_name": "swap" } _ft_has_futures: Dict = { "stoploss_order_types": {"limit": "stop", "market": "stop_market"}, diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index dc9b5f621..55bfbd232 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -28,7 +28,6 @@ class Bybit(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1000, - "ccxt_futures_name": "linear", "ohlcv_has_history": False, } _ft_has_futures: Dict = { From d1b069abfb834dfaf608211d90e9b82b167ca29d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Jan 2023 20:33:34 +0100 Subject: [PATCH 22/24] bybit: Update test to align with defaultType change --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 61e99a5ea..0ebdfd218 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3959,7 +3959,7 @@ def test_validate_trading_mode_and_margin_mode( ("binance", "margin", {"options": {"defaultType": "margin"}}), ("binance", "futures", {"options": {"defaultType": "swap"}}), ("bybit", "spot", {"options": {"defaultType": "spot"}}), - ("bybit", "futures", {"options": {"defaultType": "linear"}}), + ("bybit", "futures", {"options": {"defaultType": "swap"}}), ("gateio", "futures", {"options": {"defaultType": "swap"}}), ("hitbtc", "futures", {"options": {"defaultType": "swap"}}), ("kraken", "futures", {"options": {"defaultType": "swap"}}), From 7294db81e2a67eebecfdfa8ef3e0e2379fe9c9e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Jan 2023 18:17:09 +0100 Subject: [PATCH 23/24] Bump ccxt to 2.7.7 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 73e0e6576..dbe0e4fd4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.24.1 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.6.65 +ccxt==2.7.7 # Pin cryptography for now due to rust build errors with piwheels cryptography==38.0.1; platform_machine == 'armv7l' cryptography==39.0.0; platform_machine != 'armv7l' From f6ba0fe6aec6bfedbf6ae9c4fa7b7ced3deeff48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Jan 2023 18:23:23 +0100 Subject: [PATCH 24/24] bybit: fix broken ccxt tests --- tests/exchange/test_ccxt_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 87d9f52fd..712f9eb6b 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -186,6 +186,7 @@ def exchange_futures(request, exchange_conf, class_mocker): class_mocker.patch('freqtrade.exchange.exchange.Exchange.fetch_trading_fees') class_mocker.patch('freqtrade.exchange.okx.Okx.additional_exchange_init') class_mocker.patch('freqtrade.exchange.binance.Binance.additional_exchange_init') + class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init') class_mocker.patch('freqtrade.exchange.exchange.Exchange.load_cached_leverage_tiers', return_value=None) class_mocker.patch('freqtrade.exchange.exchange.Exchange.cache_leverage_tiers')