From 6b9915bc73bea6d08217b451a3720f9c8ae8db44 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 7 Feb 2022 01:33:42 -0600 Subject: [PATCH] moved fill_leverage_brackets and get_max_leverage to base exchange class, wrote parse_leverage_tier and load_leverage_brackets --- freqtrade/exchange/binance.py | 135 +++-------------------------- freqtrade/exchange/exchange.py | 146 +++++++++++++++++++++++++++----- freqtrade/exchange/okx.py | 4 +- tests/exchange/test_exchange.py | 6 +- 4 files changed, 145 insertions(+), 146 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index cc562530b..8ed791cfe 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -128,128 +128,6 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - @retrier - def fill_leverage_brackets(self) -> None: - """ - Assigns property _leverage_brackets to a dictionary of information about the leverage - allowed on each pair - After exectution, self._leverage_brackets = { - "pair_name": [ - [notional_floor, maintenenace_margin_ratio, maintenance_amt], - ... - ], - ... - } - e.g. { - "ETH/USDT:USDT": [ - [0.0, 0.01, 0.0], - [10000, 0.02, 0.01], - ... - ], - ... - } - """ - if self.trading_mode == TradingMode.FUTURES: - try: - if self._config['dry_run']: - leverage_brackets_path = ( - Path(__file__).parent / 'binance_leverage_tiers.json' - ) - with open(leverage_brackets_path) as json_file: - leverage_brackets = json.load(json_file) - else: - leverage_brackets = self._api.fetch_leverage_tiers() - - for pair, tiers in leverage_brackets.items(): - brackets = [] - for tier in tiers: - info = tier['info'] - brackets.append({ - 'min': tier['notionalFloor'], - 'max': tier['notionalCap'], - 'mmr': tier['maintenanceMarginRatio'], - 'lev': tier['maxLeverage'], - 'maintAmt': float(info['cum']) if 'cum' in info else None, - }) - self._leverage_brackets[pair] = brackets - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError(f'Could not fetch leverage amounts due to' - f'{e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e - - 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 - :stake_amount: The total value of the traders margin_mode in quote currency - """ - - if self.trading_mode == TradingMode.SPOT: - return 1.0 - - if self._api.has['fetchLeverageTiers']: - - # Checks and edge cases - if stake_amount is None: - raise OperationalException( - 'binance.get_max_leverage requires argument stake_amount') - if pair not in self._leverage_brackets: # Not a leveraged market - return 1.0 - if stake_amount == 0: - return self._leverage_brackets[pair][0]['lev'] # Max lev for lowest amount - - pair_brackets = self._leverage_brackets[pair] - num_brackets = len(pair_brackets) - - for bracket_index in range(num_brackets): - - bracket = pair_brackets[bracket_index] - lev = bracket['lev'] - - if bracket_index < num_brackets - 1: - next_bracket = pair_brackets[bracket_index+1] - next_floor = next_bracket['min'] / next_bracket['lev'] - if next_floor > stake_amount: # Next bracket min too high for stake amount - return min((bracket['max'] / stake_amount), lev) - # - # With the two leverage brackets below, - # - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66 - # - stakes below 133.33 = max_lev of 75 - # - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99 - # - stakes from 200 + 1000 = max_lev of 50 - # - # { - # "min": 0, # stake = 0.0 - # "max": 10000, # max_stake@75 = 10000/75 = 133.33333333333334 - # "lev": 75, - # }, - # { - # "min": 10000, # stake = 200.0 - # "max": 50000, # max_stake@50 = 50000/50 = 1000.0 - # "lev": 50, - # } - # - - else: # if on the last bracket - if stake_amount > bracket['max']: # If stake is > than max tradeable amount - raise InvalidOrderException(f'Amount {stake_amount} too high for {pair}') - else: - return bracket['lev'] - - raise OperationalException( - 'Looped through all tiers without finding a max leverage. Should never be reached' - ) - - else: # Search markets.limits for max lev - market = self.markets[pair] - if market['limits']['leverage']['max'] is not None: - return market['limits']['leverage']['max'] - else: - return 1.0 # Default if max leverage cannot be found - @retrier def _set_leverage( self, @@ -367,3 +245,16 @@ class Binance(Exchange): else: raise OperationalException( "Freqtrade only supports isolated futures for leverage trading") + + def load_leverage_brackets(self) -> Dict[str, List[Dict]]: + if self._config['dry_run']: + leverage_brackets_path = ( + Path(__file__).parent / 'binance_leverage_tiers.json' + ) + leverage_brackets = {} + with open(leverage_brackets_path) as json_file: + leverage_brackets = json.load(json_file) + return leverage_brackets + else: + leverage_brackets = self._api.fetch_leverage_tiers() + return leverage_brackets diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ac94c9f9e..2be931dc9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -75,6 +75,7 @@ class Exchange: "mark_ohlcv_timeframe": "8h", "ccxt_futures_name": "swap", "mmr_key": None, + "can_fetch_multiple_tiers": True, } _ft_has: Dict = {} @@ -1855,26 +1856,131 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def fill_leverage_brackets(self): + def load_leverage_brackets(self) -> Dict[str, List[Dict]]: + return self._api.fetch_leverage_tiers() + + @retrier + def fill_leverage_brackets(self) -> None: """ Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair - Not used if the exchange has a static max leverage value for the account or each pair + After exectution, self._leverage_brackets = { + "pair_name": [ + [notional_floor, maintenenace_margin_ratio, maintenance_amt], + ... + ], + ... + } + e.g. { + "ETH/USDT:USDT": [ + [0.0, 0.01, 0.0], + [10000, 0.02, 0.01], + ... + ], + ... + } """ - return + if self._api.has['fetchLeverageTiers']: + if self.trading_mode == TradingMode.FUTURES: + leverage_brackets = self.load_leverage_brackets() + try: + for pair, tiers in leverage_brackets.items(): + brackets = [] + for tier in tiers: + brackets.append(self.parse_leverage_tier(tier)) + self._leverage_brackets[pair] = brackets + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError(f'Could not fetch leverage amounts due to' + f'{e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + def parse_leverage_tier(self, tier) -> Dict: + info = tier['info'] + return { + 'min': tier['notionalFloor'], + 'max': tier['notionalCap'], + 'mmr': tier['maintenanceMarginRatio'], + 'lev': tier['maxLeverage'], + 'maintAmt': float(info['cum']) if 'cum' in info else 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) + :stake_amount: The total value of the traders margin_mode in quote currency """ - market = self.markets[pair] - if market['limits']['leverage']['max'] is not None: - return market['limits']['leverage']['max'] - else: + + if self.trading_mode == TradingMode.SPOT: return 1.0 + if self._api.has['fetchLeverageTiers']: + + # Checks and edge cases + if stake_amount is None: + raise OperationalException( + 'binance.get_max_leverage requires argument stake_amount') + if pair not in self._leverage_brackets: + brackets = self.get_leverage_tiers_for_pair(pair) + if not brackets: # Not a leveraged market + return 1.0 + else: + self._leverage_brackets[pair] = brackets + if stake_amount == 0: + return self._leverage_brackets[pair][0]['lev'] # Max lev for lowest amount + + pair_brackets = self._leverage_brackets[pair] + num_brackets = len(pair_brackets) + + for bracket_index in range(num_brackets): + + bracket = pair_brackets[bracket_index] + lev = bracket['lev'] + + if bracket_index < num_brackets - 1: + next_bracket = pair_brackets[bracket_index+1] + next_floor = next_bracket['min'] / next_bracket['lev'] + if next_floor > stake_amount: # Next bracket min too high for stake amount + return min((bracket['max'] / stake_amount), lev) + # + # With the two leverage brackets below, + # - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66 + # - stakes below 133.33 = max_lev of 75 + # - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99 + # - stakes from 200 + 1000 = max_lev of 50 + # + # { + # "min": 0, # stake = 0.0 + # "max": 10000, # max_stake@75 = 10000/75 = 133.33333333333334 + # "lev": 75, + # }, + # { + # "min": 10000, # stake = 200.0 + # "max": 50000, # max_stake@50 = 50000/50 = 1000.0 + # "lev": 50, + # } + # + + else: # if on the last bracket + if stake_amount > bracket['max']: # If stake is > than max tradeable amount + raise InvalidOrderException(f'Amount {stake_amount} too high for {pair}') + else: + return bracket['lev'] + + raise OperationalException( + 'Looped through all tiers without finding a max leverage. Should never be reached' + ) + + else: # Search markets.limits for max lev + market = self.markets[pair] + if market['limits']['leverage']['max'] is not None: + return market['limits']['leverage']['max'] + else: + return 1.0 # Default if max leverage cannot be found + @retrier def _set_leverage( self, @@ -2153,11 +2259,17 @@ class Exchange: raise OperationalException( "Freqtrade only supports isolated futures for leverage trading") - def get_leverage_tiers(self, pair: str): + def get_leverage_tiers_for_pair(self, pair: str): # When exchanges can load all their leverage brackets at once in the constructor # then this method does nothing, it should only be implemented when the leverage # brackets requires per symbol fetching to avoid excess api calls - return None + if not self._ft_has['can_fetch_multiple_tiers']: + try: + return self._api.fetch_leverage_tiers(pair) + except ccxt.BadRequest: + return None + else: + return None def get_maintenance_ratio_and_amt( self, @@ -2177,21 +2289,17 @@ class Exchange: if self._api.has['fetchLeverageTiers']: if pair not in self._leverage_brackets: # Used when fetchLeverageTiers cannot fetch all symbols at once - tiers = self.get_leverage_tiers(pair) + tiers = self.get_leverage_tiers_for_pair(pair) if not bool(tiers): raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}") else: self._leverage_brackets[pair] = [] for tier in tiers[pair]: - self._leverage_brackets[pair].append(( - tier['notionalFloor'], - tier['maintenanceMarginRatio'], - None, - )) + self._leverage_brackets[pair].append(self.parse_leverage_tier(tier)) pair_brackets = self._leverage_brackets[pair] - for (notional_floor, mm_ratio, amt) in reversed(pair_brackets): - if nominal_value >= notional_floor: - return (mm_ratio, amt) + for bracket in reversed(pair_brackets): + if nominal_value >= bracket['min']: + return (bracket['mmr'], bracket['maintAmt']) raise OperationalException("nominal value can not be lower than 0") # The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it # describes the min amt for a bracket, and the lowest bracket will always go down to 0 diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index a769ce9eb..8a1c693a1 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -19,6 +19,7 @@ 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]] = [ @@ -47,9 +48,6 @@ class Okx(Exchange): "posSide": "long" if side == "buy" else "short", }) - def get_leverage_tiers(self, pair: str): - return self._api.fetch_leverage_tiers(pair) - def get_max_pair_stake_amount( self, pair: str, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8457055db..322be3e02 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3561,8 +3561,10 @@ def test__ccxt_config( ("TKN/USDT", 210.30, 1.0), ]) def test_get_max_leverage(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="kraken") + default_conf['trading_mode'] = 'futures' + default_conf['margin_mode'] = 'isolated' + exchange = get_patched_exchange(mocker, default_conf, id="gateio") + exchange._api.has['fetchLeverageTiers'] = False assert exchange.get_max_leverage(pair, nominal_value) == max_lev