moved get_maintenance_ratio_and_amt to base.exchange. Wrote get_leverage_tiers. Added mmr_key to exchange._ft_has

This commit is contained in:
Sam Germain 2022-02-05 19:32:46 -06:00
parent ee5f05208e
commit a5aba4813d
7 changed files with 150 additions and 128 deletions

View File

@ -169,11 +169,11 @@ class Binance(Exchange):
+ amt + amt
) if old_ratio else 0.0 ) if old_ratio else 0.0
old_ratio = mm_ratio old_ratio = mm_ratio
brackets.append([ brackets.append((
float(notional_floor), float(notional_floor),
float(mm_ratio), float(mm_ratio),
amt, amt,
]) ))
self._leverage_brackets[pair] = brackets self._leverage_brackets[pair] = brackets
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
@ -272,34 +272,6 @@ class Binance(Exchange):
""" """
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15) 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( def dry_run_liquidation_price(
self, self,
pair: str, pair: str,

View File

@ -73,7 +73,8 @@ class Exchange:
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
"mark_ohlcv_price": "mark", "mark_ohlcv_price": "mark",
"mark_ohlcv_timeframe": "8h", "mark_ohlcv_timeframe": "8h",
"ccxt_futures_name": "swap" "ccxt_futures_name": "swap",
"mmr_key": None,
} }
_ft_has: Dict = {} _ft_has: Dict = {}
@ -90,7 +91,7 @@ class Exchange:
self._api: ccxt.Exchange = None self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {} self._markets: Dict = {}
self._leverage_brackets: Dict[str, List[List[float]]] = {} self._leverage_brackets: Dict[str, List[Tuple[float, float, Optional(float)]]] = {}
self.loop = asyncio.new_event_loop() self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop) asyncio.set_event_loop(self.loop)
@ -2099,16 +2100,6 @@ class Exchange:
else: else:
return None 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( def dry_run_liquidation_price(
self, self,
pair: str, pair: str,
@ -2161,6 +2152,59 @@ 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):
# 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
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: Optional[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 nominal_value is None:
raise OperationalException(
f"nominal value is required for {self.name}.get_maintenance_ratio_and_amt"
)
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)
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,
))
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 amt for a bracket, and the lowest bracket will always go down to 0
else:
info = self.markets[pair]['info']
mmr_key = self._ft_has['mmr_key']
if mmr_key and mmr_key in info:
return (float(info[mmr_key]), None)
else:
raise OperationalException(
f"Cannot fetch maintenance margin. Dry-run for freqtrade {self.trading_mode}"
f"is not available for {self.name}"
)
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module) return exchange_name in ccxt_exchanges(ccxt_module)

View File

@ -1,6 +1,6 @@
""" Gate.io exchange subclass """ """ Gate.io exchange subclass """
import logging import logging
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Tuple
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@ -23,6 +23,7 @@ class Gateio(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"ohlcv_volume_currency": "quote", "ohlcv_volume_currency": "quote",
"mmr_key": "maintenance_rate",
} }
_headers = {'X-Gate-Channel-Id': 'freqtrade'} _headers = {'X-Gate-Channel-Id': 'freqtrade'}
@ -40,14 +41,3 @@ class Gateio(Exchange):
if any(v == 'market' for k, v in order_types.items()): if any(v == 'market' for k, v in order_types.items()):
raise OperationalException( raise OperationalException(
f'Exchange {self.name} does not support market orders.') f'Exchange {self.name} does not support market orders.')
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
"""
info = self.markets[pair]['info']
return (float(info['maintenance_rate']), None)

View File

@ -25,7 +25,7 @@ class Okx(Exchange):
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS), # (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS), # (TradingMode.FUTURES, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.ISOLATED) (TradingMode.FUTURES, MarginMode.ISOLATED),
] ]
def _lev_prep( def _lev_prep(
@ -46,3 +46,6 @@ class Okx(Exchange):
"mgnMode": self.margin_mode.value, "mgnMode": self.margin_mode.value,
"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)

View File

@ -210,28 +210,34 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, stake_amount, max_
def test_fill_leverage_brackets_binance(default_conf, mocker): def test_fill_leverage_brackets_binance(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_leverage_brackets = MagicMock(return_value={ api_mock.load_leverage_brackets = MagicMock(return_value={
'ADA/BUSD': [[0.0, 0.025], 'ADA/BUSD': [
[100000.0, 0.05], [0.0, 0.025],
[500000.0, 0.1], [100000.0, 0.05],
[1000000.0, 0.15], [500000.0, 0.1],
[2000000.0, 0.25], [1000000.0, 0.15],
[5000000.0, 0.5]], [2000000.0, 0.25],
'BTC/USDT': [[0.0, 0.004], [5000000.0, 0.5],
[50000.0, 0.005], ],
[250000.0, 0.01], 'BTC/USDT': [
[1000000.0, 0.025], [0.0, 0.004],
[5000000.0, 0.05], [50000.0, 0.005],
[20000000.0, 0.1], [250000.0, 0.01],
[50000000.0, 0.125], [1000000.0, 0.025],
[100000000.0, 0.15], [5000000.0, 0.05],
[200000000.0, 0.25], [20000000.0, 0.1],
[300000000.0, 0.5]], [50000000.0, 0.125],
"ZEC/USDT": [[0.0, 0.01], [100000000.0, 0.15],
[5000.0, 0.025], [200000000.0, 0.25],
[25000.0, 0.05], [300000000.0, 0.5],
[100000.0, 0.1], ],
[250000.0, 0.125], "ZEC/USDT": [
[1000000.0, 0.5]], [0.0, 0.01],
[5000.0, 0.025],
[25000.0, 0.05],
[100000.0, 0.1],
[250000.0, 0.125],
[1000000.0, 0.5],
],
}) })
default_conf['dry_run'] = False default_conf['dry_run'] = False
@ -241,28 +247,34 @@ def test_fill_leverage_brackets_binance(default_conf, mocker):
exchange.fill_leverage_brackets() exchange.fill_leverage_brackets()
assert exchange._leverage_brackets == { assert exchange._leverage_brackets == {
'ADA/BUSD': [[0.0, 0.025, 0.0], 'ADA/BUSD': [
[100000.0, 0.05, 2500.0], (0.0, 0.025, 0.0),
[500000.0, 0.1, 27500.0], (100000.0, 0.05, 2500.0),
[1000000.0, 0.15, 77499.99999999999], (500000.0, 0.1, 27500.0),
[2000000.0, 0.25, 277500.0], (1000000.0, 0.15, 77499.99999999999),
[5000000.0, 0.5, 1527500.0]], (2000000.0, 0.25, 277500.0),
'BTC/USDT': [[0.0, 0.004, 0.0], (5000000.0, 0.5, 1527500.0),
[50000.0, 0.005, 50.0], ],
[250000.0, 0.01, 1300.0], 'BTC/USDT': [
[1000000.0, 0.025, 16300.000000000002], (0.0, 0.004, 0.0),
[5000000.0, 0.05, 141300.0], (50000.0, 0.005, 50.0),
[20000000.0, 0.1, 1141300.0], (250000.0, 0.01, 1300.0),
[50000000.0, 0.125, 2391300.0], (1000000.0, 0.025, 16300.000000000002),
[100000000.0, 0.15, 4891300.0], (5000000.0, 0.05, 141300.0),
[200000000.0, 0.25, 24891300.0], (20000000.0, 0.1, 1141300.0),
[300000000.0, 0.5, 99891300.0]], (50000000.0, 0.125, 2391300.0),
"ZEC/USDT": [[0.0, 0.01, 0.0], (100000000.0, 0.15, 4891300.0),
[5000.0, 0.025, 75.0], (200000000.0, 0.25, 24891300.0),
[25000.0, 0.05, 700.0], (300000000.0, 0.5, 99891300.0),
[100000.0, 0.1, 5700.0], ],
[250000.0, 0.125, 11949.999999999998], "ZEC/USDT": [
[1000000.0, 0.5, 386950.0]] (0.0, 0.01, 0.0),
(5000.0, 0.025, 75.0),
(25000.0, 0.05, 700.0),
(100000.0, 0.1, 5700.0),
(250000.0, 0.125, 11949.999999999998),
(1000000.0, 0.5, 386950.0),
]
} }
api_mock = MagicMock() api_mock = MagicMock()
@ -288,37 +300,37 @@ def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker):
leverage_brackets = { leverage_brackets = {
"1000SHIB/USDT": [ "1000SHIB/USDT": [
[0.0, 0.01, 0.0], (0.0, 0.01, 0.0),
[5000.0, 0.025, 75.0], (5000.0, 0.025, 75.0),
[25000.0, 0.05, 700.0], (25000.0, 0.05, 700.0),
[100000.0, 0.1, 5700.0], (100000.0, 0.1, 5700.0),
[250000.0, 0.125, 11949.999999999998], (250000.0, 0.125, 11949.999999999998),
[1000000.0, 0.5, 386950.0], (1000000.0, 0.5, 386950.0),
], ],
"1INCH/USDT": [ "1INCH/USDT": [
[0.0, 0.012, 0.0], (0.0, 0.012, 0.0),
[5000.0, 0.025, 65.0], (5000.0, 0.025, 65.0),
[25000.0, 0.05, 690.0], (25000.0, 0.05, 690.0),
[100000.0, 0.1, 5690.0], (100000.0, 0.1, 5690.0),
[250000.0, 0.125, 11939.999999999998], (250000.0, 0.125, 11939.999999999998),
[1000000.0, 0.5, 386940.0], (1000000.0, 0.5, 386940.0),
], ],
"AAVE/USDT": [ "AAVE/USDT": [
[0.0, 0.01, 0.0], (0.0, 0.01, 0.0),
[50000.0, 0.02, 500.0], (50000.0, 0.02, 500.0),
[250000.0, 0.05, 8000.000000000001], (250000.0, 0.05, 8000.000000000001),
[1000000.0, 0.1, 58000.0], (1000000.0, 0.1, 58000.0),
[2000000.0, 0.125, 107999.99999999999], (2000000.0, 0.125, 107999.99999999999),
[5000000.0, 0.1665, 315500.00000000006], (5000000.0, 0.1665, 315500.00000000006),
[10000000.0, 0.25, 1150500.0], (10000000.0, 0.25, 1150500.0),
], ],
"ADA/BUSD": [ "ADA/BUSD": [
[0.0, 0.025, 0.0], (0.0, 0.025, 0.0),
[100000.0, 0.05, 2500.0], (100000.0, 0.05, 2500.0),
[500000.0, 0.1, 27500.0], (500000.0, 0.1, 27500.0),
[1000000.0, 0.15, 77499.99999999999], (1000000.0, 0.15, 77499.99999999999),
[2000000.0, 0.25, 277500.0], (2000000.0, 0.25, 277500.0),
[5000000.0, 0.5, 1527500.0], (5000000.0, 0.5, 1527500.0),
] ]
} }

View File

@ -37,6 +37,7 @@ def test_validate_order_types_gateio(default_conf, mocker):
]) ])
def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio): def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio):
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio") exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio")
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.markets', 'freqtrade.exchange.Exchange.markets',

View File

@ -722,8 +722,8 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
(False, 'futures', 'binance', 'isolated', 0.05, 8.167171717171717), (False, 'futures', 'binance', 'isolated', 0.05, 8.167171717171717),
(True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304), (True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304),
(False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796), (False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796),
# (True, 'futures', 'okex', 'isolated', 11.87413417771621), (True, 'futures', 'okex', 'isolated', 11.87413417771621),
# (False, 'futures', 'okex', 'isolated', 8.085708510208207), (False, 'futures', 'okex', 'isolated', 8.085708510208207),
]) ])
def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
limit_order_open, is_short, trading_mode, limit_order_open, is_short, trading_mode,
@ -778,7 +778,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
get_min_pair_stake_amount=MagicMock(return_value=1), get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee, get_fee=fee,
get_funding_fees=MagicMock(return_value=0), get_funding_fees=MagicMock(return_value=0),
name=exchange_name name=exchange_name,
get_maintenance_ratio_and_amt=MagicMock(return_value=(0.01, 0.01)),
) )
pair = 'ETH/USDT' pair = 'ETH/USDT'
@ -922,7 +923,6 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
assert trade.open_rate_requested == 10 assert trade.open_rate_requested == 10
# In case of custom entry price not float type # In case of custom entry price not float type
freqtrade.exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
freqtrade.exchange.name = exchange_name freqtrade.exchange.name = exchange_name
order['status'] = 'open' order['status'] = 'open'
order['id'] = '5568' order['id'] = '5568'