moved fill_leverage_brackets and get_max_leverage to base exchange class, wrote parse_leverage_tier and load_leverage_brackets
This commit is contained in:
parent
a99cf2eeed
commit
6b9915bc73
@ -128,128 +128,6 @@ class Binance(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from 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
|
@retrier
|
||||||
def _set_leverage(
|
def _set_leverage(
|
||||||
self,
|
self,
|
||||||
@ -367,3 +245,16 @@ class Binance(Exchange):
|
|||||||
else:
|
else:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Freqtrade only supports isolated futures for leverage trading")
|
"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
|
||||||
|
@ -75,6 +75,7 @@ class Exchange:
|
|||||||
"mark_ohlcv_timeframe": "8h",
|
"mark_ohlcv_timeframe": "8h",
|
||||||
"ccxt_futures_name": "swap",
|
"ccxt_futures_name": "swap",
|
||||||
"mmr_key": None,
|
"mmr_key": None,
|
||||||
|
"can_fetch_multiple_tiers": True,
|
||||||
}
|
}
|
||||||
_ft_has: Dict = {}
|
_ft_has: Dict = {}
|
||||||
|
|
||||||
@ -1855,26 +1856,131 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from 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
|
Assigns property _leverage_brackets to a dictionary of information about the leverage
|
||||||
allowed on each pair
|
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:
|
def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
|
||||||
"""
|
"""
|
||||||
Returns the maximum leverage that a pair can be traded at
|
Returns the maximum leverage that a pair can be traded at
|
||||||
:param pair: The base/quote currency pair being traded
|
: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:
|
if self.trading_mode == TradingMode.SPOT:
|
||||||
return market['limits']['leverage']['max']
|
|
||||||
else:
|
|
||||||
return 1.0
|
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
|
@retrier
|
||||||
def _set_leverage(
|
def _set_leverage(
|
||||||
self,
|
self,
|
||||||
@ -2153,11 +2259,17 @@ class Exchange:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"Freqtrade only supports isolated futures for leverage trading")
|
"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
|
# 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
|
# then this method does nothing, it should only be implemented when the leverage
|
||||||
# brackets requires per symbol fetching to avoid excess api calls
|
# 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(
|
def get_maintenance_ratio_and_amt(
|
||||||
self,
|
self,
|
||||||
@ -2177,21 +2289,17 @@ class Exchange:
|
|||||||
if self._api.has['fetchLeverageTiers']:
|
if self._api.has['fetchLeverageTiers']:
|
||||||
if pair not in self._leverage_brackets:
|
if pair not in self._leverage_brackets:
|
||||||
# Used when fetchLeverageTiers cannot fetch all symbols at once
|
# 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):
|
if not bool(tiers):
|
||||||
raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}")
|
raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}")
|
||||||
else:
|
else:
|
||||||
self._leverage_brackets[pair] = []
|
self._leverage_brackets[pair] = []
|
||||||
for tier in tiers[pair]:
|
for tier in tiers[pair]:
|
||||||
self._leverage_brackets[pair].append((
|
self._leverage_brackets[pair].append(self.parse_leverage_tier(tier))
|
||||||
tier['notionalFloor'],
|
|
||||||
tier['maintenanceMarginRatio'],
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
pair_brackets = self._leverage_brackets[pair]
|
pair_brackets = self._leverage_brackets[pair]
|
||||||
for (notional_floor, mm_ratio, amt) in reversed(pair_brackets):
|
for bracket in reversed(pair_brackets):
|
||||||
if nominal_value >= notional_floor:
|
if nominal_value >= bracket['min']:
|
||||||
return (mm_ratio, amt)
|
return (bracket['mmr'], bracket['maintAmt'])
|
||||||
raise OperationalException("nominal value can not be lower than 0")
|
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
|
# 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
|
# describes the min amt for a bracket, and the lowest bracket will always go down to 0
|
||||||
|
@ -19,6 +19,7 @@ class Okx(Exchange):
|
|||||||
"ohlcv_candle_limit": 300,
|
"ohlcv_candle_limit": 300,
|
||||||
"mark_ohlcv_timeframe": "4h",
|
"mark_ohlcv_timeframe": "4h",
|
||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "8h",
|
||||||
|
"can_fetch_multiple_tiers": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
|
||||||
@ -47,9 +48,6 @@ class Okx(Exchange):
|
|||||||
"posSide": "long" if side == "buy" else "short",
|
"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(
|
def get_max_pair_stake_amount(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
|
@ -3561,8 +3561,10 @@ def test__ccxt_config(
|
|||||||
("TKN/USDT", 210.30, 1.0),
|
("TKN/USDT", 210.30, 1.0),
|
||||||
])
|
])
|
||||||
def test_get_max_leverage(default_conf, mocker, pair, nominal_value, max_lev):
|
def test_get_max_leverage(default_conf, mocker, pair, nominal_value, max_lev):
|
||||||
# Binance has a different method of getting the max leverage
|
default_conf['trading_mode'] = 'futures'
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id="kraken")
|
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
|
assert exchange.get_max_leverage(pair, nominal_value) == max_lev
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user