cleaned up liquidation price methods

This commit is contained in:
Sam Germain 2022-01-29 02:06:56 -06:00
parent ede9012fcc
commit 143c37d36f
5 changed files with 79 additions and 191 deletions

View File

@ -277,18 +277,15 @@ class Binance(Exchange):
# The lowest notional_floor for any pair in loadLeverageBrackets is always 0 because it # 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 # describes the min amount for a bracket, and the lowest bracket will always go down to 0
def liquidation_price_helper( def liquidation_price(
self, self,
open_rate: float, # Entry price of position open_rate: float, # Entry price of position
is_short: bool, is_short: bool,
leverage: float,
mm_ratio: float, mm_ratio: float,
position: float, # Absolute value of position size position: float, # Absolute value of position size
trading_mode: TradingMode, wallet_balance: float, # Or margin balance
collateral: Collateral,
maintenance_amt: Optional[float] = None, # (Binance)
wallet_balance: Optional[float] = None, # (Binance and Gateio)
taker_fee_rate: Optional[float] = None, # (Gateio & Okex) taker_fee_rate: Optional[float] = None, # (Gateio & Okex)
maintenance_amt: Optional[float] = None, # (Binance)
mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only
upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only
) -> Optional[float]: ) -> Optional[float]:
@ -299,19 +296,15 @@ class Binance(Exchange):
:param exchange_name: :param exchange_name:
:param open_rate: (EP1) Entry price of position :param open_rate: (EP1) Entry price of position
:param is_short: True if the trade is a short, false otherwise :param is_short: True if the trade is a short, false otherwise
:param leverage: The amount of leverage on the trade
:param mm_ratio: (MMR) :param mm_ratio: (MMR)
# Binance's formula specifies maintenance margin rate which is mm_ratio * 100% # Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
:param position: Absolute value of position size (in base currency) :param position: Absolute value of position size (in base currency)
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param collateral: Either ISOLATED or CROSS
:param maintenance_amt: (CUM) Maintenance Amount of position :param maintenance_amt: (CUM) Maintenance Amount of position
:param wallet_balance: (WB) :param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance Isolated-Margin Mode: isolatedWalletBalance
:param taker_fee_rate: # * Not required by Binance
# * Not required by Binance :param maintenance_amt:
:param taker_fee_rate:
# * Only required for Cross # * Only required for Cross
:param mm_ex_1: (TMM) :param mm_ex_1: (TMM)
@ -321,31 +314,32 @@ class Binance(Exchange):
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1. Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
Isolated-Margin Mode: 0 Isolated-Margin Mode: 0
""" """
if trading_mode == TradingMode.SPOT: if self.trading_mode == TradingMode.SPOT:
return None return None
elif (self.collateral is None):
raise OperationalException('Binance.collateral must be set for liquidation_price')
if not collateral: if (maintenance_amt is None):
raise OperationalException( raise OperationalException(
"Parameter collateral is required by liquidation_price when trading_mode is " f"Parameter maintenance_amt is required by Binance.liquidation_price"
f"{trading_mode}" f"for {self.collateral.value} {self.trading_mode.value}"
) )
if (
(wallet_balance is None or maintenance_amt is None or position is None) or if (self.collateral == Collateral.CROSS and (mm_ex_1 is None or upnl_ex_1 is None)):
(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( raise OperationalException(
f"Parameters {required_params} are required by Binance.liquidation_price" f"Parameters mm_ex_1 and upnl_ex_1 are required by Binance.liquidation_price"
f"for {collateral.name} {trading_mode.name}" f"for {self.collateral.value} {self.trading_mode.value}"
) )
side_1 = -1 if is_short else 1 side_1 = -1 if is_short else 1
position = abs(position) position = abs(position)
cross_vars = upnl_ex_1 - mm_ex_1 if collateral == Collateral.CROSS else 0.0 # type: ignore cross_vars = (
upnl_ex_1 - mm_ex_1 # type: ignore
if self.collateral == Collateral.CROSS else
0.0
)
if trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
return ( return (
( (
(wallet_balance + cross_vars + maintenance_amt) - (wallet_balance + cross_vars + maintenance_amt) -
@ -356,4 +350,4 @@ class Binance(Exchange):
) )
raise OperationalException( raise OperationalException(
f"Binance does not support {collateral.value} Mode {trading_mode.value} trading ") f"Binance does not support {self.collateral.value} {self.trading_mode.value} trading")

View File

@ -2018,83 +2018,9 @@ class Exchange:
self, self,
open_rate: float, # Entry price of position open_rate: float, # Entry price of position
is_short: bool, is_short: bool,
leverage: float,
mm_ratio: float,
position: float, # Absolute value of position size
trading_mode: TradingMode,
collateral: Optional[Collateral] = Collateral.ISOLATED,
maintenance_amt: Optional[float] = None, # (Binance)
wallet_balance: Optional[float] = None, # (Binance and Gateio)
taker_fee_rate: Optional[float] = None, # (Gateio & Okex)
mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only
upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
: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 mm_ratio: (MMR)
Okex: [assets in the position - (liability +interest) * mark price] /
(maintenance margin + liquidation fee)
# * Note: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
:param position: Absolute value of position size (in base currency)
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param collateral: Either ISOLATED or CROSS
# * Binance
:param maintenance_amt: (CUM) Maintenance Amount of position
# * Binance and Gateio
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
# * Gateio & Okex
:param taker_fee_rate:
# * Cross only (Binance)
: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}"
)
return self.liquidation_price_helper(
open_rate=open_rate,
is_short=is_short,
leverage=leverage,
mm_ratio=mm_ratio,
position=position,
trading_mode=trading_mode,
collateral=collateral,
maintenance_amt=maintenance_amt,
wallet_balance=wallet_balance,
taker_fee_rate=taker_fee_rate,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1,
)
def liquidation_price_helper(
self,
open_rate: float, # Entry price of position
is_short: bool,
leverage: float,
mm_ratio: float, mm_ratio: float,
position: float, # Absolute value of position size position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance wallet_balance: float, # Or margin balance
trading_mode: TradingMode,
collateral: Collateral,
taker_fee_rate: Optional[float] = None, # (Gateio & Okex) taker_fee_rate: Optional[float] = None, # (Gateio & Okex)
maintenance_amt: Optional[float] = None, # (Binance) maintenance_amt: Optional[float] = None, # (Binance)
mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only
@ -2109,7 +2035,6 @@ class Exchange:
:param exchange_name: :param exchange_name:
:param open_rate: Entry price of position :param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise :param is_short: True if the trade is a short, false otherwise
:param leverage: The amount of leverage on the trade
:param position: Absolute value of position size (in base currency) :param position: Absolute value of position size (in base currency)
:param mm_ratio: :param mm_ratio:
:param trading_mode: SPOT, MARGIN, FUTURES, etc. :param trading_mode: SPOT, MARGIN, FUTURES, etc.
@ -2124,15 +2049,17 @@ class Exchange:
:param mm_ex_1: :param mm_ex_1:
:param upnl_ex_1: :param upnl_ex_1:
""" """
if trading_mode == TradingMode.SPOT: if self.trading_mode == TradingMode.SPOT:
return None return None
elif (self.collateral is None):
raise OperationalException('Binance.collateral must be set for liquidation_price')
if (not taker_fee_rate): if (not taker_fee_rate):
raise OperationalException( raise OperationalException(
f"Parameter taker_fee_rate is required by {self.name}.liquidation_price" f"Parameter taker_fee_rate is required by {self.name}.liquidation_price"
) )
if trading_mode == TradingMode.FUTURES and collateral == Collateral.ISOLATED: if self.trading_mode == TradingMode.FUTURES and self.collateral == Collateral.ISOLATED:
# if is_inverse: # if is_inverse:
# raise OperationalException( # raise OperationalException(
# "Freqtrade does not support inverse contracts at the moment") # "Freqtrade does not support inverse contracts at the moment")
@ -2146,7 +2073,7 @@ class Exchange:
return (open_rate - value) / (1 - mm_ratio_taker) return (open_rate - value) / (1 - mm_ratio_taker)
else: else:
raise OperationalException( raise OperationalException(
f"{self.name} does not support {collateral.value} {trading_mode.value}") f"{self.name} does not support {self.collateral.value} {self.trading_mode.value}")
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:

View File

@ -1,8 +1,6 @@
import logging import logging
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Tuple
from freqtrade.enums import Collateral, TradingMode from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange

View File

@ -626,16 +626,13 @@ class FreqtradeBot(LoggingMixin):
isolated_liq = self.exchange.liquidation_price( isolated_liq = self.exchange.liquidation_price(
open_rate=open_rate, open_rate=open_rate,
is_short=is_short, is_short=is_short,
leverage=leverage, mm_ratio=mm_ratio,
trading_mode=self.trading_mode,
collateral=Collateral.ISOLATED,
mm_ex_1=0.0,
upnl_ex_1=0.0,
position=amount, position=amount,
wallet_balance=(amount * open_rate)/leverage, # TODO: Update for cross wallet_balance=(amount * open_rate)/leverage, # TODO: Update for cross
taker_fee_rate=taker_fee_rate,
maintenance_amt=maintenance_amt, maintenance_amt=maintenance_amt,
mm_ratio=mm_ratio, mm_ex_1=0.0,
taker_fee_rate=taker_fee_rate upnl_ex_1=0.0,
) )
else: else:
isolated_liq = self.exchange.get_liquidation_price(pair) isolated_liq = self.exchange.get_liquidation_price(pair)

View File

@ -3626,6 +3626,8 @@ def test_get_liquidation_price(mocker, default_conf):
exchange_has=MagicMock(return_value=True), exchange_has=MagicMock(return_value=True),
) )
default_conf['dry_run'] = False default_conf['dry_run'] = False
default_conf['trading_mode'] = 'futures'
default_conf['collateral'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
liq_price = exchange.get_liquidation_price('NEAR/USDT:USDT') liq_price = exchange.get_liquidation_price('NEAR/USDT:USDT')
@ -3973,13 +3975,13 @@ def test__amount_to_contracts(
@pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [ @pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [
# Bittrex # Bittrex
('bittrex', "2.0", False, "3.0", spot, None), ('bittrex', 2.0, False, 3.0, spot, None),
('bittrex', "2.0", False, "1.0", spot, cross), ('bittrex', 2.0, False, 1.0, spot, cross),
('bittrex', "2.0", True, "3.0", spot, isolated), ('bittrex', 2.0, True, 3.0, spot, isolated),
# Binance # Binance
('binance', "2.0", False, "3.0", spot, None), ('binance', 2.0, False, 3.0, spot, None),
('binance', "2.0", False, "1.0", spot, cross), ('binance', 2.0, False, 1.0, spot, cross),
('binance', "2.0", True, "3.0", spot, isolated), ('binance', 2.0, True, 3.0, spot, isolated),
]) ])
def test_liquidation_price_is_none( def test_liquidation_price_is_none(
mocker, mocker,
@ -3991,51 +3993,22 @@ def test_liquidation_price_is_none(
trading_mode, trading_mode,
collateral collateral
): ):
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.liquidation_price( assert exchange.liquidation_price(
open_rate, open_rate=open_rate,
is_short, is_short=is_short,
leverage, mm_ratio=1535443.01,
trading_mode, position=71200.81144,
collateral, wallet_balance=-56354.57,
1535443.01, taker_fee_rate=0.01,
71200.81144, maintenance_amt=3683.979,
-56354.57, mm_ex_1=0.10,
135365.00, upnl_ex_1=0.0
3683.979,
0.10,
) is None ) is None
@pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [
# Bittrex
('bittrex', "2.0", False, "3.0", margin, cross),
('bittrex', "2.0", False, "3.0", margin, isolated),
('bittrex', "2.0", False, "3.0", futures, cross),
('bittrex', "2.0", False, "3.0", futures, isolated),
# Binance
# Binance supports isolated margin, but freqtrade likely won't for a while on Binance
('binance', "2.0", True, "3.0", margin, isolated),
# Kraken
('kraken', "2.0", True, "1.0", margin, isolated),
('kraken', "2.0", True, "1.0", futures, isolated),
# FTX
('ftx', "2.0", True, "3.0", margin, isolated),
('ftx', "2.0", True, "3.0", futures, isolated),
])
def test_liquidation_price_exception_thrown(
exchange_name,
open_rate,
is_short,
leverage,
trading_mode,
collateral,
result
):
# TODO-lev assert exception is thrown
return # Here to avoid indent error, remove when implemented
@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, '
@ -4054,13 +4027,12 @@ def test_liquidation_price(
mocker, default_conf, exchange_name, open_rate, is_short, leverage, trading_mode, mocker, default_conf, exchange_name, open_rate, is_short, leverage, trading_mode,
collateral, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected collateral, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected
): ):
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert isclose(round(exchange.liquidation_price( assert isclose(round(exchange.liquidation_price(
open_rate=open_rate, open_rate=open_rate,
is_short=is_short, is_short=is_short,
leverage=leverage,
trading_mode=trading_mode,
collateral=collateral,
wallet_balance=wallet_balance, wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1, mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1, upnl_ex_1=upnl_ex_1,