implemented binance.get_maintenance_ratio_and_amt

This commit is contained in:
Sam Germain 2022-01-10 07:17:17 -06:00
parent ba5fc21d84
commit 69a6223ca0
7 changed files with 236 additions and 65 deletions

View File

@ -163,11 +163,10 @@ class Binance(Exchange):
if pair not in self._leverage_brackets: if pair not in self._leverage_brackets:
return 1.0 return 1.0
pair_brackets = self._leverage_brackets[pair] pair_brackets = self._leverage_brackets[pair]
max_lev = 1.0 for [notional_floor, mm_ratio, _] in reversed(pair_brackets):
for [notional_floor, maint_margin_ratio] in pair_brackets:
if nominal_value >= notional_floor: if nominal_value >= notional_floor:
max_lev = 1/maint_margin_ratio return 1/mm_ratio
return max_lev return 1.0
@retrier @retrier
def _set_leverage( def _set_leverage(
@ -227,3 +226,114 @@ class Binance(Exchange):
:return: The cutoff open time for when a funding fee is charged :return: The cutoff open time for when a funding fee is charged
""" """
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: Optional[str],
nominal_value: Optional[float]
):
'''
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
https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
'''
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 liquidation_price_helper(
self,
open_rate: float, # Entry price of position
is_short: bool,
leverage: float,
trading_mode: TradingMode,
mm_ratio: float,
collateral: Collateral,
maintenance_amt: Optional[float] = None, # (Binance)
position: Optional[float] = None, # (Binance and Gateio) Absolute value of position size
wallet_balance: Optional[float] = None, # (Binance and Gateio)
taker_fee_rate: Optional[float] = None, # (Gateio & Okex)
liability: Optional[float] = None, # (Okex)
interest: Optional[float] = None, # (Okex)
position_assets: Optional[float] = None, # * (Okex) Might be same as position
mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only
upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
:param exchange_name:
:param open_rate: (EP1) Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param leverage: The amount of leverage on the trade
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param position: Absolute value of position size (in base currency)
:param mm_ratio: (MMR)
# Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
:param collateral: Either ISOLATED or CROSS
:param maintenance_amt: (CUM) Maintenance Amount of position
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
:param position: Absolute value of position size (in base currency)
# * Not required by Binance
:param taker_fee_rate:
:param liability:
:param interest:
:param position_assets:
# * Only required for Cross
:param mm_ex_1: (TMM)
Cross-Margin Mode: Maintenance Margin of all other contracts, excluding Contract 1
Isolated-Margin Mode: 0
:param upnl_ex_1: (UPNL)
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
Isolated-Margin Mode: 0
"""
if trading_mode == TradingMode.SPOT:
return None
if not collateral:
raise OperationalException(
"Parameter collateral is required by liquidation_price when trading_mode is "
f"{trading_mode}"
)
if (
(wallet_balance is None or maintenance_amt is None or position is None) or
(collateral == Collateral.CROSS and (mm_ex_1 is None or upnl_ex_1 is None))
):
required_params = "wallet_balance, maintenance_amt, position"
if collateral == Collateral.CROSS:
required_params += ", mm_ex_1, upnl_ex_1"
raise OperationalException(
f"Parameters {required_params} are required by Binance.liquidation_price"
f"for {collateral.name} {trading_mode.name}"
)
side_1 = -1 if is_short else 1
position = abs(position)
cross_vars = upnl_ex_1 - mm_ex_1 if collateral == Collateral.CROSS else 0.0 # type: ignore
if trading_mode == TradingMode.FUTURES:
return (
(
(wallet_balance + cross_vars + maintenance_amt) -
(side_1 * position * open_rate)
) / (
(position * mm_ratio) - (side_1 * position)
)
)
raise OperationalException(
f"Binance does not support {collateral.value} Mode {trading_mode.value} trading ")

View File

@ -2003,8 +2003,11 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier def get_maintenance_ratio_and_amt(
def get_mm_amt_rate(self, pair: str, amount: float): self,
pair: Optional[str],
nominal_value: Optional[float]
):
''' '''
:return: The maintenance amount, and maintenance margin rate :return: The maintenance amount, and maintenance margin rate
''' '''

View File

@ -616,7 +616,7 @@ class FreqtradeBot(LoggingMixin):
# open_rate=open_rate, # open_rate=open_rate,
# is_short=is_short # is_short=is_short
# ) # )
maintenance_amt, mm_rate = self.exchange.get_mm_amt_rate(pair, amount) mm_ratio, maintenance_amt = self.exchange.get_maintenance_ratio_and_amt(pair, amount)
if self.collateral_type == Collateral.ISOLATED: if self.collateral_type == Collateral.ISOLATED:
if self.config['dry_run']: if self.config['dry_run']:
@ -630,9 +630,9 @@ class FreqtradeBot(LoggingMixin):
mm_ex_1=0.0, mm_ex_1=0.0,
upnl_ex_1=0.0, upnl_ex_1=0.0,
position=amount * open_rate, position=amount * open_rate,
wallet_balance=amount/leverage, # TODO-lev: Is this correct? wallet_balance=amount/leverage, # TODO: Update for cross
maintenance_amt=maintenance_amt, maintenance_amt=maintenance_amt,
mm_rate=mm_rate, mm_ratio=mm_ratio,
) )
else: else:
isolated_liq = self.exchange.get_liquidation_price(pair) isolated_liq = self.exchange.get_liquidation_price(pair)

View File

@ -35,12 +35,12 @@ def liquidation_price(
''' '''
wallet_balance wallet_balance
In Cross margin mode, WB is crossWalletBalance In Cross margin mode, WB is crossWalletBalance
In Isolated margin mode, WB is isolatedWalletBalance of the isolated position, In Isolated margin mode, WB is isolatedWalletBalance of the isolated position,
TMM=0, UPNL=0, substitute the position quantity, MMR, cum into the formula to calculate. TMM=0, UPNL=0, substitute the position quantity, MMR, cum into the formula to calculate.
Under the cross margin mode, the same ticker/symbol, Under the cross margin mode, the same ticker/symbol,
both long and short position share the same liquidation price except in the isolated mode. both long and short position share the same liquidation price except in the isolated mode.
Under the isolated mode, each isolated position will have different liquidation prices depending Under the isolated mode, each isolated position will have different liquidation prices
on the margin allocated to the positions. depending on the margin allocated to the positions.
position position
Absolute value of position size (in base currency) Absolute value of position size (in base currency)

View File

@ -368,7 +368,7 @@ class LocalTrade():
wallet_balance: Optional[float] = None, wallet_balance: Optional[float] = None,
current_price: Optional[float] = None, current_price: Optional[float] = None,
maintenance_amt: Optional[float] = None, maintenance_amt: Optional[float] = None,
mm_rate: Optional[float] = None, mm_ratio: Optional[float] = None,
): ):
""" """
Method you should use to set self.liquidation price. Method you should use to set self.liquidation price.
@ -380,6 +380,16 @@ class LocalTrade():
"wallet balance must be passed to LocalTrade.set_isolated_liq when param" "wallet balance must be passed to LocalTrade.set_isolated_liq when param"
"isolated_liq is None" "isolated_liq is None"
) )
if (
mm_ratio is None or
wallet_balance is None or
current_price is None or
maintenance_amt is None
):
raise OperationalException(
'mm_ratio, wallet_balance, current_price and maintenance_amt '
'required in set_isolated_liq when isolated_liq is None'
)
isolated_liq = liquidation_price( isolated_liq = liquidation_price(
exchange_name=self.exchange, exchange_name=self.exchange,
open_rate=self.open_rate, open_rate=self.open_rate,
@ -390,9 +400,9 @@ class LocalTrade():
mm_ex_1=0.0, mm_ex_1=0.0,
upnl_ex_1=0.0, upnl_ex_1=0.0,
position=self.amount * current_price, position=self.amount * current_price,
wallet_balance=self.amount / self.leverage, # TODO-lev: Is this correct? wallet_balance=self.amount / self.leverage, # TODO: Update for cross
maintenance_amt=maintenance_amt, maintenance_amt=maintenance_amt,
mm_rate=mm_rate, mm_ratio=mm_ratio,
) )
if isolated_liq is None: if isolated_liq is None:

View File

@ -173,30 +173,32 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max_lev): def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max_lev):
exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._leverage_brackets = { exchange._leverage_brackets = {
'BNB/BUSD': [[0.0, 0.025], 'BNB/BUSD': [[0.0, 0.025, 0.0],
[100000.0, 0.05], [100000.0, 0.05, 2500.0],
[500000.0, 0.1], [500000.0, 0.1, 27500.0],
[1000000.0, 0.15], [1000000.0, 0.15, 77499.99999999999],
[2000000.0, 0.25], [2000000.0, 0.25, 277500.0],
[5000000.0, 0.5]], [5000000.0, 0.5, 1527500.0]],
'BNB/USDT': [[0.0, 0.0065], 'BNB/USDT': [[0.0, 0.0065, 0.0],
[10000.0, 0.01], [10000.0, 0.01, 35.00000000000001],
[50000.0, 0.02], [50000.0, 0.02, 535.0],
[250000.0, 0.05], [250000.0, 0.05, 8035.000000000001],
[1000000.0, 0.1], [1000000.0, 0.1, 58035.0],
[2000000.0, 0.125], [2000000.0, 0.125, 108034.99999999999],
[5000000.0, 0.15], [5000000.0, 0.15, 233034.99999999994],
[10000000.0, 0.25]], [10000000.0, 0.25, 1233035.0]],
'BTC/USDT': [[0.0, 0.004], 'BTC/USDT': [[0.0, 0.004, 0.0],
[50000.0, 0.005], [50000.0, 0.005, 50.0],
[250000.0, 0.01], [250000.0, 0.01, 1300.0],
[1000000.0, 0.025], [1000000.0, 0.025, 16300.000000000002],
[5000000.0, 0.05], [5000000.0, 0.05, 141300.0],
[20000000.0, 0.1], [20000000.0, 0.1, 1141300.0],
[50000000.0, 0.125], [50000000.0, 0.125, 2391300.0],
[100000000.0, 0.15], [100000000.0, 0.15, 4891300.0],
[200000000.0, 0.25], [200000000.0, 0.25, 24891300.0],
[300000000.0, 0.5]], [300000000.0, 0.5, 99891300.0]
]
} }
assert exchange.get_max_leverage(pair, nominal_value) == max_lev assert exchange.get_max_leverage(pair, nominal_value) == max_lev
@ -235,28 +237,28 @@ 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], 'ADA/BUSD': [[0.0, 0.025, 0.0],
[100000.0, 0.05], [100000.0, 0.05, 2500.0],
[500000.0, 0.1], [500000.0, 0.1, 27500.0],
[1000000.0, 0.15], [1000000.0, 0.15, 77499.99999999999],
[2000000.0, 0.25], [2000000.0, 0.25, 277500.0],
[5000000.0, 0.5]], [5000000.0, 0.5, 1827500.0]],
'BTC/USDT': [[0.0, 0.004], 'BTC/USDT': [[0.0, 0.004, 0.0],
[50000.0, 0.005], [50000.0, 0.005, 50.0],
[250000.0, 0.01], [250000.0, 0.01, 1300.0],
[1000000.0, 0.025], [1000000.0, 0.025, 16300.000000000002],
[5000000.0, 0.05], [5000000.0, 0.05, 141300.0],
[20000000.0, 0.1], [20000000.0, 0.1, 1141300.0],
[50000000.0, 0.125], [50000000.0, 0.125, 2391300.0],
[100000000.0, 0.15], [100000000.0, 0.15, 4891300.0],
[200000000.0, 0.25], [200000000.0, 0.25, 24891300.0],
[300000000.0, 0.5]], [300000000.0, 0.5, 99891300.0]],
"ZEC/USDT": [[0.0, 0.01], "ZEC/USDT": [[0.0, 0.01, 0.0],
[5000.0, 0.025], [5000.0, 0.025, 75.0],
[25000.0, 0.05], [25000.0, 0.05, 700.0],
[100000.0, 0.1], [100000.0, 0.1, 5700.0],
[250000.0, 0.125], [250000.0, 0.125, 11949.999999999998],
[1000000.0, 0.5]], [1000000.0, 0.5, 386950.0]]
} }
api_mock = MagicMock() api_mock = MagicMock()
@ -389,3 +391,49 @@ def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config):
default_conf['collateral'] = collateral default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange = get_patched_exchange(mocker, default_conf, id="binance")
assert exchange._ccxt_config == config assert exchange._ccxt_config == config
@pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [
("BNB/BUSD", 0.0, 0.025, 0),
("BNB/USDT", 100.0, 0.0065, 0),
("BTC/USDT", 170.30, 0.004, 0),
("BNB/BUSD", 999999.9, 0.1, 0),
("BNB/USDT", 5000000.0, 0.5, 0),
("BTC/USDT", 300000000.1, 0.5, 0),
])
def test_get_maintenance_ratio_and_amt_binance(
default_conf,
mocker,
pair,
nominal_value,
mm_ratio,
amt
):
exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._leverage_brackets = {
'BNB/BUSD': [[0.0, 0.025],
[100000.0, 0.05],
[500000.0, 0.1],
[1000000.0, 0.15],
[2000000.0, 0.25],
[5000000.0, 0.5]],
'BNB/USDT': [[0.0, 0.0065],
[10000.0, 0.01],
[50000.0, 0.02],
[250000.0, 0.05],
[1000000.0, 0.1],
[2000000.0, 0.125],
[5000000.0, 0.15],
[10000000.0, 0.25]],
'BTC/USDT': [[0.0, 0.004],
[50000.0, 0.005],
[250000.0, 0.01],
[1000000.0, 0.025],
[5000000.0, 0.05],
[20000000.0, 0.1],
[50000000.0, 0.125],
[100000000.0, 0.15],
[200000000.0, 0.25],
[300000000.0, 0.5]],
}
assert exchange.get_max_leverage(pair, nominal_value) == max_lev

View File

@ -90,7 +90,7 @@ def test_liquidation_price_exception_thrown(
@pytest.mark.parametrize( @pytest.mark.parametrize(
'exchange_name, is_short, leverage, trading_mode, collateral, wallet_balance, ' 'exchange_name, is_short, leverage, trading_mode, collateral, wallet_balance, '
'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, ' 'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, '
'mm_rate, expected', 'mm_ratio, expected',
[ [
("binance", False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0, ("binance", False, 1, TradingMode.FUTURES, Collateral.ISOLATED, 1535443.01, 0.0,
0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78), 0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78),
@ -103,7 +103,7 @@ def test_liquidation_price_exception_thrown(
]) ])
def test_liquidation_price( def test_liquidation_price(
exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance,
mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_rate, expected mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected
): ):
assert isclose(round(liquidation_price( assert isclose(round(liquidation_price(
exchange_name=exchange_name, exchange_name=exchange_name,
@ -117,5 +117,5 @@ def test_liquidation_price(
upnl_ex_1=upnl_ex_1, upnl_ex_1=upnl_ex_1,
maintenance_amt=maintenance_amt, maintenance_amt=maintenance_amt,
position=position, position=position,
mm_rate=mm_rate mm_ratio=mm_ratio
), 2), expected) ), 2), expected)