Merge pull request #6373 from samgermain/leverage-tiers

Leverage tiers
This commit is contained in:
Matthias
2022-02-17 20:23:33 +01:00
committed by GitHub
17 changed files with 18578 additions and 1662 deletions

View File

@@ -128,91 +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_brackets.json'
)
with open(leverage_brackets_path) as json_file:
leverage_brackets = json.load(json_file)
else:
leverage_brackets = self._api.load_leverage_brackets()
for pair, brkts in leverage_brackets.items():
[amt, old_ratio] = [0.0, 0.0]
brackets = []
for [notional_floor, mm_ratio] in brkts:
amt = (
(float(notional_floor) * (float(mm_ratio) - float(old_ratio)))
+ amt
) if old_ratio else 0.0
old_ratio = mm_ratio
brackets.append([
float(notional_floor),
float(mm_ratio),
amt,
])
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 stake_amount is None:
raise OperationalException('binance.get_max_leverage requires argument stake_amount')
if pair not in self._leverage_brackets:
return 1.0
pair_brackets = self._leverage_brackets[pair]
num_brackets = len(pair_brackets)
min_amount = 0.0
for bracket_num in range(num_brackets):
[notional_floor, mm_ratio, _] = pair_brackets[bracket_num]
lev = 1.0
if mm_ratio != 0:
lev = 1.0/mm_ratio
else:
logger.warning(f"mm_ratio for {pair} with notional floor {notional_floor} is 0")
if bracket_num+1 != num_brackets: # If not on last bracket
[min_amount, _, __] = pair_brackets[bracket_num+1] # Get min_amount of next bracket
else:
return lev
nominal_value = stake_amount * lev
# Bracket is good if the leveraged trade value doesnt exceed min_amount of next bracket
if nominal_value < min_amount:
return lev
return 1.0 # default leverage
@retrier
def _set_leverage(
self,
@@ -272,34 +187,6 @@ class Binance(Exchange):
"""
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: Optional[float] = 0.0,
) -> Tuple[float, Optional[float]]:
"""
Formula: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
Maintenance amt = Floor of Position Bracket on Level n *
difference between
Maintenance Margin Rate on Level n and
Maintenance Margin Rate on Level n-1)
+ Maintenance Amount on Level n-1
:return: The maintenance margin ratio and maintenance amount
"""
if nominal_value is None:
raise OperationalException(
"nominal value is required for binance.get_maintenance_ratio_and_amt")
if pair not in self._leverage_brackets:
raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}")
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)
raise OperationalException("nominal value can not be lower than 0")
# The lowest notional_floor for any pair in loadLeverageBrackets is always 0 because it
# describes the min amount for a bracket, and the lowest bracket will always go down to 0
def dry_run_liquidation_price(
self,
pair: str,
@@ -358,3 +245,25 @@ class Binance(Exchange):
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")
@retrier
def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
if self.trading_mode == TradingMode.FUTURES:
if self._config['dry_run']:
leverage_tiers_path = (
Path(__file__).parent / 'binance_leverage_tiers.json'
)
with open(leverage_tiers_path) as json_file:
return json.load(json_file)
else:
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 fetch leverage amounts due to'
f'{e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
else:
return {}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -73,7 +73,8 @@ class Exchange:
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
"mark_ohlcv_price": "mark",
"mark_ohlcv_timeframe": "8h",
"ccxt_futures_name": "swap"
"ccxt_futures_name": "swap",
"can_fetch_multiple_tiers": True,
}
_ft_has: Dict = {}
@@ -90,7 +91,7 @@ class Exchange:
self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {}
self._leverage_brackets: Dict[str, List[List[float]]] = {}
self._leverage_tiers: Dict[str, List[Dict]] = {}
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
@@ -183,7 +184,7 @@ class Exchange:
"markets_refresh_interval", 60) * 60
if self.trading_mode != TradingMode.SPOT:
self.fill_leverage_brackets()
self.fill_leverage_tiers()
def __del__(self):
"""
@@ -346,7 +347,10 @@ class Exchange:
return self.markets.get(pair, {}).get('base', '')
def market_is_future(self, market: Dict[str, Any]) -> bool:
return market.get(self._ft_has["ccxt_futures_name"], False) is True
return (
market.get(self._ft_has["ccxt_futures_name"], False) is True and
market.get('linear', False) is True
)
def market_is_spot(self, market: Dict[str, Any]) -> bool:
return market.get('spot', False) is True
@@ -459,7 +463,7 @@ class Exchange:
# Also reload async markets to avoid issues with newly listed pairs
self._load_async_markets(reload=True)
self._last_markets_refresh = arrow.utcnow().int_timestamp
self.fill_leverage_brackets()
self.fill_leverage_tiers()
except ccxt.BaseError:
logger.exception("Could not reload markets.")
@@ -691,13 +695,14 @@ class Exchange:
self,
pair: str,
price: float,
leverage: float = 1.0
) -> float:
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
if max_stake_amount is None:
# * Should never be executed
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
'never set max_stake_amount to None')
return max_stake_amount
return max_stake_amount / leverage
def _get_stake_amount_limit(
self,
@@ -1852,23 +1857,117 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
def fill_leverage_brackets(self):
@retrier
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
else:
return {}
def fill_leverage_tiers(self) -> None:
"""
Assigns property _leverage_brackets to a dictionary of information about the leverage
Assigns property _leverage_tiers 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
"""
return
leverage_tiers = self.load_leverage_tiers()
for pair, tiers in leverage_tiers.items():
pair_tiers = []
for tier in tiers:
pair_tiers.append(self.parse_leverage_tier(tier))
self._leverage_tiers[pair] = pair_tiers
def parse_leverage_tier(self, tier) -> Dict:
info = tier.get('info', {})
return {
'min': tier['notionalFloor'],
'max': tier['notionalCap'],
'mmr': tier['maintenanceMarginRate'],
'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']
if self.trading_mode == TradingMode.SPOT:
return 1.0
if self.trading_mode == TradingMode.FUTURES:
# Checks and edge cases
if stake_amount is None:
raise OperationalException(
f'{self.name}.get_max_leverage requires argument stake_amount'
)
if pair not in self._leverage_tiers:
# Maybe raise exception because it can't be traded on futures?
return 1.0
pair_tiers = self._leverage_tiers[pair]
if stake_amount == 0:
return self._leverage_tiers[pair][0]['lev'] # Max lev for lowest amount
for tier_index in range(len(pair_tiers)):
tier = pair_tiers[tier_index]
lev = tier['lev']
if tier_index < len(pair_tiers) - 1:
next_tier = pair_tiers[tier_index+1]
next_floor = next_tier['min'] / next_tier['lev']
if next_floor > stake_amount: # Next tier min too high for stake amount
return min((tier['max'] / stake_amount), lev)
#
# With the two leverage tiers 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 tier
if stake_amount > tier['max']: # If stake is > than max tradeable amount
raise InvalidOrderException(f'Amount {stake_amount} too high for {pair}')
else:
return tier['lev']
raise OperationalException(
'Looped through all tiers without finding a max leverage. Should never be reached'
)
elif self.trading_mode == TradingMode.MARGIN: # 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
else:
return 1.0
@@ -2098,16 +2197,6 @@ class Exchange:
else:
return None
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
"""
raise OperationalException(self.name + ' does not support leverage futures trading')
def dry_run_liquidation_price(
self,
pair: str,
@@ -2160,6 +2249,37 @@ class Exchange:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: float = 0.0,
) -> Tuple[float, Optional[float]]:
"""
:param pair: Market symbol
:param nominal_value: The total trade amount in quote currency including leverage
maintenance amount only on Binance
:return: (maintenance margin ratio, maintenance amount)
"""
if self.exchange_has('fetchLeverageTiers'):
if pair not in self._leverage_tiers:
raise InvalidOrderException(
f"Maintenance margin rate for {pair} is unavailable for {self.name}"
)
pair_tiers = self._leverage_tiers[pair]
for tier in reversed(pair_tiers):
if nominal_value >= tier['min']:
return (tier['mmr'], tier['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 tier, and the lowest tier will always go down to 0
else:
raise OperationalException(f"Cannot get maintenance ratio using {self.name}")
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module)

View File

@@ -51,3 +51,15 @@ class Gateio(Exchange):
"""
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

View File

@@ -1,9 +1,12 @@
import logging
from typing import Dict, List, Tuple
import ccxt
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
logger = logging.getLogger(__name__)
@@ -19,15 +22,34 @@ 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]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.ISOLATED)
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
def _get_params(
self,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'gtc',
) -> Dict:
params = super()._get_params(
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value
return params
@retrier
def _lev_prep(
self,
pair: str,
@@ -39,10 +61,63 @@ class Okx(Exchange):
raise OperationalException(
f"{self.name}.margin_mode must be set for {self.trading_mode.value}"
)
self._api.set_leverage(
leverage,
pair,
params={
"mgnMode": self.margin_mode.value,
"posSide": "long" if side == "buy" else "short",
})
try:
# TODO-lev: Test me properly (check mgnMode passed)
self._api.set_leverage(
leverage=leverage,
symbol=pair,
params={
"mgnMode": self.margin_mode.value,
# "posSide": "net"",
})
except ccxt.DDoSProtection as e:
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
except ccxt.BaseError as e:
raise OperationalException(e) from e
def get_max_pair_stake_amount(
self,
pair: str,
price: float,
leverage: float = 1.0
) -> float:
if self.trading_mode == TradingMode.SPOT:
return float('inf') # Not actually inf, but this probably won't matter for SPOT
if pair not in self._leverage_tiers:
return float('inf')
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 {}