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
# 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,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
mm_ratio: float,
position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance
taker_fee_rate: Optional[float] = None, # (Gateio & Okex)
maintenance_amt: Optional[float] = None, # (Binance)
mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only
upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
@ -296,14 +294,10 @@ class Binance(Exchange):
:param exchange_name:
:param open_rate: (EP1) Entry price of position
: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 maintenance_amt: (CUM) Maintenance Amount of position
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
:param taker_fee_rate: # * Not required by Binance
:param maintenance_amt:
# * Only required for Cross
@ -314,30 +308,20 @@ class Binance(Exchange):
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
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
position = abs(position)
cross_vars = (
upnl_ex_1 - mm_ex_1 # type: ignore
if self.collateral == Collateral.CROSS else
0.0
)
cross_vars = upnl_ex_1 - mm_ex_1 if self.collateral == Collateral.CROSS else 0.0
# mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
# 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:
return (
@ -348,6 +332,6 @@ class Binance(Exchange):
(position * mm_ratio) - (side_1 * position)
)
)
raise OperationalException(
f"Binance does not support {self.collateral.value} {self.trading_mode.value} trading")
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")

View File

@ -1984,18 +1984,55 @@ class Exchange:
return 0.0
@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
: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"):
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:
positions = self._api.fetch_positions([pair])
position = positions[0]
return position['liquidationPrice']
pos = positions[0]
return pos['liquidationPrice']
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
@ -2014,17 +2051,15 @@ class Exchange:
"""
raise OperationalException(self.name + ' does not support leverage futures trading')
def liquidation_price(
def dry_run_liquidation_price(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
mm_ratio: float,
position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance
taker_fee_rate: Optional[float] = None, # (Gateio & Okex)
maintenance_amt: Optional[float] = None, # (Binance)
mm_ex_1: Optional[float] = 0.0, # (Binance) Cross only
upnl_ex_1: Optional[float] = 0.0, # (Binance) Cross only
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
PERPETUAL:
@ -2036,33 +2071,26 @@ class Exchange:
:param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param position: Absolute value of position size (in base currency)
:param mm_ratio:
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param collateral: Either ISOLATED or CROSS
:param wallet_balance: Amount of collateral in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
:param taker_fee_rate:
# * Not required by Gateio or OKX
:param maintenance_amt:
:param mm_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):
raise OperationalException(
f"Parameter taker_fee_rate is required by {self.name}.liquidation_price"
)
market = self.markets[pair]
taker_fee_rate = market['taker']
mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, position)
if self.trading_mode == TradingMode.FUTURES and self.collateral == Collateral.ISOLATED:
# if is_inverse:
# raise OperationalException(
# "Freqtrade does not support inverse contracts at the moment")
if market['inverse']:
raise OperationalException(
"Freqtrade does not yet support inverse contracts")
value = wallet_balance / position
@ -2073,7 +2101,7 @@ class Exchange:
return (open_rate - value) / (1 - mm_ratio_taker)
else:
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:

View File

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

View File

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

View File

@ -817,29 +817,42 @@ def get_markets():
'symbol': 'ETH/USDT',
'base': 'ETH',
'quote': 'USDT',
'spot': True,
'future': True,
'swap': True,
'margin': True,
'settle': 'USDT',
'baseId': 'ETH',
'quoteId': 'USDT',
'settleId': 'USDT',
'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,
'maker': 0.0002,
'contractSize': 1,
'expiry': 1680220800000,
'expiryDateTime': '2023-03-31T00:00:00.000Z',
'strike': None,
'optionType': None,
'precision': {
'amount': 8,
'price': 8
'price': 8,
},
'limits': {
'leverage': {
'min': 1,
'max': 100,
},
'amount': {
'min': 0.02214286,
'max': None
'max': None,
},
'price': {
'min': 1e-08,
'max': None
},
'leverage': {
'min': None,
'max': None,
},
'cost': {
@ -847,8 +860,9 @@ def get_markets():
'max': None,
},
},
'active': True,
'info': {},
'info': {
'maintenance_rate': '0.005',
},
},
'LTC/USDT': {
'id': 'USDT-LTC',
@ -1110,7 +1124,6 @@ def get_markets():
'swap': True,
'futures': False,
'option': False,
'derivative': True,
'contract': True,
'linear': True,
'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', [
# Bittrex
('bittrex', 2.0, False, 3.0, spot, None),
('bittrex', 2.0, False, 1.0, spot, cross),
('bittrex', 2.0, True, 3.0, spot, isolated),
('bittrex', 2.0, False, 3.0, 'spot', None),
('bittrex', 2.0, False, 1.0, 'spot', 'cross'),
('bittrex', 2.0, True, 3.0, 'spot', 'isolated'),
# Binance
('binance', 2.0, False, 3.0, spot, None),
('binance', 2.0, False, 1.0, spot, cross),
('binance', 2.0, True, 3.0, spot, isolated),
('binance', 2.0, False, 3.0, 'spot', None),
('binance', 2.0, False, 1.0, 'spot', 'cross'),
('binance', 2.0, True, 3.0, 'spot', 'isolated'),
])
def test_liquidation_price_is_none(
mocker,
@ -3996,14 +3996,12 @@ def test_liquidation_price_is_none(
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral
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,
is_short=is_short,
mm_ratio=1535443.01,
position=71200.81144,
wallet_balance=-56354.57,
taker_fee_rate=0.01,
maintenance_amt=3683.979,
mm_ex_1=0.10,
upnl_ex_1=0.0
) is None
@ -4014,13 +4012,13 @@ def test_liquidation_price_is_none(
'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, '
'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),
("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),
("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),
("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)
])
def test_liquidation_price(
@ -4030,13 +4028,13 @@ def test_liquidation_price(
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral
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,
is_short=is_short,
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1,
maintenance_amt=maintenance_amt,
position=position,
mm_ratio=mm_ratio
), 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))
((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))
(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))
(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)]
default_conf_usdt['trading_mode'] = trading_mode
leverage = 1.0 if trading_mode == 'spot' else 5.0
default_conf_usdt['exchange']['name'] = exchange_name
if margin_mode:
default_conf_usdt['collateral'] = margin_mode
mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes')
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_exchange(mocker, id=exchange_name)
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
freqtrade.strategy.leverage = MagicMock(return_value=leverage)