@@ -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
16481
freqtrade/exchange/binance_leverage_tiers.json
Normal file
16481
freqtrade/exchange/binance_leverage_tiers.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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 {}
|
||||
|
Reference in New Issue
Block a user