exchange.liquidation_price methods combined, dry_run check on exchange for liquidation price

This commit is contained in:
Sam Germain 2022-01-29 18:47:17 -06:00
parent 143c37d36f
commit b8f4cebce7
8 changed files with 140 additions and 124 deletions

View File

@ -277,17 +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( def dry_run_liquidation_price(
self, self,
pair: str,
open_rate: float, # Entry price of position open_rate: float, # Entry price of position
is_short: bool, is_short: bool,
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
taker_fee_rate: Optional[float] = None, # (Gateio & Okex) mm_ex_1: float = 0.0, # (Binance) Cross only
maintenance_amt: Optional[float] = None, # (Binance) upnl_ex_1: 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
) -> Optional[float]: ) -> Optional[float]:
""" """
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
@ -296,14 +294,10 @@ 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 mm_ratio: (MMR)
# 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 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
:param maintenance_amt: :param maintenance_amt:
# * Only required for Cross # * Only required for Cross
@ -314,30 +308,20 @@ 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 self.trading_mode == TradingMode.SPOT:
return None
elif (self.collateral is None):
raise OperationalException('Binance.collateral must be set for liquidation_price')
if (maintenance_amt is None):
raise OperationalException(
f"Parameter maintenance_amt is required by Binance.liquidation_price"
f"for {self.collateral.value} {self.trading_mode.value}"
)
if (self.collateral == Collateral.CROSS and (mm_ex_1 is None or upnl_ex_1 is None)):
raise OperationalException(
f"Parameters mm_ex_1 and upnl_ex_1 are required by Binance.liquidation_price"
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 = ( cross_vars = upnl_ex_1 - mm_ex_1 if self.collateral == Collateral.CROSS else 0.0
upnl_ex_1 - mm_ex_1 # type: ignore
if self.collateral == Collateral.CROSS else # mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
0.0 # maintenance_amt: (CUM) Maintenance Amount of position
) mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, position)
if (maintenance_amt is None):
raise OperationalException(
"Parameter maintenance_amt is required by Binance.liquidation_price"
f"for {self.trading_mode.value}"
)
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
return ( return (
@ -348,6 +332,6 @@ class Binance(Exchange):
(position * mm_ratio) - (side_1 * position) (position * mm_ratio) - (side_1 * position)
) )
) )
else:
raise OperationalException( raise OperationalException(
f"Binance does not support {self.collateral.value} {self.trading_mode.value} trading") "Freqtrade only supports isolated futures for leverage trading")

View File

@ -1984,18 +1984,55 @@ class Exchange:
return 0.0 return 0.0
@retrier @retrier
def get_liquidation_price(self, pair: str): def get_liquidation_price(
self,
pair: str,
# Dry-run
open_rate: Optional[float] = None, # Entry price of position
is_short: Optional[bool] = None,
position: Optional[float] = None, # Absolute value of position size
wallet_balance: Optional[float] = None, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
):
""" """
Set's the margin mode on the exchange to cross or isolated for a specific pair Set's the margin mode on the exchange to cross or isolated for a specific pair
:param pair: base/quote currency pair (e.g. "ADA/USDT") :param pair: base/quote currency pair (e.g. "ADA/USDT")
""" """
if self.trading_mode == TradingMode.SPOT:
return None
elif (self.collateral is None):
raise OperationalException(f'{self.name}.collateral must be set for liquidation_price')
elif (self.trading_mode != TradingMode.FUTURES and self.collateral != Collateral.ISOLATED):
raise OperationalException(
f"{self.name} does not support {self.collateral.value} {self.trading_mode.value}")
if self._config['dry_run'] or not self.exchange_has("fetchPositions"): if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
return if (
open_rate is None or
is_short is None or
position is None or
wallet_balance is None
):
raise OperationalException(
f"Parameters open_rate, is_short, position, wallet_balance are"
f"required by {self.name}.liquidation_price for dry_run"
)
return self.dry_run_liquidation_price(
pair=pair,
open_rate=open_rate,
is_short=is_short,
position=position,
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1
)
try: try:
positions = self._api.fetch_positions([pair]) positions = self._api.fetch_positions([pair])
position = positions[0] pos = positions[0]
return position['liquidationPrice'] return pos['liquidationPrice']
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
@ -2014,17 +2051,15 @@ class Exchange:
""" """
raise OperationalException(self.name + ' does not support leverage futures trading') raise OperationalException(self.name + ' does not support leverage futures trading')
def liquidation_price( def dry_run_liquidation_price(
self, self,
pair: str,
open_rate: float, # Entry price of position open_rate: float, # Entry price of position
is_short: bool, is_short: bool,
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
taker_fee_rate: Optional[float] = None, # (Gateio & Okex) mm_ex_1: float = 0.0, # (Binance) Cross only
maintenance_amt: Optional[float] = None, # (Binance) upnl_ex_1: 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
) -> Optional[float]: ) -> Optional[float]:
""" """
PERPETUAL: PERPETUAL:
@ -2036,33 +2071,26 @@ class Exchange:
: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 position: Absolute value of position size (in base currency) :param position: Absolute value of position size (in base currency)
:param mm_ratio:
:param trading_mode: SPOT, MARGIN, FUTURES, etc. :param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param collateral: Either ISOLATED or CROSS :param collateral: Either ISOLATED or CROSS
:param wallet_balance: Amount of collateral in the wallet being used to trade :param wallet_balance: Amount of collateral in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance Isolated-Margin Mode: isolatedWalletBalance
:param taker_fee_rate:
# * Not required by Gateio or OKX # * Not required by Gateio or OKX
:param maintenance_amt:
:param mm_ex_1: :param mm_ex_1:
:param upnl_ex_1: :param upnl_ex_1:
""" """
if self.trading_mode == TradingMode.SPOT:
return None
elif (self.collateral is None):
raise OperationalException('Binance.collateral must be set for liquidation_price')
if (not taker_fee_rate): market = self.markets[pair]
raise OperationalException( taker_fee_rate = market['taker']
f"Parameter taker_fee_rate is required by {self.name}.liquidation_price" mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, position)
)
if self.trading_mode == TradingMode.FUTURES and self.collateral == Collateral.ISOLATED: if self.trading_mode == TradingMode.FUTURES and self.collateral == Collateral.ISOLATED:
# if is_inverse:
# raise OperationalException( if market['inverse']:
# "Freqtrade does not support inverse contracts at the moment") raise OperationalException(
"Freqtrade does not yet support inverse contracts")
value = wallet_balance / position value = wallet_balance / position
@ -2073,7 +2101,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 {self.collateral.value} {self.trading_mode.value}") "Freqtrade only supports isolated futures for leverage trading")
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,5 +1,6 @@
import logging import logging
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
from freqtrade.enums import Collateral, TradingMode from freqtrade.enums import Collateral, TradingMode
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange

View File

@ -19,7 +19,7 @@ from freqtrade.edge import Edge
from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, SignalDirection, State, from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, SignalDirection, State,
TradingMode) TradingMode)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError) InvalidOrderException, OperationalException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
@ -606,38 +606,31 @@ class FreqtradeBot(LoggingMixin):
is_short: bool is_short: bool
) -> Tuple[float, Optional[float]]: ) -> Tuple[float, Optional[float]]:
interest_rate = 0.0
isolated_liq = None
# if TradingMode == TradingMode.MARGIN: # if TradingMode == TradingMode.MARGIN:
# interest_rate = self.exchange.get_interest_rate( # interest_rate = self.exchange.get_interest_rate(
# pair=pair, # pair=pair,
# open_rate=open_rate, # open_rate=open_rate,
# is_short=is_short # is_short=is_short
# ) # )
if self.trading_mode == TradingMode.SPOT:
if self.collateral_type == Collateral.ISOLATED: return (0.0, None)
if self.config['dry_run']: elif (
mm_ratio, maintenance_amt = self.exchange.get_maintenance_ratio_and_amt( self.collateral_type == Collateral.ISOLATED and
pair, self.trading_mode == TradingMode.FUTURES
amount ):
) isolated_liq = self.exchange.get_liquidation_price(
taker_fee_rate = self.exchange.markets[pair]['taker'] pair=pair,
isolated_liq = self.exchange.liquidation_price( open_rate=open_rate,
open_rate=open_rate, is_short=is_short,
is_short=is_short, position=amount,
mm_ratio=mm_ratio, wallet_balance=(amount * open_rate)/leverage, # TODO: Update for cross
position=amount, mm_ex_1=0.0,
wallet_balance=(amount * open_rate)/leverage, # TODO: Update for cross upnl_ex_1=0.0,
taker_fee_rate=taker_fee_rate, )
maintenance_amt=maintenance_amt, return (0.0, isolated_liq)
mm_ex_1=0.0, else:
upnl_ex_1=0.0, raise OperationalException(
) "Freqtrade only supports isolated futures for leverage trading")
else:
isolated_liq = self.exchange.get_liquidation_price(pair)
return interest_rate, isolated_liq
def execute_entry( def execute_entry(
self, self,

View File

@ -362,10 +362,7 @@ class LocalTrade():
self.stop_loss_pct = -1 * abs(percent) self.stop_loss_pct = -1 * abs(percent)
self.stoploss_last_update = datetime.utcnow() self.stoploss_last_update = datetime.utcnow()
def set_isolated_liq( def set_isolated_liq(self, isolated_liq: float):
self,
isolated_liq: float,
):
""" """
Method you should use to set self.liquidation price. Method you should use to set self.liquidation price.
Assures stop_loss is not passed the liquidation price Assures stop_loss is not passed the liquidation price

View File

@ -817,29 +817,42 @@ def get_markets():
'symbol': 'ETH/USDT', 'symbol': 'ETH/USDT',
'base': 'ETH', 'base': 'ETH',
'quote': 'USDT', 'quote': 'USDT',
'spot': True, 'settle': 'USDT',
'future': True, 'baseId': 'ETH',
'swap': True, 'quoteId': 'USDT',
'margin': True, 'settleId': 'USDT',
'type': 'spot', 'type': 'spot',
'contractSize': None, 'spot': True,
'margin': True,
'swap': True,
'future': True,
'option': False,
'active': True,
'contract': True,
'linear': True,
'inverse': False,
'taker': 0.0006, 'taker': 0.0006,
'maker': 0.0002, 'maker': 0.0002,
'contractSize': 1,
'expiry': 1680220800000,
'expiryDateTime': '2023-03-31T00:00:00.000Z',
'strike': None,
'optionType': None,
'precision': { 'precision': {
'amount': 8, 'amount': 8,
'price': 8 'price': 8,
}, },
'limits': { 'limits': {
'leverage': {
'min': 1,
'max': 100,
},
'amount': { 'amount': {
'min': 0.02214286, 'min': 0.02214286,
'max': None 'max': None,
}, },
'price': { 'price': {
'min': 1e-08, 'min': 1e-08,
'max': None
},
'leverage': {
'min': None,
'max': None, 'max': None,
}, },
'cost': { 'cost': {
@ -847,8 +860,9 @@ def get_markets():
'max': None, 'max': None,
}, },
}, },
'active': True, 'info': {
'info': {}, 'maintenance_rate': '0.005',
},
}, },
'LTC/USDT': { 'LTC/USDT': {
'id': 'USDT-LTC', 'id': 'USDT-LTC',
@ -1110,7 +1124,6 @@ def get_markets():
'swap': True, 'swap': True,
'futures': False, 'futures': False,
'option': False, 'option': False,
'derivative': True,
'contract': True, 'contract': True,
'linear': True, 'linear': True,
'inverse': False, 'inverse': False,

View File

@ -3975,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,
@ -3996,14 +3996,12 @@ def test_liquidation_price_is_none(
default_conf['trading_mode'] = trading_mode default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral 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.get_liquidation_price(
pair='DOGE/USDT',
open_rate=open_rate, open_rate=open_rate,
is_short=is_short, is_short=is_short,
mm_ratio=1535443.01,
position=71200.81144, position=71200.81144,
wallet_balance=-56354.57, wallet_balance=-56354.57,
taker_fee_rate=0.01,
maintenance_amt=3683.979,
mm_ex_1=0.10, mm_ex_1=0.10,
upnl_ex_1=0.0 upnl_ex_1=0.0
) is None ) is None
@ -4014,13 +4012,13 @@ def test_liquidation_price_is_none(
'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, ' 'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, '
'mm_ratio, expected', 'mm_ratio, expected',
[ [
("binance", False, 1, futures, isolated, 1535443.01, 0.0, ("binance", False, 1, 'futures', '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),
("binance", False, 1, futures, isolated, 1535443.01, 0.0, ("binance", False, 1, 'futures', 'isolated', 1535443.01, 0.0,
0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73), 0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73),
("binance", False, 1, futures, cross, 1535443.01, 71200.81144, ("binance", False, 1, 'futures', 'cross', 1535443.01, 71200.81144,
-56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26), -56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26),
("binance", False, 1, futures, cross, 1535443.01, 356512.508, ("binance", False, 1, 'futures', 'cross', 1535443.01, 356512.508,
-448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89) -448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89)
]) ])
def test_liquidation_price( def test_liquidation_price(
@ -4030,13 +4028,13 @@ def test_liquidation_price(
default_conf['trading_mode'] = trading_mode default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral 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( exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt))
assert isclose(round(exchange.get_liquidation_price(
pair='DOGE/USDT',
open_rate=open_rate, open_rate=open_rate,
is_short=is_short, is_short=is_short,
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,
maintenance_amt=maintenance_amt,
position=position, position=position,
mm_ratio=mm_ratio
), 2), expected) ), 2), expected)

View File

@ -735,11 +735,11 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position)) ((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position))
((2 + 0.01) - (1 * 1 * 10)) / ((1 * 0.01) - (1 * 1)) = 8.070707070707071 ((2 + 0.01) - (1 * 1 * 10)) / ((1 * 0.01) - (1 * 1)) = 8.070707070707071
exchange_name = gateio, is_short = true exchange_name = gateio/okex, is_short = true
(open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate)) (open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate))
(10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621 (10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621
exchange_name = gateio, is_short = false exchange_name = gateio/okex, is_short = false
(open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate)) (open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate))
(10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207 (10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207
""" """
@ -747,10 +747,12 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
order = limit_order[enter_side(is_short)] order = limit_order[enter_side(is_short)]
default_conf_usdt['trading_mode'] = trading_mode default_conf_usdt['trading_mode'] = trading_mode
leverage = 1.0 if trading_mode == 'spot' else 5.0 leverage = 1.0 if trading_mode == 'spot' else 5.0
default_conf_usdt['exchange']['name'] = exchange_name
if margin_mode: if margin_mode:
default_conf_usdt['collateral'] = margin_mode default_conf_usdt['collateral'] = margin_mode
mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes')
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker, id=exchange_name)
freqtrade = FreqtradeBot(default_conf_usdt) freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
freqtrade.strategy.leverage = MagicMock(return_value=leverage) freqtrade.strategy.leverage = MagicMock(return_value=leverage)